Skip to main content

retroglyph_core/event/
mouse.rs

1//! Mouse events: [`MouseButton`], [`MouseEventKind`], and [`MouseEvent`].
2
3use super::key::KeyModifiers;
4use crate::grid::Pos;
5
6/// Physical (pixel) position relative to the window's top-left corner.
7///
8/// Using `ixy::Pos<u32>` rather than the cell-grid [`Pos`] (`ixy::Pos<u16>`)
9/// makes the distinction type-safe: you cannot accidentally pass a pixel
10/// coordinate where a cell coordinate is expected.
11pub type PhysicalPos = ixy::Pos<u32>;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[non_exhaustive]
16/// Mouse button identifiers.
17pub enum MouseButton {
18    /// Left mouse button.
19    Left,
20    /// Right mouse button.
21    Right,
22    /// Middle mouse button.
23    Middle,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28#[non_exhaustive]
29/// Kinds of mouse events.
30///
31/// Does not derive `Eq`/`Hash`: [`Scroll`](Self::Scroll)'s `f32` fields implement neither.
32pub enum MouseEventKind {
33    /// Mouse button pressed.
34    Down(MouseButton),
35    /// Mouse button released.
36    Up(MouseButton),
37    /// Mouse moved while a button was held down; carries which button.
38    Drag(MouseButton),
39    /// Mouse moved.
40    Moved,
41    /// Mouse wheel/touchpad scroll.
42    ///
43    /// `dy > 0.0` is scroll up, `dy < 0.0` is scroll down; `dx > 0.0` is scroll right, `dx < 0.0`
44    /// is scroll left (mostly from a laptop touchpad). Magnitude is backend-dependent: the winit
45    /// backend reports the exact pixel/line delta from the platform, while the crossterm backend
46    /// synthesizes a fixed step of `1.0` per tick since terminals can't report scroll precision.
47    Scroll {
48        /// Horizontal delta. See the variant docs for the sign convention.
49        dx: f32,
50        /// Vertical delta. See the variant docs for the sign convention.
51        dy: f32,
52    },
53}
54
55#[derive(Debug, Clone, Copy, PartialEq)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57#[non_exhaustive]
58/// Mouse input event.
59///
60/// Does not derive `Eq`/`Hash`: [`MouseEventKind`] does not (its `Scroll` variant's `f32`
61/// fields implement neither).
62pub struct MouseEvent {
63    /// The kind of mouse event.
64    pub kind: MouseEventKind,
65    /// Cell-grid position of the mouse cursor.
66    pub position: Pos,
67    /// Physical pixel position of the mouse cursor, relative to the window's top-left.
68    ///
69    /// Populated by backends that support sub-cell precision (e.g. the software
70    /// renderer). `None` on character-mode backends such as crossterm.
71    pub pixel_position: Option<PhysicalPos>,
72    /// Modifiers held down during the event.
73    pub modifiers: KeyModifiers,
74}
75
76impl MouseEvent {
77    /// Creates a mouse event at the given cell-grid position, with no pixel position.
78    #[must_use]
79    pub const fn new(kind: MouseEventKind, position: Pos, modifiers: KeyModifiers) -> Self {
80        Self {
81            kind,
82            position,
83            pixel_position: None,
84            modifiers,
85        }
86    }
87
88    /// Creates a mouse event with an explicit pixel position, for backends with sub-cell
89    /// precision.
90    #[must_use]
91    pub const fn with_pixel_position(
92        kind: MouseEventKind,
93        position: Pos,
94        modifiers: KeyModifiers,
95        pixel_position: PhysicalPos,
96    ) -> Self {
97        Self {
98            kind,
99            position,
100            pixel_position: Some(pixel_position),
101            modifiers,
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::super::Event;
109    use super::*;
110
111    #[test]
112    fn test_mouse_event_no_pixel_position() {
113        let mouse_event = MouseEvent {
114            kind: MouseEventKind::Down(MouseButton::Left),
115            position: Pos { x: 10, y: 5 },
116            pixel_position: None,
117            modifiers: KeyModifiers::NONE,
118        };
119        assert!(mouse_event.pixel_position.is_none());
120        assert!(matches!(Event::Mouse(mouse_event), Event::Mouse(_)));
121    }
122
123    #[test]
124    fn test_mouse_event_with_pixel_position() {
125        let mouse_event = MouseEvent {
126            kind: MouseEventKind::Moved,
127            position: Pos { x: 3, y: 2 },
128            pixel_position: Some(PhysicalPos { x: 55, y: 38 }),
129            modifiers: KeyModifiers::NONE,
130        };
131        let px = mouse_event.pixel_position.unwrap();
132        assert_eq!(px.x, 55);
133        assert_eq!(px.y, 38);
134        // Cell and pixel positions are distinct coordinate spaces.
135        assert_ne!(px.x, u32::from(mouse_event.position.x));
136    }
137
138    #[test]
139    fn test_mouse_event_new_has_no_pixel_position() {
140        let mouse_event = MouseEvent::new(
141            MouseEventKind::Down(MouseButton::Left),
142            Pos { x: 10, y: 5 },
143            KeyModifiers::NONE,
144        );
145        assert_eq!(mouse_event.kind, MouseEventKind::Down(MouseButton::Left));
146        assert_eq!(mouse_event.position, Pos { x: 10, y: 5 });
147        assert!(mouse_event.pixel_position.is_none());
148    }
149
150    #[test]
151    fn test_mouse_event_with_pixel_position_constructor() {
152        let mouse_event = MouseEvent::with_pixel_position(
153            MouseEventKind::Moved,
154            Pos { x: 3, y: 2 },
155            KeyModifiers::NONE,
156            PhysicalPos { x: 55, y: 38 },
157        );
158        assert_eq!(
159            mouse_event.pixel_position,
160            Some(PhysicalPos { x: 55, y: 38 })
161        );
162    }
163
164    #[cfg(feature = "serde")]
165    #[test]
166    fn mouse_event_round_trips_through_serde_json() {
167        let mouse_event = MouseEvent::with_pixel_position(
168            MouseEventKind::Scroll { dx: 1.0, dy: -2.5 },
169            Pos { x: 3, y: 2 },
170            KeyModifiers::CONTROL,
171            PhysicalPos { x: 55, y: 38 },
172        );
173        let json = serde_json::to_string(&mouse_event).expect("serialize");
174        assert_eq!(
175            serde_json::from_str::<MouseEvent>(&json).expect("deserialize"),
176            mouse_event
177        );
178    }
179
180    #[test]
181    fn test_physical_pos_is_copy() {
182        let p = PhysicalPos { x: 10, y: 20 };
183        let q = p; // Copy
184        assert_eq!(p, q);
185    }
186}