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, making them trivially unit-testable.
10//!
11//! The [`GestureArena`] arbitrates when multiple recognizers compete on the
12//! same event stream: all are fed in parallel, and when one recognizes, the
13//! rest are reset (except cooperative peers — see
14//! [`GestureRecognizer::resets_on_peer_recognition`]).
15//!
16//! **Click-style recognizers carry button + modifiers.** [`TapRecognizer`],
17//! [`DoubleTapRecognizer`], [`TripleTapRecognizer`], and
18//! [`LongPressRecognizer`] all default to `ButtonMask::PRIMARY` —
19//! left-click only — and emit [`TapEvent`]s carrying position, the
20//! finalising button, and modifier state. Multi-tap recognizers
21//! require button-match across the whole sequence. Widen the accepted
22//! set with `.accept_buttons(...)` / `.accept_any_button()`.
23
24use std::time::Instant;
25
26use teksilo_canvas::{Point, Vec2};
27
28use crate::event::{Modifiers, PointerButton};
29
30mod arena;
31mod drag;
32mod long_press;
33mod multi_tap;
34mod swipe;
35mod tap;
36
37pub use arena::GestureArena;
38pub use drag::DragRecognizer;
39pub use long_press::LongPressRecognizer;
40pub use multi_tap::{DoubleTapRecognizer, TripleTapRecognizer};
41pub use swipe::SwipeRecognizer;
42pub use tap::TapRecognizer;
43
44/// Information about a recognized click-style gesture, passed to the
45/// four tap-family handlers (`on_tap`, `on_double_tap`, `on_triple_tap`,
46/// `on_long_press`).
47///
48/// The struct is `#[non_exhaustive]` so future fields (timestamp, click
49/// count for a hypothetical `on_n_tap`, pressure for stylus events) can
50/// land without breaking existing match patterns.
51#[derive(Debug, Clone, Copy)]
52#[non_exhaustive]
53pub struct TapEvent {
54    /// Pointer position in widget-local coords, captured at the
55    /// finalising event (the `Up` of the last tap for tap / double-tap /
56    /// triple-tap; the held `Down` for long-press, since long-press
57    /// recognises on a `tick` before any `Up`).
58    pub position: Point,
59
60    /// Which button finalised the gesture. Multi-tap recognizers
61    /// require every tap in the sequence to use the same button —
62    /// mixed-button sequences fail rather than spuriously firing.
63    pub button: PointerButton,
64
65    /// Modifier keys held at the finalising event. Sourced from
66    /// `WidgetEvent::PointerUp { modifiers, .. }` (or `PointerDown` for
67    /// long-press).
68    pub modifiers: Modifiers,
69}
70
71impl TapEvent {
72    /// Construct a `TapEvent` directly. Useful for tests; widgets receive
73    /// `&TapEvent` from the recognizer pipeline and rarely need to build
74    /// one by hand.
75    pub fn new(position: Point, button: PointerButton, modifiers: Modifiers) -> Self {
76        Self {
77            position,
78            button,
79            modifiers,
80        }
81    }
82}
83
84/// Raw pointer events fed into gesture recognizers.
85#[derive(Debug, Clone, Copy)]
86pub enum RawPointerEvent {
87    Down {
88        position: Point,
89        button: PointerButton,
90        modifiers: Modifiers,
91    },
92    Move {
93        position: Point,
94    },
95    Up {
96        position: Point,
97        button: PointerButton,
98        modifiers: Modifiers,
99    },
100}
101
102/// Result of processing a raw event through a gesture recognizer.
103#[derive(Debug, Clone)]
104pub enum GestureResult {
105    /// Not enough data yet — keep feeding events.
106    Pending,
107    /// A gesture has been recognized.
108    Recognized(GestureEvent),
109    /// This event sequence cannot match the gesture — recognizer should be reset.
110    Failed,
111}
112
113/// A recognized gesture event.
114///
115/// The four click-style variants (`Tap` / `DoubleTap` / `TripleTap` /
116/// `LongPress`) carry a [`TapEvent`] payload — pointer position, the
117/// finalising mouse button, and the modifier state at that moment.
118#[derive(Debug, Clone, Copy)]
119pub enum GestureEvent {
120    Tap(TapEvent),
121    DoubleTap(TapEvent),
122    TripleTap(TapEvent),
123    LongPress(TapEvent),
124    DragStarted {
125        position: Point,
126        button: PointerButton,
127    },
128    DragMoved {
129        position: Point,
130        delta: Vec2,
131    },
132    DragEnded {
133        position: Point,
134    },
135    PinchStarted {
136        center: Point,
137    },
138    PinchChanged {
139        center: Point,
140        scale: f32,
141        rotation: f32,
142    },
143    PinchEnded,
144    Swipe {
145        direction: SwipeDirection,
146        velocity: f32,
147    },
148}
149
150/// Direction of a swipe gesture.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum SwipeDirection {
153    Left,
154    Right,
155    Up,
156    Down,
157}
158
159/// Phase of a drag gesture, as delivered to an `on_drag` handler.
160///
161/// This is the public API for drag handlers — the raw `GestureEvent::Drag*`
162/// variants are an implementation detail of the recognizer pipeline. A
163/// handler only ever receives `Started` once, followed by zero or more
164/// `Moved`, then exactly one `Ended`.
165#[derive(Debug, Clone, Copy)]
166pub enum DragPhase {
167    Started {
168        position: Point,
169        button: PointerButton,
170    },
171    Moved {
172        position: Point,
173        delta: Vec2,
174    },
175    Ended {
176        position: Point,
177    },
178}
179
180/// Phase of a pinch (or rotation) gesture, as delivered to an `on_pinch`
181/// handler. On desktop these are produced by OS trackpad gestures
182/// (`TouchpadMagnify` / `RotationGesture`); on touch they come from a
183/// dedicated recognizer.
184#[derive(Debug, Clone, Copy)]
185pub enum PinchPhase {
186    Started {
187        center: Point,
188    },
189    Changed {
190        center: Point,
191        scale: f32,
192        rotation: f32,
193    },
194    Ended,
195}
196
197/// Trait for gesture recognizers. Each is a composable state machine.
198pub trait GestureRecognizer {
199    /// Feed a raw pointer event and return the recognition result.
200    fn process(&mut self, event: &RawPointerEvent) -> GestureResult;
201
202    /// Reset the recognizer to its initial state.
203    fn reset(&mut self);
204
205    /// Priority for arbitration when multiple recognizers compete.
206    /// Higher priority wins.
207    fn priority(&self) -> u32;
208
209    /// Advance any time-driven state (e.g. long-press elapsed timer).
210    /// Default is a no-op — only recognizers that depend on wall-clock
211    /// time (like [`LongPressRecognizer`]) override this.
212    fn tick(&mut self, _now: Instant) -> GestureResult {
213        GestureResult::Pending
214    }
215
216    /// Earliest future `Instant` at which calling [`GestureRecognizer::tick`]
217    /// could transition the recognizer into `Recognized` or `Failed`.
218    /// Returns `None` when the recognizer is idle or not time-driven. Used
219    /// by the event loop to schedule a wake-up before a long-press fires.
220    fn next_deadline(&self) -> Option<Instant> {
221        None
222    }
223
224    /// Whether this recognizer should be reset when a peer wins arbitration
225    /// in the same `GestureArena::process` call. The default is `true` —
226    /// winner-take-all, the usual behaviour for mutually exclusive gestures
227    /// (tap vs drag, long-press vs tap). Multi-tap recognizers
228    /// (`DoubleTapRecognizer`, `TripleTapRecognizer`) override this to
229    /// `false` so a `DoubleTap` firing at click 2 does not wipe the
230    /// `TripleTapRecognizer`'s accumulated state before click 3 arrives.
231    fn resets_on_peer_recognition(&self) -> bool {
232        true
233    }
234}
235
236pub(crate) fn distance(a: Point, b: Point) -> f32 {
237    let dx = a.x - b.x;
238    let dy = a.y - b.y;
239    (dx * dx + dy * dy).sqrt()
240}
241
242#[cfg(test)]
243pub(crate) mod test_helpers {
244    use super::{Modifiers, Point, PointerButton, RawPointerEvent};
245
246    pub fn down(pos: Point) -> RawPointerEvent {
247        down_btn(pos, PointerButton::Primary)
248    }
249
250    pub fn down_btn(pos: Point, button: PointerButton) -> RawPointerEvent {
251        down_full(pos, button, Modifiers::NONE)
252    }
253
254    pub fn down_full(pos: Point, button: PointerButton, modifiers: Modifiers) -> RawPointerEvent {
255        RawPointerEvent::Down {
256            position: pos,
257            button,
258            modifiers,
259        }
260    }
261
262    pub fn up(pos: Point) -> RawPointerEvent {
263        up_btn(pos, PointerButton::Primary)
264    }
265
266    pub fn up_btn(pos: Point, button: PointerButton) -> RawPointerEvent {
267        up_full(pos, button, Modifiers::NONE)
268    }
269
270    pub fn up_full(pos: Point, button: PointerButton, modifiers: Modifiers) -> RawPointerEvent {
271        RawPointerEvent::Up {
272            position: pos,
273            button,
274            modifiers,
275        }
276    }
277
278    pub fn move_to(pos: Point) -> RawPointerEvent {
279        RawPointerEvent::Move { position: pos }
280    }
281}