From 8cda1ac48a89ace18da3cd6eb46de469b817a389 Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:31:48 +1100 Subject: [PATCH] Add CanvasPointer callback for node widgets (#363) * Add CanvasPointer callback for node widgets * Add API documentation * nit - Docs --- API.md | 200 +++++++++++++++++++++++++++++++++++++++++++ src/CanvasPointer.ts | 5 ++ src/LGraphCanvas.ts | 7 ++ src/types/widgets.ts | 17 +++- 4 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 API.md diff --git a/API.md b/API.md new file mode 100644 index 0000000..bc1d6ee --- /dev/null +++ b/API.md @@ -0,0 +1,200 @@ +# API + +This document is intended to provide a brief introduction to new Litegraph APIs. + + + + + +# CanvasPointer API + + + +CanvasPointer replaces much of the original pointer handling code. It provides a standard click, double-click, and drag UX for users. + + + + + +## Default behaviour changes + + + + +- Dragging multiple items no longer requires that the shift key be held down + - Clicking an item when multiple nodes / etc are selected will still deselect everything else +- Clicking a connected link on an input no longer disconnects and reconnects it +- Double-clicking requires that both clicks occur nearby +- Provides much room for extension, configuration, and changes + +#### Bug fixes + +- Intermittent issue where clicking a node slightly displaces it +- Alt-clicking to add a reroute creates two undo steps + +### Selecting multiple items + +- `Ctrl + drag` - Begin multi-select +- `Ctrl + Shift + drag` - Add to selection + - `Ctrl + drag`, `Shift` - Alternate add to selection +- `Ctrl + drag`, `Alt` - Remove from selection + +### Click "drift" + +A small amount of buffering is performed between down/up events to prevent accidental micro-drag events. If either of the two controls are exceeded, the event will be considered a drag event, not a click. + +- `buffterTime` is the maximum time that tiny movements can be ignored (Default: 150ms) +- `maxClickDrift` controls how far a click can drift from its down event before it is considered a drag (Default: 6) + +### Double-click + +When double clicking, the double click callback is executed shortly after one normal click callback (if present). At present, dragging from the second click simply invalidates the event - nothing will happen. + +- `doubleClickTime` is the maximum time between two `down` events for them to be considered a double click (Default: 300ms) +- Distance between the two events must be less than `3 * maxClickDrift` + +### Configuration + +All above configuration is via class static. + +```ts +CanvasPointer.bufferTime = 150 +CanvasPointer.maxClickDrift = 6 +CanvasPointer.doubleClickTime = 300 +``` + + + + + + + +## Implementing + + + +Clicking, double-clicking, and dragging can now all be configured during the initial `pointerdown` event, and the correct callback(s) will be executed. + +A click event can be as simple as: + +```ts +if (node.isClickedInSpot(e.canvasX, e.canvasY)) this.pointer.onClick = () => node.gotClickInSpot() +``` + +Full usage can be seen in the old `processMouseDown` handler, which is still in place (several monkey patches in the wild). + + +### Registering a click or drag event + +Example usage: + +```typescript +const { pointer } = this +// Click / double click - executed on pointerup +pointer.onClick = (e) => node.executeClick(e) +pointer.onDoubleClick = node.gotDoubleClick + +// Drag events - executed on pointermove +pointer.onDragStart = (e) => { + node.isBeingDragged = true + canvas.startedDragging(e) +} +pointer.onDrag = () => {} +// finally() is preferred where possible, as it is guaranteed to run +pointer.onDragEnd = () => {} + +// Always run, regardless of outcome +pointer.finally = () => node.isBeingDragged = false +``` + +## Widgets + +Adds `onPointerDown` callback to node widgets. A few benefits of the new API: + +- Simplified usage +- Exposes callbacks like "double click", removing the need to time / measure multiple pointer events +- Unified UX - same API as used in the rest of Litegraph +- Honours the user's click speed and pointer accuracy settings + +#### Usage + +```ts +// Callbacks for each pointer action can be configured ahead of time +widget.onPointerDown = function (pointer, node, canvas) { + const e = pointer.eDown + const offsetFromNode = [ + e.canvasX - node.pos[0], + e.canvasY - node.pos[1], + ] + + // Click events - no overlap with drag events + pointer.onClick = (upEvent) => { + // Provides access to the whole lifecycle of events in every callback + console.log(pointer.eDown) + console.log(pointer.eMove ?? "Pointer didn't move") + console.log(pointer.eUp) + } + pointer.onDoubleClick = (upEvent) => this.customFunction(upEvent) + + // Runs once before the first onDrag event + pointer.onDragStart = () => {} + // Receives every movement event + pointer.onDrag = (moveEvent) => {} + // The pointerup event of a drag + pointer.onDragEnd = (upEvent) => {} + + // Semantics of a "finally" block (try/catch). Once set, the block always executes. + pointer.finally = () => {} + + // Return true to cancel regular Litegraph handling of this click / drag + return true +} +``` + + + +### TypeScript & JSDoc + +In-IDE typing is available for use in at least mainstream editors. It can be added to JS projects using the @comfyorg/litegraph npm package. + +```ts +/** @import { IWidget } from './path/to/@comfyorg/litegraph/litegraph.d.ts' */ +/** @type IWidget */ +const widget = node.widgets[0] +widget.onPointerDown = function (pointer, node, canvas) { } +``` + +#### VS Code + +![image](https://github.com/user-attachments/assets/e14afd02-247f-44dc-acbf-6333027cd488) + +## Hovering over + +Adds API for downstream consumers to handle custom cursors. A bitwise enum of items, + +```typescript +type LGraphCanvasState = { + /** If `true`, pointer move events will set the canvas cursor style. */ + shouldSetCursor: boolean, + /** Bit flags indicating what is currently below the pointer. */ + hoveringOver: CanvasItem, + ... +} + +// Disable litegraph cursors +canvas.state.shouldSetCursor = false + +// Checking state - bit operators +if (canvas.state.hoveringOver & CanvasItem.ResizeSe) element.style.cursor = 'se-resize' +``` + + + + +# Removed public interfaces + +All are unused and incomplete. Have bugs beyond just typescript typing, and are (currently) not worth maintaining. If any of these features are desired down the track, they can be reimplemented. + +- Live mode +- Subgraph +- `dragged_node` diff --git a/src/CanvasPointer.ts b/src/CanvasPointer.ts index 23462f0..1c5e6ea 100644 --- a/src/CanvasPointer.ts +++ b/src/CanvasPointer.ts @@ -5,6 +5,11 @@ import { dist2 } from "./measure" /** * Allows click and drag actions to be declared ahead of time during a pointerdown event. * + * By default, it retains the most recent event of each type until it is reset (on pointerup). + * - {@link eDown} + * - {@link eMove} + * - {@link eUp} + * * Depending on whether the user clicks or drags the pointer, only the appropriate callbacks are called: * - {@link onClick} * - {@link onDoubleClick} diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index 3c32f6e..82cabfa 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -2517,6 +2517,13 @@ export class LGraphCanvas { #processWidgetClick(e: CanvasPointerEvent, node: LGraphNode, widget: IWidget) { const { pointer } = this + + // Custom widget - CanvasPointer + if (typeof widget.onPointerDown === "function") { + const handled = widget.onPointerDown(pointer, node, this) + if (handled) return + } + const width = widget.width || node.width const oldValue = widget.value diff --git a/src/types/widgets.ts b/src/types/widgets.ts index ae9ad49..b027c04 100644 --- a/src/types/widgets.ts +++ b/src/types/widgets.ts @@ -1,5 +1,5 @@ import { CanvasColour, Point, Size } from "../interfaces" -import type { LGraphCanvas, LGraphNode } from "../litegraph" +import type { CanvasPointer, LGraphCanvas, LGraphNode } from "../litegraph" import type { CanvasMouseEvent, CanvasPointerEvent } from "./events" export interface IWidgetOptions extends Record { @@ -151,4 +151,19 @@ export interface IBaseWidget { H: number, ): void computeSize?(width: number): Size + + /** + * Callback for pointerdown events, allowing custom widgets to register callbacks to occur + * for all {@link CanvasPointer} events. + * + * This callback is operated early in the pointerdown logic; actions that prevent it from firing are: + * - `Ctrl + Drag` Multi-select + * - `Alt + Click/Drag` Clone node + * @param pointer The CanvasPointer handling this event + * @param node The node this widget belongs to + * @param canvas The LGraphCanvas where this event originated + * @return Returning `true` from this callback forces Litegraph to ignore the event and + * not process it any further. + */ + onPointerDown(pointer: CanvasPointer, node: LGraphNode, canvas: LGraphCanvas): boolean }