Show lists in the inspector and variables in their own tab (#252)
game-maker / gate (push) Successful in 3m24s
game-maker / suite (push) Successful in 10m22s

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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-27 21:56:21 +02:00
1 parent 92fad6b9e3
commit 4857bb75e6
26 files changed
+1203 -427

No files matched your search

+21 -33
View File
@@ -57,11 +57,10 @@ export type Selection =
| null;
/**
* Which list the List tab edits. Deliberately not a `Selection`: a list is not something one
* selects, it is a value inside something already selected, and exactly three places hold one.
* `path` leads to a list nested inside the value `key` names (#239): a key per struct field, an
* index per list item – `[3, 'drops']` is the list in field `drops` of item 3. A struct item needs
* no step of its own to be edited, because the List tab renders it inline.
* Where a list lives. Deliberately not a `Selection`: a list is not something one selects, it is a
* value inside something already selected, and exactly three places hold one. `path` leads to a
* list nested inside the value `key` names (#239): a key per struct field, an index per list item –
* `[3, 'drops']` is the list in field `drops` of item 3.
*/
export type ListSlot = { path?: ValuePath } & (
| { kind: 'asset'; assetId: string; key: string }
@@ -69,20 +68,12 @@ export type ListSlot = { path?: ValuePath } & (
| { kind: 'node'; assetId: string; nodeId: string; key: string }
);
function sameSlot(a: ListSlot | null, b: ListSlot | null): boolean {
if (!a || !b) return a === b;
const pa = a.path ?? [];
const pb = b.path ?? [];
if (pa.length !== pb.length || pa.some((step, i) => step !== pb[i])) return false;
switch (a.kind) {
case 'asset':
return b.kind === 'asset' && b.assetId === a.assetId && b.key === a.key;
case 'object':
return b.kind === 'object' && b.holderId === a.holderId && b.objectId === a.objectId && b.key === a.key;
case 'node':
return b.kind === 'node' && b.assetId === a.assetId && b.nodeId === a.nodeId && b.key === a.key;
}
}
/** Whose variables a Variables tab shows (#252): a behavior or Variables asset, or a placed behavior. */
export type VariablesOwner =
{ kind: 'asset'; assetId: string } | { kind: 'object'; holderId: string; objectId: string };
/** What a Variables tab shows: every variable of an owner, or one list at any depth. */
export type ValuesTarget = { kind: 'variables'; owner: VariablesOwner } | { kind: 'list'; slot: ListSlot };
/** Builds an object selection; the last id is the primary one unless given explicitly. */
export function objectSelection(holderId: string, objectIds: readonly string[], primary?: string): Selection {
@@ -188,8 +179,8 @@ export type EditorEvent =
/** A different project was loaded. */
| 'load'
| 'selection'
/** The List tab was pointed at another list (or at none). */
| 'list'
/** A Variables tab was asked for (`openedValues`). */
| 'values'
/** An asset was opened (double-click, Enter, or just created): its tab should show. */
| 'open'
/** An asset should light up where it is listed (`pingedAssetId`), without being selected. */
@@ -214,7 +205,8 @@ export class EditorState {
revision = 0;
commands = new CommandStack(this.project);
selection: Selection = null;
listSlot: ListSlot | null = null;
/** The last target asked for by `openValues`; the tabs themselves are the editor's. */
openedValues: ValuesTarget | null = null;
mode: EditorMode = 'edit';
/**
* A sub-state of play rather than a third mode: the runtime copy, the keyboard and unsaved edits all
@@ -265,7 +257,7 @@ export class EditorState {
this.revision = revision;
this.commands = new CommandStack(project);
this.selection = null;
this.listSlot = null;
this.openedValues = null;
this.clipboard = null;
this.saveStatus = 'saved';
this.emit('load');
@@ -426,22 +418,18 @@ export class EditorState {
}
/**
* Points the List tab at a list. No tab knows the `Editor`, so this is how the button on a
* list row gets there: the state changes, the editor switches.
* Asks for a Variables tab on `target` – a new one, or the one already showing it. No tab knows
* the `Editor`, so this is how the button on a list or a VARIABLES header gets there.
*/
openList(slot: ListSlot | null): void {
if (sameSlot(this.listSlot, slot)) {
if (slot) this.emit('list');
return;
}
this.listSlot = slot;
this.emit('list');
openValues(target: ValuesTarget): void {
this.openedValues = target;
this.emit('values');
}
/**
* Selects the asset and asks for its tab. Selecting alone never switches tabs (a click in the
* Assets panel is a look at the inspector); this is the way a double-click or a fresh asset gets
* there, and like `openList` it works without knowing the `Editor`.
* there, and like `openValues` it works without knowing the `Editor`.
*/
openAsset(assetId: string): void {
this.select({ kind: 'asset', assetId });