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