Show the list a running game fills in the list tab (#251)
While playing, the inspector said "2 items" for a list a graph had filled, but clicking it opened an empty list tab. The inspector reads a Variables asset from the running game, the list tab only read the list stored in the project. It did not follow the run's ticks either. - resolveList takes the live values. For a Variables asset it uses the running game's value while one runs, the project's otherwise. This is the inspector's rule. - ListEditor refreshes on runtime ticks about 10 times a second, and not while a field in the tab is being typed in, like the inspector. - Editor hands the list editor the inspector's liveVariables. - __gm.list.info() also returns rows, the rows the last build drew. - Editing in the tab while playing goes into the run's copy of the project, as in the inspector. A very long, growing list rebuilds all rows up to 10 times a second. Tests: - ./check.sh: 180 files, 2634 tests, CHECK EXIT 0 - e2e GM_STEPS=1,32: ALL PASSED, 30 checks - e2e 32b-ii, 2 checks: a graph adds an item every frame, the test pauses and clicks "N items" with a real mouse. The tab draws 26 rows for 26 items, grows to 50 after resuming and is empty after Stop. - Counter-checks: without live values the tab draws 0 rows for 23 items, and without the refresh it stays at 23 - README: 1481 editor checks, counted by hand Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b90b62886b
commit
92fad6b9e3
5 files changed
+126
-9
No files matched your search
@@ -320,6 +320,7 @@ export class Editor implements PlayHost {
|
||||
// Only one of the two ever runs a given asset: playing stops the editor run (see `EditorRun.tick`).
|
||||
this.inspector.liveVariables = (assetId) =>
|
||||
this.play.liveVariables(assetId) ?? this.editorRun.liveVariables(assetId);
|
||||
this.listEditor.liveVariables = this.inspector.liveVariables;
|
||||
|
||||
this.usersPane =
|
||||
account.role === 'admin'
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Spacer,
|
||||
TextField,
|
||||
Theme,
|
||||
ui,
|
||||
VStack,
|
||||
type Widget,
|
||||
} from '../../ui';
|
||||
@@ -50,7 +51,7 @@ import {
|
||||
} from '../inspector/fieldFactory';
|
||||
import { kindMenu, markPicker } from '../inspector/variableRows';
|
||||
import type { EditorState } from '../state';
|
||||
import { type ListAccess, resolveList } from './slot';
|
||||
import { type ListAccess, type LiveVariables, resolveList } from './slot';
|
||||
|
||||
const INDEX_WIDTH = 40;
|
||||
|
||||
@@ -89,6 +90,10 @@ export class ListEditor extends VStack {
|
||||
private signature = '';
|
||||
private setters: ((item: PropertyValue) => void)[] = [];
|
||||
private rowWidgets: RowWidgets[] = [];
|
||||
/** Last live refresh (play mode), in ms – the same pace as the inspector's. */
|
||||
private lastLiveRefresh = 0;
|
||||
/** A Variables asset's live values, set by the editor – the only place that knows the running worlds. */
|
||||
liveVariables: LiveVariables = () => null;
|
||||
|
||||
constructor(private readonly state: EditorState) {
|
||||
super({ gap: 0 });
|
||||
@@ -117,9 +122,19 @@ export class ListEditor extends VStack {
|
||||
state.on('list', () => this.rebuild());
|
||||
state.on('load', () => this.rebuild());
|
||||
state.on('project', () => this.refresh());
|
||||
state.on('runtime', () => this.refreshLive());
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
/** Play mode: follow a list the behaviors fill, ~10× a second, but never under a field being typed in. */
|
||||
private refreshLive(): void {
|
||||
const now = performance.now();
|
||||
if (now - this.lastLiveRefresh < 100) return;
|
||||
if (ui().focus.isWithin(this)) return;
|
||||
this.lastLiveRefresh = now;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/** For the check scripts: where an item's widget sits, since a number field carries no findable text. */
|
||||
fieldAt(index: number, field = ''): { x: number; y: number } | null {
|
||||
const row = this.rowWidgets[index];
|
||||
@@ -143,10 +158,12 @@ export class ListEditor extends VStack {
|
||||
}
|
||||
|
||||
/** For the check scripts: what the tab shows and how its rows read right now. */
|
||||
debugInfo(): { title: string; items: string[]; kinds: NestedKind[]; offers: NestedKind[] } {
|
||||
debugInfo(): { title: string; items: string[]; kinds: NestedKind[]; offers: NestedKind[]; rows: number } {
|
||||
const access = this.access();
|
||||
if (!access) return { title: '', items: [], kinds: [], offers: [] };
|
||||
if (!access) return { title: '', items: [], kinds: [], offers: [], rows: this.rowWidgets.length };
|
||||
return {
|
||||
// Drawn, not read: `items` re-reads the list, `rows` is what the last build put on screen.
|
||||
rows: this.rowWidgets.length,
|
||||
title: access.title,
|
||||
items: access.value.map((item) => textOf(item)),
|
||||
kinds: access.value.map(itemKindOf),
|
||||
@@ -156,7 +173,7 @@ export class ListEditor extends VStack {
|
||||
|
||||
private access(): ListAccess | null {
|
||||
const slot = this.state.listSlot;
|
||||
return slot ? resolveList(this.state, slot) : null;
|
||||
return slot ? resolveList(this.state, slot, this.liveVariables) : null;
|
||||
}
|
||||
|
||||
/** Everything that decides how many rows there are and of what kind – a struct item's fields included, at any depth. */
|
||||
|
||||
+22
-4
@@ -3,12 +3,13 @@
|
||||
* edit gets the same command, coercion and undo step. A slot with a `path` still writes the whole value.
|
||||
*/
|
||||
import { edgesInto, type Graph, type GraphNode } from '../../core/graph/Graph';
|
||||
import { withoutObjects } from '../../core/graph/liveObjects';
|
||||
import { resolveDef } from '../../core/graph/nested';
|
||||
import { inlineInputDef, inlineInputs, type NodeDef } from '../../core/graph/NodeRegistry';
|
||||
import type { Asset } from '../../core/model/Asset';
|
||||
import { graphLookup } from '../../core/model/graphLookup';
|
||||
import type { ObjectRef } from '../../core/model/instance';
|
||||
import { isList, type ListValue, type PropertyValue } from '../../core/model/Properties';
|
||||
import { isList, type ListValue, type PropertyBag, type PropertyValue } from '../../core/model/Properties';
|
||||
import { findAsset } from '../../core/model/Project';
|
||||
import { NESTED_KINDS, type NestedKind } from '../../core/properties/kinds';
|
||||
import { itemLike } from '../../core/properties/listItems';
|
||||
@@ -59,9 +60,16 @@ interface Owner<Target> {
|
||||
spec: Omit<PreviewedValue<Target>, 'toModel'>;
|
||||
}
|
||||
|
||||
export function resolveList(state: EditorState, slot: ListSlot): ListAccess | null {
|
||||
/** A Variables asset's values in the running game, or null while none runs it. */
|
||||
export type LiveVariables = (assetId: string) => PropertyBag | null;
|
||||
|
||||
export function resolveList(
|
||||
state: EditorState,
|
||||
slot: ListSlot,
|
||||
live: LiveVariables = () => null,
|
||||
): ListAccess | null {
|
||||
const inSlot = <Target>(owner: Owner<Target> | null) => owner && listIn(state, owner, slot);
|
||||
if (slot.kind === 'asset') return inSlot(assetOwner(state, slot));
|
||||
if (slot.kind === 'asset') return inSlot(assetOwner(state, slot, live));
|
||||
if (slot.kind === 'object') return inSlot(objectOwner(state, slot));
|
||||
return inSlot(nodeOwner(state, slot));
|
||||
}
|
||||
@@ -149,7 +157,15 @@ function access<Target>(
|
||||
};
|
||||
}
|
||||
|
||||
function assetOwner(state: EditorState, slot: Extract<ListSlot, { kind: 'asset' }>): Owner<Asset> | null {
|
||||
/**
|
||||
* While a run is on, a Variables asset shows what the run holds – the same rule as the inspector's
|
||||
* `shownValues`, so a list a graph fills is not "2 items" in the row and empty in the tab.
|
||||
*/
|
||||
function assetOwner(
|
||||
state: EditorState,
|
||||
slot: Extract<ListSlot, { kind: 'asset' }>,
|
||||
live: LiveVariables,
|
||||
): Owner<Asset> | null {
|
||||
const asset = findAsset(state.project, slot.assetId);
|
||||
const def = asset && schemaForAsset(state.project, asset).find((d) => d.key === slot.key);
|
||||
if (!asset || !def) return null;
|
||||
@@ -158,6 +174,8 @@ function assetOwner(state: EditorState, slot: Extract<ListSlot, { kind: 'asset'
|
||||
title: `${asset.name} · ${def.label}`,
|
||||
current: () => {
|
||||
const now = findAsset(state.project, slot.assetId);
|
||||
const bag = now?.type === 'variables' ? live(now.id) : null;
|
||||
if (bag && slot.key in bag) return withoutObjects(bag[slot.key]);
|
||||
return now && resolveAssetProps(state.project, now)[slot.key];
|
||||
},
|
||||
spec: assetValue(state, slot.assetId, slot.key),
|
||||
|
||||
Reference in new issue
Block a user