diff --git a/src/editor/inspector/portSection.ts b/src/editor/inspector/portSection.ts index c1607b37..6782f285 100644 --- a/src/editor/inspector/portSection.ts +++ b/src/editor/inspector/portSection.ts @@ -53,7 +53,7 @@ import { type Widget, } from '../../ui'; import { NAME_WIDTH, removeButton } from './fieldFactory'; -import { addNote, type InspectorHost, sectionHeader } from './host'; +import { addedRows, 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. */ @@ -188,17 +188,15 @@ function buildPickedPortSection( if (!custom && choices.length === 0 && empty) addNote(host, empty); else if (ports.length === 0) addNote(host, `No ${plural} picked yet. Each one adds a port.`); - for (const port of ports) { + const pickedRow = (port: DeclaredPort): Grip => { // A machine a variable holds is a running value; which one its states come from is picked here. const carries = noun === 'Variable' && port.kind === 'statemachine'; const toggle = [ ...(carries ? [machinePick(host, assetId, node, port)] : []), ...writeToggle(host, assetId, node, port), ]; - if (custom) { - portRow(host, assetId, node, port, portNameField(host, assetId, node, port), toggle); - continue; - } + if (custom) + return portRow(host, assetId, node, port, portNameField(host, assetId, node, port), toggle).grip; const def = choices.find((d) => d.key === port.key); const known = def !== undefined && isPortOf(port, def); const choose = new Dropdown(port.key, { variant: known ? 'default' : 'danger' }); @@ -208,9 +206,23 @@ function buildPickedPortSection( ); new Menu(items, { searchable }).openBelow(choose); }; - portRow(host, assetId, node, def && known ? { ...port, kind: portKindOf(def) } : port, choose, toggle); + const row = portRow( + host, + assetId, + node, + def && known ? { ...port, kind: portKindOf(def) } : port, + choose, + toggle, + ); if (!known) addNote(host, `"${port.key}" is not a ${noun.toLowerCase()} of ${owner}.`, 'warning'); - } + return row.grip; + }; + // The order of the ports is the order on the node; a warning goes along with the row it is about. + const blocks = ports.map((port) => { + const { rows, built } = addedRows(host, () => pickedRow(port)); + return { grip: built, rows }; + }); + sortBlocks(blocks, (from, to) => host.state.execute(new MovePortCommand(assetId, node.id, from, to))); } /** diff --git a/src/ui/widgets/Grip.ts b/src/ui/widgets/Grip.ts index 547c3679..27e8a9d1 100644 --- a/src/ui/widgets/Grip.ts +++ b/src/ui/widgets/Grip.ts @@ -6,6 +6,10 @@ import { type Size, Widget } from '../Widget'; const DOT = 2; const DOT_GAP = 1; const IDLE_ALPHA = 0.5; +/** The air on either side of the dots, so they do not touch the edge of the lit block or the mark. */ +const DOTS_PAD = 3; +/** Where the mark starts once the dots are drawn: their two columns and the air around them. */ +const DOTS_INSET = DOTS_PAD + 2 * DOT + DOT_GAP + DOTS_PAD; /** * What a row is taken hold of by to drag it to another place (#310): its mark, the same size. Once @@ -30,12 +34,16 @@ export class Grip extends Widget { } override layout(): void { - this.content.resize(this.w, this.h); + // The dots take the left edge, the mark the rest: it moves over instead of sitting on them. + const inset = this.grabbable ? DOTS_INSET : 0; + this.content.position.x = inset; + this.content.resize(Math.max(0, this.w - inset), this.h); } setGrabbable(on: boolean): void { if (this.grabbable === on) return; this.grabbable = on; + this.layout(); this.invalidate(); } @@ -56,7 +64,7 @@ export class Grip extends Widget { const top = Math.round(this.h / 2 - (3 * DOT + 2 * DOT_GAP) / 2); for (let col = 0; col < 2; col++) for (let row = 0; row < 3; row++) - this.dots.rect(col * (DOT + DOT_GAP), top + row * (DOT + DOT_GAP), DOT, DOT); + this.dots.rect(DOTS_PAD + col * (DOT + DOT_GAP), top + row * (DOT + DOT_GAP), DOT, DOT); this.dots.fill(this.lit ? Theme.colors.text : Theme.colors.textMuted); this.dots.alpha = this.lit ? 1 : IDLE_ALPHA; } diff --git a/tests/ui/block-drag.test.ts b/tests/ui/block-drag.test.ts index 01ed20ec..38cc380a 100644 --- a/tests/ui/block-drag.test.ts +++ b/tests/ui/block-drag.test.ts @@ -44,6 +44,13 @@ describe('the grips of a sortable column', () => { expect(column(false).blocks.map((b) => b.grip.dotsShown)).toEqual(['none', 'none', 'none']); }); + it('move their mark right of the dots, and leave it at the edge where there are none', () => { + const [sorted] = column().blocks; + expect(sorted!.grip.content.x).toBe(11); + expect(sorted!.grip.content.x + sorted!.grip.content.w).toBe(sorted!.grip.w); + expect(column(false).blocks[0]!.grip.content.x).toBe(0); + }); + it('light up under the pointer and back the whole block, behind its rows', () => { const { body, blocks } = column(); const grip = blocks[1]!.grip; diff --git a/tools/e2e/23-variable-nodes.mjs b/tools/e2e/23-variable-nodes.mjs index c756edd1..21c4eb02 100644 --- a/tools/e2e/23-variable-nodes.mjs +++ b/tools/e2e/23-variable-nodes.mjs @@ -156,6 +156,24 @@ export async function variableNodes(t) { await undo(); check('and undo points it back', (await portsOf('vp-get'))[0].key === 'vx'); + // 23e-ii. The ports are reordered by dragging their mark, as on a start node: the order is the + // node's. A drop just above a mark lands in front of its row. + const grabbed = await json(`__gm.inspectorMark('vy', true)`); + const below = await json(`__gm.inspectorMark('vx', true)`); + check( + 'a variable row shows the dots of a mark that drags', + grabbed?.dots === 'idle', + JSON.stringify(grabbed), + ); + await t.drag(grabbed.x, grabbed.y, below.x, below.y - 6); + check( + 'dragging a variable by its mark puts it in front of the other', + (await portsOf('vp-get')).map((p) => p.key).join(',') === 'vy,vx', + JSON.stringify(await portsOf('vp-get')), + ); + await undo(); + check('and undo puts it back', (await portsOf('vp-get')).map((p) => p.key).join(',') === 'vx,vy'); + // 23f. Collapsed, the ports are out of sight, so the title says what the node does – and only then. const titleOf = (id) => json(`__gm.graph.nodeLabel(${JSON.stringify(id)})`); const toggle = async (id) => {