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}