teksilo_core/pointer.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! The pointer vocabulary: who is pointing, when, and with what.
5//!
6//! Every input sample that reaches the widget tree is described by this
7//! module's types. A mouse, a finger and a stylus differ in tuning
8//! ([`PointerKind`], which lives in `teksilo-tokens` so a token struct can name
9//! it) but not in shape: they all arrive as a [`PointerSample`] carrying a
10//! [`PointerInfo`], and they are all timed by one [`EventTime`] measured from
11//! one tree epoch.
12//!
13//! # Identity
14//!
15//! [`PointerId`] is minted per *press*, not per device, by the process-global
16//! [`PointerIdAllocator`]. That is deliberate: winit **reuses** `Touch::id`
17//! values once a contact lifts, so a table keyed on the raw OS id can attribute
18//! a new contact's samples to the sequence the previous one left behind. A
19//! fresh id per Down defeats that without needing a generation counter — the
20//! allocator is monotonic, so an id is never handed out twice in one process.
21//!
22//! The one exception is [`PointerId::MOUSE`], the stable id every synthesized
23//! mouse event uses. A mouse is singular by construction, so it needs no
24//! per-press identity and legacy call sites can name it without an allocation.
25//!
26//! # Time
27//!
28//! [`EventTime`] is a `Duration` since the tree's epoch, never an `Instant`.
29//! See [`clock`] for why that matters and for the one-clock rule.
30//!
31//! Reference: `docs/touch-and-pen.md`.
32
33pub mod clock;
34pub mod hit_slop;
35pub mod table;
36pub mod touch_action;
37pub mod trace;
38
39use std::collections::HashMap;
40use std::num::NonZeroU64;
41use std::sync::atomic::{AtomicU64, Ordering};
42use std::sync::{Mutex, OnceLock};
43use std::time::Duration;
44
45use teksilo_canvas::{Point, Size};
46use teksilo_tokens::PointerKind;
47
48use crate::event::{ButtonMask, Modifiers, PointerButton, ScrollDelta};
49
50// ---------------------------------------------------------------------------
51// Identity
52// ---------------------------------------------------------------------------
53
54/// A process-unique, monotonically increasing pointer identity.
55///
56/// Minted per press by [`PointerIdAllocator`] (see the module docs for why per
57/// press rather than per device). `NonZeroU64` so `Option<PointerId>` is the
58/// same size as `PointerId`, and so id `0` can never be confused with "no
59/// pointer".
60///
61/// Ordering is by mint order, which makes a `BTreeMap<PointerId, _>` iterate
62/// oldest contact first — the order a multi-touch consumer wants.
63#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
64pub struct PointerId(NonZeroU64);
65
66impl PointerId {
67 /// The one stable id every synthesized mouse event uses.
68 ///
69 /// A mouse is singular: there is at most one of it, it never lifts, and
70 /// nothing about it needs a per-press identity. Reserving id 1 for it keeps
71 /// the legacy `dispatch_event` path allocation-free and makes "is this the
72 /// mouse?" a comparison rather than a lookup.
73 pub const MOUSE: Self = Self(NonZeroU64::new(1).unwrap());
74
75 /// The raw value, for a backend that must store the id compactly or hand
76 /// it to a C API. Never construct a `PointerId` from one of these — only
77 /// the allocator may mint.
78 pub const fn get(self) -> u64 {
79 self.0.get()
80 }
81}
82
83/// An opaque per-device key, derived by the platform layer from the backend's
84/// own device handle (winit's `DeviceId`, a Win32 `HANDLE`, an evdev node).
85///
86/// Two different devices can report the same OS-level contact id at the same
87/// time — two touchscreens, or a touchscreen and a digitizer — so the
88/// allocator's live table is keyed on `(device, os_id)` rather than `os_id`
89/// alone. Teksilo never interprets the value; it only needs it to be stable for
90/// the life of a device and distinct between devices.
91#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
92pub struct BackendDeviceKey(u64);
93
94impl BackendDeviceKey {
95 /// The key a backend with no device concept uses (a single-touchscreen
96 /// platform, a test harness).
97 pub const DEFAULT: Self = Self(0);
98
99 /// Wrap a backend-derived value.
100 pub const fn new(raw: u64) -> Self {
101 Self(raw)
102 }
103
104 /// The wrapped value.
105 pub const fn get(self) -> u64 {
106 self.0
107 }
108}
109
110/// Mints [`PointerId`]s and maps a backend's reused contact ids onto them.
111///
112/// One instance per process, reached through [`PointerIdAllocator::global`].
113/// The mapping is `(device, os_id) -> PointerId`, established at [`begin`] and
114/// torn down at [`end`]; [`get`] resolves the samples in between.
115///
116/// [`begin`]: Self::begin
117/// [`end`]: Self::end
118/// [`get`]: Self::get
119#[derive(Debug)]
120pub struct PointerIdAllocator {
121 /// Next id to hand out. Starts past [`PointerId::MOUSE`].
122 next: AtomicU64,
123 /// Live `(device, os_id) -> id` mappings, one per contact currently down.
124 ///
125 /// A `Mutex` rather than a `RefCell` because the platform layer may mint
126 /// from a backend thread (X11's XDND helper connection already runs on
127 /// one). It is never held across a dispatch, so it is always uncontended
128 /// in practice.
129 live: Mutex<HashMap<(BackendDeviceKey, u64), PointerId>>,
130}
131
132/// The one allocator. Not `pub`: reached through
133/// [`PointerIdAllocator::global`].
134static GLOBAL_ALLOCATOR: OnceLock<PointerIdAllocator> = OnceLock::new();
135
136impl PointerIdAllocator {
137 /// The process-global allocator.
138 pub fn global() -> &'static Self {
139 GLOBAL_ALLOCATOR.get_or_init(|| Self {
140 // 1 is `PointerId::MOUSE`; real pointers start at 2.
141 next: AtomicU64::new(2),
142 live: Mutex::new(HashMap::new()),
143 })
144 }
145
146 /// Mint a fresh id for a press and remember it for `(device, os_id)`.
147 ///
148 /// A second `begin` on a key that is already live *replaces* the mapping
149 /// and returns a new id — a backend that drops an Up (a lost contact, a
150 /// window that stopped receiving events mid-gesture) must not strand the
151 /// next press on the old identity.
152 pub fn begin(&self, device: BackendDeviceKey, os_id: u64) -> PointerId {
153 let raw = self.next.fetch_add(1, Ordering::Relaxed);
154 let id = PointerId(NonZeroU64::new(raw).expect("allocator starts at 2 and only grows"));
155 if let Ok(mut live) = self.live.lock() {
156 live.insert((device, os_id), id);
157 }
158 id
159 }
160
161 /// The id minted for a live `(device, os_id)`, if the contact is still
162 /// down. `None` after [`end`](Self::end), which is what makes a reused OS
163 /// id resolve to a *new* [`PointerId`] rather than the stale one.
164 pub fn get(&self, device: BackendDeviceKey, os_id: u64) -> Option<PointerId> {
165 self.live
166 .lock()
167 .ok()
168 .and_then(|live| live.get(&(device, os_id)).copied())
169 }
170
171 /// Forget the mapping for a lifted contact. Idempotent; returns the id that
172 /// was live, if any.
173 pub fn end(&self, device: BackendDeviceKey, os_id: u64) -> Option<PointerId> {
174 self.live
175 .lock()
176 .ok()
177 .and_then(|mut live| live.remove(&(device, os_id)))
178 }
179
180 /// Number of contacts currently mapped. Test/diagnostic helper.
181 pub fn live_count(&self) -> usize {
182 self.live.lock().map(|live| live.len()).unwrap_or(0)
183 }
184}
185
186// ---------------------------------------------------------------------------
187// Time
188// ---------------------------------------------------------------------------
189
190/// A moment on the input timeline, measured from the tree's epoch.
191///
192/// A `Duration`, never an `Instant`: a recognizer that reads `Instant::now()`
193/// cannot be driven by a simulated clock, and a test that cannot advance the
194/// clock cannot test a long press, a fling or a double-tap window without
195/// sleeping. Every deadline in the gesture layer is expressed as an
196/// `EventTime`, and the tree's one [`InputClock`](clock::InputClock) is the
197/// only thing that produces one.
198#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
199pub struct EventTime(Duration);
200
201impl EventTime {
202 /// The epoch itself.
203 pub const ZERO: Self = Self(Duration::ZERO);
204
205 /// A time this far after the epoch.
206 pub const fn from_duration(d: Duration) -> Self {
207 Self(d)
208 }
209
210 /// Milliseconds after the epoch. Convenience for tests and for backends
211 /// whose timestamps arrive in milliseconds.
212 pub const fn from_millis(ms: u64) -> Self {
213 Self(Duration::from_millis(ms))
214 }
215
216 /// How far this is after the epoch.
217 pub const fn as_duration(self) -> Duration {
218 self.0
219 }
220
221 /// How long after `earlier` this is, saturating at zero.
222 ///
223 /// Saturating rather than panicking because samples can arrive out of
224 /// order: a backend that batches coalesced moves may hand over a packet
225 /// whose timestamps precede the last one already processed, and a gesture
226 /// must degrade to "no time passed" rather than abort.
227 pub const fn saturating_since(self, earlier: Self) -> Duration {
228 self.0.saturating_sub(earlier.0)
229 }
230
231 /// This time plus `d`, or `None` on overflow.
232 ///
233 /// Deadlines are computed this way (`now.checked_add(long_press)`), so the
234 /// overflow case is real rather than theoretical for a caller that passes
235 /// `Duration::MAX` to mean "never".
236 pub fn checked_add(self, d: Duration) -> Option<Self> {
237 self.0.checked_add(d).map(Self)
238 }
239}
240
241impl std::ops::Add<Duration> for EventTime {
242 type Output = Self;
243
244 /// `now + d`, saturating at [`Duration::MAX`].
245 ///
246 /// Saturating rather than panicking for the same reason
247 /// [`checked_add`](EventTime::checked_add) exists: `Duration::MAX` is a
248 /// legitimate way to say "never", and a deadline arithmetic panic in the
249 /// middle of a gesture would be absurd. Reach for `checked_add` where the
250 /// overflow itself has to be observed.
251 fn add(self, d: Duration) -> Self {
252 Self(self.0.saturating_add(d))
253 }
254}
255
256// ---------------------------------------------------------------------------
257// Sample payloads
258// ---------------------------------------------------------------------------
259
260/// The continuous per-sample axes a device may report beyond position.
261///
262/// Every field is optional because every field is optional in the hardware: a
263/// mouse reports none of them, a touchscreen usually reports a contact patch
264/// and sometimes a pressure, a good digitizer reports all five.
265///
266/// `#[non_exhaustive]`: barrel rotation, hover distance and per-axis tilt
267/// resolution are all plausible additions.
268#[non_exhaustive]
269#[derive(Copy, Clone, Debug, Default, PartialEq)]
270pub struct PointerAxes {
271 /// Normalised tip pressure, `0.0..=1.0`. See
272 /// [`PointerInfo::effective_pressure`] for the value a consumer should
273 /// actually read.
274 pub pressure: Option<f32>,
275 /// Normalised barrel-button pressure, `0.0..=1.0` (or `-1.0..=1.0` for a
276 /// device with a centred rest position), as in the W3C Pointer Events
277 /// `tangentialPressure`.
278 pub tangential_pressure: Option<f32>,
279 /// Stylus tilt as `(tilt_x, tilt_y)` in degrees, each `-90.0..=90.0`.
280 pub tilt: Option<(f32, f32)>,
281 /// Stylus barrel rotation in degrees, `0.0..=359.0`.
282 pub twist: Option<f32>,
283 /// The size of the contact patch in logical pixels. A finger's ellipse; a
284 /// palm's is what a rejection heuristic reads.
285 pub contact: Option<Size>,
286}
287
288/// Everything that identifies and describes the pointer producing a sample.
289///
290/// Carried by [`PointerSample`], [`ScrollSample`] and (from stage 1 of the
291/// event-shape landing) by the scroll and cancel `WidgetEvent`s.
292///
293/// `#[non_exhaustive]`: construct one through [`mouse`](Self::mouse) /
294/// [`touch`](Self::touch) and adjust fields, so a later field cannot break a
295/// call site.
296#[non_exhaustive]
297#[derive(Copy, Clone, Debug, PartialEq)]
298pub struct PointerInfo {
299 /// Which pointer this is. See the module docs on per-press identity.
300 pub id: PointerId,
301 /// What kind of device it is — the axis gesture tuning reads.
302 pub kind: PointerKind,
303 /// The W3C Pointer Events `isPrimary` flag, which is **per kind**: every
304 /// mouse sample is primary, and so is the first contact of a touch sequence.
305 /// On a machine with both, a mouse and a first finger are *both* primary at
306 /// once. It is a property of the sample, set by whoever produced it, and
307 /// [`PointerTable`](table::PointerTable) never rewrites it.
308 ///
309 /// **Not** the framework's singular pointer. The one that drives the legacy
310 /// singular signals — `hovered`, the cursor, the one `PointerDown` a widget
311 /// that knows nothing of multi-touch sees — is
312 /// [`PointerTable::primary`](table::PointerTable::primary), a table-level
313 /// election that exactly one live pointer holds and that a mouse always wins.
314 /// A third notion, [`PointerTable::hover_owner`](table::PointerTable::hover_owner),
315 /// is the most recent hovering-*capable* pointer. `table.rs`'s module docs
316 /// separate all three; reading this flag as either of the others is the
317 /// mistake the separation exists to prevent.
318 pub primary: bool,
319 /// The buttons held *after* this sample is applied. A press sets its own
320 /// bit; a release clears it. Empty for a hovering pointer.
321 pub buttons: ButtonMask,
322 /// The continuous axes, where the device reports them.
323 pub axes: PointerAxes,
324 /// When the backend says this sample happened, on the tree's timeline.
325 pub time: EventTime,
326 /// The digitizer classified this contact as a palm rather than a
327 /// deliberate touch.
328 ///
329 /// Only a backend that advertises `reports_palm` ever sets it; everything
330 /// else leaves it `false`, which is why no existing call site changes
331 /// meaning. A flagged sample is refused by
332 /// [`PointerTable::begin`](table::PointerTable::begin) and never reaches a
333 /// widget: rejecting it at the table is what keeps a hand resting on a
334 /// tablet from opening menus, and it is one decision rather than one per
335 /// recognizer.
336 pub palm: bool,
337}
338
339impl PointerInfo {
340 /// The mouse: [`PointerId::MOUSE`], [`PointerKind::Mouse`], primary, no
341 /// buttons held, no axes.
342 ///
343 /// This is what every legacy `WidgetEvent` constructor defaults to, so a
344 /// call site that says nothing about pointers keeps meaning exactly what it
345 /// meant before the touch programme.
346 pub const fn mouse(time: EventTime) -> Self {
347 Self {
348 id: PointerId::MOUSE,
349 kind: PointerKind::Mouse,
350 primary: true,
351 buttons: ButtonMask::NONE,
352 axes: PointerAxes {
353 pressure: None,
354 tangential_pressure: None,
355 tilt: None,
356 twist: None,
357 contact: None,
358 },
359 time,
360 palm: false,
361 }
362 }
363
364 /// A touch contact, with the W3C per-kind [`primary`](Self::primary) flag
365 /// **clear**. A producer that knows this is the first contact of a sequence
366 /// sets it; the platform translator does. The pointer table's own election is
367 /// a separate thing and never writes this field.
368 pub const fn touch(id: PointerId, time: EventTime) -> Self {
369 Self {
370 id,
371 kind: PointerKind::Touch,
372 primary: false,
373 buttons: ButtonMask::NONE,
374 axes: PointerAxes {
375 pressure: None,
376 tangential_pressure: None,
377 tilt: None,
378 twist: None,
379 contact: None,
380 },
381 time,
382 palm: false,
383 }
384 }
385
386 /// Whether the user points at the pixel directly — see
387 /// [`PointerKind::is_direct`].
388 pub const fn is_direct(&self) -> bool {
389 self.kind.is_direct()
390 }
391
392 /// Whether the contact patch is large enough that the reported point is an
393 /// estimate — see [`PointerKind::is_coarse`].
394 pub const fn is_coarse(&self) -> bool {
395 self.kind.is_coarse()
396 }
397
398 /// Whether the reported position is accurate to about a pixel — see
399 /// [`PointerKind::is_precise`].
400 pub const fn is_precise(&self) -> bool {
401 self.kind.is_precise()
402 }
403
404 /// The pressure a consumer should read: the reported value if the device
405 /// gave one, else `0.5` while any button is down, else `0.0`.
406 ///
407 /// This is the W3C Pointer Events Level 3 rule for `pressure`, and it
408 /// exists so a pressure-sensitive surface (a brush, a force-touch
409 /// affordance) has a defined answer for a mouse without special-casing it.
410 pub fn effective_pressure(&self) -> f32 {
411 match self.axes.pressure {
412 Some(p) => p,
413 None if !self.buttons.is_empty() => 0.5,
414 None => 0.0,
415 }
416 }
417}
418
419/// Which end of a pointer's life a sample sits at.
420///
421/// `Cancel` is not an Up: an Up means the user completed the interaction, a
422/// Cancel means the system took it away (see [`CancelReason`]). Conflating them
423/// is how a drag whose window lost focus ends up "dropped" where the pointer
424/// happened to be.
425#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
426pub enum PointerPhase {
427 /// The pointer came down / a button went down.
428 Down,
429 /// The pointer moved (with or without buttons held).
430 Move,
431 /// The pointer lifted / a button went up.
432 Up,
433 /// The system revoked the pointer. Carries no meaningful end position.
434 Cancel,
435}
436
437/// One pointer sample as it enters the tree.
438///
439/// The unit [`WidgetTree::dispatch_pointer`](crate::WidgetTree::dispatch_pointer)
440/// consumes. A backend produces exactly one of these per OS packet, folding any
441/// intermediate positions the OS batched into [`coalesced`](Self::coalesced).
442#[derive(Clone, Debug)]
443pub struct PointerSample {
444 /// Who is pointing.
445 pub pointer: PointerInfo,
446 /// What happened.
447 pub phase: PointerPhase,
448 /// Where, in window-logical coordinates.
449 pub position: Point,
450 /// The button that changed on a [`Down`](PointerPhase::Down) or
451 /// [`Up`](PointerPhase::Up). `None` for a move, a cancel, and for a
452 /// buttonless direct-pointer contact.
453 pub button: Option<PointerButton>,
454 /// Modifier keys held when the sample was produced.
455 pub modifiers: Modifiers,
456 /// Positions the OS batched into this packet, oldest first, *excluding*
457 /// [`position`](Self::position) (which is the newest).
458 ///
459 /// A velocity tracker integrates over these; a drawing surface draws
460 /// through them; everything else ignores them. Deliberately a `Vec` and not
461 /// a `SmallVec`: this costs one allocation per packet that actually
462 /// coalesced, and `teksilo-core`'s dependency set is small on purpose.
463 pub coalesced: Vec<(EventTime, Point, PointerAxes)>,
464}
465
466impl PointerSample {
467 /// A mouse sample with no coalesced history — the shape every legacy
468 /// `WidgetEvent` lowers to.
469 pub fn mouse(phase: PointerPhase, position: Point, time: EventTime) -> Self {
470 Self {
471 pointer: PointerInfo::mouse(time),
472 phase,
473 position,
474 button: None,
475 modifiers: Modifiers::NONE,
476 coalesced: Vec::new(),
477 }
478 }
479
480 /// This sample with `button` recorded as the button that changed.
481 pub fn with_button(mut self, button: PointerButton) -> Self {
482 self.button = Some(button);
483 self
484 }
485
486 /// This sample with `modifiers` recorded.
487 pub fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
488 self.modifiers = modifiers;
489 self
490 }
491}
492
493// ---------------------------------------------------------------------------
494// Scroll
495// ---------------------------------------------------------------------------
496
497/// Where a scroll sits in a continuous gesture.
498///
499/// A wheel notch is [`Discrete`](Self::Discrete) — it has no beginning and no
500/// end — which is why that is the default and why nothing changes for a mouse.
501/// A trackpad gesture and a synthesised touch pan run
502/// `Began → Changed* → Ended`, optionally followed by `Momentum* →
503/// MomentumEnded` while the content coasts.
504///
505/// `#[non_exhaustive]`: a rubber-band settle phase is anticipated.
506#[non_exhaustive]
507#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
508pub enum ScrollPhase {
509 /// A self-contained scroll with no phase structure — a wheel notch. The
510 /// default, and what every scroll in Teksilo was before the touch
511 /// programme.
512 #[default]
513 Discrete,
514 /// The user's fingers went down and the gesture began.
515 Began,
516 /// The gesture is in progress.
517 Changed,
518 /// The user's fingers lifted. Any momentum follows separately.
519 Ended,
520 /// The content is coasting after the fingers lifted.
521 Momentum,
522 /// The coast finished.
523 MomentumEnded,
524 /// A one-shot flick with a release velocity, for backends that report a
525 /// fling rather than a momentum stream.
526 Fling,
527 /// The gesture was revoked before it ended.
528 Cancelled,
529}
530
531/// What produced a scroll.
532///
533/// Read by a consumer that must treat a precise pixel stream differently from a
534/// notched wheel — the classic case being "one wheel notch = one item" versus
535/// "follow the trackpad exactly".
536#[non_exhaustive]
537#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
538pub enum ScrollSource {
539 /// A notched mouse wheel. The default.
540 #[default]
541 Wheel,
542 /// A precision trackpad or a free-spinning wheel.
543 Trackpad,
544 /// A pan gesture synthesised from a direct pointer dragging the content.
545 TouchPan,
546 /// The app scrolled itself (a keyboard command, `ensure_visible`, an
547 /// animation).
548 Programmatic,
549}
550
551/// One scroll sample as it enters the tree.
552///
553/// The unit [`WidgetTree::dispatch_scroll`](crate::WidgetTree::dispatch_scroll)
554/// consumes.
555#[derive(Clone, Debug)]
556pub struct ScrollSample {
557 /// How far to scroll, in lines or pixels.
558 pub delta: ScrollDelta,
559 /// Where the pointer was, in window-logical coordinates, when the scroll
560 /// happened.
561 ///
562 /// `Some` routes the scroll by hit test; `None` falls back to the hovered
563 /// (else focused) widget. A wheel event has historically been `None` and
564 /// stays that way, so a mouse routes exactly as before; a synthesised touch
565 /// pan **must** carry a position, because a contact never writes hover and
566 /// would otherwise route nowhere.
567 pub position: Option<Point>,
568 /// Where in a continuous gesture this sample sits.
569 pub phase: ScrollPhase,
570 /// What produced it.
571 pub source: ScrollSource,
572 /// Who is pointing.
573 pub pointer: PointerInfo,
574 /// Modifier keys held when the sample was produced. Ctrl-wheel-to-zoom
575 /// reads this.
576 pub modifiers: Modifiers,
577}
578
579impl ScrollSample {
580 /// A discrete wheel notch from the mouse, routed by hover — exactly what
581 /// `WidgetEvent::Scroll` meant before the touch programme.
582 pub fn wheel(delta: ScrollDelta, modifiers: Modifiers, time: EventTime) -> Self {
583 Self {
584 delta,
585 position: None,
586 phase: ScrollPhase::Discrete,
587 source: ScrollSource::Wheel,
588 pointer: PointerInfo::mouse(time),
589 modifiers,
590 }
591 }
592
593 /// This sample routed at `position` rather than by hover.
594 pub fn at(mut self, position: Point) -> Self {
595 self.position = Some(position);
596 self
597 }
598}
599
600// ---------------------------------------------------------------------------
601// Cancellation
602// ---------------------------------------------------------------------------
603
604/// Why a pointer interaction was revoked.
605///
606/// Declared in full here so the taxonomy is one enumeration rather than a
607/// growing set of booleans, and so a consumer can `match` on it exhaustively.
608/// Every variant reaches a widget through the one funnel,
609/// [`WidgetTree::cancel_pointer`](crate::WidgetTree::cancel_pointer), and is
610/// delivered as a [`WidgetEvent::PointerCancel`](crate::event::WidgetEvent::PointerCancel).
611/// A handful name a producer whose own package has not landed and are marked
612/// as such below; `docs/touch-and-pen.md` §3.3 carries the full table of who
613/// raises each, who receives it, and what the widget must do about it.
614///
615/// `#[non_exhaustive]`: the taxonomy is expected to grow as backends reveal
616/// revocation paths Teksilo has not met.
617#[non_exhaustive]
618#[derive(Copy, Clone, PartialEq, Eq, Debug)]
619pub enum CancelReason {
620 /// The OS itself revoked the pointer (a `PointerCaptureLost`, a
621 /// `wl_touch.cancel`, a compositor grab).
622 Platform,
623 /// The window lost focus mid-interaction.
624 WindowDeactivated,
625 /// The window became fully occluded mid-interaction. Reserved for the
626 /// platform layer's occlusion path.
627 Occluded,
628 /// A modal surface opened over the interaction.
629 ModalOpened,
630 /// The interacting subtree went dormant (a `Switcher` branch was parked, a
631 /// tab was switched away from).
632 SubtreeParked,
633 /// The interacting widget was destroyed.
634 WidgetDestroyed,
635 /// The widget holding the pointer capture went away, leaving the capture
636 /// with no owner.
637 CaptureOrphaned,
638 /// A native OS drag started from this press, so the in-app interaction ends.
639 OsDragStarted,
640 /// An external (OS) drag-and-drop session took the pointer over. Reserved
641 /// for the inbound external-DnD path.
642 ExternalDndTakeover,
643 /// Another member of the gesture sequence won arbitration, so this one is
644 /// revoked.
645 PeerClaimed,
646 /// The overlay the interaction lived in was dismissed under it.
647 OverlayDismissed,
648 /// A second contact arrived on a surface that handles only one, so the
649 /// interaction is abandoned rather than misread. Reserved for
650 /// `MultiContact::First`.
651 MultiContactIgnored,
652 /// More simultaneous contacts arrived than the pointer table holds.
653 /// Refused at [`PointerTable::begin`](crate::pointer::table::PointerTable::begin),
654 /// before any event exists, so no widget is told.
655 ContactCapExceeded,
656 /// The contact was classified as a palm rather than a deliberate touch.
657 /// Refused at [`PointerTable::begin`](crate::pointer::table::PointerTable::begin),
658 /// before any event exists, so no widget is told.
659 PalmRejected,
660 /// A catch-all for a deactivation that fits none of the above. Prefer a
661 /// specific variant; this one exists so a caller is never forced to lie.
662 Deactivated,
663}
664
665// ---------------------------------------------------------------------------
666// Per-dispatch snapshot
667// ---------------------------------------------------------------------------
668
669/// What the tree knows about the sample currently being dispatched.
670///
671/// Snapshotted onto every [`EventContext`](crate::widget::EventContext) so a
672/// handler can ask which pointer it is serving without the answer having to be
673/// threaded through every handler signature.
674///
675/// The two obvious producers are a pointer sample
676/// ([`from_pointer_sample`](Self::from_pointer_sample)) and a scroll sample
677/// ([`from_scroll_sample`](Self::from_scroll_sample)). The two a reader is
678/// likely to get wrong are the ones with a pointer but **no sample**: a gesture
679/// the *timer* recognised ([`for_recognized_gesture`](Self::for_recognized_gesture))
680/// — a hold — and a **drag session** ([`for_drag_session`](Self::for_drag_session)),
681/// whose ticks fire from a layout pass and whose OS phases arrive from a platform
682/// thread. Everything else — a legacy `WidgetEvent`
683/// ([`from_event`](Self::from_event)), an accessibility action, a hand-built test
684/// context — holds the [`Default`], a mouse at the epoch.
685#[derive(Clone, Debug, PartialEq)]
686pub(crate) struct InputSnapshot {
687 pub(crate) pointer: PointerInfo,
688 pub(crate) position: Option<Point>,
689 pub(crate) scroll_phase: ScrollPhase,
690 pub(crate) scroll_source: ScrollSource,
691 /// The positions the OS batched into this packet, oldest first and
692 /// excluding [`position`](Self::position).
693 ///
694 /// Carried onto the snapshot — rather than left on the
695 /// [`PointerSample`] the dispatcher discards — because the velocity fit
696 /// behind a fling has to see them: a 500 Hz digitiser decimated to frame
697 /// rate under-reads a flick by the ratio of the two rates. Empty for every
698 /// producer that does not coalesce, which costs no allocation.
699 pub(crate) coalesced: Vec<(EventTime, Point)>,
700}
701
702impl Default for InputSnapshot {
703 fn default() -> Self {
704 Self {
705 pointer: PointerInfo::mouse(EventTime::ZERO),
706 position: None,
707 scroll_phase: ScrollPhase::Discrete,
708 scroll_source: ScrollSource::Wheel,
709 coalesced: Vec::new(),
710 }
711 }
712}
713
714impl InputSnapshot {
715 /// The snapshot a pointer sample implies.
716 pub(crate) fn from_pointer_sample(sample: &PointerSample) -> Self {
717 Self {
718 pointer: sample.pointer,
719 position: Some(sample.position),
720 coalesced: sample
721 .coalesced
722 .iter()
723 .map(|&(time, point, _)| (time, point))
724 .collect(),
725 ..Self::default()
726 }
727 }
728
729 /// The snapshot a gesture recognised by the **timer** implies.
730 ///
731 /// A hold is not a sample: nothing arrived, a deadline came due. But it is
732 /// still one contact's gesture, and a handler reached from it must not be
733 /// told it is serving the mouse — which is what it was told for as long as
734 /// this constructor did not exist, because `current_input` is
735 /// saved-and-restored around every dispatch and so holds the
736 /// [`Default`](Self::default) by the time a timer runs.
737 ///
738 /// [`position`](Self::position) stays `None` on purpose. The gesture
739 /// carries its own position, in **widget-local** coordinates, on the event
740 /// the handler is given; publishing a window position here as well would
741 /// offer a handler two answers that do not agree.
742 pub(crate) fn for_recognized_gesture(pointer: PointerInfo) -> Self {
743 Self {
744 pointer,
745 ..Self::default()
746 }
747 }
748
749 /// The snapshot a **drag session** implies.
750 ///
751 /// A drag-and-drop session outlives the sample that started it: `on_drag_tick`
752 /// fires from a layout pass, and an OS drag's phases arrive from a platform
753 /// thread. Neither is a sample, so `current_input` holds the
754 /// [`Default`](Self::default) there — and a drag handler asking which device
755 /// it is serving was told "mouse" for the whole of a finger drag. The tree
756 /// installs this around those dispatches instead; the pointer comes from
757 /// `DragSession::pointer`, recorded when the drag started.
758 ///
759 /// [`position`](Self::position) stays `None` for the same reason it does on
760 /// [`for_recognized_gesture`](Self::for_recognized_gesture): the drag
761 /// handler is handed its position in **widget-local** coordinates, and a
762 /// window position published beside it would be a second answer that
763 /// disagrees.
764 pub(crate) fn for_drag_session(pointer: PointerInfo) -> Self {
765 Self {
766 pointer,
767 ..Self::default()
768 }
769 }
770
771 /// The snapshot a scroll sample implies.
772 pub(crate) fn from_scroll_sample(sample: &ScrollSample) -> Self {
773 Self {
774 pointer: sample.pointer,
775 position: sample.position,
776 scroll_phase: sample.phase,
777 scroll_source: sample.source,
778 coalesced: Vec::new(),
779 }
780 }
781
782 /// The snapshot a legacy [`WidgetEvent`](crate::event::WidgetEvent)
783 /// implies. Pointer-bearing variants report what they carry; everything
784 /// else reports the default mouse.
785 pub(crate) fn from_event(event: &crate::event::WidgetEvent) -> Self {
786 use crate::event::WidgetEvent;
787 match event {
788 WidgetEvent::PointerDown {
789 position, pointer, ..
790 }
791 | WidgetEvent::PointerUp {
792 position, pointer, ..
793 }
794 | WidgetEvent::PointerMove {
795 position, pointer, ..
796 } => Self {
797 pointer: *pointer,
798 position: Some(*position),
799 ..Self::default()
800 },
801 // Hover transitions carry no position of their own — the move that
802 // caused them did.
803 WidgetEvent::PointerEnter { pointer } | WidgetEvent::PointerLeave { pointer } => Self {
804 pointer: *pointer,
805 ..Self::default()
806 },
807 WidgetEvent::Scroll {
808 window_position,
809 phase,
810 pointer,
811 ..
812 } => Self {
813 pointer: *pointer,
814 position: *window_position,
815 scroll_phase: *phase,
816 // A legacy `Scroll` carries no source; a wheel notch is what it
817 // has always been. `dispatch_scroll` overrides this from the
818 // sample.
819 scroll_source: ScrollSource::Wheel,
820 coalesced: Vec::new(),
821 },
822 WidgetEvent::PointerCancel {
823 window_position,
824 pointer,
825 ..
826 } => Self {
827 pointer: *pointer,
828 position: *window_position,
829 ..Self::default()
830 },
831 _ => Self::default(),
832 }
833 }
834}
835
836#[cfg(test)]
837mod tests {
838 use super::*;
839
840 // --- EventTime -------------------------------------------------------
841
842 #[test]
843 fn event_time_measures_from_the_epoch() {
844 let t = EventTime::from_millis(250);
845 assert_eq!(t.as_duration(), Duration::from_millis(250));
846 assert_eq!(EventTime::ZERO.as_duration(), Duration::ZERO);
847 assert_eq!(EventTime::default(), EventTime::ZERO);
848 }
849
850 #[test]
851 fn saturating_since_measures_forward() {
852 let a = EventTime::from_millis(100);
853 let b = EventTime::from_millis(350);
854 assert_eq!(b.saturating_since(a), Duration::from_millis(250));
855 assert_eq!(a.saturating_since(a), Duration::ZERO);
856 }
857
858 /// Samples can arrive out of order (a coalesced packet whose timestamps
859 /// predate the last one processed). An inverted pair must read as "no time
860 /// passed", not underflow.
861 #[test]
862 fn saturating_since_clamps_an_inverted_pair() {
863 let early = EventTime::from_millis(10);
864 let late = EventTime::from_millis(900);
865 assert_eq!(early.saturating_since(late), Duration::ZERO);
866 }
867
868 #[test]
869 fn checked_add_reports_overflow() {
870 let t = EventTime::from_millis(5);
871 assert_eq!(
872 t.checked_add(Duration::from_millis(15)),
873 Some(EventTime::from_millis(20))
874 );
875 assert_eq!(t.checked_add(Duration::MAX), None);
876 }
877
878 #[test]
879 fn event_times_order_by_their_offset() {
880 let mut times = [
881 EventTime::from_millis(30),
882 EventTime::ZERO,
883 EventTime::from_millis(7),
884 ];
885 times.sort();
886 assert_eq!(
887 times,
888 [
889 EventTime::ZERO,
890 EventTime::from_millis(7),
891 EventTime::from_millis(30)
892 ]
893 );
894 }
895
896 // --- PointerId -------------------------------------------------------
897
898 /// The reason there is no generation field: winit **reuses** `Touch::id`.
899 /// A press, a lift and a second press on the same raw id must produce two
900 /// different `PointerId`s, or the second contact inherits the first's
901 /// sequence.
902 #[test]
903 fn a_reused_os_id_mints_a_fresh_pointer_id() {
904 let alloc = PointerIdAllocator::global();
905 let device = BackendDeviceKey::new(0xFEED);
906
907 let first = alloc.begin(device, 7);
908 assert_eq!(alloc.get(device, 7), Some(first));
909 assert_eq!(alloc.end(device, 7), Some(first));
910 assert_eq!(alloc.get(device, 7), None);
911
912 let second = alloc.begin(device, 7);
913 assert_ne!(first, second, "a reused OS id must not reuse the PointerId");
914 assert!(second > first, "ids are monotonic");
915 alloc.end(device, 7);
916 }
917
918 /// Two devices may report the same contact id at the same time.
919 #[test]
920 fn the_same_os_id_on_two_devices_is_two_pointers() {
921 let alloc = PointerIdAllocator::global();
922 let screen = BackendDeviceKey::new(0xA1);
923 let tablet = BackendDeviceKey::new(0xB2);
924
925 let a = alloc.begin(screen, 1);
926 let b = alloc.begin(tablet, 1);
927 assert_ne!(a, b);
928 assert_eq!(alloc.get(screen, 1), Some(a));
929 assert_eq!(alloc.get(tablet, 1), Some(b));
930
931 alloc.end(screen, 1);
932 assert_eq!(alloc.get(tablet, 1), Some(b), "ending one leaves the other");
933 alloc.end(tablet, 1);
934 }
935
936 /// A backend that loses an Up must not strand the next press on the stale
937 /// identity.
938 #[test]
939 fn a_second_begin_replaces_a_stranded_mapping() {
940 let alloc = PointerIdAllocator::global();
941 let device = BackendDeviceKey::new(0xC3);
942 let first = alloc.begin(device, 42);
943 let second = alloc.begin(device, 42);
944 assert_ne!(first, second);
945 assert_eq!(alloc.get(device, 42), Some(second));
946 alloc.end(device, 42);
947 }
948
949 #[test]
950 fn ending_an_unknown_contact_is_a_no_op() {
951 let alloc = PointerIdAllocator::global();
952 assert_eq!(alloc.end(BackendDeviceKey::new(0xD4), 999), None);
953 }
954
955 #[test]
956 fn the_mouse_id_is_never_minted() {
957 let alloc = PointerIdAllocator::global();
958 let device = BackendDeviceKey::new(0xE5);
959 let id = alloc.begin(device, 3);
960 assert_ne!(id, PointerId::MOUSE);
961 assert_eq!(PointerId::MOUSE.get(), 1);
962 alloc.end(device, 3);
963 }
964
965 // --- PointerInfo -----------------------------------------------------
966
967 #[test]
968 fn the_mouse_constructor_is_the_legacy_pointer() {
969 let m = PointerInfo::mouse(EventTime::ZERO);
970 assert_eq!(m.id, PointerId::MOUSE);
971 assert_eq!(m.kind, PointerKind::Mouse);
972 assert!(m.primary);
973 assert!(m.buttons.is_empty());
974 assert_eq!(m.axes, PointerAxes::default());
975 assert!(!m.is_direct() && !m.is_coarse() && m.is_precise());
976 }
977
978 #[test]
979 fn a_touch_contact_is_direct_and_coarse() {
980 let t = PointerInfo::touch(PointerId::MOUSE, EventTime::ZERO);
981 assert_eq!(t.kind, PointerKind::Touch);
982 assert!(t.is_direct() && t.is_coarse() && !t.is_precise());
983 assert!(
984 !t.primary,
985 "primacy is the pointer table's decision, not the constructor's"
986 );
987 }
988
989 /// W3C Pointer Events L3: report what the device said; failing that, 0.5
990 /// while a button is down and 0.0 otherwise.
991 #[test]
992 fn effective_pressure_follows_the_w3c_rule() {
993 let mut m = PointerInfo::mouse(EventTime::ZERO);
994 assert_eq!(m.effective_pressure(), 0.0);
995
996 m.buttons = ButtonMask::PRIMARY;
997 assert_eq!(m.effective_pressure(), 0.5);
998
999 m.axes.pressure = Some(0.75);
1000 assert_eq!(m.effective_pressure(), 0.75);
1001
1002 m.buttons = ButtonMask::NONE;
1003 assert_eq!(m.effective_pressure(), 0.75, "a reported value always wins");
1004 }
1005
1006 // --- Samples ---------------------------------------------------------
1007
1008 #[test]
1009 fn a_mouse_sample_carries_no_coalesced_history() {
1010 let s = PointerSample::mouse(PointerPhase::Down, Point::new(3.0, 4.0), EventTime::ZERO)
1011 .with_button(PointerButton::Primary)
1012 .with_modifiers(Modifiers::SHIFT);
1013 assert!(s.coalesced.is_empty());
1014 assert_eq!(s.button, Some(PointerButton::Primary));
1015 assert_eq!(s.modifiers, Modifiers::SHIFT);
1016 assert_eq!(s.pointer.id, PointerId::MOUSE);
1017 }
1018
1019 #[test]
1020 fn a_wheel_sample_is_discrete_and_positionless() {
1021 let s = ScrollSample::wheel(
1022 ScrollDelta::Lines { x: 0.0, y: -1.0 },
1023 Modifiers::NONE,
1024 EventTime::ZERO,
1025 );
1026 assert_eq!(s.phase, ScrollPhase::Discrete);
1027 assert_eq!(s.source, ScrollSource::Wheel);
1028 assert_eq!(s.position, None);
1029
1030 let at = s.at(Point::new(10.0, 20.0));
1031 assert_eq!(at.position, Some(Point::new(10.0, 20.0)));
1032 }
1033
1034 #[test]
1035 fn scroll_defaults_are_todays_wheel() {
1036 assert_eq!(ScrollPhase::default(), ScrollPhase::Discrete);
1037 assert_eq!(ScrollSource::default(), ScrollSource::Wheel);
1038 }
1039
1040 // --- InputSnapshot ---------------------------------------------------
1041
1042 #[test]
1043 fn the_default_snapshot_is_a_mouse_at_the_epoch() {
1044 let s = InputSnapshot::default();
1045 assert_eq!(s.pointer.id, PointerId::MOUSE);
1046 assert_eq!(s.pointer.time, EventTime::ZERO);
1047 assert_eq!(s.position, None);
1048 assert_eq!(s.scroll_phase, ScrollPhase::Discrete);
1049 assert_eq!(s.scroll_source, ScrollSource::Wheel);
1050 }
1051
1052 #[test]
1053 fn a_scroll_sample_snapshot_keeps_its_phase_and_source() {
1054 let sample = ScrollSample {
1055 delta: ScrollDelta::Pixels { x: 0.0, y: 12.0 },
1056 position: Some(Point::new(5.0, 5.0)),
1057 phase: ScrollPhase::Momentum,
1058 source: ScrollSource::TouchPan,
1059 pointer: PointerInfo::mouse(EventTime::from_millis(9)),
1060 modifiers: Modifiers::NONE,
1061 };
1062 let snap = InputSnapshot::from_scroll_sample(&sample);
1063 assert_eq!(snap.scroll_phase, ScrollPhase::Momentum);
1064 assert_eq!(snap.scroll_source, ScrollSource::TouchPan);
1065 assert_eq!(snap.position, Some(Point::new(5.0, 5.0)));
1066 }
1067}