Skip to main content

retroglyph_core/
event.rs

1//! Input event system.
2
3use crate::grid::Pos;
4use alloc::string::String;
5use alloc::vec::Vec;
6use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not};
7
8/// Physical (pixel) position relative to the window's top-left corner.
9///
10/// Using `ixy::Pos<u32>` rather than the cell-grid [`Pos`] (`ixy::Pos<u16>`)
11/// makes the distinction type-safe: you cannot accidentally pass a pixel
12/// coordinate where a cell coordinate is expected.
13pub type PhysicalPos = ixy::Pos<u32>;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
16/// Keyboard modifier flags.
17///
18/// Implemented as a manual bitflag over `u8` rather than using the
19/// [`bitflags`](https://crates.io/crates/bitflags) crate to keep the
20/// dependency surface minimal for `no_std` environments. Combine with `|`.
21pub struct KeyModifiers(u8);
22
23impl KeyModifiers {
24    /// No modifiers.
25    pub const NONE: Self = Self(0);
26    /// Shift key.
27    pub const SHIFT: Self = Self(1 << 0);
28    /// Control key.
29    pub const CONTROL: Self = Self(1 << 1);
30    /// Alt key.
31    pub const ALT: Self = Self(1 << 2);
32    /// Super/Meta key (macOS Cmd, Windows/Super key).
33    pub const SUPER: Self = Self(1 << 3);
34
35    /// Returns `true` if all bits in `other` are set in `self`.
36    #[must_use]
37    pub const fn contains(self, other: Self) -> bool {
38        (self.0 & other.0) == other.0
39    }
40
41    /// Returns `true` if no modifiers are set.
42    #[must_use]
43    pub const fn is_empty(self) -> bool {
44        self.0 == 0
45    }
46}
47
48impl BitOr for KeyModifiers {
49    type Output = Self;
50    fn bitor(self, rhs: Self) -> Self {
51        Self(self.0 | rhs.0)
52    }
53}
54
55impl BitOrAssign for KeyModifiers {
56    fn bitor_assign(&mut self, rhs: Self) {
57        self.0 |= rhs.0;
58    }
59}
60
61impl BitAnd for KeyModifiers {
62    type Output = Self;
63    fn bitand(self, rhs: Self) -> Self {
64        Self(self.0 & rhs.0)
65    }
66}
67
68impl BitAndAssign for KeyModifiers {
69    fn bitand_assign(&mut self, rhs: Self) {
70        self.0 &= rhs.0;
71    }
72}
73
74impl Not for KeyModifiers {
75    type Output = Self;
76    fn not(self) -> Self {
77        Self(!self.0)
78    }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82#[non_exhaustive]
83/// A modifier key pressed as a standalone key event, independent of the [`KeyModifiers`] flags
84/// carried on non-modifier key events.
85///
86/// This is flat (no per-side variants) because side is conveyed separately: pair this with the
87/// surrounding [`KeyEvent`]'s [`KeyLocation::Left`]/[`KeyLocation::Right`] rather than duplicating
88/// left/right into `ModifierKey` itself.
89///
90/// Reporting a bare modifier press as a [`KeyCode::Modifier`] event is backend-dependent: the
91/// crossterm backend requires the terminal to support the kitty keyboard protocol with the
92/// `REPORT_ALL_KEYS_AS_ESCAPE_CODES` enhancement flag enabled; plain terminals never report these.
93pub enum ModifierKey {
94    /// Shift.
95    Shift,
96    /// Control.
97    Control,
98    /// Alt.
99    Alt,
100    /// Super/Meta (macOS Cmd, Windows/Super key).
101    Super,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105#[non_exhaustive]
106/// Keyboard key codes.
107pub enum KeyCode {
108    /// A character key.
109    Char(char),
110    /// A function key.
111    F(u8),
112    /// Backspace.
113    Backspace,
114    /// Enter.
115    Enter,
116    /// Left arrow.
117    Left,
118    /// Right arrow.
119    Right,
120    /// Up arrow.
121    Up,
122    /// Down arrow.
123    Down,
124    /// Home.
125    Home,
126    /// End.
127    End,
128    /// Page Up.
129    PageUp,
130    /// Page Down.
131    PageDown,
132    /// Tab.
133    Tab,
134    /// Backtab.
135    BackTab,
136    /// Delete.
137    Delete,
138    /// Insert.
139    Insert,
140    /// Escape.
141    Escape,
142    /// A modifier key pressed on its own, without another key. See [`ModifierKey`] for the
143    /// backend-availability caveat.
144    Modifier(ModifierKey),
145    /// Caps Lock.
146    CapsLock,
147    /// Scroll Lock.
148    ScrollLock,
149    /// Num Lock.
150    NumLock,
151    /// Print Screen.
152    PrintScreen,
153    /// Pause.
154    Pause,
155    /// Menu (context menu key).
156    Menu,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
160/// Whether a key event is a press, an auto-repeat, or a release.
161///
162/// Not every backend can distinguish these. Plain terminals only ever emit
163/// [`Press`](Self::Press). Backends with richer input report the full set:
164///
165/// - The winit/software backend emits `Press`, `Repeat` (winit's `repeat`
166///   flag), and `Release`.
167/// - The crossterm backend emits the full set only when the terminal supports
168///   the kitty keyboard protocol (kitty, `WezTerm`, foot, Ghostty, recent
169///   Alacritty); otherwise it degrades to `Press`-only.
170pub enum KeyEventKind {
171    /// The key was pressed.
172    #[default]
173    Press,
174    /// The key is held and auto-repeating.
175    Repeat,
176    /// The key was released.
177    Release,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
181#[non_exhaustive]
182/// The physical location of a key on the keyboard, for keys that appear in more than one place.
183///
184/// Mirrors [winit's `KeyLocation`](https://docs.rs/winit/latest/winit/keyboard/enum.KeyLocation.html):
185/// a key like "1" carries the same [`KeyCode`] whether it's pressed above the letters or on the
186/// numpad, and modifier keys like Shift exist on both the left and right sides. This field
187/// disambiguates those cases.
188pub enum KeyLocation {
189    /// The key is in its single, non-duplicated location, or the backend cannot determine which
190    /// side/area a duplicated key came from.
191    #[default]
192    Standard,
193    /// The key is the left-hand copy of a duplicated key (e.g. left Shift).
194    Left,
195    /// The key is the right-hand copy of a duplicated key (e.g. right Shift).
196    Right,
197    /// The key originates from the numeric keypad.
198    Numpad,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
202/// Keyboard input event.
203pub struct KeyEvent {
204    /// The key code.
205    pub code: KeyCode,
206    /// Modifiers held down during the event.
207    pub modifiers: KeyModifiers,
208    /// Whether this is a press, auto-repeat, or release.
209    ///
210    /// Backends that cannot distinguish these always report
211    /// [`KeyEventKind::Press`]. See [`KeyEventKind`] for per-backend behavior.
212    pub kind: KeyEventKind,
213    /// The physical location of the key, for keys that appear in more than one place.
214    ///
215    /// Backends that cannot determine this always report [`KeyLocation::Standard`].
216    pub location: KeyLocation,
217}
218
219impl KeyEvent {
220    /// Creates a key press event with the given code and modifiers, and
221    /// [`KeyLocation::Standard`].
222    #[must_use]
223    pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
224        Self {
225            code,
226            modifiers,
227            kind: KeyEventKind::Press,
228            location: KeyLocation::Standard,
229        }
230    }
231
232    /// Creates a key event with an explicit [`KeyEventKind`] and [`KeyLocation::Standard`].
233    #[must_use]
234    pub const fn with_kind(code: KeyCode, modifiers: KeyModifiers, kind: KeyEventKind) -> Self {
235        Self {
236            code,
237            modifiers,
238            kind,
239            location: KeyLocation::Standard,
240        }
241    }
242
243    /// Creates a key event with an explicit [`KeyEventKind`] and [`KeyLocation`].
244    #[must_use]
245    pub const fn with_location(
246        code: KeyCode,
247        modifiers: KeyModifiers,
248        kind: KeyEventKind,
249        location: KeyLocation,
250    ) -> Self {
251        Self {
252            code,
253            modifiers,
254            kind,
255            location,
256        }
257    }
258
259    /// Returns `true` if this event is a press or auto-repeat (i.e. the key is
260    /// down), and `false` for a release.
261    #[must_use]
262    pub const fn is_down(self) -> bool {
263        matches!(self.kind, KeyEventKind::Press | KeyEventKind::Repeat)
264    }
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
268#[non_exhaustive]
269/// Mouse button identifiers.
270pub enum MouseButton {
271    /// Left mouse button.
272    Left,
273    /// Right mouse button.
274    Right,
275    /// Middle mouse button.
276    Middle,
277}
278
279#[derive(Debug, Clone, Copy, PartialEq)]
280#[non_exhaustive]
281/// Kinds of mouse events.
282///
283/// Does not derive `Eq`/`Hash`: [`Scroll`](Self::Scroll)'s `f32` fields implement neither.
284pub enum MouseEventKind {
285    /// Mouse button pressed.
286    Down(MouseButton),
287    /// Mouse button released.
288    Up(MouseButton),
289    /// Mouse moved while a button was held down; carries which button.
290    Drag(MouseButton),
291    /// Mouse moved.
292    Moved,
293    /// Mouse wheel/touchpad scroll.
294    ///
295    /// `dy > 0.0` is scroll up, `dy < 0.0` is scroll down; `dx > 0.0` is scroll right, `dx < 0.0`
296    /// is scroll left (mostly from a laptop touchpad). Magnitude is backend-dependent: the winit
297    /// backend reports the exact pixel/line delta from the platform, while the crossterm backend
298    /// synthesizes a fixed step of `1.0` per tick since terminals can't report scroll precision.
299    Scroll {
300        /// Horizontal delta. See the variant docs for the sign convention.
301        dx: f32,
302        /// Vertical delta. See the variant docs for the sign convention.
303        dy: f32,
304    },
305}
306
307#[derive(Debug, Clone, Copy, PartialEq)]
308/// Mouse input event.
309///
310/// Does not derive `Eq`/`Hash`: [`MouseEventKind`] does not (its `Scroll` variant's `f32`
311/// fields implement neither).
312pub struct MouseEvent {
313    /// The kind of mouse event.
314    pub kind: MouseEventKind,
315    /// Cell-grid position of the mouse cursor.
316    pub position: Pos,
317    /// Physical pixel position of the mouse cursor, relative to the window's top-left.
318    ///
319    /// Populated by backends that support sub-cell precision (e.g. the software
320    /// renderer). `None` on character-mode backends such as crossterm.
321    pub pixel_position: Option<PhysicalPos>,
322    /// Modifiers held down during the event.
323    pub modifiers: KeyModifiers,
324}
325
326/// The system's light/dark color-scheme preference, as reported by the
327/// windowing/browser layer.
328///
329/// Currently just these two variants: every source that can report this
330/// (winit's `Theme`, the browser's `prefers-color-scheme` media query) only
331/// ever resolves to one of exactly these two, and a backend that can't
332/// determine a preference simply never emits [`Event::ThemeChanged`] rather
333/// than emitting a third "unknown" case for callers to handle. Marked
334/// `#[non_exhaustive]` for consistency with sibling public enums, in case a
335/// future source (e.g. a `HighContrast` case) needs to be added.
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
337#[non_exhaustive]
338pub enum SystemTheme {
339    /// The system prefers a light color scheme.
340    Light,
341    /// The system prefers a dark color scheme.
342    Dark,
343}
344
345#[derive(Debug, Clone, PartialEq)]
346#[non_exhaustive]
347/// Terminal input event.
348///
349/// Does not derive `Eq`/`Hash`: [`MouseEvent`] does not (its `MouseEventKind::Scroll` variant's
350/// `f32` fields implement neither).
351pub enum Event {
352    /// Keyboard event.
353    Key(KeyEvent),
354    /// Mouse event.
355    Mouse(MouseEvent),
356    /// Terminal window resized to the given `(cols, rows)`.
357    ///
358    /// This event does not resize anything on its own: the receiving app must call
359    /// [`Terminal::resize`](crate::Terminal::resize) with these dimensions to resize the
360    /// terminal's own grid buffers. A windowed backend's own reported
361    /// [`Output::size`](crate::backend::Output::size) may already reflect the new dimensions
362    /// by the time this event is polled, but that does not substitute for resizing the grid.
363    Resize(u16, u16),
364    /// Window closed.
365    Close,
366    /// The system's light/dark color-scheme preference changed, or was
367    /// determined for the first time at startup.
368    ///
369    /// Only backends with a real source of truth for this emit it: the
370    /// windowed (winit) backend, on both native and wasm (winit's web
371    /// target derives it from the browser's `prefers-color-scheme` media
372    /// query, including live updates). Character-mode backends (crossterm)
373    /// have no equivalent free API (see the windowed backend's own docs
374    /// for why) and never emit this; an app that wants a default should
375    /// pick one itself rather than waiting for an event that may never
376    /// arrive.
377    ThemeChanged(SystemTheme),
378    /// Pasted text, delivered as a single event rather than individual key
379    /// presses.
380    ///
381    /// Not emitted by all backends: see each backend's own docs for
382    /// whether and how it sources this. Content is forwarded verbatim from
383    /// the source, including embedded newlines; the receiving app is
384    /// responsible for any filtering it needs.
385    Paste(String),
386    /// The terminal or application window gained input focus.
387    ///
388    /// This reflects OS/terminal-level focus, not in-app widget focus (see
389    /// `retroglyph-widgets`' focus ring for that).
390    FocusGained,
391    /// The terminal or application window lost input focus.
392    ///
393    /// This reflects OS/terminal-level focus, not in-app widget focus (see
394    /// `retroglyph-widgets`' focus ring for that).
395    FocusLost,
396    /// An application-defined event injected from outside the normal input
397    /// source (e.g. a network, audio, or timer thread), carrying an opaque
398    /// tag the app assigns its own meaning to.
399    ///
400    /// Only emitted by backends with a real cross-thread injection point:
401    /// the windowed (winit) backend's `EventProxy`
402    /// (`retroglyph_window::winit::EventProxy::send_event`), which forwards
403    /// the `u64` unchanged. The payload is a plain `u64`
404    /// rather than an arbitrary boxed value: it keeps `Event` cheaply
405    /// `Clone`/`PartialEq`/`Eq`/`Hash` (a `Box<dyn Any>` could not derive
406    /// any of those) and needs no generic parameter threaded through every
407    /// crate that names `Event`. Treat it as a correlation id: look up
408    /// the real payload in whatever shared state or channel the sending
409    /// thread already placed it in.
410    Custom(u64),
411}
412
413/// Tracks which keys are currently held down.
414///
415/// Feed it every [`KeyEvent`] (or [`Event`]) you receive and query
416/// [`is_held`](Self::is_held) each frame for held-key movement. A key is
417/// considered held from its first [`KeyEventKind::Press`] until a matching
418/// [`KeyEventKind::Release`].
419///
420/// Held keys are keyed by `(KeyCode, KeyLocation)`, so a held Numpad8 and a held digit-row 8 are
421/// tracked separately: [`is_held`](Self::is_held) takes the pair, and [`held`](Self::held) yields
422/// it.
423///
424/// This is only useful on backends that emit release events (winit, or a
425/// terminal with the kitty keyboard protocol). On press-only backends a key
426/// never leaves the held set on its own, so call [`clear`](Self::clear) at a
427/// suitable boundary (e.g. once per turn) if you rely on it there.
428#[derive(Debug, Clone, Default)]
429pub struct KeyState {
430    held: Vec<(KeyCode, KeyLocation)>,
431}
432
433impl KeyState {
434    /// Creates an empty key-state tracker.
435    #[must_use]
436    pub const fn new() -> Self {
437        Self { held: Vec::new() }
438    }
439
440    /// Updates the held set from a key event.
441    ///
442    /// [`Press`](KeyEventKind::Press) and [`Repeat`](KeyEventKind::Repeat) add
443    /// the `(code, location)` pair; [`Release`](KeyEventKind::Release) removes it.
444    pub fn apply(&mut self, event: KeyEvent) {
445        let entry = (event.code, event.location);
446        match event.kind {
447            KeyEventKind::Press | KeyEventKind::Repeat => {
448                if !self.held.contains(&entry) {
449                    self.held.push(entry);
450                }
451            }
452            KeyEventKind::Release => {
453                self.held.retain(|&e| e != entry);
454            }
455        }
456    }
457
458    /// Updates the held set from an [`Event`], ignoring non-key events.
459    pub fn apply_event(&mut self, event: &Event) {
460        if let Event::Key(key) = event {
461            self.apply(*key);
462        }
463    }
464
465    /// Returns `true` if `code` at `location` is currently held.
466    #[must_use]
467    pub fn is_held(&self, code: KeyCode, location: KeyLocation) -> bool {
468        self.held.contains(&(code, location))
469    }
470
471    /// Iterates the currently held `(code, location)` pairs, in first-pressed order.
472    pub fn held(&self) -> impl Iterator<Item = (KeyCode, KeyLocation)> + '_ {
473        self.held.iter().copied()
474    }
475
476    /// Clears all held keys.
477    pub fn clear(&mut self) {
478        self.held.clear();
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    #[test]
487    fn test_key_modifiers() {
488        let mods = KeyModifiers::SHIFT | KeyModifiers::CONTROL;
489        assert!(mods.contains(KeyModifiers::SHIFT));
490        assert!(mods.contains(KeyModifiers::CONTROL));
491        assert!(!mods.contains(KeyModifiers::ALT));
492        assert!(!mods.is_empty());
493
494        let inverse = !mods;
495        assert!(inverse.contains(KeyModifiers::ALT));
496        assert!(inverse.contains(KeyModifiers::SUPER));
497        assert!(!inverse.contains(KeyModifiers::SHIFT));
498        assert!(!inverse.contains(KeyModifiers::CONTROL));
499    }
500
501    #[test]
502    fn test_key_modifiers_super() {
503        let mods = KeyModifiers::SUPER;
504        assert!(mods.contains(KeyModifiers::SUPER));
505        assert!(!mods.contains(KeyModifiers::SHIFT));
506        assert!(!mods.contains(KeyModifiers::CONTROL));
507        assert!(!mods.contains(KeyModifiers::ALT));
508
509        let all =
510            KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SUPER;
511        assert!(all.contains(KeyModifiers::SUPER));
512        assert!(all.contains(KeyModifiers::SHIFT));
513        assert!(all.contains(KeyModifiers::CONTROL));
514        assert!(all.contains(KeyModifiers::ALT));
515    }
516
517    #[test]
518    fn test_event_construction() {
519        let key_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::SHIFT);
520        let event = Event::Key(key_event);
521
522        if let Event::Key(ke) = event {
523            assert_eq!(ke.code, KeyCode::Char('a'));
524            assert!(ke.modifiers.contains(KeyModifiers::SHIFT));
525            assert_eq!(ke.kind, KeyEventKind::Press);
526        } else {
527            panic!("Expected Event::Key");
528        }
529    }
530
531    #[test]
532    fn test_key_event_kind_helpers() {
533        let press = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
534        assert_eq!(press.kind, KeyEventKind::Press);
535        assert!(press.is_down());
536
537        let repeat =
538            KeyEvent::with_kind(KeyCode::Char('x'), KeyModifiers::NONE, KeyEventKind::Repeat);
539        assert!(repeat.is_down());
540
541        let release = KeyEvent::with_kind(
542            KeyCode::Char('x'),
543            KeyModifiers::NONE,
544            KeyEventKind::Release,
545        );
546        assert!(!release.is_down());
547    }
548
549    #[test]
550    fn test_key_state_tracks_held_keys() {
551        let mut state = KeyState::new();
552        assert!(!state.is_held(KeyCode::Left, KeyLocation::Standard));
553
554        state.apply(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
555        assert!(state.is_held(KeyCode::Left, KeyLocation::Standard));
556
557        // Repeat keeps it held.
558        state.apply(KeyEvent::with_kind(
559            KeyCode::Left,
560            KeyModifiers::NONE,
561            KeyEventKind::Repeat,
562        ));
563        assert!(state.is_held(KeyCode::Left, KeyLocation::Standard));
564
565        state.apply(KeyEvent::with_kind(
566            KeyCode::Left,
567            KeyModifiers::NONE,
568            KeyEventKind::Release,
569        ));
570        assert!(!state.is_held(KeyCode::Left, KeyLocation::Standard));
571    }
572
573    #[test]
574    fn test_key_state_distinguishes_numpad_from_standard() {
575        let mut state = KeyState::new();
576        state.apply(KeyEvent::with_location(
577            KeyCode::Char('8'),
578            KeyModifiers::NONE,
579            KeyEventKind::Press,
580            KeyLocation::Numpad,
581        ));
582        assert!(state.is_held(KeyCode::Char('8'), KeyLocation::Numpad));
583        assert!(!state.is_held(KeyCode::Char('8'), KeyLocation::Standard));
584
585        state.apply(KeyEvent::new(KeyCode::Char('8'), KeyModifiers::NONE));
586        assert!(state.is_held(KeyCode::Char('8'), KeyLocation::Standard));
587        assert!(state.is_held(KeyCode::Char('8'), KeyLocation::Numpad));
588
589        state.apply(KeyEvent::with_kind(
590            KeyCode::Char('8'),
591            KeyModifiers::NONE,
592            KeyEventKind::Release,
593        ));
594        assert!(!state.is_held(KeyCode::Char('8'), KeyLocation::Standard));
595        assert!(state.is_held(KeyCode::Char('8'), KeyLocation::Numpad));
596    }
597
598    #[test]
599    fn test_key_state_apply_event_ignores_non_key() {
600        let mut state = KeyState::new();
601        state.apply_event(&Event::Resize(1, 1));
602        assert!(state.held().next().is_none());
603        state.apply_event(&Event::Key(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)));
604        assert!(state.is_held(KeyCode::Up, KeyLocation::Standard));
605    }
606
607    #[test]
608    fn test_paste_event_carries_text() {
609        let event = Event::Paste("hello".to_string());
610        let Event::Paste(text) = event else {
611            panic!("Expected Event::Paste");
612        };
613        assert_eq!(text, "hello");
614    }
615
616    #[test]
617    fn test_custom_event_carries_opaque_id() {
618        let event = Event::Custom(42);
619        let Event::Custom(id) = event else {
620            panic!("Expected Event::Custom");
621        };
622        assert_eq!(id, 42);
623        assert_ne!(Event::Custom(1), Event::Custom(2));
624    }
625
626    #[test]
627    fn test_focus_gained_and_lost_are_distinct() {
628        assert!(matches!(Event::FocusGained, Event::FocusGained));
629        assert!(matches!(Event::FocusLost, Event::FocusLost));
630        assert_ne!(Event::FocusGained, Event::FocusLost);
631    }
632
633    #[test]
634    fn test_mouse_event_no_pixel_position() {
635        let mouse_event = MouseEvent {
636            kind: MouseEventKind::Down(MouseButton::Left),
637            position: Pos { x: 10, y: 5 },
638            pixel_position: None,
639            modifiers: KeyModifiers::NONE,
640        };
641        assert!(mouse_event.pixel_position.is_none());
642        assert!(matches!(Event::Mouse(mouse_event), Event::Mouse(_)));
643    }
644
645    #[test]
646    fn test_mouse_event_with_pixel_position() {
647        let mouse_event = MouseEvent {
648            kind: MouseEventKind::Moved,
649            position: Pos { x: 3, y: 2 },
650            pixel_position: Some(PhysicalPos { x: 55, y: 38 }),
651            modifiers: KeyModifiers::NONE,
652        };
653        let px = mouse_event.pixel_position.unwrap();
654        assert_eq!(px.x, 55);
655        assert_eq!(px.y, 38);
656        // Cell and pixel positions are distinct coordinate spaces.
657        assert_ne!(px.x, u32::from(mouse_event.position.x));
658    }
659
660    #[test]
661    fn test_physical_pos_is_copy() {
662        let p = PhysicalPos { x: 10, y: 20 };
663        let q = p; // Copy
664        assert_eq!(p, q);
665    }
666}