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 while a button was held down; carries which button.
205    Drag(MouseButton),
206    /// Mouse moved.
207    Moved,
208    /// Mouse wheel scrolled up.
209    ScrollUp,
210    /// Mouse wheel scrolled down.
211    ScrollDown,
212    /// Mouse wheel scrolled left (mostly on a laptop touchpad).
213    ScrollLeft,
214    /// Mouse wheel scrolled right (mostly on a laptop touchpad).
215    ScrollRight,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
219/// Mouse input event.
220pub struct MouseEvent {
221    /// The kind of mouse event.
222    pub kind: MouseEventKind,
223    /// Cell-grid position of the mouse cursor.
224    pub position: Pos,
225    /// Physical pixel position of the mouse cursor, relative to the window's top-left.
226    ///
227    /// Populated by backends that support sub-cell precision (e.g. the software
228    /// renderer). `None` on character-mode backends such as crossterm.
229    pub pixel_position: Option<PhysicalPos>,
230    /// Modifiers held down during the event.
231    pub modifiers: KeyModifiers,
232}
233
234/// The system's light/dark color-scheme preference, as reported by the
235/// windowing/browser layer.
236///
237/// Currently just these two variants: every source that can report this
238/// (winit's `Theme`, the browser's `prefers-color-scheme` media query) only
239/// ever resolves to one of exactly these two, and a backend that can't
240/// determine a preference simply never emits [`Event::ThemeChanged`] rather
241/// than emitting a third "unknown" case for callers to handle. Marked
242/// `#[non_exhaustive]` for consistency with sibling public enums, in case a
243/// future source (e.g. a `HighContrast` case) needs to be added.
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
245#[non_exhaustive]
246pub enum SystemTheme {
247    /// The system prefers a light color scheme.
248    Light,
249    /// The system prefers a dark color scheme.
250    Dark,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Hash)]
254#[non_exhaustive]
255/// Terminal input event.
256pub enum Event {
257    /// Keyboard event.
258    Key(KeyEvent),
259    /// Mouse event.
260    Mouse(MouseEvent),
261    /// Terminal window resized.
262    Resize(u16, u16),
263    /// Window closed.
264    Close,
265    /// The system's light/dark color-scheme preference changed, or was
266    /// determined for the first time at startup.
267    ///
268    /// Only backends with a real source of truth for this emit it: the
269    /// windowed (winit) backend, on both native and wasm (winit's web
270    /// target derives it from the browser's `prefers-color-scheme` media
271    /// query, including live updates). Character-mode backends (crossterm)
272    /// have no equivalent free API -- see the windowed backend's own docs
273    /// for why -- and never emit this; an app that wants a default should
274    /// pick one itself rather than waiting for an event that may never
275    /// arrive.
276    ThemeChanged(SystemTheme),
277    /// Pasted text, delivered as a single event rather than individual key
278    /// presses.
279    ///
280    /// Not emitted by all backends -- see each backend's own docs for
281    /// whether and how it sources this. Content is forwarded verbatim from
282    /// the source, including embedded newlines; the receiving app is
283    /// responsible for any filtering it needs.
284    Paste(String),
285    /// The terminal or application window gained input focus.
286    ///
287    /// This reflects OS/terminal-level focus, not in-app widget focus (see
288    /// `retroglyph-widgets`' focus ring for that).
289    FocusGained,
290    /// The terminal or application window lost input focus.
291    ///
292    /// This reflects OS/terminal-level focus, not in-app widget focus (see
293    /// `retroglyph-widgets`' focus ring for that).
294    FocusLost,
295    /// An application-defined event injected from outside the normal input
296    /// source (e.g. a network, audio, or timer thread), carrying an opaque
297    /// tag the app assigns its own meaning to.
298    ///
299    /// Only emitted by backends with a real cross-thread injection point:
300    /// the windowed (winit) backend's `EventProxy`
301    /// (`retroglyph_window::winit::EventProxy::send_event`), which forwards
302    /// the `u64` unchanged. The payload is deliberately a plain `u64`
303    /// rather than an arbitrary boxed value: it keeps `Event` cheaply
304    /// `Clone`/`PartialEq`/`Eq`/`Hash` (a `Box<dyn Any>` could not derive
305    /// any of those) and needs no generic parameter threaded through every
306    /// crate that names `Event`. Treat it as a correlation id -- look up
307    /// the real payload in whatever shared state or channel the sending
308    /// thread already placed it in.
309    Custom(u64),
310}
311
312/// Tracks which keys are currently held down.
313///
314/// Feed it every [`KeyEvent`] (or [`Event`]) you receive and query
315/// [`is_held`](Self::is_held) each frame for held-key movement. A key is
316/// considered held from its first [`KeyEventKind::Press`] until a matching
317/// [`KeyEventKind::Release`].
318///
319/// This is only useful on backends that emit release events (winit, or a
320/// terminal with the kitty keyboard protocol). On press-only backends a key
321/// never leaves the held set on its own, so call [`clear`](Self::clear) at a
322/// suitable boundary (e.g. once per turn) if you rely on it there.
323#[derive(Debug, Clone, Default)]
324pub struct KeyState {
325    held: Vec<KeyCode>,
326}
327
328impl KeyState {
329    /// Creates an empty key-state tracker.
330    #[must_use]
331    pub const fn new() -> Self {
332        Self { held: Vec::new() }
333    }
334
335    /// Updates the held set from a key event.
336    ///
337    /// [`Press`](KeyEventKind::Press) and [`Repeat`](KeyEventKind::Repeat) add
338    /// the key; [`Release`](KeyEventKind::Release) removes it.
339    pub fn apply(&mut self, event: KeyEvent) {
340        match event.kind {
341            KeyEventKind::Press | KeyEventKind::Repeat => {
342                if !self.held.contains(&event.code) {
343                    self.held.push(event.code);
344                }
345            }
346            KeyEventKind::Release => {
347                self.held.retain(|&c| c != event.code);
348            }
349        }
350    }
351
352    /// Updates the held set from an [`Event`], ignoring non-key events.
353    pub fn apply_event(&mut self, event: &Event) {
354        if let Event::Key(key) = event {
355            self.apply(*key);
356        }
357    }
358
359    /// Returns `true` if `code` is currently held.
360    #[must_use]
361    pub fn is_held(&self, code: KeyCode) -> bool {
362        self.held.contains(&code)
363    }
364
365    /// Iterates the currently held keys, in first-pressed order.
366    pub fn held(&self) -> impl Iterator<Item = KeyCode> + '_ {
367        self.held.iter().copied()
368    }
369
370    /// Clears all held keys.
371    pub fn clear(&mut self) {
372        self.held.clear();
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn test_key_modifiers() {
382        let mods = KeyModifiers::SHIFT | KeyModifiers::CONTROL;
383        assert!(mods.contains(KeyModifiers::SHIFT));
384        assert!(mods.contains(KeyModifiers::CONTROL));
385        assert!(!mods.contains(KeyModifiers::ALT));
386        assert!(!mods.is_empty());
387
388        let inverse = !mods;
389        assert!(inverse.contains(KeyModifiers::ALT));
390        assert!(inverse.contains(KeyModifiers::SUPER));
391        assert!(!inverse.contains(KeyModifiers::SHIFT));
392        assert!(!inverse.contains(KeyModifiers::CONTROL));
393    }
394
395    #[test]
396    fn test_key_modifiers_super() {
397        let mods = KeyModifiers::SUPER;
398        assert!(mods.contains(KeyModifiers::SUPER));
399        assert!(!mods.contains(KeyModifiers::SHIFT));
400        assert!(!mods.contains(KeyModifiers::CONTROL));
401        assert!(!mods.contains(KeyModifiers::ALT));
402
403        let all =
404            KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SUPER;
405        assert!(all.contains(KeyModifiers::SUPER));
406        assert!(all.contains(KeyModifiers::SHIFT));
407        assert!(all.contains(KeyModifiers::CONTROL));
408        assert!(all.contains(KeyModifiers::ALT));
409    }
410
411    #[test]
412    fn test_event_construction() {
413        let key_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::SHIFT);
414        let event = Event::Key(key_event);
415
416        if let Event::Key(ke) = event {
417            assert_eq!(ke.code, KeyCode::Char('a'));
418            assert!(ke.modifiers.contains(KeyModifiers::SHIFT));
419            assert_eq!(ke.kind, KeyEventKind::Press);
420        } else {
421            panic!("Expected Event::Key");
422        }
423    }
424
425    #[test]
426    fn test_key_event_kind_helpers() {
427        let press = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
428        assert_eq!(press.kind, KeyEventKind::Press);
429        assert!(press.is_down());
430
431        let repeat =
432            KeyEvent::with_kind(KeyCode::Char('x'), KeyModifiers::NONE, KeyEventKind::Repeat);
433        assert!(repeat.is_down());
434
435        let release = KeyEvent::with_kind(
436            KeyCode::Char('x'),
437            KeyModifiers::NONE,
438            KeyEventKind::Release,
439        );
440        assert!(!release.is_down());
441    }
442
443    #[test]
444    fn test_key_state_tracks_held_keys() {
445        let mut state = KeyState::new();
446        assert!(!state.is_held(KeyCode::Left));
447
448        state.apply(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
449        assert!(state.is_held(KeyCode::Left));
450
451        // Repeat keeps it held.
452        state.apply(KeyEvent::with_kind(
453            KeyCode::Left,
454            KeyModifiers::NONE,
455            KeyEventKind::Repeat,
456        ));
457        assert!(state.is_held(KeyCode::Left));
458
459        state.apply(KeyEvent::with_kind(
460            KeyCode::Left,
461            KeyModifiers::NONE,
462            KeyEventKind::Release,
463        ));
464        assert!(!state.is_held(KeyCode::Left));
465    }
466
467    #[test]
468    fn test_key_state_apply_event_ignores_non_key() {
469        let mut state = KeyState::new();
470        state.apply_event(&Event::Resize(1, 1));
471        assert!(state.held().next().is_none());
472        state.apply_event(&Event::Key(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)));
473        assert!(state.is_held(KeyCode::Up));
474    }
475
476    #[test]
477    fn test_paste_event_carries_text() {
478        let event = Event::Paste("hello".to_string());
479        let Event::Paste(text) = event else {
480            panic!("Expected Event::Paste");
481        };
482        assert_eq!(text, "hello");
483    }
484
485    #[test]
486    fn test_custom_event_carries_opaque_id() {
487        let event = Event::Custom(42);
488        let Event::Custom(id) = event else {
489            panic!("Expected Event::Custom");
490        };
491        assert_eq!(id, 42);
492        assert_ne!(Event::Custom(1), Event::Custom(2));
493    }
494
495    #[test]
496    fn test_focus_gained_and_lost_are_distinct() {
497        assert!(matches!(Event::FocusGained, Event::FocusGained));
498        assert!(matches!(Event::FocusLost, Event::FocusLost));
499        assert_ne!(Event::FocusGained, Event::FocusLost);
500    }
501
502    #[test]
503    fn test_mouse_event_no_pixel_position() {
504        let mouse_event = MouseEvent {
505            kind: MouseEventKind::Down(MouseButton::Left),
506            position: Pos { x: 10, y: 5 },
507            pixel_position: None,
508            modifiers: KeyModifiers::NONE,
509        };
510        assert!(mouse_event.pixel_position.is_none());
511        assert!(matches!(Event::Mouse(mouse_event), Event::Mouse(_)));
512    }
513
514    #[test]
515    fn test_mouse_event_with_pixel_position() {
516        let mouse_event = MouseEvent {
517            kind: MouseEventKind::Moved,
518            position: Pos { x: 3, y: 2 },
519            pixel_position: Some(PhysicalPos { x: 55, y: 38 }),
520            modifiers: KeyModifiers::NONE,
521        };
522        let px = mouse_event.pixel_position.unwrap();
523        assert_eq!(px.x, 55);
524        assert_eq!(px.y, 38);
525        // Cell and pixel positions are distinct coordinate spaces.
526        assert_ne!(px.x, u32::from(mouse_event.position.x));
527    }
528
529    #[test]
530    fn test_physical_pos_is_copy() {
531        let p = PhysicalPos { x: 10, y: 20 };
532        let q = p; // Copy
533        assert_eq!(p, q);
534    }
535}