1#![forbid(unsafe_code)]
2
3use web_time::{Duration, Instant};
38
39use crate::event::{Event, KeyCode, KeyEventKind, Modifiers, MouseButton, MouseEventKind};
40use crate::semantic_event::{ChordKey, Position, SemanticEvent};
41
42#[derive(Debug, Clone)]
48pub struct GestureConfig {
49 pub multi_click_timeout: Duration,
51 pub long_press_threshold: Duration,
53 pub drag_threshold: u16,
55 pub chord_timeout: Duration,
57 pub swipe_velocity_threshold: f32,
59 pub click_tolerance: u16,
61}
62
63impl Default for GestureConfig {
64 fn default() -> Self {
65 Self {
66 multi_click_timeout: Duration::from_millis(300),
67 long_press_threshold: Duration::from_millis(500),
68 drag_threshold: 3,
69 chord_timeout: Duration::from_millis(1000),
70 swipe_velocity_threshold: 50.0,
71 click_tolerance: 1,
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
82struct ClickState {
83 pos: Position,
84 button: MouseButton,
85 time: Instant,
86 count: u8,
87}
88
89#[derive(Debug, Clone)]
91struct DragTracker {
92 start_pos: Position,
93 button: MouseButton,
94 last_pos: Position,
95 started: bool,
96}
97
98pub struct GestureRecognizer {
108 config: GestureConfig,
109
110 last_click: Option<ClickState>,
112
113 mouse_down: Option<(Position, MouseButton, Instant)>,
115 drag: Option<DragTracker>,
116
117 long_press_pos: Option<(Position, Instant)>,
119 long_press_fired: bool,
120
121 chord_buffer: Vec<ChordKey>,
123 chord_start: Option<Instant>,
124}
125
126impl std::fmt::Debug for GestureRecognizer {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.debug_struct("GestureRecognizer")
129 .field("dragging", &self.is_dragging())
130 .field("chord_len", &self.chord_buffer.len())
131 .finish()
132 }
133}
134
135impl GestureRecognizer {
136 #[must_use]
138 pub fn new(config: GestureConfig) -> Self {
139 Self {
140 config,
141 last_click: None,
142 mouse_down: None,
143 drag: None,
144 long_press_pos: None,
145 long_press_fired: false,
146 chord_buffer: Vec::with_capacity(4),
147 chord_start: None,
148 }
149 }
150
151 #[inline]
156 pub fn process(&mut self, event: &Event, now: Instant) -> Vec<SemanticEvent> {
157 let mut out = Vec::with_capacity(2);
158
159 self.expire_chord(now);
161
162 match event {
163 Event::Mouse(mouse) => {
164 let pos = Position::new(mouse.x, mouse.y);
165 match mouse.kind {
166 MouseEventKind::Down(button) => {
167 self.on_mouse_down(pos, button, now, &mut out);
168 }
169 MouseEventKind::Up(button) => {
170 self.on_mouse_up(pos, button, now, &mut out);
171 }
172 MouseEventKind::Drag(button) => {
173 self.on_mouse_drag(pos, button, &mut out);
174 }
175 MouseEventKind::Moved => {
176 self.long_press_pos = None;
178 }
179 _ => {}
180 }
181 }
182 Event::Key(key) => {
183 if key.kind != KeyEventKind::Press {
184 return out;
185 }
186
187 if key.code == KeyCode::Escape {
189 if let Some(drag) = self.drag.take()
190 && drag.started
191 {
192 out.push(SemanticEvent::DragCancel);
193 }
194 self.mouse_down = None;
195 self.long_press_pos = None;
196 self.chord_buffer.clear();
197 self.chord_start = None;
198 return out;
199 }
200
201 let has_modifier = key
203 .modifiers
204 .intersects(Modifiers::CTRL | Modifiers::ALT | Modifiers::SUPER);
205
206 if has_modifier {
207 let chord_key = ChordKey::new(key.code, key.modifiers);
208 if self.chord_buffer.is_empty() {
209 self.chord_start = Some(now);
210 }
211 self.chord_buffer.push(chord_key);
212
213 if self.chord_buffer.len() >= 2 {
216 out.push(SemanticEvent::Chord {
217 sequence: self.chord_buffer.clone(),
218 });
219 self.chord_buffer.clear();
220 self.chord_start = None;
221 }
222 } else {
223 self.chord_buffer.clear();
225 self.chord_start = None;
226 }
227 }
228 Event::Focus(false) => {
229 if let Some(drag) = self.drag.take()
231 && drag.started
232 {
233 out.push(SemanticEvent::DragCancel);
234 }
235 self.mouse_down = None;
236 self.long_press_pos = None;
237 self.long_press_fired = false;
238 }
239 _ => {}
240 }
241
242 out
243 }
244
245 #[inline]
250 pub fn check_long_press(&mut self, now: Instant) -> Option<SemanticEvent> {
251 if self.long_press_fired {
252 return None;
253 }
254 if let Some((pos, down_time)) = self.long_press_pos {
255 let elapsed = now.duration_since(down_time);
256 if elapsed >= self.config.long_press_threshold {
257 self.long_press_fired = true;
258 return Some(SemanticEvent::LongPress {
259 pos,
260 duration: elapsed,
261 });
262 }
263 }
264 None
265 }
266
267 #[inline]
269 #[must_use]
270 pub fn is_dragging(&self) -> bool {
271 self.drag.as_ref().is_some_and(|d| d.started)
272 }
273
274 pub fn reset(&mut self) {
276 self.last_click = None;
277 self.mouse_down = None;
278 self.drag = None;
279 self.long_press_pos = None;
280 self.long_press_fired = false;
281 self.chord_buffer.clear();
282 self.chord_start = None;
283 }
284
285 #[inline]
287 #[must_use]
288 pub fn config(&self) -> &GestureConfig {
289 &self.config
290 }
291
292 pub fn set_config(&mut self, config: GestureConfig) {
294 self.config = config;
295 }
296}
297
298impl GestureRecognizer {
303 fn on_mouse_down(
304 &mut self,
305 pos: Position,
306 button: MouseButton,
307 now: Instant,
308 _out: &mut Vec<SemanticEvent>,
309 ) {
310 self.mouse_down = Some((pos, button, now));
311 self.drag = Some(DragTracker {
312 start_pos: pos,
313 button,
314 last_pos: pos,
315 started: false,
316 });
317 self.long_press_pos = Some((pos, now));
318 self.long_press_fired = false;
319 }
320
321 fn on_mouse_up(
322 &mut self,
323 pos: Position,
324 button: MouseButton,
325 now: Instant,
326 out: &mut Vec<SemanticEvent>,
327 ) {
328 self.long_press_pos = None;
329 self.long_press_fired = false;
330
331 if let Some(drag) = self.drag.take()
333 && drag.started
334 {
335 out.push(SemanticEvent::DragEnd {
336 start: drag.start_pos,
337 end: pos,
338 });
339 self.mouse_down = None;
340 return;
341 }
342
343 self.mouse_down = None;
345
346 let click_count = if let Some(ref last) = self.last_click {
348 if last.button == button
349 && last.pos.manhattan_distance(pos) <= u32::from(self.config.click_tolerance)
350 && now.duration_since(last.time) <= self.config.multi_click_timeout
351 && last.count < 3
352 {
353 last.count + 1
354 } else {
355 1
356 }
357 } else {
358 1
359 };
360
361 self.last_click = Some(ClickState {
362 pos,
363 button,
364 time: now,
365 count: click_count,
366 });
367
368 match click_count {
369 1 => out.push(SemanticEvent::Click { pos, button }),
370 2 => out.push(SemanticEvent::DoubleClick { pos, button }),
371 3 => out.push(SemanticEvent::TripleClick { pos, button }),
372 _ => out.push(SemanticEvent::Click { pos, button }),
373 }
374 }
375
376 fn on_mouse_drag(&mut self, pos: Position, button: MouseButton, out: &mut Vec<SemanticEvent>) {
377 self.long_press_pos = None;
379
380 let Some(ref mut drag) = self.drag else {
381 self.drag = Some(DragTracker {
383 start_pos: pos,
384 button,
385 last_pos: pos,
386 started: false,
387 });
388 return;
389 };
390
391 if !drag.started {
392 let distance = drag.start_pos.manhattan_distance(pos);
394 if distance >= u32::from(self.config.drag_threshold) {
395 drag.started = true;
396 out.push(SemanticEvent::DragStart {
397 pos: drag.start_pos,
398 button: drag.button,
399 });
400 }
401 }
402
403 if drag.started {
404 let delta = (
405 pos.x as i16 - drag.last_pos.x as i16,
406 pos.y as i16 - drag.last_pos.y as i16,
407 );
408 out.push(SemanticEvent::DragMove {
409 start: drag.start_pos,
410 current: pos,
411 delta,
412 });
413 }
414
415 drag.last_pos = pos;
416 }
417
418 fn expire_chord(&mut self, now: Instant) {
419 if let Some(start) = self.chord_start
420 && now.duration_since(start) > self.config.chord_timeout
421 {
422 self.chord_buffer.clear();
423 self.chord_start = None;
424 }
425 }
426}
427
428#[cfg(test)]
433mod tests {
434 use super::*;
435 use crate::event::{KeyEvent, MouseEvent};
436
437 fn now() -> Instant {
438 Instant::now()
439 }
440
441 fn mouse_down(x: u16, y: u16, button: MouseButton) -> Event {
442 Event::Mouse(MouseEvent {
443 kind: MouseEventKind::Down(button),
444 x,
445 y,
446 modifiers: Modifiers::NONE,
447 })
448 }
449
450 fn mouse_up(x: u16, y: u16, button: MouseButton) -> Event {
451 Event::Mouse(MouseEvent {
452 kind: MouseEventKind::Up(button),
453 x,
454 y,
455 modifiers: Modifiers::NONE,
456 })
457 }
458
459 fn mouse_drag(x: u16, y: u16, button: MouseButton) -> Event {
460 Event::Mouse(MouseEvent {
461 kind: MouseEventKind::Drag(button),
462 x,
463 y,
464 modifiers: Modifiers::NONE,
465 })
466 }
467
468 fn key_press(code: KeyCode, modifiers: Modifiers) -> Event {
469 Event::Key(KeyEvent {
470 code,
471 modifiers,
472 kind: KeyEventKind::Press,
473 })
474 }
475
476 fn esc() -> Event {
477 key_press(KeyCode::Escape, Modifiers::NONE)
478 }
479
480 const MS_50: Duration = Duration::from_millis(50);
481 const MS_100: Duration = Duration::from_millis(100);
482 const MS_200: Duration = Duration::from_millis(200);
483 const MS_500: Duration = Duration::from_millis(500);
484 const MS_600: Duration = Duration::from_millis(600);
485
486 #[test]
489 fn single_click() {
490 let mut gr = GestureRecognizer::new(GestureConfig::default());
491 let t = now();
492
493 let events = gr.process(&mouse_down(5, 5, MouseButton::Left), t);
494 assert!(events.is_empty());
495
496 let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
497 assert_eq!(events.len(), 1);
498 assert!(matches!(
499 events[0],
500 SemanticEvent::Click {
501 pos: Position { x: 5, y: 5 },
502 button: MouseButton::Left,
503 }
504 ));
505 }
506
507 #[test]
508 fn double_click() {
509 let mut gr = GestureRecognizer::new(GestureConfig::default());
510 let t = now();
511
512 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
514 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
515
516 gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_100);
518 let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_200);
519
520 assert_eq!(events.len(), 1);
521 assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
522 }
523
524 #[test]
525 fn triple_click() {
526 let mut gr = GestureRecognizer::new(GestureConfig::default());
527 let t = now();
528
529 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
531 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
532
533 gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_100);
535 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_200);
536
537 gr.process(
539 &mouse_down(5, 5, MouseButton::Left),
540 t + Duration::from_millis(250),
541 );
542 let events = gr.process(
543 &mouse_up(5, 5, MouseButton::Left),
544 t + Duration::from_millis(280),
545 );
546
547 assert_eq!(events.len(), 1);
548 assert!(matches!(events[0], SemanticEvent::TripleClick { .. }));
549 }
550
551 #[test]
552 fn double_click_timeout_resets_to_single() {
553 let mut gr = GestureRecognizer::new(GestureConfig::default());
554 let t = now();
555
556 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
558 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
559
560 gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_500);
562 let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_600);
563
564 assert_eq!(events.len(), 1);
565 assert!(matches!(events[0], SemanticEvent::Click { .. }));
566 }
567
568 #[test]
569 fn different_position_resets_click_count() {
570 let mut gr = GestureRecognizer::new(GestureConfig::default());
571 let t = now();
572
573 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
575 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
576
577 gr.process(&mouse_down(20, 20, MouseButton::Left), t + MS_100);
579 let events = gr.process(&mouse_up(20, 20, MouseButton::Left), t + MS_200);
580
581 assert_eq!(events.len(), 1);
582 assert!(matches!(events[0], SemanticEvent::Click { .. }));
583 }
584
585 #[test]
586 fn different_button_resets_click_count() {
587 let mut gr = GestureRecognizer::new(GestureConfig::default());
588 let t = now();
589
590 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
592 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
593
594 gr.process(&mouse_down(5, 5, MouseButton::Right), t + MS_100);
596 let events = gr.process(&mouse_up(5, 5, MouseButton::Right), t + MS_200);
597
598 assert_eq!(events.len(), 1);
599 assert!(matches!(
600 events[0],
601 SemanticEvent::Click {
602 button: MouseButton::Right,
603 ..
604 }
605 ));
606 }
607
608 #[test]
609 fn click_position_tolerance() {
610 let mut gr = GestureRecognizer::new(GestureConfig::default());
611 let t = now();
612
613 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
615 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
616
617 gr.process(&mouse_down(6, 5, MouseButton::Left), t + MS_100);
619 let events = gr.process(&mouse_up(6, 5, MouseButton::Left), t + MS_200);
620
621 assert_eq!(events.len(), 1);
622 assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
623 }
624
625 #[test]
628 fn drag_starts_after_threshold() {
629 let mut gr = GestureRecognizer::new(GestureConfig::default());
630 let t = now();
631
632 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
633
634 let events = gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_50);
636 assert!(events.is_empty());
637 assert!(!gr.is_dragging());
638
639 let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_100);
641 assert!(!events.is_empty());
642 assert!(
643 events
644 .iter()
645 .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
646 );
647 assert!(gr.is_dragging());
648 }
649
650 #[test]
651 fn drag_move_has_correct_delta() {
652 let mut gr = GestureRecognizer::new(GestureConfig::default());
653 let t = now();
654
655 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
656 let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
658
659 let drag_move = events
661 .iter()
662 .find(|e| matches!(e, SemanticEvent::DragMove { .. }));
663 assert!(drag_move.is_some());
664 if let Some(SemanticEvent::DragMove {
665 start,
666 current,
667 delta,
668 }) = drag_move
669 {
670 assert_eq!(*start, Position::new(5, 5));
671 assert_eq!(*current, Position::new(10, 5));
672 assert_eq!(*delta, (5, 0));
673 }
674 }
675
676 #[test]
677 fn drag_end_on_mouse_up() {
678 let mut gr = GestureRecognizer::new(GestureConfig::default());
679 let t = now();
680
681 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
682 gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
683
684 let events = gr.process(&mouse_up(12, 5, MouseButton::Left), t + MS_100);
686 assert_eq!(events.len(), 1);
687 assert!(matches!(
688 events[0],
689 SemanticEvent::DragEnd {
690 start: Position { x: 5, y: 5 },
691 end: Position { x: 12, y: 5 },
692 }
693 ));
694 assert!(!gr.is_dragging());
695 }
696
697 #[test]
698 fn drag_cancel_on_escape() {
699 let mut gr = GestureRecognizer::new(GestureConfig::default());
700 let t = now();
701
702 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
703 gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
704 assert!(gr.is_dragging());
705
706 let events = gr.process(&esc(), t + MS_100);
707 assert_eq!(events.len(), 1);
708 assert!(matches!(events[0], SemanticEvent::DragCancel));
709 assert!(!gr.is_dragging());
710 }
711
712 #[test]
713 fn drag_prevents_click() {
714 let mut gr = GestureRecognizer::new(GestureConfig::default());
715 let t = now();
716
717 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
718 gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
719
720 let events = gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_100);
722 assert_eq!(events.len(), 1);
723 assert!(matches!(events[0], SemanticEvent::DragEnd { .. }));
724 assert!(
725 !events
726 .iter()
727 .any(|e| matches!(e, SemanticEvent::Click { .. }))
728 );
729 }
730
731 #[test]
734 fn long_press_fires_after_threshold() {
735 let mut gr = GestureRecognizer::new(GestureConfig::default());
736 let t = now();
737
738 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
739
740 assert!(gr.check_long_press(t + MS_200).is_none());
742
743 let lp = gr.check_long_press(t + MS_600);
745 assert!(lp.is_some());
746 if let Some(SemanticEvent::LongPress { pos, duration }) = lp {
747 assert_eq!(pos, Position::new(5, 5));
748 assert!(duration >= Duration::from_millis(500));
749 }
750 }
751
752 #[test]
753 fn long_press_not_repeated() {
754 let mut gr = GestureRecognizer::new(GestureConfig::default());
755 let t = now();
756
757 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
758
759 assert!(gr.check_long_press(t + MS_600).is_some());
761
762 assert!(
764 gr.check_long_press(t + Duration::from_millis(700))
765 .is_none()
766 );
767 }
768
769 #[test]
770 fn drag_cancels_long_press() {
771 let mut gr = GestureRecognizer::new(GestureConfig::default());
772 let t = now();
773
774 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
775 gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_100);
777
778 assert!(gr.check_long_press(t + MS_600).is_none());
780 }
781
782 #[test]
783 fn mouse_up_cancels_long_press() {
784 let mut gr = GestureRecognizer::new(GestureConfig::default());
785 let t = now();
786
787 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
788 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
789
790 assert!(gr.check_long_press(t + MS_600).is_none());
792 }
793
794 #[test]
797 fn two_key_chord() {
798 let mut gr = GestureRecognizer::new(GestureConfig::default());
799 let t = now();
800
801 let events1 = gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
803 assert!(events1.is_empty()); let events2 = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_100);
807 assert_eq!(events2.len(), 1);
808 if let SemanticEvent::Chord { sequence } = &events2[0] {
809 assert_eq!(sequence.len(), 2);
810 assert_eq!(sequence[0].code, KeyCode::Char('k'));
811 assert_eq!(sequence[1].code, KeyCode::Char('c'));
812 } else {
813 panic!("Expected Chord event");
814 }
815 }
816
817 #[test]
818 fn chord_timeout_clears_buffer() {
819 let mut gr = GestureRecognizer::new(GestureConfig::default());
820 let t = now();
821
822 gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
824
825 let events = gr.process(
827 &key_press(KeyCode::Char('c'), Modifiers::CTRL),
828 t + Duration::from_millis(1100),
829 );
830
831 assert!(events.is_empty());
833 }
834
835 #[test]
836 fn non_modifier_key_clears_chord() {
837 let mut gr = GestureRecognizer::new(GestureConfig::default());
838 let t = now();
839
840 gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
842
843 gr.process(&key_press(KeyCode::Char('x'), Modifiers::NONE), t + MS_50);
845
846 let events = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_100);
848 assert!(events.is_empty()); }
850
851 #[test]
852 fn escape_clears_chord() {
853 let mut gr = GestureRecognizer::new(GestureConfig::default());
854 let t = now();
855
856 gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
857 gr.process(&esc(), t + MS_50);
858
859 let events = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_100);
861 assert!(events.is_empty());
862 }
863
864 #[test]
867 fn focus_loss_cancels_drag() {
868 let mut gr = GestureRecognizer::new(GestureConfig::default());
869 let t = now();
870
871 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
872 gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
873 assert!(gr.is_dragging());
874
875 let events = gr.process(&Event::Focus(false), t + MS_100);
876 assert_eq!(events.len(), 1);
877 assert!(matches!(events[0], SemanticEvent::DragCancel));
878 assert!(!gr.is_dragging());
879 }
880
881 #[test]
882 fn focus_loss_without_drag_is_silent() {
883 let mut gr = GestureRecognizer::new(GestureConfig::default());
884 let t = now();
885
886 let events = gr.process(&Event::Focus(false), t);
887 assert!(events.is_empty());
888 }
889
890 #[test]
893 fn reset_clears_all_state() {
894 let mut gr = GestureRecognizer::new(GestureConfig::default());
895 let t = now();
896
897 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
899 gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
900 gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t + MS_100);
901
902 assert!(gr.is_dragging());
903
904 gr.reset();
905
906 assert!(!gr.is_dragging());
907 assert!(gr.last_click.is_none());
908 assert!(gr.mouse_down.is_none());
909 assert!(gr.drag.is_none());
910 assert!(gr.chord_buffer.is_empty());
911 assert!(gr.chord_start.is_none());
912 }
913
914 #[test]
917 fn quadruple_click_wraps_to_single() {
918 let mut gr = GestureRecognizer::new(GestureConfig::default());
919 let t = now();
920
921 for i in 0..3u32 {
923 let offset = Duration::from_millis(i as u64 * 80);
924 gr.process(&mouse_down(5, 5, MouseButton::Left), t + offset);
925 gr.process(&mouse_up(5, 5, MouseButton::Left), t + offset + MS_50);
926 }
927
928 gr.process(
930 &mouse_down(5, 5, MouseButton::Left),
931 t + Duration::from_millis(260),
932 );
933 let events = gr.process(
934 &mouse_up(5, 5, MouseButton::Left),
935 t + Duration::from_millis(280),
936 );
937
938 assert_eq!(events.len(), 1);
939 assert!(matches!(events[0], SemanticEvent::Click { .. }));
940 }
941
942 #[test]
943 fn key_release_ignored() {
944 let mut gr = GestureRecognizer::new(GestureConfig::default());
945 let t = now();
946
947 let events = gr.process(
948 &Event::Key(KeyEvent {
949 code: KeyCode::Char('k'),
950 modifiers: Modifiers::CTRL,
951 kind: KeyEventKind::Release,
952 }),
953 t,
954 );
955 assert!(events.is_empty());
956 }
957
958 #[test]
959 fn debug_format() {
960 let gr = GestureRecognizer::new(GestureConfig::default());
961 let dbg = format!("{:?}", gr);
962 assert!(dbg.contains("GestureRecognizer"));
963 }
964
965 #[test]
968 fn right_click() {
969 let mut gr = GestureRecognizer::new(GestureConfig::default());
970 let t = now();
971
972 gr.process(&mouse_down(10, 10, MouseButton::Right), t);
973 let events = gr.process(&mouse_up(10, 10, MouseButton::Right), t + MS_50);
974
975 assert_eq!(events.len(), 1);
976 assert!(matches!(
977 events[0],
978 SemanticEvent::Click {
979 button: MouseButton::Right,
980 ..
981 }
982 ));
983 }
984
985 #[test]
986 fn middle_click() {
987 let mut gr = GestureRecognizer::new(GestureConfig::default());
988 let t = now();
989
990 gr.process(&mouse_down(10, 10, MouseButton::Middle), t);
991 let events = gr.process(&mouse_up(10, 10, MouseButton::Middle), t + MS_50);
992
993 assert_eq!(events.len(), 1);
994 assert!(matches!(
995 events[0],
996 SemanticEvent::Click {
997 button: MouseButton::Middle,
998 ..
999 }
1000 ));
1001 }
1002
1003 #[test]
1004 fn click_at_origin() {
1005 let mut gr = GestureRecognizer::new(GestureConfig::default());
1006 let t = now();
1007
1008 gr.process(&mouse_down(0, 0, MouseButton::Left), t);
1009 let events = gr.process(&mouse_up(0, 0, MouseButton::Left), t + MS_50);
1010
1011 assert_eq!(events.len(), 1);
1012 if let SemanticEvent::Click { pos, .. } = &events[0] {
1013 assert_eq!(pos.x, 0);
1014 assert_eq!(pos.y, 0);
1015 }
1016 }
1017
1018 #[test]
1019 fn click_at_max_position() {
1020 let mut gr = GestureRecognizer::new(GestureConfig::default());
1021 let t = now();
1022
1023 gr.process(&mouse_down(u16::MAX, u16::MAX, MouseButton::Left), t);
1024 let events = gr.process(&mouse_up(u16::MAX, u16::MAX, MouseButton::Left), t + MS_50);
1025
1026 assert_eq!(events.len(), 1);
1027 if let SemanticEvent::Click { pos, .. } = &events[0] {
1028 assert_eq!(pos.x, u16::MAX);
1029 assert_eq!(pos.y, u16::MAX);
1030 }
1031 }
1032
1033 #[test]
1034 fn double_click_right_button() {
1035 let mut gr = GestureRecognizer::new(GestureConfig::default());
1036 let t = now();
1037
1038 gr.process(&mouse_down(5, 5, MouseButton::Right), t);
1039 gr.process(&mouse_up(5, 5, MouseButton::Right), t + MS_50);
1040
1041 gr.process(&mouse_down(5, 5, MouseButton::Right), t + MS_100);
1042 let events = gr.process(&mouse_up(5, 5, MouseButton::Right), t + MS_200);
1043
1044 assert_eq!(events.len(), 1);
1045 assert!(matches!(
1046 events[0],
1047 SemanticEvent::DoubleClick {
1048 button: MouseButton::Right,
1049 ..
1050 }
1051 ));
1052 }
1053
1054 #[test]
1055 fn click_position_beyond_tolerance() {
1056 let mut gr = GestureRecognizer::new(GestureConfig::default());
1057 let t = now();
1058
1059 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1060 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1061
1062 gr.process(&mouse_down(8, 5, MouseButton::Left), t + MS_100);
1064 let events = gr.process(&mouse_up(8, 5, MouseButton::Left), t + MS_200);
1065
1066 assert_eq!(events.len(), 1);
1067 assert!(matches!(events[0], SemanticEvent::Click { .. }));
1068 }
1069
1070 #[test]
1073 fn drag_with_right_button() {
1074 let mut gr = GestureRecognizer::new(GestureConfig::default());
1075 let t = now();
1076
1077 gr.process(&mouse_down(5, 5, MouseButton::Right), t);
1078 let events = gr.process(&mouse_drag(10, 5, MouseButton::Right), t + MS_50);
1079
1080 assert!(events.iter().any(|e| matches!(
1081 e,
1082 SemanticEvent::DragStart {
1083 button: MouseButton::Right,
1084 ..
1085 }
1086 )));
1087 }
1088
1089 #[test]
1090 fn drag_vertical() {
1091 let mut gr = GestureRecognizer::new(GestureConfig::default());
1092 let t = now();
1093
1094 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1095 let events = gr.process(&mouse_drag(5, 10, MouseButton::Left), t + MS_50);
1096
1097 assert!(
1098 events
1099 .iter()
1100 .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1101 );
1102 let drag_move = events
1103 .iter()
1104 .find(|e| matches!(e, SemanticEvent::DragMove { .. }));
1105 if let Some(SemanticEvent::DragMove { delta, .. }) = drag_move {
1106 assert_eq!(delta.0, 0); assert_eq!(delta.1, 5); }
1109 }
1110
1111 #[test]
1112 fn drag_multiple_moves() {
1113 let mut gr = GestureRecognizer::new(GestureConfig::default());
1114 let t = now();
1115
1116 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1117 gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
1118
1119 let events = gr.process(&mouse_drag(15, 5, MouseButton::Left), t + MS_100);
1121 let drag_move = events
1122 .iter()
1123 .find(|e| matches!(e, SemanticEvent::DragMove { .. }));
1124 if let Some(SemanticEvent::DragMove {
1125 start,
1126 current,
1127 delta,
1128 }) = drag_move
1129 {
1130 assert_eq!(*start, Position::new(5, 5));
1131 assert_eq!(*current, Position::new(15, 5));
1132 assert_eq!(*delta, (5, 0)); }
1134 }
1135
1136 #[test]
1137 fn drag_threshold_exactly_met() {
1138 let mut gr = GestureRecognizer::new(GestureConfig::default());
1139 let t = now();
1140
1141 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1142 let events = gr.process(&mouse_drag(8, 5, MouseButton::Left), t + MS_50);
1144
1145 assert!(
1146 events
1147 .iter()
1148 .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1149 );
1150 }
1151
1152 #[test]
1153 fn drag_threshold_one_below() {
1154 let mut gr = GestureRecognizer::new(GestureConfig::default());
1155 let t = now();
1156
1157 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1158 let events = gr.process(&mouse_drag(7, 5, MouseButton::Left), t + MS_50);
1160
1161 assert!(
1162 !events
1163 .iter()
1164 .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1165 );
1166 assert!(!gr.is_dragging());
1167 }
1168
1169 #[test]
1170 fn drag_state_reset_after_end() {
1171 let mut gr = GestureRecognizer::new(GestureConfig::default());
1172 let t = now();
1173
1174 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1176 gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
1177 gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_100);
1178
1179 assert!(!gr.is_dragging());
1180
1181 gr.process(&mouse_down(20, 20, MouseButton::Left), t + MS_200);
1183 let events = gr.process(
1184 &mouse_up(20, 20, MouseButton::Left),
1185 t + Duration::from_millis(250),
1186 );
1187
1188 assert_eq!(events.len(), 1);
1190 assert!(matches!(events[0], SemanticEvent::Click { .. }));
1191 }
1192
1193 #[test]
1194 fn no_click_after_drag_cancel() {
1195 let mut gr = GestureRecognizer::new(GestureConfig::default());
1196 let t = now();
1197
1198 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1199 gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
1200 gr.process(&esc(), t + MS_100); let events = gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_200);
1204 assert!(
1207 !events
1208 .iter()
1209 .any(|e| matches!(e, SemanticEvent::DragEnd { .. }))
1210 );
1211 }
1212
1213 #[test]
1216 fn long_press_correct_position() {
1217 let mut gr = GestureRecognizer::new(GestureConfig::default());
1218 let t = now();
1219
1220 gr.process(&mouse_down(42, 17, MouseButton::Left), t);
1221 let lp = gr.check_long_press(t + MS_600);
1222
1223 if let Some(SemanticEvent::LongPress { pos, .. }) = lp {
1224 assert_eq!(pos, Position::new(42, 17));
1225 } else {
1226 panic!("Expected LongPress");
1227 }
1228 }
1229
1230 #[test]
1231 fn long_press_with_custom_threshold() {
1232 let config = GestureConfig {
1233 long_press_threshold: Duration::from_millis(200),
1234 ..Default::default()
1235 };
1236 let mut gr = GestureRecognizer::new(config);
1237 let t = now();
1238
1239 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1240
1241 assert!(
1243 gr.check_long_press(t + Duration::from_millis(150))
1244 .is_none()
1245 );
1246
1247 assert!(
1249 gr.check_long_press(t + Duration::from_millis(250))
1250 .is_some()
1251 );
1252 }
1253
1254 #[test]
1255 fn long_press_resets_on_new_mouse_down() {
1256 let mut gr = GestureRecognizer::new(GestureConfig::default());
1257 let t = now();
1258
1259 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1260 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
1261
1262 gr.process(&mouse_down(10, 10, MouseButton::Left), t + MS_200);
1264
1265 assert!(gr.check_long_press(t + MS_600).is_none());
1267
1268 assert!(
1270 gr.check_long_press(t + Duration::from_millis(750))
1271 .is_some()
1272 );
1273 }
1274
1275 #[test]
1278 fn alt_key_chord() {
1279 let mut gr = GestureRecognizer::new(GestureConfig::default());
1280 let t = now();
1281
1282 gr.process(&key_press(KeyCode::Char('x'), Modifiers::ALT), t);
1283 let events = gr.process(&key_press(KeyCode::Char('y'), Modifiers::ALT), t + MS_100);
1284
1285 assert_eq!(events.len(), 1);
1286 if let SemanticEvent::Chord { sequence } = &events[0] {
1287 assert_eq!(sequence.len(), 2);
1288 assert!(sequence[0].modifiers.contains(Modifiers::ALT));
1289 }
1290 }
1291
1292 #[test]
1293 fn mixed_modifier_chord() {
1294 let mut gr = GestureRecognizer::new(GestureConfig::default());
1295 let t = now();
1296
1297 gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
1298 let events = gr.process(&key_press(KeyCode::Char('d'), Modifiers::ALT), t + MS_100);
1299
1300 assert_eq!(events.len(), 1);
1301 if let SemanticEvent::Chord { sequence } = &events[0] {
1302 assert_eq!(sequence[0].modifiers, Modifiers::CTRL);
1303 assert_eq!(sequence[1].modifiers, Modifiers::ALT);
1304 }
1305 }
1306
1307 #[test]
1308 fn chord_with_function_key() {
1309 let mut gr = GestureRecognizer::new(GestureConfig::default());
1310 let t = now();
1311
1312 gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
1313 let events = gr.process(&key_press(KeyCode::F(1), Modifiers::CTRL), t + MS_100);
1314
1315 assert_eq!(events.len(), 1);
1316 if let SemanticEvent::Chord { sequence } = &events[0] {
1317 assert_eq!(sequence[1].code, KeyCode::F(1));
1318 }
1319 }
1320
1321 #[test]
1322 fn single_modifier_key_no_chord_emitted() {
1323 let mut gr = GestureRecognizer::new(GestureConfig::default());
1324 let t = now();
1325
1326 let events = gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
1328 assert!(events.is_empty());
1329 }
1330
1331 #[test]
1334 fn custom_click_tolerance() {
1335 let config = GestureConfig {
1336 click_tolerance: 5,
1337 ..Default::default()
1338 };
1339 let mut gr = GestureRecognizer::new(config);
1340 let t = now();
1341
1342 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1343 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1344
1345 gr.process(&mouse_down(10, 5, MouseButton::Left), t + MS_100);
1347 let events = gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_200);
1348
1349 assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
1350 }
1351
1352 #[test]
1353 fn custom_drag_threshold() {
1354 let config = GestureConfig {
1355 drag_threshold: 10,
1356 ..Default::default()
1357 };
1358 let mut gr = GestureRecognizer::new(config);
1359 let t = now();
1360
1361 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1362 let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
1364 assert!(!gr.is_dragging());
1365 assert!(events.is_empty());
1366
1367 let events = gr.process(&mouse_drag(15, 5, MouseButton::Left), t + MS_100);
1369 assert!(gr.is_dragging());
1370 assert!(
1371 events
1372 .iter()
1373 .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1374 );
1375 }
1376
1377 #[test]
1378 fn custom_multi_click_timeout() {
1379 let config = GestureConfig {
1380 multi_click_timeout: Duration::from_millis(100),
1381 ..Default::default()
1382 };
1383 let mut gr = GestureRecognizer::new(config);
1384 let t = now();
1385
1386 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1387 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1388
1389 gr.process(
1391 &mouse_down(5, 5, MouseButton::Left),
1392 t + Duration::from_millis(150),
1393 );
1394 let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_200);
1395
1396 assert!(matches!(events[0], SemanticEvent::Click { .. }));
1397 }
1398
1399 #[test]
1400 fn config_getter_and_setter() {
1401 let mut gr = GestureRecognizer::new(GestureConfig::default());
1402
1403 assert_eq!(gr.config().drag_threshold, 3);
1404
1405 let new_config = GestureConfig {
1406 drag_threshold: 10,
1407 ..Default::default()
1408 };
1409 gr.set_config(new_config);
1410
1411 assert_eq!(gr.config().drag_threshold, 10);
1412 }
1413
1414 #[test]
1417 fn click_then_drag_are_independent() {
1418 let mut gr = GestureRecognizer::new(GestureConfig::default());
1419 let t = now();
1420
1421 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1423 let click_events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1424 assert!(matches!(click_events[0], SemanticEvent::Click { .. }));
1425
1426 gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_200);
1428 let drag_events = gr.process(
1429 &mouse_drag(10, 5, MouseButton::Left),
1430 t + Duration::from_millis(250),
1431 );
1432 assert!(
1433 drag_events
1434 .iter()
1435 .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1436 );
1437
1438 let end_events = gr.process(
1439 &mouse_up(10, 5, MouseButton::Left),
1440 t + Duration::from_millis(300),
1441 );
1442 assert!(matches!(end_events[0], SemanticEvent::DragEnd { .. }));
1443 }
1444
1445 #[test]
1446 fn interleaved_mouse_and_keyboard() {
1447 let mut gr = GestureRecognizer::new(GestureConfig::default());
1448 let t = now();
1449
1450 gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
1452
1453 gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_50);
1455 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
1456
1457 let events = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_200);
1459
1460 assert_eq!(events.len(), 1);
1461 assert!(matches!(events[0], SemanticEvent::Chord { .. }));
1462 }
1463
1464 #[test]
1465 fn rapid_clicks_produce_correct_sequence() {
1466 let mut gr = GestureRecognizer::new(GestureConfig::default());
1467 let t = now();
1468 let mut results = Vec::new();
1469
1470 for i in 0..5u32 {
1471 let offset = Duration::from_millis(i as u64 * 60);
1472 gr.process(&mouse_down(5, 5, MouseButton::Left), t + offset);
1473 let events = gr.process(
1474 &mouse_up(5, 5, MouseButton::Left),
1475 t + offset + Duration::from_millis(30),
1476 );
1477 results.extend(events);
1478 }
1479
1480 assert!(results.len() == 5);
1482 assert!(matches!(results[0], SemanticEvent::Click { .. }));
1483 assert!(matches!(results[1], SemanticEvent::DoubleClick { .. }));
1484 assert!(matches!(results[2], SemanticEvent::TripleClick { .. }));
1485 assert!(matches!(results[3], SemanticEvent::Click { .. }));
1486 assert!(matches!(results[4], SemanticEvent::DoubleClick { .. }));
1487 }
1488
1489 #[test]
1490 fn tick_event_ignored() {
1491 let mut gr = GestureRecognizer::new(GestureConfig::default());
1492 let t = now();
1493
1494 let events = gr.process(&Event::Tick, t);
1495 assert!(events.is_empty());
1496 }
1497
1498 #[test]
1499 fn resize_event_ignored() {
1500 let mut gr = GestureRecognizer::new(GestureConfig::default());
1501 let t = now();
1502
1503 let events = gr.process(
1504 &Event::Resize {
1505 width: 80,
1506 height: 24,
1507 },
1508 t,
1509 );
1510 assert!(events.is_empty());
1511 }
1512
1513 #[test]
1514 fn focus_gain_ignored() {
1515 let mut gr = GestureRecognizer::new(GestureConfig::default());
1516 let t = now();
1517
1518 let events = gr.process(&Event::Focus(true), t);
1519 assert!(events.is_empty());
1520 }
1521
1522 #[test]
1523 fn default_config_values() {
1524 let config = GestureConfig::default();
1525 assert_eq!(config.multi_click_timeout, Duration::from_millis(300));
1526 assert_eq!(config.long_press_threshold, Duration::from_millis(500));
1527 assert_eq!(config.drag_threshold, 3);
1528 assert_eq!(config.chord_timeout, Duration::from_millis(1000));
1529 assert_eq!(config.click_tolerance, 1);
1530 }
1531
1532 fn mouse_moved(x: u16, y: u16) -> Event {
1535 Event::Mouse(MouseEvent {
1536 kind: MouseEventKind::Moved,
1537 x,
1538 y,
1539 modifiers: Modifiers::NONE,
1540 })
1541 }
1542
1543 fn mouse_scroll_up(x: u16, y: u16) -> Event {
1544 Event::Mouse(MouseEvent {
1545 kind: MouseEventKind::ScrollUp,
1546 x,
1547 y,
1548 modifiers: Modifiers::NONE,
1549 })
1550 }
1551
1552 fn mouse_scroll_down(x: u16, y: u16) -> Event {
1553 Event::Mouse(MouseEvent {
1554 kind: MouseEventKind::ScrollDown,
1555 x,
1556 y,
1557 modifiers: Modifiers::NONE,
1558 })
1559 }
1560
1561 #[test]
1562 fn drag_without_prior_mouse_down() {
1563 let mut gr = GestureRecognizer::new(GestureConfig::default());
1565 let t = now();
1566
1567 let events = gr.process(&mouse_drag(10, 10, MouseButton::Left), t);
1568 assert!(events.is_empty()); assert!(!gr.is_dragging());
1570
1571 let events = gr.process(&mouse_drag(20, 10, MouseButton::Left), t + MS_50);
1573 assert!(
1574 events
1575 .iter()
1576 .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1577 );
1578 assert!(gr.is_dragging());
1579 }
1580
1581 #[test]
1582 fn mouse_moved_cancels_long_press() {
1583 let mut gr = GestureRecognizer::new(GestureConfig::default());
1584 let t = now();
1585
1586 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1587
1588 gr.process(&mouse_moved(6, 5), t + MS_100);
1590
1591 assert!(gr.check_long_press(t + MS_600).is_none());
1593 }
1594
1595 #[test]
1596 fn mouse_moved_does_not_affect_drag_tracker() {
1597 let mut gr = GestureRecognizer::new(GestureConfig::default());
1598 let t = now();
1599
1600 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1601
1602 let events = gr.process(&mouse_moved(20, 20), t + MS_50);
1604 assert!(events.is_empty());
1605
1606 let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_100);
1608 assert!(
1609 events
1610 .iter()
1611 .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1612 );
1613 }
1614
1615 #[test]
1616 fn escape_during_unstarted_drag_no_cancel() {
1617 let mut gr = GestureRecognizer::new(GestureConfig::default());
1619 let t = now();
1620
1621 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1622 gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_50);
1624 assert!(!gr.is_dragging());
1625
1626 let events = gr.process(&esc(), t + MS_100);
1627 assert!(events.is_empty()); }
1629
1630 #[test]
1631 fn focus_loss_during_unstarted_drag_no_cancel() {
1632 let mut gr = GestureRecognizer::new(GestureConfig::default());
1634 let t = now();
1635
1636 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1637 gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_50);
1639 assert!(!gr.is_dragging());
1640
1641 let events = gr.process(&Event::Focus(false), t + MS_100);
1642 assert!(events.is_empty()); }
1644
1645 #[test]
1646 fn chord_with_super_modifier() {
1647 let mut gr = GestureRecognizer::new(GestureConfig::default());
1648 let t = now();
1649
1650 gr.process(&key_press(KeyCode::Char('a'), Modifiers::SUPER), t);
1651 let events = gr.process(&key_press(KeyCode::Char('b'), Modifiers::SUPER), t + MS_100);
1652
1653 assert_eq!(events.len(), 1);
1654 if let SemanticEvent::Chord { sequence } = &events[0] {
1655 assert_eq!(sequence.len(), 2);
1656 assert!(sequence[0].modifiers.contains(Modifiers::SUPER));
1657 assert!(sequence[1].modifiers.contains(Modifiers::SUPER));
1658 } else {
1659 panic!("Expected Chord event");
1660 }
1661 }
1662
1663 #[test]
1664 fn long_press_exactly_at_threshold() {
1665 let mut gr = GestureRecognizer::new(GestureConfig::default());
1666 let t = now();
1667
1668 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1669
1670 let lp = gr.check_long_press(t + Duration::from_millis(500));
1672 assert!(lp.is_some());
1673 if let Some(SemanticEvent::LongPress { duration, .. }) = lp {
1674 assert_eq!(duration, Duration::from_millis(500));
1675 }
1676 }
1677
1678 #[test]
1679 fn long_press_one_ms_before_threshold() {
1680 let mut gr = GestureRecognizer::new(GestureConfig::default());
1681 let t = now();
1682
1683 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1684
1685 assert!(
1687 gr.check_long_press(t + Duration::from_millis(499))
1688 .is_none()
1689 );
1690 }
1691
1692 #[test]
1693 fn drag_negative_direction() {
1694 let mut gr = GestureRecognizer::new(GestureConfig::default());
1696 let t = now();
1697
1698 gr.process(&mouse_down(20, 10, MouseButton::Left), t);
1699 let events = gr.process(&mouse_drag(15, 10, MouseButton::Left), t + MS_50);
1700
1701 let drag_move = events
1702 .iter()
1703 .find(|e| matches!(e, SemanticEvent::DragMove { .. }));
1704 if let Some(SemanticEvent::DragMove { delta, .. }) = drag_move {
1705 assert_eq!(delta.0, -5); assert_eq!(delta.1, 0);
1707 } else {
1708 panic!("Expected DragMove");
1709 }
1710 }
1711
1712 #[test]
1713 fn drag_negative_y_direction() {
1714 let mut gr = GestureRecognizer::new(GestureConfig::default());
1716 let t = now();
1717
1718 gr.process(&mouse_down(5, 20, MouseButton::Left), t);
1719 let events = gr.process(&mouse_drag(5, 15, MouseButton::Left), t + MS_50);
1720
1721 let drag_move = events
1722 .iter()
1723 .find(|e| matches!(e, SemanticEvent::DragMove { .. }));
1724 if let Some(SemanticEvent::DragMove { delta, .. }) = drag_move {
1725 assert_eq!(delta.0, 0);
1726 assert_eq!(delta.1, -5); } else {
1728 panic!("Expected DragMove");
1729 }
1730 }
1731
1732 #[test]
1733 fn multiple_sequential_drags() {
1734 let mut gr = GestureRecognizer::new(GestureConfig::default());
1735 let t = now();
1736
1737 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1739 gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
1740 gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_100);
1741 assert!(!gr.is_dragging());
1742
1743 gr.process(&mouse_down(20, 20, MouseButton::Left), t + MS_200);
1745 let events = gr.process(
1746 &mouse_drag(30, 20, MouseButton::Left),
1747 t + Duration::from_millis(250),
1748 );
1749
1750 assert!(events.iter().any(|e| matches!(
1751 e,
1752 SemanticEvent::DragStart {
1753 pos: Position { x: 20, y: 20 },
1754 ..
1755 }
1756 )));
1757 assert!(gr.is_dragging());
1758
1759 let events = gr.process(
1760 &mouse_up(30, 20, MouseButton::Left),
1761 t + Duration::from_millis(300),
1762 );
1763 assert!(matches!(
1764 events[0],
1765 SemanticEvent::DragEnd {
1766 start: Position { x: 20, y: 20 },
1767 end: Position { x: 30, y: 20 },
1768 }
1769 ));
1770 }
1771
1772 #[test]
1773 fn check_long_press_no_mouse_down() {
1774 let mut gr = GestureRecognizer::new(GestureConfig::default());
1776 let t = now();
1777
1778 assert!(gr.check_long_press(t).is_none());
1779 assert!(gr.check_long_press(t + Duration::from_secs(10)).is_none());
1780 }
1781
1782 #[test]
1783 fn zero_drag_threshold() {
1784 let config = GestureConfig {
1786 drag_threshold: 0,
1787 ..Default::default()
1788 };
1789 let mut gr = GestureRecognizer::new(config);
1790 let t = now();
1791
1792 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1793 let events = gr.process(&mouse_drag(5, 5, MouseButton::Left), t + MS_50);
1795 assert!(
1796 events
1797 .iter()
1798 .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
1799 );
1800 assert!(gr.is_dragging());
1801 }
1802
1803 #[test]
1804 fn zero_click_tolerance() {
1805 let config = GestureConfig {
1807 click_tolerance: 0,
1808 ..Default::default()
1809 };
1810 let mut gr = GestureRecognizer::new(config);
1811 let t = now();
1812
1813 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1814 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1815
1816 gr.process(&mouse_down(6, 5, MouseButton::Left), t + MS_100);
1818 let events = gr.process(&mouse_up(6, 5, MouseButton::Left), t + MS_200);
1819 assert!(matches!(events[0], SemanticEvent::Click { .. }));
1820
1821 gr.process(
1823 &mouse_down(6, 5, MouseButton::Left),
1824 t + Duration::from_millis(250),
1825 );
1826 let events = gr.process(
1827 &mouse_up(6, 5, MouseButton::Left),
1828 t + Duration::from_millis(280),
1829 );
1830 assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
1831 }
1832
1833 #[test]
1834 fn scroll_events_ignored() {
1835 let mut gr = GestureRecognizer::new(GestureConfig::default());
1836 let t = now();
1837
1838 let events = gr.process(&mouse_scroll_up(5, 5), t);
1839 assert!(events.is_empty());
1840
1841 let events = gr.process(&mouse_scroll_down(5, 5), t + MS_50);
1842 assert!(events.is_empty());
1843 }
1844
1845 #[test]
1846 fn key_repeat_ignored() {
1847 let mut gr = GestureRecognizer::new(GestureConfig::default());
1848 let t = now();
1849
1850 let events = gr.process(
1851 &Event::Key(KeyEvent {
1852 code: KeyCode::Char('k'),
1853 modifiers: Modifiers::CTRL,
1854 kind: KeyEventKind::Repeat,
1855 }),
1856 t,
1857 );
1858 assert!(events.is_empty());
1859 }
1860
1861 #[test]
1862 fn double_click_at_exact_timeout_boundary() {
1863 let mut gr = GestureRecognizer::new(GestureConfig::default());
1865 let t = now();
1866
1867 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1868 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
1869
1870 gr.process(
1874 &mouse_down(5, 5, MouseButton::Left),
1875 t + Duration::from_millis(350),
1876 );
1877 let events = gr.process(
1878 &mouse_up(5, 5, MouseButton::Left),
1879 t + Duration::from_millis(350),
1880 );
1881 assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
1882 }
1883
1884 #[test]
1885 fn mouse_up_position_becomes_click_position() {
1886 let mut gr = GestureRecognizer::new(GestureConfig::default());
1888 let t = now();
1889
1890 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1891 let events = gr.process(&mouse_up(6, 6, MouseButton::Left), t + MS_50);
1893
1894 assert_eq!(events.len(), 1);
1895 if let SemanticEvent::Click { pos, .. } = &events[0] {
1896 assert_eq!(*pos, Position::new(6, 6)); } else {
1898 panic!("Expected Click");
1899 }
1900 }
1901
1902 #[test]
1903 fn multiple_mouse_downs_overwrite_state() {
1904 let mut gr = GestureRecognizer::new(GestureConfig::default());
1906 let t = now();
1907
1908 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1909 gr.process(&mouse_down(20, 20, MouseButton::Right), t + MS_50);
1910
1911 let events = gr.process(&mouse_drag(30, 20, MouseButton::Right), t + MS_100);
1913 if let Some(SemanticEvent::DragStart { pos, button }) = events
1914 .iter()
1915 .find(|e| matches!(e, SemanticEvent::DragStart { .. }))
1916 {
1917 assert_eq!(*pos, Position::new(20, 20));
1918 assert_eq!(*button, MouseButton::Right);
1919 } else {
1920 panic!("Expected DragStart from second mouse-down position");
1921 }
1922 }
1923
1924 #[test]
1925 fn triple_click_then_timeout_then_single() {
1926 let mut gr = GestureRecognizer::new(GestureConfig::default());
1927 let t = now();
1928
1929 for i in 0..3u32 {
1931 let offset = Duration::from_millis(i as u64 * 80);
1932 gr.process(&mouse_down(5, 5, MouseButton::Left), t + offset);
1933 gr.process(&mouse_up(5, 5, MouseButton::Left), t + offset + MS_50);
1934 }
1935
1936 let late = t + Duration::from_secs(2);
1938 gr.process(&mouse_down(5, 5, MouseButton::Left), late);
1939 let events = gr.process(&mouse_up(5, 5, MouseButton::Left), late + MS_50);
1940
1941 assert_eq!(events.len(), 1);
1942 assert!(matches!(events[0], SemanticEvent::Click { .. }));
1943 }
1944
1945 #[test]
1946 fn chord_ctrl_alt_combined_modifier() {
1947 let mut gr = GestureRecognizer::new(GestureConfig::default());
1949 let t = now();
1950
1951 let mods = Modifiers::CTRL | Modifiers::ALT;
1952 gr.process(&key_press(KeyCode::Char('a'), mods), t);
1953 let events = gr.process(&key_press(KeyCode::Char('b'), Modifiers::CTRL), t + MS_100);
1954
1955 assert_eq!(events.len(), 1);
1956 if let SemanticEvent::Chord { sequence } = &events[0] {
1957 assert_eq!(sequence[0].modifiers, Modifiers::CTRL | Modifiers::ALT);
1958 assert_eq!(sequence[1].modifiers, Modifiers::CTRL);
1959 }
1960 }
1961
1962 #[test]
1963 fn chord_timeout_then_new_chord() {
1964 let mut gr = GestureRecognizer::new(GestureConfig::default());
1966 let t = now();
1967
1968 gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
1970
1971 gr.process(
1974 &key_press(KeyCode::Char('a'), Modifiers::CTRL),
1975 t + Duration::from_millis(1100),
1976 );
1977 let events = gr.process(
1979 &key_press(KeyCode::Char('b'), Modifiers::CTRL),
1980 t + Duration::from_millis(1200),
1981 );
1982
1983 assert_eq!(events.len(), 1);
1984 if let SemanticEvent::Chord { sequence } = &events[0] {
1985 assert_eq!(sequence.len(), 2);
1986 assert_eq!(sequence[0].code, KeyCode::Char('a'));
1987 assert_eq!(sequence[1].code, KeyCode::Char('b'));
1988 }
1989 }
1990
1991 #[test]
1992 fn drag_start_and_move_emitted_together() {
1993 let mut gr = GestureRecognizer::new(GestureConfig::default());
1996 let t = now();
1997
1998 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
1999 let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
2000
2001 assert_eq!(events.len(), 2);
2002 assert!(matches!(events[0], SemanticEvent::DragStart { .. }));
2003 assert!(matches!(events[1], SemanticEvent::DragMove { .. }));
2004 }
2005
2006 #[test]
2007 fn config_clone() {
2008 let config = GestureConfig {
2009 drag_threshold: 42,
2010 click_tolerance: 7,
2011 ..Default::default()
2012 };
2013 let cloned = config.clone();
2014 assert_eq!(cloned.drag_threshold, 42);
2015 assert_eq!(cloned.click_tolerance, 7);
2016 assert_eq!(cloned.multi_click_timeout, config.multi_click_timeout);
2017 }
2018
2019 #[test]
2020 fn scroll_does_not_affect_click_state() {
2021 let mut gr = GestureRecognizer::new(GestureConfig::default());
2022 let t = now();
2023
2024 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
2026 gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
2027
2028 gr.process(&mouse_scroll_up(5, 5), t + MS_100);
2030
2031 gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_200);
2033 let events = gr.process(
2034 &mouse_up(5, 5, MouseButton::Left),
2035 t + Duration::from_millis(250),
2036 );
2037 assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
2038 }
2039
2040 #[test]
2041 fn escape_clears_mouse_down_state() {
2042 let mut gr = GestureRecognizer::new(GestureConfig::default());
2043 let t = now();
2044
2045 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
2046 gr.process(&esc(), t + MS_50);
2047
2048 assert!(gr.check_long_press(t + MS_600).is_none());
2050
2051 let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
2054 assert_eq!(events.len(), 1);
2055 assert!(matches!(events[0], SemanticEvent::Click { .. }));
2056 }
2057
2058 #[test]
2059 fn focus_loss_clears_long_press_fired_flag() {
2060 let mut gr = GestureRecognizer::new(GestureConfig::default());
2061 let t = now();
2062
2063 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
2065 assert!(gr.check_long_press(t + MS_600).is_some());
2066 assert!(
2067 gr.check_long_press(t + Duration::from_millis(700))
2068 .is_none()
2069 ); gr.process(&Event::Focus(false), t + Duration::from_millis(800));
2073
2074 gr.process(
2076 &mouse_down(10, 10, MouseButton::Left),
2077 t + Duration::from_secs(1),
2078 );
2079 assert!(
2080 gr.check_long_press(t + Duration::from_millis(1600))
2081 .is_some()
2082 );
2083 }
2084
2085 #[test]
2086 fn drag_at_u16_max_coordinates() {
2087 let mut gr = GestureRecognizer::new(GestureConfig::default());
2088 let t = now();
2089
2090 gr.process(&mouse_down(u16::MAX - 5, u16::MAX, MouseButton::Left), t);
2091 let events = gr.process(
2092 &mouse_drag(u16::MAX, u16::MAX, MouseButton::Left),
2093 t + MS_50,
2094 );
2095
2096 assert!(
2098 events
2099 .iter()
2100 .any(|e| matches!(e, SemanticEvent::DragStart { .. }))
2101 );
2102
2103 if let Some(SemanticEvent::DragMove { delta, .. }) = events
2105 .iter()
2106 .find(|e| matches!(e, SemanticEvent::DragMove { .. }))
2107 {
2108 assert_eq!(delta.0, 5);
2109 assert_eq!(delta.1, 0);
2110 }
2111 }
2112
2113 #[test]
2114 fn reset_during_long_press_pending() {
2115 let mut gr = GestureRecognizer::new(GestureConfig::default());
2116 let t = now();
2117
2118 gr.process(&mouse_down(5, 5, MouseButton::Left), t);
2119 gr.reset();
2121
2122 assert!(gr.check_long_press(t + MS_600).is_none());
2124 }
2125}