Skip to main content

ftui_core/
gesture.rs

1#![forbid(unsafe_code)]
2
3//! Gesture recognition: transforms raw terminal events into semantic events.
4//!
5//! [`GestureRecognizer`] is a stateful processor that converts raw [`Event`]
6//! sequences (mouse clicks, key presses, etc.) into high-level [`SemanticEvent`]s
7//! (double-click, drag, chord, etc.).
8//!
9//! # State Machine
10//!
11//! The recognizer tracks several concurrent state machines:
12//!
13//! - **Click detector**: Tracks consecutive clicks at the same position to emit
14//!   `Click`, `DoubleClick`, or `TripleClick`.
15//! - **Drag detector**: Monitors mouse-down → move → mouse-up sequences,
16//!   emitting `DragStart` / `DragMove` / `DragEnd` / `DragCancel`.
17//! - **Long press detector**: Fires when mouse is held stationary beyond a threshold.
18//! - **Chord detector**: Accumulates modifier+key sequences within a timeout window.
19//!
20//! # Invariants
21//!
22//! 1. Drag and Click never both emit for the same mouse-down → mouse-up interaction.
23//!    If a drag starts, the mouse-up produces `DragEnd`, not `Click`.
24//! 2. Click multiplicity is monotonically increasing within a multi-click window:
25//!    `Click` → `DoubleClick` → `TripleClick`.
26//! 3. `Chord` sequences are always non-empty.
27//! 4. After `reset()`, all state machines return to their initial idle state.
28//! 5. `DragCancel` is emitted if Escape is pressed during a drag.
29//!
30//! # Failure Modes
31//!
32//! - If the chord timeout expires mid-sequence, the chord buffer is cleared and
33//!   no `Chord` event is emitted (raw keys are still delivered by the caller).
34//! - If focus is lost during a drag, the caller should call `reset()` which will
35//!   not emit `DragCancel` (the caller handles focus-loss cancellation).
36
37use web_time::{Duration, Instant};
38
39use crate::event::{Event, KeyCode, KeyEventKind, Modifiers, MouseButton, MouseEventKind};
40use crate::semantic_event::{ChordKey, Position, SemanticEvent};
41
42// ---------------------------------------------------------------------------
43// Configuration
44// ---------------------------------------------------------------------------
45
46/// Thresholds and timeouts for gesture recognition.
47#[derive(Debug, Clone)]
48pub struct GestureConfig {
49    /// Time window for double/triple click detection (default: 300ms).
50    pub multi_click_timeout: Duration,
51    /// Duration before a stationary mouse-down triggers long press (default: 500ms).
52    pub long_press_threshold: Duration,
53    /// Minimum manhattan distance (cells) before a drag starts (default: 3).
54    pub drag_threshold: u16,
55    /// Time window for chord key sequence completion (default: 1000ms).
56    pub chord_timeout: Duration,
57    /// Minimum velocity (cells/sec) for swipe detection (default: 50.0).
58    pub swipe_velocity_threshold: f32,
59    /// Position tolerance for multi-click detection (manhattan distance, default: 1).
60    pub click_tolerance: u16,
61}
62
63impl Default for GestureConfig {
64    fn default() -> Self {
65        Self {
66            multi_click_timeout: Duration::from_millis(300),
67            long_press_threshold: Duration::from_millis(500),
68            drag_threshold: 3,
69            chord_timeout: Duration::from_millis(1000),
70            swipe_velocity_threshold: 50.0,
71            click_tolerance: 1,
72        }
73    }
74}
75
76// ---------------------------------------------------------------------------
77// Internal state
78// ---------------------------------------------------------------------------
79
80/// Tracks click timing for multi-click detection.
81#[derive(Debug, Clone)]
82struct ClickState {
83    pos: Position,
84    button: MouseButton,
85    time: Instant,
86    count: u8,
87}
88
89/// Tracks an ongoing or potential drag.
90#[derive(Debug, Clone)]
91struct DragTracker {
92    start_pos: Position,
93    button: MouseButton,
94    last_pos: Position,
95    started: bool,
96}
97
98// ---------------------------------------------------------------------------
99// GestureRecognizer
100// ---------------------------------------------------------------------------
101
102/// Stateful gesture recognizer that transforms raw events into semantic events.
103///
104/// Call [`process`](GestureRecognizer::process) for each incoming [`Event`].
105/// Call [`check_long_press`](GestureRecognizer::check_long_press) periodically
106/// (e.g., on tick) to detect long-press gestures.
107pub struct GestureRecognizer {
108    config: GestureConfig,
109
110    // Click tracking
111    last_click: Option<ClickState>,
112
113    // Drag tracking
114    mouse_down: Option<(Position, MouseButton, Instant)>,
115    drag: Option<DragTracker>,
116
117    // Long press tracking
118    long_press_pos: Option<(Position, Instant)>,
119    long_press_fired: bool,
120
121    // Chord tracking
122    chord_buffer: Vec<ChordKey>,
123    chord_start: Option<Instant>,
124}
125
126impl std::fmt::Debug for GestureRecognizer {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        f.debug_struct("GestureRecognizer")
129            .field("dragging", &self.is_dragging())
130            .field("chord_len", &self.chord_buffer.len())
131            .finish()
132    }
133}
134
135impl GestureRecognizer {
136    /// Create a new gesture recognizer with the given configuration.
137    #[must_use]
138    pub fn new(config: GestureConfig) -> Self {
139        Self {
140            config,
141            last_click: None,
142            mouse_down: None,
143            drag: None,
144            long_press_pos: None,
145            long_press_fired: false,
146            chord_buffer: Vec::with_capacity(4),
147            chord_start: None,
148        }
149    }
150
151    /// Process a raw event, returning any semantic events produced.
152    ///
153    /// Most events produce 0 or 1 semantic events. A mouse-up after a
154    /// multi-click sequence may produce both a `Click` and a `DoubleClick`.
155    #[inline]
156    pub fn process(&mut self, event: &Event, now: Instant) -> Vec<SemanticEvent> {
157        let mut out = Vec::with_capacity(2);
158
159        // Expire stale chord
160        self.expire_chord(now);
161
162        match event {
163            Event::Mouse(mouse) => {
164                let pos = Position::new(mouse.x, mouse.y);
165                match mouse.kind {
166                    MouseEventKind::Down(button) => {
167                        self.on_mouse_down(pos, button, now, &mut out);
168                    }
169                    MouseEventKind::Up(button) => {
170                        self.on_mouse_up(pos, button, now, &mut out);
171                    }
172                    MouseEventKind::Drag(button) => {
173                        self.on_mouse_drag(pos, button, &mut out);
174                    }
175                    MouseEventKind::Moved => {
176                        // Movement without button cancels long press
177                        self.long_press_pos = None;
178                    }
179                    _ => {}
180                }
181            }
182            Event::Key(key) => {
183                if key.kind != KeyEventKind::Press {
184                    return out;
185                }
186
187                // Escape cancels drag
188                if key.code == KeyCode::Escape {
189                    if let Some(drag) = self.drag.take()
190                        && drag.started
191                    {
192                        out.push(SemanticEvent::DragCancel);
193                    }
194                    self.mouse_down = None;
195                    self.long_press_pos = None;
196                    self.chord_buffer.clear();
197                    self.chord_start = None;
198                    return out;
199                }
200
201                // Chord detection: modifier+key combinations
202                let has_modifier = key
203                    .modifiers
204                    .intersects(Modifiers::CTRL | Modifiers::ALT | Modifiers::SUPER);
205
206                if has_modifier {
207                    let chord_key = ChordKey::new(key.code, key.modifiers);
208                    if self.chord_buffer.is_empty() {
209                        self.chord_start = Some(now);
210                    }
211                    self.chord_buffer.push(chord_key);
212
213                    // Emit chord after each addition (caller can decide if complete).
214                    // A chord of length 1 is just a modified key; length ≥2 is a sequence.
215                    if self.chord_buffer.len() >= 2 {
216                        out.push(SemanticEvent::Chord {
217                            sequence: self.chord_buffer.clone(),
218                        });
219                        self.chord_buffer.clear();
220                        self.chord_start = None;
221                    }
222                } else {
223                    // Non-modifier key clears any in-progress chord
224                    self.chord_buffer.clear();
225                    self.chord_start = None;
226                }
227            }
228            Event::Focus(false) => {
229                // Focus loss: reset drag and long press
230                if let Some(drag) = self.drag.take()
231                    && drag.started
232                {
233                    out.push(SemanticEvent::DragCancel);
234                }
235                self.mouse_down = None;
236                self.long_press_pos = None;
237                self.long_press_fired = false;
238            }
239            _ => {}
240        }
241
242        out
243    }
244
245    /// Check for long press timeout. Call periodically (e.g., on tick).
246    ///
247    /// Returns `Some(LongPress { .. })` if the mouse has been held stationary
248    /// beyond the configured threshold.
249    #[inline]
250    pub fn check_long_press(&mut self, now: Instant) -> Option<SemanticEvent> {
251        if self.long_press_fired {
252            return None;
253        }
254        if let Some((pos, down_time)) = self.long_press_pos {
255            let elapsed = now.duration_since(down_time);
256            if elapsed >= self.config.long_press_threshold {
257                self.long_press_fired = true;
258                return Some(SemanticEvent::LongPress {
259                    pos,
260                    duration: elapsed,
261                });
262            }
263        }
264        None
265    }
266
267    /// Whether a drag is currently in progress.
268    #[inline]
269    #[must_use]
270    pub fn is_dragging(&self) -> bool {
271        self.drag.as_ref().is_some_and(|d| d.started)
272    }
273
274    /// Reset all gesture state to initial idle.
275    pub fn reset(&mut self) {
276        self.last_click = None;
277        self.mouse_down = None;
278        self.drag = None;
279        self.long_press_pos = None;
280        self.long_press_fired = false;
281        self.chord_buffer.clear();
282        self.chord_start = None;
283    }
284
285    /// Get a reference to the current configuration.
286    #[inline]
287    #[must_use]
288    pub fn config(&self) -> &GestureConfig {
289        &self.config
290    }
291
292    /// Update the configuration.
293    pub fn set_config(&mut self, config: GestureConfig) {
294        self.config = config;
295    }
296}
297
298// ---------------------------------------------------------------------------
299// Internal event handlers
300// ---------------------------------------------------------------------------
301
302impl GestureRecognizer {
303    fn on_mouse_down(
304        &mut self,
305        pos: Position,
306        button: MouseButton,
307        now: Instant,
308        _out: &mut Vec<SemanticEvent>,
309    ) {
310        self.mouse_down = Some((pos, button, now));
311        self.drag = Some(DragTracker {
312            start_pos: pos,
313            button,
314            last_pos: pos,
315            started: false,
316        });
317        self.long_press_pos = Some((pos, now));
318        self.long_press_fired = false;
319    }
320
321    fn on_mouse_up(
322        &mut self,
323        pos: Position,
324        button: MouseButton,
325        now: Instant,
326        out: &mut Vec<SemanticEvent>,
327    ) {
328        self.long_press_pos = None;
329        self.long_press_fired = false;
330
331        // If we were dragging, emit DragEnd
332        if let Some(drag) = self.drag.take()
333            && drag.started
334        {
335            out.push(SemanticEvent::DragEnd {
336                start: drag.start_pos,
337                end: pos,
338            });
339            self.mouse_down = None;
340            return;
341        }
342
343        // Not a drag — emit click
344        self.mouse_down = None;
345
346        // Multi-click detection
347        let click_count = if let Some(ref last) = self.last_click {
348            if last.button == button
349                && last.pos.manhattan_distance(pos) <= u32::from(self.config.click_tolerance)
350                && now.duration_since(last.time) <= self.config.multi_click_timeout
351                && last.count < 3
352            {
353                last.count + 1
354            } else {
355                1
356            }
357        } else {
358            1
359        };
360
361        self.last_click = Some(ClickState {
362            pos,
363            button,
364            time: now,
365            count: click_count,
366        });
367
368        match click_count {
369            1 => out.push(SemanticEvent::Click { pos, button }),
370            2 => out.push(SemanticEvent::DoubleClick { pos, button }),
371            3 => out.push(SemanticEvent::TripleClick { pos, button }),
372            _ => out.push(SemanticEvent::Click { pos, button }),
373        }
374    }
375
376    fn on_mouse_drag(&mut self, pos: Position, button: MouseButton, out: &mut Vec<SemanticEvent>) {
377        // Cancel long press on any movement
378        self.long_press_pos = None;
379
380        let Some(ref mut drag) = self.drag else {
381            // Drag without prior mouse-down: create tracker
382            self.drag = Some(DragTracker {
383                start_pos: pos,
384                button,
385                last_pos: pos,
386                started: false,
387            });
388            return;
389        };
390
391        if !drag.started {
392            // Check if we've moved past the drag threshold
393            let distance = drag.start_pos.manhattan_distance(pos);
394            if distance >= u32::from(self.config.drag_threshold) {
395                drag.started = true;
396                out.push(SemanticEvent::DragStart {
397                    pos: drag.start_pos,
398                    button: drag.button,
399                });
400            }
401        }
402
403        if drag.started {
404            let delta = (
405                pos.x as i16 - drag.last_pos.x as i16,
406                pos.y as i16 - drag.last_pos.y as i16,
407            );
408            out.push(SemanticEvent::DragMove {
409                start: drag.start_pos,
410                current: pos,
411                delta,
412            });
413        }
414
415        drag.last_pos = pos;
416    }
417
418    fn expire_chord(&mut self, now: Instant) {
419        if let Some(start) = self.chord_start
420            && now.duration_since(start) > self.config.chord_timeout
421        {
422            self.chord_buffer.clear();
423            self.chord_start = None;
424        }
425    }
426}
427
428// ---------------------------------------------------------------------------
429// Tests
430// ---------------------------------------------------------------------------
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use crate::event::{KeyEvent, MouseEvent};
436
437    fn now() -> Instant {
438        Instant::now()
439    }
440
441    fn mouse_down(x: u16, y: u16, button: MouseButton) -> Event {
442        Event::Mouse(MouseEvent {
443            kind: MouseEventKind::Down(button),
444            x,
445            y,
446            modifiers: Modifiers::NONE,
447        })
448    }
449
450    fn mouse_up(x: u16, y: u16, button: MouseButton) -> Event {
451        Event::Mouse(MouseEvent {
452            kind: MouseEventKind::Up(button),
453            x,
454            y,
455            modifiers: Modifiers::NONE,
456        })
457    }
458
459    fn mouse_drag(x: u16, y: u16, button: MouseButton) -> Event {
460        Event::Mouse(MouseEvent {
461            kind: MouseEventKind::Drag(button),
462            x,
463            y,
464            modifiers: Modifiers::NONE,
465        })
466    }
467
468    fn key_press(code: KeyCode, modifiers: Modifiers) -> Event {
469        Event::Key(KeyEvent {
470            code,
471            modifiers,
472            kind: KeyEventKind::Press,
473        })
474    }
475
476    fn esc() -> Event {
477        key_press(KeyCode::Escape, Modifiers::NONE)
478    }
479
480    const MS_50: Duration = Duration::from_millis(50);
481    const MS_100: Duration = Duration::from_millis(100);
482    const MS_200: Duration = Duration::from_millis(200);
483    const MS_500: Duration = Duration::from_millis(500);
484    const MS_600: Duration = Duration::from_millis(600);
485
486    // --- Click tests ---
487
488    #[test]
489    fn single_click() {
490        let mut gr = GestureRecognizer::new(GestureConfig::default());
491        let t = now();
492
493        let events = gr.process(&mouse_down(5, 5, MouseButton::Left), t);
494        assert!(events.is_empty());
495
496        let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
497        assert_eq!(events.len(), 1);
498        assert!(matches!(
499            events[0],
500            SemanticEvent::Click {
501                pos: Position { x: 5, y: 5 },
502                button: MouseButton::Left,
503            }
504        ));
505    }
506
507    #[test]
508    fn double_click() {
509        let mut gr = GestureRecognizer::new(GestureConfig::default());
510        let t = now();
511
512        // First click
513        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
514        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
515
516        // Second click within timeout
517        gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_100);
518        let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_200);
519
520        assert_eq!(events.len(), 1);
521        assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
522    }
523
524    #[test]
525    fn triple_click() {
526        let mut gr = GestureRecognizer::new(GestureConfig::default());
527        let t = now();
528
529        // First click
530        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
531        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
532
533        // Second click
534        gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_100);
535        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_200);
536
537        // Third click
538        gr.process(
539            &mouse_down(5, 5, MouseButton::Left),
540            t + Duration::from_millis(250),
541        );
542        let events = gr.process(
543            &mouse_up(5, 5, MouseButton::Left),
544            t + Duration::from_millis(280),
545        );
546
547        assert_eq!(events.len(), 1);
548        assert!(matches!(events[0], SemanticEvent::TripleClick { .. }));
549    }
550
551    #[test]
552    fn double_click_timeout_resets_to_single() {
553        let mut gr = GestureRecognizer::new(GestureConfig::default());
554        let t = now();
555
556        // First click
557        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
558        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
559
560        // Second click AFTER timeout (>300ms)
561        gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_500);
562        let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_600);
563
564        assert_eq!(events.len(), 1);
565        assert!(matches!(events[0], SemanticEvent::Click { .. }));
566    }
567
568    #[test]
569    fn different_position_resets_click_count() {
570        let mut gr = GestureRecognizer::new(GestureConfig::default());
571        let t = now();
572
573        // First click at (5, 5)
574        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
575        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
576
577        // Second click at (20, 20) — different position
578        gr.process(&mouse_down(20, 20, MouseButton::Left), t + MS_100);
579        let events = gr.process(&mouse_up(20, 20, MouseButton::Left), t + MS_200);
580
581        assert_eq!(events.len(), 1);
582        assert!(matches!(events[0], SemanticEvent::Click { .. }));
583    }
584
585    #[test]
586    fn different_button_resets_click_count() {
587        let mut gr = GestureRecognizer::new(GestureConfig::default());
588        let t = now();
589
590        // Left click
591        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
592        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
593
594        // Right click at same position
595        gr.process(&mouse_down(5, 5, MouseButton::Right), t + MS_100);
596        let events = gr.process(&mouse_up(5, 5, MouseButton::Right), t + MS_200);
597
598        assert_eq!(events.len(), 1);
599        assert!(matches!(
600            events[0],
601            SemanticEvent::Click {
602                button: MouseButton::Right,
603                ..
604            }
605        ));
606    }
607
608    #[test]
609    fn click_position_tolerance() {
610        let mut gr = GestureRecognizer::new(GestureConfig::default());
611        let t = now();
612
613        // First click at (5, 5)
614        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
615        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
616
617        // Second click at (6, 5) — within tolerance of 1 cell
618        gr.process(&mouse_down(6, 5, MouseButton::Left), t + MS_100);
619        let events = gr.process(&mouse_up(6, 5, MouseButton::Left), t + MS_200);
620
621        assert_eq!(events.len(), 1);
622        assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
623    }
624
625    // --- Drag tests ---
626
627    #[test]
628    fn drag_starts_after_threshold() {
629        let mut gr = GestureRecognizer::new(GestureConfig::default());
630        let t = now();
631
632        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
633
634        // Small move — below threshold (manhattan distance 1 < 3)
635        let events = gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_50);
636        assert!(events.is_empty());
637        assert!(!gr.is_dragging());
638
639        // Move beyond threshold (manhattan distance 5 >= 3)
640        let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_100);
641        assert!(!events.is_empty());
642        assert!(
643            events
644                .iter()
645                .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
646        );
647        assert!(gr.is_dragging());
648    }
649
650    #[test]
651    fn drag_move_has_correct_delta() {
652        let mut gr = GestureRecognizer::new(GestureConfig::default());
653        let t = now();
654
655        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
656        // Move past threshold
657        let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
658
659        // Find the DragMove event
660        let drag_move = events
661            .iter()
662            .find(|e| matches!(e, SemanticEvent::DragMove { .. }));
663        assert!(drag_move.is_some());
664        if let Some(SemanticEvent::DragMove {
665            start,
666            current,
667            delta,
668        }) = drag_move
669        {
670            assert_eq!(*start, Position::new(5, 5));
671            assert_eq!(*current, Position::new(10, 5));
672            assert_eq!(*delta, (5, 0));
673        }
674    }
675
676    #[test]
677    fn drag_end_on_mouse_up() {
678        let mut gr = GestureRecognizer::new(GestureConfig::default());
679        let t = now();
680
681        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
682        gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
683
684        // Mouse up during drag
685        let events = gr.process(&mouse_up(12, 5, MouseButton::Left), t + MS_100);
686        assert_eq!(events.len(), 1);
687        assert!(matches!(
688            events[0],
689            SemanticEvent::DragEnd {
690                start: Position { x: 5, y: 5 },
691                end: Position { x: 12, y: 5 },
692            }
693        ));
694        assert!(!gr.is_dragging());
695    }
696
697    #[test]
698    fn drag_cancel_on_escape() {
699        let mut gr = GestureRecognizer::new(GestureConfig::default());
700        let t = now();
701
702        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
703        gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
704        assert!(gr.is_dragging());
705
706        let events = gr.process(&esc(), t + MS_100);
707        assert_eq!(events.len(), 1);
708        assert!(matches!(events[0], SemanticEvent::DragCancel));
709        assert!(!gr.is_dragging());
710    }
711
712    #[test]
713    fn drag_prevents_click() {
714        let mut gr = GestureRecognizer::new(GestureConfig::default());
715        let t = now();
716
717        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
718        gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
719
720        // Mouse up after drag → DragEnd, NOT Click
721        let events = gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_100);
722        assert_eq!(events.len(), 1);
723        assert!(matches!(events[0], SemanticEvent::DragEnd { .. }));
724        assert!(
725            !events
726                .iter()
727                .any(|e| matches!(e, SemanticEvent::Click { .. }))
728        );
729    }
730
731    // --- Long press tests ---
732
733    #[test]
734    fn long_press_fires_after_threshold() {
735        let mut gr = GestureRecognizer::new(GestureConfig::default());
736        let t = now();
737
738        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
739
740        // Before threshold — no long press
741        assert!(gr.check_long_press(t + MS_200).is_none());
742
743        // After threshold (500ms)
744        let lp = gr.check_long_press(t + MS_600);
745        assert!(lp.is_some());
746        if let Some(SemanticEvent::LongPress { pos, duration }) = lp {
747            assert_eq!(pos, Position::new(5, 5));
748            assert!(duration >= Duration::from_millis(500));
749        }
750    }
751
752    #[test]
753    fn long_press_not_repeated() {
754        let mut gr = GestureRecognizer::new(GestureConfig::default());
755        let t = now();
756
757        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
758
759        // First check fires
760        assert!(gr.check_long_press(t + MS_600).is_some());
761
762        // Second check does NOT fire again
763        assert!(
764            gr.check_long_press(t + Duration::from_millis(700))
765                .is_none()
766        );
767    }
768
769    #[test]
770    fn drag_cancels_long_press() {
771        let mut gr = GestureRecognizer::new(GestureConfig::default());
772        let t = now();
773
774        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
775        // Move the mouse
776        gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_100);
777
778        // Long press should not fire after movement
779        assert!(gr.check_long_press(t + MS_600).is_none());
780    }
781
782    #[test]
783    fn mouse_up_cancels_long_press() {
784        let mut gr = GestureRecognizer::new(GestureConfig::default());
785        let t = now();
786
787        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
788        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
789
790        // Long press should not fire after mouse up
791        assert!(gr.check_long_press(t + MS_600).is_none());
792    }
793
794    // --- Chord tests ---
795
796    #[test]
797    fn two_key_chord() {
798        let mut gr = GestureRecognizer::new(GestureConfig::default());
799        let t = now();
800
801        // Ctrl+K
802        let events1 = gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
803        assert!(events1.is_empty()); // First key in chord: no event yet
804
805        // Ctrl+C within timeout
806        let events2 = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_100);
807        assert_eq!(events2.len(), 1);
808        if let SemanticEvent::Chord { sequence } = &events2[0] {
809            assert_eq!(sequence.len(), 2);
810            assert_eq!(sequence[0].code, KeyCode::Char('k'));
811            assert_eq!(sequence[1].code, KeyCode::Char('c'));
812        } else {
813            panic!("Expected Chord event");
814        }
815    }
816
817    #[test]
818    fn chord_timeout_clears_buffer() {
819        let mut gr = GestureRecognizer::new(GestureConfig::default());
820        let t = now();
821
822        // Ctrl+K
823        gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
824
825        // Wait beyond chord timeout (>1000ms)
826        let events = gr.process(
827            &key_press(KeyCode::Char('c'), Modifiers::CTRL),
828            t + Duration::from_millis(1100),
829        );
830
831        // Should not emit a chord (timeout expired, buffer was cleared)
832        assert!(events.is_empty());
833    }
834
835    #[test]
836    fn non_modifier_key_clears_chord() {
837        let mut gr = GestureRecognizer::new(GestureConfig::default());
838        let t = now();
839
840        // Start chord with Ctrl+K
841        gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
842
843        // Plain key (no modifier) — clears chord
844        gr.process(&key_press(KeyCode::Char('x'), Modifiers::NONE), t + MS_50);
845
846        // Now Ctrl+C — should not form a chord with Ctrl+K
847        let events = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_100);
848        assert!(events.is_empty()); // Only one key in new chord buffer
849    }
850
851    #[test]
852    fn escape_clears_chord() {
853        let mut gr = GestureRecognizer::new(GestureConfig::default());
854        let t = now();
855
856        gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
857        gr.process(&esc(), t + MS_50);
858
859        // Chord buffer should be cleared
860        let events = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_100);
861        assert!(events.is_empty());
862    }
863
864    // --- Focus loss tests ---
865
866    #[test]
867    fn focus_loss_cancels_drag() {
868        let mut gr = GestureRecognizer::new(GestureConfig::default());
869        let t = now();
870
871        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
872        gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
873        assert!(gr.is_dragging());
874
875        let events = gr.process(&Event::Focus(false), t + MS_100);
876        assert_eq!(events.len(), 1);
877        assert!(matches!(events[0], SemanticEvent::DragCancel));
878        assert!(!gr.is_dragging());
879    }
880
881    #[test]
882    fn focus_loss_without_drag_is_silent() {
883        let mut gr = GestureRecognizer::new(GestureConfig::default());
884        let t = now();
885
886        let events = gr.process(&Event::Focus(false), t);
887        assert!(events.is_empty());
888    }
889
890    // --- Reset tests ---
891
892    #[test]
893    fn reset_clears_all_state() {
894        let mut gr = GestureRecognizer::new(GestureConfig::default());
895        let t = now();
896
897        // Build up state
898        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
899        gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
900        gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t + MS_100);
901
902        assert!(gr.is_dragging());
903
904        gr.reset();
905
906        assert!(!gr.is_dragging());
907        assert!(gr.last_click.is_none());
908        assert!(gr.mouse_down.is_none());
909        assert!(gr.drag.is_none());
910        assert!(gr.chord_buffer.is_empty());
911        assert!(gr.chord_start.is_none());
912    }
913
914    // --- Edge cases ---
915
916    #[test]
917    fn quadruple_click_wraps_to_single() {
918        let mut gr = GestureRecognizer::new(GestureConfig::default());
919        let t = now();
920
921        // Click 1-3
922        for i in 0..3u32 {
923            let offset = Duration::from_millis(i as u64 * 80);
924            gr.process(&mouse_down(5, 5, MouseButton::Left), t + offset);
925            gr.process(&mouse_up(5, 5, MouseButton::Left), t + offset + MS_50);
926        }
927
928        // Click 4 — should wrap back to single click (count capped at 3)
929        gr.process(
930            &mouse_down(5, 5, MouseButton::Left),
931            t + Duration::from_millis(260),
932        );
933        let events = gr.process(
934            &mouse_up(5, 5, MouseButton::Left),
935            t + Duration::from_millis(280),
936        );
937
938        assert_eq!(events.len(), 1);
939        assert!(matches!(events[0], SemanticEvent::Click { .. }));
940    }
941
942    #[test]
943    fn key_release_ignored() {
944        let mut gr = GestureRecognizer::new(GestureConfig::default());
945        let t = now();
946
947        let events = gr.process(
948            &Event::Key(KeyEvent {
949                code: KeyCode::Char('k'),
950                modifiers: Modifiers::CTRL,
951                kind: KeyEventKind::Release,
952            }),
953            t,
954        );
955        assert!(events.is_empty());
956    }
957
958    #[test]
959    fn debug_format() {
960        let gr = GestureRecognizer::new(GestureConfig::default());
961        let dbg = format!("{:?}", gr);
962        assert!(dbg.contains("GestureRecognizer"));
963    }
964
965    // --- Additional click tests (bd-950w) ---
966
967    #[test]
968    fn right_click() {
969        let mut gr = GestureRecognizer::new(GestureConfig::default());
970        let t = now();
971
972        gr.process(&mouse_down(10, 10, MouseButton::Right), t);
973        let events = gr.process(&mouse_up(10, 10, MouseButton::Right), t + MS_50);
974
975        assert_eq!(events.len(), 1);
976        assert!(matches!(
977            events[0],
978            SemanticEvent::Click {
979                button: MouseButton::Right,
980                ..
981            }
982        ));
983    }
984
985    #[test]
986    fn middle_click() {
987        let mut gr = GestureRecognizer::new(GestureConfig::default());
988        let t = now();
989
990        gr.process(&mouse_down(10, 10, MouseButton::Middle), t);
991        let events = gr.process(&mouse_up(10, 10, MouseButton::Middle), t + MS_50);
992
993        assert_eq!(events.len(), 1);
994        assert!(matches!(
995            events[0],
996            SemanticEvent::Click {
997                button: MouseButton::Middle,
998                ..
999            }
1000        ));
1001    }
1002
1003    #[test]
1004    fn click_at_origin() {
1005        let mut gr = GestureRecognizer::new(GestureConfig::default());
1006        let t = now();
1007
1008        gr.process(&mouse_down(0, 0, MouseButton::Left), t);
1009        let events = gr.process(&mouse_up(0, 0, MouseButton::Left), t + MS_50);
1010
1011        assert_eq!(events.len(), 1);
1012        if let SemanticEvent::Click { pos, .. } = &events[0] {
1013            assert_eq!(pos.x, 0);
1014            assert_eq!(pos.y, 0);
1015        }
1016    }
1017
1018    #[test]
1019    fn click_at_max_position() {
1020        let mut gr = GestureRecognizer::new(GestureConfig::default());
1021        let t = now();
1022
1023        gr.process(&mouse_down(u16::MAX, u16::MAX, MouseButton::Left), t);
1024        let events = gr.process(&mouse_up(u16::MAX, u16::MAX, MouseButton::Left), t + MS_50);
1025
1026        assert_eq!(events.len(), 1);
1027        if let SemanticEvent::Click { pos, .. } = &events[0] {
1028            assert_eq!(pos.x, u16::MAX);
1029            assert_eq!(pos.y, u16::MAX);
1030        }
1031    }
1032
1033    #[test]
1034    fn double_click_right_button() {
1035        let mut gr = GestureRecognizer::new(GestureConfig::default());
1036        let t = now();
1037
1038        gr.process(&mouse_down(5, 5, MouseButton::Right), t);
1039        gr.process(&mouse_up(5, 5, MouseButton::Right), t + MS_50);
1040
1041        gr.process(&mouse_down(5, 5, MouseButton::Right), t + MS_100);
1042        let events = gr.process(&mouse_up(5, 5, MouseButton::Right), t + MS_200);
1043
1044        assert_eq!(events.len(), 1);
1045        assert!(matches!(
1046            events[0],
1047            SemanticEvent::DoubleClick {
1048                button: MouseButton::Right,
1049                ..
1050            }
1051        ));
1052    }
1053
1054    #[test]
1055    fn click_position_beyond_tolerance() {
1056        let mut gr = GestureRecognizer::new(GestureConfig::default());
1057        let t = now();
1058
1059        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1060        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1061
1062        // Second click at (8, 5) — manhattan distance 3, beyond tolerance of 1
1063        gr.process(&mouse_down(8, 5, MouseButton::Left), t + MS_100);
1064        let events = gr.process(&mouse_up(8, 5, MouseButton::Left), t + MS_200);
1065
1066        assert_eq!(events.len(), 1);
1067        assert!(matches!(events[0], SemanticEvent::Click { .. }));
1068    }
1069
1070    // --- Additional drag tests (bd-950w) ---
1071
1072    #[test]
1073    fn drag_with_right_button() {
1074        let mut gr = GestureRecognizer::new(GestureConfig::default());
1075        let t = now();
1076
1077        gr.process(&mouse_down(5, 5, MouseButton::Right), t);
1078        let events = gr.process(&mouse_drag(10, 5, MouseButton::Right), t + MS_50);
1079
1080        assert!(events.iter().any(|e| matches!(
1081            e,
1082            SemanticEvent::DragStart {
1083                button: MouseButton::Right,
1084                ..
1085            }
1086        )));
1087    }
1088
1089    #[test]
1090    fn drag_vertical() {
1091        let mut gr = GestureRecognizer::new(GestureConfig::default());
1092        let t = now();
1093
1094        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1095        let events = gr.process(&mouse_drag(5, 10, MouseButton::Left), t + MS_50);
1096
1097        assert!(
1098            events
1099                .iter()
1100                .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1101        );
1102        let drag_move = events
1103            .iter()
1104            .find(|e| matches!(e, SemanticEvent::DragMove { .. }));
1105        if let Some(SemanticEvent::DragMove { delta, .. }) = drag_move {
1106            assert_eq!(delta.0, 0); // no horizontal movement
1107            assert_eq!(delta.1, 5); // 5 cells down
1108        }
1109    }
1110
1111    #[test]
1112    fn drag_multiple_moves() {
1113        let mut gr = GestureRecognizer::new(GestureConfig::default());
1114        let t = now();
1115
1116        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1117        gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
1118
1119        // Second move
1120        let events = gr.process(&mouse_drag(15, 5, MouseButton::Left), t + MS_100);
1121        let drag_move = events
1122            .iter()
1123            .find(|e| matches!(e, SemanticEvent::DragMove { .. }));
1124        if let Some(SemanticEvent::DragMove {
1125            start,
1126            current,
1127            delta,
1128        }) = drag_move
1129        {
1130            assert_eq!(*start, Position::new(5, 5));
1131            assert_eq!(*current, Position::new(15, 5));
1132            assert_eq!(*delta, (5, 0)); // delta from last position (10,5) to (15,5)
1133        }
1134    }
1135
1136    #[test]
1137    fn drag_threshold_exactly_met() {
1138        let mut gr = GestureRecognizer::new(GestureConfig::default());
1139        let t = now();
1140
1141        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1142        // Manhattan distance = 3 (exactly at threshold)
1143        let events = gr.process(&mouse_drag(8, 5, MouseButton::Left), t + MS_50);
1144
1145        assert!(
1146            events
1147                .iter()
1148                .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1149        );
1150    }
1151
1152    #[test]
1153    fn drag_threshold_one_below() {
1154        let mut gr = GestureRecognizer::new(GestureConfig::default());
1155        let t = now();
1156
1157        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1158        // Manhattan distance = 2 (below threshold of 3)
1159        let events = gr.process(&mouse_drag(7, 5, MouseButton::Left), t + MS_50);
1160
1161        assert!(
1162            !events
1163                .iter()
1164                .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1165        );
1166        assert!(!gr.is_dragging());
1167    }
1168
1169    #[test]
1170    fn drag_state_reset_after_end() {
1171        let mut gr = GestureRecognizer::new(GestureConfig::default());
1172        let t = now();
1173
1174        // Complete a drag
1175        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1176        gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
1177        gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_100);
1178
1179        assert!(!gr.is_dragging());
1180
1181        // New mouse down should start fresh
1182        gr.process(&mouse_down(20, 20, MouseButton::Left), t + MS_200);
1183        let events = gr.process(
1184            &mouse_up(20, 20, MouseButton::Left),
1185            t + Duration::from_millis(250),
1186        );
1187
1188        // Should be a click, not a drag
1189        assert_eq!(events.len(), 1);
1190        assert!(matches!(events[0], SemanticEvent::Click { .. }));
1191    }
1192
1193    #[test]
1194    fn no_click_after_drag_cancel() {
1195        let mut gr = GestureRecognizer::new(GestureConfig::default());
1196        let t = now();
1197
1198        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1199        gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
1200        gr.process(&esc(), t + MS_100); // Cancel
1201
1202        // Mouse up after cancel — should not produce click or DragEnd
1203        let events = gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_200);
1204        // Click is emitted because drag state was cleared by cancel
1205        // This is expected: after Escape cancels the drag, mouse up is treated as a fresh interaction
1206        assert!(
1207            !events
1208                .iter()
1209                .any(|e| matches!(e, SemanticEvent::DragEnd { .. }))
1210        );
1211    }
1212
1213    // --- Additional long press tests (bd-950w) ---
1214
1215    #[test]
1216    fn long_press_correct_position() {
1217        let mut gr = GestureRecognizer::new(GestureConfig::default());
1218        let t = now();
1219
1220        gr.process(&mouse_down(42, 17, MouseButton::Left), t);
1221        let lp = gr.check_long_press(t + MS_600);
1222
1223        if let Some(SemanticEvent::LongPress { pos, .. }) = lp {
1224            assert_eq!(pos, Position::new(42, 17));
1225        } else {
1226            panic!("Expected LongPress");
1227        }
1228    }
1229
1230    #[test]
1231    fn long_press_with_custom_threshold() {
1232        let config = GestureConfig {
1233            long_press_threshold: Duration::from_millis(200),
1234            ..Default::default()
1235        };
1236        let mut gr = GestureRecognizer::new(config);
1237        let t = now();
1238
1239        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1240
1241        // Should not fire at 150ms
1242        assert!(
1243            gr.check_long_press(t + Duration::from_millis(150))
1244                .is_none()
1245        );
1246
1247        // Should fire at 250ms (past 200ms threshold)
1248        assert!(
1249            gr.check_long_press(t + Duration::from_millis(250))
1250                .is_some()
1251        );
1252    }
1253
1254    #[test]
1255    fn long_press_resets_on_new_mouse_down() {
1256        let mut gr = GestureRecognizer::new(GestureConfig::default());
1257        let t = now();
1258
1259        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1260        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
1261
1262        // New mouse down resets long press timer
1263        gr.process(&mouse_down(10, 10, MouseButton::Left), t + MS_200);
1264
1265        // Should not fire based on old timer
1266        assert!(gr.check_long_press(t + MS_600).is_none());
1267
1268        // Should fire based on new timer (200ms + 500ms = 700ms)
1269        assert!(
1270            gr.check_long_press(t + Duration::from_millis(750))
1271                .is_some()
1272        );
1273    }
1274
1275    // --- Additional chord tests (bd-950w) ---
1276
1277    #[test]
1278    fn alt_key_chord() {
1279        let mut gr = GestureRecognizer::new(GestureConfig::default());
1280        let t = now();
1281
1282        gr.process(&key_press(KeyCode::Char('x'), Modifiers::ALT), t);
1283        let events = gr.process(&key_press(KeyCode::Char('y'), Modifiers::ALT), t + MS_100);
1284
1285        assert_eq!(events.len(), 1);
1286        if let SemanticEvent::Chord { sequence } = &events[0] {
1287            assert_eq!(sequence.len(), 2);
1288            assert!(sequence[0].modifiers.contains(Modifiers::ALT));
1289        }
1290    }
1291
1292    #[test]
1293    fn mixed_modifier_chord() {
1294        let mut gr = GestureRecognizer::new(GestureConfig::default());
1295        let t = now();
1296
1297        gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
1298        let events = gr.process(&key_press(KeyCode::Char('d'), Modifiers::ALT), t + MS_100);
1299
1300        assert_eq!(events.len(), 1);
1301        if let SemanticEvent::Chord { sequence } = &events[0] {
1302            assert_eq!(sequence[0].modifiers, Modifiers::CTRL);
1303            assert_eq!(sequence[1].modifiers, Modifiers::ALT);
1304        }
1305    }
1306
1307    #[test]
1308    fn chord_with_function_key() {
1309        let mut gr = GestureRecognizer::new(GestureConfig::default());
1310        let t = now();
1311
1312        gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
1313        let events = gr.process(&key_press(KeyCode::F(1), Modifiers::CTRL), t + MS_100);
1314
1315        assert_eq!(events.len(), 1);
1316        if let SemanticEvent::Chord { sequence } = &events[0] {
1317            assert_eq!(sequence[1].code, KeyCode::F(1));
1318        }
1319    }
1320
1321    #[test]
1322    fn single_modifier_key_no_chord_emitted() {
1323        let mut gr = GestureRecognizer::new(GestureConfig::default());
1324        let t = now();
1325
1326        // Single Ctrl+K does not emit a chord (needs ≥2 keys)
1327        let events = gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
1328        assert!(events.is_empty());
1329    }
1330
1331    // --- Config tests (bd-950w) ---
1332
1333    #[test]
1334    fn custom_click_tolerance() {
1335        let config = GestureConfig {
1336            click_tolerance: 5,
1337            ..Default::default()
1338        };
1339        let mut gr = GestureRecognizer::new(config);
1340        let t = now();
1341
1342        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1343        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1344
1345        // Click at (10, 5) — manhattan distance 5, within tolerance of 5
1346        gr.process(&mouse_down(10, 5, MouseButton::Left), t + MS_100);
1347        let events = gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_200);
1348
1349        assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
1350    }
1351
1352    #[test]
1353    fn custom_drag_threshold() {
1354        let config = GestureConfig {
1355            drag_threshold: 10,
1356            ..Default::default()
1357        };
1358        let mut gr = GestureRecognizer::new(config);
1359        let t = now();
1360
1361        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1362        // Move 5 cells — below new threshold of 10
1363        let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
1364        assert!(!gr.is_dragging());
1365        assert!(events.is_empty());
1366
1367        // Move 10 cells — at threshold
1368        let events = gr.process(&mouse_drag(15, 5, MouseButton::Left), t + MS_100);
1369        assert!(gr.is_dragging());
1370        assert!(
1371            events
1372                .iter()
1373                .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1374        );
1375    }
1376
1377    #[test]
1378    fn custom_multi_click_timeout() {
1379        let config = GestureConfig {
1380            multi_click_timeout: Duration::from_millis(100),
1381            ..Default::default()
1382        };
1383        let mut gr = GestureRecognizer::new(config);
1384        let t = now();
1385
1386        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1387        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1388
1389        // Second click at 150ms — beyond 100ms timeout
1390        gr.process(
1391            &mouse_down(5, 5, MouseButton::Left),
1392            t + Duration::from_millis(150),
1393        );
1394        let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_200);
1395
1396        assert!(matches!(events[0], SemanticEvent::Click { .. }));
1397    }
1398
1399    #[test]
1400    fn config_getter_and_setter() {
1401        let mut gr = GestureRecognizer::new(GestureConfig::default());
1402
1403        assert_eq!(gr.config().drag_threshold, 3);
1404
1405        let new_config = GestureConfig {
1406            drag_threshold: 10,
1407            ..Default::default()
1408        };
1409        gr.set_config(new_config);
1410
1411        assert_eq!(gr.config().drag_threshold, 10);
1412    }
1413
1414    // --- Integration / sequence tests (bd-950w) ---
1415
1416    #[test]
1417    fn click_then_drag_are_independent() {
1418        let mut gr = GestureRecognizer::new(GestureConfig::default());
1419        let t = now();
1420
1421        // Click
1422        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1423        let click_events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1424        assert!(matches!(click_events[0], SemanticEvent::Click { .. }));
1425
1426        // Drag
1427        gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_200);
1428        let drag_events = gr.process(
1429            &mouse_drag(10, 5, MouseButton::Left),
1430            t + Duration::from_millis(250),
1431        );
1432        assert!(
1433            drag_events
1434                .iter()
1435                .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1436        );
1437
1438        let end_events = gr.process(
1439            &mouse_up(10, 5, MouseButton::Left),
1440            t + Duration::from_millis(300),
1441        );
1442        assert!(matches!(end_events[0], SemanticEvent::DragEnd { .. }));
1443    }
1444
1445    #[test]
1446    fn interleaved_mouse_and_keyboard() {
1447        let mut gr = GestureRecognizer::new(GestureConfig::default());
1448        let t = now();
1449
1450        // Start typing a chord
1451        gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
1452
1453        // Mouse click in between
1454        gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_50);
1455        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
1456
1457        // Continue chord — chord buffer was not cleared by mouse events
1458        let events = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_200);
1459
1460        assert_eq!(events.len(), 1);
1461        assert!(matches!(events[0], SemanticEvent::Chord { .. }));
1462    }
1463
1464    #[test]
1465    fn rapid_clicks_produce_correct_sequence() {
1466        let mut gr = GestureRecognizer::new(GestureConfig::default());
1467        let t = now();
1468        let mut results = Vec::new();
1469
1470        for i in 0..5u32 {
1471            let offset = Duration::from_millis(i as u64 * 60);
1472            gr.process(&mouse_down(5, 5, MouseButton::Left), t + offset);
1473            let events = gr.process(
1474                &mouse_up(5, 5, MouseButton::Left),
1475                t + offset + Duration::from_millis(30),
1476            );
1477            results.extend(events);
1478        }
1479
1480        // Should see: Click, DoubleClick, TripleClick, Click (wrap), DoubleClick
1481        assert!(results.len() == 5);
1482        assert!(matches!(results[0], SemanticEvent::Click { .. }));
1483        assert!(matches!(results[1], SemanticEvent::DoubleClick { .. }));
1484        assert!(matches!(results[2], SemanticEvent::TripleClick { .. }));
1485        assert!(matches!(results[3], SemanticEvent::Click { .. }));
1486        assert!(matches!(results[4], SemanticEvent::DoubleClick { .. }));
1487    }
1488
1489    #[test]
1490    fn tick_event_ignored() {
1491        let mut gr = GestureRecognizer::new(GestureConfig::default());
1492        let t = now();
1493
1494        let events = gr.process(&Event::Tick, t);
1495        assert!(events.is_empty());
1496    }
1497
1498    #[test]
1499    fn resize_event_ignored() {
1500        let mut gr = GestureRecognizer::new(GestureConfig::default());
1501        let t = now();
1502
1503        let events = gr.process(
1504            &Event::Resize {
1505                width: 80,
1506                height: 24,
1507            },
1508            t,
1509        );
1510        assert!(events.is_empty());
1511    }
1512
1513    #[test]
1514    fn focus_gain_ignored() {
1515        let mut gr = GestureRecognizer::new(GestureConfig::default());
1516        let t = now();
1517
1518        let events = gr.process(&Event::Focus(true), t);
1519        assert!(events.is_empty());
1520    }
1521
1522    #[test]
1523    fn default_config_values() {
1524        let config = GestureConfig::default();
1525        assert_eq!(config.multi_click_timeout, Duration::from_millis(300));
1526        assert_eq!(config.long_press_threshold, Duration::from_millis(500));
1527        assert_eq!(config.drag_threshold, 3);
1528        assert_eq!(config.chord_timeout, Duration::from_millis(1000));
1529        assert_eq!(config.click_tolerance, 1);
1530    }
1531
1532    // --- Edge-case tests (bd-3kkqc) ---
1533
1534    fn mouse_moved(x: u16, y: u16) -> Event {
1535        Event::Mouse(MouseEvent {
1536            kind: MouseEventKind::Moved,
1537            x,
1538            y,
1539            modifiers: Modifiers::NONE,
1540        })
1541    }
1542
1543    fn mouse_scroll_up(x: u16, y: u16) -> Event {
1544        Event::Mouse(MouseEvent {
1545            kind: MouseEventKind::ScrollUp,
1546            x,
1547            y,
1548            modifiers: Modifiers::NONE,
1549        })
1550    }
1551
1552    fn mouse_scroll_down(x: u16, y: u16) -> Event {
1553        Event::Mouse(MouseEvent {
1554            kind: MouseEventKind::ScrollDown,
1555            x,
1556            y,
1557            modifiers: Modifiers::NONE,
1558        })
1559    }
1560
1561    #[test]
1562    fn drag_without_prior_mouse_down() {
1563        // Drag event arrives with no prior mouse-down: creates tracker on-the-fly
1564        let mut gr = GestureRecognizer::new(GestureConfig::default());
1565        let t = now();
1566
1567        let events = gr.process(&mouse_drag(10, 10, MouseButton::Left), t);
1568        assert!(events.is_empty()); // First drag just creates tracker, no DragStart
1569        assert!(!gr.is_dragging());
1570
1571        // Second drag — might cross threshold from (10,10) origin
1572        let events = gr.process(&mouse_drag(20, 10, MouseButton::Left), t + MS_50);
1573        assert!(
1574            events
1575                .iter()
1576                .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1577        );
1578        assert!(gr.is_dragging());
1579    }
1580
1581    #[test]
1582    fn mouse_moved_cancels_long_press() {
1583        let mut gr = GestureRecognizer::new(GestureConfig::default());
1584        let t = now();
1585
1586        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1587
1588        // Moved event (no button held in terminal terms) cancels long press
1589        gr.process(&mouse_moved(6, 5), t + MS_100);
1590
1591        // Long press should NOT fire
1592        assert!(gr.check_long_press(t + MS_600).is_none());
1593    }
1594
1595    #[test]
1596    fn mouse_moved_does_not_affect_drag_tracker() {
1597        let mut gr = GestureRecognizer::new(GestureConfig::default());
1598        let t = now();
1599
1600        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1601
1602        // Moved event — should not start or cancel drag
1603        let events = gr.process(&mouse_moved(20, 20), t + MS_50);
1604        assert!(events.is_empty());
1605
1606        // Drag event should still work against the original mouse-down position
1607        let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_100);
1608        assert!(
1609            events
1610                .iter()
1611                .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1612        );
1613    }
1614
1615    #[test]
1616    fn escape_during_unstarted_drag_no_cancel() {
1617        // Drag tracker exists but started=false → Escape should NOT emit DragCancel
1618        let mut gr = GestureRecognizer::new(GestureConfig::default());
1619        let t = now();
1620
1621        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1622        // Small move — below threshold, drag not started
1623        gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_50);
1624        assert!(!gr.is_dragging());
1625
1626        let events = gr.process(&esc(), t + MS_100);
1627        assert!(events.is_empty()); // No DragCancel because drag wasn't started
1628    }
1629
1630    #[test]
1631    fn focus_loss_during_unstarted_drag_no_cancel() {
1632        // Drag tracker exists but started=false → Focus(false) should NOT emit DragCancel
1633        let mut gr = GestureRecognizer::new(GestureConfig::default());
1634        let t = now();
1635
1636        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1637        // Below threshold
1638        gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_50);
1639        assert!(!gr.is_dragging());
1640
1641        let events = gr.process(&Event::Focus(false), t + MS_100);
1642        assert!(events.is_empty()); // No DragCancel
1643    }
1644
1645    #[test]
1646    fn chord_with_super_modifier() {
1647        let mut gr = GestureRecognizer::new(GestureConfig::default());
1648        let t = now();
1649
1650        gr.process(&key_press(KeyCode::Char('a'), Modifiers::SUPER), t);
1651        let events = gr.process(&key_press(KeyCode::Char('b'), Modifiers::SUPER), t + MS_100);
1652
1653        assert_eq!(events.len(), 1);
1654        if let SemanticEvent::Chord { sequence } = &events[0] {
1655            assert_eq!(sequence.len(), 2);
1656            assert!(sequence[0].modifiers.contains(Modifiers::SUPER));
1657            assert!(sequence[1].modifiers.contains(Modifiers::SUPER));
1658        } else {
1659            panic!("Expected Chord event");
1660        }
1661    }
1662
1663    #[test]
1664    fn long_press_exactly_at_threshold() {
1665        let mut gr = GestureRecognizer::new(GestureConfig::default());
1666        let t = now();
1667
1668        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1669
1670        // Exactly at 500ms threshold (elapsed == threshold → should fire due to >=)
1671        let lp = gr.check_long_press(t + Duration::from_millis(500));
1672        assert!(lp.is_some());
1673        if let Some(SemanticEvent::LongPress { duration, .. }) = lp {
1674            assert_eq!(duration, Duration::from_millis(500));
1675        }
1676    }
1677
1678    #[test]
1679    fn long_press_one_ms_before_threshold() {
1680        let mut gr = GestureRecognizer::new(GestureConfig::default());
1681        let t = now();
1682
1683        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1684
1685        // 1ms before threshold
1686        assert!(
1687            gr.check_long_press(t + Duration::from_millis(499))
1688                .is_none()
1689        );
1690    }
1691
1692    #[test]
1693    fn drag_negative_direction() {
1694        // Drag from right to left (negative x delta)
1695        let mut gr = GestureRecognizer::new(GestureConfig::default());
1696        let t = now();
1697
1698        gr.process(&mouse_down(20, 10, MouseButton::Left), t);
1699        let events = gr.process(&mouse_drag(15, 10, MouseButton::Left), t + MS_50);
1700
1701        let drag_move = events
1702            .iter()
1703            .find(|e| matches!(e, SemanticEvent::DragMove { .. }));
1704        if let Some(SemanticEvent::DragMove { delta, .. }) = drag_move {
1705            assert_eq!(delta.0, -5); // Negative x
1706            assert_eq!(delta.1, 0);
1707        } else {
1708            panic!("Expected DragMove");
1709        }
1710    }
1711
1712    #[test]
1713    fn drag_negative_y_direction() {
1714        // Drag upward (negative y delta)
1715        let mut gr = GestureRecognizer::new(GestureConfig::default());
1716        let t = now();
1717
1718        gr.process(&mouse_down(5, 20, MouseButton::Left), t);
1719        let events = gr.process(&mouse_drag(5, 15, MouseButton::Left), t + MS_50);
1720
1721        let drag_move = events
1722            .iter()
1723            .find(|e| matches!(e, SemanticEvent::DragMove { .. }));
1724        if let Some(SemanticEvent::DragMove { delta, .. }) = drag_move {
1725            assert_eq!(delta.0, 0);
1726            assert_eq!(delta.1, -5); // Negative y
1727        } else {
1728            panic!("Expected DragMove");
1729        }
1730    }
1731
1732    #[test]
1733    fn multiple_sequential_drags() {
1734        let mut gr = GestureRecognizer::new(GestureConfig::default());
1735        let t = now();
1736
1737        // First drag
1738        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1739        gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
1740        gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_100);
1741        assert!(!gr.is_dragging());
1742
1743        // Second drag immediately after
1744        gr.process(&mouse_down(20, 20, MouseButton::Left), t + MS_200);
1745        let events = gr.process(
1746            &mouse_drag(30, 20, MouseButton::Left),
1747            t + Duration::from_millis(250),
1748        );
1749
1750        assert!(events.iter().any(|e| matches!(
1751            e,
1752            SemanticEvent::DragStart {
1753                pos: Position { x: 20, y: 20 },
1754                ..
1755            }
1756        )));
1757        assert!(gr.is_dragging());
1758
1759        let events = gr.process(
1760            &mouse_up(30, 20, MouseButton::Left),
1761            t + Duration::from_millis(300),
1762        );
1763        assert!(matches!(
1764            events[0],
1765            SemanticEvent::DragEnd {
1766                start: Position { x: 20, y: 20 },
1767                end: Position { x: 30, y: 20 },
1768            }
1769        ));
1770    }
1771
1772    #[test]
1773    fn check_long_press_no_mouse_down() {
1774        // check_long_press when no mouse-down has occurred
1775        let mut gr = GestureRecognizer::new(GestureConfig::default());
1776        let t = now();
1777
1778        assert!(gr.check_long_press(t).is_none());
1779        assert!(gr.check_long_press(t + Duration::from_secs(10)).is_none());
1780    }
1781
1782    #[test]
1783    fn zero_drag_threshold() {
1784        // Any movement should trigger drag immediately
1785        let config = GestureConfig {
1786            drag_threshold: 0,
1787            ..Default::default()
1788        };
1789        let mut gr = GestureRecognizer::new(config);
1790        let t = now();
1791
1792        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1793        // Even a 0-distance drag should start (0 >= 0)
1794        let events = gr.process(&mouse_drag(5, 5, MouseButton::Left), t + MS_50);
1795        assert!(
1796            events
1797                .iter()
1798                .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1799        );
1800        assert!(gr.is_dragging());
1801    }
1802
1803    #[test]
1804    fn zero_click_tolerance() {
1805        // Only exact same position counts for multi-click
1806        let config = GestureConfig {
1807            click_tolerance: 0,
1808            ..Default::default()
1809        };
1810        let mut gr = GestureRecognizer::new(config);
1811        let t = now();
1812
1813        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1814        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1815
1816        // Second click 1 cell away — should NOT be double click with tolerance 0
1817        gr.process(&mouse_down(6, 5, MouseButton::Left), t + MS_100);
1818        let events = gr.process(&mouse_up(6, 5, MouseButton::Left), t + MS_200);
1819        assert!(matches!(events[0], SemanticEvent::Click { .. }));
1820
1821        // But exact same position should double-click
1822        gr.process(
1823            &mouse_down(6, 5, MouseButton::Left),
1824            t + Duration::from_millis(250),
1825        );
1826        let events = gr.process(
1827            &mouse_up(6, 5, MouseButton::Left),
1828            t + Duration::from_millis(280),
1829        );
1830        assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
1831    }
1832
1833    #[test]
1834    fn scroll_events_ignored() {
1835        let mut gr = GestureRecognizer::new(GestureConfig::default());
1836        let t = now();
1837
1838        let events = gr.process(&mouse_scroll_up(5, 5), t);
1839        assert!(events.is_empty());
1840
1841        let events = gr.process(&mouse_scroll_down(5, 5), t + MS_50);
1842        assert!(events.is_empty());
1843    }
1844
1845    #[test]
1846    fn key_repeat_ignored() {
1847        let mut gr = GestureRecognizer::new(GestureConfig::default());
1848        let t = now();
1849
1850        let events = gr.process(
1851            &Event::Key(KeyEvent {
1852                code: KeyCode::Char('k'),
1853                modifiers: Modifiers::CTRL,
1854                kind: KeyEventKind::Repeat,
1855            }),
1856            t,
1857        );
1858        assert!(events.is_empty());
1859    }
1860
1861    #[test]
1862    fn double_click_at_exact_timeout_boundary() {
1863        // Multi-click timeout uses <=, so exactly at boundary should still count
1864        let mut gr = GestureRecognizer::new(GestureConfig::default());
1865        let t = now();
1866
1867        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1868        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1869
1870        // Second click exactly at 300ms after first mouse-up (350ms total)
1871        // The check is: now.duration_since(last.time) <= 300ms
1872        // last.time = t + 50ms, now = t + 350ms → 300ms <= 300ms → true
1873        gr.process(
1874            &mouse_down(5, 5, MouseButton::Left),
1875            t + Duration::from_millis(350),
1876        );
1877        let events = gr.process(
1878            &mouse_up(5, 5, MouseButton::Left),
1879            t + Duration::from_millis(350),
1880        );
1881        assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
1882    }
1883
1884    #[test]
1885    fn mouse_up_position_becomes_click_position() {
1886        // Click position comes from mouse-up, not mouse-down
1887        let mut gr = GestureRecognizer::new(GestureConfig::default());
1888        let t = now();
1889
1890        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1891        // Mouse up at slightly different position (no drag because no Drag event)
1892        let events = gr.process(&mouse_up(6, 6, MouseButton::Left), t + MS_50);
1893
1894        assert_eq!(events.len(), 1);
1895        if let SemanticEvent::Click { pos, .. } = &events[0] {
1896            assert_eq!(*pos, Position::new(6, 6)); // Uses up position, not down
1897        } else {
1898            panic!("Expected Click");
1899        }
1900    }
1901
1902    #[test]
1903    fn multiple_mouse_downs_overwrite_state() {
1904        // Two mouse-downs without mouse-up: second overwrites the first
1905        let mut gr = GestureRecognizer::new(GestureConfig::default());
1906        let t = now();
1907
1908        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1909        gr.process(&mouse_down(20, 20, MouseButton::Right), t + MS_50);
1910
1911        // Drag should track from second position
1912        let events = gr.process(&mouse_drag(30, 20, MouseButton::Right), t + MS_100);
1913        if let Some(SemanticEvent::DragStart { pos, button }) = events
1914            .iter()
1915            .find(|e| matches!(e, SemanticEvent::DragStart { .. }))
1916        {
1917            assert_eq!(*pos, Position::new(20, 20));
1918            assert_eq!(*button, MouseButton::Right);
1919        } else {
1920            panic!("Expected DragStart from second mouse-down position");
1921        }
1922    }
1923
1924    #[test]
1925    fn triple_click_then_timeout_then_single() {
1926        let mut gr = GestureRecognizer::new(GestureConfig::default());
1927        let t = now();
1928
1929        // Click 1, 2, 3
1930        for i in 0..3u32 {
1931            let offset = Duration::from_millis(i as u64 * 80);
1932            gr.process(&mouse_down(5, 5, MouseButton::Left), t + offset);
1933            gr.process(&mouse_up(5, 5, MouseButton::Left), t + offset + MS_50);
1934        }
1935
1936        // Wait well past timeout, then click again
1937        let late = t + Duration::from_secs(2);
1938        gr.process(&mouse_down(5, 5, MouseButton::Left), late);
1939        let events = gr.process(&mouse_up(5, 5, MouseButton::Left), late + MS_50);
1940
1941        assert_eq!(events.len(), 1);
1942        assert!(matches!(events[0], SemanticEvent::Click { .. }));
1943    }
1944
1945    #[test]
1946    fn chord_ctrl_alt_combined_modifier() {
1947        // Single key with both CTRL+ALT should start chord accumulation
1948        let mut gr = GestureRecognizer::new(GestureConfig::default());
1949        let t = now();
1950
1951        let mods = Modifiers::CTRL | Modifiers::ALT;
1952        gr.process(&key_press(KeyCode::Char('a'), mods), t);
1953        let events = gr.process(&key_press(KeyCode::Char('b'), Modifiers::CTRL), t + MS_100);
1954
1955        assert_eq!(events.len(), 1);
1956        if let SemanticEvent::Chord { sequence } = &events[0] {
1957            assert_eq!(sequence[0].modifiers, Modifiers::CTRL | Modifiers::ALT);
1958            assert_eq!(sequence[1].modifiers, Modifiers::CTRL);
1959        }
1960    }
1961
1962    #[test]
1963    fn chord_timeout_then_new_chord() {
1964        // After chord timeout expires, a fresh chord can start
1965        let mut gr = GestureRecognizer::new(GestureConfig::default());
1966        let t = now();
1967
1968        // Start chord
1969        gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
1970
1971        // Timeout expires (>1000ms)
1972        // Second key after timeout — starts new chord buffer
1973        gr.process(
1974            &key_press(KeyCode::Char('a'), Modifiers::CTRL),
1975            t + Duration::from_millis(1100),
1976        );
1977        // Third key completes the NEW chord
1978        let events = gr.process(
1979            &key_press(KeyCode::Char('b'), Modifiers::CTRL),
1980            t + Duration::from_millis(1200),
1981        );
1982
1983        assert_eq!(events.len(), 1);
1984        if let SemanticEvent::Chord { sequence } = &events[0] {
1985            assert_eq!(sequence.len(), 2);
1986            assert_eq!(sequence[0].code, KeyCode::Char('a'));
1987            assert_eq!(sequence[1].code, KeyCode::Char('b'));
1988        }
1989    }
1990
1991    #[test]
1992    fn drag_start_and_move_emitted_together() {
1993        // When drag threshold is first crossed, both DragStart and DragMove
1994        // are emitted in the same process() call
1995        let mut gr = GestureRecognizer::new(GestureConfig::default());
1996        let t = now();
1997
1998        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1999        let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
2000
2001        assert_eq!(events.len(), 2);
2002        assert!(matches!(events[0], SemanticEvent::DragStart { .. }));
2003        assert!(matches!(events[1], SemanticEvent::DragMove { .. }));
2004    }
2005
2006    #[test]
2007    fn config_clone() {
2008        let config = GestureConfig {
2009            drag_threshold: 42,
2010            click_tolerance: 7,
2011            ..Default::default()
2012        };
2013        let cloned = config.clone();
2014        assert_eq!(cloned.drag_threshold, 42);
2015        assert_eq!(cloned.click_tolerance, 7);
2016        assert_eq!(cloned.multi_click_timeout, config.multi_click_timeout);
2017    }
2018
2019    #[test]
2020    fn scroll_does_not_affect_click_state() {
2021        let mut gr = GestureRecognizer::new(GestureConfig::default());
2022        let t = now();
2023
2024        // First click
2025        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
2026        gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
2027
2028        // Scroll in between
2029        gr.process(&mouse_scroll_up(5, 5), t + MS_100);
2030
2031        // Second click should still be DoubleClick
2032        gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_200);
2033        let events = gr.process(
2034            &mouse_up(5, 5, MouseButton::Left),
2035            t + Duration::from_millis(250),
2036        );
2037        assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
2038    }
2039
2040    #[test]
2041    fn escape_clears_mouse_down_state() {
2042        let mut gr = GestureRecognizer::new(GestureConfig::default());
2043        let t = now();
2044
2045        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
2046        gr.process(&esc(), t + MS_50);
2047
2048        // Long press should not fire (Escape cleared long_press_pos)
2049        assert!(gr.check_long_press(t + MS_600).is_none());
2050
2051        // Mouse up should produce a click from fresh state (mouse_down is None,
2052        // so no drag → click with fresh count)
2053        let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
2054        assert_eq!(events.len(), 1);
2055        assert!(matches!(events[0], SemanticEvent::Click { .. }));
2056    }
2057
2058    #[test]
2059    fn focus_loss_clears_long_press_fired_flag() {
2060        let mut gr = GestureRecognizer::new(GestureConfig::default());
2061        let t = now();
2062
2063        // Fire a long press
2064        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
2065        assert!(gr.check_long_press(t + MS_600).is_some());
2066        assert!(
2067            gr.check_long_press(t + Duration::from_millis(700))
2068                .is_none()
2069        ); // Already fired
2070
2071        // Focus loss resets long_press_fired
2072        gr.process(&Event::Focus(false), t + Duration::from_millis(800));
2073
2074        // New mouse down + long press should fire again
2075        gr.process(
2076            &mouse_down(10, 10, MouseButton::Left),
2077            t + Duration::from_secs(1),
2078        );
2079        assert!(
2080            gr.check_long_press(t + Duration::from_millis(1600))
2081                .is_some()
2082        );
2083    }
2084
2085    #[test]
2086    fn drag_at_u16_max_coordinates() {
2087        let mut gr = GestureRecognizer::new(GestureConfig::default());
2088        let t = now();
2089
2090        gr.process(&mouse_down(u16::MAX - 5, u16::MAX, MouseButton::Left), t);
2091        let events = gr.process(
2092            &mouse_drag(u16::MAX, u16::MAX, MouseButton::Left),
2093            t + MS_50,
2094        );
2095
2096        // Manhattan distance = 5, above threshold of 3
2097        assert!(
2098            events
2099                .iter()
2100                .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
2101        );
2102
2103        // Delta: u16::MAX - (u16::MAX - 5) = 5
2104        if let Some(SemanticEvent::DragMove { delta, .. }) = events
2105            .iter()
2106            .find(|e| matches!(e, SemanticEvent::DragMove { .. }))
2107        {
2108            assert_eq!(delta.0, 5);
2109            assert_eq!(delta.1, 0);
2110        }
2111    }
2112
2113    #[test]
2114    fn reset_during_long_press_pending() {
2115        let mut gr = GestureRecognizer::new(GestureConfig::default());
2116        let t = now();
2117
2118        gr.process(&mouse_down(5, 5, MouseButton::Left), t);
2119        // Long press is pending but hasn't fired yet
2120        gr.reset();
2121
2122        // Long press should NOT fire after reset
2123        assert!(gr.check_long_press(t + MS_600).is_none());
2124    }
2125}