Pointer<E> — typed pointer events
Click, drag, hover, scroll. 15 typed variants. One dispatcher. Bubble walks the scene tree until a handler calls
stop_bubble().
Sketchpad (1963)
In 1963 Ivan Sutherland defended his MIT PhD thesis with a working program called Sketchpad. The demo showed a young man at a TX-2 mainframe holding a light pen — a stylus the size of a pencil wired to a vacuum-tube display. He drew a line on the screen with the pen. He drew another. He pointed at the first line and made it horizontal. Then he selected both lines and constrained them to be the same length. As he dragged one endpoint, both lines updated in real time.
Sketchpad introduced two ideas that PointerPress, Drag, and
Click variants instead of one polymorphic "pointer happened"
callback.
The 15-variant taxonomy
flowchart TD
A[pointer enters target] --> Over
B[pointer leaves target] --> Out
C[pointer moves while over] --> Move
D[OS cancelled] --> Cancel
E[button down] --> Press
F[button up on press path] --> Release
G[press + release on same target] --> Click
H[press + 5px move] --> DragStart
I[motion while dragging] --> Drag
J[release while dragging] --> DragEnd
K[wheel rotated] --> Scroll
L[drag enters another node] --> DragEnter
M[drag is over another node] --> DragOver
N[drag leaves another node] --> DragLeave
O[drag released over target] --> DragDrop
15 variants because the kind of pointer event determines what the handler is doing — a click handler shouldn't fire on a stray hover move, and a drag handler shouldn't fire on a single click.
Press-path bookkeeping (PixiJS pattern)
A common bug in naive event systems: user presses on button A, drags off the button, releases on the background. Naive code emits Release on the background — but the press fired on A, so logically the release should fire on A too (so the button can un-highlight).
We solve this with the press-path bookkeeping pattern from
PixiJS's EventBoundary.ts:677-708: at press time, record the
ancestor chain of the target node. At release time, replay the
release on that recorded chain regardless of where the pointer
landed. Same for DragEnd.
The downside is one HashMap of state per PointerId. The upside is
draggable UI elements that survive the pointer leaving the host
canvas entirely (a frequent web-browser failure mode).
Stop bubbling — interior mutability via Cell<bool>
Pointer<E> carries a Cell<bool> bubble_stopped flag. Handlers
have Fn(&Pointer<E>) signature (no &mut), but they can still
halt ancestor dispatch by calling event.stop_bubble(). The
dispatcher reads the flag after each handler and returns early if
set.
Quickstart
#![allow(unused)] fn main() { use wisp_interaction::{ CallbackRegistry, Click, Pointer, PointerId, }; let mut registry = CallbackRegistry::new(); registry.on_click(my_button_node, |e: &Pointer<Click>| { println!("clicked on {:?} at {:?}", e.target, e.location.viewport); e.stop_bubble(); // parent handlers won't fire }); }
Multi-touch is free
PointerId::{Mouse, Touch(u64), Custom(u128)} keys the per-pointer
state map. Two fingers on a touchscreen produce two distinct press
paths — two clicks total, not one merged "average" click. The
dispatcher walks each independently.