teksilo_core/gesture/
swipe.rs1use std::time::Instant;
5
6use teksilo_canvas::Point;
7
8use super::{GestureEvent, GestureRecognizer, GestureResult, RawPointerEvent, SwipeDirection};
9
10#[derive(Debug)]
12pub struct SwipeRecognizer {
13 min_velocity: f32,
14 min_distance: f32,
15 max_cross_ratio: f32,
16 down_position: Option<Point>,
17 down_time: Option<Instant>,
18}
19
20impl SwipeRecognizer {
21 pub fn new() -> Self {
22 Self {
23 min_velocity: 200.0, min_distance: 30.0, max_cross_ratio: 0.5, down_position: None,
27 down_time: None,
28 }
29 }
30
31 pub fn min_velocity(mut self, v: f32) -> Self {
32 self.min_velocity = v;
33 self
34 }
35
36 pub fn min_distance(mut self, d: f32) -> Self {
37 self.min_distance = d;
38 self
39 }
40
41 pub fn process_at(&mut self, event: &RawPointerEvent, now: Instant) -> GestureResult {
43 match event {
44 RawPointerEvent::Down { position, .. } => {
45 self.down_position = Some(*position);
46 self.down_time = Some(now);
47 GestureResult::Pending
48 }
49 RawPointerEvent::Move { .. } => GestureResult::Pending,
50 RawPointerEvent::Up { position, .. } => {
51 let (Some(down), Some(time)) = (self.down_position, self.down_time) else {
52 return GestureResult::Failed;
53 };
54
55 let dx = position.x - down.x;
56 let dy = position.y - down.y;
57 let dist = (dx * dx + dy * dy).sqrt();
58 let elapsed = now.duration_since(time).as_secs_f32();
59
60 if dist < self.min_distance || elapsed <= 0.0 {
61 self.reset();
62 return GestureResult::Failed;
63 }
64
65 let velocity = dist / elapsed;
66 if velocity < self.min_velocity {
67 self.reset();
68 return GestureResult::Failed;
69 }
70
71 let abs_dx = dx.abs();
72 let abs_dy = dy.abs();
73
74 let (direction, cross_ratio) = if abs_dx >= abs_dy {
76 let dir = if dx > 0.0 {
77 SwipeDirection::Right
78 } else {
79 SwipeDirection::Left
80 };
81 (dir, abs_dy / abs_dx.max(0.001))
82 } else {
83 let dir = if dy > 0.0 {
84 SwipeDirection::Down
85 } else {
86 SwipeDirection::Up
87 };
88 (dir, abs_dx / abs_dy.max(0.001))
89 };
90
91 if cross_ratio > self.max_cross_ratio {
92 self.reset();
93 return GestureResult::Failed;
94 }
95
96 self.reset();
97 GestureResult::Recognized(GestureEvent::Swipe {
98 direction,
99 velocity,
100 })
101 }
102 }
103 }
104}
105
106impl Default for SwipeRecognizer {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl GestureRecognizer for SwipeRecognizer {
113 fn process(&mut self, event: &RawPointerEvent) -> GestureResult {
114 self.process_at(event, Instant::now())
115 }
116
117 fn reset(&mut self) {
118 self.down_position = None;
119 self.down_time = None;
120 }
121
122 fn priority(&self) -> u32 {
123 30 }
125}
126
127#[cfg(test)]
128mod tests {
129 use std::time::Duration;
130
131 use super::*;
132 use crate::gesture::test_helpers::*;
133
134 #[test]
135 fn swipe_right_recognized() {
136 let mut rec = SwipeRecognizer::new()
137 .min_velocity(100.0)
138 .min_distance(20.0);
139 let t0 = Instant::now();
140
141 rec.process_at(&down(Point::new(10.0, 50.0)), t0);
142 let result = rec.process_at(
143 &up(Point::new(200.0, 55.0)),
144 t0 + Duration::from_millis(100),
145 );
146 match result {
147 GestureResult::Recognized(GestureEvent::Swipe {
148 direction,
149 velocity,
150 }) => {
151 assert_eq!(direction, SwipeDirection::Right);
152 assert!(velocity > 100.0);
153 }
154 other => panic!("Expected Swipe, got {:?}", other),
155 }
156 }
157
158 #[test]
159 fn swipe_left_recognized() {
160 let mut rec = SwipeRecognizer::new()
161 .min_velocity(100.0)
162 .min_distance(20.0);
163 let t0 = Instant::now();
164
165 rec.process_at(&down(Point::new(200.0, 50.0)), t0);
166 let result = rec.process_at(&up(Point::new(10.0, 55.0)), t0 + Duration::from_millis(100));
167 assert!(matches!(
168 result,
169 GestureResult::Recognized(GestureEvent::Swipe {
170 direction: SwipeDirection::Left,
171 ..
172 })
173 ));
174 }
175
176 #[test]
177 fn swipe_fails_if_too_slow() {
178 let mut rec = SwipeRecognizer::new().min_velocity(500.0);
179 let t0 = Instant::now();
180
181 rec.process_at(&down(Point::new(10.0, 10.0)), t0);
182 let result = rec.process_at(
183 &up(Point::new(50.0, 10.0)),
184 t0 + Duration::from_secs(5), );
186 assert!(matches!(result, GestureResult::Failed));
187 }
188
189 #[test]
190 fn swipe_fails_if_diagonal() {
191 let mut rec = SwipeRecognizer::new()
192 .min_velocity(100.0)
193 .min_distance(20.0);
194 let t0 = Instant::now();
195
196 rec.process_at(&down(Point::new(10.0, 10.0)), t0);
197 let result = rec.process_at(
199 &up(Point::new(100.0, 100.0)),
200 t0 + Duration::from_millis(100),
201 );
202 assert!(matches!(result, GestureResult::Failed));
203 }
204}