Another project
1use core::num::NonZeroU32;
2use std::collections::BTreeSet;
3
4use serde::{Deserialize, Serialize};
5
6use crate::input::InputSnapshot;
7use crate::widget_id::WidgetId;
8
9#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct FocusScopeId(NonZeroU32);
12
13impl FocusScopeId {
14 pub const ROOT: Self = match NonZeroU32::new(1) {
15 Some(n) => Self(n),
16 None => panic!("ROOT scope id is 1"),
17 };
18
19 #[must_use]
20 pub const fn get(self) -> NonZeroU32 {
21 self.0
22 }
23}
24
25#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub enum FocusScopeKind {
27 Window,
28 Modal,
29 Roving,
30}
31
32#[derive(Copy, Clone, Debug, PartialEq, Eq)]
33struct FocusScope {
34 id: FocusScopeId,
35 kind: FocusScopeKind,
36}
37
38const FIRST_CHILD_SCOPE: NonZeroU32 = match NonZeroU32::new(2) {
39 Some(n) => n,
40 None => panic!("first child scope id is 2"),
41};
42
43#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub enum InputModality {
45 Keyboard,
46 Pointer,
47}
48
49#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub enum RovingDirection {
51 Next,
52 Prev,
53 First,
54 Last,
55}
56
57#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
58pub enum FocusRequest {
59 Set(WidgetId),
60 Clear,
61 Advance,
62 Retreat,
63 Rove(RovingDirection),
64}
65
66#[derive(Clone, Debug, PartialEq)]
67pub struct FocusManager {
68 focused: Option<WidgetId>,
69 modality: InputModality,
70 scopes: Vec<FocusScope>,
71 next_scope: NonZeroU32,
72 tab_stops: Vec<(WidgetId, FocusScopeId)>,
73 tab_stop_ids: BTreeSet<WidgetId>,
74 focusable: BTreeSet<WidgetId>,
75 text_inputs: BTreeSet<WidgetId>,
76 request: Option<FocusRequest>,
77}
78
79impl Default for FocusManager {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl FocusManager {
86 #[must_use]
87 pub fn new() -> Self {
88 let root = FocusScope {
89 id: FocusScopeId::ROOT,
90 kind: FocusScopeKind::Window,
91 };
92 Self {
93 focused: None,
94 modality: InputModality::Pointer,
95 scopes: vec![root],
96 next_scope: FIRST_CHILD_SCOPE,
97 tab_stops: Vec::new(),
98 tab_stop_ids: BTreeSet::new(),
99 focusable: BTreeSet::new(),
100 text_inputs: BTreeSet::new(),
101 request: None,
102 }
103 }
104
105 #[must_use]
106 pub fn focused(&self) -> Option<WidgetId> {
107 self.focused
108 }
109
110 #[must_use]
111 pub fn focus_visible(&self) -> bool {
112 matches!(self.modality, InputModality::Keyboard)
113 }
114
115 pub fn observe_input(&mut self, input: &InputSnapshot) {
116 if !input.keys_pressed.is_empty() {
117 self.modality = InputModality::Keyboard;
118 } else if !input.buttons_pressed.is_empty() || !input.buttons_released.is_empty() {
119 self.modality = InputModality::Pointer;
120 }
121 }
122
123 #[must_use]
124 pub fn current_scope(&self) -> FocusScopeId {
125 self.scopes.last().map_or(FocusScopeId::ROOT, |s| s.id)
126 }
127
128 pub fn push_scope(&mut self, kind: FocusScopeKind) -> FocusScopeId {
129 let id = FocusScopeId(self.next_scope);
130 self.next_scope = match self.next_scope.checked_add(1) {
131 Some(n) => n,
132 None => panic!("focus scope id space exhausted"),
133 };
134 self.scopes.push(FocusScope { id, kind });
135 id
136 }
137
138 pub fn pop_scope(&mut self) -> Option<FocusScopeId> {
139 if self.scopes.len() <= 1 {
140 return None;
141 }
142 self.scopes.pop().map(|s| s.id)
143 }
144
145 pub fn register_tab_stop(&mut self, id: WidgetId) {
146 if !self.tab_stop_ids.insert(id) {
147 return;
148 }
149 self.focusable.insert(id);
150 let scope = self.current_scope();
151 self.tab_stops.push((id, scope));
152 }
153
154 pub fn register_focusable(&mut self, id: WidgetId) {
155 self.focusable.insert(id);
156 }
157
158 pub fn register_text_input(&mut self, id: WidgetId) {
159 self.text_inputs.insert(id);
160 }
161
162 #[must_use]
163 pub fn is_text_input_focused(&self) -> bool {
164 self.focused
165 .is_some_and(|id| self.text_inputs.contains(&id))
166 }
167
168 #[must_use]
169 pub fn tab_stops(&self) -> &[(WidgetId, FocusScopeId)] {
170 &self.tab_stops
171 }
172
173 pub fn focusable_ids(&self) -> impl Iterator<Item = WidgetId> + '_ {
174 self.focusable.iter().copied()
175 }
176
177 pub fn request(&mut self, request: FocusRequest) {
178 self.request = Some(request);
179 }
180
181 pub fn request_focus(&mut self, id: WidgetId) {
182 self.request(FocusRequest::Set(id));
183 }
184
185 pub fn begin_frame(&mut self) {
186 self.tab_stops.clear();
187 self.tab_stop_ids.clear();
188 self.focusable.clear();
189 self.text_inputs.clear();
190 self.scopes.truncate(1);
191 self.next_scope = FIRST_CHILD_SCOPE;
192 }
193
194 pub fn end_frame(&mut self) {
195 let request = self.request.take();
196 let next = match request {
197 None => self.focused,
198 Some(FocusRequest::Set(id)) => Some(id),
199 Some(FocusRequest::Clear) => None,
200 Some(FocusRequest::Advance) => self.advance_within_scope(false),
201 Some(FocusRequest::Retreat) => self.advance_within_scope(true),
202 Some(FocusRequest::Rove(direction)) => self.rove_within_roving_scope(direction),
203 };
204 self.focused = self.validate_focus(next);
205 }
206
207 fn validate_focus(&self, candidate: Option<WidgetId>) -> Option<WidgetId> {
208 candidate.filter(|id| self.focusable.contains(id))
209 }
210
211 fn nearest_scope(&self, kind: FocusScopeKind) -> Option<FocusScopeId> {
212 self.scopes
213 .iter()
214 .rev()
215 .find(|s| s.kind == kind)
216 .map(|s| s.id)
217 }
218
219 fn stops_for_scope(&self, scope: FocusScopeId) -> Vec<WidgetId> {
220 self.tab_stops
221 .iter()
222 .filter(|(_, s)| *s == scope)
223 .map(|(id, _)| *id)
224 .collect()
225 }
226
227 fn advance_within_scope(&self, reverse: bool) -> Option<WidgetId> {
228 let trap = self
229 .nearest_scope(FocusScopeKind::Modal)
230 .unwrap_or(FocusScopeId::ROOT);
231 let stops = self.stops_for_scope(trap);
232 cycle(&stops, self.focused, reverse)
233 }
234
235 fn rove_within_roving_scope(&self, direction: RovingDirection) -> Option<WidgetId> {
236 let scope = self.nearest_scope(FocusScopeKind::Roving)?;
237 let stops = self.stops_for_scope(scope);
238 match direction {
239 RovingDirection::Next => cycle(&stops, self.focused, false),
240 RovingDirection::Prev => cycle(&stops, self.focused, true),
241 RovingDirection::First => stops.first().copied(),
242 RovingDirection::Last => stops.last().copied(),
243 }
244 }
245}
246
247fn cycle(stops: &[WidgetId], current: Option<WidgetId>, reverse: bool) -> Option<WidgetId> {
248 if stops.is_empty() {
249 return None;
250 }
251 let len = stops.len();
252 let next_index = match current.and_then(|id| stops.iter().position(|s| *s == id)) {
253 None => {
254 if reverse {
255 len - 1
256 } else {
257 0
258 }
259 }
260 Some(idx) => {
261 if reverse {
262 (idx + len - 1) % len
263 } else {
264 (idx + 1) % len
265 }
266 }
267 };
268 stops.get(next_index).copied()
269}
270
271#[cfg(test)]
272mod tests {
273 use super::{FocusManager, FocusRequest, FocusScopeKind, RovingDirection};
274 use crate::input::{
275 FrameInstant, InputSnapshot, KeyChar, KeyCode, KeyEvent, ModifierMask, PointerButton,
276 PointerButtonMask,
277 };
278 use crate::widget_id::{WidgetId, WidgetKey};
279
280 fn id(key: &'static str) -> WidgetId {
281 WidgetId::ROOT.child(WidgetKey::new(key))
282 }
283
284 fn run_frame<F: FnOnce(&mut FocusManager)>(focus: &mut FocusManager, build: F) {
285 focus.begin_frame();
286 build(focus);
287 focus.end_frame();
288 }
289
290 fn key_input() -> InputSnapshot {
291 let mut input = InputSnapshot::idle(FrameInstant::ZERO);
292 input.keys_pressed.push(KeyEvent::new(
293 KeyCode::Char(KeyChar::from_char('a')),
294 ModifierMask::NONE,
295 ));
296 input
297 }
298
299 fn pointer_button_input() -> InputSnapshot {
300 let mut input = InputSnapshot::idle(FrameInstant::ZERO);
301 input.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);
302 input
303 }
304
305 #[test]
306 fn observe_input_flips_to_keyboard_on_keypress() {
307 let mut focus = FocusManager::new();
308 focus.observe_input(&pointer_button_input());
309 assert!(!focus.focus_visible());
310 focus.observe_input(&key_input());
311 assert!(focus.focus_visible());
312 }
313
314 #[test]
315 fn observe_input_flips_back_to_pointer_on_button() {
316 let mut focus = FocusManager::new();
317 focus.observe_input(&key_input());
318 assert!(focus.focus_visible());
319 focus.observe_input(&pointer_button_input());
320 assert!(!focus.focus_visible());
321 }
322
323 #[test]
324 fn observe_input_idle_keeps_modality() {
325 let mut focus = FocusManager::new();
326 focus.observe_input(&key_input());
327 assert!(focus.focus_visible());
328 focus.observe_input(&InputSnapshot::idle(FrameInstant::ZERO));
329 assert!(focus.focus_visible(), "idle frames must not flip modality");
330 }
331
332 #[test]
333 fn advance_walks_tab_stops_in_order() {
334 let mut focus = FocusManager::new();
335 let a = id("a");
336 let b = id("b");
337 let c = id("c");
338 run_frame(&mut focus, |f| {
339 f.register_tab_stop(a);
340 f.register_tab_stop(b);
341 f.register_tab_stop(c);
342 f.request(FocusRequest::Advance);
343 });
344 assert_eq!(focus.focused(), Some(a));
345 run_frame(&mut focus, |f| {
346 f.register_tab_stop(a);
347 f.register_tab_stop(b);
348 f.register_tab_stop(c);
349 f.request(FocusRequest::Advance);
350 });
351 assert_eq!(focus.focused(), Some(b));
352 run_frame(&mut focus, |f| {
353 f.register_tab_stop(a);
354 f.register_tab_stop(b);
355 f.register_tab_stop(c);
356 f.request(FocusRequest::Advance);
357 });
358 assert_eq!(focus.focused(), Some(c));
359 run_frame(&mut focus, |f| {
360 f.register_tab_stop(a);
361 f.register_tab_stop(b);
362 f.register_tab_stop(c);
363 f.request(FocusRequest::Advance);
364 });
365 assert_eq!(focus.focused(), Some(a));
366 }
367
368 #[test]
369 fn retreat_walks_in_reverse() {
370 let mut focus = FocusManager::new();
371 let a = id("a");
372 let b = id("b");
373 run_frame(&mut focus, |f| {
374 f.register_tab_stop(a);
375 f.register_tab_stop(b);
376 f.request_focus(a);
377 });
378 run_frame(&mut focus, |f| {
379 f.register_tab_stop(a);
380 f.register_tab_stop(b);
381 f.request(FocusRequest::Retreat);
382 });
383 assert_eq!(focus.focused(), Some(b));
384 }
385
386 #[test]
387 fn modal_trap_restricts_tab_traversal() {
388 let mut focus = FocusManager::new();
389 let outer = id("outer");
390 let modal_a = id("modal_a");
391 let modal_b = id("modal_b");
392 run_frame(&mut focus, |f| {
393 f.register_tab_stop(outer);
394 f.push_scope(FocusScopeKind::Modal);
395 f.register_tab_stop(modal_a);
396 f.register_tab_stop(modal_b);
397 f.request_focus(modal_a);
398 });
399 assert_eq!(focus.focused(), Some(modal_a));
400 run_frame(&mut focus, |f| {
401 f.register_tab_stop(outer);
402 f.push_scope(FocusScopeKind::Modal);
403 f.register_tab_stop(modal_a);
404 f.register_tab_stop(modal_b);
405 f.request(FocusRequest::Advance);
406 });
407 assert_eq!(focus.focused(), Some(modal_b));
408 run_frame(&mut focus, |f| {
409 f.register_tab_stop(outer);
410 f.push_scope(FocusScopeKind::Modal);
411 f.register_tab_stop(modal_a);
412 f.register_tab_stop(modal_b);
413 f.request(FocusRequest::Advance);
414 });
415 assert_eq!(focus.focused(), Some(modal_a));
416 }
417
418 #[test]
419 fn roving_within_composite_widget() {
420 let mut focus = FocusManager::new();
421 let toolbar_a = id("tool_a");
422 let toolbar_b = id("tool_b");
423 let toolbar_c = id("tool_c");
424 run_frame(&mut focus, |f| {
425 f.push_scope(FocusScopeKind::Roving);
426 f.register_tab_stop(toolbar_a);
427 f.register_tab_stop(toolbar_b);
428 f.register_tab_stop(toolbar_c);
429 f.request_focus(toolbar_b);
430 });
431 run_frame(&mut focus, |f| {
432 f.push_scope(FocusScopeKind::Roving);
433 f.register_tab_stop(toolbar_a);
434 f.register_tab_stop(toolbar_b);
435 f.register_tab_stop(toolbar_c);
436 f.request(FocusRequest::Rove(RovingDirection::Next));
437 });
438 assert_eq!(focus.focused(), Some(toolbar_c));
439 run_frame(&mut focus, |f| {
440 f.push_scope(FocusScopeKind::Roving);
441 f.register_tab_stop(toolbar_a);
442 f.register_tab_stop(toolbar_b);
443 f.register_tab_stop(toolbar_c);
444 f.request(FocusRequest::Rove(RovingDirection::First));
445 });
446 assert_eq!(focus.focused(), Some(toolbar_a));
447 }
448
449 #[test]
450 fn focus_drops_when_widget_disappears() {
451 let mut focus = FocusManager::new();
452 let a = id("a");
453 let b = id("b");
454 run_frame(&mut focus, |f| {
455 f.register_tab_stop(a);
456 f.register_tab_stop(b);
457 f.request_focus(b);
458 });
459 assert_eq!(focus.focused(), Some(b));
460 run_frame(&mut focus, |f| {
461 f.register_tab_stop(a);
462 });
463 assert_eq!(focus.focused(), None);
464 }
465
466 #[test]
467 fn pop_scope_does_not_unseat_root() {
468 let mut focus = FocusManager::new();
469 assert!(focus.pop_scope().is_none());
470 let inner = focus.push_scope(FocusScopeKind::Modal);
471 assert_eq!(focus.pop_scope(), Some(inner));
472 assert!(focus.pop_scope().is_none());
473 }
474
475 #[test]
476 fn tab_skips_roving_stops_without_modal() {
477 let mut focus = FocusManager::new();
478 let host = id("toolbar");
479 let rove_a = id("rove_a");
480 let rove_b = id("rove_b");
481 let trailing = id("after");
482 run_frame(&mut focus, |f| {
483 f.register_tab_stop(host);
484 f.push_scope(FocusScopeKind::Roving);
485 f.register_tab_stop(rove_a);
486 f.register_tab_stop(rove_b);
487 f.pop_scope();
488 f.register_tab_stop(trailing);
489 f.request(FocusRequest::Advance);
490 });
491 assert_eq!(focus.focused(), Some(host));
492 run_frame(&mut focus, |f| {
493 f.register_tab_stop(host);
494 f.push_scope(FocusScopeKind::Roving);
495 f.register_tab_stop(rove_a);
496 f.register_tab_stop(rove_b);
497 f.pop_scope();
498 f.register_tab_stop(trailing);
499 f.request(FocusRequest::Advance);
500 });
501 assert_eq!(focus.focused(), Some(trailing));
502 }
503
504 #[test]
505 fn rove_returns_none_outside_roving_scope() {
506 let mut focus = FocusManager::new();
507 let a = id("a");
508 let b = id("b");
509 run_frame(&mut focus, |f| {
510 f.register_tab_stop(a);
511 f.register_tab_stop(b);
512 f.request_focus(a);
513 });
514 assert_eq!(focus.focused(), Some(a));
515 run_frame(&mut focus, |f| {
516 f.register_tab_stop(a);
517 f.register_tab_stop(b);
518 f.request(FocusRequest::Rove(RovingDirection::Next));
519 });
520 assert_eq!(focus.focused(), None);
521 }
522
523 #[test]
524 fn begin_frame_resets_scope_ids() {
525 let mut focus = FocusManager::new();
526 focus.begin_frame();
527 let first = focus.push_scope(FocusScopeKind::Modal);
528 focus.end_frame();
529 focus.begin_frame();
530 let second = focus.push_scope(FocusScopeKind::Modal);
531 focus.end_frame();
532 assert_eq!(first, second, "scope ids reset each frame");
533 }
534
535 #[test]
536 fn begin_frame_truncates_leaked_scopes() {
537 let mut focus = FocusManager::new();
538 focus.push_scope(FocusScopeKind::Modal);
539 focus.push_scope(FocusScopeKind::Roving);
540 focus.begin_frame();
541 assert_eq!(focus.current_scope(), super::FocusScopeId::ROOT);
542 }
543
544 #[test]
545 fn register_tab_stop_dedupes_per_frame() {
546 let mut focus = FocusManager::new();
547 let a = id("a");
548 let b = id("b");
549 run_frame(&mut focus, |f| {
550 f.register_tab_stop(a);
551 f.register_tab_stop(a);
552 f.register_tab_stop(b);
553 f.request_focus(a);
554 });
555 assert_eq!(focus.tab_stops().len(), 2);
556 run_frame(&mut focus, |f| {
557 f.register_tab_stop(a);
558 f.register_tab_stop(a);
559 f.register_tab_stop(b);
560 f.request(FocusRequest::Advance);
561 });
562 assert_eq!(focus.focused(), Some(b));
563 }
564
565 #[test]
566 fn programmatic_focus_holds_for_focusable_non_tab_stop() {
567 let mut focus = FocusManager::new();
568 let click_only = id("canvas");
569 run_frame(&mut focus, |f| {
570 f.register_focusable(click_only);
571 f.request_focus(click_only);
572 });
573 assert_eq!(focus.focused(), Some(click_only));
574 }
575
576 #[test]
577 fn programmatic_focus_drops_for_unknown_widget() {
578 let mut focus = FocusManager::new();
579 let ghost = id("ghost");
580 run_frame(&mut focus, |f| {
581 f.request_focus(ghost);
582 });
583 assert_eq!(focus.focused(), None);
584 }
585
586 #[test]
587 fn is_text_input_focused_distinguishes_text_input_from_button() {
588 let mut focus = FocusManager::new();
589 let button = id("button");
590 let input = id("input");
591 run_frame(&mut focus, |f| {
592 f.register_focusable(button);
593 f.register_focusable(input);
594 f.register_text_input(input);
595 f.request_focus(button);
596 });
597 assert_eq!(focus.focused(), Some(button));
598 assert!(
599 !focus.is_text_input_focused(),
600 "button focus must not match"
601 );
602 run_frame(&mut focus, |f| {
603 f.register_focusable(button);
604 f.register_focusable(input);
605 f.register_text_input(input);
606 f.request_focus(input);
607 });
608 assert_eq!(focus.focused(), Some(input));
609 assert!(focus.is_text_input_focused());
610 }
611
612 #[test]
613 fn is_text_input_focused_false_when_nothing_focused() {
614 let focus = FocusManager::new();
615 assert!(!focus.is_text_input_focused());
616 }
617
618 #[test]
619 fn modal_traps_tab_with_nested_roving_excluded() {
620 let mut focus = FocusManager::new();
621 let modal_a = id("modal_a");
622 let rove_x = id("rove_x");
623 let modal_b = id("modal_b");
624 run_frame(&mut focus, |f| {
625 f.push_scope(FocusScopeKind::Modal);
626 f.register_tab_stop(modal_a);
627 f.push_scope(FocusScopeKind::Roving);
628 f.register_tab_stop(rove_x);
629 f.pop_scope();
630 f.register_tab_stop(modal_b);
631 f.request_focus(modal_a);
632 });
633 run_frame(&mut focus, |f| {
634 f.push_scope(FocusScopeKind::Modal);
635 f.register_tab_stop(modal_a);
636 f.push_scope(FocusScopeKind::Roving);
637 f.register_tab_stop(rove_x);
638 f.pop_scope();
639 f.register_tab_stop(modal_b);
640 f.request(FocusRequest::Advance);
641 });
642 assert_eq!(focus.focused(), Some(modal_b));
643 }
644}