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