Make NewVideoGrid support arbitrary layout systems

In preparation for adding layouts other than big grid to the NewVideoGrid component, I've abstracted the grid layout system into an interface called Layout. For now, the only implementation of this interface is BigGrid, but this will allow us to easily plug in Spotlight, SplitGrid, and OneOnOne layout systems so we can get rid of the old VideoGrid component and have One Grid to Rule Them All™.

Please do shout if any of this seems obtuse or underdocumented, because I'm not super happy with how approachable the NewVideoGrid code looks right now…

Incidentally, this refactoring made it way easier to save the state of the grid while in fullscreen / another layout, so I went ahead and did that.
This commit is contained in:
Robin Townsend 2023-06-27 12:19:06 -04:00
commit cc35f243f2
8 changed files with 501 additions and 298 deletions

View file

@ -20,23 +20,23 @@ import {
cycleTileSize,
fillGaps,
forEachCellInArea,
Grid,
BigGridState,
resize,
row,
tryMoveTile,
} from "../../src/video-grid/model";
moveTile,
} from "../../src/video-grid/BigGrid";
import { TileDescriptor } from "../../src/video-grid/VideoGrid";
/**
* Builds a grid from a string specifying the contents of each cell as a letter.
*/
function mkGrid(spec: string): Grid {
function mkGrid(spec: string): BigGridState {
const secondNewline = spec.indexOf("\n", 1);
const columns = secondNewline === -1 ? spec.length : secondNewline - 1;
const cells = spec.match(/[a-z ]/g) ?? ([] as string[]);
const areas = new Set(cells);
areas.delete(" "); // Space represents an empty cell, not an area
const grid: Grid = { columns, cells: new Array(cells.length) };
const grid: BigGridState = { columns, cells: new Array(cells.length) };
for (const area of areas) {
const start = cells.indexOf(area);
@ -60,12 +60,12 @@ function mkGrid(spec: string): Grid {
/**
* Turns a grid into a string showing the contents of each cell as a letter.
*/
function showGrid(g: Grid): string {
function showGrid(g: BigGridState): string {
let result = "\n";
g.cells.forEach((c, i) => {
for (let i = 0; i < g.cells.length; i++) {
if (i > 0 && i % g.columns == 0) result += "\n";
result += c?.item.id ?? " ";
});
result += g.cells[i]?.item.id ?? " ";
}
return result;
}
@ -222,21 +222,12 @@ function testCycleTileSize(
output: string
): void {
test(`cycleTileSize ${title}`, () => {
expect(showGrid(cycleTileSize(tileId, mkGrid(input)))).toBe(output);
const grid = mkGrid(input);
const tile = grid.cells.find((c) => c?.item.id === tileId)!.item;
expect(showGrid(cycleTileSize(grid, tile))).toBe(output);
});
}
testCycleTileSize(
"does nothing if the tile is not present",
"z",
`
abcd
efgh`,
`
abcd
efgh`
);
testCycleTileSize(
"expands a tile to 2×2 in a 3 column layout",
"c",
@ -345,8 +336,8 @@ abc
def`,
`
abc
gfe
d`
g
def`
);
testAddItems(
@ -362,19 +353,19 @@ gge
d`
);
function testTryMoveTile(
function testMoveTile(
title: string,
from: number,
to: number,
input: string,
output: string
): void {
test(`tryMoveTile ${title}`, () => {
expect(showGrid(tryMoveTile(mkGrid(input), from, to))).toBe(output);
test(`moveTile ${title}`, () => {
expect(showGrid(moveTile(mkGrid(input), from, to))).toBe(output);
});
}
testTryMoveTile(
testMoveTile(
"refuses to move a tile too far to the left",
1,
-1,
@ -384,7 +375,7 @@ abc`,
abc`
);
testTryMoveTile(
testMoveTile(
"refuses to move a tile too far to the right",
1,
3,
@ -394,7 +385,7 @@ abc`,
abc`
);
testTryMoveTile(
testMoveTile(
"moves a large tile to an unoccupied space",
3,
1,
@ -408,7 +399,7 @@ bcc
d e`
);
testTryMoveTile(
testMoveTile(
"refuses to move a large tile to an occupied space",
3,
1,