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}