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            // Ignores magnitude for now, treating every `Scroll` event as one unit step,
94            // matching pre-#445 behavior; see retroglyph#445 for why magnitude exists but isn't
95            // consumed here yet.
96            MouseEventKind::Scroll { dy, .. } if dy > 0.0 => self.scroll_delta -= 1,
97            MouseEventKind::Scroll { dy, .. } if dy < 0.0 => self.scroll_delta += 1,
98            // Moved, plus future MouseEventKind/MouseButton variants (both
99            // #[non_exhaustive]): ignored until this crate is updated to track them.
100            _ => {}
101        }
102    }
103
104    /// Clear every button's one-shot `pressed`/`released` and this frame's
105    /// `scroll_delta`. Call once per frame, after drawing.
106    pub const fn end_frame(&mut self) {
107        let mut i = 0;
108        while i < self.buttons.len() {
109            self.buttons[i].pressed = false;
110            self.buttons[i].released = false;
111            i += 1;
112        }
113        self.scroll_delta = 0;
114    }
115
116    /// The pointer's last known cell-grid position, or `None` if no mouse
117    /// event has arrived yet.
118    #[must_use]
119    pub const fn pos(&self) -> Option<Pos> {
120        self.pos
121    }
122
123    /// `true` while `button` is held down.
124    ///
125    /// Always `false` for a `MouseButton` variant this crate doesn't yet track
126    /// (see `button_slot`'s doc comment).
127    #[must_use]
128    pub const fn is_down(&self, button: MouseButton) -> bool {
129        match button_slot(button) {
130            Some(slot) => self.buttons[slot].down,
131            None => false,
132        }
133    }
134
135    /// `true` for exactly the frame `button` went down.
136    ///
137    /// Always `false` for a `MouseButton` variant this crate doesn't yet track
138    /// (see `button_slot`'s doc comment).
139    #[must_use]
140    pub const fn pressed(&self, button: MouseButton) -> bool {
141        match button_slot(button) {
142            Some(slot) => self.buttons[slot].pressed,
143            None => false,
144        }
145    }
146
147    /// `true` for exactly the frame `button` went up.
148    ///
149    /// Always `false` for a `MouseButton` variant this crate doesn't yet track
150    /// (see `button_slot`'s doc comment).
151    #[must_use]
152    pub const fn released(&self, button: MouseButton) -> bool {
153        match button_slot(button) {
154            Some(slot) => self.buttons[slot].released,
155            None => false,
156        }
157    }
158
159    /// Scroll wheel delta accumulated this frame: positive is down/forward,
160    /// negative is up/backward. Zero if nothing scrolled.
161    #[must_use]
162    pub const fn scroll_delta(&self) -> i32 {
163        self.scroll_delta
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use retroglyph_core::{KeyModifiers, MouseEvent};
170
171    use super::*;
172
173    fn mouse(kind: MouseEventKind, pos: Pos) -> Event {
174        Event::Mouse(MouseEvent {
175            kind,
176            position: pos,
177            pixel_position: None,
178            modifiers: KeyModifiers::NONE,
179        })
180    }
181
182    #[test]
183    fn press_and_release_are_one_shot() {
184        let mut p = Pointer::new();
185        p.handle_event(&mouse(
186            MouseEventKind::Down(MouseButton::Left),
187            Pos::new(3, 4),
188        ));
189        assert!(p.is_down(MouseButton::Left));
190        assert!(p.pressed(MouseButton::Left));
191        assert_eq!(p.pos(), Some(Pos::new(3, 4)));
192
193        p.end_frame();
194        assert!(p.is_down(MouseButton::Left)); // level state survives end_frame
195        assert!(!p.pressed(MouseButton::Left)); // one-shot cleared
196
197        p.handle_event(&mouse(
198            MouseEventKind::Up(MouseButton::Left),
199            Pos::new(3, 4),
200        ));
201        assert!(!p.is_down(MouseButton::Left));
202        assert!(p.released(MouseButton::Left));
203    }
204
205    #[test]
206    fn buttons_are_tracked_independently() {
207        let mut p = Pointer::new();
208        p.handle_event(&mouse(
209            MouseEventKind::Down(MouseButton::Right),
210            Pos::new(1, 1),
211        ));
212        assert!(p.is_down(MouseButton::Right));
213        assert!(p.pressed(MouseButton::Right));
214        // Left is untouched by a Right-button event.
215        assert!(!p.is_down(MouseButton::Left));
216        assert!(!p.pressed(MouseButton::Left));
217        assert!(!p.is_down(MouseButton::Middle));
218    }
219
220    #[test]
221    fn scroll_accumulates_within_a_frame_and_clears_on_end_frame() {
222        let mut p = Pointer::new();
223        let scroll_down = MouseEventKind::Scroll { dx: 0.0, dy: -1.0 };
224        let scroll_up = MouseEventKind::Scroll { dx: 0.0, dy: 1.0 };
225        p.handle_event(&mouse(scroll_down, Pos::new(0, 0)));
226        p.handle_event(&mouse(scroll_down, Pos::new(0, 0)));
227        p.handle_event(&mouse(scroll_up, Pos::new(0, 0)));
228        assert_eq!(p.scroll_delta(), 1);
229
230        p.end_frame();
231        assert_eq!(p.scroll_delta(), 0);
232    }
233
234    #[test]
235    fn non_mouse_events_are_ignored() {
236        let mut p = Pointer::new();
237        p.handle_event(&Event::Resize(80, 24));
238        assert_eq!(p.pos(), None);
239    }
240}