A few breaking tweaks (not working) Change-Id: I8f421b29dbcbac2837bf4cd7fb7385954645532a
diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/graph/node_config.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/graph/node_config.ts index f469cb6..fe76a73 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/graph/node_config.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/graph/node_config.ts
@@ -154,7 +154,7 @@ return { id: qnode.nodeId, - titleBar: undefined, + headerBar: undefined, inputs, outputs, canDockTop: isSingle,
diff --git a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/group_node/inner_graph_preview.ts b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/group_node/inner_graph_preview.ts index f4cc461..e2ab9d4 100644 --- a/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/group_node/inner_graph_preview.ts +++ b/ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/group_node/inner_graph_preview.ts
@@ -163,7 +163,7 @@ id: inputId, x: i * nodeSpacingX, y: -80, - titleBar: {title: `Input ${conn.groupPort + 1}`}, + headerBar: {title: `Input ${conn.groupPort + 1}`}, outputs: [{direction: 'bottom'}], className: isConnected ? undefined : 'pf-node--disconnected', });
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/demos/nodegraph_demo.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/nodegraph_demo.ts index bb20577..98359bd 100644 --- a/ui/src/plugins/dev.perfetto.WidgetsPage/demos/nodegraph_demo.ts +++ b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/nodegraph_demo.ts
@@ -768,7 +768,7 @@ ], content: manifest.render(tempNode, () => {}), accentBar: attrs.accentBars, - titleBar: attrs.titleBars + headerBar: attrs.titleBars ? { title: manifest.title, icon: attrs.headerIcons ? manifest.icon : undefined,
diff --git a/ui/src/widgets/nodegraph/model.ts b/ui/src/widgets/nodegraph/model.ts index 546491c..4e5bbdb 100644 --- a/ui/src/widgets/nodegraph/model.ts +++ b/ui/src/widgets/nodegraph/model.ts
@@ -20,7 +20,7 @@ readonly toPort: string; // port id } -export interface NodeTitleBar { +export interface NodeHeaderBar { readonly title: m.Children; readonly icon?: string; } @@ -40,7 +40,7 @@ readonly y: number; readonly hue: number; // Color of the title / accent bar (0-360) readonly accentBar?: boolean; // Optional strip of accent color on the left side (doesn't work well with titleBar) - readonly titleBar?: NodeTitleBar; // Optional title bar (doesn't work well with accentBar or docking) + readonly headerBar?: NodeHeaderBar; // Optional title bar (doesn't work well with accentBar or docking) readonly inputs?: ReadonlyArray<NodePort>; readonly outputs?: ReadonlyArray<NodePort>; readonly content?: m.Children; // Optional custom content to render in node body
diff --git a/ui/src/widgets/nodegraph/views/nodegraph.ts b/ui/src/widgets/nodegraph/views/nodegraph.ts index 6049179..4f261e0 100644 --- a/ui/src/widgets/nodegraph/views/nodegraph.ts +++ b/ui/src/widgets/nodegraph/views/nodegraph.ts
@@ -14,27 +14,28 @@ import m from 'mithril'; import {assertIsInstance} from '../../../base/assert'; -import {Point2D} from '../../../base/geom'; -import {MithrilEvent} from '../../../base/mithril_utils'; -import {DockedNode, Node, NodeGraphAttrs} from '../model'; -import {NGCardHeader, NGNode, NGPort, NGCard, NGCardBody} from './node'; -import {NGToolbar} from './toolbar'; import {captureDrag} from '../../../base/dom_utils'; -import {DisposableStack} from '../../../base/disposable_stack'; +import {Point2D, Vector2D} from '../../../base/geom'; +import {MithrilEvent} from '../../../base/mithril_utils'; import {shortUuid} from '../../../base/uuid'; -import {arrowheadMarker, connectionPath} from '../svg'; -import type {PortDirection} from '../svg'; import {PopupMenu} from '../../../widgets/menu'; import {PopupPosition} from '../../../widgets/popup'; -import {start} from 'repl'; +import {DockedNode, Node, NodeGraphAttrs, NodePort} from '../model'; +import type {PortDirection} from '../svg'; +import {arrowheadMarker, connectionPath} from '../svg'; +import {NGCard, NGCardBody, NGCardHeader, NGNode, NGPort} from './node'; +import {NGToolbar} from './toolbar'; const WHEEL_ZOOM_SCALING_FACTOR = 0.006; const MIN_ZOOM = 0.01; const MAX_ZOOM = 3.0; const GRID_SIZE = 24; -function snapToGrid(v: number): number { - return Math.round(v / GRID_SIZE) * GRID_SIZE; +function snapToGrid(p: Point2D): Vector2D { + return new Vector2D({ + x: Math.round(p.x / GRID_SIZE) * GRID_SIZE, + y: Math.round(p.y / GRID_SIZE) * GRID_SIZE, + }); } function portDirFromEl(el: Element): PortDirection { @@ -99,7 +100,7 @@ // Port ID of the output port whose context menu is currently open. private openPortMenuId?: string; - private getNodePosition(nodeEl: HTMLElement): Point2D { + private getNodePosition(nodeEl: HTMLElement) { const rect = nodeEl.getBoundingClientRect(); const canvasRect = this.rootEl.getBoundingClientRect(); return this.getWorkspacePosition({ @@ -108,12 +109,20 @@ }); } - private getWorkspacePosition(pos: Point2D): Point2D { + private getWorkspacePosition(pos: Point2D) { const {offset, zoom} = this.viewport; - return { + return new Vector2D({ x: pos.x / zoom + offset.x, y: pos.y / zoom + offset.y, - }; + }); + } + + private client2Workspace(client: Point2D) { + const canvasRect = this.rootEl.getBoundingClientRect(); + return this.getWorkspacePosition({ + x: client.x - canvasRect.left, + y: client.y - canvasRect.top, + }); } private createGhostNode(nodeEl: HTMLElement) { @@ -190,6 +199,212 @@ className, } = attrs; + const createNodeDragHandler = ( + node: DockedNode, + parentId: string | undefined, + ) => { + return async (e: MithrilEvent<PointerEvent>) => { + // Let interactive elements (inputs, buttons, selects, etc.) handle + // their own events without triggering a node drag. + const target = e.target as HTMLElement; + if (target.closest('input, button, select, textarea, a')) return; + + // Stop this pointer event from hitting the canvas + e.stopPropagation(); + + // Don't redraw just yet... + e.redraw = false; + + // Secure the node element + const nodeEl = assertIsInstance(e.currentTarget, HTMLElement); + + // Find the initial offset within the node in canvas space + const pMouse = {x: e.clientX, y: e.clientY}; + const pMouseWs = this.client2Workspace(pMouse); + + const nodeRect = nodeEl.getBoundingClientRect(); + const pNode = {x: nodeRect.left, y: nodeRect.top}; + const pNodeWs = this.client2Workspace(pNode); + + const grabPoint = pMouseWs.sub(pNodeWs); + + // Wait for this to turn into a proper drag + const drag = await captureDrag({el: this.rootEl, e, deadzone: 5}); + + if (drag) { + // Work out where in the workspace the node is right now + const startPosition = this.getNodePosition(nodeEl); + + using tempNode = this.moveNodeToWorkspace(nodeEl); + tempNode.moveTo(startPosition); + + using ghost = this.createGhostNode(nodeEl); + ghost.moveTo(startPosition); + + let pNode = startPosition; + let dockTarget: string | undefined = undefined; + + using edgePan = this.startEdgePanning((dx, dy) => { + pNode = pNode.add({x: dx, y: dy}); + tempNode.moveTo(pNode); + this.updateConnections(attrs); + }); + + for await (const mv of drag) { + // Find the initial offset within the node in canvas space + const pMouseWs = this.client2Workspace(mv.client); + pNode = pMouseWs.sub(grabPoint); + + edgePan.updatePointer(mv.client); + tempNode.moveTo(pNode); + + dockTarget = node.canDockTop + ? this.findDockTarget(nodeEl) + : undefined; + + if (dockTarget) { + const targetEl = this.getNodeElement(dockTarget); + ghost.dockToNode(targetEl); + } else { + ghost.undock(); + ghost.moveTo(snapToGrid(pNode)); + } + this.updateConnections(attrs); + } + + if (dockTarget) { + if (dockTarget !== parentId) { + onNodeDock?.(node.id, dockTarget); + } + } else { + const snapped = snapToGrid(pNode); + onNodeMove?.(node.id, snapped.x, snapped.y); + } + + m.redraw(); + } else { + // Failed drag - treat as a click and select the node + if (e.ctrlKey || e.metaKey) { + if (selectedNodeIds?.has(node.id)) { + onSelectionRemove?.(node.id); + } else { + onSelectionAdd?.(node.id); + } + } else { + // No key held - just select this node and deselect everything else + onSelect?.([node.id]); + } + m.redraw(); + } + }; + }; + + const createPortDragHandler = (node: DockedNode, output: NodePort) => { + return async (e: PointerEvent) => { + e.stopPropagation(); + const drag = await captureDrag({ + el: e.currentTarget as HTMLElement, + e, + }); + + if (!drag) { + // Click (no drag): open context menu if available + if (output.contextMenuItems != null) { + this.openPortMenuId = output.id; + m.redraw(); + } + return; + } + + let clientX = e.clientX; + let clientY = e.clientY; + + const toCanvasPoint = (): Point2D => { + const {zoom, offset} = this.viewport; + const rect = this.rootEl.getBoundingClientRect(); + return { + x: (clientX - rect.left) / zoom + offset.x, + y: (clientY - rect.top) / zoom + offset.y, + }; + }; + + const nodeDirToPortDir: Record<string, PortDirection> = { + north: 'top', + south: 'bottom', + east: 'right', + west: 'left', + }; + const freeToDir = oppositeDir( + nodeDirToPortDir[output.direction] ?? 'right', + ); + const makeWireDrag = (snap: typeof snapTarget) => ({ + fromPortId: output.id, + toPoint: snap ? this.portCenterToCanvas(snap.el) : toCanvasPoint(), + toDir: snap ? portDirFromEl(snap.el) : freeToDir, + }); + + this.wireDrag = makeWireDrag(undefined); + this.rootEl.classList.add('pf-ng--wire-dragging'); + this.updateConnections(attrs); + + let snapTarget: {el: HTMLElement; portId: string} | undefined; + + using edgePan = this.startEdgePanning(() => { + this.wireDrag = makeWireDrag(snapTarget); + this.updateConnections(attrs); + }); + + const processMove = (client: {x: number; y: number}) => { + clientX = client.x; + clientY = client.y; + edgePan.updatePointer(client); + const prevSnap = snapTarget; + snapTarget = this.findWireSnapTarget(node.id, clientX, clientY); + if (prevSnap?.el !== snapTarget?.el) { + prevSnap?.el.classList.remove('pf-wire-snap'); + snapTarget?.el.classList.add('pf-wire-snap'); + } + this.wireDrag = makeWireDrag(snapTarget); + this.updateConnections(attrs); + }; + + for await (const mv of drag) { + processMove(mv.client); + } + snapTarget?.el.classList.remove('pf-wire-snap'); + this.rootEl.classList.remove('pf-ng--wire-dragging'); + + if (snapTarget !== undefined) { + onConnect?.({ + fromPort: output.id, + toPort: snapTarget.portId, + }); + } else { + // Fallback: check if the pointer is directly over an input port. + const target = document.elementFromPoint(clientX, clientY); + const inputPortEl = + target?.closest('.pf-ng__port-dot.pf-input') ?? null; + if (inputPortEl) { + const nodeEl = inputPortEl.closest( + '[data-node-id]', + ) as HTMLElement | null; + const toNodeId = nodeEl?.getAttribute('data-node-id'); + const portId = inputPortEl.getAttribute('data-port-id'); + if (portId && toNodeId && toNodeId !== node.id) { + onConnect?.({ + fromPort: output.id, + toPort: portId, + }); + } + } + } + + this.wireDrag = undefined; + this.updateConnections(attrs); + m.redraw(); + }; + }; + // parentId is set for docked nodes; absent for root nodes. const renderNode = (node: DockedNode, parentId?: string): m.Children => { const isDocked = parentId !== undefined; @@ -203,103 +418,7 @@ position: rootNode ? {x: rootNode.x, y: rootNode.y} : undefined, // Recursively render the whole tree nextNode: node.next && renderNode(node.next, node.id), - onpointerdown: async (e: MithrilEvent<PointerEvent>) => { - // Let interactive elements (inputs, buttons, selects, etc.) handle - // their own events without triggering a node drag. - const target = e.target as HTMLElement; - if (target.closest('input, button, select, textarea, a')) return; - - // Stop this pointer event from hitting the canvas - e.stopPropagation(); - - // Don't redraw just yet... - e.redraw = false; - - // Secure the node element - const nodeEl = assertIsInstance(e.currentTarget, HTMLElement); - - // Wait for this to turn into a proper drag - const drag = await captureDrag({el: this.rootEl, e, deadzone: 5}); - - if (drag) { - // Work out where in the workspace the node is right now - const startPosition = this.getNodePosition(nodeEl); - - using tempNode = this.moveNodeToWorkspace(nodeEl); - tempNode.moveTo(startPosition); - - using ghost = this.createGhostNode(nodeEl); - ghost.moveTo(startPosition); - - let currentNodePos = startPosition; - let dockTargetId: string | undefined = undefined; - - using edgePan = this.startEdgePanning((dx, dy) => { - currentNodePos = { - x: currentNodePos.x + dx, - y: currentNodePos.y + dy, - }; - tempNode.moveTo(currentNodePos); - this.updateConnections(attrs); - }); - - for await (const mv of drag) { - const {zoom} = this.viewport; - edgePan.updatePointer(mv.client); - currentNodePos = { - x: currentNodePos.x + mv.delta.x / zoom, - y: currentNodePos.y + mv.delta.y / zoom, - }; - - tempNode.moveTo(currentNodePos); - - dockTargetId = node.canDockTop - ? this.findDockTarget(nodeEl) - : undefined; - - console.log('Dock target:', dockTargetId); - - if (dockTargetId) { - const targetEl = this.getNodeElement(dockTargetId); - ghost.dockToNode(targetEl); - } else { - ghost.undock(); - ghost.moveTo({ - x: snapToGrid(currentNodePos.x), - y: snapToGrid(currentNodePos.y), - }); - } - this.updateConnections(attrs); - } - - if (dockTargetId) { - if (dockTargetId !== parentId) { - onNodeDock?.(node.id, dockTargetId); - } - } else { - onNodeMove?.( - node.id, - snapToGrid(currentNodePos.x), - snapToGrid(currentNodePos.y), - ); - } - - m.redraw(); - } else { - // Failed drag - treat as a click and select the node - if (e.ctrlKey || e.metaKey) { - if (selectedNodeIds?.has(node.id)) { - onSelectionRemove?.(node.id); - } else { - onSelectionAdd?.(node.id); - } - } else { - // No key held - just select this node and deselect everything else - onSelect?.([node.id]); - } - m.redraw(); - } - }, + onpointerdown: createNodeDragHandler(node, parentId), }, m( NGCard, @@ -310,10 +429,10 @@ className: node.className, }, [ - node.titleBar && + node.headerBar && m(NGCardHeader, { - title: node.titleBar.title, - icon: node.titleBar.icon, + title: node.headerBar.title, + icon: node.headerBar.icon, }), node.inputs?.map((input) => m(NGPort, { @@ -349,115 +468,7 @@ connected: attrs.connections?.some((c) => c.fromPort === output.id) ?? false, - onpointerdown: async (e: PointerEvent) => { - e.stopPropagation(); - const drag = await captureDrag({ - el: e.currentTarget as HTMLElement, - e, - }); - - if (!drag) { - // Click (no drag): open context menu if available - if (output.contextMenuItems != null) { - this.openPortMenuId = output.id; - m.redraw(); - } - return; - } - - let clientX = e.clientX; - let clientY = e.clientY; - - const toCanvasPoint = (): Point2D => { - const {zoom, offset} = this.viewport; - const rect = this.rootEl.getBoundingClientRect(); - return { - x: (clientX - rect.left) / zoom + offset.x, - y: (clientY - rect.top) / zoom + offset.y, - }; - }; - - const nodeDirToPortDir: Record<string, PortDirection> = { - north: 'top', - south: 'bottom', - east: 'right', - west: 'left', - }; - const freeToDir = oppositeDir( - nodeDirToPortDir[output.direction] ?? 'right', - ); - const makeWireDrag = (snap: typeof snapTarget) => ({ - fromPortId: output.id, - toPoint: snap - ? this.portCenterToCanvas(snap.el) - : toCanvasPoint(), - toDir: snap ? portDirFromEl(snap.el) : freeToDir, - }); - - this.wireDrag = makeWireDrag(undefined); - this.rootEl.classList.add('pf-ng--wire-dragging'); - this.updateConnections(attrs); - - let snapTarget: {el: HTMLElement; portId: string} | undefined; - - using edgePan = this.startEdgePanning(() => { - this.wireDrag = makeWireDrag(snapTarget); - this.updateConnections(attrs); - }); - - const processMove = (client: {x: number; y: number}) => { - clientX = client.x; - clientY = client.y; - edgePan.updatePointer(client); - const prevSnap = snapTarget; - snapTarget = this.findWireSnapTarget( - node.id, - clientX, - clientY, - ); - if (prevSnap?.el !== snapTarget?.el) { - prevSnap?.el.classList.remove('pf-wire-snap'); - snapTarget?.el.classList.add('pf-wire-snap'); - } - this.wireDrag = makeWireDrag(snapTarget); - this.updateConnections(attrs); - }; - - for await (const mv of drag) { - processMove(mv.client); - } - snapTarget?.el.classList.remove('pf-wire-snap'); - this.rootEl.classList.remove('pf-ng--wire-dragging'); - - if (snapTarget !== undefined) { - onConnect?.({ - fromPort: output.id, - toPort: snapTarget.portId, - }); - } else { - // Fallback: check if the pointer is directly over an input port. - const target = document.elementFromPoint(clientX, clientY); - const inputPortEl = - target?.closest('.pf-ng__port-dot.pf-input') ?? null; - if (inputPortEl) { - const nodeEl = inputPortEl.closest( - '[data-node-id]', - ) as HTMLElement | null; - const toNodeId = nodeEl?.getAttribute('data-node-id'); - const portId = inputPortEl.getAttribute('data-port-id'); - if (portId && toNodeId && toNodeId !== node.id) { - onConnect?.({ - fromPort: output.id, - toPort: portId, - }); - } - } - } - - this.wireDrag = undefined; - this.updateConnections(attrs); - m.redraw(); - }, + onpointerdown: createPortDragHandler(node, output), }); if (output.contextMenuItems == null) return portEl; return m( @@ -465,7 +476,6 @@ { trigger: portEl, position: PopupPosition.Bottom, - isOpen: this.openPortMenuId === output.id, onChange: (open) => { this.openPortMenuId = open ? output.id : undefined; m.redraw();