mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-27 01:39:47 +00:00
Add CanvasPointer API (#308)
* Add position rounding feature Replaces previous impls. which only worked on some items, and were triggered when unexpected e.g. clicking a node that hadn't been moved. Update test expectations * Narrow TS types - readonly * nit - Clean up, Doc * nit - Clean up legacy accessors Marks as deprecated * Fix TS type - IContextMenuOptions.scale * [Refactor] dist2 for use in pointer API * Add CanvasPointer - API for pointer events Add TS strict types Add final click drag distance math Add option to retain events * nit - Rename * nit * Remove Subgraph - unused & not maintained * Remove live_mode Unused, not maintained. * Update README Remove live_mode reference * Update delete selected - include reroutes & groups * Bypass link menu if alt/shift pressed * Remove old dragged_node interface Incomplete impl. - unused. Superceded by selectedItems * Fix top/left edge of rectangles not in hitbox * [Refactor] Match function names to interface names * Add interface to find widgets by Point LGraphNode.getWidgetOnPos * Add widget search param - includeDisabled * nit - Doc * Rewrite canvas mouse handling - Rewrites most pointer handling to use CanvasPointer callbacks - All callbacks are declared ahead of time during the initial pointerdown event, logically grouped together - Drastically simplifies the alteration or creation of new click / drag interactions - Click events are all clicks, rather than some processed on mouse down, others on mouse up - Functions return instead of setting and repeatedly checking multiple state vars - Removes all lines that needed THIRTEEN tab indents * Split middle click out from processMouseDown * Use pointer API for link menus * Narrow canvas event interfaces * Fix canvas event types Replaces original workarounds with final types * Refactor - deprecated isInsideRectangle * Add canvas hovering over state - Centralises cursor set behaviour - Provides simple downstream override * nit * [Refactor] Use measure functions * Add double-click API to CanvasPointer a * nit - Doc * Allow larger gap between double click events * Rewrite double-click into CanvasPointer actions * Improve double-click UX Prefer down events over up events * Add production defaults * Add middle-click handling * Remove debug code * Remove redundant code * Fix add reroute alt-click adds two undo steps * Fix click on connected input disconnects Old behaviour was to disconnect, then recreate a new link on drop. * Add module export: CanvasPointer
This commit is contained in:
268
src/CanvasPointer.ts
Normal file
268
src/CanvasPointer.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import type { CanvasPointerEvent } from "./types/events"
|
||||
import type { LGraphCanvas } from "./LGraphCanvas"
|
||||
import { dist2 } from "./measure"
|
||||
|
||||
/**
|
||||
* Allows click and drag actions to be declared ahead of time during a pointerdown event.
|
||||
*
|
||||
* Depending on whether the user clicks or drags the pointer, only the appropriate callbacks are called:
|
||||
* - {@link onClick}
|
||||
* - {@link onDoubleClick}
|
||||
* - {@link onDragStart}
|
||||
* - {@link onDrag}
|
||||
* - {@link onDragEnd}
|
||||
* - {@link finally}
|
||||
*
|
||||
* @seealso
|
||||
* - {@link LGraphCanvas.processMouseDown}
|
||||
* - {@link LGraphCanvas.processMouseMove}
|
||||
* - {@link LGraphCanvas.processMouseUp}
|
||||
*/
|
||||
export class CanvasPointer {
|
||||
/** Maximum time in milliseconds to ignore click drift */
|
||||
static bufferTime = 150
|
||||
|
||||
/** Maximum gap between pointerup and pointerdown events to be considered as a double click */
|
||||
static doubleClickTime = 300
|
||||
|
||||
/** Maximum offset from click location */
|
||||
static get maxClickDrift() {
|
||||
return this.#maxClickDrift
|
||||
}
|
||||
static set maxClickDrift(value) {
|
||||
this.#maxClickDrift = value
|
||||
this.#maxClickDrift2 = value * value
|
||||
}
|
||||
static #maxClickDrift = 6
|
||||
/** {@link maxClickDrift} squared. Used to calculate click drift without `sqrt`. */
|
||||
static #maxClickDrift2 = this.#maxClickDrift ** 2
|
||||
|
||||
/** The element this PointerState should capture input against when dragging. */
|
||||
element: Element
|
||||
/** Pointer ID used by drag capture. */
|
||||
pointerId: number
|
||||
|
||||
/** Set to true when if the pointer moves far enough after a down event, before the corresponding up event is fired. */
|
||||
dragStarted: boolean = false
|
||||
|
||||
/** The {@link eUp} from the last successful click */
|
||||
eLastDown?: CanvasPointerEvent
|
||||
|
||||
/** Used downstream for touch event support. */
|
||||
isDouble: boolean = false
|
||||
/** Used downstream for touch event support. */
|
||||
isDown: boolean = false
|
||||
|
||||
/**
|
||||
* If `true`, {@link eDown}, {@link eMove}, and {@link eUp} will be set to
|
||||
* `null` when {@link reset} is called.
|
||||
*
|
||||
* Default: `true`
|
||||
*/
|
||||
clearEventsOnReset: boolean = true
|
||||
|
||||
/** The last pointerdown event for the primary button */
|
||||
eDown: CanvasPointerEvent | null = null
|
||||
/** The last pointermove event for the primary button */
|
||||
eMove: CanvasPointerEvent | null = null
|
||||
/** The last pointerup event for the primary button */
|
||||
eUp: CanvasPointerEvent | null = null
|
||||
|
||||
/** If set, as soon as the mouse moves outside the click drift threshold, this action is run once. */
|
||||
onDragStart?(): unknown
|
||||
|
||||
/**
|
||||
* Called on pointermove whilst dragging.
|
||||
* @param eMove The pointermove event of this ongoing drag action
|
||||
*/
|
||||
onDrag?(eMove: CanvasPointerEvent): unknown
|
||||
|
||||
/**
|
||||
* Called on pointerup after dragging (i.e. not called if clicked).
|
||||
* @param upEvent The pointerup or pointermove event that triggered this callback
|
||||
*/
|
||||
onDragEnd?(upEvent: CanvasPointerEvent): unknown
|
||||
|
||||
/**
|
||||
* Callback that will be run once, the next time a pointerup event appears to be a normal click.
|
||||
* @param upEvent The pointerup or pointermove event that triggered this callback
|
||||
*/
|
||||
onClick?(upEvent: CanvasPointerEvent): unknown
|
||||
|
||||
/**
|
||||
* Callback that will be run once, the next time a pointerup event appears to be a normal click.
|
||||
* @param upEvent The pointerup or pointermove event that triggered this callback
|
||||
*/
|
||||
onDoubleClick?(upEvent: CanvasPointerEvent): unknown
|
||||
|
||||
/**
|
||||
* Run-once callback, called at the end of any click or drag, whether or not it was successful in any way.
|
||||
*
|
||||
* The setter of this callback will call the existing value before replacing it.
|
||||
* Therefore, simply setting this value twice will execute the first callback.
|
||||
*/
|
||||
get finally() {
|
||||
return this.#finally
|
||||
}
|
||||
set finally(value) {
|
||||
try {
|
||||
this.#finally?.()
|
||||
} finally {
|
||||
this.#finally = value
|
||||
}
|
||||
}
|
||||
#finally?: () => unknown
|
||||
|
||||
constructor(element: Element) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `pointerdown` events. To be used as the event handler (or called by it).
|
||||
* @param e The `pointerdown` event
|
||||
*/
|
||||
down(e: CanvasPointerEvent): void {
|
||||
this.reset()
|
||||
this.eDown = e
|
||||
this.pointerId = e.pointerId
|
||||
this.element.setPointerCapture(e.pointerId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `pointermove` events. To be used as the event handler (or called by it).
|
||||
* @param e The `pointermove` event
|
||||
*/
|
||||
move(e: CanvasPointerEvent): void {
|
||||
const { eDown } = this
|
||||
if (!eDown) return
|
||||
|
||||
// No buttons down, but eDown exists - clean up & leave
|
||||
if (!e.buttons) {
|
||||
this.reset()
|
||||
return
|
||||
}
|
||||
|
||||
// Primary button released - treat as pointerup.
|
||||
if (!(e.buttons & eDown.buttons)) {
|
||||
this.#completeClick(e)
|
||||
this.reset()
|
||||
return
|
||||
}
|
||||
this.eMove = e
|
||||
this.onDrag?.(e)
|
||||
|
||||
// Dragging, but no callback to run
|
||||
if (this.dragStarted) return
|
||||
|
||||
const longerThanBufferTime = e.timeStamp - eDown.timeStamp > CanvasPointer.bufferTime
|
||||
if (longerThanBufferTime || !this.#hasSamePosition(e, eDown)) {
|
||||
this.#setDragStarted()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `pointerup` events. To be used as the event handler (or called by it).
|
||||
* @param e The `pointerup` event
|
||||
*/
|
||||
up(e: CanvasPointerEvent): boolean {
|
||||
if (e.button !== this.eDown?.button) return false
|
||||
|
||||
this.#completeClick(e)
|
||||
const { dragStarted } = this
|
||||
this.reset()
|
||||
return !dragStarted
|
||||
}
|
||||
|
||||
#completeClick(e: CanvasPointerEvent): void {
|
||||
const { eDown } = this
|
||||
if (!eDown) return
|
||||
|
||||
this.eUp = e
|
||||
|
||||
if (this.dragStarted) {
|
||||
// A move event already started drag
|
||||
this.onDragEnd?.(e)
|
||||
} else if (!this.#hasSamePosition(e, eDown)) {
|
||||
// Teleport without a move event (e.g. tab out, move, tab back)
|
||||
this.#setDragStarted()
|
||||
this.onDragEnd?.(e)
|
||||
} else if (this.onDoubleClick && this.#isDoubleClick()) {
|
||||
// Double-click event
|
||||
this.onDoubleClick(e)
|
||||
this.eLastDown = undefined
|
||||
} else {
|
||||
// Normal click event
|
||||
this.onClick?.(e)
|
||||
this.eLastDown = eDown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two events occurred near each other - not further apart than the maximum click drift.
|
||||
* @param a The first event to compare
|
||||
* @param b The second event to compare
|
||||
* @param tolerance2 The maximum distance (squared) before the positions are considered different
|
||||
* @returns `true` if the two events were no more than {@link maxClickDrift} apart, otherwise `false`
|
||||
*/
|
||||
#hasSamePosition(
|
||||
a: PointerEvent,
|
||||
b: PointerEvent,
|
||||
tolerance2 = CanvasPointer.#maxClickDrift2,
|
||||
): boolean {
|
||||
const drift = dist2(a.clientX, a.clientY, b.clientX, b.clientY)
|
||||
return drift <= tolerance2
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the pointer is currently past the max click drift threshold.
|
||||
* @param e The latest pointer event
|
||||
* @returns `true` if the latest pointer event is past the the click drift threshold
|
||||
*/
|
||||
#isDoubleClick(): boolean {
|
||||
const { eDown, eLastDown } = this
|
||||
if (!eDown || !eLastDown) return false
|
||||
|
||||
// Use thrice the drift distance for double-click gap
|
||||
const tolerance2 = (3 * CanvasPointer.#maxClickDrift) ** 2
|
||||
const diff = eDown.timeStamp - eLastDown.timeStamp
|
||||
return diff > 0 &&
|
||||
diff < CanvasPointer.doubleClickTime &&
|
||||
this.#hasSamePosition(eDown, eLastDown, tolerance2)
|
||||
}
|
||||
|
||||
#setDragStarted(): void {
|
||||
this.dragStarted = true
|
||||
this.onDragStart?.()
|
||||
delete this.onDragStart
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the state of this {@link CanvasPointer} instance.
|
||||
*
|
||||
* The {@link finally} callback is first executed, then all callbacks and intra-click
|
||||
* state is cleared.
|
||||
*/
|
||||
reset(): void {
|
||||
// The setter executes the callback before clearing it
|
||||
this.finally = undefined
|
||||
delete this.onClick
|
||||
delete this.onDoubleClick
|
||||
delete this.onDragStart
|
||||
delete this.onDrag
|
||||
delete this.onDragEnd
|
||||
|
||||
this.isDown = false
|
||||
this.isDouble = false
|
||||
this.dragStarted = false
|
||||
|
||||
if (this.clearEventsOnReset) {
|
||||
this.eDown = null
|
||||
this.eMove = null
|
||||
this.eUp = null
|
||||
}
|
||||
|
||||
const { element, pointerId } = this
|
||||
if (element.hasPointerCapture(pointerId))
|
||||
element.releasePointerCapture(pointerId)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user