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}