Skip to main content

retroglyph_widgets/interact/
pointer.rs

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