Files
feuerwerk/src/ui/layout/SectionStack.ts
T
adminandClaude Opus 5.5 393caadc9d
game-maker / gate (push) Successful in 2m39s
game-maker / suite (push) Successful in 13m10s
Head the groups of the inspector with bands that fold (#290)
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>
2026-10-01 22:08:28 +02:00

70 lines
2.5 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 { hiddenBySections, sectionOffsets, type SectionRow, sectionStartOf } from '../sections';
import { type Size, Widget } from '../Widget';
import { type StackOptions, VStack } from './Stack';
/**
* A column of rows in groups (#290): a `SectionHeader` runs across the padding as a band, the rows
* after it stand inside it, and a folded header hides its rows down to the next one. Children are
* never stretched along the column – it is meant for a scroll container.
*/
export class SectionStack extends VStack {
/** After a group was folded or unfolded – the owner measures the column again. */
onFold: () => void = () => {};
/** The children a fold hid, so unfolding shows only those again. */
private readonly hiddenByFold = new WeakSet<Widget>();
constructor(options: StackOptions = {}) {
super(options);
}
refold(): void {
this.applyFolds();
this.onFold();
}
private applyFolds(): void {
const widgets = this.children.filter((c): c is Widget => c instanceof Widget);
const hidden = hiddenBySections(widgets.map((w) => sectionStartOf(w)?.sectionCollapsed ?? null));
widgets.forEach((widget, i) => {
if (hidden[i] && widget.visible) {
widget.visible = false;
this.hiddenByFold.add(widget);
} else if (!hidden[i] && this.hiddenByFold.has(widget)) {
widget.visible = true;
this.hiddenByFold.delete(widget);
}
});
}
private rows(sizes: readonly Size[]): SectionRow[] {
return this.items.map((item, i) => {
const start = sectionStartOf(item);
return {
h: sizes[i]!.h,
start: start !== null,
band: start !== null && (start.sectionStart === 'band' || start.sectionCollapsed),
};
});
}
override measure(): Size {
this.applyFolds();
const sizes = this.items.map((item) => item.measure());
const w = Math.max(0, ...sizes.map((size) => size.w)) + this.padding * 2;
const { height } = sectionOffsets(this.rows(sizes), this.gap, this.padding);
return { w: Math.max(w, this.minWidth), h: Math.max(height, this.minHeight) };
}
override layout(): void {
this.applyFolds();
const items = this.items;
const sizes = items.map((item) => item.measure());
const { ys } = sectionOffsets(this.rows(sizes), this.gap, this.padding);
items.forEach((item, i) => {
const band = sectionStartOf(item)?.sectionStart === 'band';
item.position.set(band ? 0 : this.padding, Math.round(ys[i]!));
item.resize(Math.max(0, band ? this.w : this.w - this.padding * 2), Math.round(sizes[i]!.h));
});
}
}