1#![forbid(unsafe_code)]
2
3use ahash::AHashMap;
10
11use ftui_core::event::KeyCode;
12
13use super::indicator::FocusIndicator;
14use super::spatial;
15use super::{FocusGraph, FocusId, NavDirection};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum FocusEvent {
20 FocusGained { id: FocusId },
21 FocusLost { id: FocusId },
22 FocusMoved { from: FocusId, to: FocusId },
23}
24
25#[derive(Debug, Clone)]
27pub struct FocusGroup {
28 pub id: u32,
29 pub members: Vec<FocusId>,
30 pub wrap: bool,
31 pub exit_key: Option<KeyCode>,
32}
33
34impl FocusGroup {
35 #[must_use]
36 pub fn new(id: u32, members: Vec<FocusId>) -> Self {
37 Self {
38 id,
39 members,
40 wrap: true,
41 exit_key: None,
42 }
43 }
44
45 #[must_use]
46 pub fn with_wrap(mut self, wrap: bool) -> Self {
47 self.wrap = wrap;
48 self
49 }
50
51 #[must_use]
52 pub fn with_exit_key(mut self, key: KeyCode) -> Self {
53 self.exit_key = Some(key);
54 self
55 }
56
57 fn contains(&self, id: FocusId) -> bool {
58 self.members.contains(&id)
59 }
60}
61
62#[derive(Debug, Clone, Copy)]
64pub struct FocusTrap {
65 pub group_id: u32,
66 pub return_focus: Option<FocusId>,
67}
68
69#[derive(Debug, Default)]
74pub struct FocusManager {
75 graph: FocusGraph,
76 current: Option<FocusId>,
77 history: Vec<FocusId>,
78 trap_stack: Vec<FocusTrap>,
79 groups: AHashMap<u32, FocusGroup>,
80 last_event: Option<FocusEvent>,
81 indicator: FocusIndicator,
82 focus_change_count: u64,
84}
85
86impl FocusManager {
87 #[must_use]
89 pub fn new() -> Self {
90 Self::default()
91 }
92
93 #[must_use]
95 pub fn graph(&self) -> &FocusGraph {
96 &self.graph
97 }
98
99 pub fn graph_mut(&mut self) -> &mut FocusGraph {
101 &mut self.graph
102 }
103
104 #[inline]
106 #[must_use]
107 pub fn current(&self) -> Option<FocusId> {
108 self.current
109 }
110
111 #[must_use]
113 pub fn is_focused(&self, id: FocusId) -> bool {
114 self.current == Some(id)
115 }
116
117 pub fn focus(&mut self, id: FocusId) -> Option<FocusId> {
119 if !self.can_focus(id) || !self.allowed_by_trap(id) {
120 return None;
121 }
122 let prev = self.current;
123 if prev == Some(id) {
124 return prev;
125 }
126 self.set_focus(id);
127 prev
128 }
129
130 pub fn blur(&mut self) -> Option<FocusId> {
132 let prev = self.current.take();
133 if let Some(id) = prev {
134 #[cfg(feature = "tracing")]
135 tracing::debug!(from_widget = id, trigger = "blur", "focus.change");
136 self.last_event = Some(FocusEvent::FocusLost { id });
137 self.focus_change_count += 1;
138 }
139 prev
140 }
141
142 pub fn navigate(&mut self, dir: NavDirection) -> bool {
144 match dir {
145 NavDirection::Next => self.focus_next(),
146 NavDirection::Prev => self.focus_prev(),
147 _ => {
148 let Some(current) = self.current else {
149 return false;
150 };
151 let target = self
153 .graph
154 .navigate(current, dir)
155 .or_else(|| spatial::spatial_navigate(&self.graph, current, dir));
156 let Some(target) = target else {
157 return false;
158 };
159 if !self.allowed_by_trap(target) {
160 return false;
161 }
162 self.set_focus(target)
163 }
164 }
165 }
166
167 pub fn focus_next(&mut self) -> bool {
169 self.move_in_tab_order(true)
170 }
171
172 pub fn focus_prev(&mut self) -> bool {
174 self.move_in_tab_order(false)
175 }
176
177 pub fn focus_first(&mut self) -> bool {
179 let order = self.active_tab_order();
180 let Some(first) = order.first().copied() else {
181 return false;
182 };
183 self.set_focus(first)
184 }
185
186 pub fn focus_last(&mut self) -> bool {
188 let order = self.active_tab_order();
189 let Some(last) = order.last().copied() else {
190 return false;
191 };
192 self.set_focus(last)
193 }
194
195 pub fn focus_back(&mut self) -> bool {
197 while let Some(id) = self.history.pop() {
198 if self.can_focus(id) && self.allowed_by_trap(id) {
199 let prev = self.current;
202 self.current = Some(id);
203 self.last_event = Some(match prev {
204 Some(from) => FocusEvent::FocusMoved { from, to: id },
205 None => FocusEvent::FocusGained { id },
206 });
207 return true;
208 }
209 }
210 false
211 }
212
213 pub fn clear_history(&mut self) {
215 self.history.clear();
216 }
217
218 pub fn push_trap(&mut self, group_id: u32) {
220 let return_focus = self.current;
221 #[cfg(feature = "tracing")]
222 tracing::debug!(
223 group_id,
224 return_focus = ?return_focus,
225 "focus.trap_push"
226 );
227 self.trap_stack.push(FocusTrap {
228 group_id,
229 return_focus,
230 });
231
232 if !self.is_current_in_group(group_id) {
233 self.focus_first_in_group(group_id);
234 }
235 }
236
237 pub fn pop_trap(&mut self) -> bool {
239 let Some(trap) = self.trap_stack.pop() else {
240 return false;
241 };
242 #[cfg(feature = "tracing")]
243 tracing::debug!(
244 group_id = trap.group_id,
245 return_focus = ?trap.return_focus,
246 "focus.trap_pop"
247 );
248
249 if let Some(id) = trap.return_focus
250 && self.can_focus(id)
251 && self.allowed_by_trap(id)
252 {
253 return self.set_focus(id);
254 }
255
256 if let Some(active) = self.active_trap_group() {
257 return self.focus_first_in_group(active);
258 }
259
260 self.focus_first()
261 }
262
263 #[must_use]
265 pub fn is_trapped(&self) -> bool {
266 !self.trap_stack.is_empty()
267 }
268
269 pub fn create_group(&mut self, id: u32, members: Vec<FocusId>) {
271 let members = self.filter_focusable(members);
272 self.groups.insert(id, FocusGroup::new(id, members));
273 }
274
275 pub fn add_to_group(&mut self, group_id: u32, widget_id: FocusId) {
277 if !self.can_focus(widget_id) {
278 return;
279 }
280 let group = self
281 .groups
282 .entry(group_id)
283 .or_insert_with(|| FocusGroup::new(group_id, Vec::new()));
284 if !group.contains(widget_id) {
285 group.members.push(widget_id);
286 }
287 }
288
289 pub fn remove_from_group(&mut self, group_id: u32, widget_id: FocusId) {
291 let Some(group) = self.groups.get_mut(&group_id) else {
292 return;
293 };
294 group.members.retain(|id| *id != widget_id);
295 }
296
297 #[must_use]
299 pub fn focus_event(&self) -> Option<&FocusEvent> {
300 self.last_event.as_ref()
301 }
302
303 #[must_use]
305 pub fn take_focus_event(&mut self) -> Option<FocusEvent> {
306 self.last_event.take()
307 }
308
309 #[inline]
311 #[must_use]
312 pub fn indicator(&self) -> &FocusIndicator {
313 &self.indicator
314 }
315
316 pub fn set_indicator(&mut self, indicator: FocusIndicator) {
318 self.indicator = indicator;
319 }
320
321 #[inline]
323 #[must_use]
324 pub fn focus_change_count(&self) -> u64 {
325 self.focus_change_count
326 }
327
328 fn set_focus(&mut self, id: FocusId) -> bool {
329 if !self.can_focus(id) || !self.allowed_by_trap(id) {
330 return false;
331 }
332 if self.current == Some(id) {
333 return false;
334 }
335
336 let prev = self.current;
337 if let Some(prev_id) = prev {
338 if Some(prev_id) != self.history.last().copied() {
339 self.history.push(prev_id);
340 }
341 let event = FocusEvent::FocusMoved {
342 from: prev_id,
343 to: id,
344 };
345 #[cfg(feature = "tracing")]
346 tracing::debug!(
347 from_widget = prev_id,
348 to_widget = id,
349 trigger = "navigate",
350 "focus.change"
351 );
352 self.last_event = Some(event);
353 } else {
354 #[cfg(feature = "tracing")]
355 tracing::debug!(to_widget = id, trigger = "initial", "focus.change");
356 self.last_event = Some(FocusEvent::FocusGained { id });
357 }
358
359 self.current = Some(id);
360 self.focus_change_count += 1;
361 true
362 }
363
364 fn can_focus(&self, id: FocusId) -> bool {
365 self.graph.get(id).map(|n| n.is_focusable).unwrap_or(false)
366 }
367
368 fn active_trap_group(&self) -> Option<u32> {
369 self.trap_stack.last().map(|t| t.group_id)
370 }
371
372 fn allowed_by_trap(&self, id: FocusId) -> bool {
373 let Some(group_id) = self.active_trap_group() else {
374 return true;
375 };
376 self.groups
377 .get(&group_id)
378 .map(|g| g.contains(id))
379 .unwrap_or(false)
380 }
381
382 fn is_current_in_group(&self, group_id: u32) -> bool {
383 let Some(current) = self.current else {
384 return false;
385 };
386 self.groups
387 .get(&group_id)
388 .map(|g| g.contains(current))
389 .unwrap_or(false)
390 }
391
392 fn active_tab_order(&self) -> Vec<FocusId> {
393 if let Some(group_id) = self.active_trap_group() {
394 return self.group_tab_order(group_id);
395 }
396 self.graph.tab_order()
397 }
398
399 fn group_tab_order(&self, group_id: u32) -> Vec<FocusId> {
400 let Some(group) = self.groups.get(&group_id) else {
401 return Vec::new();
402 };
403 let order = self.graph.tab_order();
404 order.into_iter().filter(|id| group.contains(*id)).collect()
405 }
406
407 fn focus_first_in_group(&mut self, group_id: u32) -> bool {
408 let order = self.group_tab_order(group_id);
409 let Some(first) = order.first().copied() else {
410 return false;
411 };
412 self.set_focus(first)
413 }
414
415 fn move_in_tab_order(&mut self, forward: bool) -> bool {
416 let order = self.active_tab_order();
417 if order.is_empty() {
418 return false;
419 }
420
421 let wrap = self
422 .active_trap_group()
423 .and_then(|id| self.groups.get(&id).map(|g| g.wrap))
424 .unwrap_or(true);
425
426 let next = match self.current {
427 None => order[0],
428 Some(current) => {
429 let pos = order.iter().position(|id| *id == current);
430 match pos {
431 None => order[0],
432 Some(idx) if forward => {
433 if idx + 1 < order.len() {
434 order[idx + 1]
435 } else if wrap {
436 order[0]
437 } else {
438 return false;
439 }
440 }
441 Some(idx) => {
442 if idx > 0 {
443 order[idx - 1]
444 } else if wrap {
445 *order.last().unwrap()
446 } else {
447 return false;
448 }
449 }
450 }
451 }
452 };
453
454 self.set_focus(next)
455 }
456
457 fn filter_focusable(&self, ids: Vec<FocusId>) -> Vec<FocusId> {
458 let mut out = Vec::new();
459 for id in ids {
460 if self.can_focus(id) && !out.contains(&id) {
461 out.push(id);
462 }
463 }
464 out
465 }
466}
467
468#[cfg(test)]
473mod tests {
474 use super::*;
475 use crate::focus::FocusNode;
476 use ftui_core::geometry::Rect;
477
478 fn node(id: FocusId, tab: i32) -> FocusNode {
479 FocusNode::new(id, Rect::new(0, 0, 1, 1)).with_tab_index(tab)
480 }
481
482 #[test]
483 fn focus_basic() {
484 let mut fm = FocusManager::new();
485 fm.graph_mut().insert(node(1, 0));
486 fm.graph_mut().insert(node(2, 1));
487
488 assert!(fm.focus(1).is_none());
489 assert_eq!(fm.current(), Some(1));
490
491 assert_eq!(fm.focus(2), Some(1));
492 assert_eq!(fm.current(), Some(2));
493
494 assert_eq!(fm.blur(), Some(2));
495 assert_eq!(fm.current(), None);
496 }
497
498 #[test]
499 fn focus_history_back() {
500 let mut fm = FocusManager::new();
501 fm.graph_mut().insert(node(1, 0));
502 fm.graph_mut().insert(node(2, 1));
503 fm.graph_mut().insert(node(3, 2));
504
505 fm.focus(1);
506 fm.focus(2);
507 fm.focus(3);
508
509 assert!(fm.focus_back());
510 assert_eq!(fm.current(), Some(2));
511
512 assert!(fm.focus_back());
513 assert_eq!(fm.current(), Some(1));
514 }
515
516 #[test]
517 fn focus_next_prev() {
518 let mut fm = FocusManager::new();
519 fm.graph_mut().insert(node(1, 0));
520 fm.graph_mut().insert(node(2, 1));
521 fm.graph_mut().insert(node(3, 2));
522
523 assert!(fm.focus_next());
524 assert_eq!(fm.current(), Some(1));
525
526 assert!(fm.focus_next());
527 assert_eq!(fm.current(), Some(2));
528
529 assert!(fm.focus_prev());
530 assert_eq!(fm.current(), Some(1));
531 }
532
533 #[test]
534 fn focus_trap_push_pop() {
535 let mut fm = FocusManager::new();
536 fm.graph_mut().insert(node(1, 0));
537 fm.graph_mut().insert(node(2, 1));
538 fm.graph_mut().insert(node(3, 2));
539
540 fm.focus(3);
541 fm.create_group(7, vec![1, 2]);
542
543 fm.push_trap(7);
544 assert!(fm.is_trapped());
545 assert_eq!(fm.current(), Some(1));
546
547 fm.pop_trap();
548 assert!(!fm.is_trapped());
549 assert_eq!(fm.current(), Some(3));
550 }
551
552 #[test]
553 fn focus_group_wrap_respected() {
554 let mut fm = FocusManager::new();
555 fm.graph_mut().insert(node(1, 0));
556 fm.graph_mut().insert(node(2, 1));
557 fm.create_group(9, vec![1, 2]);
558 fm.groups.get_mut(&9).unwrap().wrap = false;
559
560 fm.push_trap(9);
561 fm.focus(2);
562 assert!(!fm.focus_next());
563 assert_eq!(fm.current(), Some(2));
564 }
565
566 #[test]
567 fn focus_event_generation() {
568 let mut fm = FocusManager::new();
569 fm.graph_mut().insert(node(1, 0));
570 fm.graph_mut().insert(node(2, 1));
571
572 fm.focus(1);
573 assert_eq!(
574 fm.take_focus_event(),
575 Some(FocusEvent::FocusGained { id: 1 })
576 );
577
578 fm.focus(2);
579 assert_eq!(
580 fm.take_focus_event(),
581 Some(FocusEvent::FocusMoved { from: 1, to: 2 })
582 );
583
584 fm.blur();
585 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 2 }));
586 }
587
588 #[test]
589 fn trap_prevents_focus_outside_group() {
590 let mut fm = FocusManager::new();
591 fm.graph_mut().insert(node(1, 0));
592 fm.graph_mut().insert(node(2, 1));
593 fm.graph_mut().insert(node(3, 2));
594 fm.create_group(5, vec![1, 2]);
595
596 fm.push_trap(5);
597 assert_eq!(fm.current(), Some(1));
598
599 assert!(fm.focus(3).is_none());
601 assert_ne!(fm.current(), Some(3));
602 }
603
604 fn spatial_node(id: FocusId, x: u16, y: u16, w: u16, h: u16, tab: i32) -> FocusNode {
607 FocusNode::new(id, Rect::new(x, y, w, h)).with_tab_index(tab)
608 }
609
610 #[test]
611 fn navigate_spatial_fallback() {
612 let mut fm = FocusManager::new();
613 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
615 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
616
617 fm.focus(1);
618 assert!(fm.navigate(NavDirection::Right));
619 assert_eq!(fm.current(), Some(2));
620
621 assert!(fm.navigate(NavDirection::Left));
622 assert_eq!(fm.current(), Some(1));
623 }
624
625 #[test]
626 fn navigate_explicit_edge_overrides_spatial() {
627 let mut fm = FocusManager::new();
628 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
629 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1)); fm.graph_mut().insert(spatial_node(3, 40, 0, 10, 3, 2)); fm.graph_mut().connect(1, NavDirection::Right, 3);
634
635 fm.focus(1);
636 assert!(fm.navigate(NavDirection::Right));
637 assert_eq!(fm.current(), Some(3));
638 }
639
640 #[test]
641 fn navigate_spatial_respects_trap() {
642 let mut fm = FocusManager::new();
643 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
644 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
645 fm.graph_mut().insert(spatial_node(3, 40, 0, 10, 3, 2));
646
647 fm.create_group(1, vec![1, 2]);
649 fm.focus(2);
650 fm.push_trap(1);
651
652 assert!(!fm.navigate(NavDirection::Right));
654 assert_eq!(fm.current(), Some(2));
655 }
656
657 #[test]
658 fn navigate_spatial_grid_round_trip() {
659 let mut fm = FocusManager::new();
660 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
662 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
663 fm.graph_mut().insert(spatial_node(3, 0, 6, 10, 3, 2));
664 fm.graph_mut().insert(spatial_node(4, 20, 6, 10, 3, 3));
665
666 fm.focus(1);
667
668 assert!(fm.navigate(NavDirection::Right));
670 assert_eq!(fm.current(), Some(2));
671
672 assert!(fm.navigate(NavDirection::Down));
673 assert_eq!(fm.current(), Some(4));
674
675 assert!(fm.navigate(NavDirection::Left));
676 assert_eq!(fm.current(), Some(3));
677
678 assert!(fm.navigate(NavDirection::Up));
679 assert_eq!(fm.current(), Some(1));
680 }
681
682 #[test]
683 fn navigate_spatial_no_candidate() {
684 let mut fm = FocusManager::new();
685 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
686 fm.focus(1);
687
688 assert!(!fm.navigate(NavDirection::Right));
690 assert!(!fm.navigate(NavDirection::Up));
691 assert_eq!(fm.current(), Some(1));
692 }
693
694 #[test]
697 fn new_manager_has_no_focus() {
698 let fm = FocusManager::new();
699 assert_eq!(fm.current(), None);
700 assert!(!fm.is_trapped());
701 }
702
703 #[test]
704 fn default_and_new_are_equivalent() {
705 let a = FocusManager::new();
706 let b = FocusManager::default();
707 assert_eq!(a.current(), b.current());
708 assert_eq!(a.is_trapped(), b.is_trapped());
709 }
710
711 #[test]
714 fn is_focused_returns_true_for_current() {
715 let mut fm = FocusManager::new();
716 fm.graph_mut().insert(node(1, 0));
717 fm.focus(1);
718 assert!(fm.is_focused(1));
719 assert!(!fm.is_focused(2));
720 }
721
722 #[test]
723 fn is_focused_returns_false_when_no_focus() {
724 let fm = FocusManager::new();
725 assert!(!fm.is_focused(1));
726 }
727
728 #[test]
731 fn focus_non_existent_node_returns_none() {
732 let mut fm = FocusManager::new();
733 assert!(fm.focus(999).is_none());
734 assert_eq!(fm.current(), None);
735 }
736
737 #[test]
738 fn focus_already_focused_returns_same_id() {
739 let mut fm = FocusManager::new();
740 fm.graph_mut().insert(node(1, 0));
741 fm.focus(1);
742 assert_eq!(fm.focus(1), Some(1));
744 assert_eq!(fm.current(), Some(1));
745 }
746
747 #[test]
750 fn blur_when_no_focus_returns_none() {
751 let mut fm = FocusManager::new();
752 assert_eq!(fm.blur(), None);
753 }
754
755 #[test]
756 fn blur_generates_focus_lost_event() {
757 let mut fm = FocusManager::new();
758 fm.graph_mut().insert(node(1, 0));
759 fm.focus(1);
760 let _ = fm.take_focus_event(); fm.blur();
762 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
763 }
764
765 #[test]
768 fn focus_first_selects_lowest_tab_index() {
769 let mut fm = FocusManager::new();
770 fm.graph_mut().insert(node(3, 2));
771 fm.graph_mut().insert(node(1, 0));
772 fm.graph_mut().insert(node(2, 1));
773
774 assert!(fm.focus_first());
775 assert_eq!(fm.current(), Some(1));
776 }
777
778 #[test]
779 fn focus_last_selects_highest_tab_index() {
780 let mut fm = FocusManager::new();
781 fm.graph_mut().insert(node(1, 0));
782 fm.graph_mut().insert(node(2, 1));
783 fm.graph_mut().insert(node(3, 2));
784
785 assert!(fm.focus_last());
786 assert_eq!(fm.current(), Some(3));
787 }
788
789 #[test]
790 fn focus_first_on_empty_graph_returns_false() {
791 let mut fm = FocusManager::new();
792 assert!(!fm.focus_first());
793 }
794
795 #[test]
796 fn focus_last_on_empty_graph_returns_false() {
797 let mut fm = FocusManager::new();
798 assert!(!fm.focus_last());
799 }
800
801 #[test]
804 fn focus_next_wraps_at_end() {
805 let mut fm = FocusManager::new();
806 fm.graph_mut().insert(node(1, 0));
807 fm.graph_mut().insert(node(2, 1));
808
809 fm.focus(2);
810 assert!(fm.focus_next()); assert_eq!(fm.current(), Some(1));
812 }
813
814 #[test]
815 fn focus_prev_wraps_at_start() {
816 let mut fm = FocusManager::new();
817 fm.graph_mut().insert(node(1, 0));
818 fm.graph_mut().insert(node(2, 1));
819
820 fm.focus(1);
821 assert!(fm.focus_prev()); assert_eq!(fm.current(), Some(2));
823 }
824
825 #[test]
826 fn focus_next_with_no_current_selects_first() {
827 let mut fm = FocusManager::new();
828 fm.graph_mut().insert(node(1, 0));
829 fm.graph_mut().insert(node(2, 1));
830
831 assert!(fm.focus_next());
832 assert_eq!(fm.current(), Some(1));
833 }
834
835 #[test]
836 fn focus_next_on_empty_returns_false() {
837 let mut fm = FocusManager::new();
838 assert!(!fm.focus_next());
839 }
840
841 #[test]
844 fn focus_back_on_empty_history_returns_false() {
845 let mut fm = FocusManager::new();
846 fm.graph_mut().insert(node(1, 0));
847 fm.focus(1);
848 assert!(!fm.focus_back());
849 }
850
851 #[test]
852 fn clear_history_prevents_back() {
853 let mut fm = FocusManager::new();
854 fm.graph_mut().insert(node(1, 0));
855 fm.graph_mut().insert(node(2, 1));
856
857 fm.focus(1);
858 fm.focus(2);
859 fm.clear_history();
860 assert!(!fm.focus_back());
861 assert_eq!(fm.current(), Some(2));
862 }
863
864 #[test]
865 fn focus_back_skips_removed_nodes() {
866 let mut fm = FocusManager::new();
867 fm.graph_mut().insert(node(1, 0));
868 fm.graph_mut().insert(node(2, 1));
869 fm.graph_mut().insert(node(3, 2));
870
871 fm.focus(1);
872 fm.focus(2);
873 fm.focus(3);
874
875 let _ = fm.graph_mut().remove(2);
877
878 assert!(fm.focus_back());
880 assert_eq!(fm.current(), Some(1));
881 }
882
883 #[test]
886 fn create_group_filters_non_focusable() {
887 let mut fm = FocusManager::new();
888 fm.graph_mut().insert(node(1, 0));
889 fm.create_group(1, vec![1, 999]);
891
892 let group = fm.groups.get(&1).unwrap();
893 assert_eq!(group.members.len(), 1);
894 assert!(group.contains(1));
895 }
896
897 #[test]
898 fn add_to_group_creates_group_if_needed() {
899 let mut fm = FocusManager::new();
900 fm.graph_mut().insert(node(1, 0));
901 fm.add_to_group(42, 1);
902 assert!(fm.groups.contains_key(&42));
903 assert!(fm.groups.get(&42).unwrap().contains(1));
904 }
905
906 #[test]
907 fn add_to_group_skips_unfocusable() {
908 let mut fm = FocusManager::new();
909 fm.add_to_group(1, 999); if let Some(group) = fm.groups.get(&1) {
912 assert!(!group.contains(999));
913 }
914 }
915
916 #[test]
917 fn add_to_group_no_duplicates() {
918 let mut fm = FocusManager::new();
919 fm.graph_mut().insert(node(1, 0));
920 fm.add_to_group(1, 1);
921 fm.add_to_group(1, 1);
922 assert_eq!(fm.groups.get(&1).unwrap().members.len(), 1);
923 }
924
925 #[test]
926 fn remove_from_group() {
927 let mut fm = FocusManager::new();
928 fm.graph_mut().insert(node(1, 0));
929 fm.graph_mut().insert(node(2, 1));
930 fm.create_group(1, vec![1, 2]);
931 fm.remove_from_group(1, 1);
932 assert!(!fm.groups.get(&1).unwrap().contains(1));
933 assert!(fm.groups.get(&1).unwrap().contains(2));
934 }
935
936 #[test]
937 fn remove_from_nonexistent_group_is_noop() {
938 let mut fm = FocusManager::new();
939 fm.remove_from_group(999, 1); }
941
942 #[test]
945 fn focus_group_with_wrap() {
946 let group = FocusGroup::new(1, vec![1, 2]).with_wrap(false);
947 assert!(!group.wrap);
948 }
949
950 #[test]
951 fn focus_group_with_exit_key() {
952 let group = FocusGroup::new(1, vec![]).with_exit_key(KeyCode::Escape);
953 assert_eq!(group.exit_key, Some(KeyCode::Escape));
954 }
955
956 #[test]
957 fn focus_group_default_wraps() {
958 let group = FocusGroup::new(1, vec![]);
959 assert!(group.wrap);
960 assert_eq!(group.exit_key, None);
961 }
962
963 #[test]
966 fn nested_traps() {
967 let mut fm = FocusManager::new();
968 fm.graph_mut().insert(node(1, 0));
969 fm.graph_mut().insert(node(2, 1));
970 fm.graph_mut().insert(node(3, 2));
971 fm.graph_mut().insert(node(4, 3));
972
973 fm.create_group(10, vec![1, 2]);
974 fm.create_group(20, vec![3, 4]);
975
976 fm.focus(1);
977 fm.push_trap(10);
978 assert!(fm.is_trapped());
979
980 fm.push_trap(20);
981 assert_eq!(fm.current(), Some(3));
983
984 fm.pop_trap();
986 assert!(fm.is_trapped());
988
989 fm.pop_trap();
991 assert!(!fm.is_trapped());
992 }
993
994 #[test]
995 fn pop_trap_on_empty_returns_false() {
996 let mut fm = FocusManager::new();
997 assert!(!fm.pop_trap());
998 }
999
1000 #[test]
1003 fn take_focus_event_clears_it() {
1004 let mut fm = FocusManager::new();
1005 fm.graph_mut().insert(node(1, 0));
1006 fm.focus(1);
1007
1008 assert!(fm.take_focus_event().is_some());
1009 assert!(fm.take_focus_event().is_none());
1010 }
1011
1012 #[test]
1013 fn focus_event_accessor() {
1014 let mut fm = FocusManager::new();
1015 fm.graph_mut().insert(node(1, 0));
1016 fm.focus(1);
1017
1018 assert_eq!(fm.focus_event(), Some(&FocusEvent::FocusGained { id: 1 }));
1019 }
1020
1021 #[test]
1024 fn navigate_direction_with_no_current_returns_false() {
1025 let mut fm = FocusManager::new();
1026 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1027 assert!(!fm.navigate(NavDirection::Right));
1028 }
1029
1030 #[test]
1033 fn graph_accessor_returns_reference() {
1034 let mut fm = FocusManager::new();
1035 fm.graph_mut().insert(node(1, 0));
1036 assert!(fm.graph().get(1).is_some());
1037 }
1038
1039 #[test]
1042 fn default_indicator_is_reverse() {
1043 let fm = FocusManager::new();
1044 assert!(fm.indicator().is_visible());
1045 assert_eq!(
1046 fm.indicator().kind(),
1047 crate::focus::FocusIndicatorKind::StyleOverlay
1048 );
1049 }
1050
1051 #[test]
1052 fn set_indicator() {
1053 let mut fm = FocusManager::new();
1054 fm.set_indicator(crate::focus::FocusIndicator::underline());
1055 assert_eq!(
1056 fm.indicator().kind(),
1057 crate::focus::FocusIndicatorKind::Underline
1058 );
1059 }
1060
1061 #[test]
1064 fn focus_change_count_increments() {
1065 let mut fm = FocusManager::new();
1066 fm.graph_mut().insert(node(1, 0));
1067 fm.graph_mut().insert(node(2, 1));
1068
1069 assert_eq!(fm.focus_change_count(), 0);
1070
1071 fm.focus(1);
1072 assert_eq!(fm.focus_change_count(), 1);
1073
1074 fm.focus(2);
1075 assert_eq!(fm.focus_change_count(), 2);
1076
1077 fm.blur();
1078 assert_eq!(fm.focus_change_count(), 3);
1079 }
1080
1081 #[test]
1082 fn focus_change_count_zero_on_no_op() {
1083 let mut fm = FocusManager::new();
1084 fm.graph_mut().insert(node(1, 0));
1085 fm.focus(1);
1086 assert_eq!(fm.focus_change_count(), 1);
1087
1088 fm.focus(1);
1090 assert_eq!(fm.focus_change_count(), 1);
1091 }
1092}