Skip to main content

presentar_core/
gesture.rs

1#![allow(
2    clippy::unwrap_used,
3    clippy::disallowed_methods,
4    clippy::many_single_char_names
5)]
6//! Gesture recognition from touch/pointer events.
7//!
8//! This module provides gesture recognizers that process raw touch and pointer
9//! events and emit high-level gesture events like pinch, rotate, pan, tap, etc.
10
11use crate::event::{Event, GestureState, PointerId, PointerType, TouchId};
12use crate::geometry::Point;
13use std::collections::HashMap;
14use std::time::{Duration, Instant};
15
16/// Configuration for gesture recognition.
17#[derive(Debug, Clone)]
18pub struct GestureConfig {
19    /// Minimum distance to start a pan gesture (in pixels).
20    pub pan_threshold: f32,
21    /// Maximum time for a tap (in milliseconds).
22    pub tap_timeout_ms: u64,
23    /// Maximum movement for a tap to still be valid.
24    pub tap_slop: f32,
25    /// Time required for a long press (in milliseconds).
26    pub long_press_ms: u64,
27    /// Maximum time between taps for a double tap.
28    pub double_tap_ms: u64,
29    /// Minimum scale change to start pinch.
30    pub pinch_threshold: f32,
31    /// Minimum rotation to start rotate gesture (radians).
32    pub rotate_threshold: f32,
33}
34
35impl Default for GestureConfig {
36    fn default() -> Self {
37        Self {
38            pan_threshold: 10.0,
39            tap_timeout_ms: 300,
40            tap_slop: 10.0,
41            long_press_ms: 500,
42            double_tap_ms: 300,
43            pinch_threshold: 0.05,
44            rotate_threshold: 0.05,
45        }
46    }
47}
48
49/// Active touch point being tracked.
50#[derive(Debug, Clone)]
51pub struct TouchPoint {
52    /// Touch ID.
53    pub id: TouchId,
54    /// Starting position.
55    pub start_position: Point,
56    /// Current position.
57    pub current_position: Point,
58    /// Previous position.
59    pub previous_position: Point,
60    /// When the touch started.
61    pub start_time: Instant,
62    /// Pressure (0.0-1.0).
63    pub pressure: f32,
64}
65
66impl TouchPoint {
67    /// Create a new touch point.
68    pub fn new(id: TouchId, position: Point, pressure: f32) -> Self {
69        let now = Instant::now();
70        Self {
71            id,
72            start_position: position,
73            current_position: position,
74            previous_position: position,
75            start_time: now,
76            pressure,
77        }
78    }
79
80    /// Update the touch point position.
81    pub fn update(&mut self, position: Point, pressure: f32) {
82        self.previous_position = self.current_position;
83        self.current_position = position;
84        self.pressure = pressure;
85    }
86
87    /// Get the total distance moved from start.
88    pub fn total_distance(&self) -> f32 {
89        self.start_position.distance(&self.current_position)
90    }
91
92    /// Get the delta from previous position.
93    pub fn delta(&self) -> Point {
94        self.current_position - self.previous_position
95    }
96
97    /// Get duration since touch started.
98    pub fn duration(&self) -> Duration {
99        self.start_time.elapsed()
100    }
101}
102
103/// State of a recognized gesture.
104#[derive(Debug, Clone, Default)]
105pub enum RecognizedGesture {
106    /// No gesture recognized yet.
107    #[default]
108    None,
109    /// Tap gesture (with tap count).
110    Tap { position: Point, count: u8 },
111    /// Long press gesture.
112    LongPress { position: Point },
113    /// Pan/drag gesture.
114    Pan {
115        delta: Point,
116        velocity: Point,
117        state: GestureState,
118    },
119    /// Pinch gesture.
120    Pinch {
121        scale: f32,
122        center: Point,
123        state: GestureState,
124    },
125    /// Rotate gesture.
126    Rotate {
127        angle: f32,
128        center: Point,
129        state: GestureState,
130    },
131}
132
133/// Tracks last tap for double-tap detection.
134#[derive(Debug, Clone)]
135struct LastTap {
136    position: Point,
137    time: Instant,
138    count: u8,
139}
140
141/// Multi-touch gesture recognizer.
142#[derive(Debug)]
143pub struct GestureRecognizer {
144    /// Configuration.
145    config: GestureConfig,
146    /// Active touch points.
147    touches: HashMap<TouchId, TouchPoint>,
148    /// Currently recognized gesture.
149    current_gesture: RecognizedGesture,
150    /// Initial distance between two fingers (for pinch).
151    initial_pinch_distance: Option<f32>,
152    /// Initial angle between two fingers (for rotate).
153    initial_rotation_angle: Option<f32>,
154    /// Last tap info for double-tap detection.
155    last_tap: Option<LastTap>,
156    /// Velocity tracking for pan.
157    velocity_samples: Vec<(Point, Instant)>,
158}
159
160impl GestureRecognizer {
161    /// Create a new gesture recognizer with default config.
162    pub fn new() -> Self {
163        Self::with_config(GestureConfig::default())
164    }
165
166    /// Create a new gesture recognizer with custom config.
167    pub fn with_config(config: GestureConfig) -> Self {
168        Self {
169            config,
170            touches: HashMap::new(),
171            current_gesture: RecognizedGesture::None,
172            initial_pinch_distance: None,
173            initial_rotation_angle: None,
174            last_tap: None,
175            velocity_samples: Vec::new(),
176        }
177    }
178
179    /// Get the current gesture configuration.
180    pub fn config(&self) -> &GestureConfig {
181        &self.config
182    }
183
184    /// Get the number of active touches.
185    pub fn touch_count(&self) -> usize {
186        self.touches.len()
187    }
188
189    /// Get an active touch by ID.
190    pub fn touch(&self, id: TouchId) -> Option<&TouchPoint> {
191        self.touches.get(&id)
192    }
193
194    /// Process an event and return any recognized gesture.
195    pub fn process(&mut self, event: &Event) -> Option<Event> {
196        match event {
197            Event::TouchStart {
198                id,
199                position,
200                pressure,
201            } => self.on_touch_start(*id, *position, *pressure),
202            Event::TouchMove {
203                id,
204                position,
205                pressure,
206            } => self.on_touch_move(*id, *position, *pressure),
207            Event::TouchEnd { id, position } => self.on_touch_end(*id, *position),
208            Event::TouchCancel { id } => self.on_touch_cancel(*id),
209            _ => None,
210        }
211    }
212
213    fn on_touch_start(&mut self, id: TouchId, position: Point, pressure: f32) -> Option<Event> {
214        let touch = TouchPoint::new(id, position, pressure);
215        self.touches.insert(id, touch);
216
217        // Reset gesture state when new touch starts
218        if self.touches.len() == 2 {
219            self.init_two_finger_tracking();
220        }
221
222        None
223    }
224
225    fn on_touch_move(&mut self, id: TouchId, position: Point, pressure: f32) -> Option<Event> {
226        {
227            let touch = self.touches.get_mut(&id)?;
228            touch.update(position, pressure);
229        }
230
231        match self.touches.len() {
232            1 => self.recognize_single_finger_move(),
233            2 => self.recognize_two_finger_move(),
234            _ => None,
235        }
236    }
237
238    fn on_touch_end(&mut self, id: TouchId, _position: Point) -> Option<Event> {
239        let touch = self.touches.remove(&id)?;
240
241        // Check for tap if this was the last touch
242        if self.touches.is_empty() {
243            let duration = touch.duration();
244            let distance = touch.total_distance();
245
246            if duration.as_millis() < u128::from(self.config.tap_timeout_ms)
247                && distance < self.config.tap_slop
248            {
249                return self.handle_tap(touch.start_position);
250            }
251
252            // End any active gesture
253            self.end_active_gesture()
254        } else if self.touches.len() == 1 {
255            // Went from 2 to 1 finger - end pinch/rotate
256            self.end_two_finger_gesture()
257        } else {
258            None
259        }
260    }
261
262    fn on_touch_cancel(&mut self, id: TouchId) -> Option<Event> {
263        self.touches.remove(&id);
264
265        if self.touches.is_empty() {
266            self.end_active_gesture()
267        } else {
268            None
269        }
270    }
271
272    fn init_two_finger_tracking(&mut self) {
273        let (dist, angle) = if let Some((t1, t2)) = self.get_two_touches() {
274            let dist = t1.current_position.distance(&t2.current_position);
275            let angle = (t2.current_position.y - t1.current_position.y)
276                .atan2(t2.current_position.x - t1.current_position.x);
277            (Some(dist), Some(angle))
278        } else {
279            (None, None)
280        };
281        self.initial_pinch_distance = dist;
282        self.initial_rotation_angle = angle;
283    }
284
285    fn recognize_single_finger_move(&mut self) -> Option<Event> {
286        let touch = self.touches.values().next()?;
287        let distance = touch.total_distance();
288
289        if distance >= self.config.pan_threshold {
290            let delta = touch.delta();
291            let velocity = self.calculate_velocity();
292
293            let state = match &self.current_gesture {
294                RecognizedGesture::Pan { .. } => GestureState::Changed,
295                _ => GestureState::Started,
296            };
297
298            self.current_gesture = RecognizedGesture::Pan {
299                delta,
300                velocity,
301                state,
302            };
303
304            Some(Event::GesturePan {
305                delta,
306                velocity,
307                state,
308            })
309        } else {
310            None
311        }
312    }
313
314    fn recognize_two_finger_move(&mut self) -> Option<Event> {
315        let (t1, t2) = self.get_two_touches()?;
316
317        let current_distance = t1.current_position.distance(&t2.current_position);
318        let initial_distance = self.initial_pinch_distance?;
319        let scale = current_distance / initial_distance;
320
321        let current_angle = self.angle_between(&t1.current_position, &t2.current_position);
322        let initial_angle = self.initial_rotation_angle?;
323        let angle_delta = current_angle - initial_angle;
324
325        let center = Point::new(
326            (t1.current_position.x + t2.current_position.x) / 2.0,
327            (t1.current_position.y + t2.current_position.y) / 2.0,
328        );
329
330        // Determine if this is more pinch or rotate
331        let scale_change = (scale - 1.0).abs();
332        let angle_change = angle_delta.abs();
333
334        if scale_change > self.config.pinch_threshold || angle_change > self.config.rotate_threshold
335        {
336            // Prefer the larger change
337            if scale_change > angle_change {
338                let state = match &self.current_gesture {
339                    RecognizedGesture::Pinch { .. } => GestureState::Changed,
340                    _ => GestureState::Started,
341                };
342
343                self.current_gesture = RecognizedGesture::Pinch {
344                    scale,
345                    center,
346                    state,
347                };
348
349                Some(Event::GesturePinch {
350                    scale,
351                    center,
352                    state,
353                })
354            } else {
355                let state = match &self.current_gesture {
356                    RecognizedGesture::Rotate { .. } => GestureState::Changed,
357                    _ => GestureState::Started,
358                };
359
360                self.current_gesture = RecognizedGesture::Rotate {
361                    angle: angle_delta,
362                    center,
363                    state,
364                };
365
366                Some(Event::GestureRotate {
367                    angle: angle_delta,
368                    center,
369                    state,
370                })
371            }
372        } else {
373            None
374        }
375    }
376
377    fn handle_tap(&mut self, position: Point) -> Option<Event> {
378        let now = Instant::now();
379
380        let count = if let Some(last) = &self.last_tap {
381            if now.duration_since(last.time).as_millis() < u128::from(self.config.double_tap_ms)
382                && position.distance(&last.position) < self.config.tap_slop
383            {
384                last.count + 1
385            } else {
386                1
387            }
388        } else {
389            1
390        };
391
392        self.last_tap = Some(LastTap {
393            position,
394            time: now,
395            count,
396        });
397
398        Some(Event::GestureTap { position, count })
399    }
400
401    fn end_active_gesture(&mut self) -> Option<Event> {
402        let result = match &self.current_gesture {
403            RecognizedGesture::Pan {
404                delta, velocity, ..
405            } => Some(Event::GesturePan {
406                delta: *delta,
407                velocity: *velocity,
408                state: GestureState::Ended,
409            }),
410            _ => None,
411        };
412
413        self.current_gesture = RecognizedGesture::None;
414        self.velocity_samples.clear();
415        result
416    }
417
418    fn end_two_finger_gesture(&mut self) -> Option<Event> {
419        let result = match &self.current_gesture {
420            RecognizedGesture::Pinch { scale, center, .. } => Some(Event::GesturePinch {
421                scale: *scale,
422                center: *center,
423                state: GestureState::Ended,
424            }),
425            RecognizedGesture::Rotate { angle, center, .. } => Some(Event::GestureRotate {
426                angle: *angle,
427                center: *center,
428                state: GestureState::Ended,
429            }),
430            _ => None,
431        };
432
433        self.current_gesture = RecognizedGesture::None;
434        self.initial_pinch_distance = None;
435        self.initial_rotation_angle = None;
436        result
437    }
438
439    fn get_two_touches(&self) -> Option<(&TouchPoint, &TouchPoint)> {
440        let mut iter = self.touches.values();
441        let t1 = iter.next()?;
442        let t2 = iter.next()?;
443        Some((t1, t2))
444    }
445
446    fn angle_between(&self, p1: &Point, p2: &Point) -> f32 {
447        (p2.y - p1.y).atan2(p2.x - p1.x)
448    }
449
450    fn calculate_velocity(&mut self) -> Point {
451        let now = Instant::now();
452
453        // Keep only recent samples (last 100ms)
454        self.velocity_samples
455            .retain(|(_, time)| now.duration_since(*time).as_millis() < 100);
456
457        if let Some(touch) = self.touches.values().next() {
458            self.velocity_samples.push((touch.current_position, now));
459        }
460
461        if self.velocity_samples.len() < 2 {
462            return Point::ORIGIN;
463        }
464
465        let (first_pos, first_time) = self.velocity_samples.first().expect("checked len >= 2");
466        let (last_pos, last_time) = self.velocity_samples.last().expect("checked len >= 2");
467
468        let dt = last_time.duration_since(*first_time).as_secs_f32();
469        if dt < 0.001 {
470            return Point::ORIGIN;
471        }
472
473        Point::new(
474            (last_pos.x - first_pos.x) / dt,
475            (last_pos.y - first_pos.y) / dt,
476        )
477    }
478
479    /// Check if a long press has occurred.
480    /// Call this periodically (e.g., from a timer) to detect long press.
481    pub fn check_long_press(&mut self) -> Option<Event> {
482        if self.touches.len() != 1 {
483            return None;
484        }
485
486        let touch = self.touches.values().next()?;
487
488        if touch.duration().as_millis() >= u128::from(self.config.long_press_ms)
489            && touch.total_distance() < self.config.tap_slop
490            && matches!(self.current_gesture, RecognizedGesture::None)
491        {
492            self.current_gesture = RecognizedGesture::LongPress {
493                position: touch.start_position,
494            };
495
496            Some(Event::GestureLongPress {
497                position: touch.start_position,
498            })
499        } else {
500            None
501        }
502    }
503
504    /// Reset the recognizer state.
505    pub fn reset(&mut self) {
506        self.touches.clear();
507        self.current_gesture = RecognizedGesture::None;
508        self.initial_pinch_distance = None;
509        self.initial_rotation_angle = None;
510        self.velocity_samples.clear();
511    }
512}
513
514impl Default for GestureRecognizer {
515    fn default() -> Self {
516        Self::new()
517    }
518}
519
520/// Pointer gesture recognizer that unifies mouse, touch, and pen input.
521#[derive(Debug)]
522pub struct PointerGestureRecognizer {
523    /// Active pointers.
524    pointers: HashMap<PointerId, PointerInfo>,
525    /// Configuration.
526    config: GestureConfig,
527    /// Primary pointer ID.
528    primary_pointer: Option<PointerId>,
529}
530
531/// Information about an active pointer.
532#[derive(Debug, Clone)]
533pub struct PointerInfo {
534    /// Pointer ID.
535    pub id: PointerId,
536    /// Pointer type.
537    pub pointer_type: PointerType,
538    /// Starting position.
539    pub start_position: Point,
540    /// Current position.
541    pub current_position: Point,
542    /// Start time.
543    pub start_time: Instant,
544    /// Is primary pointer.
545    pub is_primary: bool,
546    /// Pressure (0.0-1.0).
547    pub pressure: f32,
548}
549
550impl PointerGestureRecognizer {
551    /// Create a new pointer gesture recognizer.
552    pub fn new() -> Self {
553        Self::with_config(GestureConfig::default())
554    }
555
556    /// Create with custom config.
557    pub fn with_config(config: GestureConfig) -> Self {
558        Self {
559            pointers: HashMap::new(),
560            config,
561            primary_pointer: None,
562        }
563    }
564
565    /// Get the gesture configuration.
566    pub fn config(&self) -> &GestureConfig {
567        &self.config
568    }
569
570    /// Get the number of active pointers.
571    pub fn pointer_count(&self) -> usize {
572        self.pointers.len()
573    }
574
575    /// Get the primary pointer.
576    pub fn primary(&self) -> Option<&PointerInfo> {
577        self.primary_pointer.and_then(|id| self.pointers.get(&id))
578    }
579
580    /// Process a pointer event.
581    pub fn process(&mut self, event: &Event) -> Option<Event> {
582        match event {
583            Event::PointerDown {
584                pointer_id,
585                pointer_type,
586                position,
587                pressure,
588                is_primary,
589                ..
590            } => {
591                let info = PointerInfo {
592                    id: *pointer_id,
593                    pointer_type: *pointer_type,
594                    start_position: *position,
595                    current_position: *position,
596                    start_time: Instant::now(),
597                    is_primary: *is_primary,
598                    pressure: *pressure,
599                };
600
601                if *is_primary || self.primary_pointer.is_none() {
602                    self.primary_pointer = Some(*pointer_id);
603                }
604
605                self.pointers.insert(*pointer_id, info);
606                None
607            }
608            Event::PointerMove {
609                pointer_id,
610                position,
611                pressure,
612                ..
613            } => {
614                if let Some(info) = self.pointers.get_mut(pointer_id) {
615                    info.current_position = *position;
616                    info.pressure = *pressure;
617                }
618                None
619            }
620            Event::PointerUp { pointer_id, .. } | Event::PointerCancel { pointer_id } => {
621                self.pointers.remove(pointer_id);
622                if self.primary_pointer == Some(*pointer_id) {
623                    self.primary_pointer = self.pointers.keys().next().copied();
624                }
625                None
626            }
627            _ => None,
628        }
629    }
630
631    /// Reset the recognizer.
632    pub fn reset(&mut self) {
633        self.pointers.clear();
634        self.primary_pointer = None;
635    }
636}
637
638impl Default for PointerGestureRecognizer {
639    fn default() -> Self {
640        Self::new()
641    }
642}
643
644#[cfg(test)]
645mod tests {
646    use super::*;
647
648    // GestureConfig tests
649    #[test]
650    fn test_gesture_config_default() {
651        let config = GestureConfig::default();
652        assert_eq!(config.pan_threshold, 10.0);
653        assert_eq!(config.tap_timeout_ms, 300);
654        assert_eq!(config.tap_slop, 10.0);
655        assert_eq!(config.long_press_ms, 500);
656        assert_eq!(config.double_tap_ms, 300);
657        assert!((config.pinch_threshold - 0.05).abs() < 0.001);
658        assert!((config.rotate_threshold - 0.05).abs() < 0.001);
659    }
660
661    #[test]
662    fn test_gesture_config_custom() {
663        let config = GestureConfig {
664            pan_threshold: 20.0,
665            tap_timeout_ms: 200,
666            tap_slop: 15.0,
667            long_press_ms: 800,
668            double_tap_ms: 400,
669            pinch_threshold: 0.1,
670            rotate_threshold: 0.1,
671        };
672        assert_eq!(config.pan_threshold, 20.0);
673        assert_eq!(config.long_press_ms, 800);
674    }
675
676    // TouchPoint tests
677    #[test]
678    fn test_touch_point_new() {
679        let point = TouchPoint::new(TouchId::new(1), Point::new(100.0, 200.0), 0.5);
680        assert_eq!(point.id, TouchId(1));
681        assert_eq!(point.start_position, Point::new(100.0, 200.0));
682        assert_eq!(point.current_position, Point::new(100.0, 200.0));
683        assert_eq!(point.pressure, 0.5);
684    }
685
686    #[test]
687    fn test_touch_point_update() {
688        let mut point = TouchPoint::new(TouchId::new(1), Point::new(100.0, 200.0), 0.5);
689        point.update(Point::new(150.0, 250.0), 0.8);
690
691        assert_eq!(point.current_position, Point::new(150.0, 250.0));
692        assert_eq!(point.previous_position, Point::new(100.0, 200.0));
693        assert_eq!(point.pressure, 0.8);
694    }
695
696    #[test]
697    fn test_touch_point_total_distance() {
698        let mut point = TouchPoint::new(TouchId::new(1), Point::new(0.0, 0.0), 0.5);
699        point.update(Point::new(3.0, 4.0), 0.5);
700
701        assert!((point.total_distance() - 5.0).abs() < 0.001);
702    }
703
704    #[test]
705    fn test_touch_point_delta() {
706        let mut point = TouchPoint::new(TouchId::new(1), Point::new(0.0, 0.0), 0.5);
707        point.update(Point::new(10.0, 20.0), 0.5);
708        point.update(Point::new(15.0, 25.0), 0.5);
709
710        let delta = point.delta();
711        assert_eq!(delta.x, 5.0);
712        assert_eq!(delta.y, 5.0);
713    }
714
715    // GestureRecognizer tests
716    #[test]
717    fn test_recognizer_new() {
718        let recognizer = GestureRecognizer::new();
719        assert_eq!(recognizer.touch_count(), 0);
720    }
721
722    #[test]
723    fn test_recognizer_with_config() {
724        let config = GestureConfig {
725            pan_threshold: 20.0,
726            ..Default::default()
727        };
728        let recognizer = GestureRecognizer::with_config(config);
729        assert_eq!(recognizer.config().pan_threshold, 20.0);
730    }
731
732    #[test]
733    fn test_recognizer_touch_start() {
734        let mut recognizer = GestureRecognizer::new();
735
736        let event = Event::TouchStart {
737            id: TouchId::new(1),
738            position: Point::new(100.0, 200.0),
739            pressure: 0.5,
740        };
741
742        recognizer.process(&event);
743        assert_eq!(recognizer.touch_count(), 1);
744
745        let touch = recognizer.touch(TouchId::new(1)).unwrap();
746        assert_eq!(touch.start_position, Point::new(100.0, 200.0));
747    }
748
749    #[test]
750    fn test_recognizer_touch_move() {
751        let mut recognizer = GestureRecognizer::new();
752
753        // Start touch
754        recognizer.process(&Event::TouchStart {
755            id: TouchId::new(1),
756            position: Point::new(100.0, 200.0),
757            pressure: 0.5,
758        });
759
760        // Move touch
761        recognizer.process(&Event::TouchMove {
762            id: TouchId::new(1),
763            position: Point::new(150.0, 250.0),
764            pressure: 0.6,
765        });
766
767        let touch = recognizer.touch(TouchId::new(1)).unwrap();
768        assert_eq!(touch.current_position, Point::new(150.0, 250.0));
769    }
770
771    #[test]
772    fn test_recognizer_touch_end() {
773        let mut recognizer = GestureRecognizer::new();
774
775        recognizer.process(&Event::TouchStart {
776            id: TouchId::new(1),
777            position: Point::new(100.0, 200.0),
778            pressure: 0.5,
779        });
780
781        recognizer.process(&Event::TouchEnd {
782            id: TouchId::new(1),
783            position: Point::new(100.0, 200.0),
784        });
785
786        assert_eq!(recognizer.touch_count(), 0);
787    }
788
789    #[test]
790    fn test_recognizer_tap() {
791        let mut recognizer = GestureRecognizer::new();
792
793        recognizer.process(&Event::TouchStart {
794            id: TouchId::new(1),
795            position: Point::new(100.0, 200.0),
796            pressure: 0.5,
797        });
798
799        // End immediately (tap)
800        let result = recognizer.process(&Event::TouchEnd {
801            id: TouchId::new(1),
802            position: Point::new(100.0, 200.0),
803        });
804
805        assert!(matches!(result, Some(Event::GestureTap { count: 1, .. })));
806    }
807
808    #[test]
809    fn test_recognizer_pan() {
810        let mut recognizer = GestureRecognizer::new();
811
812        recognizer.process(&Event::TouchStart {
813            id: TouchId::new(1),
814            position: Point::new(100.0, 200.0),
815            pressure: 0.5,
816        });
817
818        // Move beyond pan threshold
819        let result = recognizer.process(&Event::TouchMove {
820            id: TouchId::new(1),
821            position: Point::new(150.0, 250.0), // 50px moved
822            pressure: 0.5,
823        });
824
825        assert!(matches!(
826            result,
827            Some(Event::GesturePan {
828                state: GestureState::Started,
829                ..
830            })
831        ));
832    }
833
834    #[test]
835    fn test_recognizer_pan_continued() {
836        let mut recognizer = GestureRecognizer::new();
837
838        recognizer.process(&Event::TouchStart {
839            id: TouchId::new(1),
840            position: Point::new(100.0, 200.0),
841            pressure: 0.5,
842        });
843
844        // First move - starts pan
845        recognizer.process(&Event::TouchMove {
846            id: TouchId::new(1),
847            position: Point::new(150.0, 250.0),
848            pressure: 0.5,
849        });
850
851        // Second move - continues pan
852        let result = recognizer.process(&Event::TouchMove {
853            id: TouchId::new(1),
854            position: Point::new(200.0, 300.0),
855            pressure: 0.5,
856        });
857
858        assert!(matches!(
859            result,
860            Some(Event::GesturePan {
861                state: GestureState::Changed,
862                ..
863            })
864        ));
865    }
866
867    #[test]
868    fn test_recognizer_two_touches() {
869        let mut recognizer = GestureRecognizer::new();
870
871        recognizer.process(&Event::TouchStart {
872            id: TouchId::new(1),
873            position: Point::new(100.0, 200.0),
874            pressure: 0.5,
875        });
876
877        recognizer.process(&Event::TouchStart {
878            id: TouchId::new(2),
879            position: Point::new(200.0, 200.0),
880            pressure: 0.5,
881        });
882
883        assert_eq!(recognizer.touch_count(), 2);
884    }
885
886    #[test]
887    fn test_recognizer_pinch() {
888        let mut recognizer = GestureRecognizer::with_config(GestureConfig {
889            pinch_threshold: 0.01,
890            ..Default::default()
891        });
892
893        // Start with two fingers 100px apart
894        recognizer.process(&Event::TouchStart {
895            id: TouchId::new(1),
896            position: Point::new(100.0, 200.0),
897            pressure: 0.5,
898        });
899
900        recognizer.process(&Event::TouchStart {
901            id: TouchId::new(2),
902            position: Point::new(200.0, 200.0),
903            pressure: 0.5,
904        });
905
906        // Move fingers apart to 200px
907        recognizer.process(&Event::TouchMove {
908            id: TouchId::new(1),
909            position: Point::new(50.0, 200.0),
910            pressure: 0.5,
911        });
912
913        let result = recognizer.process(&Event::TouchMove {
914            id: TouchId::new(2),
915            position: Point::new(250.0, 200.0),
916            pressure: 0.5,
917        });
918
919        assert!(matches!(result, Some(Event::GesturePinch { .. })));
920    }
921
922    #[test]
923    fn test_recognizer_reset() {
924        let mut recognizer = GestureRecognizer::new();
925
926        recognizer.process(&Event::TouchStart {
927            id: TouchId::new(1),
928            position: Point::new(100.0, 200.0),
929            pressure: 0.5,
930        });
931
932        assert_eq!(recognizer.touch_count(), 1);
933
934        recognizer.reset();
935        assert_eq!(recognizer.touch_count(), 0);
936    }
937
938    #[test]
939    fn test_recognizer_touch_cancel() {
940        let mut recognizer = GestureRecognizer::new();
941
942        recognizer.process(&Event::TouchStart {
943            id: TouchId::new(1),
944            position: Point::new(100.0, 200.0),
945            pressure: 0.5,
946        });
947
948        recognizer.process(&Event::TouchCancel {
949            id: TouchId::new(1),
950        });
951
952        assert_eq!(recognizer.touch_count(), 0);
953    }
954
955    #[test]
956    fn test_recognizer_ignores_non_touch_events() {
957        let mut recognizer = GestureRecognizer::new();
958
959        let result = recognizer.process(&Event::MouseMove {
960            position: Point::new(100.0, 200.0),
961        });
962
963        assert!(result.is_none());
964        assert_eq!(recognizer.touch_count(), 0);
965    }
966
967    // PointerGestureRecognizer tests
968    #[test]
969    fn test_pointer_recognizer_new() {
970        let recognizer = PointerGestureRecognizer::new();
971        assert_eq!(recognizer.pointer_count(), 0);
972        assert!(recognizer.primary().is_none());
973    }
974
975    #[test]
976    fn test_pointer_recognizer_pointer_down() {
977        let mut recognizer = PointerGestureRecognizer::new();
978
979        recognizer.process(&Event::PointerDown {
980            pointer_id: PointerId::new(1),
981            pointer_type: PointerType::Touch,
982            position: Point::new(100.0, 200.0),
983            pressure: 0.5,
984            is_primary: true,
985            button: None,
986        });
987
988        assert_eq!(recognizer.pointer_count(), 1);
989
990        let primary = recognizer.primary().unwrap();
991        assert_eq!(primary.id, PointerId(1));
992        assert_eq!(primary.pointer_type, PointerType::Touch);
993        assert!(primary.is_primary);
994    }
995
996    #[test]
997    fn test_pointer_recognizer_pointer_move() {
998        let mut recognizer = PointerGestureRecognizer::new();
999
1000        recognizer.process(&Event::PointerDown {
1001            pointer_id: PointerId::new(1),
1002            pointer_type: PointerType::Mouse,
1003            position: Point::new(100.0, 200.0),
1004            pressure: 0.5,
1005            is_primary: true,
1006            button: None,
1007        });
1008
1009        recognizer.process(&Event::PointerMove {
1010            pointer_id: PointerId::new(1),
1011            pointer_type: PointerType::Mouse,
1012            position: Point::new(150.0, 250.0),
1013            pressure: 0.6,
1014            is_primary: true,
1015        });
1016
1017        let primary = recognizer.primary().unwrap();
1018        assert_eq!(primary.current_position, Point::new(150.0, 250.0));
1019    }
1020
1021    #[test]
1022    fn test_pointer_recognizer_pointer_up() {
1023        let mut recognizer = PointerGestureRecognizer::new();
1024
1025        recognizer.process(&Event::PointerDown {
1026            pointer_id: PointerId::new(1),
1027            pointer_type: PointerType::Pen,
1028            position: Point::new(100.0, 200.0),
1029            pressure: 0.5,
1030            is_primary: true,
1031            button: None,
1032        });
1033
1034        recognizer.process(&Event::PointerUp {
1035            pointer_id: PointerId::new(1),
1036            pointer_type: PointerType::Pen,
1037            position: Point::new(100.0, 200.0),
1038            is_primary: true,
1039            button: None,
1040        });
1041
1042        assert_eq!(recognizer.pointer_count(), 0);
1043        assert!(recognizer.primary().is_none());
1044    }
1045
1046    #[test]
1047    fn test_pointer_recognizer_multiple_pointers() {
1048        let mut recognizer = PointerGestureRecognizer::new();
1049
1050        // First pointer (primary)
1051        recognizer.process(&Event::PointerDown {
1052            pointer_id: PointerId::new(1),
1053            pointer_type: PointerType::Touch,
1054            position: Point::new(100.0, 200.0),
1055            pressure: 0.5,
1056            is_primary: true,
1057            button: None,
1058        });
1059
1060        // Second pointer (not primary)
1061        recognizer.process(&Event::PointerDown {
1062            pointer_id: PointerId::new(2),
1063            pointer_type: PointerType::Touch,
1064            position: Point::new(200.0, 200.0),
1065            pressure: 0.5,
1066            is_primary: false,
1067            button: None,
1068        });
1069
1070        assert_eq!(recognizer.pointer_count(), 2);
1071
1072        let primary = recognizer.primary().unwrap();
1073        assert_eq!(primary.id, PointerId(1));
1074    }
1075
1076    #[test]
1077    fn test_pointer_recognizer_primary_changes_on_remove() {
1078        let mut recognizer = PointerGestureRecognizer::new();
1079
1080        recognizer.process(&Event::PointerDown {
1081            pointer_id: PointerId::new(1),
1082            pointer_type: PointerType::Touch,
1083            position: Point::new(100.0, 200.0),
1084            pressure: 0.5,
1085            is_primary: true,
1086            button: None,
1087        });
1088
1089        recognizer.process(&Event::PointerDown {
1090            pointer_id: PointerId::new(2),
1091            pointer_type: PointerType::Touch,
1092            position: Point::new(200.0, 200.0),
1093            pressure: 0.5,
1094            is_primary: false,
1095            button: None,
1096        });
1097
1098        // Remove primary
1099        recognizer.process(&Event::PointerUp {
1100            pointer_id: PointerId::new(1),
1101            pointer_type: PointerType::Touch,
1102            position: Point::new(100.0, 200.0),
1103            is_primary: true,
1104            button: None,
1105        });
1106
1107        assert_eq!(recognizer.pointer_count(), 1);
1108        // Primary should now be the remaining pointer
1109        assert!(recognizer.primary().is_some());
1110    }
1111
1112    #[test]
1113    fn test_pointer_recognizer_reset() {
1114        let mut recognizer = PointerGestureRecognizer::new();
1115
1116        recognizer.process(&Event::PointerDown {
1117            pointer_id: PointerId::new(1),
1118            pointer_type: PointerType::Touch,
1119            position: Point::new(100.0, 200.0),
1120            pressure: 0.5,
1121            is_primary: true,
1122            button: None,
1123        });
1124
1125        recognizer.reset();
1126
1127        assert_eq!(recognizer.pointer_count(), 0);
1128        assert!(recognizer.primary().is_none());
1129    }
1130
1131    #[test]
1132    fn test_pointer_recognizer_cancel() {
1133        let mut recognizer = PointerGestureRecognizer::new();
1134
1135        recognizer.process(&Event::PointerDown {
1136            pointer_id: PointerId::new(1),
1137            pointer_type: PointerType::Touch,
1138            position: Point::new(100.0, 200.0),
1139            pressure: 0.5,
1140            is_primary: true,
1141            button: None,
1142        });
1143
1144        recognizer.process(&Event::PointerCancel {
1145            pointer_id: PointerId::new(1),
1146        });
1147
1148        assert_eq!(recognizer.pointer_count(), 0);
1149    }
1150
1151    // RecognizedGesture tests
1152    #[test]
1153    fn test_recognized_gesture_default() {
1154        let gesture = RecognizedGesture::default();
1155        assert!(matches!(gesture, RecognizedGesture::None));
1156    }
1157
1158    // PointerInfo tests
1159    #[test]
1160    fn test_pointer_info_clone() {
1161        let info = PointerInfo {
1162            id: PointerId::new(1),
1163            pointer_type: PointerType::Mouse,
1164            start_position: Point::new(100.0, 200.0),
1165            current_position: Point::new(150.0, 250.0),
1166            start_time: Instant::now(),
1167            is_primary: true,
1168            pressure: 0.8,
1169        };
1170
1171        let cloned = info.clone();
1172        assert_eq!(cloned.id, info.id);
1173        assert_eq!(cloned.pointer_type, info.pointer_type);
1174        assert_eq!(cloned.pressure, info.pressure);
1175    }
1176
1177    // =========================================================================
1178    // Additional Edge Case Tests
1179    // =========================================================================
1180
1181    #[test]
1182    fn test_gesture_config_debug() {
1183        let config = GestureConfig::default();
1184        let debug = format!("{config:?}");
1185        assert!(debug.contains("GestureConfig"));
1186    }
1187
1188    #[test]
1189    fn test_gesture_config_clone() {
1190        let config = GestureConfig {
1191            pan_threshold: 25.0,
1192            ..Default::default()
1193        };
1194        let cloned = config;
1195        assert_eq!(cloned.pan_threshold, 25.0);
1196    }
1197
1198    #[test]
1199    fn test_touch_point_debug() {
1200        let point = TouchPoint::new(TouchId::new(1), Point::new(100.0, 200.0), 0.5);
1201        let debug = format!("{point:?}");
1202        assert!(debug.contains("TouchPoint"));
1203    }
1204
1205    #[test]
1206    fn test_touch_point_clone() {
1207        let point = TouchPoint::new(TouchId::new(1), Point::new(100.0, 200.0), 0.5);
1208        let cloned = point.clone();
1209        assert_eq!(cloned.id, point.id);
1210        assert_eq!(cloned.start_position, point.start_position);
1211    }
1212
1213    #[test]
1214    fn test_touch_point_duration() {
1215        let point = TouchPoint::new(TouchId::new(1), Point::new(100.0, 200.0), 0.5);
1216        let duration = point.duration();
1217        // Performance target: <100ms (verify via cargo bench, not wall-clock in tests)
1218        if duration.as_millis() >= 100 {
1219            eprintln!(
1220                "[PERF WARNING] touch point duration {}ms (target <100ms)",
1221                duration.as_millis()
1222            );
1223        }
1224    }
1225
1226    #[test]
1227    fn test_recognized_gesture_debug() {
1228        let gesture = RecognizedGesture::Tap {
1229            position: Point::new(50.0, 50.0),
1230            count: 2,
1231        };
1232        let debug = format!("{gesture:?}");
1233        assert!(debug.contains("Tap"));
1234    }
1235
1236    #[test]
1237    fn test_recognized_gesture_clone() {
1238        let gesture = RecognizedGesture::Pan {
1239            delta: Point::new(10.0, 20.0),
1240            velocity: Point::new(100.0, 200.0),
1241            state: GestureState::Started,
1242        };
1243        let cloned = gesture;
1244        assert!(matches!(cloned, RecognizedGesture::Pan { .. }));
1245    }
1246
1247    #[test]
1248    fn test_recognized_gesture_all_variants() {
1249        let gestures = vec![
1250            RecognizedGesture::None,
1251            RecognizedGesture::Tap {
1252                position: Point::ORIGIN,
1253                count: 1,
1254            },
1255            RecognizedGesture::LongPress {
1256                position: Point::ORIGIN,
1257            },
1258            RecognizedGesture::Pan {
1259                delta: Point::ORIGIN,
1260                velocity: Point::ORIGIN,
1261                state: GestureState::Started,
1262            },
1263            RecognizedGesture::Pinch {
1264                scale: 1.0,
1265                center: Point::ORIGIN,
1266                state: GestureState::Changed,
1267            },
1268            RecognizedGesture::Rotate {
1269                angle: 0.5,
1270                center: Point::ORIGIN,
1271                state: GestureState::Ended,
1272            },
1273        ];
1274
1275        for gesture in gestures {
1276            let debug = format!("{gesture:?}");
1277            assert!(!debug.is_empty());
1278        }
1279    }
1280
1281    #[test]
1282    fn test_gesture_recognizer_default() {
1283        let recognizer = GestureRecognizer::default();
1284        assert_eq!(recognizer.touch_count(), 0);
1285        assert_eq!(recognizer.config().pan_threshold, 10.0);
1286    }
1287
1288    #[test]
1289    fn test_gesture_recognizer_debug() {
1290        let recognizer = GestureRecognizer::new();
1291        let debug = format!("{recognizer:?}");
1292        assert!(debug.contains("GestureRecognizer"));
1293    }
1294
1295    #[test]
1296    fn test_gesture_recognizer_touch_move_unknown_id() {
1297        let mut recognizer = GestureRecognizer::new();
1298
1299        // Move without starting touch
1300        let result = recognizer.process(&Event::TouchMove {
1301            id: TouchId::new(99),
1302            position: Point::new(150.0, 250.0),
1303            pressure: 0.5,
1304        });
1305
1306        assert!(result.is_none());
1307    }
1308
1309    #[test]
1310    fn test_gesture_recognizer_touch_end_unknown_id() {
1311        let mut recognizer = GestureRecognizer::new();
1312
1313        // End without starting touch
1314        let result = recognizer.process(&Event::TouchEnd {
1315            id: TouchId::new(99),
1316            position: Point::new(100.0, 200.0),
1317        });
1318
1319        assert!(result.is_none());
1320    }
1321
1322    #[test]
1323    fn test_gesture_recognizer_pan_end() {
1324        let mut recognizer = GestureRecognizer::new();
1325
1326        recognizer.process(&Event::TouchStart {
1327            id: TouchId::new(1),
1328            position: Point::new(100.0, 200.0),
1329            pressure: 0.5,
1330        });
1331
1332        // Move to start pan
1333        recognizer.process(&Event::TouchMove {
1334            id: TouchId::new(1),
1335            position: Point::new(150.0, 250.0),
1336            pressure: 0.5,
1337        });
1338
1339        // End touch
1340        let result = recognizer.process(&Event::TouchEnd {
1341            id: TouchId::new(1),
1342            position: Point::new(150.0, 250.0),
1343        });
1344
1345        assert!(matches!(
1346            result,
1347            Some(Event::GesturePan {
1348                state: GestureState::Ended,
1349                ..
1350            })
1351        ));
1352    }
1353
1354    #[test]
1355    fn test_gesture_recognizer_rotate() {
1356        let mut recognizer = GestureRecognizer::with_config(GestureConfig {
1357            rotate_threshold: 0.01,
1358            pinch_threshold: 1.0, // High threshold to prefer rotation
1359            ..Default::default()
1360        });
1361
1362        // Start with two fingers
1363        recognizer.process(&Event::TouchStart {
1364            id: TouchId::new(1),
1365            position: Point::new(100.0, 200.0),
1366            pressure: 0.5,
1367        });
1368
1369        recognizer.process(&Event::TouchStart {
1370            id: TouchId::new(2),
1371            position: Point::new(200.0, 200.0),
1372            pressure: 0.5,
1373        });
1374
1375        // Rotate (move one finger up, other down, same distance apart)
1376        recognizer.process(&Event::TouchMove {
1377            id: TouchId::new(1),
1378            position: Point::new(100.0, 150.0),
1379            pressure: 0.5,
1380        });
1381
1382        let result = recognizer.process(&Event::TouchMove {
1383            id: TouchId::new(2),
1384            position: Point::new(200.0, 250.0),
1385            pressure: 0.5,
1386        });
1387
1388        assert!(matches!(result, Some(Event::GestureRotate { .. })));
1389    }
1390
1391    #[test]
1392    fn test_gesture_recognizer_two_finger_end() {
1393        let mut recognizer = GestureRecognizer::with_config(GestureConfig {
1394            pinch_threshold: 0.01,
1395            ..Default::default()
1396        });
1397
1398        // Start pinch
1399        recognizer.process(&Event::TouchStart {
1400            id: TouchId::new(1),
1401            position: Point::new(100.0, 200.0),
1402            pressure: 0.5,
1403        });
1404
1405        recognizer.process(&Event::TouchStart {
1406            id: TouchId::new(2),
1407            position: Point::new(200.0, 200.0),
1408            pressure: 0.5,
1409        });
1410
1411        // Move to trigger pinch
1412        recognizer.process(&Event::TouchMove {
1413            id: TouchId::new(1),
1414            position: Point::new(50.0, 200.0),
1415            pressure: 0.5,
1416        });
1417
1418        recognizer.process(&Event::TouchMove {
1419            id: TouchId::new(2),
1420            position: Point::new(250.0, 200.0),
1421            pressure: 0.5,
1422        });
1423
1424        // End one finger
1425        let result = recognizer.process(&Event::TouchEnd {
1426            id: TouchId::new(1),
1427            position: Point::new(50.0, 200.0),
1428        });
1429
1430        assert!(matches!(
1431            result,
1432            Some(Event::GesturePinch {
1433                state: GestureState::Ended,
1434                ..
1435            })
1436        ));
1437    }
1438
1439    #[test]
1440    fn test_gesture_recognizer_three_touches() {
1441        let mut recognizer = GestureRecognizer::new();
1442
1443        recognizer.process(&Event::TouchStart {
1444            id: TouchId::new(1),
1445            position: Point::new(100.0, 200.0),
1446            pressure: 0.5,
1447        });
1448
1449        recognizer.process(&Event::TouchStart {
1450            id: TouchId::new(2),
1451            position: Point::new(200.0, 200.0),
1452            pressure: 0.5,
1453        });
1454
1455        recognizer.process(&Event::TouchStart {
1456            id: TouchId::new(3),
1457            position: Point::new(300.0, 200.0),
1458            pressure: 0.5,
1459        });
1460
1461        assert_eq!(recognizer.touch_count(), 3);
1462
1463        // Move with 3 touches should return None (not handled)
1464        let result = recognizer.process(&Event::TouchMove {
1465            id: TouchId::new(1),
1466            position: Point::new(150.0, 250.0),
1467            pressure: 0.5,
1468        });
1469
1470        assert!(result.is_none());
1471    }
1472
1473    #[test]
1474    fn test_pointer_recognizer_debug() {
1475        let recognizer = PointerGestureRecognizer::new();
1476        let debug = format!("{recognizer:?}");
1477        assert!(debug.contains("PointerGestureRecognizer"));
1478    }
1479
1480    #[test]
1481    fn test_pointer_recognizer_default() {
1482        let recognizer = PointerGestureRecognizer::default();
1483        assert_eq!(recognizer.pointer_count(), 0);
1484    }
1485
1486    #[test]
1487    fn test_pointer_recognizer_with_config() {
1488        let config = GestureConfig {
1489            pan_threshold: 30.0,
1490            ..Default::default()
1491        };
1492        let recognizer = PointerGestureRecognizer::with_config(config);
1493        assert_eq!(recognizer.config().pan_threshold, 30.0);
1494    }
1495
1496    #[test]
1497    fn test_pointer_recognizer_ignores_non_pointer_events() {
1498        let mut recognizer = PointerGestureRecognizer::new();
1499
1500        let result = recognizer.process(&Event::MouseMove {
1501            position: Point::new(100.0, 200.0),
1502        });
1503
1504        assert!(result.is_none());
1505        assert_eq!(recognizer.pointer_count(), 0);
1506    }
1507
1508    #[test]
1509    fn test_pointer_recognizer_move_unknown_pointer() {
1510        let mut recognizer = PointerGestureRecognizer::new();
1511
1512        let result = recognizer.process(&Event::PointerMove {
1513            pointer_id: PointerId::new(99),
1514            pointer_type: PointerType::Touch,
1515            position: Point::new(150.0, 250.0),
1516            pressure: 0.5,
1517            is_primary: true,
1518        });
1519
1520        assert!(result.is_none());
1521    }
1522
1523    #[test]
1524    fn test_pointer_info_debug() {
1525        let info = PointerInfo {
1526            id: PointerId::new(1),
1527            pointer_type: PointerType::Touch,
1528            start_position: Point::new(100.0, 200.0),
1529            current_position: Point::new(100.0, 200.0),
1530            start_time: Instant::now(),
1531            is_primary: true,
1532            pressure: 0.5,
1533        };
1534        let debug = format!("{info:?}");
1535        assert!(debug.contains("PointerInfo"));
1536    }
1537
1538    #[test]
1539    fn test_pointer_recognizer_first_non_primary_becomes_primary() {
1540        let mut recognizer = PointerGestureRecognizer::new();
1541
1542        // First pointer is not marked as primary, but should become primary
1543        recognizer.process(&Event::PointerDown {
1544            pointer_id: PointerId::new(1),
1545            pointer_type: PointerType::Touch,
1546            position: Point::new(100.0, 200.0),
1547            pressure: 0.5,
1548            is_primary: false,
1549            button: None,
1550        });
1551
1552        // Since no primary existed, this should be set as primary
1553        assert!(recognizer.primary().is_some());
1554    }
1555
1556    #[test]
1557    fn test_gesture_recognizer_below_pan_threshold() {
1558        let mut recognizer = GestureRecognizer::new();
1559
1560        recognizer.process(&Event::TouchStart {
1561            id: TouchId::new(1),
1562            position: Point::new(100.0, 200.0),
1563            pressure: 0.5,
1564        });
1565
1566        // Small move below threshold
1567        let result = recognizer.process(&Event::TouchMove {
1568            id: TouchId::new(1),
1569            position: Point::new(102.0, 202.0), // Only ~2.8px moved
1570            pressure: 0.5,
1571        });
1572
1573        assert!(result.is_none());
1574    }
1575
1576    #[test]
1577    fn test_gesture_recognizer_below_pinch_threshold() {
1578        let mut recognizer = GestureRecognizer::new();
1579
1580        recognizer.process(&Event::TouchStart {
1581            id: TouchId::new(1),
1582            position: Point::new(100.0, 200.0),
1583            pressure: 0.5,
1584        });
1585
1586        recognizer.process(&Event::TouchStart {
1587            id: TouchId::new(2),
1588            position: Point::new(200.0, 200.0),
1589            pressure: 0.5,
1590        });
1591
1592        // Tiny movement that doesn't trigger pinch/rotate
1593        let result = recognizer.process(&Event::TouchMove {
1594            id: TouchId::new(1),
1595            position: Point::new(99.0, 200.0),
1596            pressure: 0.5,
1597        });
1598
1599        assert!(result.is_none());
1600    }
1601}