Back to Tldraw

Input handling

apps/docs/content/sdk-features/input-handling.mdx

5.4.07.9 KB
Original Source

The InputsManager class tracks pointer and keyboard state for the editor. It stores pointer positions in both screen space and page space, tracks pressed keys and buttons, detects device types (mouse, touch, pen), and calculates pointer velocity. Access it through editor.inputs.

All input state is reactive. The manager stores values as atoms from @tldraw/state, so components that read input state automatically update when those values change. The manager updates on every input event, converting coordinates between screen space and page space.

Pointer position tracking

The manager tracks pointer positions in two coordinate spaces. Screen space is pixels relative to the canvas container's origin. Page space is the position on the infinite canvas, adjusted for camera position and zoom.

In each space, the manager keeps three positions: current, previous, and origin.

Current and previous positions

typescript
editor.inputs.getCurrentScreenPoint() // Current position in screen space
editor.inputs.getCurrentPagePoint() // Current position in page space

editor.inputs.getPreviousScreenPoint() // Previous position in screen space
editor.inputs.getPreviousPagePoint() // Previous position in page space

The current position updates on every pointer event. The previous position stores where the pointer was before the most recent update. You can use these together to calculate deltas for dragging and panning:

typescript
const delta = Vec.Sub(editor.inputs.getCurrentPagePoint(), editor.inputs.getPreviousPagePoint())

Origin positions

The origin position captures where the most recent pointer_down event occurred:

typescript
editor.inputs.getOriginScreenPoint() // Where pointer_down occurred in screen space
editor.inputs.getOriginPagePoint() // Where pointer_down occurred in page space

Tools use the origin to calculate drag distances and determine whether an interaction has moved far enough to trigger behaviors like dragging. The origin resets on every pointer_down event and continuously while a pinch is in progress.

Coordinate space conversion

The manager converts screen coordinates to page coordinates using the camera's position and zoom:

typescript
// Screen to page conversion
const pageX = screenX / camera.z - camera.x
const pageY = screenY / camera.z - camera.y

Pointer velocity

The manager tracks pointer velocity for gesture detection:

typescript
editor.inputs.getPointerVelocity() // Vec with x/y velocity in pixels per millisecond

The manager listens to the editor's frame event and recomputes velocity once per frame (not on each pointer event) from the screen-space distance traveled since the previous frame. It smooths the result against the previous value and clamps components below 0.01 to zero to prevent jitter.

Tools use velocity to distinguish between slow, precise interactions and fast flick gestures. Velocity resets to zero on pointer_down events and continuously while a pinch is in progress.

Input device detection

The manager tracks whether the most recent pointer event came from a pen (pointerType === 'pen'):

typescript
editor.inputs.getIsPen() // true for stylus input

Pen mode ignores non-pen input to prevent accidental touch interactions while using a stylus. The editor only turns pen mode on automatically for direct-display pens (Apple Pencil, Surface Pen), flagged as isPenDirect on the pointer event; desktop graphics tablets still draw as pens without enabling it.

Modifier keys and button states

The manager tracks modifier key states:

typescript
editor.inputs.getShiftKey()
editor.inputs.getAltKey()
editor.inputs.getCtrlKey()
editor.inputs.getMetaKey()
editor.inputs.getAccelKey() // Cmd on Mac, Ctrl elsewhere

The getAccelKey() method returns true for Command on macOS and Control on other platforms. Use this for cross-platform shortcuts.

Button tracking

The manager tracks currently pressed pointer buttons in a reactive set:

typescript
editor.inputs.buttons.has(0) // Primary button (left click)
editor.inputs.buttons.has(1) // Middle button
editor.inputs.buttons.has(2) // Secondary button (right click)

Buttons are added on pointer_down events and removed on pointer_up events.

Keyboard key tracking

The manager tracks pressed keyboard keys in a reactive set:

typescript
editor.inputs.keys.has('Space')
editor.inputs.keys.has('ShiftLeft')

The editor adds keys on key_down and removes them on key_up. Tools can use this to detect held keys during pointer operations. For example, the editor checks keys.has('Space') on pointer up to decide whether to keep spacebar panning active.

Interaction state flags

The manager tracks the current interaction state:

typescript
editor.inputs.getIsPointing() // Pointer button is down
editor.inputs.getIsRightPointing() // Right button is down, before the drag threshold
editor.inputs.getIsDragging() // Pointer moved beyond drag threshold while pointing
editor.inputs.getIsPinching() // Two-finger pinch gesture active
editor.inputs.getIsEditing() // Editing text or other content
editor.inputs.getIsPanning() // Panning the canvas
editor.inputs.getIsSpacebarPanning() // Panning via spacebar (vs. other panning modes)

The editor sets these flags during event processing. For example, isPointing becomes true on pointer_down and false on pointer_up. The isDragging flag becomes true when the pointer moves beyond the drag distance threshold (dragDistanceSquared or coarseDragDistanceSquared in the editor's options) while pointing.

Event processing flow

When an input event occurs, the editor processes it through these stages:

  1. The browser fires a native DOM event
  2. The canvas event handlers transform it into a typed event info object and call Editor#dispatch
  3. pointer_move, wheel, and pinch events are queued and flushed once per frame; other events flush immediately
  4. The editor emits before-event, then updates modifier key state
  5. For pointer, pinch, and wheel events, the editor calls updateFromEvent() on the InputsManager
  6. For pointer events, the editor updates buttons and interaction flags, then hands the event to the ClickManager for double-click detection
  7. The editor sends the event to the state machine via root.handleEvent(), which propagates it through active tool states, then emits event

See Events for subscribing to before-event and event.

The typed event info objects are:

Event typeInfo type
Pointer eventsTLPointerEventInfo
Click eventsTLClickEventInfo
Keyboard eventsTLKeyboardEventInfo
Wheel eventsTLWheelEventInfo
Pinch eventsTLPinchEventInfo

In collaborative sessions, updateFromEvent() also updates the user's pointer presence record in the store, broadcasting pointer position to other users.

Input normalization

The editor listens to Pointer Events, so mouse, touch, and pen input arrive through the same handlers, and the manager tracks the device type through the isPen flag.

Pointer positions include a z coordinate carrying the pointer event's pressure; synthetic events without a z default to 0.5. The manager subtracts the container's screen bounds from client coordinates, so the editor works correctly in nested layouts and scrolled containers.

State serialization

The manager provides a toJson() method for debugging:

typescript
const state = editor.inputs.toJson()
// Returns all position vectors, modifier key states, interaction flags,
// device type, and the contents of the keys and buttons sets

We use this serialized form when generating crash reports.

  • Reactive inputs - Display pointer positions, velocity, and other input state reactively
  • Canvas events - Log pointer, keyboard, and wheel events to see the event flow