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]
514 fn semantic_event_clone_and_eq() {
515 let original = SemanticEvent::DoubleClick {
516 pos: pos(3, 7),
517 button: MouseButton::Left,
518 };
519 let cloned = original.clone();
520 assert_eq!(original, cloned);
521 }
522}