1#![forbid(unsafe_code)]
2
3use crate::event::{KeyCode, Modifiers, MouseButton};
25use std::time::Duration;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
33pub struct Position {
34 pub x: u16,
35 pub y: u16,
36}
37
38impl Position {
39 #[must_use]
41 pub const fn new(x: u16, y: u16) -> Self {
42 Self { x, y }
43 }
44
45 #[must_use]
47 pub fn manhattan_distance(self, other: Self) -> u32 {
48 (self.x as i32 - other.x as i32).unsigned_abs()
49 + (self.y as i32 - other.y as i32).unsigned_abs()
50 }
51}
52
53impl From<(u16, u16)> for Position {
54 fn from((x, y): (u16, u16)) -> Self {
55 Self { x, y }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65pub struct ChordKey {
66 pub code: KeyCode,
67 pub modifiers: Modifiers,
68}
69
70impl ChordKey {
71 #[must_use]
73 pub const fn new(code: KeyCode, modifiers: Modifiers) -> Self {
74 Self { code, modifiers }
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84pub enum SwipeDirection {
85 Up,
86 Down,
87 Left,
88 Right,
89}
90
91impl SwipeDirection {
92 #[must_use]
94 pub const fn opposite(self) -> Self {
95 match self {
96 Self::Up => Self::Down,
97 Self::Down => Self::Up,
98 Self::Left => Self::Right,
99 Self::Right => Self::Left,
100 }
101 }
102
103 #[must_use]
105 pub const fn is_vertical(self) -> bool {
106 matches!(self, Self::Up | Self::Down)
107 }
108
109 #[must_use]
111 pub const fn is_horizontal(self) -> bool {
112 matches!(self, Self::Left | Self::Right)
113 }
114}
115
116#[derive(Debug, Clone, PartialEq)]
125pub enum SemanticEvent {
126 Click { pos: Position, button: MouseButton },
129
130 DoubleClick { pos: Position, button: MouseButton },
132
133 TripleClick { pos: Position, button: MouseButton },
135
136 LongPress { pos: Position, duration: Duration },
138
139 DragStart { pos: Position, button: MouseButton },
142
143 DragMove {
145 start: Position,
146 current: Position,
147 delta: (i16, i16),
149 },
150
151 DragEnd { start: Position, end: Position },
153
154 DragCancel,
156
157 Chord { sequence: Vec<ChordKey> },
162
163 Swipe {
166 direction: SwipeDirection,
167 distance: u16,
169 velocity: f32,
171 },
172}
173
174impl SemanticEvent {
175 #[must_use]
177 pub fn is_drag(&self) -> bool {
178 matches!(
179 self,
180 Self::DragStart { .. }
181 | Self::DragMove { .. }
182 | Self::DragEnd { .. }
183 | Self::DragCancel
184 )
185 }
186
187 #[must_use]
189 pub fn is_click(&self) -> bool {
190 matches!(
191 self,
192 Self::Click { .. } | Self::DoubleClick { .. } | Self::TripleClick { .. }
193 )
194 }
195
196 #[must_use]
198 pub fn position(&self) -> Option<Position> {
199 match self {
200 Self::Click { pos, .. }
201 | Self::DoubleClick { pos, .. }
202 | Self::TripleClick { pos, .. }
203 | Self::LongPress { pos, .. }
204 | Self::DragStart { pos, .. } => Some(*pos),
205 Self::DragMove { current, .. } => Some(*current),
206 Self::DragEnd { end, .. } => Some(*end),
207 Self::Chord { .. } | Self::DragCancel | Self::Swipe { .. } => None,
208 }
209 }
210
211 #[must_use]
213 pub fn button(&self) -> Option<MouseButton> {
214 match self {
215 Self::Click { button, .. }
216 | Self::DoubleClick { button, .. }
217 | Self::TripleClick { button, .. }
218 | Self::DragStart { button, .. } => Some(*button),
219 _ => None,
220 }
221 }
222}
223
224#[cfg(test)]
229mod tests {
230 use super::*;
231
232 fn pos(x: u16, y: u16) -> Position {
233 Position::new(x, y)
234 }
235
236 #[test]
239 fn position_new_and_from_tuple() {
240 let p = Position::new(5, 10);
241 assert_eq!(p, Position::from((5, 10)));
242 assert_eq!(p.x, 5);
243 assert_eq!(p.y, 10);
244 }
245
246 #[test]
247 fn position_manhattan_distance() {
248 assert_eq!(pos(0, 0).manhattan_distance(pos(3, 4)), 7);
249 assert_eq!(pos(5, 5).manhattan_distance(pos(5, 5)), 0);
250 assert_eq!(pos(10, 0).manhattan_distance(pos(0, 10)), 20);
251 }
252
253 #[test]
254 fn position_default_is_origin() {
255 assert_eq!(Position::default(), pos(0, 0));
256 }
257
258 #[test]
261 fn chord_key_equality() {
262 let k1 = ChordKey::new(KeyCode::Char('k'), Modifiers::CTRL);
263 let k2 = ChordKey::new(KeyCode::Char('k'), Modifiers::CTRL);
264 let k3 = ChordKey::new(KeyCode::Char('c'), Modifiers::CTRL);
265
266 assert_eq!(k1, k2);
267 assert_ne!(k1, k3);
268 }
269
270 #[test]
271 fn chord_key_hash_consistency() {
272 use std::collections::HashSet;
273 let mut set = HashSet::new();
274 set.insert(ChordKey::new(KeyCode::Char('k'), Modifiers::CTRL));
275 set.insert(ChordKey::new(KeyCode::Char('k'), Modifiers::CTRL)); assert_eq!(set.len(), 1);
277 }
278
279 #[test]
282 fn swipe_direction_opposite() {
283 assert_eq!(SwipeDirection::Up.opposite(), SwipeDirection::Down);
284 assert_eq!(SwipeDirection::Down.opposite(), SwipeDirection::Up);
285 assert_eq!(SwipeDirection::Left.opposite(), SwipeDirection::Right);
286 assert_eq!(SwipeDirection::Right.opposite(), SwipeDirection::Left);
287 }
288
289 #[test]
290 fn swipe_direction_axes() {
291 assert!(SwipeDirection::Up.is_vertical());
292 assert!(SwipeDirection::Down.is_vertical());
293 assert!(!SwipeDirection::Left.is_vertical());
294 assert!(!SwipeDirection::Right.is_vertical());
295
296 assert!(SwipeDirection::Left.is_horizontal());
297 assert!(SwipeDirection::Right.is_horizontal());
298 assert!(!SwipeDirection::Up.is_horizontal());
299 assert!(!SwipeDirection::Down.is_horizontal());
300 }
301
302 #[test]
305 fn is_drag_classification() {
306 assert!(
307 SemanticEvent::DragStart {
308 pos: pos(0, 0),
309 button: MouseButton::Left,
310 }
311 .is_drag()
312 );
313
314 assert!(
315 SemanticEvent::DragMove {
316 start: pos(0, 0),
317 current: pos(5, 5),
318 delta: (5, 5),
319 }
320 .is_drag()
321 );
322
323 assert!(
324 SemanticEvent::DragEnd {
325 start: pos(0, 0),
326 end: pos(10, 10),
327 }
328 .is_drag()
329 );
330
331 assert!(SemanticEvent::DragCancel.is_drag());
332
333 assert!(
335 !SemanticEvent::Click {
336 pos: pos(0, 0),
337 button: MouseButton::Left,
338 }
339 .is_drag()
340 );
341
342 assert!(
343 !SemanticEvent::Chord {
344 sequence: vec![ChordKey::new(KeyCode::Char('k'), Modifiers::CTRL)],
345 }
346 .is_drag()
347 );
348 }
349
350 #[test]
351 fn is_click_classification() {
352 assert!(
353 SemanticEvent::Click {
354 pos: pos(1, 2),
355 button: MouseButton::Left,
356 }
357 .is_click()
358 );
359
360 assert!(
361 SemanticEvent::DoubleClick {
362 pos: pos(1, 2),
363 button: MouseButton::Left,
364 }
365 .is_click()
366 );
367
368 assert!(
369 SemanticEvent::TripleClick {
370 pos: pos(1, 2),
371 button: MouseButton::Left,
372 }
373 .is_click()
374 );
375
376 assert!(
377 !SemanticEvent::DragStart {
378 pos: pos(0, 0),
379 button: MouseButton::Left,
380 }
381 .is_click()
382 );
383 }
384
385 #[test]
386 fn position_extraction() {
387 assert_eq!(
388 SemanticEvent::Click {
389 pos: pos(5, 10),
390 button: MouseButton::Left,
391 }
392 .position(),
393 Some(pos(5, 10))
394 );
395
396 assert_eq!(
397 SemanticEvent::DragMove {
398 start: pos(0, 0),
399 current: pos(15, 20),
400 delta: (1, 1),
401 }
402 .position(),
403 Some(pos(15, 20))
404 );
405
406 assert_eq!(
407 SemanticEvent::DragEnd {
408 start: pos(0, 0),
409 end: pos(30, 40),
410 }
411 .position(),
412 Some(pos(30, 40))
413 );
414
415 assert_eq!(SemanticEvent::DragCancel.position(), None);
416
417 assert_eq!(SemanticEvent::Chord { sequence: vec![] }.position(), None);
418
419 assert_eq!(
420 SemanticEvent::Swipe {
421 direction: SwipeDirection::Up,
422 distance: 10,
423 velocity: 100.0,
424 }
425 .position(),
426 None
427 );
428 }
429
430 #[test]
431 fn button_extraction() {
432 assert_eq!(
433 SemanticEvent::Click {
434 pos: pos(0, 0),
435 button: MouseButton::Right,
436 }
437 .button(),
438 Some(MouseButton::Right)
439 );
440
441 assert_eq!(
442 SemanticEvent::DragStart {
443 pos: pos(0, 0),
444 button: MouseButton::Middle,
445 }
446 .button(),
447 Some(MouseButton::Middle)
448 );
449
450 assert_eq!(SemanticEvent::DragCancel.button(), None);
451
452 assert_eq!(
453 SemanticEvent::LongPress {
454 pos: pos(0, 0),
455 duration: Duration::from_millis(500),
456 }
457 .button(),
458 None
459 );
460 }
461
462 #[test]
463 fn long_press_carries_duration() {
464 let event = SemanticEvent::LongPress {
465 pos: pos(10, 20),
466 duration: Duration::from_millis(750),
467 };
468 assert_eq!(event.position(), Some(pos(10, 20)));
469 assert!(!event.is_drag());
470 assert!(!event.is_click());
471 }
472
473 #[test]
474 fn swipe_velocity_and_direction() {
475 let event = SemanticEvent::Swipe {
476 direction: SwipeDirection::Right,
477 distance: 25,
478 velocity: 150.0,
479 };
480 assert!(!event.is_drag());
481 assert!(!event.is_click());
482 assert_eq!(event.position(), None);
483 }
484
485 #[test]
486 fn chord_sequence_contents() {
487 let chord = SemanticEvent::Chord {
488 sequence: vec![
489 ChordKey::new(KeyCode::Char('k'), Modifiers::CTRL),
490 ChordKey::new(KeyCode::Char('c'), Modifiers::CTRL),
491 ],
492 };
493 if let SemanticEvent::Chord { sequence } = &chord {
494 assert_eq!(sequence.len(), 2);
495 assert_eq!(sequence[0].code, KeyCode::Char('k'));
496 assert_eq!(sequence[1].code, KeyCode::Char('c'));
497 } else {
498 panic!("Expected Chord variant");
499 }
500 }
501
502 #[test]
503 fn semantic_event_debug_format() {
504 let click = SemanticEvent::Click {
505 pos: pos(5, 10),
506 button: MouseButton::Left,
507 };
508 let dbg = format!("{:?}", click);
509 assert!(dbg.contains("Click"));
510 assert!(dbg.contains("Position"));
511 }
512
513 #[test]
518 fn position_manhattan_distance_max_coordinates() {
519 let p1 = Position::new(0, 0);
520 let p2 = Position::new(u16::MAX, u16::MAX);
521 assert_eq!(p1.manhattan_distance(p2), 131070);
523 }
524
525 #[test]
526 fn position_manhattan_distance_symmetric() {
527 let a = pos(10, 20);
528 let b = pos(50, 3);
529 assert_eq!(a.manhattan_distance(b), b.manhattan_distance(a));
530 }
531
532 #[test]
533 fn position_manhattan_distance_same_point() {
534 let p = pos(100, 200);
535 assert_eq!(p.manhattan_distance(p), 0);
536 }
537
538 #[test]
539 fn position_manhattan_distance_horizontal_only() {
540 assert_eq!(pos(0, 5).manhattan_distance(pos(10, 5)), 10);
541 }
542
543 #[test]
544 fn position_manhattan_distance_vertical_only() {
545 assert_eq!(pos(5, 0).manhattan_distance(pos(5, 10)), 10);
546 }
547
548 #[test]
549 fn position_from_tuple_max() {
550 let p: Position = (u16::MAX, u16::MAX).into();
551 assert_eq!(p.x, u16::MAX);
552 assert_eq!(p.y, u16::MAX);
553 }
554
555 #[test]
556 fn position_hash_consistency() {
557 use std::collections::HashSet;
558 let mut set = HashSet::new();
559 set.insert(pos(10, 20));
560 set.insert(pos(10, 20)); assert_eq!(set.len(), 1);
562 set.insert(pos(20, 10)); assert_eq!(set.len(), 2);
564 }
565
566 #[test]
567 fn position_copy_semantics() {
568 let p = pos(5, 10);
569 let q = p; assert_eq!(p, q); }
572
573 #[test]
576 fn chord_key_different_modifiers_not_equal() {
577 let k1 = ChordKey::new(KeyCode::Char('k'), Modifiers::CTRL);
578 let k2 = ChordKey::new(KeyCode::Char('k'), Modifiers::ALT);
579 assert_ne!(k1, k2);
580 }
581
582 #[test]
583 fn chord_key_clone_independence() {
584 let original = ChordKey::new(KeyCode::Char('x'), Modifiers::SHIFT);
585 let cloned = original.clone();
586 assert_eq!(original, cloned);
587 }
588
589 #[test]
590 fn chord_key_no_modifiers() {
591 let k = ChordKey::new(KeyCode::Enter, Modifiers::NONE);
592 assert_eq!(k.modifiers, Modifiers::NONE);
593 }
594
595 #[test]
596 fn chord_key_debug_format() {
597 let k = ChordKey::new(KeyCode::Char('a'), Modifiers::CTRL);
598 let dbg = format!("{k:?}");
599 assert!(dbg.contains("ChordKey"));
600 }
601
602 #[test]
605 fn swipe_direction_double_opposite_is_identity() {
606 for dir in [
607 SwipeDirection::Up,
608 SwipeDirection::Down,
609 SwipeDirection::Left,
610 SwipeDirection::Right,
611 ] {
612 assert_eq!(dir.opposite().opposite(), dir);
613 }
614 }
615
616 #[test]
617 fn swipe_direction_vertical_horizontal_mutually_exclusive() {
618 for dir in [
619 SwipeDirection::Up,
620 SwipeDirection::Down,
621 SwipeDirection::Left,
622 SwipeDirection::Right,
623 ] {
624 assert_ne!(dir.is_vertical(), dir.is_horizontal());
625 }
626 }
627
628 #[test]
629 fn swipe_direction_copy_semantics() {
630 let d = SwipeDirection::Up;
631 let e = d; assert_eq!(d, e);
633 }
634
635 #[test]
636 fn swipe_direction_hash_consistency() {
637 use std::collections::HashSet;
638 let mut set = HashSet::new();
639 set.insert(SwipeDirection::Up);
640 set.insert(SwipeDirection::Up);
641 assert_eq!(set.len(), 1);
642 set.insert(SwipeDirection::Down);
643 assert_eq!(set.len(), 2);
644 }
645
646 #[test]
649 fn position_for_double_click() {
650 assert_eq!(
651 SemanticEvent::DoubleClick {
652 pos: pos(33, 44),
653 button: MouseButton::Right,
654 }
655 .position(),
656 Some(pos(33, 44))
657 );
658 }
659
660 #[test]
661 fn position_for_triple_click() {
662 assert_eq!(
663 SemanticEvent::TripleClick {
664 pos: pos(1, 2),
665 button: MouseButton::Middle,
666 }
667 .position(),
668 Some(pos(1, 2))
669 );
670 }
671
672 #[test]
673 fn position_for_long_press() {
674 assert_eq!(
675 SemanticEvent::LongPress {
676 pos: pos(99, 88),
677 duration: Duration::from_secs(1),
678 }
679 .position(),
680 Some(pos(99, 88))
681 );
682 }
683
684 #[test]
685 fn position_for_drag_start() {
686 assert_eq!(
687 SemanticEvent::DragStart {
688 pos: pos(7, 8),
689 button: MouseButton::Left,
690 }
691 .position(),
692 Some(pos(7, 8))
693 );
694 }
695
696 #[test]
697 fn button_for_double_click() {
698 assert_eq!(
699 SemanticEvent::DoubleClick {
700 pos: pos(0, 0),
701 button: MouseButton::Middle,
702 }
703 .button(),
704 Some(MouseButton::Middle)
705 );
706 }
707
708 #[test]
709 fn button_for_triple_click() {
710 assert_eq!(
711 SemanticEvent::TripleClick {
712 pos: pos(0, 0),
713 button: MouseButton::Right,
714 }
715 .button(),
716 Some(MouseButton::Right)
717 );
718 }
719
720 #[test]
721 fn button_none_for_drag_move() {
722 assert_eq!(
723 SemanticEvent::DragMove {
724 start: pos(0, 0),
725 current: pos(1, 1),
726 delta: (1, 1),
727 }
728 .button(),
729 None
730 );
731 }
732
733 #[test]
734 fn button_none_for_drag_end() {
735 assert_eq!(
736 SemanticEvent::DragEnd {
737 start: pos(0, 0),
738 end: pos(5, 5),
739 }
740 .button(),
741 None
742 );
743 }
744
745 #[test]
746 fn button_none_for_swipe() {
747 assert_eq!(
748 SemanticEvent::Swipe {
749 direction: SwipeDirection::Left,
750 distance: 5,
751 velocity: 1.0,
752 }
753 .button(),
754 None
755 );
756 }
757
758 #[test]
759 fn button_none_for_chord() {
760 assert_eq!(
761 SemanticEvent::Chord {
762 sequence: vec![ChordKey::new(KeyCode::Char('a'), Modifiers::NONE)],
763 }
764 .button(),
765 None
766 );
767 }
768
769 #[test]
770 fn is_drag_false_for_long_press() {
771 assert!(
772 !SemanticEvent::LongPress {
773 pos: pos(0, 0),
774 duration: Duration::from_millis(100),
775 }
776 .is_drag()
777 );
778 }
779
780 #[test]
781 fn is_drag_false_for_swipe() {
782 assert!(
783 !SemanticEvent::Swipe {
784 direction: SwipeDirection::Down,
785 distance: 10,
786 velocity: 50.0,
787 }
788 .is_drag()
789 );
790 }
791
792 #[test]
793 fn is_click_false_for_long_press() {
794 assert!(
795 !SemanticEvent::LongPress {
796 pos: pos(0, 0),
797 duration: Duration::from_millis(500),
798 }
799 .is_click()
800 );
801 }
802
803 #[test]
804 fn is_click_false_for_swipe() {
805 assert!(
806 !SemanticEvent::Swipe {
807 direction: SwipeDirection::Up,
808 distance: 5,
809 velocity: 20.0,
810 }
811 .is_click()
812 );
813 }
814
815 #[test]
816 fn drag_move_with_negative_delta() {
817 let ev = SemanticEvent::DragMove {
818 start: pos(20, 20),
819 current: pos(10, 10),
820 delta: (-10, -10),
821 };
822 assert!(ev.is_drag());
823 assert_eq!(ev.position(), Some(pos(10, 10)));
824 }
825
826 #[test]
827 fn drag_move_with_zero_delta() {
828 let ev = SemanticEvent::DragMove {
829 start: pos(5, 5),
830 current: pos(5, 5),
831 delta: (0, 0),
832 };
833 assert!(ev.is_drag());
834 assert_eq!(ev.position(), Some(pos(5, 5)));
835 }
836
837 #[test]
838 fn swipe_zero_velocity() {
839 let ev = SemanticEvent::Swipe {
840 direction: SwipeDirection::Right,
841 distance: 0,
842 velocity: 0.0,
843 };
844 assert_eq!(ev.position(), None);
845 assert!(!ev.is_drag());
846 assert!(!ev.is_click());
847 }
848
849 #[test]
850 fn swipe_large_velocity() {
851 let ev = SemanticEvent::Swipe {
852 direction: SwipeDirection::Up,
853 distance: u16::MAX,
854 velocity: f32::MAX,
855 };
856 assert_eq!(ev.position(), None);
857 }
858
859 #[test]
860 fn long_press_zero_duration() {
861 let ev = SemanticEvent::LongPress {
862 pos: pos(0, 0),
863 duration: Duration::ZERO,
864 };
865 assert_eq!(ev.position(), Some(pos(0, 0)));
866 assert!(!ev.is_click());
867 }
868
869 #[test]
870 fn chord_empty_sequence() {
871 let ev = SemanticEvent::Chord { sequence: vec![] };
873 assert_eq!(ev.position(), None);
874 assert_eq!(ev.button(), None);
875 assert!(!ev.is_drag());
876 assert!(!ev.is_click());
877 }
878
879 #[test]
880 fn chord_clone_deep_copy() {
881 let original = SemanticEvent::Chord {
882 sequence: vec![
883 ChordKey::new(KeyCode::Char('k'), Modifiers::CTRL),
884 ChordKey::new(KeyCode::Char('c'), Modifiers::CTRL),
885 ],
886 };
887 let cloned = original.clone();
888 assert_eq!(original, cloned);
889 if let SemanticEvent::Chord { sequence } = &cloned {
891 assert_eq!(sequence.len(), 2);
892 }
893 }
894
895 #[test]
896 fn swipe_nan_velocity_not_equal_to_itself() {
897 let ev1 = SemanticEvent::Swipe {
898 direction: SwipeDirection::Up,
899 distance: 5,
900 velocity: f32::NAN,
901 };
902 let ev2 = ev1.clone();
903 assert_ne!(ev1, ev2);
905 }
906
907 #[test]
908 fn drag_cancel_is_minimal() {
909 let ev = SemanticEvent::DragCancel;
910 assert!(ev.is_drag());
911 assert!(!ev.is_click());
912 assert_eq!(ev.position(), None);
913 assert_eq!(ev.button(), None);
914 }
915
916 #[test]
917 fn drag_end_position_is_end_not_start() {
918 let ev = SemanticEvent::DragEnd {
919 start: pos(0, 0),
920 end: pos(100, 200),
921 };
922 assert_eq!(ev.position(), Some(pos(100, 200)));
923 }
924
925 #[test]
926 fn click_with_right_button() {
927 let ev = SemanticEvent::Click {
928 pos: pos(5, 10),
929 button: MouseButton::Right,
930 };
931 assert!(ev.is_click());
932 assert_eq!(ev.button(), Some(MouseButton::Right));
933 }
934
935 #[test]
936 fn click_with_middle_button() {
937 let ev = SemanticEvent::Click {
938 pos: pos(0, 0),
939 button: MouseButton::Middle,
940 };
941 assert!(ev.is_click());
942 assert_eq!(ev.button(), Some(MouseButton::Middle));
943 }
944
945 #[test]
948 fn semantic_event_clone_and_eq() {
949 let original = SemanticEvent::DoubleClick {
950 pos: pos(3, 7),
951 button: MouseButton::Left,
952 };
953 let cloned = original.clone();
954 assert_eq!(original, cloned);
955 }
956}