Multiselection and container assets: create container from selection, template instances

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-07 17:40:51 +02:00
1 parent 91ee2feadd
commit 0b78382bcc
26 files changed
+897 -114

No files matched your search

+4
View File
@@ -35,6 +35,10 @@ export const Theme = {
/** Collider outlines: solid (blocking) and trigger (events only). */
collider: 0x4fd18b,
trigger: 0xf2c84b,
/** Container (group) symbols. */
container: 0x3fb8c9,
/** Rubber-band selection rectangle. */
marquee: 0x4c8dff,
/** Node graph: canvas, node chrome, per-category title colors, port colors, live highlight. */
graphBackground: 0x15171b,
node: 0x262a32,
+1 -1
View File
@@ -8,7 +8,7 @@ export { Checkbox } from './widgets/Checkbox';
export { Panel } from './widgets/Panel';
export { Splitter } from './widgets/Splitter';
export { ScrollContainer } from './widgets/ScrollContainer';
export { ListView, type ListItem } from './widgets/ListView';
export { ListView, type ListItem, type PickModifiers } from './widgets/ListView';
export { Popup, Menu, Dialog, type MenuItem } from './widgets/Popup';
export { TextField } from './widgets/TextField';
export { NumberField } from './widgets/NumberField';
+32 -7
View File
@@ -17,16 +17,26 @@ export interface ListItem {
/** Pixels the pointer must travel before a press turns into a drag. */
const DRAG_THRESHOLD = 4;
/** Modifier keys held while a row was clicked. */
export interface PickModifiers {
/** Ctrl (or ⌘) – toggle the row in a multi-selection. */
ctrl: boolean;
shift: boolean;
}
/**
* Single-select list of rows. Rows are rebuilt from `setItems()`; selection is by id so
* it survives rebuilds. Emits select, activate (double-click / Enter) and context events.
* With `onDrop` set, rows can be dragged onto other rows (or onto the empty area below).
* List of rows with one selected row (by id, so it survives `setItems()` rebuilds) and,
* optionally, further *highlighted* rows the owner manages (a multi-selection). Emits select,
* activate (double-click / Enter) and context events. A click with Ctrl/Shift held does not
* change the selection itself but reports the modifiers, so the owner can toggle. With
* `onDrop` set, rows can be dragged onto other rows (or onto the empty area below).
*/
export class ListView extends Widget {
private items: ListItem[] = [];
private rows: Row[] = [];
private selectedId: string | null = null;
onSelect?: (id: string | null) => void;
private highlighted = new Set<string>();
onSelect?: (id: string | null, modifiers?: PickModifiers) => void;
onActivate?: (id: string) => void;
onContextMenu?: (id: string, globalX: number, globalY: number) => void;
/** Called when a dragged row is released over another row (`targetId`) or over empty space (null). */
@@ -66,11 +76,16 @@ export class ListView extends Widget {
this.rows = items.map((item, index) => {
const row = new Row(item, this.rowHeight);
row.on('pointerdown', (e: FederatedPointerEvent) => {
const modifiers: PickModifiers = { ctrl: e.ctrlKey || e.metaKey, shift: e.shiftKey };
if (e.button === 2) {
this.select(item.id);
// A right-click inside a multi-selection keeps it (the menu acts on all of it).
if (!this.highlighted.has(item.id)) this.select(item.id);
this.onContextMenu?.(item.id, e.global.x, e.global.y);
} else if (modifiers.ctrl || modifiers.shift) {
this.onSelect?.(item.id, modifiers);
} else {
this.select(item.id);
// Clicking a highlighted row keeps the multi-selection so it can be dragged as a whole.
if (!this.highlighted.has(item.id) || item.id === this.selectedId) this.select(item.id);
if (e.detail === 2) this.onActivate?.(item.id);
else if (this.onDrop) this.drag = { id: item.id, startX: e.global.x, startY: e.global.y, active: false, targetId: null };
}
@@ -92,6 +107,16 @@ export class ListView extends Widget {
if (!silent) this.onSelect?.(id);
}
/** Rows drawn as selected in addition to the selected one (the owner's multi-selection). */
highlight(ids: Iterable<string>): void {
this.highlighted = new Set(ids);
this.redraw();
}
get highlightedIds(): string[] {
return [...this.highlighted];
}
override measure(): Size {
return { w: this.minWidth, h: Math.max(this.minHeight, this.items.length * this.rowHeight) };
}
@@ -120,7 +145,7 @@ export class ListView extends Widget {
}
override redraw(): void {
for (const row of this.rows) row.setSelected(row.item.id === this.selectedId, this.focused);
for (const row of this.rows) row.setSelected(row.item.id === this.selectedId || this.highlighted.has(row.item.id), this.focused);
this.emptyLabel.text = this.items.length === 0 ? this.emptyText : '';
this.emptyLabel.position.set(Theme.padding, Math.round((this.rowHeight - this.emptyLabel.height) / 2));
this.drawDropMarker();