Show a placed object's struct the way its asset does
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.
This commit is contained in:
1 parent
14f542365e
commit
9f482573e5
9 files changed
+606
-207
No files matched your search
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user