Skip to main content

teksilo_core/
gesture.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! UIKit-style gesture recognizer model.
5//!
6//! Gesture recognizers are composable state machines attached to widgets.
7//! Each recognizer monitors the raw pointer event stream and emits recognized
8//! gestures when patterns complete. They are pure state machines with no
9//! platform dependencies and **no wall clock** — every threshold and every
10//! instant reaches them through a [`RecognizerContext`], which is what makes
11//! them tunable per pointer kind and testable against a
12//! [`ManualClock`](crate::pointer::clock::ManualClock).
13//!
14//! The [`GestureArena`] arbitrates when multiple recognizers compete on the
15//! same event stream: all are fed in parallel, and when one recognizes, the
16//! rest are reset (except cooperative peers — see
17//! [`GestureRecognizer::resets_on_peer_recognition`]). One arena serves one
18//! *contact*; the [`GestureArenaSet`] a node carries owns one arena per live
19//! [`PointerId`](crate::pointer::PointerId) plus the node's [`TapStreak`],
20//! which outlives every contact so a touch double tap — two presses, two
21//! different pointer ids — can be recognized at all.
22//!
23//! **Click-style recognizers carry button + modifiers.** [`TapRecognizer`],
24//! [`DoubleTapRecognizer`], [`TripleTapRecognizer`], and
25//! [`LongPressRecognizer`] all default to `ButtonMask::PRIMARY` —
26//! left-click only — and emit [`TapEvent`]s carrying position, the
27//! finalising button, and modifier state. Multi-tap recognizers
28//! require button-match across the whole sequence. Widen the accepted
29//! set with `.accept_buttons(...)` / `.accept_any_button()`.
30
31use teksilo_canvas::{Point, Vec2};
32
33use crate::event::{Modifiers, PointerButton};
34use crate::pointer::{CancelReason, EventTime, PointerInfo};
35
36mod arena;
37mod arena_set;
38mod config;
39mod drag;
40mod long_press;
41mod multi_tap;
42mod palm;
43mod pan;
44mod pinch;
45mod sequence;
46mod swipe;
47mod tap;
48
49pub use arena::GestureArena;
50pub use arena_set::{GestureArenaSet, GestureProto};
51pub use config::{MultiContact, RecognizerContext, TapStreak, default_profile};
52pub use drag::DragRecognizer;
53pub use long_press::LongPressRecognizer;
54pub use multi_tap::{DoubleTapRecognizer, TripleTapRecognizer};
55pub use palm::{PALM_CONTACT_THRESHOLD, PalmWatch};
56pub use pan::PanRecognizer;
57pub use pinch::TouchPinchRecognizer;
58pub use sequence::{MemberRole, MemberState, PointerSequence, SequenceMember, TapBoundary};
59pub use swipe::SwipeRecognizer;
60pub use tap::TapRecognizer;
61
62/// Information about a recognized click-style gesture, passed to the
63/// four tap-family handlers (`on_tap`, `on_double_tap`, `on_triple_tap`,
64/// `on_long_press`).
65///
66/// The struct is `#[non_exhaustive]` so future fields (timestamp, click
67/// count for a hypothetical `on_n_tap`, pressure for stylus events) can
68/// land without breaking existing match patterns.
69#[derive(Debug, Clone, Copy)]
70#[non_exhaustive]
71pub struct TapEvent {
72    /// Pointer position in widget-local coords, captured at the
73    /// finalising event (the `Up` of the last tap for tap / double-tap /
74    /// triple-tap; the held `Down` for long-press, since long-press
75    /// recognises on a `tick` before any `Up`).
76    pub position: Point,
77
78    /// Which button finalised the gesture. Multi-tap recognizers
79    /// require every tap in the sequence to use the same button —
80    /// mixed-button sequences fail rather than spuriously firing.
81    pub button: PointerButton,
82
83    /// Modifier keys held at the finalising event. Sourced from
84    /// `WidgetEvent::PointerUp { modifiers, .. }` (or `PointerDown` for
85    /// long-press).
86    pub modifiers: Modifiers,
87
88    /// Which pointer produced the gesture — the mouse, a numbered finger, a
89    /// stylus. A handler reads `pointer.kind` to tell a finger tap from a
90    /// click without consulting the tree.
91    pub pointer: PointerInfo,
92}
93
94impl TapEvent {
95    /// Construct a `TapEvent` directly. Useful for tests; widgets receive
96    /// `&TapEvent` from the recognizer pipeline and rarely need to build
97    /// one by hand.
98    pub fn new(position: Point, button: PointerButton, modifiers: Modifiers) -> Self {
99        Self {
100            position,
101            button,
102            modifiers,
103            pointer: PointerInfo::mouse(EventTime::ZERO),
104        }
105    }
106
107    /// The same event attributed to `pointer`. The recognizers build their
108    /// `TapEvent`s this way, from the pointer on the sample that finalised the
109    /// gesture.
110    pub fn with_pointer(mut self, pointer: PointerInfo) -> Self {
111        self.pointer = pointer;
112        self
113    }
114}
115
116/// Raw pointer events fed into gesture recognizers.
117///
118/// Every variant carries the [`PointerInfo`] that produced it and the
119/// [`EventTime`] the backend stamped it with, so a recognizer never has to ask
120/// *which* contact this is or *when* it happened — the two questions a
121/// per-contact, clock-free recognizer cannot answer for itself.
122#[derive(Debug, Clone, Copy)]
123pub enum RawPointerEvent {
124    Down {
125        position: Point,
126        button: PointerButton,
127        modifiers: Modifiers,
128        pointer: PointerInfo,
129        time: EventTime,
130    },
131    Move {
132        position: Point,
133        pointer: PointerInfo,
134        time: EventTime,
135    },
136    Up {
137        position: Point,
138        button: PointerButton,
139        modifiers: Modifiers,
140        pointer: PointerInfo,
141        time: EventTime,
142    },
143    /// The interaction was revoked rather than completed — the window lost
144    /// focus, a modal opened over it, the OS took the pointer. Distinct from
145    /// `Up` on purpose: an `Up` means the user finished, a `Cancel` means the
146    /// system interrupted, and conflating them is how a drag ends up "dropped"
147    /// wherever the pointer happened to be.
148    Cancel {
149        position: Point,
150        pointer: PointerInfo,
151        reason: CancelReason,
152        time: EventTime,
153    },
154}
155
156impl RawPointerEvent {
157    /// Which pointer produced this event.
158    pub fn pointer(&self) -> PointerInfo {
159        match self {
160            Self::Down { pointer, .. }
161            | Self::Move { pointer, .. }
162            | Self::Up { pointer, .. }
163            | Self::Cancel { pointer, .. } => *pointer,
164        }
165    }
166
167    /// When it happened, on the tree's input timeline.
168    pub fn time(&self) -> EventTime {
169        match self {
170            Self::Down { time, .. }
171            | Self::Move { time, .. }
172            | Self::Up { time, .. }
173            | Self::Cancel { time, .. } => *time,
174        }
175    }
176
177    /// Where it happened, in the receiving widget's local coordinates.
178    pub fn position(&self) -> Point {
179        match self {
180            Self::Down { position, .. }
181            | Self::Move { position, .. }
182            | Self::Up { position, .. }
183            | Self::Cancel { position, .. } => *position,
184        }
185    }
186}
187
188/// Result of processing a raw event through a gesture recognizer.
189#[derive(Debug, Clone)]
190pub enum GestureResult {
191    /// Not enough data yet — keep feeding events.
192    Pending,
193    /// A gesture has been recognized.
194    Recognized(GestureEvent),
195    /// This event sequence cannot match the gesture — recognizer should be reset.
196    Failed,
197}
198
199/// A recognized gesture event.
200///
201/// The four click-style variants (`Tap` / `DoubleTap` / `TripleTap` /
202/// `LongPress`) carry a [`TapEvent`] payload — pointer position, the
203/// finalising mouse button, and the modifier state at that moment.
204#[derive(Debug, Clone, Copy)]
205pub enum GestureEvent {
206    Tap(TapEvent),
207    DoubleTap(TapEvent),
208    TripleTap(TapEvent),
209    LongPress(TapEvent),
210    DragStarted {
211        position: Point,
212        button: PointerButton,
213        pointer: PointerInfo,
214    },
215    DragMoved {
216        position: Point,
217        delta: Vec2,
218        pointer: PointerInfo,
219    },
220    DragEnded {
221        position: Point,
222        pointer: PointerInfo,
223    },
224    /// The drag was revoked rather than released. A handler that has been
225    /// mutating state since `DragStarted` must undo it here, not commit it.
226    DragCancelled {
227        position: Point,
228        pointer: PointerInfo,
229        reason: CancelReason,
230    },
231    PinchStarted {
232        center: Point,
233    },
234    /// A running pinch's geometry changed.
235    ///
236    /// # The producer contract
237    ///
238    /// Both `scale` and `rotation` are **per-sample deltas**, measured against
239    /// the previous sample of this same gesture — against the geometry at
240    /// [`PinchStarted`](Self::PinchStarted) for the first one. A consumer folds
241    /// each sample into what it already holds (multiplying for `scale`, adding
242    /// for `rotation`) and never reads a sample as an absolute.
243    ///
244    /// Deltas rather than values cumulative since the start, because a pinch has
245    /// two producers that must agree and only one of them *can* report a
246    /// cumulative value: winit's trackpad `PinchGesture` / `RotationGesture`
247    /// report a change per event and hand over no gesture-start baseline to
248    /// divide by. [`TouchPinchRecognizer`], which does have one, keeps it
249    /// internally and exposes it under a name that cannot be mistaken for these
250    /// fields — see
251    /// [`cumulative_scale`](TouchPinchRecognizer::cumulative_scale).
252    PinchChanged {
253        /// Midpoint of the two contacts (or of the trackpad gesture), in the
254        /// receiving widget's local coordinates.
255        center: Point,
256        /// The span **now** divided by the span at the previous sample. `1.0` is
257        /// no change, above `1.0` a spread, below `1.0` a squeeze. A producer
258        /// never emits `0.0`, a negative or a non-finite value; a consumer
259        /// handed one anyway should drop the sample rather than apply it.
260        /// Multiply by it — do not assign it.
261        scale: f32,
262        /// The twist since the previous sample, in **radians** (never degrees:
263        /// the platform translator converts at the seam, where winit's unit is
264        /// known). Signed, and unwrapped — a gesture turned past ±π keeps
265        /// producing same-signed steps instead of jumping by 2π. `0.0` from a
266        /// producer that reports magnification without rotation. Add it — do not
267        /// assign it.
268        rotation: f32,
269    },
270    PinchEnded,
271    /// The pinch was revoked rather than released. Emitted by
272    /// [`TouchPinchRecognizer`] when the cancel funnel takes one of its two
273    /// contacts away.
274    PinchCancelled {
275        reason: CancelReason,
276    },
277    Swipe {
278        direction: SwipeDirection,
279        velocity: f32,
280    },
281}
282
283/// Direction of a swipe gesture.
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum SwipeDirection {
286    Left,
287    Right,
288    Up,
289    Down,
290}
291
292/// Phase of a drag gesture, as delivered to an `on_drag` handler.
293///
294/// This is the public API for drag handlers — the raw `GestureEvent::Drag*`
295/// variants are an implementation detail of the recognizer pipeline. A
296/// handler only ever receives `Started` once, followed by zero or more
297/// `Moved`, then exactly one `Ended`.
298///
299/// `#[non_exhaustive]`: [`Cancelled`](Self::Cancelled) joined the enum with the
300/// cancel funnel, and a phase carrying velocity is anticipated for the fling
301/// work, so a `match` on it needs a `_` arm.
302#[derive(Debug, Clone, Copy)]
303#[non_exhaustive]
304pub enum DragPhase {
305    Started {
306        position: Point,
307        button: PointerButton,
308        pointer: PointerInfo,
309    },
310    Moved {
311        position: Point,
312        delta: Vec2,
313        pointer: PointerInfo,
314    },
315    Ended {
316        position: Point,
317        pointer: PointerInfo,
318    },
319    /// The drag was revoked. Exactly one of `Ended` or `Cancelled` follows a
320    /// `Started`; a handler that committed nothing until `Ended` has nothing
321    /// to undo, and one that mutated as it went must roll back here.
322    Cancelled {
323        position: Point,
324        pointer: PointerInfo,
325        reason: CancelReason,
326    },
327}
328
329/// Phase of a pinch (or rotation) gesture, as delivered to an `on_pinch`
330/// handler. On desktop these are produced by OS trackpad gestures
331/// (`TouchpadMagnify` / `RotationGesture`); on touch they come from a
332/// dedicated recognizer ([`TouchPinchRecognizer`]). Both producers satisfy one
333/// contract, stated on [`Changed`](Self::Changed).
334///
335/// `#[non_exhaustive]` for the same reason as [`DragPhase`].
336#[derive(Debug, Clone, Copy)]
337#[non_exhaustive]
338pub enum PinchPhase {
339    Started {
340        center: Point,
341        pointer: PointerInfo,
342    },
343    /// The pinch's geometry changed.
344    ///
345    /// `scale` and `rotation` are **per-sample deltas** against the previous
346    /// sample of this gesture, exactly as
347    /// [`GestureEvent::PinchChanged`] defines them — fold each sample in
348    /// (multiply for `scale`, add for `rotation`) rather than assigning it.
349    Changed {
350        /// Midpoint of the gesture, in the receiving widget's local
351        /// coordinates.
352        center: Point,
353        /// The span now over the span at the previous sample. Multiply by it.
354        scale: f32,
355        /// The twist since the previous sample, in **radians**. Add it.
356        rotation: f32,
357        pointer: PointerInfo,
358    },
359    Ended {
360        pointer: PointerInfo,
361    },
362    /// The pinch was revoked rather than released.
363    Cancelled {
364        pointer: PointerInfo,
365        reason: CancelReason,
366    },
367}
368
369/// Trait for gesture recognizers. Each is a composable state machine.
370///
371/// Every method that could depend on the outside world takes a
372/// [`RecognizerContext`]: the current time, the [`GestureProfile`] for the
373/// pointer in play, the owning node's local bounds, the pointer itself, and
374/// the node's [`TapStreak`]. A recognizer therefore holds only the state of the
375/// *one contact* it is following — no clock, no thresholds of its own beyond
376/// explicit per-instance overrides, and no cross-contact tap counting.
377///
378/// [`GestureProfile`]: teksilo_tokens::GestureProfile
379pub trait GestureRecognizer {
380    /// Feed a raw pointer event and return the recognition result.
381    fn process(&mut self, event: &RawPointerEvent, cx: &RecognizerContext) -> GestureResult;
382
383    /// Advance any time-driven state (e.g. the long-press elapsed timer).
384    /// Default is a no-op — only recognizers that depend on time (like
385    /// [`LongPressRecognizer`]) override this.
386    fn tick(&mut self, _cx: &RecognizerContext) -> GestureResult {
387        GestureResult::Pending
388    }
389
390    /// Earliest future [`EventTime`] at which calling
391    /// [`tick`](GestureRecognizer::tick) could transition the recognizer into
392    /// `Recognized` or `Failed`. Returns `None` when the recognizer is idle or
393    /// not time-driven. Used by the event loop to schedule a wake-up before a
394    /// long press fires.
395    fn next_deadline(&self) -> Option<EventTime> {
396        None
397    }
398
399    /// Abandon the attempt in progress without emitting anything.
400    ///
401    /// Distinct from [`reset`](GestureRecognizer::reset) in intent rather than
402    /// in default behaviour: `reset` is arbitration bookkeeping ("you lost,
403    /// start over"), `cancel` is the user or the system taking the interaction
404    /// away. Defaults to `reset`; a recognizer whose mid-gesture state needs a
405    /// different unwind overrides it.
406    fn cancel(&mut self) {
407        self.reset();
408    }
409
410    /// Reset the recognizer to its initial state.
411    fn reset(&mut self);
412
413    /// Priority for arbitration when multiple recognizers compete.
414    /// Higher priority wins.
415    fn priority(&self) -> u32;
416
417    /// Whether this recognizer should be reset when a peer wins arbitration
418    /// in the same `GestureArena::process` call. The default is `true` —
419    /// winner-take-all, the usual behaviour for mutually exclusive gestures
420    /// (tap vs drag, long-press vs tap). Multi-tap recognizers
421    /// (`DoubleTapRecognizer`, `TripleTapRecognizer`) override this to
422    /// `false` so a `DoubleTap` firing at click 2 does not wipe the
423    /// `TripleTapRecognizer`'s accumulated state before click 3 arrives.
424    fn resets_on_peer_recognition(&self) -> bool {
425        true
426    }
427
428    /// Whether this recognizer belongs to the *tap family* — tap, double tap,
429    /// triple tap, long press.
430    ///
431    /// Read by [`GestureArenaSet::cancel_taps`], which revokes exactly this
432    /// family and leaves a live drag alone. That asymmetry is what WCAG 2.2
433    /// SC 2.5.2 ("Pointer Cancellation") needs: sliding off a control must
434    /// abort its activation, without aborting a drag the same press started.
435    fn tap_family(&self) -> bool {
436        false
437    }
438
439    /// Whether this recognizer takes part in cross-node sequence arbitration —
440    /// the "who owns this press" negotiation between a scrollable and the row
441    /// inside it. [`PanRecognizer`] is the one that says `true`.
442    ///
443    /// It is a **declaration, not a hook**: the router arbitrates on the
444    /// sequence's own [`MemberRole`], which it holds directly, so nothing on
445    /// the dispatch path has to interrogate a boxed recognizer to find out
446    /// what kind of competitor it is. The flag is what a reader — and a
447    /// third-party recognizer author — reads to know which side of that
448    /// negotiation a type belongs on.
449    fn competes_for_sequence(&self) -> bool {
450        false
451    }
452
453    /// Whether this recognizer wants every live contact rather than just the
454    /// one its arena was created for. [`TouchPinchRecognizer`] says `true`;
455    /// every single-contact recognizer says `false`.
456    ///
457    /// Also a declaration rather than a hook, and for a structural reason: a
458    /// [`GestureArena`] serves exactly one contact, so a recognizer that needs
459    /// two cannot live in one at all. The tree owns its pinch directly and
460    /// feeds it every contact (`widget_tree::pan_arbiter::feed_pinch`); the
461    /// flag is how such a type declares that it must be owned that way.
462    fn wants_all_pointers(&self) -> bool {
463        false
464    }
465}
466
467pub(crate) fn distance(a: Point, b: Point) -> f32 {
468    let dx = a.x - b.x;
469    let dy = a.y - b.y;
470    (dx * dx + dy * dy).sqrt()
471}
472
473#[cfg(test)]
474pub(crate) mod test_helpers {
475    use super::{EventTime, Modifiers, Point, PointerButton, PointerInfo, RawPointerEvent};
476    use crate::pointer::CancelReason;
477
478    /// The pointer every helper below attributes its event to unless told
479    /// otherwise: the mouse, at the epoch.
480    pub fn mouse_pointer() -> PointerInfo {
481        PointerInfo::mouse(EventTime::ZERO)
482    }
483
484    pub fn down(pos: Point) -> RawPointerEvent {
485        down_btn(pos, PointerButton::Primary)
486    }
487
488    pub fn down_btn(pos: Point, button: PointerButton) -> RawPointerEvent {
489        down_full(pos, button, Modifiers::NONE)
490    }
491
492    pub fn down_full(pos: Point, button: PointerButton, modifiers: Modifiers) -> RawPointerEvent {
493        RawPointerEvent::Down {
494            position: pos,
495            button,
496            modifiers,
497            pointer: mouse_pointer(),
498            time: EventTime::ZERO,
499        }
500    }
501
502    pub fn up(pos: Point) -> RawPointerEvent {
503        up_btn(pos, PointerButton::Primary)
504    }
505
506    pub fn up_btn(pos: Point, button: PointerButton) -> RawPointerEvent {
507        up_full(pos, button, Modifiers::NONE)
508    }
509
510    pub fn up_full(pos: Point, button: PointerButton, modifiers: Modifiers) -> RawPointerEvent {
511        RawPointerEvent::Up {
512            position: pos,
513            button,
514            modifiers,
515            pointer: mouse_pointer(),
516            time: EventTime::ZERO,
517        }
518    }
519
520    pub fn move_to(pos: Point) -> RawPointerEvent {
521        RawPointerEvent::Move {
522            position: pos,
523            pointer: mouse_pointer(),
524            time: EventTime::ZERO,
525        }
526    }
527
528    pub fn cancel_at(pos: Point) -> RawPointerEvent {
529        RawPointerEvent::Cancel {
530            position: pos,
531            pointer: mouse_pointer(),
532            reason: CancelReason::Platform,
533            time: EventTime::ZERO,
534        }
535    }
536
537    /// The same event attributed to `pointer` and stamped at `time`.
538    pub fn retimed(
539        event: RawPointerEvent,
540        pointer: PointerInfo,
541        time: EventTime,
542    ) -> RawPointerEvent {
543        match event {
544            RawPointerEvent::Down {
545                position,
546                button,
547                modifiers,
548                ..
549            } => RawPointerEvent::Down {
550                position,
551                button,
552                modifiers,
553                pointer,
554                time,
555            },
556            RawPointerEvent::Move { position, .. } => RawPointerEvent::Move {
557                position,
558                pointer,
559                time,
560            },
561            RawPointerEvent::Up {
562                position,
563                button,
564                modifiers,
565                ..
566            } => RawPointerEvent::Up {
567                position,
568                button,
569                modifiers,
570                pointer,
571                time,
572            },
573            RawPointerEvent::Cancel {
574                position, reason, ..
575            } => RawPointerEvent::Cancel {
576                position,
577                pointer,
578                reason,
579                time,
580            },
581        }
582    }
583}
584
585#[cfg(test)]
586mod source_scan_tests {
587    /// Every `.rs` file the gesture layer is made of.
588    fn gesture_sources() -> Vec<(String, String)> {
589        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/gesture");
590        let mut out = vec![(
591            "gesture.rs".to_string(),
592            std::fs::read_to_string(
593                std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/gesture.rs"),
594            )
595            .expect("gesture.rs is readable"),
596        )];
597        for entry in std::fs::read_dir(&root).expect("src/gesture is readable") {
598            let path = entry.expect("readable dir entry").path();
599            if path.extension().and_then(|e| e.to_str()) == Some("rs") {
600                let name = path
601                    .file_name()
602                    .and_then(|n| n.to_str())
603                    .unwrap_or("?")
604                    .to_string();
605                out.push((name, std::fs::read_to_string(&path).expect("readable")));
606            }
607        }
608        out
609    }
610
611    /// Everything outside a `#[cfg(test)] mod` / `#[cfg(test)] impl` block:
612    /// the code that actually ships.
613    ///
614    /// Byte-indexed throughout — these files are full of em-dashes, and the
615    /// only positions it ever cuts at (`#[cfg(test)]`, `{`, `}`) are ASCII, so
616    /// every slice lands on a character boundary.
617    fn production_only(source: &str) -> String {
618        const MARKER: &[u8] = b"#[cfg(test)]";
619        let bytes = source.as_bytes();
620        let mut out = String::with_capacity(source.len());
621        let mut kept_from = 0usize;
622        let mut i = 0usize;
623        while i < bytes.len() {
624            if !bytes[i..].starts_with(MARKER) {
625                i += 1;
626                continue;
627            }
628            // Only `mod` and `impl` introduce a whole block of test-only code.
629            // A `#[cfg(test)]` on a field or a single fn is left in place — and
630            // must therefore still be free of wall-clock reads to matter.
631            let after = i + MARKER.len();
632            let head_end = bytes[after..]
633                .iter()
634                .position(|c| *c == b'{')
635                .map(|off| after + off);
636            let Some(head_end) = head_end else { break };
637            let head = source[after..head_end].trim_start();
638            if !(head.starts_with("mod ") || head.starts_with("impl ")) {
639                i = after;
640                continue;
641            }
642            // Brace-match from the block's opening brace.
643            let mut depth = 0usize;
644            let mut j = head_end;
645            while j < bytes.len() {
646                match bytes[j] {
647                    b'{' => depth += 1,
648                    b'}' => {
649                        depth -= 1;
650                        if depth == 0 {
651                            j += 1;
652                            break;
653                        }
654                    }
655                    _ => {}
656                }
657                j += 1;
658            }
659            out.push_str(&source[kept_from..i]);
660            kept_from = j;
661            i = j;
662        }
663        out.push_str(&source[kept_from..]);
664        out
665    }
666
667    /// A recognizer that reads the wall clock cannot be driven by a simulated
668    /// one, and a test that cannot advance the clock cannot test a long press
669    /// or a double-tap window without sleeping. `Instant::now()` is therefore
670    /// banned from the gesture layer outside its own test blocks — time
671    /// arrives through `RecognizerContext::now`.
672    /// Everything before the first `//` on each line: code, not prose. Keeps
673    /// the scan from tripping over its own documentation, which names
674    /// `Instant::now()` to explain why it is banned.
675    fn code_only(source: &str) -> String {
676        source
677            .lines()
678            .map(|line| match line.find("//") {
679                Some(idx) => &line[..idx],
680                None => line,
681            })
682            .collect::<Vec<_>>()
683            .join("\n")
684    }
685
686    #[test]
687    fn no_wall_clock_in_gestures() {
688        for (name, source) in gesture_sources() {
689            let production = code_only(&production_only(&source));
690            assert!(
691                !production.contains("Instant::now()"),
692                "{name} reads the wall clock outside its test blocks; gesture \
693                 recognizers must take their time from `RecognizerContext::now`"
694            );
695        }
696    }
697
698    /// The stripper must not eat the whole file — otherwise the scan above
699    /// passes vacuously.
700    #[test]
701    fn the_scan_keeps_the_production_half_of_each_file() {
702        for (name, source) in gesture_sources() {
703            let production = production_only(&source);
704            assert!(
705                production.contains("// SPDX-License-Identifier"),
706                "{name}: the test-block stripper ate the file header"
707            );
708            assert!(
709                code_only(&production).contains("use "),
710                "{name}: the comment stripper ate the code"
711            );
712            if source.contains("pub struct") {
713                assert!(
714                    production.contains("pub struct"),
715                    "{name}: the test-block stripper ate the production types"
716                );
717            }
718        }
719    }
720
721    /// The scan is only meaningful if it actually found the files.
722    #[test]
723    fn the_source_scan_sees_every_recognizer() {
724        let names: Vec<String> = gesture_sources().into_iter().map(|(n, _)| n).collect();
725        for expected in [
726            "gesture.rs",
727            "arena.rs",
728            "arena_set.rs",
729            "config.rs",
730            "drag.rs",
731            "long_press.rs",
732            "multi_tap.rs",
733            "palm.rs",
734            "pan.rs",
735            "pinch.rs",
736            "swipe.rs",
737            "tap.rs",
738        ] {
739            assert!(
740                names.iter().any(|n| n == expected),
741                "the wall-clock scan missed {expected}; it saw {names:?}"
742            );
743        }
744    }
745}