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}