Skip to main content

retroglyph_ui/interact/
focus.rs

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