Files
feuerwerk/src/editor/inspector/variableRows.ts
T
rootandClaude Opus 5 3ed8b68ecf Describe every value kind in one table instead of spreading it over the code
A kind like colour or texture used to be spelled out in five parallel
lists, its port type, its empty value, its glyph, its label and its list
tag - 22 literal places for texture alone. core/properties/kinds.ts now
holds one row per kind and the rest is derived from it, so adding a kind
is one row. The five literal nodes collapse into literals.ts, the six
kind menus into one helper.

Number, Text, Boolean is now the one order everywhere; the variable and
port menus used to list Boolean before Text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:47:17 +02:00

171 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { PropertyValue } from '../../core/model/Properties';
import { kindLabel, kindMark, STRUCT_FIELD_KINDS, type StructFieldKind } from '../../core/properties/kinds';
import type { PropertyDef, StructDef, StructFieldDef } from '../../core/properties/PropertySchema';
import {
Button,
Dropdown,
HStack,
Label,
Menu,
type MenuItem,
Spacer,
TextField,
Theme,
type Widget,
} from '../../ui';
import {
addValue,
attachRowActions,
createField,
FIELD_INDENT,
type FieldHandlers,
fieldDef,
type FieldRow,
MARK_WIDTH,
NAME_WIDTH,
READ_ONLY,
removeButton,
type RowActions,
structFieldValue,
} from './fieldFactory';
import { addNote, type InspectorHost } from './host';
/** The menu every "add a kind" button opens: one entry per kind, named as the kind table names it. */
export function kindMenu<K extends string>(kinds: readonly K[], pick: (kind: K) => void): MenuItem[] {
return kinds.map((kind) => ({ label: kindLabel(kind), onSelect: () => pick(kind) }));
}
/** Changing a declaration – offered only on the asset that declares it. */
export interface VariableEditing {
rename: (next: string) => void;
remove: () => void;
renameField: (field: StructFieldDef, next: string) => void;
removeField: (field: StructFieldDef) => void;
addField: (kind: StructFieldKind) => void;
}
export interface VariableOptions {
edit?: VariableEditing;
/** ↺/↑ on the first row: an override covers a whole struct, so its fields share them. */
actions?: RowActions;
}
/**
* One variable, drawn the same on its asset and on a placed object – minus `edit` there, since the
* shape belongs to the asset. Adds its rows to the body itself and returns the first.
*/
export function buildVariable(
host: InspectorHost,
def: PropertyDef,
value: PropertyValue,
handlers: FieldHandlers,
options: VariableOptions = {},
): FieldRow {
const { edit, actions } = options;
const name = nameCell(def.key, 0, edit && ((next) => edit.rename(next)));
const head = gridRow(0, markLabel(kindMark(def.kind)), name);
let setValue: (next: PropertyValue) => void = () => {};
if (def.kind === 'struct') {
head.add(new Spacer(), 1);
if (edit) head.add(addFieldMenu(STRUCT_FIELD_KINDS, edit));
} else {
setValue = addValue(head, createField(host, def, value, handlers));
}
if (edit) head.add(removeButton(edit.remove));
const refreshActions = actions ? attachRowActions(head, actions) : () => {};
host.body.add(head);
if (def.kind === 'struct') setValue = structFieldRows(host, def, value, handlers, edit);
return {
row: head,
setValue: (next) => setValue(next),
setModified: (modified) => {
if (name instanceof Label) name.setStyle(modified ? 'override' : 'muted');
refreshActions();
},
};
}
/** One row per field, each with its own value; returns the setter that pushes a bundle into them. */
function structFieldRows(
host: InspectorHost,
def: StructDef,
value: PropertyValue,
handlers: FieldHandlers,
edit: VariableEditing | undefined,
): (next: PropertyValue) => void {
if (def.fields.length === 0) addNote(host, 'No fields yet.', 'small');
const setters = new Map<string, (item: PropertyValue) => void>();
for (const field of def.fields) {
const row = fieldRow(field, edit);
const item = createField(
host,
fieldDef(field),
structFieldValue(value, field),
handlers.structFieldHandlers?.(field) ?? READ_ONLY,
);
setters.set(field.key, addValue(row, item));
if (edit) row.add(removeButton(() => edit.removeField(field)));
host.body.add(row);
}
return (next) => {
for (const field of def.fields) setters.get(field.key)?.(structFieldValue(next, field));
};
}
function fieldRow(field: StructFieldDef, edit: VariableEditing | undefined): HStack {
const name = nameCell(field.key, 1, edit && ((next) => edit.renameField(field, next)));
return gridRow(1, markLabel(kindMark(field.kind)), name);
}
/** Indent, mark and name – sized so that whatever follows starts on the value column at any depth. */
function gridRow(depth: number, mark: Widget, name: Widget): HStack {
const row = new HStack({ gap: Theme.gap, align: 'center' });
row.minHeight = Theme.rowHeight;
if (depth > 0) row.add(new Spacer(FIELD_INDENT));
row.add(mark);
row.add(name);
return row;
}
function nameWidth(depth: number): number {
return NAME_WIDTH - depth * (FIELD_INDENT + Theme.gap);
}
/** A text field where the name may be changed, else a label whose text sits where the field's would. */
function nameCell(text: string, depth: number, rename: ((next: string) => void) | undefined): Widget {
const width = nameWidth(depth);
return rename
? new TextField(text, { width, onChange: (next) => rename(next.trim()) })
: new Label(text, { style: 'muted', width, padding: Theme.fieldPadding });
}
export function markLabel(glyph: string): Label {
return new Label(glyph, { style: 'small', align: 'center', width: MARK_WIDTH });
}
/**
* A kind mark that can be clicked – the List tab's way of saying what an item holds. The label may
* take the button's whole width: the column is sized for its widest glyph, and the usual inset
* would cut `Aa` to `A…`.
*/
class MarkButton extends Button {
protected override labelSlot(): number {
return this.w;
}
}
/** The mark of a kind, as a button that offers the others. */
export function markPicker(glyph: string, items: MenuItem[]): Button {
const picker = new MarkButton(glyph, { variant: 'ghost', width: MARK_WIDTH, style: 'small' });
picker.onClick = () => new Menu(items).openBelow(picker);
return picker;
}
function addFieldMenu(kinds: readonly StructFieldKind[], edit: VariableEditing): Dropdown {
const add = new Dropdown('+ Field', { variant: 'ghost' });
add.onClick = () => new Menu(kindMenu(kinds, (kind) => edit.addField(kind))).openBelow(add);
return add;
}