-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
- Loading branch information
1 parent
f26f7db
commit b29a32c
Showing
14 changed files
with
1,266 additions
and
1,363 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.