The List tab could show one list at a time and was the only place to see its items. A list now shows its items right where it lives, and a new Variables tab shows all variables of an asset or placed object. - InlineList draws the items of a list under its row in the inspector, in struct fields and in the Variables tab. A list inside a list or a struct field is drawn the same way, one step indented. - The index column is the indent. Items sit under their list without an own margin, and the index column is 16 px wide instead of 28 px. - Above 20 items (INLINE_LIMIT) the rest collapses into "... N more - open in tab". During a run the rows show the values from the game. - ListHead puts "N items" and "+ Item" in one row. "N items" opens the list in a tab. A separate Open button squeezed the counter to zero width in the narrow inspector column. - VariablesEditor shows every variable of a behavior asset, a variables asset or a placed behavior. On an asset it edits the declarations. On a placed object an edit only overrides that object, with the usual reset and push buttons. - Opening a list inside a tab adds a new tab and keeps the open one. valuesKey gives each list its own document, so a list that is already open is shown instead of opened twice. - Titles read "Pickup · Variables" or "Listy · nums · [3]". The tab closes when its asset or object is deleted. - Lists on node inputs stay inline only, a node owns no variables. - The List tab, ListEditor and the __gm.list hook are gone. Tests: - ./check.sh: 181 files, 2645 tests, CHECK EXIT 0 - GM_STEPS=1,21,22,32: ALL PASSED, 123 checks - e2e 21 is rewritten for inline lists and the new tab, 22 and 32 are adapted. Sum in README: 1486 (+6 in 21, -1 in 22), counted by hand. - Counter-probes fail as expected: sub-lists not told apart (1 unit test), sub-list replacing the open tab, all items drawn inline (60 rows instead of 20), no live values (0 rows at 24 items in a run). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
6.3 KiB
TypeScript
142 lines
6.3 KiB
TypeScript
/**
|
||
* The rules behind the Variables tab (#252): which document a target is – the same list asked for
|
||
* twice is one tab, a list inside it another – what the tab says, and how much of a list the
|
||
* inspector draws.
|
||
*/
|
||
import { beforeEach, describe, expect, it } from 'vitest';
|
||
import { CreateAssetCommand, DeleteAssetCommand } from '../../src/core/commands/assetCommands';
|
||
import { AddNodeCommand } from '../../src/core/commands/graphCommands';
|
||
import { PlaceAssetCommand, RenameSceneObjectCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import { AddVariableCommand } from '../../src/core/commands/variableCommands';
|
||
import { createEmptyProject } from '../../src/core/model/Project';
|
||
import { resolveList } from '../../src/editor/list/slot';
|
||
import { EditorState, type ListSlot, type ValuesTarget } from '../../src/editor/state';
|
||
import { deeperSlot, INLINE_LIMIT, shownItems, valuesKey } from '../../src/editor/variables/target';
|
||
import { valuesTitle } from '../../src/editor/variables/VariablesEditor';
|
||
|
||
function setup() {
|
||
const state = new EditorState();
|
||
state.loadProject(createEmptyProject('p', 'P'), 1);
|
||
const create = new CreateAssetCommand('behavior', 'Pickup');
|
||
expect(state.execute(create)).toBe(true);
|
||
const id = create.asset.id;
|
||
expect(state.execute(new AddVariableCommand(id, 'loots', 'list'))).toBe(true);
|
||
return { state, id };
|
||
}
|
||
|
||
describe('the document of a Variables tab', () => {
|
||
const slot: ListSlot = { kind: 'asset', assetId: 'a', key: 'loots' };
|
||
|
||
it('is the same for the same list, however the address was built', () => {
|
||
const built = { key: 'loots', assetId: 'a', kind: 'asset', path: [] } as const;
|
||
expect(valuesKey({ kind: 'list', slot })).toBe(valuesKey({ kind: 'list', slot: built }));
|
||
expect(valuesKey({ kind: 'list', slot: deeperSlot(slot, [2]) })).toBe(
|
||
valuesKey({ kind: 'list', slot: { ...slot, path: [2] } }),
|
||
);
|
||
});
|
||
|
||
it('is another one for a list inside it, so opening that one adds a tab', () => {
|
||
const outer = valuesKey({ kind: 'list', slot });
|
||
const inner = valuesKey({ kind: 'list', slot: deeperSlot(slot, [2, 'drops']) });
|
||
expect(inner).not.toBe(outer);
|
||
// A key and an index that read alike are still two steps.
|
||
expect(valuesKey({ kind: 'list', slot: deeperSlot(slot, ['2']) })).not.toBe(
|
||
valuesKey({ kind: 'list', slot: deeperSlot(slot, [2]) }),
|
||
);
|
||
});
|
||
|
||
it('keeps the variables of an owner apart from a list of it and from another owner', () => {
|
||
const asset: ValuesTarget = { kind: 'variables', owner: { kind: 'asset', assetId: 'a' } };
|
||
const object: ValuesTarget = {
|
||
kind: 'variables',
|
||
owner: { kind: 'object', holderId: 's', objectId: 'a' },
|
||
};
|
||
const keys = new Set([asset, object, { kind: 'list', slot } as const].map(valuesKey));
|
||
expect(keys.size).toBe(3);
|
||
});
|
||
});
|
||
|
||
describe('how much of a list the inspector draws', () => {
|
||
it('draws everything up to the limit and counts the rest', () => {
|
||
expect(shownItems(5, INLINE_LIMIT)).toEqual({ shown: 5, more: 0 });
|
||
expect(shownItems(INLINE_LIMIT, INLINE_LIMIT)).toEqual({ shown: INLINE_LIMIT, more: 0 });
|
||
expect(shownItems(23, INLINE_LIMIT)).toEqual({ shown: INLINE_LIMIT, more: 3 });
|
||
});
|
||
|
||
it('draws all of it without a limit – the tab', () => {
|
||
expect(shownItems(500)).toEqual({ shown: 500, more: 0 });
|
||
});
|
||
});
|
||
|
||
describe('what a Variables tab says', () => {
|
||
let f: ReturnType<typeof setup>;
|
||
beforeEach(() => (f = setup()));
|
||
|
||
it('names the asset, the placed object and the list', () => {
|
||
expect(valuesTitle(f.state, { kind: 'variables', owner: { kind: 'asset', assetId: f.id } })).toBe(
|
||
'Pickup · Variables',
|
||
);
|
||
const createScene = new CreateAssetCommand('scene', 'Level');
|
||
expect(f.state.execute(createScene)).toBe(true);
|
||
const scene = createScene.asset.id;
|
||
const place = new PlaceAssetCommand(scene, f.id, { x: 0, y: 0 });
|
||
expect(f.state.execute(place)).toBe(true);
|
||
const object: ValuesTarget = {
|
||
kind: 'variables',
|
||
owner: { kind: 'object', holderId: scene, objectId: place.objectId },
|
||
};
|
||
expect(valuesTitle(f.state, object)).toBe('Pickup · Variables');
|
||
expect(f.state.execute(new RenameSceneObjectCommand(scene, place.objectId, 'Coin 3'))).toBe(true);
|
||
expect(valuesTitle(f.state, object)).toBe('Coin 3 · Variables');
|
||
const slot: ListSlot = { kind: 'asset', assetId: f.id, key: 'loots' };
|
||
expect(valuesTitle(f.state, { kind: 'list', slot })).toBe('Pickup · loots');
|
||
});
|
||
|
||
it('says nothing once the owner is gone – the tab closes then', () => {
|
||
const target: ValuesTarget = { kind: 'variables', owner: { kind: 'asset', assetId: f.id } };
|
||
const list: ValuesTarget = { kind: 'list', slot: { kind: 'asset', assetId: f.id, key: 'loots' } };
|
||
expect(f.state.execute(new DeleteAssetCommand(f.id))).toBe(true);
|
||
expect(valuesTitle(f.state, target)).toBe(null);
|
||
expect(valuesTitle(f.state, list)).toBe(null);
|
||
});
|
||
|
||
it('has no Variables tab for an asset that declares none', () => {
|
||
const texture = new CreateAssetCommand('texture', 'T');
|
||
expect(f.state.execute(texture)).toBe(true);
|
||
const target: ValuesTarget = { kind: 'variables', owner: { kind: 'asset', assetId: texture.asset.id } };
|
||
expect(valuesTitle(f.state, target)).toBe(null);
|
||
});
|
||
});
|
||
|
||
describe('opening a list in a tab', () => {
|
||
let f: ReturnType<typeof setup>;
|
||
beforeEach(() => (f = setup()));
|
||
|
||
it('asks for a tab on the list itself, or on one inside it', () => {
|
||
const list = resolveList(f.state, { kind: 'asset', assetId: f.id, key: 'loots' })!;
|
||
list.replace([[1, 2]]);
|
||
let asked = 0;
|
||
f.state.on('values', () => (asked += 1));
|
||
list.openList!([]);
|
||
expect(f.state.openedValues).toEqual({
|
||
kind: 'list',
|
||
slot: { kind: 'asset', assetId: f.id, key: 'loots', path: [] },
|
||
});
|
||
list.openList!([0]);
|
||
expect(f.state.openedValues).toEqual({
|
||
kind: 'list',
|
||
slot: { kind: 'asset', assetId: f.id, key: 'loots', path: [0] },
|
||
});
|
||
expect(asked).toBe(2);
|
||
});
|
||
|
||
it('offers no tab for a list a node holds inline', () => {
|
||
const add = new AddNodeCommand(f.id, 'List', 0, 0);
|
||
expect(f.state.execute(add)).toBe(true);
|
||
const node = resolveList(f.state, { kind: 'node', assetId: f.id, nodeId: add.node.id, key: 'value' })!;
|
||
expect(node.openList).toBeUndefined();
|
||
node.replace([[1]]);
|
||
expect(node.sub([0])!.openList).toBeUndefined();
|
||
});
|
||
});
|