Support input from drawing tablets on web (#4445)

* Rename the editor mouse state types to Pointer* and the input_ modules

* Add optional pen attributes to the editor pointer state types

* Desktop: Send tablet pointer clicks to CEF as mouse clicks

* Desktop: End pointer lock and pending window drags on any left-equivalent release

* Desktop: Route pointer input carrying pen attributes directly to the editor

* Desktop: Route wheel and pinch input inside the viewport directly to the editor

* Desktop: Track double clicks on the direct editor route

* Desktop: Move the pointer lock position into InputState

* Restore the input state pointer position when pointer lock ends (review)

* Fix pen input being swallowed by Windows resize helper

* Update winit to 0.31.0-beta.2 from crates.io

* Add event time to the editor pointer states

The brush trace's hot core is position plus per-sample time, and the
frame clock is too coarse for high-rate tablets: several samples share
one timestamp and speed dynamics see dt = 0. The desktop stamps
editor-routed pointer input with a monotonic arrival time since winit
does not surface hardware timestamps; the web path can thread the
PointerEvent timeStamp through later.

* Capture pen input via browser api

* Replay coalesced pointer samples for smoother pen strokes

* Handle missing or empty coalesced pen events
This commit is contained in:
Timon
2026-08-27 15:18:59 +00:00
committed by GitHub
parent 5a16e18b2a
commit 681b38d3d6
2 changed files with 87 additions and 9 deletions

View File

@@ -123,7 +123,10 @@ export function onPointerMove(e: PointerEvent, editor: EditorWrapper, documentSt
const modifiers = makeKeyboardModifiersBitfield(e);
if (detectShake(e)) editor.onMouseShake(e.clientX, e.clientY, e.buttons, modifiers);
editor.onMouseMove(e.clientX, e.clientY, e.buttons, modifiers);
const coalesced = e.pointerType === "pen" && typeof e.getCoalescedEvents === "function" ? e.getCoalescedEvents() : [];
const samples = coalesced.length > 0 ? coalesced : [e];
for (const sample of samples) editor.onMouseMove(sample.clientX, sample.clientY, sample.buttons, modifiers, ...pointerAttributes(sample));
}
export function onPointerDown(e: PointerEvent, editor: EditorWrapper, dialogStore: DialogStore) {
@@ -153,7 +156,7 @@ export function onPointerDown(e: PointerEvent, editor: EditorWrapper, dialogStor
if (viewportPointerInteractionOngoing && isTargetingCanvas instanceof Element) {
const modifiers = makeKeyboardModifiersBitfield(e);
editor.onMouseDown(e.clientX, e.clientY, e.buttons, modifiers);
editor.onMouseDown(e.clientX, e.clientY, e.buttons, modifiers, ...pointerAttributes(e));
}
}
@@ -171,7 +174,7 @@ export function onPointerUp(e: PointerEvent, editor: EditorWrapper) {
if (textToolInteractiveInputElement) return;
const modifiers = makeKeyboardModifiersBitfield(e);
editor.onMouseUp(e.clientX, e.clientY, e.buttons, modifiers);
editor.onMouseUp(e.clientX, e.clientY, e.buttons, modifiers, ...pointerAttributes(e));
}
// Mouse events
@@ -278,6 +281,12 @@ export function onFocusOut() {
canvasFocused = false;
}
function pointerAttributes(e: PointerEvent): [number, number | undefined, number | undefined, number | undefined, number | undefined, number | undefined, boolean] {
const isPen = e.pointerType === "pen";
const eraser = isPen && (e.buttons & 0b10_0000) !== 0; // Pen eraser end is reported as the 32 button bit
return [e.timeStamp, isPen ? e.pressure : undefined, isPen ? e.tiltX : undefined, isPen ? e.tiltY : undefined, isPen ? e.twist : undefined, isPen ? e.tangentialPressure : undefined, eraser];
}
function detectShake(e: PointerEvent | MouseEvent): boolean {
const SENSITIVITY_DIRECTION_CHANGES = 3;
const SENSITIVITY_DISTANCE_TO_DISPLACEMENT_RATIO = 0.1;