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}