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 by every pointer-bearing
291/// `WidgetEvent` — the pointer, scroll and cancel variants alike.
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 position the OS batched into a packet, with the axes it was sampled at
438/// and its own time.
439///
440/// A backend that reports a batch — a digitizer whose packets arrive faster
441/// than the window's message rate, a platform with an explicit coalescing API —
442/// hands over **one** [`PointerSample`] per packet with the intermediate
443/// positions in [`PointerSample::coalesced`]. The alternative, one whole tree
444/// dispatch per digitizer packet, costs a hit test, an arbitration turn and a
445/// handler walk for a position no one had a chance to draw between.
446///
447/// Consumers see these through
448/// [`EventContext::coalesced`](crate::EventContext::coalesced). A drawing
449/// surface fans out over them and then over the sample's own position; a
450/// velocity tracker integrates them; everything else ignores them, which costs
451/// nothing because the list is empty for every producer that does not batch.
452///
453/// # Coordinates
454///
455/// [`window_position`](Self::window_position) is **window**-logical, exactly
456/// like [`PointerSample::position`] and
457/// [`WidgetEvent::Scroll`](crate::event::WidgetEvent::Scroll)'s `window_position`, and is
458/// named for it. The router localises the *event's* position against the
459/// captor's current bounds; a batch has no single widget to localise against,
460/// and a handler that needs widget-local coordinates converts at the use site
461/// — a `SceneView` does it through `SceneView::view_transform_signal`.
462#[derive(Copy, Clone, Debug, PartialEq)]
463#[non_exhaustive]
464pub struct CoalescedSample {
465 /// When the device produced this position, on the tree's timeline.
466 pub time: EventTime,
467 /// Where, in **window**-logical coordinates. See the type's docs.
468 pub window_position: Point,
469 /// The continuous axes as they read at this position — a digitizer varies
470 /// pressure and tilt *within* a batch, and that variation is the whole
471 /// reason to keep the intermediate samples rather than the endpoints.
472 pub axes: PointerAxes,
473}
474
475impl CoalescedSample {
476 /// One batched position. Axes default to "reported nothing"; set them with
477 /// [`with_axes`](Self::with_axes).
478 pub fn new(time: EventTime, window_position: Point) -> Self {
479 Self {
480 time,
481 window_position,
482 axes: PointerAxes::default(),
483 }
484 }
485
486 /// This position with its axes recorded.
487 pub fn with_axes(mut self, axes: PointerAxes) -> Self {
488 self.axes = axes;
489 self
490 }
491}
492
493/// One pointer sample as it enters the tree.
494///
495/// The unit [`WidgetTree::dispatch_pointer`](crate::WidgetTree::dispatch_pointer)
496/// consumes. A backend produces exactly one of these per OS packet, folding any
497/// intermediate positions the OS batched into [`coalesced`](Self::coalesced).
498#[derive(Clone, Debug)]
499pub struct PointerSample {
500 /// Who is pointing.
501 pub pointer: PointerInfo,
502 /// What happened.
503 pub phase: PointerPhase,
504 /// Where, in window-logical coordinates.
505 pub position: Point,
506 /// The button that changed on a [`Down`](PointerPhase::Down) or
507 /// [`Up`](PointerPhase::Up). `None` for a move, a cancel, and for a
508 /// buttonless direct-pointer contact.
509 pub button: Option<PointerButton>,
510 /// Modifier keys held when the sample was produced.
511 pub modifiers: Modifiers,
512 /// Positions the OS batched into this packet, oldest first, *excluding*
513 /// [`position`](Self::position) (which is the newest).
514 ///
515 /// A velocity tracker integrates over these; a drawing surface draws
516 /// through them; everything else ignores them. Deliberately a `Vec` and not
517 /// a `SmallVec`: this costs one allocation per packet that actually
518 /// coalesced, and `teksilo-core`'s dependency set is small on purpose.
519 ///
520 /// Reaches a handler as [`EventContext::coalesced`](crate::EventContext::coalesced).
521 pub coalesced: Vec<CoalescedSample>,
522}
523
524impl PointerSample {
525 /// A mouse sample with no coalesced history — the shape every legacy
526 /// `WidgetEvent` lowers to.
527 pub fn mouse(phase: PointerPhase, position: Point, time: EventTime) -> Self {
528 Self {
529 pointer: PointerInfo::mouse(time),
530 phase,
531 position,
532 button: None,
533 modifiers: Modifiers::NONE,
534 coalesced: Vec::new(),
535 }
536 }
537
538 /// This sample with `button` recorded as the button that changed.
539 pub fn with_button(mut self, button: PointerButton) -> Self {
540 self.button = Some(button);
541 self
542 }
543
544 /// This sample with `modifiers` recorded.
545 pub fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
546 self.modifiers = modifiers;
547 self
548 }
549}
550
551// ---------------------------------------------------------------------------
552// Scroll
553// ---------------------------------------------------------------------------
554
555/// Where a scroll sits in a continuous gesture.
556///
557/// A wheel notch is [`Discrete`](Self::Discrete) — it has no beginning and no
558/// end — which is why that is the default and why nothing changes for a mouse.
559/// A trackpad gesture and a synthesised touch pan run
560/// `Began → Changed* → Ended`, optionally followed by `Momentum* →
561/// MomentumEnded` while the content coasts.
562///
563/// `#[non_exhaustive]`: a rubber-band settle phase is anticipated.
564#[non_exhaustive]
565#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
566pub enum ScrollPhase {
567 /// A self-contained scroll with no phase structure — a wheel notch. The
568 /// default, and what every scroll in Teksilo was before the touch
569 /// programme.
570 #[default]
571 Discrete,
572 /// The user's fingers went down and the gesture began.
573 Began,
574 /// The gesture is in progress.
575 Changed,
576 /// The user's fingers lifted. Any momentum follows separately.
577 Ended,
578 /// The content is coasting after the fingers lifted.
579 Momentum,
580 /// The coast finished.
581 MomentumEnded,
582 /// A one-shot flick with a release velocity, for backends that report a
583 /// fling rather than a momentum stream.
584 Fling,
585 /// The gesture was revoked before it ended.
586 Cancelled,
587}
588
589/// What produced a scroll.
590///
591/// Read by a consumer that must treat a precise pixel stream differently from a
592/// notched wheel — the classic case being "one wheel notch = one item" versus
593/// "follow the trackpad exactly".
594#[non_exhaustive]
595#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
596pub enum ScrollSource {
597 /// A notched mouse wheel. The default.
598 #[default]
599 Wheel,
600 /// A precision trackpad or a free-spinning wheel.
601 Trackpad,
602 /// A pan gesture synthesised from a direct pointer dragging the content.
603 TouchPan,
604 /// The app scrolled itself (a keyboard command, `ensure_visible`, an
605 /// animation).
606 Programmatic,
607}
608
609/// One scroll sample as it enters the tree.
610///
611/// The unit [`WidgetTree::dispatch_scroll`](crate::WidgetTree::dispatch_scroll)
612/// consumes.
613#[derive(Clone, Debug)]
614pub struct ScrollSample {
615 /// How far to scroll, in lines or pixels.
616 pub delta: ScrollDelta,
617 /// Where the pointer was, in window-logical coordinates, when the scroll
618 /// happened.
619 ///
620 /// `Some` routes the scroll by hit test; `None` falls back to the hovered
621 /// (else focused) widget. A wheel event has historically been `None` and
622 /// stays that way, so a mouse routes exactly as before; a synthesised touch
623 /// pan **must** carry a position, because a contact never writes hover and
624 /// would otherwise route nowhere.
625 pub position: Option<Point>,
626 /// Where in a continuous gesture this sample sits.
627 pub phase: ScrollPhase,
628 /// What produced it.
629 pub source: ScrollSource,
630 /// Who is pointing.
631 pub pointer: PointerInfo,
632 /// Modifier keys held when the sample was produced. Ctrl-wheel-to-zoom
633 /// reads this.
634 pub modifiers: Modifiers,
635}
636
637impl ScrollSample {
638 /// A discrete wheel notch from the mouse, routed by hover — exactly what
639 /// `WidgetEvent::Scroll` meant before the touch programme.
640 pub fn wheel(delta: ScrollDelta, modifiers: Modifiers, time: EventTime) -> Self {
641 Self {
642 delta,
643 position: None,
644 phase: ScrollPhase::Discrete,
645 source: ScrollSource::Wheel,
646 pointer: PointerInfo::mouse(time),
647 modifiers,
648 }
649 }
650
651 /// This sample routed at `position` rather than by hover.
652 pub fn at(mut self, position: Point) -> Self {
653 self.position = Some(position);
654 self
655 }
656}
657
658// ---------------------------------------------------------------------------
659// Cancellation
660// ---------------------------------------------------------------------------
661
662/// Why a pointer interaction was revoked.
663///
664/// Declared in full here so the taxonomy is one enumeration rather than a
665/// growing set of booleans, and so a consumer can `match` on it exhaustively.
666/// Every variant reaches a widget through the one funnel,
667/// [`WidgetTree::cancel_pointer`](crate::WidgetTree::cancel_pointer), and is
668/// delivered as a [`WidgetEvent::PointerCancel`](crate::event::WidgetEvent::PointerCancel).
669/// A handful name a producer whose own package has not landed and are marked
670/// as such below; `docs/touch-and-pen.md` §3.3 carries the full table of who
671/// raises each, who receives it, and what the widget must do about it.
672///
673/// `#[non_exhaustive]`: the taxonomy is expected to grow as backends reveal
674/// revocation paths Teksilo has not met.
675#[non_exhaustive]
676#[derive(Copy, Clone, PartialEq, Eq, Debug)]
677pub enum CancelReason {
678 /// The OS itself revoked the pointer (a `PointerCaptureLost`, a
679 /// `wl_touch.cancel`, a compositor grab).
680 Platform,
681 /// The window lost focus mid-interaction.
682 WindowDeactivated,
683 /// The window became fully occluded mid-interaction. Raised by the
684 /// platform layer's occlusion path (`WindowEvent::Occluded`).
685 Occluded,
686 /// A modal surface opened over the interaction.
687 ModalOpened,
688 /// The interacting subtree went dormant (a `Switcher` branch was parked, a
689 /// tab was switched away from).
690 SubtreeParked,
691 /// The interacting widget was destroyed.
692 WidgetDestroyed,
693 /// The widget holding the pointer capture went away, leaving the capture
694 /// with no owner.
695 CaptureOrphaned,
696 /// A native OS drag started from this press, so the in-app interaction ends.
697 OsDragStarted,
698 /// An external (OS) drag-and-drop session took the pointer over. Reserved
699 /// for the inbound external-DnD path.
700 ExternalDndTakeover,
701 /// Another member of the gesture sequence won arbitration, so this one is
702 /// revoked.
703 PeerClaimed,
704 /// The overlay the interaction lived in was dismissed under it.
705 OverlayDismissed,
706 /// A second contact arrived on a surface that handles only one, so the
707 /// interaction is abandoned rather than misread. Reserved for
708 /// `MultiContact::First`.
709 MultiContactIgnored,
710 /// More simultaneous contacts arrived than the pointer table holds.
711 /// Refused at [`PointerTable::begin`](crate::pointer::table::PointerTable::begin),
712 /// before any event exists, so no widget is told.
713 ContactCapExceeded,
714 /// The contact was classified as a palm rather than a deliberate touch.
715 /// A contact the *backend* flags is refused at
716 /// [`PointerTable::begin`](crate::pointer::table::PointerTable::begin),
717 /// before any event exists, so no widget is told; one the `PalmWatch`
718 /// heuristic rejects on its release goes through the cancel funnel
719 /// instead, so its widget *is* told — which is the only way to guarantee
720 /// it fires no tap.
721 PalmRejected,
722 /// A catch-all for a deactivation that fits none of the above. Prefer a
723 /// specific variant; this one exists so a caller is never forced to lie.
724 Deactivated,
725}
726
727// ---------------------------------------------------------------------------
728// Per-dispatch snapshot
729// ---------------------------------------------------------------------------
730
731/// What the tree knows about the sample currently being dispatched.
732///
733/// Snapshotted onto every [`EventContext`](crate::widget::EventContext) so a
734/// handler can ask which pointer it is serving without the answer having to be
735/// threaded through every handler signature.
736///
737/// The two obvious producers are a pointer sample
738/// ([`from_pointer_sample`](Self::from_pointer_sample)) and a scroll sample
739/// ([`from_scroll_sample`](Self::from_scroll_sample)). The two a reader is
740/// likely to get wrong are the ones with a pointer but **no sample**: a gesture
741/// the *timer* recognised ([`for_recognized_gesture`](Self::for_recognized_gesture))
742/// — a hold — and a **drag session** ([`for_drag_session`](Self::for_drag_session)),
743/// whose ticks fire from a layout pass and whose OS phases arrive from a platform
744/// thread. Everything else — a legacy `WidgetEvent`
745/// ([`from_event`](Self::from_event)), an accessibility action, a hand-built test
746/// context — holds the [`Default`], a mouse at the epoch.
747#[derive(Clone, Debug, PartialEq)]
748pub(crate) struct InputSnapshot {
749 pub(crate) pointer: PointerInfo,
750 pub(crate) position: Option<Point>,
751 pub(crate) scroll_phase: ScrollPhase,
752 pub(crate) scroll_source: ScrollSource,
753 /// The positions the OS batched into this packet, oldest first and
754 /// excluding [`position`](Self::position).
755 ///
756 /// Carried onto the snapshot — rather than left on the
757 /// [`PointerSample`] the dispatcher discards — because two consumers need
758 /// them. The velocity fit behind a fling, first: a 500 Hz digitiser
759 /// decimated to frame rate under-reads a flick by the ratio of the two
760 /// rates. And a drawing surface, which reads them through
761 /// [`EventContext::coalesced`](crate::EventContext::coalesced) and would
762 /// otherwise draw a stroke through one position in every batch.
763 ///
764 /// The axes ride along. They used to be dropped here — the snapshot held
765 /// `(EventTime, Point)` — which left the one consumer that existed (the
766 /// velocity fit) correct and silently made the field useless for ink,
767 /// because a digitizer's pressure varies *within* a batch and it is that
768 /// variation a stroke's width is made of.
769 ///
770 /// Empty for every producer that does not coalesce, which costs no
771 /// allocation.
772 pub(crate) coalesced: Vec<CoalescedSample>,
773}
774
775impl Default for InputSnapshot {
776 fn default() -> Self {
777 Self {
778 pointer: PointerInfo::mouse(EventTime::ZERO),
779 position: None,
780 scroll_phase: ScrollPhase::Discrete,
781 scroll_source: ScrollSource::Wheel,
782 coalesced: Vec::new(),
783 }
784 }
785}
786
787impl InputSnapshot {
788 /// The snapshot a pointer sample implies.
789 pub(crate) fn from_pointer_sample(sample: &PointerSample) -> Self {
790 Self {
791 pointer: sample.pointer,
792 position: Some(sample.position),
793 coalesced: sample.coalesced.clone(),
794 ..Self::default()
795 }
796 }
797
798 /// The snapshot a gesture recognised by the **timer** implies.
799 ///
800 /// A hold is not a sample: nothing arrived, a deadline came due. But it is
801 /// still one contact's gesture, and a handler reached from it must not be
802 /// told it is serving the mouse — which is what it was told for as long as
803 /// this constructor did not exist, because `current_input` is
804 /// saved-and-restored around every dispatch and so holds the
805 /// [`Default`](Self::default) by the time a timer runs.
806 ///
807 /// [`position`](Self::position) stays `None` on purpose. The gesture
808 /// carries its own position, in **widget-local** coordinates, on the event
809 /// the handler is given; publishing a window position here as well would
810 /// offer a handler two answers that do not agree.
811 ///
812 /// [`coalesced`](Self::coalesced) stays empty for the same reason, and the
813 /// emptiness is written out below rather than inherited from
814 /// [`Default`](Self::default) so that it reads as the decision it is: a
815 /// deadline coming due batched nothing, and handing back the positions of
816 /// whichever sample happened to arrive last would attribute them to a
817 /// gesture that did not produce them. A surface that wants every position
818 /// — an ink tool — reads them on the **sample** path, which is where they
819 /// are; see [`EventContext::coalesced`](crate::EventContext::coalesced).
820 pub(crate) fn for_recognized_gesture(pointer: PointerInfo) -> Self {
821 Self {
822 pointer,
823 coalesced: Vec::new(),
824 ..Self::default()
825 }
826 }
827
828 /// The snapshot a **drag session** implies.
829 ///
830 /// A drag-and-drop session outlives the sample that started it: `on_drag_tick`
831 /// fires from a layout pass, and an OS drag's phases arrive from a platform
832 /// thread. Neither is a sample, so `current_input` holds the
833 /// [`Default`](Self::default) there — and a drag handler asking which device
834 /// it is serving was told "mouse" for the whole of a finger drag. The tree
835 /// installs this around those dispatches instead; the pointer comes from
836 /// `DragSession::pointer`, recorded when the drag started.
837 ///
838 /// [`position`](Self::position) stays `None` for the same reason it does on
839 /// [`for_recognized_gesture`](Self::for_recognized_gesture): the drag
840 /// handler is handed its position in **widget-local** coordinates, and a
841 /// window position published beside it would be a second answer that
842 /// disagrees. [`coalesced`](Self::coalesced) is empty on the same grounds,
843 /// and written out for the same reason — a tick fired from a layout pass
844 /// batched nothing.
845 pub(crate) fn for_drag_session(pointer: PointerInfo) -> Self {
846 Self {
847 pointer,
848 coalesced: Vec::new(),
849 ..Self::default()
850 }
851 }
852
853 /// The snapshot a scroll sample implies.
854 pub(crate) fn from_scroll_sample(sample: &ScrollSample) -> Self {
855 Self {
856 pointer: sample.pointer,
857 position: sample.position,
858 scroll_phase: sample.phase,
859 scroll_source: sample.source,
860 coalesced: Vec::new(),
861 }
862 }
863
864 /// The snapshot a legacy [`WidgetEvent`](crate::event::WidgetEvent)
865 /// implies. Pointer-bearing variants report what they carry; everything
866 /// else reports the default mouse.
867 pub(crate) fn from_event(event: &crate::event::WidgetEvent) -> Self {
868 use crate::event::WidgetEvent;
869 match event {
870 WidgetEvent::PointerDown {
871 position, pointer, ..
872 }
873 | WidgetEvent::PointerUp {
874 position, pointer, ..
875 }
876 | WidgetEvent::PointerMove {
877 position, pointer, ..
878 } => Self {
879 pointer: *pointer,
880 position: Some(*position),
881 ..Self::default()
882 },
883 // Hover transitions carry no position of their own — the move that
884 // caused them did.
885 WidgetEvent::PointerEnter { pointer } | WidgetEvent::PointerLeave { pointer } => Self {
886 pointer: *pointer,
887 ..Self::default()
888 },
889 WidgetEvent::Scroll {
890 window_position,
891 phase,
892 pointer,
893 ..
894 } => Self {
895 pointer: *pointer,
896 position: *window_position,
897 scroll_phase: *phase,
898 // A legacy `Scroll` carries no source; a wheel notch is what it
899 // has always been. `dispatch_scroll` overrides this from the
900 // sample.
901 scroll_source: ScrollSource::Wheel,
902 coalesced: Vec::new(),
903 },
904 WidgetEvent::PointerCancel {
905 window_position,
906 pointer,
907 ..
908 } => Self {
909 pointer: *pointer,
910 position: *window_position,
911 ..Self::default()
912 },
913 _ => Self::default(),
914 }
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 use super::*;
921
922 // --- EventTime -------------------------------------------------------
923
924 #[test]
925 fn event_time_measures_from_the_epoch() {
926 let t = EventTime::from_millis(250);
927 assert_eq!(t.as_duration(), Duration::from_millis(250));
928 assert_eq!(EventTime::ZERO.as_duration(), Duration::ZERO);
929 assert_eq!(EventTime::default(), EventTime::ZERO);
930 }
931
932 #[test]
933 fn saturating_since_measures_forward() {
934 let a = EventTime::from_millis(100);
935 let b = EventTime::from_millis(350);
936 assert_eq!(b.saturating_since(a), Duration::from_millis(250));
937 assert_eq!(a.saturating_since(a), Duration::ZERO);
938 }
939
940 /// Samples can arrive out of order (a coalesced packet whose timestamps
941 /// predate the last one processed). An inverted pair must read as "no time
942 /// passed", not underflow.
943 #[test]
944 fn saturating_since_clamps_an_inverted_pair() {
945 let early = EventTime::from_millis(10);
946 let late = EventTime::from_millis(900);
947 assert_eq!(early.saturating_since(late), Duration::ZERO);
948 }
949
950 #[test]
951 fn checked_add_reports_overflow() {
952 let t = EventTime::from_millis(5);
953 assert_eq!(
954 t.checked_add(Duration::from_millis(15)),
955 Some(EventTime::from_millis(20))
956 );
957 assert_eq!(t.checked_add(Duration::MAX), None);
958 }
959
960 #[test]
961 fn event_times_order_by_their_offset() {
962 let mut times = [
963 EventTime::from_millis(30),
964 EventTime::ZERO,
965 EventTime::from_millis(7),
966 ];
967 times.sort();
968 assert_eq!(
969 times,
970 [
971 EventTime::ZERO,
972 EventTime::from_millis(7),
973 EventTime::from_millis(30)
974 ]
975 );
976 }
977
978 // --- PointerId -------------------------------------------------------
979
980 /// The reason there is no generation field: winit **reuses** `Touch::id`.
981 /// A press, a lift and a second press on the same raw id must produce two
982 /// different `PointerId`s, or the second contact inherits the first's
983 /// sequence.
984 #[test]
985 fn a_reused_os_id_mints_a_fresh_pointer_id() {
986 let alloc = PointerIdAllocator::global();
987 let device = BackendDeviceKey::new(0xFEED);
988
989 let first = alloc.begin(device, 7);
990 assert_eq!(alloc.get(device, 7), Some(first));
991 assert_eq!(alloc.end(device, 7), Some(first));
992 assert_eq!(alloc.get(device, 7), None);
993
994 let second = alloc.begin(device, 7);
995 assert_ne!(first, second, "a reused OS id must not reuse the PointerId");
996 assert!(second > first, "ids are monotonic");
997 alloc.end(device, 7);
998 }
999
1000 /// Two devices may report the same contact id at the same time.
1001 #[test]
1002 fn the_same_os_id_on_two_devices_is_two_pointers() {
1003 let alloc = PointerIdAllocator::global();
1004 let screen = BackendDeviceKey::new(0xA1);
1005 let tablet = BackendDeviceKey::new(0xB2);
1006
1007 let a = alloc.begin(screen, 1);
1008 let b = alloc.begin(tablet, 1);
1009 assert_ne!(a, b);
1010 assert_eq!(alloc.get(screen, 1), Some(a));
1011 assert_eq!(alloc.get(tablet, 1), Some(b));
1012
1013 alloc.end(screen, 1);
1014 assert_eq!(alloc.get(tablet, 1), Some(b), "ending one leaves the other");
1015 alloc.end(tablet, 1);
1016 }
1017
1018 /// A backend that loses an Up must not strand the next press on the stale
1019 /// identity.
1020 #[test]
1021 fn a_second_begin_replaces_a_stranded_mapping() {
1022 let alloc = PointerIdAllocator::global();
1023 let device = BackendDeviceKey::new(0xC3);
1024 let first = alloc.begin(device, 42);
1025 let second = alloc.begin(device, 42);
1026 assert_ne!(first, second);
1027 assert_eq!(alloc.get(device, 42), Some(second));
1028 alloc.end(device, 42);
1029 }
1030
1031 #[test]
1032 fn ending_an_unknown_contact_is_a_no_op() {
1033 let alloc = PointerIdAllocator::global();
1034 assert_eq!(alloc.end(BackendDeviceKey::new(0xD4), 999), None);
1035 }
1036
1037 #[test]
1038 fn the_mouse_id_is_never_minted() {
1039 let alloc = PointerIdAllocator::global();
1040 let device = BackendDeviceKey::new(0xE5);
1041 let id = alloc.begin(device, 3);
1042 assert_ne!(id, PointerId::MOUSE);
1043 assert_eq!(PointerId::MOUSE.get(), 1);
1044 alloc.end(device, 3);
1045 }
1046
1047 // --- PointerInfo -----------------------------------------------------
1048
1049 #[test]
1050 fn the_mouse_constructor_is_the_legacy_pointer() {
1051 let m = PointerInfo::mouse(EventTime::ZERO);
1052 assert_eq!(m.id, PointerId::MOUSE);
1053 assert_eq!(m.kind, PointerKind::Mouse);
1054 assert!(m.primary);
1055 assert!(m.buttons.is_empty());
1056 assert_eq!(m.axes, PointerAxes::default());
1057 assert!(!m.is_direct() && !m.is_coarse() && m.is_precise());
1058 }
1059
1060 #[test]
1061 fn a_touch_contact_is_direct_and_coarse() {
1062 let t = PointerInfo::touch(PointerId::MOUSE, EventTime::ZERO);
1063 assert_eq!(t.kind, PointerKind::Touch);
1064 assert!(t.is_direct() && t.is_coarse() && !t.is_precise());
1065 assert!(
1066 !t.primary,
1067 "primacy is the pointer table's decision, not the constructor's"
1068 );
1069 }
1070
1071 /// W3C Pointer Events L3: report what the device said; failing that, 0.5
1072 /// while a button is down and 0.0 otherwise.
1073 #[test]
1074 fn effective_pressure_follows_the_w3c_rule() {
1075 let mut m = PointerInfo::mouse(EventTime::ZERO);
1076 assert_eq!(m.effective_pressure(), 0.0);
1077
1078 m.buttons = ButtonMask::PRIMARY;
1079 assert_eq!(m.effective_pressure(), 0.5);
1080
1081 m.axes.pressure = Some(0.75);
1082 assert_eq!(m.effective_pressure(), 0.75);
1083
1084 m.buttons = ButtonMask::NONE;
1085 assert_eq!(m.effective_pressure(), 0.75, "a reported value always wins");
1086 }
1087
1088 // --- Samples ---------------------------------------------------------
1089
1090 #[test]
1091 fn a_mouse_sample_carries_no_coalesced_history() {
1092 let s = PointerSample::mouse(PointerPhase::Down, Point::new(3.0, 4.0), EventTime::ZERO)
1093 .with_button(PointerButton::Primary)
1094 .with_modifiers(Modifiers::SHIFT);
1095 assert!(s.coalesced.is_empty());
1096 assert_eq!(s.button, Some(PointerButton::Primary));
1097 assert_eq!(s.modifiers, Modifiers::SHIFT);
1098 assert_eq!(s.pointer.id, PointerId::MOUSE);
1099 }
1100
1101 #[test]
1102 fn a_wheel_sample_is_discrete_and_positionless() {
1103 let s = ScrollSample::wheel(
1104 ScrollDelta::Lines { x: 0.0, y: -1.0 },
1105 Modifiers::NONE,
1106 EventTime::ZERO,
1107 );
1108 assert_eq!(s.phase, ScrollPhase::Discrete);
1109 assert_eq!(s.source, ScrollSource::Wheel);
1110 assert_eq!(s.position, None);
1111
1112 let at = s.at(Point::new(10.0, 20.0));
1113 assert_eq!(at.position, Some(Point::new(10.0, 20.0)));
1114 }
1115
1116 #[test]
1117 fn scroll_defaults_are_todays_wheel() {
1118 assert_eq!(ScrollPhase::default(), ScrollPhase::Discrete);
1119 assert_eq!(ScrollSource::default(), ScrollSource::Wheel);
1120 }
1121
1122 // --- InputSnapshot ---------------------------------------------------
1123
1124 #[test]
1125 fn the_default_snapshot_is_a_mouse_at_the_epoch() {
1126 let s = InputSnapshot::default();
1127 assert_eq!(s.pointer.id, PointerId::MOUSE);
1128 assert_eq!(s.pointer.time, EventTime::ZERO);
1129 assert_eq!(s.position, None);
1130 assert_eq!(s.scroll_phase, ScrollPhase::Discrete);
1131 assert_eq!(s.scroll_source, ScrollSource::Wheel);
1132 }
1133
1134 #[test]
1135 fn a_scroll_sample_snapshot_keeps_its_phase_and_source() {
1136 let sample = ScrollSample {
1137 delta: ScrollDelta::Pixels { x: 0.0, y: 12.0 },
1138 position: Some(Point::new(5.0, 5.0)),
1139 phase: ScrollPhase::Momentum,
1140 source: ScrollSource::TouchPan,
1141 pointer: PointerInfo::mouse(EventTime::from_millis(9)),
1142 modifiers: Modifiers::NONE,
1143 };
1144 let snap = InputSnapshot::from_scroll_sample(&sample);
1145 assert_eq!(snap.scroll_phase, ScrollPhase::Momentum);
1146 assert_eq!(snap.scroll_source, ScrollSource::TouchPan);
1147 assert_eq!(snap.position, Some(Point::new(5.0, 5.0)));
1148 }
1149}