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:
1 parent
91ee2feadd
commit
0b78382bcc
26 files changed
+897
-114
No files matched your search
@@ -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
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user