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` — or one `Cancelled` in its place.
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/// (`PinchGesture` / `RotationGesture`); on touch they come from a Both producers satisfy one
331/// contract, stated on [`Changed`](Self::Changed).
332///
333/// `#[non_exhaustive]` for the same reason as [`DragPhase`].
334#[derive(Debug, Clone, Copy)]
335#[non_exhaustive]
336pub enum PinchPhase {
337    Started {
338        center: Point,
339        pointer: PointerInfo,
340    },
341    /// The pinch's geometry changed.
342    ///
343    /// `scale` and `rotation` are **per-sample deltas** against the previous
344    /// sample of this gesture, exactly as
345    /// [`GestureEvent::PinchChanged`] defines them — fold each sample in
346    /// (multiply for `scale`, add for `rotation`) rather than assigning it.
347    Changed {
348        /// Midpoint of the gesture, in the receiving widget's local
349        /// coordinates.
350        center: Point,
351        /// The span now over the span at the previous sample. Multiply by it.
352        scale: f32,
353        /// The twist since the previous sample, in **radians**. Add it.
354        rotation: f32,
355        pointer: PointerInfo,
356    },
357    Ended {
358        pointer: PointerInfo,
359    },
360    /// The pinch was revoked rather than released.
361    Cancelled {
362        pointer: PointerInfo,
363        reason: CancelReason,
364    },
365}
366
367/// Trait for gesture recognizers. Each is a composable state machine.
368///
369/// Every method that could depend on the outside world takes a
370/// [`RecognizerContext`]: the current time, the [`GestureProfile`] for the
371/// pointer in play, the owning node's local bounds, the pointer itself, and
372/// the node's [`TapStreak`]. A recognizer therefore holds only the state of the
373/// *one contact* it is following — no clock, no thresholds of its own beyond
374/// explicit per-instance overrides, and no cross-contact tap counting.
375///
376/// [`GestureProfile`]: teksilo_tokens::GestureProfile
377pub trait GestureRecognizer {
378    /// Feed a raw pointer event and return the recognition result.
379    fn process(&mut self, event: &RawPointerEvent, cx: &RecognizerContext) -> GestureResult;
380
381    /// Advance any time-driven state (e.g. the long-press elapsed timer).
382    /// Default is a no-op — only recognizers that depend on time (like
383    /// [`LongPressRecognizer`]) override this.
384    fn tick(&mut self, _cx: &RecognizerContext) -> GestureResult {
385        GestureResult::Pending
386    }
387
388    /// Earliest future [`EventTime`] at which calling
389    /// [`tick`](GestureRecognizer::tick) could transition the recognizer into
390    /// `Recognized` or `Failed`. Returns `None` when the recognizer is idle or
391    /// not time-driven. Used by the event loop to schedule a wake-up before a
392    /// long press fires.
393    fn next_deadline(&self) -> Option<EventTime> {
394        None
395    }
396
397    /// Abandon the attempt in progress without emitting anything.
398    ///
399    /// Distinct from [`reset`](GestureRecognizer::reset) in intent rather than
400    /// in default behaviour: `reset` is arbitration bookkeeping ("you lost,
401    /// start over"), `cancel` is the user or the system taking the interaction
402    /// away. Defaults to `reset`; a recognizer whose mid-gesture state needs a
403    /// different unwind overrides it.
404    fn cancel(&mut self) {
405        self.reset();
406    }
407
408    /// Reset the recognizer to its initial state.
409    fn reset(&mut self);
410
411    /// Priority for arbitration when multiple recognizers compete.
412    /// Higher priority wins.
413    fn priority(&self) -> u32;
414
415    /// Whether this recognizer should be reset when a peer wins arbitration
416    /// in the same `GestureArena::process` call. The default is `true` —
417    /// winner-take-all, the usual behaviour for mutually exclusive gestures
418    /// (tap vs drag, long-press vs tap). Multi-tap recognizers
419    /// (`DoubleTapRecognizer`, `TripleTapRecognizer`) override this to
420    /// `false` so a `DoubleTap` firing at click 2 does not wipe the
421    /// `TripleTapRecognizer`'s accumulated state before click 3 arrives.
422    fn resets_on_peer_recognition(&self) -> bool {
423        true
424    }
425
426    /// Whether this recognizer belongs to the *tap family* — tap, double tap,
427    /// triple tap, long press.
428    ///
429    /// Read by [`GestureArenaSet::cancel_taps`], which revokes exactly this
430    /// family and leaves a live drag alone. That asymmetry is what WCAG 2.2
431    /// SC 2.5.2 ("Pointer Cancellation") needs: sliding off a control must
432    /// abort its activation, without aborting a drag the same press started.
433    fn tap_family(&self) -> bool {
434        false
435    }
436
437    /// Whether this recognizer takes part in cross-node sequence arbitration —
438    /// the "who owns this press" negotiation between a scrollable and the row
439    /// inside it. [`PanRecognizer`] is the one that says `true`.
440    ///
441    /// It is a **declaration, not a hook**: the router arbitrates on the
442    /// sequence's own [`MemberRole`], which it holds directly, so nothing on
443    /// the dispatch path has to interrogate a boxed recognizer to find out
444    /// what kind of competitor it is. The flag is what a reader — and a
445    /// third-party recognizer author — reads to know which side of that
446    /// negotiation a type belongs on.
447    fn competes_for_sequence(&self) -> bool {
448        false
449    }
450
451    /// Whether this recognizer wants every live contact rather than just the
452    /// one its arena was created for. [`TouchPinchRecognizer`] says `true`;
453    /// every single-contact recognizer says `false`.
454    ///
455    /// Also a declaration rather than a hook, and for a structural reason: a
456    /// [`GestureArena`] serves exactly one contact, so a recognizer that needs
457    /// two cannot live in one at all. The tree owns its pinch directly and
458    /// feeds it every contact (`widget_tree::pan_arbiter::feed_pinch`); the
459    /// flag is how such a type declares that it must be owned that way.
460    fn wants_all_pointers(&self) -> bool {
461        false
462    }
463}
464
465pub(crate) fn distance(a: Point, b: Point) -> f32 {
466    let dx = a.x - b.x;
467    let dy = a.y - b.y;
468    (dx * dx + dy * dy).sqrt()
469}
470
471#[cfg(test)]
472pub(crate) mod test_helpers {
473    use super::{EventTime, Modifiers, Point, PointerButton, PointerInfo, RawPointerEvent};
474    use crate::pointer::CancelReason;
475
476    /// The pointer every helper below attributes its event to unless told
477    /// otherwise: the mouse, at the epoch.
478    pub fn mouse_pointer() -> PointerInfo {
479        PointerInfo::mouse(EventTime::ZERO)
480    }
481
482    pub fn down(pos: Point) -> RawPointerEvent {
483        down_btn(pos, PointerButton::Primary)
484    }
485
486    pub fn down_btn(pos: Point, button: PointerButton) -> RawPointerEvent {
487        down_full(pos, button, Modifiers::NONE)
488    }
489
490    pub fn down_full(pos: Point, button: PointerButton, modifiers: Modifiers) -> RawPointerEvent {
491        RawPointerEvent::Down {
492            position: pos,
493            button,
494            modifiers,
495            pointer: mouse_pointer(),
496            time: EventTime::ZERO,
497        }
498    }
499
500    pub fn up(pos: Point) -> RawPointerEvent {
501        up_btn(pos, PointerButton::Primary)
502    }
503
504    pub fn up_btn(pos: Point, button: PointerButton) -> RawPointerEvent {
505        up_full(pos, button, Modifiers::NONE)
506    }
507
508    pub fn up_full(pos: Point, button: PointerButton, modifiers: Modifiers) -> RawPointerEvent {
509        RawPointerEvent::Up {
510            position: pos,
511            button,
512            modifiers,
513            pointer: mouse_pointer(),
514            time: EventTime::ZERO,
515        }
516    }
517
518    pub fn move_to(pos: Point) -> RawPointerEvent {
519        RawPointerEvent::Move {
520            position: pos,
521            pointer: mouse_pointer(),
522            time: EventTime::ZERO,
523        }
524    }
525
526    pub fn cancel_at(pos: Point) -> RawPointerEvent {
527        RawPointerEvent::Cancel {
528            position: pos,
529            pointer: mouse_pointer(),
530            reason: CancelReason::Platform,
531            time: EventTime::ZERO,
532        }
533    }
534
535    /// The same event attributed to `pointer` and stamped at `time`.
536    pub fn retimed(
537        event: RawPointerEvent,
538        pointer: PointerInfo,
539        time: EventTime,
540    ) -> RawPointerEvent {
541        match event {
542            RawPointerEvent::Down {
543                position,
544                button,
545                modifiers,
546                ..
547            } => RawPointerEvent::Down {
548                position,
549                button,
550                modifiers,
551                pointer,
552                time,
553            },
554            RawPointerEvent::Move { position, .. } => RawPointerEvent::Move {
555                position,
556                pointer,
557                time,
558            },
559            RawPointerEvent::Up {
560                position,
561                button,
562                modifiers,
563                ..
564            } => RawPointerEvent::Up {
565                position,
566                button,
567                modifiers,
568                pointer,
569                time,
570            },
571            RawPointerEvent::Cancel {
572                position, reason, ..
573            } => RawPointerEvent::Cancel {
574                position,
575                pointer,
576                reason,
577                time,
578            },
579        }
580    }
581}
582
583#[cfg(test)]
584mod source_scan_tests {
585    /// Every `.rs` file the gesture layer is made of.
586    fn gesture_sources() -> Vec<(String, String)> {
587        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/gesture");
588        let mut out = vec![(
589            "gesture.rs".to_string(),
590            std::fs::read_to_string(
591                std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/gesture.rs"),
592            )
593            .expect("gesture.rs is readable"),
594        )];
595        for entry in std::fs::read_dir(&root).expect("src/gesture is readable") {
596            let path = entry.expect("readable dir entry").path();
597            if path.extension().and_then(|e| e.to_str()) == Some("rs") {
598                let name = path
599                    .file_name()
600                    .and_then(|n| n.to_str())
601                    .unwrap_or("?")
602                    .to_string();
603                out.push((name, std::fs::read_to_string(&path).expect("readable")));
604            }
605        }
606        out
607    }
608
609    /// Everything outside a `#[cfg(test)] mod` / `#[cfg(test)] impl` block:
610    /// the code that actually ships.
611    ///
612    /// Byte-indexed throughout — these files are full of em-dashes, and the
613    /// only positions it ever cuts at (`#[cfg(test)]`, `{`, `}`) are ASCII, so
614    /// every slice lands on a character boundary.
615    fn production_only(source: &str) -> String {
616        const MARKER: &[u8] = b"#[cfg(test)]";
617        let bytes = source.as_bytes();
618        let mut out = String::with_capacity(source.len());
619        let mut kept_from = 0usize;
620        let mut i = 0usize;
621        while i < bytes.len() {
622            if !bytes[i..].starts_with(MARKER) {
623                i += 1;
624                continue;
625            }
626            // Only `mod` and `impl` introduce a whole block of test-only code.
627            // A `#[cfg(test)]` on a field or a single fn is left in place — and
628            // must therefore still be free of wall-clock reads to matter.
629            let after = i + MARKER.len();
630            let head_end = bytes[after..]
631                .iter()
632                .position(|c| *c == b'{')
633                .map(|off| after + off);
634            let Some(head_end) = head_end else { break };
635            let head = source[after..head_end].trim_start();
636            if !(head.starts_with("mod ") || head.starts_with("impl ")) {
637                i = after;
638                continue;
639            }
640            // Brace-match from the block's opening brace.
641            let mut depth = 0usize;
642            let mut j = head_end;
643            while j < bytes.len() {
644                match bytes[j] {
645                    b'{' => depth += 1,
646                    b'}' => {
647                        depth -= 1;
648                        if depth == 0 {
649                            j += 1;
650                            break;
651                        }
652                    }
653                    _ => {}
654                }
655                j += 1;
656            }
657            out.push_str(&source[kept_from..i]);
658            kept_from = j;
659            i = j;
660        }
661        out.push_str(&source[kept_from..]);
662        out
663    }
664
665    /// A recognizer that reads the wall clock cannot be driven by a simulated
666    /// one, and a test that cannot advance the clock cannot test a long press
667    /// or a double-tap window without sleeping. `Instant::now()` is therefore
668    /// banned from the gesture layer outside its own test blocks — time
669    /// arrives through `RecognizerContext::now`.
670    /// Everything before the first `//` on each line: code, not prose. Keeps
671    /// the scan from tripping over its own documentation, which names
672    /// `Instant::now()` to explain why it is banned.
673    fn code_only(source: &str) -> String {
674        source
675            .lines()
676            .map(|line| match line.find("//") {
677                Some(idx) => &line[..idx],
678                None => line,
679            })
680            .collect::<Vec<_>>()
681            .join("\n")
682    }
683
684    #[test]
685    fn no_wall_clock_in_gestures() {
686        for (name, source) in gesture_sources() {
687            let production = code_only(&production_only(&source));
688            assert!(
689                !production.contains("Instant::now()"),
690                "{name} reads the wall clock outside its test blocks; gesture \
691                 recognizers must take their time from `RecognizerContext::now`"
692            );
693        }
694    }
695
696    /// The stripper must not eat the whole file — otherwise the scan above
697    /// passes vacuously.
698    #[test]
699    fn the_scan_keeps_the_production_half_of_each_file() {
700        for (name, source) in gesture_sources() {
701            let production = production_only(&source);
702            assert!(
703                production.contains("// SPDX-License-Identifier"),
704                "{name}: the test-block stripper ate the file header"
705            );
706            assert!(
707                code_only(&production).contains("use "),
708                "{name}: the comment stripper ate the code"
709            );
710            if source.contains("pub struct") {
711                assert!(
712                    production.contains("pub struct"),
713                    "{name}: the test-block stripper ate the production types"
714                );
715            }
716        }
717    }
718
719    /// The scan is only meaningful if it actually found the files.
720    #[test]
721    fn the_source_scan_sees_every_recognizer() {
722        let names: Vec<String> = gesture_sources().into_iter().map(|(n, _)| n).collect();
723        for expected in [
724            "gesture.rs",
725            "arena.rs",
726            "arena_set.rs",
727            "config.rs",
728            "drag.rs",
729            "long_press.rs",
730            "multi_tap.rs",
731            "palm.rs",
732            "pan.rs",
733            "pinch.rs",
734            "swipe.rs",
735            "tap.rs",
736        ] {
737            assert!(
738                names.iter().any(|n| n == expected),
739                "the wall-clock scan missed {expected}; it saw {names:?}"
740            );
741        }
742    }
743}