Give every behavior variable one row shape on asset and instance
buildStructBlock only covered structs, so a placed instance drew numbers,
text, bools and lists through createFieldRow on a 64 px name column while
its struct block sat on the asset's grid below it. variableRows.ts now
builds the row for every variable kind on both sides; the edit option
decides what the owner may change (asset: rename, x, item kind; instance:
revert and push). Label and Button take a fixed width so the mark column
no longer shifts names when a list shows its item-kind picker.
The two structFields calls that became dead with 9f48257 are gone. The
e2e steps locate the Enabled toggle through __gm.inspectorField instead
of clicking a fixed offset from its label.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9f482573e5
commit
631cf54c72
17 files changed
+515
-472
No files matched your search
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* A variable is drawn by one builder on the asset that declares it and on a placed object, and
|
||||
* every row of the inspector shares one column grid. The asset may rename and reshape; a placed
|
||||
* object may not, so there the names are labels. `NumberField extends TextField`, so counting
|
||||
* editable widgets by type would count the value fields too: the tests go by the name's widget.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createEmptyProject } from '../../src/core/model/Project';
|
||||
import type { PropertyDef, StructDef } from '../../src/core/properties/PropertySchema';
|
||||
import { HStack, Label, TextField, Theme, VStack, type Widget } from '../../src/ui';
|
||||
import { type FieldHandlers, labelledRow } from '../../src/editor/inspector/fieldFactory';
|
||||
import type { InspectorHost } from '../../src/editor/inspector/host';
|
||||
import { buildVariable, type VariableEditing } from '../../src/editor/inspector/variableRows';
|
||||
import { EditorState } from '../../src/editor/state';
|
||||
|
||||
const STRUCT: StructDef = {
|
||||
key: 'piece',
|
||||
label: 'piece',
|
||||
kind: 'struct',
|
||||
default: { x: 0, tag: '' },
|
||||
fields: [
|
||||
{ key: 'x', kind: 'number' },
|
||||
{ key: 'tag', kind: 'text' },
|
||||
],
|
||||
};
|
||||
|
||||
const VALUE = { x: 30, tag: 'tee' };
|
||||
|
||||
const PLAIN: PropertyDef[] = [
|
||||
{ key: 'speed', label: 'speed', kind: 'number', default: 0 },
|
||||
{ key: 'title', label: 'title', kind: 'text', default: '' },
|
||||
{ key: 'alive', label: 'alive', kind: 'boolean', default: true },
|
||||
{ key: 'cells', label: 'cells', kind: 'list', default: [], items: 'text' },
|
||||
{ key: 'a_rather_long_variable_name', label: 'a_rather_long_variable_name', kind: 'number', default: 0 },
|
||||
];
|
||||
|
||||
function makeHost(): InspectorHost {
|
||||
const state = new EditorState();
|
||||
state.loadProject(createEmptyProject('p', 'P'), 1);
|
||||
return { state, body: new VStack(), fields: new Map(), nameField: null, project: () => state.project };
|
||||
}
|
||||
|
||||
const noHandlers = (): FieldHandlers => ({ onInput: () => {}, onChange: () => {} });
|
||||
const withFields: FieldHandlers = { ...noHandlers(), structFieldHandlers: noHandlers };
|
||||
|
||||
const EDIT: VariableEditing = {
|
||||
rename: () => {},
|
||||
remove: () => {},
|
||||
setItems: () => {},
|
||||
renameField: () => {},
|
||||
removeField: () => {},
|
||||
addField: () => {},
|
||||
};
|
||||
|
||||
const rows = (host: InspectorHost): Widget[][] =>
|
||||
host.body.children.map((row) => ('children' in row ? (row.children as Widget[]) : []));
|
||||
|
||||
/** Every widget of the body that carries `text` as its own content, name or value. */
|
||||
const carrying = (host: InspectorHost, text: string): Widget[] =>
|
||||
rows(host)
|
||||
.flat()
|
||||
.filter((w) => (w instanceof Label && w.text === text) || (w instanceof TextField && w.value === text));
|
||||
|
||||
const isName = (w: Widget, name: string) =>
|
||||
(w instanceof Label && w.text === name) || (w instanceof TextField && w.value === name);
|
||||
|
||||
/** Where the widget after the name starts – summed from the measured cells before it. */
|
||||
function valueX(host: InspectorHost, name: string): number {
|
||||
for (const row of host.body.children) {
|
||||
if (!(row instanceof HStack)) continue;
|
||||
const items = row.items;
|
||||
const at = items.findIndex((w) => isName(w, name));
|
||||
if (at < 0) continue;
|
||||
return items.slice(0, at + 1).reduce((x, w) => x + w.measure().w + row.gap, row.padding);
|
||||
}
|
||||
throw new Error(`no row named ${name}`);
|
||||
}
|
||||
|
||||
function buildAll(edit?: VariableEditing): InspectorHost {
|
||||
const host = makeHost();
|
||||
for (const def of PLAIN) buildVariable(host, def, def.default, noHandlers(), { edit });
|
||||
buildVariable(host, STRUCT, VALUE, withFields, { edit });
|
||||
return host;
|
||||
}
|
||||
|
||||
const NAMES = [...PLAIN.map((d) => d.key), 'x', 'tag'];
|
||||
|
||||
describe('the column grid', () => {
|
||||
it('starts every value at the same x on a placed object, whatever the kind or depth', () => {
|
||||
const host = buildAll();
|
||||
const xs = NAMES.map((name) => valueX(host, name));
|
||||
expect(new Set(xs), JSON.stringify(xs)).toEqual(new Set([xs[0]]));
|
||||
});
|
||||
|
||||
it('and at that same x on the asset, where names are fields and the list mark is a picker', () => {
|
||||
const placed = valueX(buildAll(), 'speed');
|
||||
const host = buildAll(EDIT);
|
||||
const xs = NAMES.map((name) => valueX(host, name));
|
||||
expect(xs, JSON.stringify(xs)).toEqual(NAMES.map(() => placed));
|
||||
});
|
||||
|
||||
it('which is where a plain labelled row starts its value too', () => {
|
||||
const { label } = labelledRow('Enabled');
|
||||
expect(label.minWidth + Theme.gap).toBe(valueX(buildAll(), 'speed'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('a struct on a placed object', () => {
|
||||
it('is a head row plus one row per field', () => {
|
||||
const host = makeHost();
|
||||
buildVariable(host, STRUCT, VALUE, withFields);
|
||||
expect(host.body.children.length).toBe(1 + STRUCT.fields.length);
|
||||
});
|
||||
|
||||
it('names the bundle and its fields with labels, so they cannot be edited', () => {
|
||||
const host = makeHost();
|
||||
buildVariable(host, STRUCT, VALUE, withFields);
|
||||
for (const name of ['piece', 'x', 'tag']) {
|
||||
const [widget] = carrying(host, name);
|
||||
expect(widget, name).toBeInstanceOf(Label);
|
||||
}
|
||||
});
|
||||
|
||||
it('still shows the values in editable fields', () => {
|
||||
const host = makeHost();
|
||||
buildVariable(host, STRUCT, VALUE, withFields);
|
||||
expect(carrying(host, 'tee')[0]).toBeInstanceOf(TextField);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the same struct on the asset that declares it', () => {
|
||||
it('names the bundle and its fields with text fields, so they can be renamed', () => {
|
||||
const host = makeHost();
|
||||
buildVariable(host, STRUCT, VALUE, withFields, { edit: EDIT });
|
||||
for (const name of ['piece', 'x', 'tag']) {
|
||||
const [widget] = carrying(host, name);
|
||||
expect(widget, name).toBeInstanceOf(TextField);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the same row count as the placed one', () => {
|
||||
const host = makeHost();
|
||||
buildVariable(host, STRUCT, VALUE, withFields, { edit: EDIT });
|
||||
expect(host.body.children.length).toBe(1 + STRUCT.fields.length);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user