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
@@ -16,6 +16,9 @@ import {
|
||||
refreshObjectSection,
|
||||
} from './objectSection';
|
||||
import { buildTrackSection, refreshTrackSection, trackSignature } from './trackSection';
|
||||
import { INLINE_LIMIT } from '../variables/target';
|
||||
import { inlineListsIn } from '../variables/VariablesEditor';
|
||||
import type { InlineList } from '../list/InlineList';
|
||||
|
||||
/**
|
||||
* Shows the properties of the current selection. The rows are generated from the PropertySchema
|
||||
@@ -73,6 +76,11 @@ export class Inspector extends Panel {
|
||||
return { rebuilds: this.rebuilds, postponed: this.rebuildWhenLeft };
|
||||
}
|
||||
|
||||
/** For the check scripts: every list drawn inline in here, at any depth. */
|
||||
lists(): InlineList[] {
|
||||
return inlineListsIn(this.host.body);
|
||||
}
|
||||
|
||||
private emptyHost(): InspectorHost {
|
||||
return {
|
||||
state: this.state,
|
||||
@@ -80,6 +88,7 @@ export class Inspector extends Panel {
|
||||
project: () => this.state.project,
|
||||
liveVariables: (assetId) => this.liveVariables(assetId),
|
||||
followReference: (assetId, e) => this.followReference(assetId, e),
|
||||
listLimit: INLINE_LIMIT,
|
||||
body: new VStack({ padding: Theme.padding, gap: Theme.gap }),
|
||||
fields: new Map(),
|
||||
nameField: null,
|
||||
|
||||
@@ -42,12 +42,13 @@ import { Button, Dropdown, HStack, Label, Menu, Theme } from '../../ui';
|
||||
import { FontPreview } from '../font/FontPreview';
|
||||
import { clearSaved, hasSaved } from '../saves';
|
||||
import { SoundPreview } from '../sound/SoundPreview';
|
||||
import type { VariablesOwner } from '../state';
|
||||
import { aspectActions, sizeCommand } from './aspectAction';
|
||||
import { actionIf, createFieldRow, type FieldHandlers } from './fieldFactory';
|
||||
import { addNameField, addNote, addTitle, type InspectorHost } from './host';
|
||||
import { buildShapeListSection, shapeListSignature } from './shapeSection';
|
||||
import { buildVariable, kindMenu, type VariableEditing } from './variableRows';
|
||||
import { listOpener, previewingHandlers, structFields } from './previewHandlers';
|
||||
import { listSource, previewingHandlers, structFields } from './previewHandlers';
|
||||
import { assetValue } from './valueSpecs';
|
||||
|
||||
/** A struct field declares its own fields, starting with none. */
|
||||
@@ -76,7 +77,7 @@ export function assetSignature(project: Project, assetId: string): string {
|
||||
}
|
||||
|
||||
export function defSignature(def: PropertyDef | StructFieldDef): string {
|
||||
// Only a struct has rows of its own, at any depth; a list is one row whatever its items are.
|
||||
// Only a struct has rows of its own, at any depth; a list's items rebuild themselves (#252).
|
||||
const fields = def.kind === 'struct' ? def.fields.map(defSignature).join('+') : '';
|
||||
return `${def.key}:${def.kind}${fields ? `(${fields})` : ''}`;
|
||||
}
|
||||
@@ -170,7 +171,7 @@ function assetHandlers(host: InspectorHost, assetId: string, def: PropertyDef):
|
||||
const spec = assetValue(host.state, assetId, def.key);
|
||||
return {
|
||||
...previewingHandlers<Asset>(host, spec),
|
||||
...listOpener(host.state, def, () => ({ kind: 'asset', assetId, key: def.key })),
|
||||
...listSource(host, def, () => ({ kind: 'asset', assetId, key: def.key })),
|
||||
...structFields<Asset>(host, def, spec, (path) => ({ kind: 'asset', assetId, key: def.key, path })),
|
||||
};
|
||||
}
|
||||
@@ -193,8 +194,15 @@ function fieldsAt(fields: readonly StructFieldDef[], path: readonly string[]): r
|
||||
return field?.kind === 'struct' ? fieldsAt(field.fields, rest) : [];
|
||||
}
|
||||
|
||||
/** Declared variables, each with its declaration editable – plus an "add" menu. */
|
||||
function buildVariables(host: InspectorHost, asset: BehaviorAsset | VariablesAsset): void {
|
||||
/**
|
||||
* Declared variables, each with its declaration editable – plus an "add" menu, and in the inspector
|
||||
* the way to the same rows in a Variables tab (#252).
|
||||
*/
|
||||
export function buildVariables(
|
||||
host: InspectorHost,
|
||||
asset: BehaviorAsset | VariablesAsset,
|
||||
inTab = false,
|
||||
): void {
|
||||
const header = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
header.add(new Label('VARIABLES', { style: 'title' }), 1);
|
||||
const add = new Dropdown('+ Variable', { variant: 'ghost' });
|
||||
@@ -207,6 +215,7 @@ function buildVariables(host: InspectorHost, asset: BehaviorAsset | VariablesAss
|
||||
new Menu(items).openBelow(add);
|
||||
};
|
||||
header.add(add);
|
||||
if (!inTab) header.add(openVariablesButton(host, { kind: 'asset', assetId: asset.id }));
|
||||
host.body.add(header);
|
||||
|
||||
if (asset.variables.length === 0) addNote(host, 'No variables yet.');
|
||||
@@ -244,6 +253,14 @@ function buildVariables(host: InspectorHost, asset: BehaviorAsset | VariablesAss
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the variables of `owner` in a Variables tab of their own, or brings up the one they have. */
|
||||
export function openVariablesButton(host: InspectorHost, owner: VariablesOwner): Button {
|
||||
return new Button('Open', {
|
||||
variant: 'ghost',
|
||||
onClick: () => host.state.openValues({ kind: 'variables', owner }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Not in the schema table either: a row there would share the bag with the declared values. */
|
||||
const PERSISTENT: PropertyDef = { key: 'persistent', label: 'Persistent', kind: 'boolean', default: false };
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ import {
|
||||
type Widget,
|
||||
} from '../../ui';
|
||||
import { ROW_ACTION_ICON_SVG } from '../panels/markIcons';
|
||||
import { InlineList, ListHead } from '../list/InlineList';
|
||||
import type { ListAccess } from '../list/slot';
|
||||
|
||||
/**
|
||||
* What a field needs beyond its own value: the project, to resolve an asset reference to a name
|
||||
@@ -62,6 +64,8 @@ export interface FieldContext {
|
||||
* plain text – only the inspector knows what a click (ping) and a double-click (open) do.
|
||||
*/
|
||||
followReference?(assetId: string, e: FederatedPointerEvent): void;
|
||||
/** Items a list draws inline before "… N more" (#252); none means all of them. */
|
||||
listLimit?: number;
|
||||
}
|
||||
|
||||
export interface FieldHandlers {
|
||||
@@ -69,8 +73,11 @@ export interface FieldHandlers {
|
||||
onInput: (value: PropertyValue) => void;
|
||||
/** Final value – commit as a command. */
|
||||
onChange: (value: PropertyValue) => void;
|
||||
/** Sends a list to the List tab; only the caller knows where the value it edits lives. */
|
||||
openList?: () => void;
|
||||
/**
|
||||
* The list a `list` row draws inline (#252), re-resolved on every call; only the caller knows
|
||||
* where the value it edits lives. Without it the row only says how many items there are.
|
||||
*/
|
||||
list?: () => ListAccess | null;
|
||||
/**
|
||||
* Handlers for one field of a struct, `path` being the keys from the bundle down to it (a struct
|
||||
* field nests further, #239). Writing a field means writing the whole bundle, so only the caller –
|
||||
@@ -250,7 +257,7 @@ export function createFieldRow(
|
||||
retarget(value);
|
||||
|
||||
return {
|
||||
row,
|
||||
row: withBelow(row, field, 1),
|
||||
setValue: (v) => {
|
||||
field.setValue(v);
|
||||
retarget(v);
|
||||
@@ -294,6 +301,32 @@ export interface Field {
|
||||
setValue(value: PropertyValue): void;
|
||||
/** The per-field widgets of a struct, so a caller can address one of them. */
|
||||
parts?: ReadonlyMap<string, Widget>;
|
||||
/** What goes under the row rather than into it: a list's items (#252). */
|
||||
below?: Widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* The row alone, or – for a field with something below it – the row with that under it, `depth`
|
||||
* indent steps in.
|
||||
*/
|
||||
export function withBelow(row: Widget, field: Field, depth: number): Widget {
|
||||
if (!field.below) return row;
|
||||
const stack = new VStack({ gap: Theme.gap });
|
||||
stack.add(row);
|
||||
stack.add(indentedBy(field.below, depth));
|
||||
return stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget `depth` indent steps in, as far as a struct field of that depth. A list's items go one
|
||||
* step less: their index column already reads as the step in.
|
||||
*/
|
||||
export function indentedBy(widget: Widget, depth: number): HStack {
|
||||
const steps = widget instanceof InlineList ? Math.max(0, depth - 1) : depth;
|
||||
const line = new HStack({ gap: 0 });
|
||||
if (steps > 0) line.add(new Spacer(steps * (FIELD_INDENT + Theme.gap)));
|
||||
line.add(widget, 1);
|
||||
return line;
|
||||
}
|
||||
|
||||
/** A widget that does not grow gets a spacer after it, so whatever follows sits at the row's end. */
|
||||
@@ -452,11 +485,26 @@ export function createField(
|
||||
return { widget: field, grow: true, setValue: (v) => field.setValue(Number(v)) };
|
||||
}
|
||||
case 'list': {
|
||||
// A board is hundreds of entries: the row says how many and hands the editing to the
|
||||
// List tab, where a row per item has the space for it.
|
||||
const count = (v: PropertyValue) => `${(isList(v) ? v : []).length} items`;
|
||||
const button = new Button(count(value), { variant: 'ghost', onClick: handlers.openList });
|
||||
return { widget: button, grow: true, setValue: (v) => button.setText(count(v)) };
|
||||
const count = (v: PropertyValue) => (isList(v) ? v : []).length;
|
||||
const source = handlers.list;
|
||||
if (!source) {
|
||||
const label = new Label(`${count(value)} items`, { style: 'muted', padding: Theme.fieldPadding });
|
||||
return { widget: label, grow: true, setValue: (v) => label.setText(`${count(v)} items`) };
|
||||
}
|
||||
// The items go under the row: a board is hundreds of entries, so the inspector draws the
|
||||
// first few and the head opens the rest in a tab of its own.
|
||||
const list = new InlineList(ctx, source, { limit: ctx.listLimit });
|
||||
const head = new ListHead(source, (kind) => list.addItem(kind));
|
||||
list.head = head;
|
||||
return {
|
||||
widget: head,
|
||||
grow: true,
|
||||
setValue: (v) => {
|
||||
head.setCount(count(v));
|
||||
list.refresh();
|
||||
},
|
||||
below: list,
|
||||
};
|
||||
}
|
||||
case 'struct': {
|
||||
// A bundle has few fields, so unlike a list they fit under each other right here. Editing
|
||||
@@ -471,7 +519,7 @@ export function createField(
|
||||
const sub = createField(ctx, fieldDef(field), structFieldValue(value, field), own);
|
||||
addValue(row, sub);
|
||||
fields.set(field.key, sub);
|
||||
stack.add(row);
|
||||
stack.add(withBelow(row, sub, 1));
|
||||
}
|
||||
return {
|
||||
widget: stack,
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
buildVariablePortSection,
|
||||
variableChoices,
|
||||
} from './portSection';
|
||||
import { listOpener, previewingHandlers } from './previewHandlers';
|
||||
import { listSource, previewingHandlers } from './previewHandlers';
|
||||
import { buildTransitionPicker, buildTransitionSection } from './transitionSection';
|
||||
import { currentNode, nodeParamValue } from './valueSpecs';
|
||||
|
||||
@@ -162,6 +162,6 @@ function paramHandlers(
|
||||
const key = def.key;
|
||||
return {
|
||||
...previewingHandlers<GraphNode>(host, nodeParamValue(host.state, assetId, nodeId, key)),
|
||||
...listOpener(host.state, def, () => ({ kind: 'node', assetId, nodeId, key })),
|
||||
...listSource(host, def, () => ({ kind: 'node', assetId, nodeId, key })),
|
||||
};
|
||||
}
|
||||
@@ -44,10 +44,10 @@ import {
|
||||
deviatesFromTemplate,
|
||||
resolveSceneObjectProps,
|
||||
} from '../../core/properties/resolve';
|
||||
import { Button, TextField, Theme } from '../../ui';
|
||||
import { Button, HStack, Label, TextField, Theme } from '../../ui';
|
||||
import { createContainer, removeObjects } from '../actions';
|
||||
import { aspectActions, sizeCommand } from './aspectAction';
|
||||
import { defSignature } from './assetSection';
|
||||
import { defSignature, openVariablesButton } from './assetSection';
|
||||
import {
|
||||
actionIf,
|
||||
createFieldRow,
|
||||
@@ -59,7 +59,7 @@ import {
|
||||
import { addGap, addNameField, addNote, addTitle, type InspectorHost } from './host';
|
||||
import type { EditorState, ListSlot } from '../state';
|
||||
import { parentRow } from './parentRow';
|
||||
import { listOpener, previewingHandlers, structFields } from './previewHandlers';
|
||||
import { listSource, previewingHandlers, structFields } from './previewHandlers';
|
||||
import { overrideValue, tilemapSizeValue } from './valueSpecs';
|
||||
import { buildVariable } from './variableRows';
|
||||
import { currentProps, currentRef, deviations, positionMoved, targetOf, viewOf } from './objectQueries';
|
||||
@@ -214,19 +214,37 @@ export function buildObjectSection(host: InspectorHost, holderId: string, object
|
||||
});
|
||||
}
|
||||
|
||||
// Asset properties (or behavior variables) with overrides; the switches are shown above.
|
||||
const values = resolveSceneObjectProps(host.state.project, asset, object);
|
||||
const own = objectRows(host.state.project, asset, object).filter((def) => isRowShown(def, values));
|
||||
if (own.length > 0 || asset.type === 'behavior')
|
||||
addTitle(host, asset.type === 'behavior' ? 'VARIABLES' : 'PROPERTIES');
|
||||
if (own.length === 0 && asset.type === 'behavior') addNote(host, 'The behavior declares no variables.');
|
||||
for (const def of own) propertyRow(host, holderId, objectId, asset, def, values, asset.type === 'behavior');
|
||||
buildOwnRows(host, holderId, objectId);
|
||||
markObjectRows(host, object, asset, target);
|
||||
|
||||
if (asset.type === 'tilemap') gridEdgesSection(host, holderId, object);
|
||||
if (asset.type === 'container') contentSection(host, holderId, object, asset.objects.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset properties (or behavior variables) with overrides; the switches are shown above them. A
|
||||
* behavior's variables also open in a Variables tab (#252), which builds just this part.
|
||||
*/
|
||||
export function buildOwnRows(host: InspectorHost, holderId: string, objectId: string, inTab = false): void {
|
||||
const { project } = host.state;
|
||||
const view = viewOf(project, holderId);
|
||||
const object = view && findObject(view, objectId);
|
||||
const asset = object && findAsset(project, object.assetId);
|
||||
if (!object || !asset) return;
|
||||
const values = resolveSceneObjectProps(project, asset, object);
|
||||
const own = objectRows(project, asset, object).filter((def) => isRowShown(def, values));
|
||||
const behavior = asset.type === 'behavior';
|
||||
if (behavior) {
|
||||
const header = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
header.add(new Label('VARIABLES', { style: 'title' }), 1);
|
||||
if (!inTab) header.add(openVariablesButton(host, { kind: 'object', holderId, objectId }));
|
||||
host.body.add(header);
|
||||
} else if (own.length > 0) addTitle(host, 'PROPERTIES');
|
||||
if (own.length === 0 && behavior) addNote(host, 'The behavior declares no variables.');
|
||||
for (const def of own) propertyRow(host, holderId, objectId, asset, def, values, behavior);
|
||||
if (inTab) markObjectRows(host, object, asset, targetOf(project, holderId, objectId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Grows and shrinks the grid at any edge, one column or row per click, while the painted cells stay
|
||||
* where they are in the scene (#136). The cells belong to the asset, so they move in every other
|
||||
@@ -388,7 +406,7 @@ function propertyRow(
|
||||
const value = values[def.key] ?? def.default;
|
||||
const handlers = {
|
||||
...previewingHandlers<ObjectRef>(host, overrideSpec),
|
||||
...listOpener(host.state, def, () => ({ kind: 'object', holderId, objectId, key: def.key })),
|
||||
...listSource(host, def, () => ({ kind: 'object', holderId, objectId, key: def.key })),
|
||||
...structFields<ObjectRef>(host, def, overrideSpec, fieldSlot),
|
||||
};
|
||||
// A variable reads exactly like it does on its asset, which adds its own rows to the body.
|
||||
|
||||
@@ -9,18 +9,18 @@ import type { Command } from '../../core/commands/Command';
|
||||
import type { PropertyValue } from '../../core/model/Properties';
|
||||
import { type ValuePath, withValueAt } from '../../core/properties/valuePath';
|
||||
import type { EditorState, ListSlot } from '../state';
|
||||
import { type LiveVariables, resolveList } from '../list/slot';
|
||||
import type { FieldHandlers } from './fieldFactory';
|
||||
|
||||
/** What a section knows beyond the state: a Variables asset's values in the run going on. */
|
||||
type Host = { state: EditorState; liveVariables?: LiveVariables };
|
||||
|
||||
/**
|
||||
* The `openList` half of a row's handlers, for the sections that know where their value lives.
|
||||
* Empty for every other kind, so spreading it into the handlers is always safe.
|
||||
* The `list` half of a row's handlers, for the sections that know where their value lives. Empty
|
||||
* for every other kind, so spreading it into the handlers is always safe.
|
||||
*/
|
||||
export function listOpener(
|
||||
state: EditorState,
|
||||
def: PropertyDef,
|
||||
slot: () => ListSlot,
|
||||
): Pick<FieldHandlers, 'openList'> {
|
||||
return def.kind === 'list' ? { openList: () => state.openList(slot()) } : {};
|
||||
export function listSource(host: Host, def: PropertyDef, slot: () => ListSlot): Pick<FieldHandlers, 'list'> {
|
||||
return def.kind === 'list' ? { list: () => resolveList(host.state, slot(), host.liveVariables) } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +29,7 @@ export function listOpener(
|
||||
* field edited undo the first.
|
||||
*/
|
||||
export function structFields<Target>(
|
||||
host: { state: EditorState },
|
||||
host: Host,
|
||||
def: PropertyDef,
|
||||
spec: PreviewedValue<Target>,
|
||||
fieldSlot?: (path: ValuePath) => ListSlot,
|
||||
@@ -38,11 +38,11 @@ export function structFields<Target>(
|
||||
}
|
||||
|
||||
/**
|
||||
* `fieldSlot` is what a `list` field needs: its own address, so the List tab can be opened on it.
|
||||
* `fieldSlot` is what a `list` field needs: its own address, so its items can be drawn and opened.
|
||||
* `path` leads from the bundle to the field, through struct fields as deep as they nest (#239).
|
||||
*/
|
||||
export function structFieldsOf<Target>(
|
||||
host: { state: EditorState },
|
||||
host: Host,
|
||||
def: StructDef,
|
||||
spec: PreviewedValue<Target>,
|
||||
fieldSlot?: (path: ValuePath) => ListSlot,
|
||||
@@ -55,7 +55,9 @@ export function structFieldsOf<Target>(
|
||||
return withValueAt(bundle, path, coerceFieldValue(input, field)) ?? bundle;
|
||||
},
|
||||
}),
|
||||
...(field.kind === 'list' && fieldSlot ? { openList: () => host.state.openList(fieldSlot(path)) } : {}),
|
||||
...(field.kind === 'list' && fieldSlot
|
||||
? { list: () => resolveList(host.state, fieldSlot(path), host.liveVariables) }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { EditorState } from '../state';
|
||||
import { currentProps, currentRef } from './objectQueries';
|
||||
import type { PreviewedValue } from './previewHandlers';
|
||||
|
||||
/** Where one value lives, shared by the inspector and the List tab so both write it the same way. */
|
||||
/** Where one value lives, shared by the inspector rows and the inline lists so both write it the same way. */
|
||||
export function assetValue(state: EditorState, assetId: string, key: string): PreviewedValue<Asset> {
|
||||
return {
|
||||
target: () => findAsset(state.project, assetId),
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type FieldHandlers,
|
||||
fieldDef,
|
||||
type FieldRow,
|
||||
indentedBy,
|
||||
MARK_WIDTH,
|
||||
nameWidth,
|
||||
READ_ONLY,
|
||||
@@ -77,16 +78,20 @@ export function buildVariable(
|
||||
const name = nameCell(def.key, 0, edit && ((next) => edit.rename(next)));
|
||||
const head = gridRow(0, markLabel(def.kind), name);
|
||||
let setValue: (next: PropertyValue) => void = () => {};
|
||||
let below: Widget | undefined;
|
||||
if (def.kind === 'struct') {
|
||||
head.add(new Spacer(), 1);
|
||||
if (edit) head.add(addFieldMenu(edit, []));
|
||||
} else {
|
||||
setValue = addValue(head, createField(host, def, value, handlers));
|
||||
const field = createField(host, def, value, handlers);
|
||||
setValue = addValue(head, field);
|
||||
below = field.below;
|
||||
}
|
||||
if (keep) head.add(keepButton(keep));
|
||||
if (edit) head.add(removeButton(edit.remove));
|
||||
const refreshActions = actions ? attachRowActions(head, actions) : () => {};
|
||||
host.body.add(head);
|
||||
if (below) host.body.add(indentedBy(below, 1));
|
||||
|
||||
if (def.kind === 'struct') {
|
||||
if (def.fields.length === 0) addNote(host, 'No fields yet.', 'small');
|
||||
@@ -141,9 +146,11 @@ function structFieldRows(
|
||||
continue;
|
||||
}
|
||||
const own = handlers.structFieldHandlers?.([...path, field.key], field) ?? READ_ONLY;
|
||||
setters.set(field.key, addValue(row, createField(host, fieldDef(field), item, own)));
|
||||
const sub = createField(host, fieldDef(field), item, own);
|
||||
setters.set(field.key, addValue(row, sub));
|
||||
if (edit) row.add(removeButton(() => edit.removeField(path, field)));
|
||||
host.body.add(row);
|
||||
if (sub.below) host.body.add(indentedBy(sub.below, path.length + 2));
|
||||
}
|
||||
return (next) => {
|
||||
for (const field of fields) setters.get(field.key)?.(structFieldValue(next, field));
|
||||
@@ -183,7 +190,7 @@ export function markLabel(kind: string): Widget {
|
||||
}
|
||||
|
||||
/**
|
||||
* A kind mark that can be clicked – the List tab's way of saying what an item holds. The label may
|
||||
* A kind mark that can be clicked – a list's way of saying what an item holds. The label may
|
||||
* take the button's whole width: the column is sized for its widest mark, and the usual inset
|
||||
* would cut `Aa` to `A…`.
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user