1use crate::drag::PointerData;
4
5const RAD_TO_DEG: f32 = 180.0 / core::f32::consts::PI;
6const PINCH_EPSILON: f32 = 0.01;
7const ROTATION_EPSILON_DEG: f32 = 1.0;
8
9#[derive(Clone, Copy, Debug, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct GestureConfig {
13 pub tap_max_distance: f32,
15 pub tap_max_duration: f32,
17 pub swipe_min_distance: f32,
19 pub long_press_duration: f32,
21 pub double_tap_max_interval: f32,
23}
24
25impl Default for GestureConfig {
26 fn default() -> Self {
27 Self {
28 tap_max_distance: 8.0,
29 tap_max_duration: 0.25,
30 swipe_min_distance: 40.0,
31 long_press_duration: 0.5,
32 double_tap_max_interval: 0.3,
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum SwipeDirection {
41 Up,
43 Down,
45 Left,
47 Right,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub enum Gesture {
55 Tap {
57 position: [f32; 2],
59 },
60 DoubleTap {
62 position: [f32; 2],
64 },
65 LongPress {
67 position: [f32; 2],
69 duration: f32,
71 },
72 Swipe {
74 direction: SwipeDirection,
76 velocity: f32,
78 distance: f32,
80 },
81 Pinch {
83 scale: f32,
85 center: [f32; 2],
87 },
88 Rotation {
90 angle_delta: f32,
92 center: [f32; 2],
94 },
95}
96
97#[derive(Clone, Debug)]
103pub struct GestureRecognizer {
104 config: GestureConfig,
105 active: [Option<PointerTrack>; 2],
106 two_start_distance: f32,
107 two_start_angle: f32,
108 last_tap: Option<TapRecord>,
109}
110
111impl Default for GestureRecognizer {
112 fn default() -> Self {
113 Self::new(GestureConfig::default())
114 }
115}
116
117impl GestureRecognizer {
118 pub fn new(config: GestureConfig) -> Self {
120 Self {
121 config,
122 active: [None, None],
123 two_start_distance: 0.0,
124 two_start_angle: 0.0,
125 last_tap: None,
126 }
127 }
128
129 pub fn config(&self) -> GestureConfig {
131 self.config
132 }
133
134 pub fn on_pointer_down(&mut self, data: PointerData, time_seconds: f32) {
136 let time_seconds = time_seconds.max(0.0);
137 if let Some(index) = self.find_index(data.pointer_id) {
138 self.active[index] = Some(PointerTrack::new(data, time_seconds));
139 self.refresh_two_pointer_start();
140 return;
141 }
142
143 if let Some(slot) = self.active.iter_mut().find(|slot| slot.is_none()) {
144 *slot = Some(PointerTrack::new(data, time_seconds));
145 self.refresh_two_pointer_start();
146 }
147 }
148
149 pub fn on_pointer_move(&mut self, data: PointerData, time_seconds: f32) {
151 if let Some(index) = self.find_index(data.pointer_id)
152 && let Some(track) = &mut self.active[index]
153 {
154 track.last = data.position();
155 track.last_time = time_seconds.max(track.start_time);
156 }
157 }
158
159 pub fn on_pointer_up(&mut self, data: PointerData, time_seconds: f32) -> Option<Gesture> {
161 let index = self.find_index(data.pointer_id)?;
162 let mut released = self.active[index]?;
163 released.last = data.position();
164 released.last_time = time_seconds.max(released.start_time);
165
166 if self.active_count() == 2 {
167 let gesture = self.two_pointer_gesture(index, released);
168 self.active[index] = None;
169 self.refresh_two_pointer_start();
170 return gesture;
171 }
172
173 self.active[index] = None;
174 self.single_pointer_gesture(released)
175 }
176
177 fn single_pointer_gesture(&mut self, track: PointerTrack) -> Option<Gesture> {
178 let duration = (track.last_time - track.start_time).max(0.0);
179 let delta = [
180 track.last[0] - track.start[0],
181 track.last[1] - track.start[1],
182 ];
183 let distance = length(delta);
184
185 if duration >= self.config.long_press_duration && distance <= self.config.tap_max_distance {
186 return Some(Gesture::LongPress {
187 position: track.last,
188 duration,
189 });
190 }
191
192 if distance >= self.config.swipe_min_distance {
193 return Some(Gesture::Swipe {
194 direction: swipe_direction(delta),
195 velocity: if duration > 0.0 {
196 distance / duration
197 } else {
198 0.0
199 },
200 distance,
201 });
202 }
203
204 if duration <= self.config.tap_max_duration && distance <= self.config.tap_max_distance {
205 let position = track.last;
206 if let Some(last) = self.last_tap
207 && track.last_time - last.time <= self.config.double_tap_max_interval
208 && length([
209 position[0] - last.position[0],
210 position[1] - last.position[1],
211 ]) <= self.config.tap_max_distance
212 {
213 self.last_tap = None;
214 return Some(Gesture::DoubleTap { position });
215 }
216
217 self.last_tap = Some(TapRecord {
218 position,
219 time: track.last_time,
220 });
221 return Some(Gesture::Tap { position });
222 }
223
224 None
225 }
226
227 fn two_pointer_gesture(
228 &self,
229 released_index: usize,
230 released: PointerTrack,
231 ) -> Option<Gesture> {
232 let other = self.active.iter().enumerate().find_map(|(index, track)| {
233 if index != released_index {
234 *track
235 } else {
236 None
237 }
238 })?;
239
240 if self.two_start_distance <= f32::EPSILON {
241 return None;
242 }
243
244 let current_distance = distance(released.last, other.last);
245 let scale = current_distance / self.two_start_distance;
246 let current_angle = angle_between(released.last, other.last);
247 let angle_delta = normalize_degrees(current_angle - self.two_start_angle);
248 let center = midpoint(released.last, other.last);
249 let scale_delta = (scale - 1.0).abs();
250
251 if angle_delta.abs() >= ROTATION_EPSILON_DEG && angle_delta.abs() / 45.0 >= scale_delta {
252 Some(Gesture::Rotation {
253 angle_delta,
254 center,
255 })
256 } else if scale_delta >= PINCH_EPSILON {
257 Some(Gesture::Pinch { scale, center })
258 } else {
259 None
260 }
261 }
262
263 fn find_index(&self, pointer_id: u64) -> Option<usize> {
264 self.active.iter().position(|track| {
265 track
266 .map(|track| track.pointer_id == pointer_id)
267 .unwrap_or(false)
268 })
269 }
270
271 fn active_count(&self) -> usize {
272 self.active.iter().filter(|track| track.is_some()).count()
273 }
274
275 fn refresh_two_pointer_start(&mut self) {
276 if self.active_count() != 2 {
277 self.two_start_distance = 0.0;
278 self.two_start_angle = 0.0;
279 return;
280 }
281
282 let first = self.active.iter().flatten().next().unwrap().last;
283 let second = self.active.iter().flatten().nth(1).unwrap().last;
284 self.two_start_distance = distance(first, second);
285 self.two_start_angle = angle_between(first, second);
286 }
287}
288
289#[derive(Clone, Copy, Debug)]
290struct PointerTrack {
291 pointer_id: u64,
292 start: [f32; 2],
293 last: [f32; 2],
294 start_time: f32,
295 last_time: f32,
296}
297
298impl PointerTrack {
299 fn new(data: PointerData, time: f32) -> Self {
300 Self {
301 pointer_id: data.pointer_id,
302 start: data.position(),
303 last: data.position(),
304 start_time: time,
305 last_time: time,
306 }
307 }
308}
309
310#[derive(Clone, Copy, Debug)]
311struct TapRecord {
312 position: [f32; 2],
313 time: f32,
314}
315
316#[inline]
317fn distance(a: [f32; 2], b: [f32; 2]) -> f32 {
318 length([b[0] - a[0], b[1] - a[1]])
319}
320
321#[inline]
322fn length(vector: [f32; 2]) -> f32 {
323 libm::sqrtf(vector[0] * vector[0] + vector[1] * vector[1])
324}
325
326#[inline]
327fn angle_between(a: [f32; 2], b: [f32; 2]) -> f32 {
328 libm::atan2f(b[1] - a[1], b[0] - a[0]) * RAD_TO_DEG
329}
330
331#[inline]
332fn midpoint(a: [f32; 2], b: [f32; 2]) -> [f32; 2] {
333 [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5]
334}
335
336#[inline]
337fn normalize_degrees(mut angle: f32) -> f32 {
338 while angle > 180.0 {
339 angle -= 360.0;
340 }
341 while angle < -180.0 {
342 angle += 360.0;
343 }
344 angle
345}
346
347#[inline]
348fn swipe_direction(delta: [f32; 2]) -> SwipeDirection {
349 if delta[0].abs() >= delta[1].abs() {
350 if delta[0] >= 0.0 {
351 SwipeDirection::Right
352 } else {
353 SwipeDirection::Left
354 }
355 } else if delta[1] >= 0.0 {
356 SwipeDirection::Down
357 } else {
358 SwipeDirection::Up
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn recognizes_tap() {
368 let mut recognizer = GestureRecognizer::default();
369 recognizer.on_pointer_down(PointerData::new(10.0, 20.0, 1), 0.0);
370 let gesture = recognizer.on_pointer_up(PointerData::new(12.0, 20.0, 1), 0.1);
371 assert_eq!(
372 gesture,
373 Some(Gesture::Tap {
374 position: [12.0, 20.0]
375 })
376 );
377 }
378
379 #[test]
380 fn recognizes_double_tap() {
381 let mut recognizer = GestureRecognizer::default();
382 recognizer.on_pointer_down(PointerData::new(0.0, 0.0, 1), 0.0);
383 recognizer.on_pointer_up(PointerData::new(0.0, 0.0, 1), 0.1);
384 recognizer.on_pointer_down(PointerData::new(1.0, 1.0, 1), 0.2);
385 let gesture = recognizer.on_pointer_up(PointerData::new(1.0, 1.0, 1), 0.25);
386 assert_eq!(
387 gesture,
388 Some(Gesture::DoubleTap {
389 position: [1.0, 1.0]
390 })
391 );
392 }
393
394 #[test]
395 fn recognizes_long_press() {
396 let mut recognizer = GestureRecognizer::default();
397 recognizer.on_pointer_down(PointerData::new(0.0, 0.0, 1), 0.0);
398 let gesture = recognizer.on_pointer_up(PointerData::new(1.0, 1.0, 1), 0.7);
399 assert!(matches!(gesture, Some(Gesture::LongPress { .. })));
400 }
401
402 #[test]
403 fn recognizes_swipe() {
404 let mut recognizer = GestureRecognizer::default();
405 recognizer.on_pointer_down(PointerData::new(0.0, 0.0, 1), 0.0);
406 let gesture = recognizer.on_pointer_up(PointerData::new(80.0, 10.0, 1), 0.2);
407 assert!(matches!(
408 gesture,
409 Some(Gesture::Swipe {
410 direction: SwipeDirection::Right,
411 ..
412 })
413 ));
414 }
415
416 #[test]
417 fn recognizes_pinch() {
418 let mut recognizer = GestureRecognizer::default();
419 recognizer.on_pointer_down(PointerData::new(0.0, 0.0, 1), 0.0);
420 recognizer.on_pointer_down(PointerData::new(10.0, 0.0, 2), 0.0);
421 recognizer.on_pointer_move(PointerData::new(20.0, 0.0, 2), 0.1);
422 let gesture = recognizer.on_pointer_up(PointerData::new(0.0, 0.0, 1), 0.2);
423 assert_eq!(
424 gesture,
425 Some(Gesture::Pinch {
426 scale: 2.0,
427 center: [10.0, 0.0]
428 })
429 );
430 }
431
432 #[test]
433 fn recognizes_rotation() {
434 let mut recognizer = GestureRecognizer::default();
435 recognizer.on_pointer_down(PointerData::new(0.0, 0.0, 1), 0.0);
436 recognizer.on_pointer_down(PointerData::new(10.0, 0.0, 2), 0.0);
437 recognizer.on_pointer_move(PointerData::new(0.0, 10.0, 2), 0.1);
438 let gesture = recognizer.on_pointer_up(PointerData::new(0.0, 0.0, 1), 0.2);
439 assert!(matches!(
440 gesture,
441 Some(Gesture::Rotation {
442 angle_delta,
443 ..
444 }) if (angle_delta - 90.0).abs() < 0.01
445 ));
446 }
447}