The instance inspector drew its own, flatter version of a struct: one row per field, no bundle head. It now draws the same block as the asset side, because there is only one implementation left - both call buildStructBlock. What the instance does not get is everything that touches the shape: renaming a field, '+ Field', and the per-field '×'. The shape belongs to the asset; a placed object overrides values. The revert and override marks sit on the head rather than on a field, since an override covers the whole bundle at once. Underneath this, editing a single field of an unoverridden struct used to blank its siblings: structFieldsOf filled the untouched fields from spec.read, which is object.overrides[key] and therefore undefined until something is overridden. structOf then handed back the empty shape. PreviewedValue carries 'current' now - the resolved value the row actually shows - and the fields come from that. Without it the old path stays, which is right for the asset side, whose prop bag is dense.
86 lines
3.4 KiB
TypeScript
86 lines
3.4 KiB
TypeScript
/**
|
||
* A struct is drawn as a block – a head naming the bundle, a row per field – in both places that
|
||
* show one. The asset that declares it may rename and add fields; 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 below go by which widget carries the name.
|
||
*/
|
||
import { describe, expect, it } from 'vitest';
|
||
import { createEmptyProject } from '../../src/core/model/Project';
|
||
import type { StructDef } from '../../src/core/properties/PropertySchema';
|
||
import { Label, TextField, VStack, type Widget } from '../../src/ui';
|
||
import type { InspectorHost } from '../../src/editor/inspector/host';
|
||
import { buildStructBlock } from '../../src/editor/inspector/structBlock';
|
||
import { EditorState } from '../../src/editor/state';
|
||
|
||
const DEF: 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' };
|
||
|
||
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 = () => ({ onInput: () => {}, onChange: () => {} });
|
||
|
||
const rows = (host: InspectorHost): Widget[][] =>
|
||
host.body.children.map((row) => ('children' in row ? (row.children as Widget[]) : []));
|
||
|
||
/** Every widget of the block 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 noop = { renameStruct: () => {}, removeStruct: () => {}, renameField: () => {}, removeField: () => {} };
|
||
|
||
describe('a struct block on a placed object', () => {
|
||
it('is a head row plus one row per field', () => {
|
||
const host = makeHost();
|
||
buildStructBlock(host, DEF, VALUE, noHandlers);
|
||
expect(host.body.children.length).toBe(1 + DEF.fields.length);
|
||
});
|
||
|
||
it('names the bundle and its fields with labels, so they cannot be edited', () => {
|
||
const host = makeHost();
|
||
buildStructBlock(host, DEF, VALUE, noHandlers);
|
||
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();
|
||
buildStructBlock(host, DEF, VALUE, noHandlers);
|
||
expect(carrying(host, 'tee')[0]).toBeInstanceOf(TextField);
|
||
});
|
||
});
|
||
|
||
describe('the same block on the asset that declares it', () => {
|
||
it('names the bundle and its fields with text fields, so they can be renamed', () => {
|
||
const host = makeHost();
|
||
buildStructBlock(host, DEF, VALUE, noHandlers, { edit: { ...noop, addField: () => {} } });
|
||
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();
|
||
buildStructBlock(host, DEF, VALUE, noHandlers, { edit: { ...noop, addField: () => {} } });
|
||
expect(host.body.children.length).toBe(1 + DEF.fields.length);
|
||
});
|
||
});
|