1use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers};
4use presentar_core::{Event, Key, MouseButton, Point};
5
6#[derive(Debug, Clone)]
8pub struct KeyBinding {
9 pub code: KeyCode,
11 pub modifiers: KeyModifiers,
13 pub action: String,
15}
16
17impl KeyBinding {
18 #[must_use]
20 pub fn new(code: KeyCode, modifiers: KeyModifiers, action: impl Into<String>) -> Self {
21 Self {
22 code,
23 modifiers,
24 action: action.into(),
25 }
26 }
27
28 #[must_use]
30 pub fn simple(code: KeyCode, action: impl Into<String>) -> Self {
31 Self::new(code, KeyModifiers::NONE, action)
32 }
33
34 #[must_use]
36 pub fn matches(&self, event: &KeyEvent) -> bool {
37 event.code == self.code && event.modifiers.contains(self.modifiers)
38 }
39}
40
41#[derive(Debug, Default)]
43pub struct InputHandler {
44 bindings: Vec<KeyBinding>,
45}
46
47impl InputHandler {
48 #[must_use]
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn add_binding(&mut self, binding: KeyBinding) {
56 self.bindings.push(binding);
57 }
58
59 #[must_use]
61 pub fn convert(&self, event: CrosstermEvent) -> Option<Event> {
62 match event {
63 CrosstermEvent::Key(key) => self.convert_key(key),
64 CrosstermEvent::Mouse(mouse) => Some(self.convert_mouse(mouse)),
65 CrosstermEvent::Resize(width, height) => Some(Event::Resize {
66 width: f32::from(width),
67 height: f32::from(height),
68 }),
69 CrosstermEvent::FocusGained => Some(Event::FocusIn),
70 CrosstermEvent::FocusLost => Some(Event::FocusOut),
71 CrosstermEvent::Paste(text) => Some(Event::TextInput { text }),
72 }
73 }
74
75 fn convert_key(&self, key: KeyEvent) -> Option<Event> {
76 let presentar_key = match key.code {
77 KeyCode::Char(ch) => char_to_key(ch)?,
78 KeyCode::F(n) => fn_key(n)?,
79 _ => non_char_key(key.code)?,
80 };
81
82 let modifiers = presentar_core::Modifiers {
83 ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
84 alt: key.modifiers.contains(KeyModifiers::ALT),
85 shift: key.modifiers.contains(KeyModifiers::SHIFT),
86 meta: key.modifiers.contains(KeyModifiers::SUPER),
87 };
88 Some(Event::KeyDown {
89 key: presentar_key,
90 modifiers,
91 })
92 }
93
94 fn convert_mouse(&self, mouse: crossterm::event::MouseEvent) -> Event {
95 use crossterm::event::{MouseButton as CtMouseButton, MouseEventKind};
96
97 let position = Point::new(f32::from(mouse.column), f32::from(mouse.row));
98
99 match mouse.kind {
100 MouseEventKind::Down(button) => Event::MouseDown {
101 position,
102 button: match button {
103 CtMouseButton::Left => MouseButton::Left,
104 CtMouseButton::Right => MouseButton::Right,
105 CtMouseButton::Middle => MouseButton::Middle,
106 },
107 },
108 MouseEventKind::Up(button) => Event::MouseUp {
109 position,
110 button: match button {
111 CtMouseButton::Left => MouseButton::Left,
112 CtMouseButton::Right => MouseButton::Right,
113 CtMouseButton::Middle => MouseButton::Middle,
114 },
115 },
116 MouseEventKind::Moved | MouseEventKind::Drag(_) => Event::MouseMove { position },
117 MouseEventKind::ScrollUp => Event::Scroll {
118 delta_x: 0.0,
119 delta_y: -1.0,
120 },
121 MouseEventKind::ScrollDown => Event::Scroll {
122 delta_x: 0.0,
123 delta_y: 1.0,
124 },
125 MouseEventKind::ScrollLeft => Event::Scroll {
126 delta_x: -1.0,
127 delta_y: 0.0,
128 },
129 MouseEventKind::ScrollRight => Event::Scroll {
130 delta_x: 1.0,
131 delta_y: 0.0,
132 },
133 }
134 }
135
136 #[must_use]
138 pub fn find_binding(&self, event: &KeyEvent) -> Option<&KeyBinding> {
139 self.bindings.iter().find(|b| b.matches(event))
140 }
141}
142
143static CHAR_PUNCT_TABLE: &[(char, Key)] = &[
149 (' ', Key::Space),
150 ('\'', Key::Quote),
151 (',', Key::Comma),
152 ('-', Key::Minus),
153 ('.', Key::Period),
154 ('/', Key::Slash),
155 (';', Key::Semicolon),
156 ('=', Key::Equal),
157 ('[', Key::BracketLeft),
158 ('\\', Key::Backslash),
159 (']', Key::BracketRight),
160 ('`', Key::Grave),
161];
162
163fn char_to_key(ch: char) -> Option<Key> {
165 match ch {
166 'a' | 'A' => Some(Key::A),
167 'b' | 'B' => Some(Key::B),
168 'c' | 'C' => Some(Key::C),
169 'd' | 'D' => Some(Key::D),
170 'e' | 'E' => Some(Key::E),
171 'f' | 'F' => Some(Key::F),
172 'g' | 'G' => Some(Key::G),
173 'h' | 'H' => Some(Key::H),
174 'i' | 'I' => Some(Key::I),
175 'j' | 'J' => Some(Key::J),
176 'k' | 'K' => Some(Key::K),
177 'l' | 'L' => Some(Key::L),
178 'm' | 'M' => Some(Key::M),
179 'n' | 'N' => Some(Key::N),
180 'o' | 'O' => Some(Key::O),
181 'p' | 'P' => Some(Key::P),
182 'q' | 'Q' => Some(Key::Q),
183 'r' | 'R' => Some(Key::R),
184 's' | 'S' => Some(Key::S),
185 't' | 'T' => Some(Key::T),
186 'u' | 'U' => Some(Key::U),
187 'v' | 'V' => Some(Key::V),
188 'w' | 'W' => Some(Key::W),
189 'x' | 'X' => Some(Key::X),
190 'y' | 'Y' => Some(Key::Y),
191 'z' | 'Z' => Some(Key::Z),
192 '0' => Some(Key::Num0),
193 '1' => Some(Key::Num1),
194 '2' => Some(Key::Num2),
195 '3' => Some(Key::Num3),
196 '4' => Some(Key::Num4),
197 '5' => Some(Key::Num5),
198 '6' => Some(Key::Num6),
199 '7' => Some(Key::Num7),
200 '8' => Some(Key::Num8),
201 '9' => Some(Key::Num9),
202 _ => CHAR_PUNCT_TABLE
203 .binary_search_by_key(&ch, |&(c, _)| c)
204 .map(|i| CHAR_PUNCT_TABLE[i].1)
205 .ok(),
206 }
207}
208
209fn fn_key(n: u8) -> Option<Key> {
211 static FN_KEYS: [Key; 12] = [
212 Key::F1,
213 Key::F2,
214 Key::F3,
215 Key::F4,
216 Key::F5,
217 Key::F6,
218 Key::F7,
219 Key::F8,
220 Key::F9,
221 Key::F10,
222 Key::F11,
223 Key::F12,
224 ];
225 FN_KEYS.get(n.wrapping_sub(1) as usize).copied()
226}
227
228fn non_char_key(code: KeyCode) -> Option<Key> {
230 Some(match code {
231 KeyCode::Enter => Key::Enter,
232 KeyCode::Esc => Key::Escape,
233 KeyCode::Backspace => Key::Backspace,
234 KeyCode::Tab => Key::Tab,
235 KeyCode::Delete => Key::Delete,
236 KeyCode::Insert => Key::Insert,
237 KeyCode::Up => Key::Up,
238 KeyCode::Down => Key::Down,
239 KeyCode::Left => Key::Left,
240 KeyCode::Right => Key::Right,
241 KeyCode::Home => Key::Home,
242 KeyCode::End => Key::End,
243 KeyCode::PageUp => Key::PageUp,
244 KeyCode::PageDown => Key::PageDown,
245 _ => return None,
246 })
247}
248
249#[cfg(test)]
250#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
251mod tests {
252 use super::*;
253 use crossterm::event::{MouseButton as CtMouseButton, MouseEvent, MouseEventKind};
254
255 #[test]
256 fn test_key_binding_simple() {
257 let binding = KeyBinding::simple(KeyCode::Char('q'), "quit");
258 assert_eq!(binding.action, "quit");
259 assert_eq!(binding.modifiers, KeyModifiers::NONE);
260 }
261
262 #[test]
263 fn test_key_binding_with_modifiers() {
264 let binding = KeyBinding::new(KeyCode::Char('c'), KeyModifiers::CONTROL, "copy");
265 let event = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
266 assert!(binding.matches(&event));
267 }
268
269 #[test]
270 fn test_key_binding_no_match() {
271 let binding = KeyBinding::new(KeyCode::Char('c'), KeyModifiers::CONTROL, "copy");
272 let event = KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL);
273 assert!(!binding.matches(&event));
274 }
275
276 #[test]
277 fn test_input_handler_add_binding() {
278 let mut handler = InputHandler::new();
279 handler.add_binding(KeyBinding::simple(KeyCode::Char('q'), "quit"));
280 assert_eq!(handler.bindings.len(), 1);
281 }
282
283 #[test]
284 fn test_input_handler_find_binding() {
285 let mut handler = InputHandler::new();
286 handler.add_binding(KeyBinding::simple(KeyCode::Char('q'), "quit"));
287 handler.add_binding(KeyBinding::new(
288 KeyCode::Char('s'),
289 KeyModifiers::CONTROL,
290 "save",
291 ));
292
293 let event = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
294 let binding = handler.find_binding(&event);
295 assert!(binding.is_some());
296 assert_eq!(binding.unwrap().action, "quit");
297
298 let event2 = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
299 assert!(handler.find_binding(&event2).is_none());
300 }
301
302 #[test]
303 fn test_convert_letter_keys() {
304 let handler = InputHandler::new();
305 for ch in 'a'..='z' {
306 let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
307 let result = handler.convert(event);
308 assert!(result.is_some());
309 }
310 for ch in 'A'..='Z' {
311 let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
312 let result = handler.convert(event);
313 assert!(result.is_some());
314 }
315 }
316
317 #[test]
318 fn test_convert_number_keys() {
319 let handler = InputHandler::new();
320 for ch in '0'..='9' {
321 let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
322 let result = handler.convert(event);
323 assert!(result.is_some());
324 }
325 }
326
327 #[test]
328 fn test_convert_special_keys() {
329 let handler = InputHandler::new();
330 let special_keys = [
331 KeyCode::Enter,
332 KeyCode::Esc,
333 KeyCode::Backspace,
334 KeyCode::Tab,
335 KeyCode::Delete,
336 KeyCode::Insert,
337 KeyCode::Up,
338 KeyCode::Down,
339 KeyCode::Left,
340 KeyCode::Right,
341 KeyCode::Home,
342 KeyCode::End,
343 KeyCode::PageUp,
344 KeyCode::PageDown,
345 ];
346 for key in special_keys {
347 let event = CrosstermEvent::Key(KeyEvent::new(key, KeyModifiers::NONE));
348 let result = handler.convert(event);
349 assert!(result.is_some(), "Failed for {:?}", key);
350 }
351 }
352
353 #[test]
354 fn test_convert_function_keys() {
355 let handler = InputHandler::new();
356 for n in 1..=12 {
357 let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::F(n), KeyModifiers::NONE));
358 let result = handler.convert(event);
359 assert!(result.is_some(), "Failed for F{}", n);
360 }
361 }
362
363 #[test]
364 fn test_convert_punctuation_keys() {
365 let handler = InputHandler::new();
366 let punct = [' ', '-', '=', '[', ']', '\\', ';', '\'', '`', ',', '.', '/'];
367 for ch in punct {
368 let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
369 let result = handler.convert(event);
370 assert!(result.is_some(), "Failed for {:?}", ch);
371 }
372 }
373
374 #[test]
375 fn test_convert_unknown_key() {
376 let handler = InputHandler::new();
377 let event = CrosstermEvent::Key(KeyEvent::new(KeyCode::Char('£'), KeyModifiers::NONE));
378 let result = handler.convert(event);
379 assert!(result.is_none());
380 }
381
382 #[test]
383 fn test_convert_mouse_down() {
384 let handler = InputHandler::new();
385 let mouse = MouseEvent {
386 kind: MouseEventKind::Down(CtMouseButton::Left),
387 column: 10,
388 row: 5,
389 modifiers: KeyModifiers::NONE,
390 };
391 let event = CrosstermEvent::Mouse(mouse);
392 let result = handler.convert(event).unwrap();
393 assert!(
394 matches!(result, Event::MouseDown { position, button: MouseButton::Left } if position.x == 10.0 && position.y == 5.0)
395 );
396 }
397
398 #[test]
399 fn test_convert_mouse_up() {
400 let handler = InputHandler::new();
401 let mouse = MouseEvent {
402 kind: MouseEventKind::Up(CtMouseButton::Right),
403 column: 15,
404 row: 8,
405 modifiers: KeyModifiers::NONE,
406 };
407 let event = CrosstermEvent::Mouse(mouse);
408 let result = handler.convert(event).unwrap();
409 assert!(matches!(
410 result,
411 Event::MouseUp {
412 button: MouseButton::Right,
413 ..
414 }
415 ));
416 }
417
418 #[test]
419 fn test_convert_mouse_middle() {
420 let handler = InputHandler::new();
421 let mouse = MouseEvent {
422 kind: MouseEventKind::Down(CtMouseButton::Middle),
423 column: 0,
424 row: 0,
425 modifiers: KeyModifiers::NONE,
426 };
427 let event = CrosstermEvent::Mouse(mouse);
428 let result = handler.convert(event).unwrap();
429 assert!(matches!(
430 result,
431 Event::MouseDown {
432 button: MouseButton::Middle,
433 ..
434 }
435 ));
436 }
437
438 #[test]
439 fn test_convert_mouse_move() {
440 let handler = InputHandler::new();
441 let mouse = MouseEvent {
442 kind: MouseEventKind::Moved,
443 column: 20,
444 row: 10,
445 modifiers: KeyModifiers::NONE,
446 };
447 let event = CrosstermEvent::Mouse(mouse);
448 let result = handler.convert(event).unwrap();
449 assert!(matches!(result, Event::MouseMove { position } if position.x == 20.0));
450 }
451
452 #[test]
453 fn test_convert_mouse_drag() {
454 let handler = InputHandler::new();
455 let mouse = MouseEvent {
456 kind: MouseEventKind::Drag(CtMouseButton::Left),
457 column: 25,
458 row: 12,
459 modifiers: KeyModifiers::NONE,
460 };
461 let event = CrosstermEvent::Mouse(mouse);
462 let result = handler.convert(event).unwrap();
463 assert!(matches!(result, Event::MouseMove { .. }));
464 }
465
466 #[test]
467 fn test_convert_scroll_events() {
468 let handler = InputHandler::new();
469
470 let scroll_up = MouseEvent {
471 kind: MouseEventKind::ScrollUp,
472 column: 0,
473 row: 0,
474 modifiers: KeyModifiers::NONE,
475 };
476 let result = handler.convert(CrosstermEvent::Mouse(scroll_up)).unwrap();
477 assert!(matches!(result, Event::Scroll { delta_y, .. } if delta_y < 0.0));
478
479 let scroll_down = MouseEvent {
480 kind: MouseEventKind::ScrollDown,
481 column: 0,
482 row: 0,
483 modifiers: KeyModifiers::NONE,
484 };
485 let result = handler.convert(CrosstermEvent::Mouse(scroll_down)).unwrap();
486 assert!(matches!(result, Event::Scroll { delta_y, .. } if delta_y > 0.0));
487
488 let scroll_left = MouseEvent {
489 kind: MouseEventKind::ScrollLeft,
490 column: 0,
491 row: 0,
492 modifiers: KeyModifiers::NONE,
493 };
494 let result = handler.convert(CrosstermEvent::Mouse(scroll_left)).unwrap();
495 assert!(matches!(result, Event::Scroll { delta_x, .. } if delta_x < 0.0));
496
497 let scroll_right = MouseEvent {
498 kind: MouseEventKind::ScrollRight,
499 column: 0,
500 row: 0,
501 modifiers: KeyModifiers::NONE,
502 };
503 let result = handler
504 .convert(CrosstermEvent::Mouse(scroll_right))
505 .unwrap();
506 assert!(matches!(result, Event::Scroll { delta_x, .. } if delta_x > 0.0));
507 }
508
509 #[test]
510 fn test_convert_resize() {
511 let handler = InputHandler::new();
512 let event = CrosstermEvent::Resize(120, 40);
513 let result = handler.convert(event).unwrap();
514 assert!(
515 matches!(result, Event::Resize { width, height } if width == 120.0 && height == 40.0)
516 );
517 }
518
519 #[test]
520 fn test_convert_focus_events() {
521 let handler = InputHandler::new();
522
523 let result = handler.convert(CrosstermEvent::FocusGained).unwrap();
524 assert!(matches!(result, Event::FocusIn));
525
526 let result = handler.convert(CrosstermEvent::FocusLost).unwrap();
527 assert!(matches!(result, Event::FocusOut));
528 }
529
530 #[test]
531 fn test_convert_paste() {
532 let handler = InputHandler::new();
533 let event = CrosstermEvent::Paste("hello world".to_string());
534 let result = handler.convert(event).unwrap();
535 assert!(matches!(result, Event::TextInput { text } if text == "hello world"));
536 }
537}