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 (from stage 1 of the
291/// event-shape landing) by the scroll and cancel `WidgetEvent`s.
292///
293/// `#[non_exhaustive]`: construct one through [`mouse`](Self::mouse) /
294/// [`touch`](Self::touch) and adjust fields, so a later field cannot break a
295/// call site.
296#[non_exhaustive]
297#[derive(Copy, Clone, Debug, PartialEq)]
298pub struct PointerInfo {
299    /// Which pointer this is. See the module docs on per-press identity.
300    pub id: PointerId,
301    /// What kind of device it is — the axis gesture tuning reads.
302    pub kind: PointerKind,
303    /// The W3C Pointer Events `isPrimary` flag, which is **per kind**: every
304    /// mouse sample is primary, and so is the first contact of a touch sequence.
305    /// On a machine with both, a mouse and a first finger are *both* primary at
306    /// once. It is a property of the sample, set by whoever produced it, and
307    /// [`PointerTable`](table::PointerTable) never rewrites it.
308    ///
309    /// **Not** the framework's singular pointer. The one that drives the legacy
310    /// singular signals — `hovered`, the cursor, the one `PointerDown` a widget
311    /// that knows nothing of multi-touch sees — is
312    /// [`PointerTable::primary`](table::PointerTable::primary), a table-level
313    /// election that exactly one live pointer holds and that a mouse always wins.
314    /// A third notion, [`PointerTable::hover_owner`](table::PointerTable::hover_owner),
315    /// is the most recent hovering-*capable* pointer. `table.rs`'s module docs
316    /// separate all three; reading this flag as either of the others is the
317    /// mistake the separation exists to prevent.
318    pub primary: bool,
319    /// The buttons held *after* this sample is applied. A press sets its own
320    /// bit; a release clears it. Empty for a hovering pointer.
321    pub buttons: ButtonMask,
322    /// The continuous axes, where the device reports them.
323    pub axes: PointerAxes,
324    /// When the backend says this sample happened, on the tree's timeline.
325    pub time: EventTime,
326    /// The digitizer classified this contact as a palm rather than a
327    /// deliberate touch.
328    ///
329    /// Only a backend that advertises `reports_palm` ever sets it; everything
330    /// else leaves it `false`, which is why no existing call site changes
331    /// meaning. A flagged sample is refused by
332    /// [`PointerTable::begin`](table::PointerTable::begin) and never reaches a
333    /// widget: rejecting it at the table is what keeps a hand resting on a
334    /// tablet from opening menus, and it is one decision rather than one per
335    /// recognizer.
336    pub palm: bool,
337}
338
339impl PointerInfo {
340    /// The mouse: [`PointerId::MOUSE`], [`PointerKind::Mouse`], primary, no
341    /// buttons held, no axes.
342    ///
343    /// This is what every legacy `WidgetEvent` constructor defaults to, so a
344    /// call site that says nothing about pointers keeps meaning exactly what it
345    /// meant before the touch programme.
346    pub const fn mouse(time: EventTime) -> Self {
347        Self {
348            id: PointerId::MOUSE,
349            kind: PointerKind::Mouse,
350            primary: true,
351            buttons: ButtonMask::NONE,
352            axes: PointerAxes {
353                pressure: None,
354                tangential_pressure: None,
355                tilt: None,
356                twist: None,
357                contact: None,
358            },
359            time,
360            palm: false,
361        }
362    }
363
364    /// A touch contact, with the W3C per-kind [`primary`](Self::primary) flag
365    /// **clear**. A producer that knows this is the first contact of a sequence
366    /// sets it; the platform translator does. The pointer table's own election is
367    /// a separate thing and never writes this field.
368    pub const fn touch(id: PointerId, time: EventTime) -> Self {
369        Self {
370            id,
371            kind: PointerKind::Touch,
372            primary: false,
373            buttons: ButtonMask::NONE,
374            axes: PointerAxes {
375                pressure: None,
376                tangential_pressure: None,
377                tilt: None,
378                twist: None,
379                contact: None,
380            },
381            time,
382            palm: false,
383        }
384    }
385
386    /// Whether the user points at the pixel directly — see
387    /// [`PointerKind::is_direct`].
388    pub const fn is_direct(&self) -> bool {
389        self.kind.is_direct()
390    }
391
392    /// Whether the contact patch is large enough that the reported point is an
393    /// estimate — see [`PointerKind::is_coarse`].
394    pub const fn is_coarse(&self) -> bool {
395        self.kind.is_coarse()
396    }
397
398    /// Whether the reported position is accurate to about a pixel — see
399    /// [`PointerKind::is_precise`].
400    pub const fn is_precise(&self) -> bool {
401        self.kind.is_precise()
402    }
403
404    /// The pressure a consumer should read: the reported value if the device
405    /// gave one, else `0.5` while any button is down, else `0.0`.
406    ///
407    /// This is the W3C Pointer Events Level 3 rule for `pressure`, and it
408    /// exists so a pressure-sensitive surface (a brush, a force-touch
409    /// affordance) has a defined answer for a mouse without special-casing it.
410    pub fn effective_pressure(&self) -> f32 {
411        match self.axes.pressure {
412            Some(p) => p,
413            None if !self.buttons.is_empty() => 0.5,
414            None => 0.0,
415        }
416    }
417}
418
419/// Which end of a pointer's life a sample sits at.
420///
421/// `Cancel` is not an Up: an Up means the user completed the interaction, a
422/// Cancel means the system took it away (see [`CancelReason`]). Conflating them
423/// is how a drag whose window lost focus ends up "dropped" where the pointer
424/// happened to be.
425#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
426pub enum PointerPhase {
427    /// The pointer came down / a button went down.
428    Down,
429    /// The pointer moved (with or without buttons held).
430    Move,
431    /// The pointer lifted / a button went up.
432    Up,
433    /// The system revoked the pointer. Carries no meaningful end position.
434    Cancel,
435}
436
437/// One 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. Reserved for the
684    /// platform layer's occlusion path.
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    /// Refused at [`PointerTable::begin`](crate::pointer::table::PointerTable::begin),
716    /// before any event exists, so no widget is told.
717    PalmRejected,
718    /// A catch-all for a deactivation that fits none of the above. Prefer a
719    /// specific variant; this one exists so a caller is never forced to lie.
720    Deactivated,
721}
722
723// ---------------------------------------------------------------------------
724// Per-dispatch snapshot
725// ---------------------------------------------------------------------------
726
727/// What the tree knows about the sample currently being dispatched.
728///
729/// Snapshotted onto every [`EventContext`](crate::widget::EventContext) so a
730/// handler can ask which pointer it is serving without the answer having to be
731/// threaded through every handler signature.
732///
733/// The two obvious producers are a pointer sample
734/// ([`from_pointer_sample`](Self::from_pointer_sample)) and a scroll sample
735/// ([`from_scroll_sample`](Self::from_scroll_sample)). The two a reader is
736/// likely to get wrong are the ones with a pointer but **no sample**: a gesture
737/// the *timer* recognised ([`for_recognized_gesture`](Self::for_recognized_gesture))
738/// — a hold — and a **drag session** ([`for_drag_session`](Self::for_drag_session)),
739/// whose ticks fire from a layout pass and whose OS phases arrive from a platform
740/// thread. Everything else — a legacy `WidgetEvent`
741/// ([`from_event`](Self::from_event)), an accessibility action, a hand-built test
742/// context — holds the [`Default`], a mouse at the epoch.
743#[derive(Clone, Debug, PartialEq)]
744pub(crate) struct InputSnapshot {
745    pub(crate) pointer: PointerInfo,
746    pub(crate) position: Option<Point>,
747    pub(crate) scroll_phase: ScrollPhase,
748    pub(crate) scroll_source: ScrollSource,
749    /// The positions the OS batched into this packet, oldest first and
750    /// excluding [`position`](Self::position).
751    ///
752    /// Carried onto the snapshot — rather than left on the
753    /// [`PointerSample`] the dispatcher discards — because two consumers need
754    /// them. The velocity fit behind a fling, first: a 500 Hz digitiser
755    /// decimated to frame rate under-reads a flick by the ratio of the two
756    /// rates. And a drawing surface, which reads them through
757    /// [`EventContext::coalesced`](crate::EventContext::coalesced) and would
758    /// otherwise draw a stroke through one position in every batch.
759    ///
760    /// The axes ride along. They used to be dropped here — the snapshot held
761    /// `(EventTime, Point)` — which left the one consumer that existed (the
762    /// velocity fit) correct and silently made the field useless for ink,
763    /// because a digitizer's pressure varies *within* a batch and it is that
764    /// variation a stroke's width is made of.
765    ///
766    /// Empty for every producer that does not coalesce, which costs no
767    /// allocation.
768    pub(crate) coalesced: Vec<CoalescedSample>,
769}
770
771impl Default for InputSnapshot {
772    fn default() -> Self {
773        Self {
774            pointer: PointerInfo::mouse(EventTime::ZERO),
775            position: None,
776            scroll_phase: ScrollPhase::Discrete,
777            scroll_source: ScrollSource::Wheel,
778            coalesced: Vec::new(),
779        }
780    }
781}
782
783impl InputSnapshot {
784    /// The snapshot a pointer sample implies.
785    pub(crate) fn from_pointer_sample(sample: &PointerSample) -> Self {
786        Self {
787            pointer: sample.pointer,
788            position: Some(sample.position),
789            coalesced: sample.coalesced.clone(),
790            ..Self::default()
791        }
792    }
793
794    /// The snapshot a gesture recognised by the **timer** implies.
795    ///
796    /// A hold is not a sample: nothing arrived, a deadline came due. But it is
797    /// still one contact's gesture, and a handler reached from it must not be
798    /// told it is serving the mouse — which is what it was told for as long as
799    /// this constructor did not exist, because `current_input` is
800    /// saved-and-restored around every dispatch and so holds the
801    /// [`Default`](Self::default) by the time a timer runs.
802    ///
803    /// [`position`](Self::position) stays `None` on purpose. The gesture
804    /// carries its own position, in **widget-local** coordinates, on the event
805    /// the handler is given; publishing a window position here as well would
806    /// offer a handler two answers that do not agree.
807    ///
808    /// [`coalesced`](Self::coalesced) stays empty for the same reason, and the
809    /// emptiness is written out below rather than inherited from
810    /// [`Default`](Self::default) so that it reads as the decision it is: a
811    /// deadline coming due batched nothing, and handing back the positions of
812    /// whichever sample happened to arrive last would attribute them to a
813    /// gesture that did not produce them. A surface that wants every position
814    /// — an ink tool — reads them on the **sample** path, which is where they
815    /// are; see [`EventContext::coalesced`](crate::EventContext::coalesced).
816    pub(crate) fn for_recognized_gesture(pointer: PointerInfo) -> Self {
817        Self {
818            pointer,
819            coalesced: Vec::new(),
820            ..Self::default()
821        }
822    }
823
824    /// The snapshot a **drag session** implies.
825    ///
826    /// A drag-and-drop session outlives the sample that started it: `on_drag_tick`
827    /// fires from a layout pass, and an OS drag's phases arrive from a platform
828    /// thread. Neither is a sample, so `current_input` holds the
829    /// [`Default`](Self::default) there — and a drag handler asking which device
830    /// it is serving was told "mouse" for the whole of a finger drag. The tree
831    /// installs this around those dispatches instead; the pointer comes from
832    /// `DragSession::pointer`, recorded when the drag started.
833    ///
834    /// [`position`](Self::position) stays `None` for the same reason it does on
835    /// [`for_recognized_gesture`](Self::for_recognized_gesture): the drag
836    /// handler is handed its position in **widget-local** coordinates, and a
837    /// window position published beside it would be a second answer that
838    /// disagrees. [`coalesced`](Self::coalesced) is empty on the same grounds,
839    /// and written out for the same reason — a tick fired from a layout pass
840    /// batched nothing.
841    pub(crate) fn for_drag_session(pointer: PointerInfo) -> Self {
842        Self {
843            pointer,
844            coalesced: Vec::new(),
845            ..Self::default()
846        }
847    }
848
849    /// The snapshot a scroll sample implies.
850    pub(crate) fn from_scroll_sample(sample: &ScrollSample) -> Self {
851        Self {
852            pointer: sample.pointer,
853            position: sample.position,
854            scroll_phase: sample.phase,
855            scroll_source: sample.source,
856            coalesced: Vec::new(),
857        }
858    }
859
860    /// The snapshot a legacy [`WidgetEvent`](crate::event::WidgetEvent)
861    /// implies. Pointer-bearing variants report what they carry; everything
862    /// else reports the default mouse.
863    pub(crate) fn from_event(event: &crate::event::WidgetEvent) -> Self {
864        use crate::event::WidgetEvent;
865        match event {
866            WidgetEvent::PointerDown {
867                position, pointer, ..
868            }
869            | WidgetEvent::PointerUp {
870                position, pointer, ..
871            }
872            | WidgetEvent::PointerMove {
873                position, pointer, ..
874            } => Self {
875                pointer: *pointer,
876                position: Some(*position),
877                ..Self::default()
878            },
879            // Hover transitions carry no position of their own — the move that
880            // caused them did.
881            WidgetEvent::PointerEnter { pointer } | WidgetEvent::PointerLeave { pointer } => Self {
882                pointer: *pointer,
883                ..Self::default()
884            },
885            WidgetEvent::Scroll {
886                window_position,
887                phase,
888                pointer,
889                ..
890            } => Self {
891                pointer: *pointer,
892                position: *window_position,
893                scroll_phase: *phase,
894                // A legacy `Scroll` carries no source; a wheel notch is what it
895                // has always been. `dispatch_scroll` overrides this from the
896                // sample.
897                scroll_source: ScrollSource::Wheel,
898                coalesced: Vec::new(),
899            },
900            WidgetEvent::PointerCancel {
901                window_position,
902                pointer,
903                ..
904            } => Self {
905                pointer: *pointer,
906                position: *window_position,
907                ..Self::default()
908            },
909            _ => Self::default(),
910        }
911    }
912}
913
914#[cfg(test)]
915mod tests {
916    use super::*;
917
918    // --- EventTime -------------------------------------------------------
919
920    #[test]
921    fn event_time_measures_from_the_epoch() {
922        let t = EventTime::from_millis(250);
923        assert_eq!(t.as_duration(), Duration::from_millis(250));
924        assert_eq!(EventTime::ZERO.as_duration(), Duration::ZERO);
925        assert_eq!(EventTime::default(), EventTime::ZERO);
926    }
927
928    #[test]
929    fn saturating_since_measures_forward() {
930        let a = EventTime::from_millis(100);
931        let b = EventTime::from_millis(350);
932        assert_eq!(b.saturating_since(a), Duration::from_millis(250));
933        assert_eq!(a.saturating_since(a), Duration::ZERO);
934    }
935
936    /// Samples can arrive out of order (a coalesced packet whose timestamps
937    /// predate the last one processed). An inverted pair must read as "no time
938    /// passed", not underflow.
939    #[test]
940    fn saturating_since_clamps_an_inverted_pair() {
941        let early = EventTime::from_millis(10);
942        let late = EventTime::from_millis(900);
943        assert_eq!(early.saturating_since(late), Duration::ZERO);
944    }
945
946    #[test]
947    fn checked_add_reports_overflow() {
948        let t = EventTime::from_millis(5);
949        assert_eq!(
950            t.checked_add(Duration::from_millis(15)),
951            Some(EventTime::from_millis(20))
952        );
953        assert_eq!(t.checked_add(Duration::MAX), None);
954    }
955
956    #[test]
957    fn event_times_order_by_their_offset() {
958        let mut times = [
959            EventTime::from_millis(30),
960            EventTime::ZERO,
961            EventTime::from_millis(7),
962        ];
963        times.sort();
964        assert_eq!(
965            times,
966            [
967                EventTime::ZERO,
968                EventTime::from_millis(7),
969                EventTime::from_millis(30)
970            ]
971        );
972    }
973
974    // --- PointerId -------------------------------------------------------
975
976    /// The reason there is no generation field: winit **reuses** `Touch::id`.
977    /// A press, a lift and a second press on the same raw id must produce two
978    /// different `PointerId`s, or the second contact inherits the first's
979    /// sequence.
980    #[test]
981    fn a_reused_os_id_mints_a_fresh_pointer_id() {
982        let alloc = PointerIdAllocator::global();
983        let device = BackendDeviceKey::new(0xFEED);
984
985        let first = alloc.begin(device, 7);
986        assert_eq!(alloc.get(device, 7), Some(first));
987        assert_eq!(alloc.end(device, 7), Some(first));
988        assert_eq!(alloc.get(device, 7), None);
989
990        let second = alloc.begin(device, 7);
991        assert_ne!(first, second, "a reused OS id must not reuse the PointerId");
992        assert!(second > first, "ids are monotonic");
993        alloc.end(device, 7);
994    }
995
996    /// Two devices may report the same contact id at the same time.
997    #[test]
998    fn the_same_os_id_on_two_devices_is_two_pointers() {
999        let alloc = PointerIdAllocator::global();
1000        let screen = BackendDeviceKey::new(0xA1);
1001        let tablet = BackendDeviceKey::new(0xB2);
1002
1003        let a = alloc.begin(screen, 1);
1004        let b = alloc.begin(tablet, 1);
1005        assert_ne!(a, b);
1006        assert_eq!(alloc.get(screen, 1), Some(a));
1007        assert_eq!(alloc.get(tablet, 1), Some(b));
1008
1009        alloc.end(screen, 1);
1010        assert_eq!(alloc.get(tablet, 1), Some(b), "ending one leaves the other");
1011        alloc.end(tablet, 1);
1012    }
1013
1014    /// A backend that loses an Up must not strand the next press on the stale
1015    /// identity.
1016    #[test]
1017    fn a_second_begin_replaces_a_stranded_mapping() {
1018        let alloc = PointerIdAllocator::global();
1019        let device = BackendDeviceKey::new(0xC3);
1020        let first = alloc.begin(device, 42);
1021        let second = alloc.begin(device, 42);
1022        assert_ne!(first, second);
1023        assert_eq!(alloc.get(device, 42), Some(second));
1024        alloc.end(device, 42);
1025    }
1026
1027    #[test]
1028    fn ending_an_unknown_contact_is_a_no_op() {
1029        let alloc = PointerIdAllocator::global();
1030        assert_eq!(alloc.end(BackendDeviceKey::new(0xD4), 999), None);
1031    }
1032
1033    #[test]
1034    fn the_mouse_id_is_never_minted() {
1035        let alloc = PointerIdAllocator::global();
1036        let device = BackendDeviceKey::new(0xE5);
1037        let id = alloc.begin(device, 3);
1038        assert_ne!(id, PointerId::MOUSE);
1039        assert_eq!(PointerId::MOUSE.get(), 1);
1040        alloc.end(device, 3);
1041    }
1042
1043    // --- PointerInfo -----------------------------------------------------
1044
1045    #[test]
1046    fn the_mouse_constructor_is_the_legacy_pointer() {
1047        let m = PointerInfo::mouse(EventTime::ZERO);
1048        assert_eq!(m.id, PointerId::MOUSE);
1049        assert_eq!(m.kind, PointerKind::Mouse);
1050        assert!(m.primary);
1051        assert!(m.buttons.is_empty());
1052        assert_eq!(m.axes, PointerAxes::default());
1053        assert!(!m.is_direct() && !m.is_coarse() && m.is_precise());
1054    }
1055
1056    #[test]
1057    fn a_touch_contact_is_direct_and_coarse() {
1058        let t = PointerInfo::touch(PointerId::MOUSE, EventTime::ZERO);
1059        assert_eq!(t.kind, PointerKind::Touch);
1060        assert!(t.is_direct() && t.is_coarse() && !t.is_precise());
1061        assert!(
1062            !t.primary,
1063            "primacy is the pointer table's decision, not the constructor's"
1064        );
1065    }
1066
1067    /// W3C Pointer Events L3: report what the device said; failing that, 0.5
1068    /// while a button is down and 0.0 otherwise.
1069    #[test]
1070    fn effective_pressure_follows_the_w3c_rule() {
1071        let mut m = PointerInfo::mouse(EventTime::ZERO);
1072        assert_eq!(m.effective_pressure(), 0.0);
1073
1074        m.buttons = ButtonMask::PRIMARY;
1075        assert_eq!(m.effective_pressure(), 0.5);
1076
1077        m.axes.pressure = Some(0.75);
1078        assert_eq!(m.effective_pressure(), 0.75);
1079
1080        m.buttons = ButtonMask::NONE;
1081        assert_eq!(m.effective_pressure(), 0.75, "a reported value always wins");
1082    }
1083
1084    // --- Samples ---------------------------------------------------------
1085
1086    #[test]
1087    fn a_mouse_sample_carries_no_coalesced_history() {
1088        let s = PointerSample::mouse(PointerPhase::Down, Point::new(3.0, 4.0), EventTime::ZERO)
1089            .with_button(PointerButton::Primary)
1090            .with_modifiers(Modifiers::SHIFT);
1091        assert!(s.coalesced.is_empty());
1092        assert_eq!(s.button, Some(PointerButton::Primary));
1093        assert_eq!(s.modifiers, Modifiers::SHIFT);
1094        assert_eq!(s.pointer.id, PointerId::MOUSE);
1095    }
1096
1097    #[test]
1098    fn a_wheel_sample_is_discrete_and_positionless() {
1099        let s = ScrollSample::wheel(
1100            ScrollDelta::Lines { x: 0.0, y: -1.0 },
1101            Modifiers::NONE,
1102            EventTime::ZERO,
1103        );
1104        assert_eq!(s.phase, ScrollPhase::Discrete);
1105        assert_eq!(s.source, ScrollSource::Wheel);
1106        assert_eq!(s.position, None);
1107
1108        let at = s.at(Point::new(10.0, 20.0));
1109        assert_eq!(at.position, Some(Point::new(10.0, 20.0)));
1110    }
1111
1112    #[test]
1113    fn scroll_defaults_are_todays_wheel() {
1114        assert_eq!(ScrollPhase::default(), ScrollPhase::Discrete);
1115        assert_eq!(ScrollSource::default(), ScrollSource::Wheel);
1116    }
1117
1118    // --- InputSnapshot ---------------------------------------------------
1119
1120    #[test]
1121    fn the_default_snapshot_is_a_mouse_at_the_epoch() {
1122        let s = InputSnapshot::default();
1123        assert_eq!(s.pointer.id, PointerId::MOUSE);
1124        assert_eq!(s.pointer.time, EventTime::ZERO);
1125        assert_eq!(s.position, None);
1126        assert_eq!(s.scroll_phase, ScrollPhase::Discrete);
1127        assert_eq!(s.scroll_source, ScrollSource::Wheel);
1128    }
1129
1130    #[test]
1131    fn a_scroll_sample_snapshot_keeps_its_phase_and_source() {
1132        let sample = ScrollSample {
1133            delta: ScrollDelta::Pixels { x: 0.0, y: 12.0 },
1134            position: Some(Point::new(5.0, 5.0)),
1135            phase: ScrollPhase::Momentum,
1136            source: ScrollSource::TouchPan,
1137            pointer: PointerInfo::mouse(EventTime::from_millis(9)),
1138            modifiers: Modifiers::NONE,
1139        };
1140        let snap = InputSnapshot::from_scroll_sample(&sample);
1141        assert_eq!(snap.scroll_phase, ScrollPhase::Momentum);
1142        assert_eq!(snap.scroll_source, ScrollSource::TouchPan);
1143        assert_eq!(snap.position, Some(Point::new(5.0, 5.0)));
1144    }
1145}