Skip to main content

tui_lipan/core/
event.rs

1/// Mouse button.
2#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
3pub enum MouseButton {
4    /// Left mouse button.
5    Left,
6    /// Right mouse button.
7    Right,
8    /// Middle mouse button.
9    Middle,
10}
11
12/// High-level mouse event kind.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum MouseKind {
15    /// Button pressed.
16    Down(MouseButton),
17    /// Button released.
18    Up(MouseButton),
19    /// Mouse moved while a button is pressed.
20    Drag(MouseButton),
21    /// Mouse moved.
22    Moved,
23    /// Scroll up.
24    ScrollUp,
25    /// Scroll down.
26    ScrollDown,
27}
28
29/// A mouse event in terminal coordinates.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub struct MouseEvent {
32    /// X coordinate (column).
33    pub x: u16,
34    /// Y coordinate (row).
35    pub y: u16,
36    /// Event kind.
37    pub kind: MouseKind,
38    /// Modifiers.
39    pub mods: KeyMods,
40}
41
42/// A mouse-move event with both global and local coordinates.
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
44pub struct MouseMoveEvent {
45    /// Global X coordinate (column) in content-space.
46    pub x: u16,
47    /// Global Y coordinate (row) in content-space.
48    pub y: u16,
49    /// X coordinate relative to the target widget rect.
50    pub local_x: u16,
51    /// Y coordinate relative to the target widget rect.
52    pub local_y: u16,
53    /// Target widget width in cells.
54    pub target_w: u16,
55    /// Target widget height in cells.
56    pub target_h: u16,
57    /// Modifiers for the move event.
58    pub mods: KeyMods,
59}
60
61/// A mouse drag event with global and region-local coordinates.
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
63pub struct MouseDragEvent {
64    /// Global X coordinate where the drag started.
65    pub from_x: u16,
66    /// Global Y coordinate where the drag started.
67    pub from_y: u16,
68    /// Starting X coordinate relative to the target widget rect.
69    pub from_local_x: u16,
70    /// Starting Y coordinate relative to the target widget rect.
71    pub from_local_y: u16,
72    /// Current global X coordinate.
73    pub x: u16,
74    /// Current global Y coordinate.
75    pub y: u16,
76    /// Current X coordinate relative to the target widget rect.
77    pub local_x: u16,
78    /// Current Y coordinate relative to the target widget rect.
79    pub local_y: u16,
80    /// X delta since the previous drag tick.
81    pub delta_x: i16,
82    /// Y delta since the previous drag tick.
83    pub delta_y: i16,
84    /// Target widget width in cells.
85    pub target_w: u16,
86    /// Target widget height in cells.
87    pub target_h: u16,
88    /// Modifiers for the drag event.
89    pub mods: KeyMods,
90}
91
92/// Key modifiers.
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
94pub struct KeyMods {
95    /// Control modifier.
96    pub ctrl: bool,
97    /// Alt modifier.
98    pub alt: bool,
99    /// Shift modifier.
100    pub shift: bool,
101    /// Super modifier (Windows/Command/Meta).
102    pub super_key: bool,
103}
104
105impl KeyMods {
106    /// No modifiers.
107    pub const NONE: Self = Self {
108        ctrl: false,
109        alt: false,
110        shift: false,
111        super_key: false,
112    };
113
114    /// Shift modifier only.
115    pub const SHIFT: Self = Self {
116        ctrl: false,
117        alt: false,
118        shift: true,
119        super_key: false,
120    };
121
122    /// Control modifier only.
123    pub const CTRL: Self = Self {
124        ctrl: true,
125        alt: false,
126        shift: false,
127        super_key: false,
128    };
129
130    /// Alt modifier only.
131    pub const ALT: Self = Self {
132        ctrl: false,
133        alt: true,
134        shift: false,
135        super_key: false,
136    };
137
138    /// Returns true when no modifiers are set.
139    pub fn is_empty(&self) -> bool {
140        !self.ctrl && !self.alt && !self.shift && !self.super_key
141    }
142}
143
144/// Key code.
145#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
146pub enum KeyCode {
147    /// A unicode character.
148    Char(char),
149    /// Insert.
150    Insert,
151    /// Enter.
152    Enter,
153    /// Escape.
154    Esc,
155    /// Tab.
156    Tab,
157    /// Shift+Tab.
158    BackTab,
159    /// Backspace.
160    Backspace,
161    /// Delete.
162    Delete,
163    /// Home.
164    Home,
165    /// End.
166    End,
167    /// Page up.
168    PageUp,
169    /// Page down.
170    PageDown,
171    /// Arrow up.
172    Up,
173    /// Arrow down.
174    Down,
175    /// Arrow left.
176    Left,
177    /// Arrow right.
178    Right,
179    /// Function key.
180    F(u8),
181}
182
183/// A keyboard event.
184///
185/// Common pitfall: matching only `key.code` ignores modifiers. Prefer
186/// `key.is(...)` for plain-key checks or `key.is_with(...)` for exact
187/// key+modifier combinations.
188#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
189pub struct KeyEvent {
190    /// Key code.
191    pub code: KeyCode,
192    /// Modifiers.
193    pub mods: KeyMods,
194}
195
196impl KeyEvent {
197    /// Returns true when this key matches `code` with no modifiers.
198    pub fn is(&self, code: KeyCode) -> bool {
199        self.code == code && self.mods.is_empty()
200    }
201
202    /// Returns true when this key matches `code` with exactly `mods`.
203    pub fn is_with(&self, code: KeyCode, mods: KeyMods) -> bool {
204        self.code == code && self.mods == mods
205    }
206
207    /// Formats the key event into a readable string (e.g., "Ctrl+E" or "ctrl+e").
208    ///
209    /// If `lowercase` is true, the resulting string will be in lowercase.
210    pub fn to_formatted_string(&self, lowercase: bool) -> String {
211        let mut parts = Vec::new();
212
213        if self.mods.ctrl {
214            parts.push("Ctrl");
215        }
216        if self.mods.alt {
217            parts.push("Alt");
218        }
219        if self.mods.super_key {
220            parts.push("Super");
221        }
222        if self.mods.shift {
223            parts.push("Shift");
224        }
225
226        let code_str = match self.code {
227            KeyCode::Char(c) => {
228                if c == ' ' {
229                    "Space".to_string()
230                } else {
231                    c.to_uppercase().to_string()
232                }
233            }
234            KeyCode::Insert => "Insert".to_string(),
235            KeyCode::Enter => "Enter".to_string(),
236            KeyCode::Esc => "Esc".to_string(),
237            KeyCode::Tab => "Tab".to_string(),
238            KeyCode::BackTab => "BackTab".to_string(),
239            KeyCode::Backspace => "Backspace".to_string(),
240            KeyCode::Delete => "Delete".to_string(),
241            KeyCode::Home => "Home".to_string(),
242            KeyCode::End => "End".to_string(),
243            KeyCode::PageUp => "PageUp".to_string(),
244            KeyCode::PageDown => "PageDown".to_string(),
245            KeyCode::Up => "Up".to_string(),
246            KeyCode::Down => "Down".to_string(),
247            KeyCode::Left => "Left".to_string(),
248            KeyCode::Right => "Right".to_string(),
249            KeyCode::F(n) => format!("F{n}"),
250        };
251        parts.push(&code_str);
252
253        let result = parts.join("+");
254        if lowercase {
255            result.to_lowercase()
256        } else {
257            result
258        }
259    }
260}
261
262impl std::fmt::Display for KeyEvent {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        write!(f, "{}", self.to_formatted_string(false))
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_key_event_formatting() {
274        let key1 = KeyEvent {
275            code: KeyCode::Char('e'),
276            mods: KeyMods {
277                ctrl: true,
278                alt: false,
279                shift: false,
280                super_key: false,
281            },
282        };
283        assert_eq!(key1.to_formatted_string(false), "Ctrl+E");
284        assert_eq!(key1.to_formatted_string(true), "ctrl+e");
285        assert_eq!(key1.to_string(), "Ctrl+E");
286
287        let key2 = KeyEvent {
288            code: KeyCode::Enter,
289            mods: KeyMods {
290                ctrl: true,
291                alt: true,
292                shift: true,
293                super_key: false,
294            },
295        };
296        assert_eq!(key2.to_formatted_string(false), "Ctrl+Alt+Shift+Enter");
297        assert_eq!(key2.to_formatted_string(true), "ctrl+alt+shift+enter");
298        assert_eq!(key2.to_string(), "Ctrl+Alt+Shift+Enter");
299
300        let key3 = KeyEvent {
301            code: KeyCode::Char(' '),
302            mods: KeyMods {
303                ctrl: false,
304                alt: false,
305                shift: false,
306                super_key: false,
307            },
308        };
309        assert_eq!(key3.to_formatted_string(false), "Space");
310        assert_eq!(key3.to_string(), "Space");
311
312        let key4 = KeyEvent {
313            code: KeyCode::F(12),
314            mods: KeyMods {
315                ctrl: false,
316                alt: false,
317                shift: false,
318                super_key: false,
319            },
320        };
321        assert_eq!(key4.to_formatted_string(false), "F12");
322        assert_eq!(key4.to_formatted_string(true), "f12");
323        assert_eq!(key4.to_string(), "F12");
324    }
325
326    #[test]
327    fn test_key_event_matching_helpers_and_mod_constants() {
328        let plain_enter = KeyEvent {
329            code: KeyCode::Enter,
330            mods: KeyMods::NONE,
331        };
332        assert!(plain_enter.is(KeyCode::Enter));
333        assert!(plain_enter.is_with(KeyCode::Enter, KeyMods::NONE));
334
335        let shift_enter = KeyEvent {
336            code: KeyCode::Enter,
337            mods: KeyMods::SHIFT,
338        };
339        assert!(!shift_enter.is(KeyCode::Enter));
340        assert!(shift_enter.is_with(KeyCode::Enter, KeyMods::SHIFT));
341        assert!(!shift_enter.is_with(KeyCode::Enter, KeyMods::NONE));
342
343        assert!(KeyMods::NONE.is_empty());
344        assert!(!KeyMods::SHIFT.is_empty());
345    }
346}