Head the groups of the inspector with bands that fold (#290)
game-maker / gate (push) Successful in 2m39s
game-maker / suite (push) Successful in 13m10s

The groups of the inspector had a small grey label as their title, in
the same grid as a property row, so with many rows one group hardly
stood apart from the next. Now every group has a band as its head,
from edge to edge of the panel, and a click on it folds the group.

- A band runs across the padding of its column in the header colour,
  with the title and a mark that says whether the group is open. The
  titles stay as they are, as answered in the issue.
- A click on a band folds its group: the rows go, the band stays and
  the next band moves up against it. A control in the band (Open,
  + Variable, + Property) does its own job and folds nothing.
- A placed object has a group SWITCHES under its name, asset and
  parent: Enabled, Keep on scene load and the switches of a behavior.
  An asset has SWITCHES and PROPERTIES, and a group without rows is
  left out.
- The same bands head the tilemap panel, the Variables tab, the side
  of the Pixel art tab and the shape list of vector art. A list with a
  title (Layers, Frames, Shapes) folds with a click on its header,
  clear of its plus.
- ui/sections.ts holds the rules as pure functions, SectionStack is
  the column and SectionHeader the band. Every title of the inspector
  goes through addTitle or sectionHeader in inspector/host.ts.

Decided here:

- A fold is kept for good, in localStorage of this browser. The issue
  asked whether for the session or for good and the answer left it
  open.
- A fold is remembered by title, per panel. PROPERTIES folded once is
  folded on every object, asset and node. A number in a title does
  not count.
- A behavior asset has all its rows under SWITCHES. A Variables asset
  and a state machine get a group SCOPE.
- A greyed out control in a band takes the click too. Without that the
  click fell through to the band and folded the group.
- The Curve tab has no column of this kind and is unchanged.

Found on the way:

- The inspector of a sprite is taller than the 800 pixel window of
  the checks now. The hooks that hand out a row scroll it into view
  first, and module 28 scrolls before it looks for a row by its text.

Tests:
- Gate: 202 files, 3008 tests (2982 + 26), exit 0
- e2e: no complete run for this state. Modules 28 to 61 passed alone
  (749 checks). A full run was cut off in the accounts module at 1492
  passes without a failure, the modules after it and the smokes are
  missing. The sum of 1639 (1635 + 4) is reckoned, CI measures it.
- 26 unit tests in tests/editor/sections.test.ts
- Four e2e checks with a real mouse, three in module 02 and one in
  module 48
- 25 unit counter-probes all fail tests, run on stein2
- Screenshots of the bands looked at before the last build, none
  after it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-01 22:08:28 +02:00
1 parent c5ba21533d
commit 393caadc9d
22 files changed
+1072 -53

No files matched your search

+7
View File
@@ -46,6 +46,13 @@ export function pixelArtHooks({ state, editor }: HookContext) {
const at = list.addGlobal();
return at ? { ...at, enabled: true, text: '+' } : null;
}
// Nor is the band it stands in (#290): where a click folds the list, and whether it is folded.
const headed = name === 'layer:head' ? side.layers : name === 'frame:head' ? side.frames : null;
if (headed) {
side.reveal(headed);
const at = headed.headGlobal();
return at ? { ...at, h: headed.h, enabled: true, text: '', folded: headed.sectionCollapsed } : null;
}
const widget = byName[name];
// A tool's options hide in their row (#201), so a hidden parent hides the control as well.
for (let at: Container | null = widget ?? null; at; at = at.parent) if (!at.visible) return null;
+33 -2
View File
@@ -1,5 +1,15 @@
import type { AssetType } from '../../core/model/AssetTypes';
import { Button, Label, Panel, TextField, Widget, activeDrag, currentMenu, ui } from '../../ui';
import {
Button,
Label,
Panel,
SectionHeader,
TextField,
Widget,
activeDrag,
currentMenu,
ui,
} from '../../ui';
import { Theme } from '../../ui/Theme';
import { currentFontPicker } from '../font/FontPicker';
import { currentGroupDialog } from '../panels/GroupNameDialog';
@@ -59,7 +69,10 @@ export function uiHooks({ app, state, editor }: HookContext) {
const row = owner?.parent;
if (!row || !owner) return null;
const control = row.children[row.children.indexOf(owner) + 1];
return control instanceof Widget ? control : null;
if (!(control instanceof Widget)) return null;
// Taller than the window since its groups have bands (#290): the row is scrolled into view.
inspector.reveal(control);
return control;
};
/**
* The visible text of the overlay layer, in drawing order – everything an open popup shows.
@@ -169,6 +182,7 @@ export function uiHooks({ app, state, editor }: HookContext) {
walk(inspector);
const label = found as Label | null;
if (!label) return null;
inspector.reveal(label);
const shown = label.children.find((child): child is Text => child instanceof Text);
const row = label.parent;
const next = row?.children[row.children.indexOf(label) + 1];
@@ -180,6 +194,23 @@ export function uiHooks({ app, state, editor }: HookContext) {
pressable: label.eventMode === 'static',
};
},
/**
* The band of a group of rows (#290), by its title: where to click it clear of its controls, its
* rectangle and whether the group is folded. The first one showing, or the last with `last`.
*/
section: (title: string, last = false) => {
const found: SectionHeader[] = [];
const walk = (node: Container): void => {
if (!node.visible) return;
if (node instanceof SectionHeader && node.title === title) found.push(node);
for (const child of node.children) walk(child);
};
walk(app.stage);
const header = last ? found.at(-1) : found[0];
if (!header) return null;
const rect = rectOf(header);
return { x: rect.x + 60, y: rect.y + rect.h / 2, rect, collapsed: header.sectionCollapsed };
},
/** The inspector panel on screen – what tells whether a row of it can be clicked at all. */
inspectorRect: () => rectOf(inspector),
/** Where the focused widget sits on screen, or `null` – for checks on its focus border. */
+16 -2
View File
@@ -1,6 +1,6 @@
import type { PropertyBag } from '../../core/model/Properties';
import type { FederatedPointerEvent } from 'pixi.js';
import { Label, Panel, pressCount, ScrollContainer, Theme, ui, VStack, type Widget } from '../../ui';
import { Label, Panel, pressCount, ScrollContainer, SectionStack, Theme, ui, type Widget } from '../../ui';
import { activateAsset } from '../actions';
import { type EditorState, sameSelection, type Selection } from '../state';
import { assetSignature, buildAssetSection, refreshAssetSection } from './assetSection';
@@ -77,12 +77,26 @@ export class Inspector extends Panel {
return { rebuilds: this.rebuilds, postponed: this.rebuildWhenLeft };
}
/**
* Scrolls until `widget` is in view, when it is one of the rows in here – the check scripts
* reveal a row before they click it, as a user scrolls to it.
*/
reveal(widget: Widget): void {
let inside = false;
for (let at = widget.parent; at && !inside; at = at.parent) inside = at === this.host.body;
if (!inside) return;
const top = widget.getGlobalPosition().y - this.host.body.getGlobalPosition().y;
this.scroll.ensureVisible(top, widget.h);
}
/** For the check scripts: every list drawn inline in here, at any depth. */
lists(): InlineList[] {
return inlineListsIn(this.host.body);
}
private emptyHost(): InspectorHost {
const body = new SectionStack({ padding: Theme.padding, gap: Theme.gap });
body.onFold = () => this.scroll.relayoutContent();
return {
state: this.state,
// Read through the state on every call: load and play mode replace the project object.
@@ -90,7 +104,7 @@ export class Inspector extends Panel {
liveVariables: (assetId) => this.liveVariables(assetId),
followReference: (assetId, e) => this.followReference(assetId, e),
listLimit: INLINE_LIMIT,
body: new VStack({ padding: Theme.padding, gap: Theme.gap }),
body,
fields: new Map(),
nameField: null,
};
+27 -8
View File
@@ -42,6 +42,7 @@ import type { PropertyBag, PropertyValue } from '../../core/model/Properties';
import { readScope, readSharedScope, type Scope, scopeOf, sharedScopeOf } from '../../core/model/Scope';
import { type NestedKind, VARIABLE_KINDS } from '../../core/properties/kinds';
import {
COMMON_PROPS,
isRowShown,
type PropertyDef,
schemaForAsset,
@@ -49,14 +50,14 @@ import {
} from '../../core/properties/PropertySchema';
import { markedAssetSchema } from '../../core/properties/deviation';
import { isModified, resolveAssetProps } from '../../core/properties/resolve';
import { Button, Dropdown, HStack, Label, Menu, type MenuEntry, Theme } from '../../ui';
import { Button, Dropdown, Label, Menu, type MenuEntry } 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 { addNameField, addNote, addTitle, type InspectorHost, sectionHeader } from './host';
import { buildShapeListSection, shapeListSignature } from './shapeSection';
import { buildVariable, kindMenu, type StructTyping, type VariableEditing } from './variableRows';
import { listSource, previewingHandlers, structFields } from './previewHandlers';
@@ -131,13 +132,27 @@ export function buildAssetSection(host: InspectorHost, assetId: string): void {
},
});
buildProps(host, asset, markedAssetSchema(host.state.project, asset));
// The switches in a group of their own, as on a placed object (#290). A behavior's rows are all
// switches: what it declares is listed under VARIABLES.
const schema = markedAssetSchema(host.state.project, asset);
const switches = schema.filter((def) => asset.type === 'behavior' || isSwitch(def.key));
buildProps(host, asset, switches, 'SWITCHES');
buildProps(
host,
asset,
schema.filter((def) => !switches.includes(def)),
'PROPERTIES',
);
if (isVariableOwner(asset)) buildVariables(host, asset);
if (asset.type === 'variables') {
addTitle(host, 'SCOPE');
buildVariablesScope(host, asset);
buildPersistence(host, asset);
}
if (asset.type === 'statemachine') buildMachineScope(host, asset);
if (asset.type === 'statemachine') {
addTitle(host, 'SCOPE');
buildMachineScope(host, asset);
}
// The drawing itself: its shapes are picked here as a texture's frames are picked in its tab (#129).
if (asset.type === 'vectorart') buildShapeListSection(host, asset);
// A font has no properties at all, so without this its inspector would be the name and nothing.
@@ -166,10 +181,15 @@ export function refreshAssetSection(host: InspectorHost, assetId: string): boole
return true;
}
function buildProps(host: InspectorHost, asset: Asset, schema: readonly PropertyDef[]): void {
const isSwitch = (key: string): boolean => COMMON_PROPS.some((def) => def.key === key);
/** The rows of `schema` that are shown, under `title` – which is left out with them. */
function buildProps(host: InspectorHost, asset: Asset, schema: readonly PropertyDef[], title: string): void {
const project = () => host.state.project;
const values = resolveAssetProps(project(), asset);
for (const def of schema.filter((d) => isRowShown(d, values))) {
const shown = schema.filter((d) => isRowShown(d, values));
if (shown.length > 0) addTitle(host, title);
for (const def of shown) {
const row = createFieldRow(
host,
def,
@@ -238,8 +258,7 @@ function fieldsAt(fields: readonly StructFieldDef[], path: readonly string[]): r
*/
export function buildVariables(host: InspectorHost, asset: VariableOwnerAsset, inTab = false): void {
const isType = asset.type === 'struct';
const header = new HStack({ gap: Theme.gap, align: 'center' });
header.add(new Label(isType ? 'FIELDS' : 'VARIABLES', { style: 'title' }), 1);
const header = sectionHeader(host, isType ? 'FIELDS' : 'VARIABLES');
const add = new Dropdown(isType ? '+ Field' : '+ Variable', { variant: 'ghost' });
const run = (command: Command, rename?: FieldRename): void =>
void host.state.execute(isType ? new WithStructTypes(command, rename) : command);
+11 -2
View File
@@ -1,5 +1,6 @@
import type { PropertyBag } from '../../core/model/Properties';
import { Label, TextField, type TextFieldOptions, type VStack } from '../../ui';
import { Label, SectionHeader, TextField, type TextFieldOptions, type VStack } from '../../ui';
import { foldKey, isFolded, setFolded } from '../sectionFolds';
import type { EditorState } from '../state';
import { type FieldContext, type FieldRow, labelledRow } from './fieldFactory';
@@ -13,10 +14,18 @@ export interface InspectorHost extends FieldContext {
readonly fields: Map<string, FieldRow>;
/** The name field at the top, when the selection has a name. */
nameField: TextField | null;
/** Names the panel where its folded groups are remembered (#290); the inspector's when left out. */
readonly foldScope?: string;
}
/** The band that opens a group of rows; controls go to its right end with `add()`. Not yet in the body. */
export function sectionHeader(host: InspectorHost, text: string): SectionHeader {
const key = foldKey(host.foldScope ?? 'inspector', text);
return new SectionHeader(text, { collapsed: isFolded(key), onToggle: (on) => setFolded(key, on) });
}
export function addTitle(host: InspectorHost, text: string): void {
host.body.add(new Label(text, { style: 'title' }));
host.body.add(sectionHeader(host, text));
}
export function addNote(
+5 -5
View File
@@ -46,7 +46,7 @@ import {
deviatesFromTemplate,
resolveSceneObjectProps,
} from '../../core/properties/resolve';
import { Button, HStack, Label, TextField, Theme } from '../../ui';
import { Button, TextField, Theme } from '../../ui';
import { createContainer, removeObjects } from '../actions';
import { aspectActions, sizeCommand } from './aspectAction';
import { defSignature, openVariablesButton } from './assetSection';
@@ -58,7 +58,7 @@ import {
linkLabel,
type RowActions,
} from './fieldFactory';
import { addGap, addNameField, addNote, addTitle, type InspectorHost } from './host';
import { addGap, addNameField, addNote, addTitle, type InspectorHost, sectionHeader } from './host';
import type { EditorState, ListSlot } from '../state';
import { parentRow } from './parentRow';
import { listSource, previewingHandlers, structFields } from './previewHandlers';
@@ -165,7 +165,8 @@ export function buildObjectSection(host: InspectorHost, holderId: string, object
// Content of a placed container: values compare against the template, not just the asset.
if (target) assetLink(host, PART_OF_ROW, 'Part of', target.asset);
// The master switch, right where it is looked for: above everything type-specific.
// The switches in a group of their own (#290), above everything type-specific.
addTitle(host, 'SWITCHES');
const resolved = resolveSceneObjectProps(host.state.project, asset, object);
// Only a top-level object of a scene travels through a `Load scene` (#165), so only there is the row.
const travels = object.parentId === '' && findAsset(project, holderId)?.type === 'scene';
@@ -252,8 +253,7 @@ export function buildOwnRows(host: InspectorHost, holderId: string, objectId: st
host.objectPicker = objectPicker(host.state, holderId, objectId, host.objectChoices);
const behavior = asset.type === 'behavior';
if (behavior) {
const header = new HStack({ gap: Theme.gap, align: 'center' });
header.add(new Label('VARIABLES', { style: 'title' }), 1);
const header = sectionHeader(host, 'VARIABLES');
if (!inTab) header.add(openVariablesButton(host, { kind: 'object', holderId, objectId }));
host.body.add(header);
} else if (own.length > 0) addTitle(host, 'PROPERTIES');
+5 -6
View File
@@ -37,13 +37,14 @@ import {
Label,
Menu,
type MenuItem,
type SectionHeader,
Spacer,
TextField,
Theme,
type Widget,
} from '../../ui';
import { NAME_WIDTH, removeButton } from './fieldFactory';
import { addNote, type InspectorHost } from './host';
import { addNote, type InspectorHost, sectionHeader } from './host';
import { kindMenu, markLabel } from './variableRows';
/** Whose declarations a Variables node picks its ports from: the asset it names, else the behavior. */
@@ -164,8 +165,7 @@ function buildPickedPortSection(
);
const searchable = noun === 'Property';
const header = new HStack({ gap: Theme.gap, align: 'center' });
header.add(new Label(plural.toUpperCase(), { style: 'title' }), 1);
const header = sectionHeader(host, plural.toUpperCase());
const add = new Dropdown(`+ ${noun}`, { variant: 'ghost' });
add.onClick = () => {
const items = pick('', (port) => host.state.execute(new AddPortCommand(assetId, node.id, port)));
@@ -214,8 +214,7 @@ export function buildPortSection(
const noun = isTerminalNode(node) ? (onStart ? 'Input' : 'Output') : 'Field';
const base = noun === 'Field' ? 'field' : onStart ? 'in' : 'out';
const header = new HStack({ gap: Theme.gap, align: 'center' });
header.add(new Label(`${noun.toUpperCase()}S`, { style: 'title' }), 1);
const header = sectionHeader(host, `${noun.toUpperCase()}S`);
// With a Struct asset as its type the fields are that asset's (#253): changed there, not here.
if (typeName !== null) {
host.body.add(header);
@@ -295,7 +294,7 @@ function addShapePicker(
assetId: string,
node: GraphNode,
variables: readonly PropertyDef[],
header: HStack,
header: SectionHeader,
): void {
const shaped = variables.filter((v) => structShapeOf(v) !== undefined);
if (shaped.length === 0) return;
+10 -2
View File
@@ -30,16 +30,17 @@ import {
Menu,
Panel,
ScrollContainer,
SectionStack,
SortableList,
Theme,
ui,
VStack,
type Widget,
} from '../../ui';
import type { EditorState, TilemapFocus, TilemapTool } from '../state';
import { TilePalette } from '../tile/TilePalette';
import { labelledRow } from '../inspector/fieldFactory';
import { addNote, addTitle, type InspectorHost } from '../inspector/host';
import { listFold } from '../sectionFolds';
const FOCUS: { focus: TilemapFocus; label: string }[] = [
{ focus: 'all', label: 'All layers' },
@@ -217,13 +218,16 @@ export class TilemapPanel extends Panel {
}
private emptyHost(): InspectorHost {
const body = new SectionStack({ padding: Theme.padding, gap: Theme.gap });
body.onFold = () => this.scroll.relayoutContent();
return {
state: this.state,
project: () => this.state.project,
liveVariables: () => null,
body: new VStack({ padding: Theme.padding, gap: Theme.gap }),
body,
fields: new Map(),
nameField: null,
foldScope: FOLD_SCOPE,
};
}
@@ -256,6 +260,9 @@ export class TilemapPanel extends Panel {
}
}
/** Where the panel's folded groups are remembered, apart from the inspector's. */
const FOLD_SCOPE = 'tilemap';
function buildPanelContent(host: InspectorHost, asset: TilemapAsset): void {
const { state } = host;
const paint = state.tilemapPaint;
@@ -277,6 +284,7 @@ function buildPanelContent(host: InspectorHost, asset: TilemapAsset): void {
palette: null,
layers: new SortableList({
title: 'Layers',
...listFold(FOLD_SCOPE, 'Layers'),
reversed: true,
keepOne: true,
onPick: (layerId) => state.setTilemapPaint({ layerId }),
+17 -5
View File
@@ -20,15 +20,17 @@ import {
Menu,
NumberField,
ScrollContainer,
SectionHeader,
SectionStack,
SortableList,
Spacer,
TextField,
Theme,
VStack,
type Widget,
} from '../../ui';
import { pixelCels } from '../../viewport/PixelCels';
import { FramePreview } from '../framePreview';
import { foldKey, isFolded, listFold, setFolded } from '../sectionFolds';
import type { EditorState } from '../state';
import { PaletteGrid } from './PaletteGrid';
import { type PixelArtCanvas, PIXEL_TOOL_LABELS, PIXEL_TOOLS, type PixelTool } from './PixelArtCanvas';
@@ -60,7 +62,7 @@ function brushLabel(size: number): string {
}
export class PixelArtSide extends ScrollContainer {
private readonly column = new VStack({ gap: Theme.gap, padding: Theme.padding });
private readonly column = new SectionStack({ gap: Theme.gap, padding: Theme.padding });
/** Public for the check scripts: every control they press. */
readonly toolButtons = new Map<PixelTool, Button>();
readonly brushButton = new Dropdown(brushLabel(1), { width: 120 });
@@ -97,6 +99,7 @@ export class PixelArtSide extends ScrollContainer {
this.perFrame = new FramePreview(state, 'pixelart-side');
this.preview = new PixelPreview(state);
const c = this.column;
c.onFold = () => this.relayoutContent();
c.add(heading('Tools'));
for (let i = 0; i < PIXEL_TOOLS.length; i += TOOLS_PER_ROW) {
@@ -141,6 +144,7 @@ export class PixelArtSide extends ScrollContainer {
this.layers = new SortableList({
title: 'Layers',
...listFold(FOLD_SCOPE, 'Layers'),
reversed: true,
keepOne: true,
onPick: (id) => this.canvas.selectLayer(id),
@@ -176,6 +180,7 @@ export class PixelArtSide extends ScrollContainer {
this.frames = new SortableList({
title: 'Frames',
...listFold(FOLD_SCOPE, 'Frames'),
keepOne: true,
onPick: (id) => this.canvas.selectFrame(id),
onMove: (from, to) =>
@@ -422,9 +427,16 @@ export class PixelArtSide extends ScrollContainer {
}
}
function heading(text: string): Label {
// The style the lists' own headers use, so the column reads as one.
return new Label(text.toUpperCase(), { style: 'title' });
/** Where the column's folded groups are remembered, apart from the inspector's. */
const FOLD_SCOPE = 'pixelart';
/** The band the lists' own headers are, so the column reads as one. */
function heading(text: string): SectionHeader {
const key = foldKey(FOLD_SCOPE, text.toUpperCase());
return new SectionHeader(text.toUpperCase(), {
collapsed: isFolded(key),
onToggle: (on) => setFolded(key, on),
});
}
function row(...widgets: Widget[]): HStack {
+68
View File
@@ -0,0 +1,68 @@
/**
* Which groups of rows are folded (#290), kept in this browser: a group folded once stays folded
* for every selection that shows it, and after a reload. Best effort, like the saves – a browser
* that refuses `localStorage` forgets with the page.
*/
import { Theme } from '../ui/Theme';
const STORAGE_KEY = 'game-maker.folded-sections';
let folded: Set<string> | null = null;
function storage(): Pick<Storage, 'getItem' | 'setItem'> | null {
try {
return typeof window === 'undefined' ? null : window.localStorage;
} catch {
return null;
}
}
function read(): Set<string> {
try {
const stored: unknown = JSON.parse(storage()?.getItem(STORAGE_KEY) ?? '[]');
return new Set(Array.isArray(stored) ? stored.filter((key) => typeof key === 'string') : []);
} catch {
return new Set();
}
}
/**
* Names a group: the panel it stands in and its title, numbers left out – "3 OBJECTS SELECTED" and
* "KEY AT FRAME 12" are the same group whatever they count.
*/
export function foldKey(scope: string, title: string): string {
return `${scope}:${title.replace(/\d+/g, '#')}`;
}
export function isFolded(key: string): boolean {
folded ??= read();
return folded.has(key);
}
export function setFolded(key: string, on: boolean): void {
folded ??= read();
if (on) folded.add(key);
else folded.delete(key);
try {
storage()?.setItem(STORAGE_KEY, JSON.stringify([...folded].sort()));
} catch {
// Kept for this page only.
}
}
/**
* What a list with a header of its own takes to fold like the groups of the column it stands in:
* the band reaches across the column's padding, and the fold is remembered under the list's title.
*/
export function listFold(
scope: string,
title: string,
): { bleed: number; collapsed: boolean; onFold: (collapsed: boolean) => void } {
const key = foldKey(scope, title.toUpperCase());
return { bleed: Theme.padding, collapsed: isFolded(key), onFold: (on) => setFolded(key, on) };
}
/** Forgets what was read, so the next question asks the browser again (tests). */
export function resetFolds(): void {
folded = null;
}
+5 -2
View File
@@ -8,7 +8,7 @@ import { isVariableOwner } from '../../core/model/Asset';
import { findObject } from '../../core/model/hierarchy';
import { findAsset } from '../../core/model/Project';
import { objectName } from '../../core/model/SceneObject';
import { Label, ScrollContainer, Theme, ui, VStack, HStack } from '../../ui';
import { Label, ScrollContainer, SectionStack, Theme, ui, VStack, HStack } from '../../ui';
import { assetSignature, buildVariables, refreshAssetSection } from '../inspector/assetSection';
import type { InspectorHost } from '../inspector/host';
import { viewOf } from '../inspector/objectQueries';
@@ -101,13 +101,16 @@ export class VariablesEditor extends VStack {
}
private emptyHost(): InspectorHost {
const body = new SectionStack({ padding: Theme.padding, gap: Theme.gap });
body.onFold = () => this.scroll.relayoutContent();
return {
state: this.state,
project: () => this.state.project,
liveVariables: (assetId) => this.liveVariables(assetId),
body: new VStack({ padding: Theme.padding, gap: Theme.gap }),
body,
fields: new Map(),
nameField: null,
foldScope: 'variables',
};
}
+2
View File
@@ -18,6 +18,7 @@ import { shapeDrawing } from '../../core/model/vectorPath';
import { textureLibrary } from '../../viewport/TextureLibrary';
import { PaintCache, paintDrawing } from '../../viewport/vectorArtPicture';
import { SortableList, type ThumbClip, type Widget } from '../../ui';
import { listFold } from '../sectionFolds';
import type { EditorState } from '../state';
export class ShapeList extends SortableList {
@@ -46,6 +47,7 @@ export class ShapeList extends SortableList {
) {
super({
title: 'Shapes',
...listFold('inspector', 'Shapes'),
reversed: true,
multiSelect: true,
onPick: rows.onPick,