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