Skip to main content

ftui_core/
event.rs

1#![forbid(unsafe_code)]
2
3//! Canonical input/event types.
4//!
5//! This module defines the standard event types used throughout ftui for
6//! input handling. All events derive `Clone`, `PartialEq`, and `Eq` for
7//! use in tests and pattern matching.
8//!
9//! # Design Notes
10//!
11//! - Mouse coordinates are 0-indexed (terminal is 1-indexed internally)
12//! - `KeyEventKind` defaults to `Press` when not available from the terminal
13//! - `Modifiers` use bitflags for easy combination
14//! - Clipboard events are optional and feature-gated in the future
15
16use bitflags::bitflags;
17#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
18use crossterm::event as cte;
19
20/// Canonical input event.
21///
22/// This enum represents all possible input events that ftui can receive
23/// from the terminal.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Event {
26    /// A keyboard event.
27    Key(KeyEvent),
28
29    /// A mouse event.
30    Mouse(MouseEvent),
31
32    /// Terminal was resized.
33    Resize {
34        /// New terminal width in columns.
35        width: u16,
36        /// New terminal height in rows.
37        height: u16,
38    },
39
40    /// Paste event (from bracketed paste mode).
41    Paste(PasteEvent),
42
43    /// Focus gained or lost.
44    ///
45    /// `true` = focus gained, `false` = focus lost.
46    Focus(bool),
47
48    /// Clipboard content received (optional, from OSC 52 response).
49    Clipboard(ClipboardEvent),
50
51    /// A tick event from the runtime.
52    ///
53    /// Fired when a scheduled tick interval elapses. Applications use this
54    /// for periodic updates (animations, polling, timers). The model's `update`
55    /// method receives the tick and can respond with state changes.
56    Tick,
57}
58
59impl Event {
60    /// Convert a Crossterm event into an ftui [`Event`].
61    #[must_use]
62    #[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
63    pub fn from_crossterm(event: cte::Event) -> Option<Self> {
64        map_crossterm_event_internal(event)
65    }
66
67    /// Return a static label for the event type (for metrics/tracing).
68    #[must_use]
69    pub const fn event_type_label(&self) -> &'static str {
70        match self {
71            Event::Key(_) => "key",
72            Event::Mouse(_) => "mouse",
73            Event::Resize { .. } => "resize",
74            Event::Paste(_) => "paste",
75            Event::Focus(_) => "focus",
76            Event::Clipboard(_) => "clipboard",
77            Event::Tick => "tick",
78        }
79    }
80}
81
82/// A keyboard event.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct KeyEvent {
85    /// The key code that was pressed.
86    pub code: KeyCode,
87
88    /// Modifier keys held during the event.
89    pub modifiers: Modifiers,
90
91    /// The type of key event (press, repeat, or release).
92    pub kind: KeyEventKind,
93}
94
95impl KeyEvent {
96    /// Create a new key event with default modifiers and Press kind.
97    #[must_use]
98    pub const fn new(code: KeyCode) -> Self {
99        Self {
100            code,
101            modifiers: Modifiers::NONE,
102            kind: KeyEventKind::Press,
103        }
104    }
105
106    /// Create a key event with modifiers.
107    #[must_use]
108    pub const fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
109        self.modifiers = modifiers;
110        self
111    }
112
113    /// Create a key event with a specific kind.
114    #[must_use]
115    pub const fn with_kind(mut self, kind: KeyEventKind) -> Self {
116        self.kind = kind;
117        self
118    }
119
120    /// Check if this is a specific character key.
121    #[must_use]
122    pub fn is_char(&self, c: char) -> bool {
123        matches!(self.code, KeyCode::Char(ch) if ch == c)
124    }
125
126    /// Check if Ctrl modifier is held.
127    #[must_use]
128    pub const fn ctrl(&self) -> bool {
129        self.modifiers.contains(Modifiers::CTRL)
130    }
131
132    /// Check if Alt modifier is held.
133    #[must_use]
134    pub const fn alt(&self) -> bool {
135        self.modifiers.contains(Modifiers::ALT)
136    }
137
138    /// Check if Shift modifier is held.
139    #[must_use]
140    pub const fn shift(&self) -> bool {
141        self.modifiers.contains(Modifiers::SHIFT)
142    }
143
144    /// Check if Super/Meta/Cmd modifier is held.
145    #[must_use]
146    pub const fn super_key(&self) -> bool {
147        self.modifiers.contains(Modifiers::SUPER)
148    }
149}
150
151/// Key codes for keyboard events.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153pub enum KeyCode {
154    /// A regular character key.
155    Char(char),
156
157    /// Enter/Return key.
158    Enter,
159
160    /// Escape key.
161    Escape,
162
163    /// Backspace key.
164    Backspace,
165
166    /// Tab key.
167    Tab,
168
169    /// Shift+Tab (back-tab).
170    BackTab,
171
172    /// Delete key.
173    Delete,
174
175    /// Insert key.
176    Insert,
177
178    /// Home key.
179    Home,
180
181    /// End key.
182    End,
183
184    /// Page Up key.
185    PageUp,
186
187    /// Page Down key.
188    PageDown,
189
190    /// Up arrow key.
191    Up,
192
193    /// Down arrow key.
194    Down,
195
196    /// Left arrow key.
197    Left,
198
199    /// Right arrow key.
200    Right,
201
202    /// Function key (F1-F24).
203    F(u8),
204
205    /// Null character (Ctrl+Space or Ctrl+@).
206    Null,
207
208    /// Media key: Play/Pause.
209    MediaPlayPause,
210
211    /// Media key: Stop.
212    MediaStop,
213
214    /// Media key: Next track.
215    MediaNextTrack,
216
217    /// Media key: Previous track.
218    MediaPrevTrack,
219}
220
221/// The type of key event.
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
223pub enum KeyEventKind {
224    /// Key was pressed (default when not distinguishable).
225    #[default]
226    Press,
227
228    /// Key is being held (repeat event).
229    Repeat,
230
231    /// Key was released.
232    Release,
233}
234
235bitflags! {
236    /// Modifier keys that can be held during a key event.
237    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
238    pub struct Modifiers: u8 {
239        /// No modifiers.
240        const NONE  = 0b0000;
241        /// Shift key.
242        const SHIFT = 0b0001;
243        /// Alt/Option key.
244        const ALT   = 0b0010;
245        /// Control key.
246        const CTRL  = 0b0100;
247        /// Super/Meta/Command key.
248        const SUPER = 0b1000;
249    }
250}
251
252impl Default for Modifiers {
253    fn default() -> Self {
254        Self::NONE
255    }
256}
257
258/// A mouse event.
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub struct MouseEvent {
261    /// The type of mouse event.
262    pub kind: MouseEventKind,
263
264    /// X coordinate (0-indexed, leftmost column is 0).
265    pub x: u16,
266
267    /// Y coordinate (0-indexed, topmost row is 0).
268    pub y: u16,
269
270    /// Modifier keys held during the event.
271    pub modifiers: Modifiers,
272}
273
274impl MouseEvent {
275    /// Create a new mouse event.
276    #[must_use]
277    pub const fn new(kind: MouseEventKind, x: u16, y: u16) -> Self {
278        Self {
279            kind,
280            x,
281            y,
282            modifiers: Modifiers::NONE,
283        }
284    }
285
286    /// Create a mouse event with modifiers.
287    #[must_use]
288    pub const fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
289        self.modifiers = modifiers;
290        self
291    }
292
293    /// Get the position as a tuple.
294    #[must_use]
295    pub const fn position(&self) -> (u16, u16) {
296        (self.x, self.y)
297    }
298}
299
300/// The type of mouse event.
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
302pub enum MouseEventKind {
303    /// Mouse button pressed down.
304    Down(MouseButton),
305
306    /// Mouse button released.
307    Up(MouseButton),
308
309    /// Mouse dragged while button held.
310    Drag(MouseButton),
311
312    /// Mouse moved (no button pressed).
313    Moved,
314
315    /// Mouse wheel scrolled up.
316    ScrollUp,
317
318    /// Mouse wheel scrolled down.
319    ScrollDown,
320
321    /// Mouse wheel scrolled left (horizontal scroll).
322    ScrollLeft,
323
324    /// Mouse wheel scrolled right (horizontal scroll).
325    ScrollRight,
326}
327
328/// Mouse button identifiers.
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
330pub enum MouseButton {
331    /// Left mouse button.
332    Left,
333
334    /// Right mouse button.
335    Right,
336
337    /// Middle mouse button (scroll wheel click).
338    Middle,
339}
340
341/// A paste event from bracketed paste mode.
342#[derive(Debug, Clone, PartialEq, Eq)]
343pub struct PasteEvent {
344    /// The pasted text content.
345    pub text: String,
346
347    /// True if this came from bracketed paste mode.
348    ///
349    /// When true, the text was received atomically and should be
350    /// treated as a single paste operation rather than individual
351    /// key presses.
352    pub bracketed: bool,
353}
354
355impl PasteEvent {
356    /// Create a new paste event.
357    #[must_use]
358    pub fn new(text: impl Into<String>, bracketed: bool) -> Self {
359        Self {
360            text: text.into(),
361            bracketed,
362        }
363    }
364
365    /// Create a bracketed paste event (the common case).
366    #[must_use]
367    pub fn bracketed(text: impl Into<String>) -> Self {
368        Self::new(text, true)
369    }
370}
371
372/// A clipboard event from OSC 52 response.
373///
374/// This is optional and may not be supported by all terminals.
375#[derive(Debug, Clone, PartialEq, Eq)]
376pub struct ClipboardEvent {
377    /// The clipboard content (decoded from base64).
378    pub content: String,
379
380    /// The source of the clipboard content.
381    pub source: ClipboardSource,
382}
383
384impl ClipboardEvent {
385    /// Create a new clipboard event.
386    #[must_use]
387    pub fn new(content: impl Into<String>, source: ClipboardSource) -> Self {
388        Self {
389            content: content.into(),
390            source,
391        }
392    }
393}
394
395/// The source of clipboard content.
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
397pub enum ClipboardSource {
398    /// Clipboard content from OSC 52 protocol.
399    Osc52,
400
401    /// Unknown or unspecified source.
402    #[default]
403    Unknown,
404}
405
406#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
407fn map_crossterm_event_internal(event: cte::Event) -> Option<Event> {
408    match event {
409        cte::Event::Key(key) => map_key_event(key).map(Event::Key),
410        cte::Event::Mouse(mouse) => Some(Event::Mouse(map_mouse_event(mouse))),
411        cte::Event::Resize(width, height) => Some(Event::Resize { width, height }),
412        cte::Event::Paste(text) => Some(Event::Paste(PasteEvent::bracketed(text))),
413        cte::Event::FocusGained => Some(Event::Focus(true)),
414        cte::Event::FocusLost => Some(Event::Focus(false)),
415    }
416}
417
418#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
419fn map_key_event(event: cte::KeyEvent) -> Option<KeyEvent> {
420    let code = map_key_code(event.code)?;
421    let modifiers = map_modifiers(event.modifiers);
422    let kind = map_key_kind(event.kind);
423    Some(KeyEvent {
424        code,
425        modifiers,
426        kind,
427    })
428}
429
430#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
431fn map_key_kind(kind: cte::KeyEventKind) -> KeyEventKind {
432    match kind {
433        cte::KeyEventKind::Press => KeyEventKind::Press,
434        cte::KeyEventKind::Repeat => KeyEventKind::Repeat,
435        cte::KeyEventKind::Release => KeyEventKind::Release,
436    }
437}
438
439#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
440pub(crate) fn map_key_code(code: cte::KeyCode) -> Option<KeyCode> {
441    match code {
442        cte::KeyCode::Backspace => Some(KeyCode::Backspace),
443        cte::KeyCode::Enter => Some(KeyCode::Enter),
444        cte::KeyCode::Left => Some(KeyCode::Left),
445        cte::KeyCode::Right => Some(KeyCode::Right),
446        cte::KeyCode::Up => Some(KeyCode::Up),
447        cte::KeyCode::Down => Some(KeyCode::Down),
448        cte::KeyCode::Home => Some(KeyCode::Home),
449        cte::KeyCode::End => Some(KeyCode::End),
450        cte::KeyCode::PageUp => Some(KeyCode::PageUp),
451        cte::KeyCode::PageDown => Some(KeyCode::PageDown),
452        cte::KeyCode::Tab => Some(KeyCode::Tab),
453        cte::KeyCode::BackTab => Some(KeyCode::BackTab),
454        cte::KeyCode::Delete => Some(KeyCode::Delete),
455        cte::KeyCode::Insert => Some(KeyCode::Insert),
456        cte::KeyCode::F(n) => Some(KeyCode::F(n)),
457        cte::KeyCode::Char(c) => Some(KeyCode::Char(c)),
458        cte::KeyCode::Null => Some(KeyCode::Null),
459        cte::KeyCode::Esc => Some(KeyCode::Escape),
460        cte::KeyCode::Media(media) => map_media_key(media),
461        _ => None,
462    }
463}
464
465#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
466fn map_media_key(code: cte::MediaKeyCode) -> Option<KeyCode> {
467    match code {
468        cte::MediaKeyCode::Play | cte::MediaKeyCode::Pause | cte::MediaKeyCode::PlayPause => {
469            Some(KeyCode::MediaPlayPause)
470        }
471        cte::MediaKeyCode::Stop => Some(KeyCode::MediaStop),
472        cte::MediaKeyCode::TrackNext => Some(KeyCode::MediaNextTrack),
473        cte::MediaKeyCode::TrackPrevious => Some(KeyCode::MediaPrevTrack),
474        _ => None,
475    }
476}
477
478#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
479fn map_modifiers(modifiers: cte::KeyModifiers) -> Modifiers {
480    let mut mapped = Modifiers::NONE;
481    if modifiers.contains(cte::KeyModifiers::SHIFT) {
482        mapped |= Modifiers::SHIFT;
483    }
484    if modifiers.contains(cte::KeyModifiers::ALT) {
485        mapped |= Modifiers::ALT;
486    }
487    if modifiers.contains(cte::KeyModifiers::CONTROL) {
488        mapped |= Modifiers::CTRL;
489    }
490    if modifiers.contains(cte::KeyModifiers::SUPER)
491        || modifiers.contains(cte::KeyModifiers::HYPER)
492        || modifiers.contains(cte::KeyModifiers::META)
493    {
494        mapped |= Modifiers::SUPER;
495    }
496    mapped
497}
498
499#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
500fn map_mouse_event(event: cte::MouseEvent) -> MouseEvent {
501    let kind = match event.kind {
502        cte::MouseEventKind::Down(button) => MouseEventKind::Down(map_mouse_button(button)),
503        cte::MouseEventKind::Up(button) => MouseEventKind::Up(map_mouse_button(button)),
504        cte::MouseEventKind::Drag(button) => MouseEventKind::Drag(map_mouse_button(button)),
505        cte::MouseEventKind::Moved => MouseEventKind::Moved,
506        cte::MouseEventKind::ScrollUp => MouseEventKind::ScrollUp,
507        cte::MouseEventKind::ScrollDown => MouseEventKind::ScrollDown,
508        cte::MouseEventKind::ScrollLeft => MouseEventKind::ScrollLeft,
509        cte::MouseEventKind::ScrollRight => MouseEventKind::ScrollRight,
510    };
511
512    MouseEvent::new(kind, event.column, event.row).with_modifiers(map_modifiers(event.modifiers))
513}
514
515#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
516fn map_mouse_button(button: cte::MouseButton) -> MouseButton {
517    match button {
518        cte::MouseButton::Left => MouseButton::Left,
519        cte::MouseButton::Right => MouseButton::Right,
520        cte::MouseButton::Middle => MouseButton::Middle,
521    }
522}
523
524#[cfg(all(test, not(target_arch = "wasm32"), feature = "crossterm"))]
525mod tests {
526    use super::*;
527    use crossterm::event as ct_event;
528
529    #[test]
530    fn key_event_is_char() {
531        let event = KeyEvent::new(KeyCode::Char('q'));
532        assert!(event.is_char('q'));
533        assert!(!event.is_char('x'));
534    }
535
536    #[test]
537    fn key_event_modifiers() {
538        let event = KeyEvent::new(KeyCode::Char('c')).with_modifiers(Modifiers::CTRL);
539        assert!(event.ctrl());
540        assert!(!event.alt());
541        assert!(!event.shift());
542        assert!(!event.super_key());
543    }
544
545    #[test]
546    fn key_event_combined_modifiers() {
547        let event =
548            KeyEvent::new(KeyCode::Char('s')).with_modifiers(Modifiers::CTRL | Modifiers::SHIFT);
549        assert!(event.ctrl());
550        assert!(event.shift());
551        assert!(!event.alt());
552    }
553
554    #[test]
555    fn key_event_kind() {
556        let press = KeyEvent::new(KeyCode::Enter);
557        assert_eq!(press.kind, KeyEventKind::Press);
558
559        let release = press.with_kind(KeyEventKind::Release);
560        assert_eq!(release.kind, KeyEventKind::Release);
561    }
562
563    #[test]
564    fn mouse_event_position() {
565        let event = MouseEvent::new(MouseEventKind::Down(MouseButton::Left), 10, 20);
566        assert_eq!(event.position(), (10, 20));
567        assert_eq!(event.x, 10);
568        assert_eq!(event.y, 20);
569    }
570
571    #[test]
572    fn mouse_event_with_modifiers() {
573        let event = MouseEvent::new(MouseEventKind::Moved, 0, 0).with_modifiers(Modifiers::ALT);
574        assert_eq!(event.modifiers, Modifiers::ALT);
575    }
576
577    #[test]
578    fn paste_event_creation() {
579        let paste = PasteEvent::bracketed("hello world");
580        assert_eq!(paste.text, "hello world");
581        assert!(paste.bracketed);
582    }
583
584    #[test]
585    fn clipboard_event_creation() {
586        let clip = ClipboardEvent::new("copied text", ClipboardSource::Osc52);
587        assert_eq!(clip.content, "copied text");
588        assert_eq!(clip.source, ClipboardSource::Osc52);
589    }
590
591    #[test]
592    fn event_variants() {
593        // Test that all event variants can be created
594        let _key = Event::Key(KeyEvent::new(KeyCode::Char('a')));
595        let _mouse = Event::Mouse(MouseEvent::new(
596            MouseEventKind::Down(MouseButton::Left),
597            0,
598            0,
599        ));
600        let _resize = Event::Resize {
601            width: 80,
602            height: 24,
603        };
604        let _paste = Event::Paste(PasteEvent::bracketed("test"));
605        let _focus = Event::Focus(true);
606        let _clipboard = Event::Clipboard(ClipboardEvent::new("test", ClipboardSource::Unknown));
607        let _tick = Event::Tick;
608    }
609
610    #[test]
611    fn modifiers_default() {
612        assert_eq!(Modifiers::default(), Modifiers::NONE);
613    }
614
615    #[test]
616    fn key_event_kind_default() {
617        assert_eq!(KeyEventKind::default(), KeyEventKind::Press);
618    }
619
620    #[test]
621    fn clipboard_source_default() {
622        assert_eq!(ClipboardSource::default(), ClipboardSource::Unknown);
623    }
624
625    #[test]
626    fn function_keys() {
627        let f1 = KeyEvent::new(KeyCode::F(1));
628        let f12 = KeyEvent::new(KeyCode::F(12));
629        assert_eq!(f1.code, KeyCode::F(1));
630        assert_eq!(f12.code, KeyCode::F(12));
631    }
632
633    #[test]
634    fn event_is_clone_and_eq() {
635        let event = Event::Key(KeyEvent::new(KeyCode::Char('x')));
636        let cloned = event.clone();
637        assert_eq!(event, cloned);
638    }
639
640    // -- Crossterm mapping tests --
641
642    #[test]
643    fn map_modifiers_ctrl() {
644        let mapped = map_modifiers(ct_event::KeyModifiers::CONTROL);
645        assert!(mapped.contains(Modifiers::CTRL));
646        assert!(!mapped.contains(Modifiers::SHIFT));
647    }
648
649    #[test]
650    fn map_modifiers_alt() {
651        let mapped = map_modifiers(ct_event::KeyModifiers::ALT);
652        assert!(mapped.contains(Modifiers::ALT));
653    }
654
655    #[test]
656    fn map_modifiers_super_variants() {
657        let super_mapped = map_modifiers(ct_event::KeyModifiers::SUPER);
658        assert!(super_mapped.contains(Modifiers::SUPER));
659
660        let hyper_mapped = map_modifiers(ct_event::KeyModifiers::HYPER);
661        assert!(hyper_mapped.contains(Modifiers::SUPER));
662
663        let meta_mapped = map_modifiers(ct_event::KeyModifiers::META);
664        assert!(meta_mapped.contains(Modifiers::SUPER));
665    }
666
667    #[test]
668    fn map_modifiers_combined() {
669        let combined = ct_event::KeyModifiers::SHIFT | ct_event::KeyModifiers::CONTROL;
670        let mapped = map_modifiers(combined);
671        assert!(mapped.contains(Modifiers::SHIFT));
672        assert!(mapped.contains(Modifiers::CTRL));
673        assert!(!mapped.contains(Modifiers::ALT));
674    }
675
676    #[test]
677    fn map_mouse_button_all() {
678        assert_eq!(
679            map_mouse_button(ct_event::MouseButton::Left),
680            MouseButton::Left
681        );
682        assert_eq!(
683            map_mouse_button(ct_event::MouseButton::Right),
684            MouseButton::Right
685        );
686        assert_eq!(
687            map_mouse_button(ct_event::MouseButton::Middle),
688            MouseButton::Middle
689        );
690    }
691
692    #[test]
693    fn map_mouse_event_down() {
694        let ct_event = ct_event::MouseEvent {
695            kind: ct_event::MouseEventKind::Down(ct_event::MouseButton::Left),
696            column: 10,
697            row: 5,
698            modifiers: ct_event::KeyModifiers::NONE,
699        };
700        let mapped = map_mouse_event(ct_event);
701        assert!(matches!(
702            mapped.kind,
703            MouseEventKind::Down(MouseButton::Left)
704        ));
705        assert_eq!(mapped.x, 10);
706        assert_eq!(mapped.y, 5);
707    }
708
709    #[test]
710    fn map_mouse_event_up() {
711        let ct_event = ct_event::MouseEvent {
712            kind: ct_event::MouseEventKind::Up(ct_event::MouseButton::Right),
713            column: 20,
714            row: 15,
715            modifiers: ct_event::KeyModifiers::NONE,
716        };
717        let mapped = map_mouse_event(ct_event);
718        assert!(matches!(
719            mapped.kind,
720            MouseEventKind::Up(MouseButton::Right)
721        ));
722        assert_eq!(mapped.x, 20);
723        assert_eq!(mapped.y, 15);
724    }
725
726    #[test]
727    fn map_mouse_event_drag() {
728        let ct_event = ct_event::MouseEvent {
729            kind: ct_event::MouseEventKind::Drag(ct_event::MouseButton::Middle),
730            column: 5,
731            row: 10,
732            modifiers: ct_event::KeyModifiers::NONE,
733        };
734        let mapped = map_mouse_event(ct_event);
735        assert!(matches!(
736            mapped.kind,
737            MouseEventKind::Drag(MouseButton::Middle)
738        ));
739    }
740
741    #[test]
742    fn map_mouse_event_moved() {
743        let ct_event = ct_event::MouseEvent {
744            kind: ct_event::MouseEventKind::Moved,
745            column: 0,
746            row: 0,
747            modifiers: ct_event::KeyModifiers::NONE,
748        };
749        let mapped = map_mouse_event(ct_event);
750        assert!(matches!(mapped.kind, MouseEventKind::Moved));
751    }
752
753    #[test]
754    fn map_mouse_event_scroll() {
755        let scroll_up = ct_event::MouseEvent {
756            kind: ct_event::MouseEventKind::ScrollUp,
757            column: 0,
758            row: 0,
759            modifiers: ct_event::KeyModifiers::NONE,
760        };
761        let scroll_down = ct_event::MouseEvent {
762            kind: ct_event::MouseEventKind::ScrollDown,
763            column: 0,
764            row: 0,
765            modifiers: ct_event::KeyModifiers::NONE,
766        };
767        let scroll_left = ct_event::MouseEvent {
768            kind: ct_event::MouseEventKind::ScrollLeft,
769            column: 0,
770            row: 0,
771            modifiers: ct_event::KeyModifiers::NONE,
772        };
773        let scroll_right = ct_event::MouseEvent {
774            kind: ct_event::MouseEventKind::ScrollRight,
775            column: 0,
776            row: 0,
777            modifiers: ct_event::KeyModifiers::NONE,
778        };
779
780        assert!(matches!(
781            map_mouse_event(scroll_up).kind,
782            MouseEventKind::ScrollUp
783        ));
784        assert!(matches!(
785            map_mouse_event(scroll_down).kind,
786            MouseEventKind::ScrollDown
787        ));
788        assert!(matches!(
789            map_mouse_event(scroll_left).kind,
790            MouseEventKind::ScrollLeft
791        ));
792        assert!(matches!(
793            map_mouse_event(scroll_right).kind,
794            MouseEventKind::ScrollRight
795        ));
796    }
797
798    #[test]
799    fn map_mouse_event_modifiers() {
800        let ct_event = ct_event::MouseEvent {
801            kind: ct_event::MouseEventKind::Down(ct_event::MouseButton::Left),
802            column: 0,
803            row: 0,
804            modifiers: ct_event::KeyModifiers::SHIFT | ct_event::KeyModifiers::ALT,
805        };
806        let mapped = map_mouse_event(ct_event);
807        assert!(mapped.modifiers.contains(Modifiers::SHIFT));
808        assert!(mapped.modifiers.contains(Modifiers::ALT));
809    }
810
811    #[test]
812    fn map_key_event_char() {
813        let ct_event = ct_event::KeyEvent {
814            code: ct_event::KeyCode::Char('x'),
815            modifiers: ct_event::KeyModifiers::CONTROL,
816            kind: ct_event::KeyEventKind::Press,
817            state: ct_event::KeyEventState::NONE,
818        };
819        let mapped = map_key_event(ct_event).expect("should map");
820        assert_eq!(mapped.code, KeyCode::Char('x'));
821        assert!(mapped.modifiers.contains(Modifiers::CTRL));
822        assert_eq!(mapped.kind, KeyEventKind::Press);
823    }
824
825    #[test]
826    fn map_key_event_function_key() {
827        let ct_event = ct_event::KeyEvent {
828            code: ct_event::KeyCode::F(5),
829            modifiers: ct_event::KeyModifiers::NONE,
830            kind: ct_event::KeyEventKind::Press,
831            state: ct_event::KeyEventState::NONE,
832        };
833        let mapped = map_key_event(ct_event).expect("should map");
834        assert_eq!(mapped.code, KeyCode::F(5));
835    }
836
837    #[test]
838    fn map_crossterm_event_key() {
839        let ct_event = ct_event::Event::Key(ct_event::KeyEvent {
840            code: ct_event::KeyCode::Enter,
841            modifiers: ct_event::KeyModifiers::NONE,
842            kind: ct_event::KeyEventKind::Press,
843            state: ct_event::KeyEventState::NONE,
844        });
845        let mapped = map_crossterm_event_internal(ct_event).expect("should map");
846        assert!(matches!(mapped, Event::Key(_)));
847    }
848
849    #[test]
850    fn map_crossterm_event_mouse() {
851        let ct_event = ct_event::Event::Mouse(ct_event::MouseEvent {
852            kind: ct_event::MouseEventKind::Down(ct_event::MouseButton::Left),
853            column: 10,
854            row: 5,
855            modifiers: ct_event::KeyModifiers::NONE,
856        });
857        let mapped = map_crossterm_event_internal(ct_event).expect("should map");
858        assert!(matches!(mapped, Event::Mouse(_)));
859    }
860
861    #[test]
862    fn map_crossterm_event_resize() {
863        let ct_event = ct_event::Event::Resize(80, 24);
864        let mapped = map_crossterm_event_internal(ct_event).expect("should map");
865        assert!(matches!(
866            mapped,
867            Event::Resize {
868                width: 80,
869                height: 24
870            }
871        ));
872    }
873
874    #[test]
875    fn map_crossterm_event_paste() {
876        let ct_event = ct_event::Event::Paste("hello world".to_string());
877        let mapped = map_crossterm_event_internal(ct_event).expect("should map");
878        match mapped {
879            Event::Paste(paste) => assert_eq!(paste.text, "hello world"),
880            _ => panic!("expected Paste event"),
881        }
882    }
883
884    #[test]
885    fn map_crossterm_event_focus() {
886        let gained = ct_event::Event::FocusGained;
887        let lost = ct_event::Event::FocusLost;
888
889        assert!(matches!(
890            map_crossterm_event_internal(gained),
891            Some(Event::Focus(true))
892        ));
893        assert!(matches!(
894            map_crossterm_event_internal(lost),
895            Some(Event::Focus(false))
896        ));
897    }
898
899    #[test]
900    fn map_key_kind_repeat_and_release() {
901        assert_eq!(
902            map_key_kind(ct_event::KeyEventKind::Repeat),
903            KeyEventKind::Repeat
904        );
905        assert_eq!(
906            map_key_kind(ct_event::KeyEventKind::Release),
907            KeyEventKind::Release
908        );
909    }
910
911    #[test]
912    fn map_key_code_escape() {
913        assert_eq!(map_key_code(ct_event::KeyCode::Esc), Some(KeyCode::Escape));
914    }
915
916    #[test]
917    fn map_key_code_unmapped_returns_none() {
918        // These variants exist in crossterm but are intentionally not mapped into ftui.
919        assert_eq!(map_key_code(ct_event::KeyCode::CapsLock), None);
920        assert_eq!(map_key_code(ct_event::KeyCode::ScrollLock), None);
921        assert_eq!(map_key_code(ct_event::KeyCode::NumLock), None);
922        assert_eq!(map_key_code(ct_event::KeyCode::PrintScreen), None);
923        assert_eq!(map_key_code(ct_event::KeyCode::Pause), None);
924        assert_eq!(map_key_code(ct_event::KeyCode::Menu), None);
925        assert_eq!(map_key_code(ct_event::KeyCode::KeypadBegin), None);
926        assert_eq!(
927            map_key_code(ct_event::KeyCode::Modifier(
928                ct_event::ModifierKeyCode::LeftShift
929            )),
930            None
931        );
932    }
933
934    #[test]
935    fn map_media_key_known_and_unknown_variants() {
936        // Known mapped variants.
937        assert_eq!(
938            map_media_key(ct_event::MediaKeyCode::Play),
939            Some(KeyCode::MediaPlayPause)
940        );
941        assert_eq!(
942            map_media_key(ct_event::MediaKeyCode::Pause),
943            Some(KeyCode::MediaPlayPause)
944        );
945        assert_eq!(
946            map_media_key(ct_event::MediaKeyCode::PlayPause),
947            Some(KeyCode::MediaPlayPause)
948        );
949        assert_eq!(
950            map_media_key(ct_event::MediaKeyCode::TrackNext),
951            Some(KeyCode::MediaNextTrack)
952        );
953        assert_eq!(
954            map_media_key(ct_event::MediaKeyCode::TrackPrevious),
955            Some(KeyCode::MediaPrevTrack)
956        );
957        assert_eq!(
958            map_media_key(ct_event::MediaKeyCode::Stop),
959            Some(KeyCode::MediaStop)
960        );
961
962        // A representative unmapped variant.
963        assert_eq!(map_media_key(ct_event::MediaKeyCode::Reverse), None);
964    }
965
966    #[test]
967    fn from_crossterm_returns_none_for_unmapped_keys() {
968        let ct_event = ct_event::Event::Key(ct_event::KeyEvent {
969            code: ct_event::KeyCode::CapsLock,
970            modifiers: ct_event::KeyModifiers::NONE,
971            kind: ct_event::KeyEventKind::Press,
972            state: ct_event::KeyEventState::NONE,
973        });
974
975        assert_eq!(Event::from_crossterm(ct_event), None);
976    }
977}