Skip to main content

retroglyph_widgets/interact/
focus.rs

1//! [`FocusRing`]: keyboard focus and Tab/Shift+Tab cycling over a set of
2//! ids established each frame.
3
4use retroglyph_core::{Event, KeyCode};
5
6/// Which id currently holds keyboard focus, plus Tab/Shift+Tab cycling
7/// through the ids [`register`](Self::register)ed as focusable.
8///
9/// Like [`HitTester`](crate::HitTester), registrations are per-frame and
10/// draw-ordered, but [`advance`](Self::advance)/[`retreat`](Self::retreat)
11/// always walk *last* frame's finalized order -- this frame's registrations
12/// aren't complete until the draw pass finishes. `current` itself, unlike
13/// the order, persists across frames like any other piece of app state,
14/// until focus moves or is [`clear`](Self::clear)ed.
15///
16/// If the currently focused id isn't in the order being cycled (e.g. it
17/// scrolled out of a list, or its widget wasn't drawn this frame), the next
18/// [`advance`](Self::advance)/[`retreat`](Self::retreat) treats that the
19/// same as nothing being focused, landing on the first/last registered id
20/// rather than getting stuck.
21#[derive(Debug, Clone)]
22pub struct FocusRing<Id> {
23    current: Option<Id>,
24    order: Vec<Id>,
25    pending: Vec<Id>,
26}
27
28impl<Id> FocusRing<Id> {
29    /// Nothing focused, nothing registered.
30    #[must_use]
31    pub const fn new() -> Self {
32        Self {
33            current: None,
34            order: Vec::new(),
35            pending: Vec::new(),
36        }
37    }
38
39    /// Finalize this frame's [`register`](Self::register) calls into the
40    /// order [`advance`](Self::advance)/[`retreat`](Self::retreat) will walk
41    /// during the frame that's about to start, and clear the registration
42    /// list for fresh calls. Call once per frame, before drawing.
43    pub fn begin_frame(&mut self) {
44        self.order = core::mem::take(&mut self.pending);
45    }
46
47    /// Drop focus entirely.
48    pub fn clear(&mut self) {
49        self.current = None;
50    }
51}
52
53impl<Id: Copy + PartialEq> FocusRing<Id> {
54    /// Register `id` as focusable this frame.
55    pub fn register(&mut self, id: Id) {
56        self.pending.push(id);
57    }
58
59    /// The currently focused id, if any.
60    #[must_use]
61    pub const fn focused(&self) -> Option<Id> {
62        self.current
63    }
64
65    /// `true` if `id` currently holds focus.
66    #[must_use]
67    pub fn is_focused(&self, id: Id) -> bool {
68        self.current == Some(id)
69    }
70
71    /// Explicitly focus `id`, e.g. in response to a click.
72    pub const fn request(&mut self, id: Id) {
73        self.current = Some(id);
74    }
75
76    /// Move focus to the next id in last frame's registration order,
77    /// wrapping past the end. Focuses the first registered id if nothing
78    /// was focused; a no-op if nothing was registered.
79    pub fn advance(&mut self) {
80        self.current = Self::step(&self.order, self.current, 1);
81    }
82
83    /// Move focus to the previous id in last frame's registration order,
84    /// wrapping past the start. Focuses the last registered id if nothing
85    /// was focused; a no-op if nothing was registered.
86    pub fn retreat(&mut self) {
87        self.current = Self::step(&self.order, self.current, -1);
88    }
89
90    /// Default Tab/Shift+Tab handling: [`advance`](Self::advance) on `Tab`,
91    /// [`retreat`](Self::retreat) on `BackTab` (shift+tab). Called
92    /// automatically by [`Interaction::handle_event`](crate::Interaction::handle_event);
93    /// call it yourself if you're using `FocusRing` standalone, or skip it
94    /// entirely and drive [`advance`](Self::advance)/[`retreat`](Self::retreat)
95    /// from something else (a gamepad shoulder button, say) if `Tab` needs
96    /// to mean something different in your app (inserting a literal tab
97    /// into a text field, for instance).
98    pub fn handle_event(&mut self, event: &Event) {
99        let Event::Key(key) = event else {
100            return;
101        };
102        if !key.is_down() {
103            return;
104        }
105        match key.code {
106            KeyCode::Tab => self.advance(),
107            KeyCode::BackTab => self.retreat(),
108            _ => {}
109        }
110    }
111
112    /// Shared wraparound math for `advance`/`retreat`, mirroring
113    /// [`ListState`](crate::ListState)'s `select_next`/`select_previous`:
114    /// `delta` is `1` or `-1`, and a `current` that's missing (or not found
115    /// in `order`) starts from the end opposite the direction of travel so
116    /// the first press lands somewhere sensible.
117    fn step(order: &[Id], current: Option<Id>, delta: i32) -> Option<Id> {
118        if order.is_empty() {
119            return None;
120        }
121        let Ok(len) = i32::try_from(order.len()) else {
122            return current; // absurdly large order; leave focus alone
123        };
124        let index = current.and_then(|id| order.iter().position(|&o| o == id));
125        let base = index.map_or(if delta > 0 { -1 } else { 0 }, |i| {
126            i32::try_from(i).unwrap_or(0)
127        });
128        let next = (base + delta).rem_euclid(len);
129        usize::try_from(next)
130            .ok()
131            .and_then(|i| order.get(i))
132            .copied()
133    }
134}
135
136// Not `#[derive(Default)]`: that would add an unnecessary `Id: Default`
137// bound to the generated impl, even though empty `Vec<Id>`s never need one.
138impl<Id> Default for FocusRing<Id> {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use retroglyph_core::{KeyEvent, KeyModifiers};
147
148    use super::*;
149
150    fn ring_of(ids: &[&'static str]) -> FocusRing<&'static str> {
151        let mut ring = FocusRing::new();
152        for &id in ids {
153            ring.register(id);
154        }
155        ring.begin_frame();
156        ring
157    }
158
159    #[test]
160    fn advance_from_nothing_focuses_the_first() {
161        let mut ring = ring_of(&["a", "b", "c"]);
162        ring.advance();
163        assert_eq!(ring.focused(), Some("a"));
164    }
165
166    #[test]
167    fn retreat_from_nothing_focuses_the_last() {
168        let mut ring = ring_of(&["a", "b", "c"]);
169        ring.retreat();
170        assert_eq!(ring.focused(), Some("c"));
171    }
172
173    #[test]
174    fn advance_wraps_past_the_end() {
175        let mut ring = ring_of(&["a", "b"]);
176        ring.request("b");
177        ring.advance();
178        assert_eq!(ring.focused(), Some("a"));
179    }
180
181    #[test]
182    fn retreat_wraps_past_the_start() {
183        let mut ring = ring_of(&["a", "b"]);
184        ring.request("a");
185        ring.retreat();
186        assert_eq!(ring.focused(), Some("b"));
187    }
188
189    #[test]
190    fn stale_focus_not_in_order_is_treated_as_unfocused() {
191        let mut ring = ring_of(&["a", "b"]);
192        ring.request("gone"); // e.g. the widget that had focus wasn't drawn this frame
193        ring.advance();
194        assert_eq!(ring.focused(), Some("a"));
195    }
196
197    #[test]
198    fn empty_order_is_a_no_op() {
199        let mut ring: FocusRing<&str> = FocusRing::new();
200        ring.begin_frame();
201        ring.advance();
202        assert_eq!(ring.focused(), None);
203    }
204
205    #[test]
206    fn tab_and_backtab_cycle_focus() {
207        let mut ring = ring_of(&["a", "b"]);
208        ring.handle_event(&Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)));
209        assert_eq!(ring.focused(), Some("a"));
210        ring.handle_event(&Event::Key(KeyEvent::new(
211            KeyCode::BackTab,
212            KeyModifiers::NONE,
213        )));
214        assert_eq!(ring.focused(), Some("b")); // wraps backward from "a"
215    }
216
217    #[test]
218    fn clear_drops_focus() {
219        let mut ring = ring_of(&["a"]);
220        ring.request("a");
221        ring.clear();
222        assert_eq!(ring.focused(), None);
223    }
224}