diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 80f1314..8922f6a 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -45,6 +45,7 @@ import { cycleTileSize, appendItems, tryMoveTile, + resize, } from "./model"; import { TileWrapper } from "./TileWrapper"; @@ -82,8 +83,11 @@ const useGridState = ( }), }; - // Step 2: Backfill gaps left behind by removed tiles - const grid2 = fillGaps(grid1); + // Step 2: Resize the grid if necessary and backfill gaps left behind by + // removed tiles + // Resizing already takes care of backfilling gaps + const grid2 = + columns !== grid1.columns ? resize(grid1, columns!) : fillGaps(grid1); // Step 3: Add new tiles to the end of the grid const existingItemIds = new Set( @@ -205,14 +209,10 @@ export const NewVideoGrid: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [slotGrid, slotGridGeneration, gridBounds]); - const [columns] = useReactiveState( - // Since grid resizing isn't implemented yet, pick a column count on mount - // and stick to it - (prevColumns) => - prevColumns !== undefined && prevColumns !== null - ? prevColumns - : // The grid bounds might not be known yet - gridBounds.width === 0 + const columns = useMemo( + () => + // The grid bounds might not be known yet + gridBounds.width === 0 ? null : Math.max(2, Math.floor(gridBounds.width * 0.0045)), [gridBounds] diff --git a/src/video-grid/model.ts b/src/video-grid/model.ts index 4a6cc32..6ca352e 100644 --- a/src/video-grid/model.ts +++ b/src/video-grid/model.ts @@ -418,6 +418,11 @@ export function appendItems(items: TileDescriptor[], g: Grid): Grid { }; } +const largeTileDimensions = (g: Grid): [number, number] => [ + Math.min(3, Math.max(2, g.columns - 1)), + 2, +]; + /** * Changes the size of a tile, rearranging the grid to make space. * @param tileId The ID of the tile to modify. @@ -433,9 +438,7 @@ export function cycleTileSize(tileId: string, g: Grid): Grid { // The target dimensions, which toggle between 1×1 and larger than 1×1 const [toWidth, toHeight] = - fromWidth === 1 && fromHeight === 1 - ? [Math.min(3, Math.max(2, g.columns - 1)), 2] - : [1, 1]; + fromWidth === 1 && fromHeight === 1 ? largeTileDimensions(g) : [1, 1]; // If we're expanding the tile, we want to create enough new rows at the // tile's target position such that every new unit of grid area created during @@ -547,3 +550,46 @@ export function cycleTileSize(tileId: string, g: Grid): Grid { // Fill any gaps that remain return fillGaps(gappyGrid); } + +/** + * Resizes the grid to a new column width. + */ +export function resize(g: Grid, columns: number): Grid { + const result: Grid = { columns, cells: [] }; + const [largeColumns, largeRows] = largeTileDimensions(result); + + // Copy each tile from the old grid to the resized one in the same order + + // The next index in the result grid to copy a tile to + let next = 0; + + for (const cell of g.cells) { + if (cell?.origin) { + const [nextColumns, nextRows] = + cell.columns > 1 || cell.rows > 1 ? [largeColumns, largeRows] : [1, 1]; + + // If there isn't enough space left on this row, jump to the next row + if (columns - column(next, result) < nextColumns) + next = columns * (Math.floor(next / columns) + 1); + const nextEnd = areaEnd(next, nextColumns, nextRows, result); + + // Expand the cells array as necessary + if (result.cells.length <= nextEnd) + result.cells.push(...new Array(nextEnd + 1 - result.cells.length)); + + // Copy the tile into place + forEachCellInArea(next, nextEnd, result, (_c, i) => { + result.cells[i] = { + item: cell.item, + origin: i === next, + columns: nextColumns, + rows: nextRows, + }; + }); + + next = nextEnd + 1; + } + } + + return fillGaps(result); +} diff --git a/test/video-grid/model-test.ts b/test/video-grid/model-test.ts index 5765915..dd25e8a 100644 --- a/test/video-grid/model-test.ts +++ b/test/video-grid/model-test.ts @@ -21,6 +21,7 @@ import { fillGaps, forEachCellInArea, Grid, + resize, row, tryMoveTile, } from "../../src/video-grid/model"; @@ -386,3 +387,52 @@ abb ccd cce` ); + +function testResize( + title: string, + columns: number, + input: string, + output: string +): void { + test(`resize ${title}`, () => { + expect(showGrid(resize(mkGrid(input), columns))).toBe(output); + }); +} + +testResize( + "contracts the grid", + 2, + ` +abbb +cbbb +ddde +dddf +gh`, + ` +af +bb +bb +ch +dd +dd +eg` +); + +testResize( + "expands the grid", + 4, + ` +af +bb +bb +ch +dd +dd +eg`, + ` +bbbc +bbbf +addd +hddd +ge` +);