Show lists in the inspector and variables in their own tab (#252)
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:
1 parent
92fad6b9e3
commit
4857bb75e6
26 files changed
+1203
-427
No files matched your search
+55
-33
@@ -44,8 +44,8 @@ import { ContainerEditor } from './container/ContainerEditor';
|
||||
import { EditorRun } from './EditorRun';
|
||||
import { GraphEditor } from './graph/GraphEditor';
|
||||
import { Inspector } from './inspector/Inspector';
|
||||
import { ListEditor } from './list/ListEditor';
|
||||
import { resolveList } from './list/slot';
|
||||
import { valuesTitle, VariablesEditor } from './variables/VariablesEditor';
|
||||
import { valuesKey } from './variables/target';
|
||||
import { AnimationEditor } from './animation/AnimationEditor';
|
||||
import { TileEditor } from './tile/TileEditor';
|
||||
import { AssetPanel } from './panels/AssetPanel';
|
||||
@@ -66,7 +66,7 @@ import { ShaderEditor } from './shader/ShaderEditor';
|
||||
import { StateMachineEditor } from './statemachine/StateMachineEditor';
|
||||
import { installShortcuts } from './shortcuts';
|
||||
import type { PlayFrom } from '../core/model/playSettings';
|
||||
import type { EditorState } from './state';
|
||||
import type { EditorState, ValuesTarget } from './state';
|
||||
import { copySelection } from './clipboard';
|
||||
import { PixelArtEditor } from './pixelart/PixelArtEditor';
|
||||
import { VectorArtEditor } from './vectorart/VectorArtEditor';
|
||||
@@ -92,7 +92,7 @@ export type CentreTab =
|
||||
| 'curve'
|
||||
| 'tile'
|
||||
| 'container'
|
||||
| 'list'
|
||||
| 'variables'
|
||||
| 'management';
|
||||
|
||||
/** A tab a plugin added; the prefix keeps it apart from every built-in one. */
|
||||
@@ -100,10 +100,11 @@ export type PluginTabId = `plugin:${string}`;
|
||||
/** What is showing: a fixed tab, a plugin's, or the kind of the open document on screen. */
|
||||
export type TabId = CentreTab | PluginTabId;
|
||||
/**
|
||||
* An open document (#171): an asset by its id, or the list the List tab edits – there is one. The
|
||||
* user management (#179) is no document, but it opens and closes like one.
|
||||
* An open document (#171): an asset by its id, or what a Variables tab shows (#252) by its
|
||||
* `valuesKey` – as many of those as were opened. The user management (#179) is no document, but it
|
||||
* opens and closes like one.
|
||||
*/
|
||||
export type DocumentId = `asset:${string}` | 'list' | 'management';
|
||||
export type DocumentId = `asset:${string}` | `values:${string}` | 'management';
|
||||
/** The account behind this session; the menu for it sits at the far right of the toolbar. */
|
||||
export interface EditorAccount {
|
||||
/** Who is signed in – the menu says so, since the toolbar has no room for a second label. */
|
||||
@@ -121,7 +122,7 @@ export type StripTab = FixedTab | PluginTabId | DocumentId;
|
||||
|
||||
type FixedTab = (typeof FIXED_TABS)[number];
|
||||
/** The kinds whose documents are assets, each shown by one editor that is pointed at them in turn. */
|
||||
type AssetKind = Exclude<CentreTab, FixedTab | 'list' | 'management'>;
|
||||
type AssetKind = Exclude<CentreTab, FixedTab | 'variables' | 'management'>;
|
||||
|
||||
const FIXED_TABS = ['scene', 'game'] as const;
|
||||
|
||||
@@ -139,7 +140,7 @@ const PANE_ORDER = [
|
||||
'curve',
|
||||
'tile',
|
||||
'container',
|
||||
'list',
|
||||
'variables',
|
||||
'management',
|
||||
] as const;
|
||||
|
||||
@@ -158,7 +159,7 @@ const TAB_LABELS: Record<CentreTab, string> = {
|
||||
curve: 'Curve',
|
||||
tile: 'Tile',
|
||||
container: 'Container',
|
||||
list: 'List',
|
||||
variables: 'Variables',
|
||||
management: 'User management',
|
||||
};
|
||||
|
||||
@@ -169,7 +170,7 @@ const FIXED_TAB_ICONS: Partial<Record<FixedTab, string>> = {
|
||||
};
|
||||
|
||||
function isDocument(tab: StripTab): tab is DocumentId {
|
||||
return tab === 'list' || tab === 'management' || tab.startsWith('asset:');
|
||||
return tab === 'management' || tab.startsWith('asset:') || tab.startsWith('values:');
|
||||
}
|
||||
|
||||
const MIN_LEFT_WIDTH = 160;
|
||||
@@ -204,7 +205,9 @@ export class Editor implements PlayHost {
|
||||
readonly curveEditor: CurveEditor;
|
||||
readonly tileEditor: TileEditor;
|
||||
readonly containerEditor: ContainerEditor;
|
||||
readonly listEditor: ListEditor;
|
||||
readonly variablesEditor: VariablesEditor;
|
||||
/** What each open Variables tab shows, by its document. */
|
||||
private readonly valueTargets = new Map<DocumentId, ValuesTarget>();
|
||||
/** The switch per file in `data/plugins/`, in the user management tab (#179). */
|
||||
readonly pluginsPane = new PluginsPane();
|
||||
/** The accounts, above the plugins; null unless an administrator is signed in. */
|
||||
@@ -284,7 +287,7 @@ export class Editor implements PlayHost {
|
||||
this.curveEditor = new CurveEditor(state);
|
||||
this.tileEditor = new TileEditor(state);
|
||||
this.containerEditor = new ContainerEditor(state);
|
||||
this.listEditor = new ListEditor(state);
|
||||
this.variablesEditor = new VariablesEditor(state);
|
||||
this.hierarchy = new HierarchyPanel(state);
|
||||
this.inspector = new Inspector(state);
|
||||
this.tilemapPanel = new TilemapPanel(state);
|
||||
@@ -320,7 +323,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.variablesEditor.liveVariables = this.inspector.liveVariables;
|
||||
|
||||
this.usersPane =
|
||||
account.role === 'admin'
|
||||
@@ -395,7 +398,7 @@ export class Editor implements PlayHost {
|
||||
scene: this.scenePane,
|
||||
game: this.gamePane,
|
||||
...this.assetEditors,
|
||||
list: this.listEditor,
|
||||
variables: this.variablesEditor,
|
||||
management: this.managementPane,
|
||||
};
|
||||
// Following a call goes on in the same tab: the tab changes its document, not its place.
|
||||
@@ -472,10 +475,13 @@ export class Editor implements PlayHost {
|
||||
if (findAsset(state.project, s.assetId)?.type === 'scene') this.showTab('scene');
|
||||
else this.openDocument(`asset:${s.assetId}`);
|
||||
});
|
||||
// Opening a list is a request to look at it, so this switches even when the slot is unchanged.
|
||||
state.on('list', () => {
|
||||
if (state.listSlot) this.openDocument('list');
|
||||
else this.closeDocument('list');
|
||||
// A tab per target: a list opened from inside a tab gets one beside it, not that tab (#252).
|
||||
state.on('values', () => {
|
||||
const target = state.openedValues;
|
||||
if (!target) return;
|
||||
const doc: DocumentId = `values:${valuesKey(target)}`;
|
||||
if (!this.valueTargets.has(doc)) this.valueTargets.set(doc, target);
|
||||
this.openDocument(doc);
|
||||
});
|
||||
state.on('project', () => {
|
||||
this.play.syncFromModel();
|
||||
@@ -486,6 +492,7 @@ export class Editor implements PlayHost {
|
||||
this.editorRun.invalidate();
|
||||
for (const doc of this.documents.docs) this.tabs.removeTab(doc);
|
||||
this.documents = { docs: [], active: null };
|
||||
this.valueTargets.clear();
|
||||
this.showTab('scene');
|
||||
});
|
||||
state.on('mode', () => this.editorRun.invalidate());
|
||||
@@ -614,7 +621,7 @@ export class Editor implements PlayHost {
|
||||
openDocument(doc: DocumentId): void {
|
||||
if (!this.kindOf(doc)) return;
|
||||
if (this.documents.active === doc) {
|
||||
// Still worth a look at the name: the List tab may have been pointed at another list.
|
||||
// Still worth a look at the name: a Variables tab's owner may have been renamed.
|
||||
this.syncStrip();
|
||||
return;
|
||||
}
|
||||
@@ -632,8 +639,7 @@ export class Editor implements PlayHost {
|
||||
const kind = this.kindOf(doc);
|
||||
if (kind && kind in this.assetEditors)
|
||||
this.assetEditors[kind as AssetKind].forgetView(doc.slice('asset:'.length));
|
||||
// The List tab edits the one slot there is; a closed tab lets go of it.
|
||||
if (doc === 'list' && this.state.listSlot) this.state.openList(null);
|
||||
this.valueTargets.delete(doc);
|
||||
if (!wasShowing) return;
|
||||
if (this.documents.active) this.showDocument(this.documents.active);
|
||||
else this.showTab('scene');
|
||||
@@ -645,8 +651,9 @@ export class Editor implements PlayHost {
|
||||
* a document without knowing its tab.
|
||||
*/
|
||||
showTab(tab: TabId): void {
|
||||
if (tab === 'list') {
|
||||
if (this.state.listSlot) this.openDocument('list');
|
||||
if (tab === 'variables') {
|
||||
const doc = [...this.documents.docs].reverse().find((d) => d.startsWith('values:'));
|
||||
if (doc) this.openDocument(doc);
|
||||
return;
|
||||
}
|
||||
if (tab === 'management') {
|
||||
@@ -672,7 +679,10 @@ export class Editor implements PlayHost {
|
||||
}
|
||||
|
||||
private kindOf(doc: DocumentId): CentreTab | null {
|
||||
if (doc === 'list') return this.state.listSlot ? 'list' : null;
|
||||
if (doc.startsWith('values:')) {
|
||||
const target = this.valueTargets.get(doc);
|
||||
return target && valuesTitle(this.state, target) !== null ? 'variables' : null;
|
||||
}
|
||||
if (doc === 'management') return 'management';
|
||||
const type = findAsset(this.state.project, doc.slice('asset:'.length))?.type;
|
||||
const kind = type ? TAB_OF_ASSET[type] : undefined;
|
||||
@@ -683,25 +693,27 @@ export class Editor implements PlayHost {
|
||||
const kind = this.kindOf(doc);
|
||||
if (!kind) return;
|
||||
if (kind in this.assetEditors) this.assetEditors[kind as AssetKind].showAsset(doc.slice('asset:'.length));
|
||||
const target = this.valueTargets.get(doc);
|
||||
if (kind === 'variables' && target) this.variablesEditor.show(target);
|
||||
// Read on every showing: an account may have been changed from another tab in the meantime.
|
||||
if (kind === 'management') void this.usersPane?.reload();
|
||||
this.display(kind, doc);
|
||||
}
|
||||
|
||||
/** What a document's tab says: the asset's name, or the list's owner and name, as its tab heads it. */
|
||||
/** What a document's tab says: the asset's name, or what a Variables tab shows, as its header says. */
|
||||
private labelOf(doc: DocumentId): string {
|
||||
if (doc === 'management') return this.usersPane ? TAB_LABELS.management : 'Plugins';
|
||||
if (doc === 'list') {
|
||||
const slot = this.state.listSlot;
|
||||
return (slot && resolveList(this.state, slot)?.title) ?? '';
|
||||
}
|
||||
const target = this.valueTargets.get(doc);
|
||||
if (target) return valuesTitle(this.state, target, this.inspector.liveVariables) ?? '';
|
||||
return findAsset(this.state.project, doc.slice('asset:'.length))?.name ?? '';
|
||||
}
|
||||
|
||||
/** The icon in front of a document's name: its asset type's, as in the asset panel. */
|
||||
private iconOf(doc: DocumentId): string {
|
||||
if (doc === 'management') return this.usersPane ? USERS_ICON_SVG : PLUGINS_ICON_SVG;
|
||||
const type = doc === 'list' ? undefined : findAsset(this.state.project, doc.slice('asset:'.length))?.type;
|
||||
const target = this.valueTargets.get(doc);
|
||||
if (target) return target.kind === 'list' ? LIST_ICON_SVG : ASSET_ICON_SVG.variables;
|
||||
const type = findAsset(this.state.project, doc.slice('asset:'.length))?.type;
|
||||
return type ? ASSET_ICON_SVG[type] : LIST_ICON_SVG;
|
||||
}
|
||||
|
||||
@@ -764,8 +776,18 @@ export class Editor implements PlayHost {
|
||||
* Global x-extent of the tab buttons in the row, from the widget box – not `getBounds()`, which
|
||||
* on an untoggled ghost tab sees nothing but the label (the background is not drawn at all).
|
||||
*/
|
||||
tabRow(): { shown: TabPlace<StripTab>[]; hidden: StripTab[]; order: StripTab[] } {
|
||||
return { shown: this.tabs.places(), hidden: this.tabs.hidden(), order: [...this.tabs.tabs] };
|
||||
tabRow(): {
|
||||
shown: TabPlace<StripTab>[];
|
||||
hidden: StripTab[];
|
||||
order: StripTab[];
|
||||
labels: Record<string, string>;
|
||||
} {
|
||||
return {
|
||||
shown: this.tabs.places(),
|
||||
hidden: this.tabs.hidden(),
|
||||
order: [...this.tabs.tabs],
|
||||
labels: Object.fromEntries(this.documents.docs.map((doc) => [doc, this.labelOf(doc)])),
|
||||
};
|
||||
}
|
||||
|
||||
boot(): Promise<void> {
|
||||
|
||||
Reference in new issue
Block a user