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}