Skip to main content

retroglyph_core/
event.rs

1//! Input event system.
2
3use crate::grid::Pos;
4use alloc::vec::Vec;
5use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not};
6
7/// Physical (pixel) position relative to the window's top-left corner.
8///
9/// Using `ixy::Pos<u32>` rather than the cell-grid [`Pos`] (`ixy::Pos<u16>`)
10/// makes the distinction type-safe: you cannot accidentally pass a pixel
11/// coordinate where a cell coordinate is expected.
12pub type PhysicalPos = ixy::Pos<u32>;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
15/// Keyboard modifier flags.
16///
17/// Implemented as a manual bitflag over `u8` rather than using the
18/// [`bitflags`](https://crates.io/crates/bitflags) crate to keep the
19/// dependency surface minimal for `no_std` environments. Combine with `|`.
20pub struct KeyModifiers(u8);
21
22impl KeyModifiers {
23    /// No modifiers.
24    pub const NONE: Self = Self(0);
25    /// Shift key.
26    pub const SHIFT: Self = Self(1 << 0);
27    /// Control key.
28    pub const CONTROL: Self = Self(1 << 1);
29    /// Alt key.
30    pub const ALT: Self = Self(1 << 2);
31    /// Super/Meta key (macOS Cmd, Windows/Super key).
32    pub const SUPER: Self = Self(1 << 3);
33
34    /// Returns `true` if all bits in `other` are set in `self`.
35    #[must_use]
36    pub const fn contains(self, other: Self) -> bool {
37        (self.0 & other.0) == other.0
38    }
39
40    /// Returns `true` if no modifiers are set.
41    #[must_use]
42    pub const fn is_empty(self) -> bool {
43        self.0 == 0
44    }
45}
46
47impl BitOr for KeyModifiers {
48    type Output = Self;
49    fn bitor(self, rhs: Self) -> Self {
50        Self(self.0 | rhs.0)
51    }
52}
53
54impl BitOrAssign for KeyModifiers {
55    fn bitor_assign(&mut self, rhs: Self) {
56        self.0 |= rhs.0;
57    }
58}
59
60impl BitAnd for KeyModifiers {
61    type Output = Self;
62    fn bitand(self, rhs: Self) -> Self {
63        Self(self.0 & rhs.0)
64    }
65}
66
67impl BitAndAssign for KeyModifiers {
68    fn bitand_assign(&mut self, rhs: Self) {
69        self.0 &= rhs.0;
70    }
71}
72
73impl Not for KeyModifiers {
74    type Output = Self;
75    fn not(self) -> Self {
76        Self(!self.0)
77    }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
81#[non_exhaustive]
82/// Keyboard key codes.
83pub enum KeyCode {
84    /// A character key.
85    Char(char),
86    /// A function key.
87    F(u8),
88    /// Backspace.
89    Backspace,
90    /// Enter.
91    Enter,
92    /// Left arrow.
93    Left,
94    /// Right arrow.
95    Right,
96    /// Up arrow.
97    Up,
98    /// Down arrow.
99    Down,
100    /// Home.
101    Home,
102    /// End.
103    End,
104    /// Page Up.
105    PageUp,
106    /// Page Down.
107    PageDown,
108    /// Tab.
109    Tab,
110    /// Backtab.
111    BackTab,
112    /// Delete.
113    Delete,
114    /// Insert.
115    Insert,
116    /// Escape.
117    Escape,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
121/// Whether a key event is a press, an auto-repeat, or a release.
122///
123/// Not every backend can distinguish these. Plain terminals only ever emit
124/// [`Press`](Self::Press). Backends with richer input report the full set:
125///
126/// - The winit/software backend emits `Press`, `Repeat` (winit's `repeat`
127///   flag), and `Release`.
128/// - The crossterm backend emits the full set only when the terminal supports
129///   the kitty keyboard protocol (kitty, `WezTerm`, foot, Ghostty, recent
130///   Alacritty); otherwise it degrades to `Press`-only.
131pub enum KeyEventKind {
132    /// The key was pressed.
133    #[default]
134    Press,
135    /// The key is held and auto-repeating.
136    Repeat,
137    /// The key was released.
138    Release,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
142/// Keyboard input event.
143pub struct KeyEvent {
144    /// The key code.
145    pub code: KeyCode,
146    /// Modifiers held down during the event.
147    pub modifiers: KeyModifiers,
148    /// Whether this is a press, auto-repeat, or release.
149    ///
150    /// Backends that cannot distinguish these always report
151    /// [`KeyEventKind::Press`]. See [`KeyEventKind`] for per-backend behavior.
152    pub kind: KeyEventKind,
153}
154
155impl KeyEvent {
156    /// Creates a key press event with the given code and modifiers.
157    #[must_use]
158    pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
159        Self {
160            code,
161            modifiers,
162            kind: KeyEventKind::Press,
163        }
164    }
165
166    /// Creates a key event with an explicit [`KeyEventKind`].
167    #[must_use]
168    pub const fn with_kind(code: KeyCode, modifiers: KeyModifiers, kind: KeyEventKind) -> Self {
169        Self {
170            code,
171            modifiers,
172            kind,
173        }
174    }
175
176    /// Returns `true` if this event is a press or auto-repeat (i.e. the key is
177    /// down), and `false` for a release.
178    #[must_use]
179    pub const fn is_down(self) -> bool {
180        matches!(self.kind, KeyEventKind::Press | KeyEventKind::Repeat)
181    }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
185#[non_exhaustive]
186/// Mouse button identifiers.
187pub enum MouseButton {
188    /// Left mouse button.
189    Left,
190    /// Right mouse button.
191    Right,
192    /// Middle mouse button.
193    Middle,
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
197#[non_exhaustive]
198/// Kinds of mouse events.
199pub enum MouseEventKind {
200    /// Mouse button pressed.
201    Down(MouseButton),
202    /// Mouse button released.
203    Up(MouseButton),
204    /// Mouse moved.
205    Moved,
206    /// Mouse wheel scrolled up.
207    ScrollUp,
208    /// Mouse wheel scrolled down.
209    ScrollDown,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
213/// Mouse input event.
214pub struct MouseEvent {
215    /// The kind of mouse event.
216    pub kind: MouseEventKind,
217    /// Cell-grid position of the mouse cursor.
218    pub position: Pos,
219    /// Physical pixel position of the mouse cursor, relative to the window's top-left.
220    ///
221    /// Populated by backends that support sub-cell precision (e.g. the software
222    /// renderer). `None` on character-mode backends such as crossterm.
223    pub pixel_position: Option<PhysicalPos>,
224    /// Modifiers held down during the event.
225    pub modifiers: KeyModifiers,
226}
227
228/// The system's light/dark color-scheme preference, as reported by the
229/// windowing/browser layer.
230///
231/// Deliberately just these two variants (not, say, a `HighContrast` or
232/// `Auto` case): every source that can report this (winit's `Theme`, the
233/// browser's `prefers-color-scheme` media query) only ever resolves to one
234/// of exactly these two, and a backend that can't determine a preference
235/// simply never emits [`Event::ThemeChanged`] rather than emitting a third
236/// "unknown" case for callers to handle.
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
238pub enum SystemTheme {
239    /// The system prefers a light color scheme.
240    Light,
241    /// The system prefers a dark color scheme.
242    Dark,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Hash)]
246#[non_exhaustive]
247/// Terminal input event.
248pub enum Event {
249    /// Keyboard event.
250    Key(KeyEvent),
251    /// Mouse event.
252    Mouse(MouseEvent),
253    /// Terminal window resized.
254    Resize(u16, u16),
255    /// Window closed.
256    Close,
257    /// The system's light/dark color-scheme preference changed, or was
258    /// determined for the first time at startup.
259    ///
260    /// Only backends with a real source of truth for this emit it: the
261    /// windowed (winit) backend, on both native and wasm (winit's web
262    /// target derives it from the browser's `prefers-color-scheme` media
263    /// query, including live updates). Character-mode backends (crossterm)
264    /// have no equivalent free API -- see the windowed backend's own docs
265    /// for why -- and never emit this; an app that wants a default should
266    /// pick one itself rather than waiting for an event that may never
267    /// arrive.
268    ThemeChanged(SystemTheme),
269    /// Pasted text, delivered as a single event rather than individual key
270    /// presses.
271    ///
272    /// Not emitted by all backends -- see each backend's own docs for
273    /// whether and how it sources this. Content is forwarded verbatim from
274    /// the source, including embedded newlines; the receiving app is
275    /// responsible for any filtering it needs.
276    Paste(String),
277    /// The terminal or application window gained input focus.
278    ///
279    /// This reflects OS/terminal-level focus, not in-app widget focus (see
280    /// `retroglyph-widgets`' focus ring for that).
281    FocusGained,
282    /// The terminal or application window lost input focus.
283    ///
284    /// This reflects OS/terminal-level focus, not in-app widget focus (see
285    /// `retroglyph-widgets`' focus ring for that).
286    FocusLost,
287    /// An application-defined event injected from outside the normal input
288    /// source (e.g. a network, audio, or timer thread), carrying an opaque
289    /// tag the app assigns its own meaning to.
290    ///
291    /// Only emitted by backends with a real cross-thread injection point:
292    /// the windowed (winit) backend's `EventProxy`
293    /// (`retroglyph_window::winit::EventProxy::send_event`), which forwards
294    /// the `u64` unchanged. The payload is deliberately a plain `u64`
295    /// rather than an arbitrary boxed value: it keeps `Event` cheaply
296    /// `Clone`/`PartialEq`/`Eq`/`Hash` (a `Box<dyn Any>` could not derive
297    /// any of those) and needs no generic parameter threaded through every
298    /// crate that names `Event`. Treat it as a correlation id -- look up
299    /// the real payload in whatever shared state or channel the sending
300    /// thread already placed it in.
301    Custom(u64),
302}
303
304/// Tracks which keys are currently held down.
305///
306/// Feed it every [`KeyEvent`] (or [`Event`]) you receive and query
307/// [`is_held`](Self::is_held) each frame for held-key movement. A key is
308/// considered held from its first [`KeyEventKind::Press`] until a matching
309/// [`KeyEventKind::Release`].
310///
311/// This is only useful on backends that emit release events (winit, or a
312/// terminal with the kitty keyboard protocol). On press-only backends a key
313/// never leaves the held set on its own, so call [`clear`](Self::clear) at a
314/// suitable boundary (e.g. once per turn) if you rely on it there.
315#[derive(Debug, Clone, Default)]
316pub struct KeyState {
317    held: Vec<KeyCode>,
318}
319
320impl KeyState {
321    /// Creates an empty key-state tracker.
322    #[must_use]
323    pub const fn new() -> Self {
324        Self { held: Vec::new() }
325    }
326
327    /// Updates the held set from a key event.
328    ///
329    /// [`Press`](KeyEventKind::Press) and [`Repeat`](KeyEventKind::Repeat) add
330    /// the key; [`Release`](KeyEventKind::Release) removes it.
331    pub fn apply(&mut self, event: KeyEvent) {
332        match event.kind {
333            KeyEventKind::Press | KeyEventKind::Repeat => {
334                if !self.held.contains(&event.code) {
335                    self.held.push(event.code);
336                }
337            }
338            KeyEventKind::Release => {
339                self.held.retain(|&c| c != event.code);
340            }
341        }
342    }
343
344    /// Updates the held set from an [`Event`], ignoring non-key events.
345    pub fn apply_event(&mut self, event: &Event) {
346        if let Event::Key(key) = event {
347            self.apply(*key);
348        }
349    }
350
351    /// Returns `true` if `code` is currently held.
352    #[must_use]
353    pub fn is_held(&self, code: KeyCode) -> bool {
354        self.held.contains(&code)
355    }
356
357    /// Iterates the currently held keys, in first-pressed order.
358    pub fn held(&self) -> impl Iterator<Item = KeyCode> + '_ {
359        self.held.iter().copied()
360    }
361
362    /// Clears all held keys.
363    pub fn clear(&mut self) {
364        self.held.clear();
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn test_key_modifiers() {
374        let mods = KeyModifiers::SHIFT | KeyModifiers::CONTROL;
375        assert!(mods.contains(KeyModifiers::SHIFT));
376        assert!(mods.contains(KeyModifiers::CONTROL));
377        assert!(!mods.contains(KeyModifiers::ALT));
378        assert!(!mods.is_empty());
379
380        let inverse = !mods;
381        assert!(inverse.contains(KeyModifiers::ALT));
382        assert!(inverse.contains(KeyModifiers::SUPER));
383        assert!(!inverse.contains(KeyModifiers::SHIFT));
384        assert!(!inverse.contains(KeyModifiers::CONTROL));
385    }
386
387    #[test]
388    fn test_key_modifiers_super() {
389        let mods = KeyModifiers::SUPER;
390        assert!(mods.contains(KeyModifiers::SUPER));
391        assert!(!mods.contains(KeyModifiers::SHIFT));
392        assert!(!mods.contains(KeyModifiers::CONTROL));
393        assert!(!mods.contains(KeyModifiers::ALT));
394
395        let all =
396            KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SUPER;
397        assert!(all.contains(KeyModifiers::SUPER));
398        assert!(all.contains(KeyModifiers::SHIFT));
399        assert!(all.contains(KeyModifiers::CONTROL));
400        assert!(all.contains(KeyModifiers::ALT));
401    }
402
403    #[test]
404    fn test_event_construction() {
405        let key_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::SHIFT);
406        let event = Event::Key(key_event);
407
408        if let Event::Key(ke) = event {
409            assert_eq!(ke.code, KeyCode::Char('a'));
410            assert!(ke.modifiers.contains(KeyModifiers::SHIFT));
411            assert_eq!(ke.kind, KeyEventKind::Press);
412        } else {
413            panic!("Expected Event::Key");
414        }
415    }
416
417    #[test]
418    fn test_key_event_kind_helpers() {
419        let press = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
420        assert_eq!(press.kind, KeyEventKind::Press);
421        assert!(press.is_down());
422
423        let repeat =
424            KeyEvent::with_kind(KeyCode::Char('x'), KeyModifiers::NONE, KeyEventKind::Repeat);
425        assert!(repeat.is_down());
426
427        let release = KeyEvent::with_kind(
428            KeyCode::Char('x'),
429            KeyModifiers::NONE,
430            KeyEventKind::Release,
431        );
432        assert!(!release.is_down());
433    }
434
435    #[test]
436    fn test_key_state_tracks_held_keys() {
437        let mut state = KeyState::new();
438        assert!(!state.is_held(KeyCode::Left));
439
440        state.apply(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
441        assert!(state.is_held(KeyCode::Left));
442
443        // Repeat keeps it held.
444        state.apply(KeyEvent::with_kind(
445            KeyCode::Left,
446            KeyModifiers::NONE,
447            KeyEventKind::Repeat,
448        ));
449        assert!(state.is_held(KeyCode::Left));
450
451        state.apply(KeyEvent::with_kind(
452            KeyCode::Left,
453            KeyModifiers::NONE,
454            KeyEventKind::Release,
455        ));
456        assert!(!state.is_held(KeyCode::Left));
457    }
458
459    #[test]
460    fn test_key_state_apply_event_ignores_non_key() {
461        let mut state = KeyState::new();
462        state.apply_event(&Event::Resize(1, 1));
463        assert!(state.held().next().is_none());
464        state.apply_event(&Event::Key(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)));
465        assert!(state.is_held(KeyCode::Up));
466    }
467
468    #[test]
469    fn test_paste_event_carries_text() {
470        let event = Event::Paste("hello".to_string());
471        let Event::Paste(text) = event else {
472            panic!("Expected Event::Paste");
473        };
474        assert_eq!(text, "hello");
475    }
476
477    #[test]
478    fn test_custom_event_carries_opaque_id() {
479        let event = Event::Custom(42);
480        let Event::Custom(id) = event else {
481            panic!("Expected Event::Custom");
482        };
483        assert_eq!(id, 42);
484        assert_ne!(Event::Custom(1), Event::Custom(2));
485    }
486
487    #[test]
488    fn test_focus_gained_and_lost_are_distinct() {
489        assert!(matches!(Event::FocusGained, Event::FocusGained));
490        assert!(matches!(Event::FocusLost, Event::FocusLost));
491        assert_ne!(Event::FocusGained, Event::FocusLost);
492    }
493
494    #[test]
495    fn test_mouse_event_no_pixel_position() {
496        let mouse_event = MouseEvent {
497            kind: MouseEventKind::Down(MouseButton::Left),
498            position: Pos { x: 10, y: 5 },
499            pixel_position: None,
500            modifiers: KeyModifiers::NONE,
501        };
502        assert!(mouse_event.pixel_position.is_none());
503        assert!(matches!(Event::Mouse(mouse_event), Event::Mouse(_)));
504    }
505
506    #[test]
507    fn test_mouse_event_with_pixel_position() {
508        let mouse_event = MouseEvent {
509            kind: MouseEventKind::Moved,
510            position: Pos { x: 3, y: 2 },
511            pixel_position: Some(PhysicalPos { x: 55, y: 38 }),
512            modifiers: KeyModifiers::NONE,
513        };
514        let px = mouse_event.pixel_position.unwrap();
515        assert_eq!(px.x, 55);
516        assert_eq!(px.y, 38);
517        // Cell and pixel positions are distinct coordinate spaces.
518        assert_ne!(px.x, u32::from(mouse_event.position.x));
519    }
520
521    #[test]
522    fn test_physical_pos_is_copy() {
523        let p = PhysicalPos { x: 10, y: 20 };
524        let q = p; // Copy
525        assert_eq!(p, q);
526    }
527}