| 1 |
<?php |
| 2 |
/** |
| 3 |
* OpenStation — the icon grid, server side. |
| 4 |
* |
| 5 |
* The PHP mirror of `src/desktop-files/grid.ts`. Same pitch, same |
| 6 |
* fallbacks, same reading order — because the server picks cells for |
| 7 |
* tiles the client then has to lay out, and the two disagreeing is |
| 8 |
* visible on the wallpaper. |
| 9 |
* |
| 10 |
* It disagreed for a while. The client's grid was retuned to a |
| 11 |
* 108 × 120 pitch and this side kept the 96 × 110 one it was written |
| 12 |
* against, so every coordinate the server minted landed off-grid; |
| 13 |
* from the seventh row down two server rows aliased onto the same |
| 14 |
* client cell and the tiles had to be displaced on sight. Worse, the |
| 15 |
* server packed a column to unbounded depth — it has no viewport and |
| 16 |
* asked nothing about one — so a desktop with more icons than fit in |
| 17 |
* one column always had a tile stored past the bottom edge of a layer |
| 18 |
* that doesn't scroll. The client's rescue pass then repacked the |
| 19 |
* whole wallpaper to bring it back, and the desktop the user knew |
| 20 |
* came back rearranged. |
| 21 |
* |
| 22 |
* Hence {@see openstation_files_grid_fallback_rows()}: the server |
| 23 |
* can't measure the canvas, so it assumes a small one and wraps |
| 24 |
* early. Wrapping one cell early costs a column the desktop had room |
| 25 |
* for. Wrapping one cell late loses a tile. |
| 26 |
* |
| 27 |
* `Tests_OpenStation_DesktopFilesGrid` parses the TypeScript and |
| 28 |
* fails if either side moves without the other. |
| 29 |
* |
| 30 |
* @package OpenStation |
| 31 |
*/ |
| 32 |
|
| 33 |
defined( 'ABSPATH' ) || exit; |
| 34 |
|
| 35 |
/** |
| 36 |
* Gutter from the top / inline-start edge of an icon canvas. |
| 37 |
* |
| 38 |
* Mirrors `GRID_PADDING` / `--os-grid-padding`. |
| 39 |
*/ |
| 40 |
const OPENSTATION_GRID_PADDING = 16; |
| 41 |
|
| 42 |
/** |
| 43 |
* Cell pitch — the tile box plus the air after it. |
| 44 |
* |
| 45 |
* Mirrors `GRID_CELL_W` / `GRID_CELL_H`, which are themselves derived |
| 46 |
* (`--os-tile-w` + `--os-grid-gap-x`, `--os-tile-h` + `--os-grid-gap-y`). |
| 47 |
* Declared here rather than derived because PHP has no reason to know |
| 48 |
* a cell is made of two things; the test proves the total agrees. |
| 49 |
*/ |
| 50 |
const OPENSTATION_GRID_CELL_W = 108; |
| 51 |
const OPENSTATION_GRID_CELL_H = 120; |
| 52 |
|
| 53 |
/** |
| 54 |
* How far a scan runs along the axis it wraps on. |
| 55 |
* |
| 56 |
* The server never measures a canvas, so it always uses these. |
| 57 |
* Mirrors `GRID_FALLBACK_ROWS` / `GRID_FALLBACK_COLS`. |
| 58 |
*/ |
| 59 |
const OPENSTATION_GRID_FALLBACK_ROWS = 5; |
| 60 |
const OPENSTATION_GRID_FALLBACK_COLS = 4; |
| 61 |
|
| 62 |
/** |
| 63 |
* Upper bound on a scan's unbounded axis. Packing 999 columns of |
| 64 |
* icons is not a layout, it's a runaway loop. |
| 65 |
*/ |
| 66 |
const OPENSTATION_GRID_SCAN_LIMIT = 999; |
| 67 |
|
| 68 |
/** |
| 69 |
* Which way a canvas reads. |
| 70 |
* |
| 71 |
* The desktop root reads in columns — it is tall, and every desktop |
| 72 |
* metaphor worth copying fills a column before starting the next. A |
| 73 |
* folder reads in rows: it is wide and short, and a column-major fill |
| 74 |
* would run a two-item folder off the bottom of its own window. |
| 75 |
* |
| 76 |
* The same rule as `orderForFolder()` in `src/desktop-files/layer.ts`. |
| 77 |
* |
| 78 |
* @param int $parent_id Folder id. `0` is the desktop root. |
| 79 |
* @return string `'column'` or `'row'`. |
| 80 |
*/ |
| 81 |
function openstation_files_grid_order( $parent_id ) { |
| 82 |
return 0 === (int) $parent_id ? 'column' : 'row'; |
| 83 |
} |
| 84 |
|
| 85 |
/** |
| 86 |
* Snap a stored pixel coordinate to the cell it belongs to. |
| 87 |
* |
| 88 |
* Rounds rather than floors, so a coordinate that predates a pitch |
| 89 |
* change — or that a plugin wrote by hand — is read as the cell it is |
| 90 |
* nearest to instead of leaving a phantom hole one cell up and left. |
| 91 |
* The client's `pointToCell()` does the same, which is what keeps an |
| 92 |
* occupancy set built here and one built there describing the same |
| 93 |
* canvas. |
| 94 |
* |
| 95 |
* @param int $x Horizontal pixel offset. |
| 96 |
* @param int $y Vertical pixel offset. |
| 97 |
* @return array{0:int,1:int} `array( $col, $row )`, both >= 0. |
| 98 |
*/ |
| 99 |
function openstation_files_grid_point_to_cell( $x, $y ) { |
| 100 |
$col = (int) round( ( (int) $x - OPENSTATION_GRID_PADDING ) / OPENSTATION_GRID_CELL_W ); |
| 101 |
$row = (int) round( ( (int) $y - OPENSTATION_GRID_PADDING ) / OPENSTATION_GRID_CELL_H ); |
| 102 |
return array( max( 0, $col ), max( 0, $row ) ); |
| 103 |
} |
| 104 |
|
| 105 |
/** |
| 106 |
* The pixel coordinate of a cell. |
| 107 |
* |
| 108 |
* @param int $col Column index. |
| 109 |
* @param int $row Row index. |
| 110 |
* @return array{x:int,y:int} Placement coordinates. |
| 111 |
*/ |
| 112 |
function openstation_files_grid_cell_to_point( $col, $row ) { |
| 113 |
return array( |
| 114 |
'x' => OPENSTATION_GRID_PADDING + max( 0, (int) $col ) * OPENSTATION_GRID_CELL_W, |
| 115 |
'y' => OPENSTATION_GRID_PADDING + max( 0, (int) $row ) * OPENSTATION_GRID_CELL_H, |
| 116 |
); |
| 117 |
} |
| 118 |
|
| 119 |
/** |
| 120 |
* Build an occupancy set from rows carrying `x` / `y`. |
| 121 |
* |
| 122 |
* Keys are `"<col>,<row>"`, the same convention `cellKey()` uses on |
| 123 |
* the client. |
| 124 |
* |
| 125 |
* @param array $rows Rows with `x` and `y` keys. |
| 126 |
* @return array<string,bool> Occupancy set. |
| 127 |
*/ |
| 128 |
function openstation_files_grid_occupied( $rows ) { |
| 129 |
$occupied = array(); |
| 130 |
foreach ( (array) $rows as $row ) { |
| 131 |
if ( ! isset( $row['x'], $row['y'] ) ) { |
| 132 |
continue; |
| 133 |
} |
| 134 |
list( $col, $r ) = openstation_files_grid_point_to_cell( $row['x'], $row['y'] ); |
| 135 |
$occupied[ "$col,$r" ] = true; |
| 136 |
} |
| 137 |
return $occupied; |
| 138 |
} |
| 139 |
|
| 140 |
/** |
| 141 |
* First free cell in `$order`, wrapping at the assumed canvas edge. |
| 142 |
* |
| 143 |
* Marks the cell it returns, so a caller allocating several in a row |
| 144 |
* gets consecutive slots without bookkeeping of its own. |
| 145 |
* |
| 146 |
* @param array<string,bool> $occupied Occupancy set, by reference. |
| 147 |
* @param string $order `'column'` or `'row'`. |
| 148 |
* @return array{0:int,1:int} `array( $col, $row )`. |
| 149 |
*/ |
| 150 |
function openstation_files_grid_next_free( &$occupied, $order = 'column' ) { |
| 151 |
if ( 'row' === $order ) { |
| 152 |
$outer = OPENSTATION_GRID_SCAN_LIMIT; |
| 153 |
$inner = OPENSTATION_GRID_FALLBACK_COLS; |
| 154 |
} else { |
| 155 |
$outer = OPENSTATION_GRID_SCAN_LIMIT; |
| 156 |
$inner = OPENSTATION_GRID_FALLBACK_ROWS; |
| 157 |
} |
| 158 |
for ( $o = 0; $o < $outer; $o++ ) { |
| 159 |
for ( $i = 0; $i < $inner; $i++ ) { |
| 160 |
$col = 'row' === $order ? $i : $o; |
| 161 |
$row = 'row' === $order ? $o : $i; |
| 162 |
if ( ! isset( $occupied[ "$col,$row" ] ) ) { |
| 163 |
$occupied[ "$col,$row" ] = true; |
| 164 |
return array( $col, $row ); |
| 165 |
} |
| 166 |
} |
| 167 |
} |
| 168 |
$occupied['0,0'] = true; |
| 169 |
return array( 0, 0 ); |
| 170 |
} |
| 171 |
|