Skip to main content

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}