Skip to main content

retroglyph_ui/interact/
pointer.rs

1//! [`Pointer`]: raw mouse/pointer state derived from a stream of
2//! [`Event`]s.
3
4use retroglyph_core::event::{Event, MouseButton, MouseEventKind};
5use retroglyph_core::grid::Pos;
6
7/// Per-button down/pressed/released state, tracked independently for each
8/// [`MouseButton`].
9#[derive(Debug, Clone, Copy, Default)]
10struct ButtonState {
11    down: bool,
12    pressed: bool,
13    released: bool,
14}
15
16/// Index into [`Pointer::buttons`] for a given [`MouseButton`]. A plain
17/// match over three fixed variants rather than a `HashMap`: no allocation,
18/// no hashing, and the array stays small/`Copy`: fits this crate's
19/// dependency-minimal, `no_std`-friendly habits (see
20/// [`Sense`](crate::Sense)'s doc comment for the same reasoning applied to
21/// bitflags).
22///
23/// `MouseButton` is `#[non_exhaustive]`, so any future variant this crate
24/// doesn't yet know about returns `None` rather than aliasing onto an
25/// existing slot (which would silently misreport that button's state).
26const fn button_slot(button: MouseButton) -> Option<usize> {
27    match button {
28        MouseButton::Left => Some(0),
29        MouseButton::Right => Some(1),
30        MouseButton::Middle => Some(2),
31        _ => None,
32    }
33}
34
35/// Cell-grid pointer position and per-button state, updated by feeding it
36/// every [`Event`] you receive.
37///
38/// Tracks all three [`MouseButton`] variants independently (unlike
39/// [`Interaction`](crate::Interaction)'s higher-level click/drag/focus
40/// resolution, which only ever resolves the primary button plus a narrower
41/// secondary-click signal; see [`Sense::SECONDARY_CLICK`](crate::Sense::SECONDARY_CLICK)).
42/// Mirrors [`KeyState`](retroglyph_core::event::KeyState)'s "feed events in, query
43/// state out" shape.
44///
45/// [`pressed`](Self::pressed)/[`released`](Self::released)/[`scroll_delta`](Self::scroll_delta)
46/// are one-shot: populated only for the frame the underlying event arrived
47/// in, then cleared by [`end_frame`](Self::end_frame).
48/// [`pos`](Self::pos)/[`is_down`](Self::is_down) are level state that
49/// persists until the next change.
50#[derive(Debug, Clone, Copy, Default)]
51pub struct Pointer {
52    pos: Option<Pos>,
53    buttons: [ButtonState; 3],
54    scroll_delta: i32,
55}
56
57impl Pointer {
58    /// No known position, nothing pressed.
59    #[must_use]
60    pub const fn new() -> Self {
61        Self {
62            pos: None,
63            buttons: [ButtonState {
64                down: false,
65                pressed: false,
66                released: false,
67            }; 3],
68            scroll_delta: 0,
69        }
70    }
71
72    /// Update from a raw input event; ignores everything but
73    /// [`Event::Mouse`].
74    pub const fn handle_event(&mut self, event: &Event) {
75        let Event::Mouse(mouse) = event else {
76            return;
77        };
78        self.pos = Some(mouse.position);
79        match mouse.kind {
80            MouseEventKind::Down(button) => {
81                if let Some(slot) = button_slot(button) {
82                    let slot = &mut self.buttons[slot];
83                    slot.down = true;
84                    slot.pressed = true;
85                }
86            }
87            MouseEventKind::Up(button) => {
88                if let Some(slot) = button_slot(button) {
89                    let slot = &mut self.buttons[slot];
90                    slot.down = false;
91                    slot.released = true;
92                }
93            }
94            // Ignores magnitude for now, treating every `Scroll` event as one unit step,
95            // matching pre-#445 behavior; see retroglyph#445 for why magnitude exists but isn't
96            // consumed here yet.
97            MouseEventKind::Scroll { dy, .. } if dy > 0.0 => self.scroll_delta -= 1,
98            MouseEventKind::Scroll { dy, .. } if dy < 0.0 => self.scroll_delta += 1,
99            // Moved, plus future MouseEventKind/MouseButton variants (both
100            // #[non_exhaustive]): ignored until this crate is updated to track them.
101            _ => {}
102        }
103    }
104
105    /// Clear every button's one-shot `pressed`/`released` and this frame's
106    /// `scroll_delta`. Call once per frame, after drawing.
107    pub const fn end_frame(&mut self) {
108        let mut i = 0;
109        while i < self.buttons.len() {
110            self.buttons[i].pressed = false;
111            self.buttons[i].released = false;
112            i += 1;
113        }
114        self.scroll_delta = 0;
115    }
116
117    /// The pointer's last known cell-grid position, or `None` if no mouse
118    /// event has arrived yet.
119    #[must_use]
120    pub const fn pos(&self) -> Option<Pos> {
121        self.pos
122    }
123
124    /// `true` while `button` is held down.
125    ///
126    /// Always `false` for a `MouseButton` variant this crate doesn't yet track
127    /// (see `button_slot`'s doc comment).
128    #[must_use]
129    pub const fn is_down(&self, button: MouseButton) -> bool {
130        match button_slot(button) {
131            Some(slot) => self.buttons[slot].down,
132            None => false,
133        }
134    }
135
136    /// `true` for exactly the frame `button` went down.
137    ///
138    /// Always `false` for a `MouseButton` variant this crate doesn't yet track
139    /// (see `button_slot`'s doc comment).
140    #[must_use]
141    pub const fn pressed(&self, button: MouseButton) -> bool {
142        match button_slot(button) {
143            Some(slot) => self.buttons[slot].pressed,
144            None => false,
145        }
146    }
147
148    /// `true` for exactly the frame `button` went up.
149    ///
150    /// Always `false` for a `MouseButton` variant this crate doesn't yet track
151    /// (see `button_slot`'s doc comment).
152    #[must_use]
153    pub const fn released(&self, button: MouseButton) -> bool {
154        match button_slot(button) {
155            Some(slot) => self.buttons[slot].released,
156            None => false,
157        }
158    }
159
160    /// Scroll wheel delta accumulated this frame: positive is down/forward,
161    /// negative is up/backward. Zero if nothing scrolled.
162    #[must_use]
163    pub const fn scroll_delta(&self) -> i32 {
164        self.scroll_delta
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use retroglyph_core::event::{KeyModifiers, MouseEvent};
171
172    use super::*;
173
174    fn mouse(kind: MouseEventKind, pos: Pos) -> Event {
175        Event::Mouse(MouseEvent::new(kind, pos, KeyModifiers::NONE))
176    }
177
178    #[test]
179    fn press_and_release_are_one_shot() {
180        let mut p = Pointer::new();
181        p.handle_event(&mouse(
182            MouseEventKind::Down(MouseButton::Left),
183            Pos::new(3, 4),
184        ));
185        assert!(p.is_down(MouseButton::Left));
186        assert!(p.pressed(MouseButton::Left));
187        assert_eq!(p.pos(), Some(Pos::new(3, 4)));
188
189        p.end_frame();
190        assert!(p.is_down(MouseButton::Left)); // level state survives end_frame
191        assert!(!p.pressed(MouseButton::Left)); // one-shot cleared
192
193        p.handle_event(&mouse(
194            MouseEventKind::Up(MouseButton::Left),
195            Pos::new(3, 4),
196        ));
197        assert!(!p.is_down(MouseButton::Left));
198        assert!(p.released(MouseButton::Left));
199    }
200
201    #[test]
202    fn buttons_are_tracked_independently() {
203        let mut p = Pointer::new();
204        p.handle_event(&mouse(
205            MouseEventKind::Down(MouseButton::Right),
206            Pos::new(1, 1),
207        ));
208        assert!(p.is_down(MouseButton::Right));
209        assert!(p.pressed(MouseButton::Right));
210        // Left is untouched by a Right-button event.
211        assert!(!p.is_down(MouseButton::Left));
212        assert!(!p.pressed(MouseButton::Left));
213        assert!(!p.is_down(MouseButton::Middle));
214    }
215
216    #[test]
217    fn scroll_accumulates_within_a_frame_and_clears_on_end_frame() {
218        let mut p = Pointer::new();
219        let scroll_down = MouseEventKind::Scroll { dx: 0.0, dy: -1.0 };
220        let scroll_up = MouseEventKind::Scroll { dx: 0.0, dy: 1.0 };
221        p.handle_event(&mouse(scroll_down, Pos::new(0, 0)));
222        p.handle_event(&mouse(scroll_down, Pos::new(0, 0)));
223        p.handle_event(&mouse(scroll_up, Pos::new(0, 0)));
224        assert_eq!(p.scroll_delta(), 1);
225
226        p.end_frame();
227        assert_eq!(p.scroll_delta(), 0);
228    }
229
230    #[test]
231    fn non_mouse_events_are_ignored() {
232        let mut p = Pointer::new();
233        p.handle_event(&Event::Resize(80, 24));
234        assert_eq!(p.pos(), None);
235    }
236}