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