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}