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}