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)]
74pub struct FocusManager {
75 graph: FocusGraph,
76 current: Option<FocusId>,
77 host_focused: bool,
78 pending_focus_on_host_gain: Option<FocusId>,
79 history: Vec<FocusId>,
80 trap_stack: Vec<FocusTrap>,
81 groups: AHashMap<u32, FocusGroup>,
82 last_event: Option<FocusEvent>,
83 indicator: FocusIndicator,
84 focus_change_count: u64,
86}
87
88impl Default for FocusManager {
89 fn default() -> Self {
90 Self {
91 graph: FocusGraph::default(),
92 current: None,
93 host_focused: true,
94 pending_focus_on_host_gain: None,
95 history: Vec::new(),
96 trap_stack: Vec::new(),
97 groups: AHashMap::new(),
98 last_event: None,
99 indicator: FocusIndicator::default(),
100 focus_change_count: 0,
101 }
102 }
103}
104
105impl FocusManager {
106 #[must_use]
108 pub fn new() -> Self {
109 Self::default()
110 }
111
112 #[must_use]
114 pub fn graph(&self) -> &FocusGraph {
115 &self.graph
116 }
117
118 pub fn graph_mut(&mut self) -> &mut FocusGraph {
120 &mut self.graph
121 }
122
123 #[inline]
125 #[must_use]
126 pub fn current(&self) -> Option<FocusId> {
127 self.current
128 }
129
130 #[must_use]
131 pub(crate) fn host_focused(&self) -> bool {
132 self.host_focused
133 }
134
135 pub(crate) fn set_host_focused(&mut self, focused: bool) {
136 self.host_focused = focused;
137 if focused {
138 self.pending_focus_on_host_gain = None;
139 }
140 }
141
142 #[must_use]
144 pub fn is_focused(&self, id: FocusId) -> bool {
145 self.current == Some(id)
146 }
147
148 pub fn focus(&mut self, id: FocusId) -> Option<FocusId> {
150 if !self.can_focus(id) || !self.allowed_by_trap(id) {
151 return None;
152 }
153 let prev = self.active_focus_target();
154 if prev == Some(id) {
155 return prev;
156 }
157 self.set_focus(id);
158 prev
159 }
160
161 pub fn blur(&mut self) -> Option<FocusId> {
163 let prev = self.current.take();
164 if let Some(id) = prev {
165 #[cfg(feature = "tracing")]
166 tracing::debug!(from_widget = id, trigger = "blur", "focus.change");
167 self.last_event = Some(FocusEvent::FocusLost { id });
168 self.focus_change_count += 1;
169 }
170 prev
171 }
172
173 pub fn apply_host_focus(&mut self, focused: bool) -> bool {
183 if !focused {
184 if let Some(current) = self.current {
185 self.pending_focus_on_host_gain = Some(current);
186 }
187 self.host_focused = false;
188 return self.blur().is_some();
189 }
190
191 self.host_focused = true;
192 let had_current = self.current.is_some();
193 if let Some(current) = self.current
194 && self.can_focus(current)
195 && self.allowed_by_trap(current)
196 {
197 self.pending_focus_on_host_gain = None;
198 return false;
199 }
200
201 let pending_focus = self.pending_focus_on_host_gain.take();
202 if let Some(id) = pending_focus
203 && self.can_focus(id)
204 && self.allowed_by_trap(id)
205 {
206 return self.set_focus_without_history(id);
207 }
208
209 if let Some(group_id) = self.active_trap_group()
210 && self.focus_first_in_group_without_history(group_id)
211 {
212 return true;
213 }
214
215 if self.focus_first_without_history() {
216 return true;
217 }
218
219 if had_current {
220 return self.blur().is_some();
221 }
222
223 false
224 }
225
226 pub fn navigate(&mut self, dir: NavDirection) -> bool {
228 match dir {
229 NavDirection::Next => self.focus_next(),
230 NavDirection::Prev => self.focus_prev(),
231 _ => {
232 let Some(current) = self.active_focus_target() else {
233 return false;
234 };
235 let target = self
237 .graph
238 .navigate(current, dir)
239 .or_else(|| spatial::spatial_navigate(&self.graph, current, dir));
240 let Some(target) = target else {
241 return false;
242 };
243 if !self.allowed_by_trap(target) {
244 return false;
245 }
246 self.set_focus(target)
247 }
248 }
249 }
250
251 pub fn focus_next(&mut self) -> bool {
253 self.move_in_tab_order(true)
254 }
255
256 pub fn focus_prev(&mut self) -> bool {
258 self.move_in_tab_order(false)
259 }
260
261 pub fn focus_first(&mut self) -> bool {
263 let order = self.active_tab_order();
264 let Some(first) = order.first().copied() else {
265 return false;
266 };
267 self.set_focus(first)
268 }
269
270 pub fn focus_last(&mut self) -> bool {
272 let order = self.active_tab_order();
273 let Some(last) = order.last().copied() else {
274 return false;
275 };
276 self.set_focus(last)
277 }
278
279 pub fn focus_back(&mut self) -> bool {
281 let active_focus = self.active_focus_target();
282 while let Some(id) = self.history.pop() {
283 if active_focus == Some(id) {
284 continue;
285 }
286 if self.can_focus(id) && self.allowed_by_trap(id) {
287 if !self.host_focused {
288 return self.set_pending_focus_target(id);
289 }
290 let prev = self.current;
293 self.current = Some(id);
294 self.last_event = Some(match prev {
295 Some(from) => FocusEvent::FocusMoved { from, to: id },
296 None => FocusEvent::FocusGained { id },
297 });
298 self.focus_change_count += 1;
299 return true;
300 }
301 }
302 false
303 }
304
305 pub fn clear_history(&mut self) {
307 self.history.clear();
308 }
309
310 pub fn push_trap(&mut self, group_id: u32) -> bool {
317 let return_focus = if self.host_focused {
318 self.current
319 } else {
320 self.current.or(self.deferred_focus_target())
321 };
322 if !self.push_trap_with_return_focus(group_id, return_focus) {
323 #[cfg(feature = "tracing")]
324 tracing::warn!(group_id, "focus.trap_push rejected: group missing or empty");
325 return false;
326 }
327
328 if self.host_focused && !self.is_current_focusable_in_group(group_id) {
329 self.focus_first_in_group_without_history(group_id);
330 } else if !self.host_focused {
331 self.pending_focus_on_host_gain = self.group_primary_focus_target(group_id);
332 }
333 true
334 }
335
336 pub fn pop_trap(&mut self) -> bool {
338 let Some(trap) = self.trap_stack.pop() else {
339 return false;
340 };
341 let had_current = self.current.is_some();
342 #[cfg(feature = "tracing")]
343 tracing::debug!(
344 group_id = trap.group_id,
345 return_focus = ?trap.return_focus,
346 "focus.trap_pop"
347 );
348
349 if !self.host_focused {
350 self.pending_focus_on_host_gain = trap
351 .return_focus
352 .filter(|id| self.can_focus(*id) && self.allowed_by_trap(*id))
353 .or_else(|| {
354 self.active_trap_group()
355 .and_then(|group_id| self.group_primary_focus_target(group_id))
356 });
357 return if had_current {
358 self.blur().is_some()
359 } else {
360 false
361 };
362 }
363
364 if let Some(id) = trap.return_focus
365 && self.can_focus(id)
366 && self.allowed_by_trap(id)
367 {
368 return self.set_focus_without_history(id);
369 }
370
371 if let Some(active) = self.active_trap_group() {
372 return self.focus_first_in_group_without_history(active);
373 }
374
375 if trap.return_focus.is_none() {
376 return if had_current {
377 self.blur().is_some()
378 } else {
379 false
380 };
381 }
382
383 if self.focus_first_without_history() {
384 return true;
385 }
386
387 if had_current && self.current.is_some_and(|id| !self.can_focus(id)) {
388 return self.blur().is_some();
389 }
390
391 false
392 }
393
394 #[must_use]
396 pub fn is_trapped(&self) -> bool {
397 self.active_trap_group().is_some()
398 }
399
400 pub fn clear_traps(&mut self) {
402 self.trap_stack.clear();
403 }
404
405 pub fn create_group(&mut self, id: u32, members: Vec<FocusId>) {
407 let members = self.filter_focusable(members);
408 self.groups.insert(id, FocusGroup::new(id, members));
409 self.repair_focus_after_group_change();
410 }
411
412 pub(crate) fn create_group_preserving_members(&mut self, id: u32, members: Vec<FocusId>) {
413 let members = self.dedup_members(members);
414 self.groups.insert(id, FocusGroup::new(id, members));
415 self.repair_focus_after_group_change();
416 }
417
418 pub fn add_to_group(&mut self, group_id: u32, widget_id: FocusId) {
420 if !self.can_focus(widget_id) {
421 return;
422 }
423 let group = self
424 .groups
425 .entry(group_id)
426 .or_insert_with(|| FocusGroup::new(group_id, Vec::new()));
427 if !group.contains(widget_id) {
428 group.members.push(widget_id);
429 }
430 self.repair_focus_after_group_change();
431 }
432
433 pub fn remove_from_group(&mut self, group_id: u32, widget_id: FocusId) {
435 let Some(group) = self.groups.get_mut(&group_id) else {
436 return;
437 };
438 group.members.retain(|id| *id != widget_id);
439 self.repair_focus_after_group_change();
440 }
441
442 pub fn remove_group(&mut self, group_id: u32) {
444 if self.groups.remove(&group_id).is_none() {
445 return;
446 }
447 self.trap_stack.retain(|trap| trap.group_id != group_id);
448 self.repair_focus_after_group_change();
449 }
450
451 #[must_use]
453 pub fn focus_event(&self) -> Option<&FocusEvent> {
454 self.last_event.as_ref()
455 }
456
457 #[must_use]
459 pub fn take_focus_event(&mut self) -> Option<FocusEvent> {
460 self.last_event.take()
461 }
462
463 #[inline]
465 #[must_use]
466 pub fn indicator(&self) -> &FocusIndicator {
467 &self.indicator
468 }
469
470 pub fn set_indicator(&mut self, indicator: FocusIndicator) {
472 self.indicator = indicator;
473 }
474
475 #[inline]
477 #[must_use]
478 pub fn focus_change_count(&self) -> u64 {
479 self.focus_change_count
480 }
481
482 #[cfg(test)]
483 #[must_use]
484 pub(crate) fn group_count(&self) -> usize {
485 self.groups.len()
486 }
487
488 #[must_use]
489 pub(crate) fn has_group(&self, group_id: u32) -> bool {
490 self.groups.contains_key(&group_id)
491 }
492
493 #[must_use]
494 pub(crate) fn group_members(&self, group_id: u32) -> Vec<FocusId> {
495 self.groups
496 .get(&group_id)
497 .map(|group| group.members.clone())
498 .unwrap_or_default()
499 }
500
501 #[cfg(test)]
502 #[must_use]
503 pub(crate) fn base_trap_return_focus(&self) -> Option<Option<FocusId>> {
504 self.trap_stack.first().map(|trap| trap.return_focus)
505 }
506
507 #[must_use]
508 pub(crate) fn deferred_focus_target(&self) -> Option<FocusId> {
509 if let Some(id) = self.active_focus_target() {
510 return Some(id);
511 }
512
513 self.active_trap_group()
514 .and_then(|group_id| self.group_primary_focus_target(group_id))
515 }
516
517 #[must_use]
518 pub(crate) fn logical_focus_target(&self) -> Option<FocusId> {
519 self.active_focus_target()
520 }
521
522 pub(crate) fn focus_without_history(&mut self, id: FocusId) -> bool {
523 self.set_focus_without_history(id)
524 }
525
526 pub(crate) fn focus_first_without_history_for_restore(&mut self) -> bool {
527 self.focus_first_without_history()
528 }
529
530 pub(crate) fn replace_deferred_focus_target(&mut self, target: Option<FocusId>) {
531 self.current = None;
532 self.pending_focus_on_host_gain =
533 target.filter(|id| self.can_focus(*id) && self.allowed_by_trap(*id));
534 }
535
536 pub(crate) fn remove_group_without_repair(&mut self, group_id: u32) -> bool {
537 if self.groups.remove(&group_id).is_none() {
538 return false;
539 }
540 self.trap_stack.retain(|trap| trap.group_id != group_id);
541 true
542 }
543
544 pub(crate) fn push_trap_with_return_focus(
545 &mut self,
546 group_id: u32,
547 return_focus: Option<FocusId>,
548 ) -> bool {
549 if !self.group_has_focusable_member(group_id) {
550 return false;
551 }
552
553 #[cfg(feature = "tracing")]
554 tracing::debug!(
555 group_id,
556 return_focus = ?return_focus,
557 "focus.trap_push"
558 );
559 self.trap_stack.push(FocusTrap {
560 group_id,
561 return_focus,
562 });
563 true
564 }
565
566 pub(crate) fn repair_focus_after_excluding_ids(&mut self, excluded: &[FocusId]) {
567 if !self.host_focused {
568 if self.current.is_some_and(|id| excluded.contains(&id)) {
569 let _ = self.blur();
570 }
571 return;
572 }
573
574 if self.is_trapped() || !self.current.is_some_and(|id| excluded.contains(&id)) {
575 return;
576 }
577
578 for id in self.graph.tab_order() {
579 if excluded.contains(&id) {
580 continue;
581 }
582 if self.set_focus_without_history(id) {
583 return;
584 }
585 }
586
587 let _ = self.blur();
588 }
589
590 pub(crate) fn clear_deferred_focus_if_excluded(&mut self, excluded: &[FocusId]) {
591 if self
592 .pending_focus_on_host_gain
593 .is_some_and(|id| excluded.contains(&id))
594 {
595 self.pending_focus_on_host_gain = None;
596 }
597 }
598
599 pub(crate) fn restore_focus_after_invalid_current(&mut self) {
600 if !self.host_focused {
601 return;
602 }
603
604 if let Some(group_id) = self.active_trap_group()
605 && self.focus_first_in_group_without_history(group_id)
606 {
607 return;
608 }
609
610 let _ = self.focus_first_without_history();
611 }
612
613 fn set_focus(&mut self, id: FocusId) -> bool {
614 self.set_focus_target(id, true)
615 }
616
617 fn set_focus_without_history(&mut self, id: FocusId) -> bool {
618 self.set_focus_target(id, false)
619 }
620
621 fn set_focus_target(&mut self, id: FocusId, record_history: bool) -> bool {
622 if !self.host_focused {
623 return self.set_pending_focus_target(id);
624 }
625 self.set_focus_internal(id, record_history)
626 }
627
628 fn set_focus_internal(&mut self, id: FocusId, record_history: bool) -> bool {
629 if !self.can_focus(id) || !self.allowed_by_trap(id) {
630 return false;
631 }
632 if self.current == Some(id) {
633 return false;
634 }
635
636 let prev = self.current;
637 if let Some(prev_id) = prev {
638 if record_history && Some(prev_id) != self.history.last().copied() {
639 self.history.push(prev_id);
640 }
641 let event = FocusEvent::FocusMoved {
642 from: prev_id,
643 to: id,
644 };
645 #[cfg(feature = "tracing")]
646 tracing::debug!(
647 from_widget = prev_id,
648 to_widget = id,
649 trigger = "navigate",
650 "focus.change"
651 );
652 self.last_event = Some(event);
653 } else {
654 #[cfg(feature = "tracing")]
655 tracing::debug!(to_widget = id, trigger = "initial", "focus.change");
656 self.last_event = Some(FocusEvent::FocusGained { id });
657 }
658
659 self.current = Some(id);
660 self.focus_change_count += 1;
661 true
662 }
663
664 fn can_focus(&self, id: FocusId) -> bool {
665 self.graph.get(id).map(|n| n.is_focusable).unwrap_or(false)
666 }
667
668 fn active_focus_target(&self) -> Option<FocusId> {
669 if let Some(current) = self.current
670 && self.can_focus(current)
671 && self.allowed_by_trap(current)
672 {
673 return Some(current);
674 }
675
676 if self.host_focused {
677 return None;
678 }
679
680 self.pending_focus_on_host_gain
681 .filter(|id| self.can_focus(*id) && self.allowed_by_trap(*id))
682 }
683
684 fn set_pending_focus_target(&mut self, id: FocusId) -> bool {
685 if !self.can_focus(id) || !self.allowed_by_trap(id) {
686 return false;
687 }
688
689 let prev = self.active_focus_target();
690 self.current = None;
691 if prev == Some(id) {
692 return false;
693 }
694
695 self.pending_focus_on_host_gain = Some(id);
696 true
697 }
698
699 fn active_trap_group(&self) -> Option<u32> {
700 self.trap_stack
701 .iter()
702 .rev()
703 .find(|trap| self.group_has_focusable_member(trap.group_id))
704 .map(|trap| trap.group_id)
705 }
706
707 fn allowed_by_trap(&self, id: FocusId) -> bool {
708 let Some(group_id) = self.active_trap_group() else {
709 return true;
710 };
711 self.groups
712 .get(&group_id)
713 .map(|g| g.contains(id))
714 .unwrap_or(false)
715 }
716
717 fn group_has_focusable_member(&self, group_id: u32) -> bool {
718 self.groups
719 .get(&group_id)
720 .is_some_and(|group| group.members.iter().any(|id| self.can_focus(*id)))
721 }
722
723 fn repair_focus_after_group_change(&mut self) {
724 if !self.host_focused {
725 if self.current.is_some() {
726 let _ = self.blur();
727 }
728 return;
729 }
730
731 match self.active_trap_group() {
732 Some(group_id) => {
733 let current_allowed = self
734 .current
735 .is_some_and(|id| self.can_focus(id) && self.allowed_by_trap(id));
736 if !current_allowed {
737 let _ = self.focus_first_in_group_without_history(group_id);
738 }
739 }
740 None => {
741 if self.current.is_some_and(|id| !self.can_focus(id))
742 && !self.focus_first_without_history()
743 {
744 let _ = self.blur();
745 }
746 }
747 }
748 }
749
750 fn is_current_focusable_in_group(&self, group_id: u32) -> bool {
751 let Some(current) = self.current else {
752 return false;
753 };
754 self.can_focus(current)
755 && self
756 .groups
757 .get(&group_id)
758 .map(|g| g.contains(current))
759 .unwrap_or(false)
760 }
761
762 fn active_tab_order(&self) -> Vec<FocusId> {
763 if let Some(group_id) = self.active_trap_group() {
764 return self.group_tab_order(group_id);
765 }
766 self.graph.tab_order()
767 }
768
769 fn group_tab_order(&self, group_id: u32) -> Vec<FocusId> {
770 let Some(group) = self.groups.get(&group_id) else {
771 return Vec::new();
772 };
773 let order = self.graph.tab_order();
774 order.into_iter().filter(|id| group.contains(*id)).collect()
775 }
776
777 pub(crate) fn group_primary_focus_target(&self, group_id: u32) -> Option<FocusId> {
778 self.group_tab_order(group_id).first().copied().or_else(|| {
779 self.groups
780 .get(&group_id)
781 .and_then(|group| group.members.iter().copied().find(|id| self.can_focus(*id)))
782 })
783 }
784
785 fn focus_first_in_group_without_history(&mut self, group_id: u32) -> bool {
786 let Some(first) = self.group_primary_focus_target(group_id) else {
787 return false;
788 };
789 self.set_focus_without_history(first)
790 }
791
792 fn focus_first_without_history(&mut self) -> bool {
793 let order = self.active_tab_order();
794 let Some(first) = order.first().copied() else {
795 return false;
796 };
797 self.set_focus_without_history(first)
798 }
799
800 fn move_in_tab_order(&mut self, forward: bool) -> bool {
801 let order = self.active_tab_order();
802 if order.is_empty() {
803 return false;
804 }
805 let first = order[0];
806 let last = order[order.len() - 1];
807 let fallback = if forward { first } else { last };
808
809 let wrap = self
810 .active_trap_group()
811 .and_then(|id| self.groups.get(&id).map(|g| g.wrap))
812 .unwrap_or(true);
813
814 let next = match self.active_focus_target() {
815 None => fallback,
816 Some(current) => {
817 let pos = order.iter().position(|id| *id == current);
818 match pos {
819 None => fallback,
820 Some(idx) if forward => {
821 if idx + 1 < order.len() {
822 order[idx + 1]
823 } else if wrap {
824 order[0]
825 } else {
826 return false;
827 }
828 }
829 Some(idx) => {
830 if idx > 0 {
831 order[idx - 1]
832 } else if wrap {
833 last
834 } else {
835 return false;
836 }
837 }
838 }
839 }
840 };
841
842 self.set_focus(next)
843 }
844
845 fn dedup_members(&self, ids: Vec<FocusId>) -> Vec<FocusId> {
846 let mut out = Vec::new();
847 for id in ids {
848 if !out.contains(&id) {
849 out.push(id);
850 }
851 }
852 out
853 }
854
855 fn filter_focusable(&self, ids: Vec<FocusId>) -> Vec<FocusId> {
856 self.dedup_members(ids)
857 .into_iter()
858 .filter(|id| self.can_focus(*id))
859 .collect()
860 }
861}
862
863#[cfg(test)]
868mod tests {
869 use super::*;
870 use crate::focus::FocusNode;
871 use ftui_core::geometry::Rect;
872
873 fn node(id: FocusId, tab: i32) -> FocusNode {
874 FocusNode::new(id, Rect::new(0, 0, 1, 1)).with_tab_index(tab)
875 }
876
877 #[test]
878 fn focus_basic() {
879 let mut fm = FocusManager::new();
880 fm.graph_mut().insert(node(1, 0));
881 fm.graph_mut().insert(node(2, 1));
882
883 assert!(fm.focus(1).is_none());
884 assert_eq!(fm.current(), Some(1));
885
886 assert_eq!(fm.focus(2), Some(1));
887 assert_eq!(fm.current(), Some(2));
888
889 assert_eq!(fm.blur(), Some(2));
890 assert_eq!(fm.current(), None);
891 }
892
893 #[test]
894 fn focus_history_back() {
895 let mut fm = FocusManager::new();
896 fm.graph_mut().insert(node(1, 0));
897 fm.graph_mut().insert(node(2, 1));
898 fm.graph_mut().insert(node(3, 2));
899
900 fm.focus(1);
901 fm.focus(2);
902 fm.focus(3);
903
904 assert!(fm.focus_back());
905 assert_eq!(fm.current(), Some(2));
906
907 assert!(fm.focus_back());
908 assert_eq!(fm.current(), Some(1));
909 }
910
911 #[test]
912 fn focus_back_skips_current_id_in_history() {
913 let mut fm = FocusManager::new();
914 fm.graph_mut().insert(node(1, 0));
915 fm.graph_mut().insert(node(2, 1));
916
917 fm.focus(1);
918 fm.focus(2);
919 assert_eq!(fm.current(), Some(2));
920
921 assert_eq!(fm.blur(), Some(2));
922 assert_eq!(fm.current(), None);
923
924 fm.focus(1);
925 assert_eq!(fm.current(), Some(1));
926 let _ = fm.take_focus_event();
927 let before = fm.focus_change_count();
928
929 assert!(!fm.focus_back());
930 assert_eq!(fm.current(), Some(1));
931 assert!(fm.take_focus_event().is_none());
932 assert_eq!(fm.focus_change_count(), before);
933 }
934
935 #[test]
936 fn focus_next_prev() {
937 let mut fm = FocusManager::new();
938 fm.graph_mut().insert(node(1, 0));
939 fm.graph_mut().insert(node(2, 1));
940 fm.graph_mut().insert(node(3, 2));
941
942 assert!(fm.focus_next());
943 assert_eq!(fm.current(), Some(1));
944
945 assert!(fm.focus_next());
946 assert_eq!(fm.current(), Some(2));
947
948 assert!(fm.focus_prev());
949 assert_eq!(fm.current(), Some(1));
950 }
951
952 #[test]
953 fn apply_host_focus_loss_blurs_current() {
954 let mut fm = FocusManager::new();
955 fm.graph_mut().insert(node(1, 0));
956 fm.focus(1);
957 let _ = fm.take_focus_event();
958
959 assert!(fm.apply_host_focus(false));
960 assert_eq!(fm.current(), None);
961 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
962 }
963
964 #[test]
965 fn apply_host_focus_gain_focuses_first_when_unfocused() {
966 let mut fm = FocusManager::new();
967 fm.graph_mut().insert(node(10, 1));
968 fm.graph_mut().insert(node(5, 0));
969
970 assert!(fm.apply_host_focus(true));
971 assert_eq!(fm.current(), Some(5));
972 assert_eq!(
973 fm.take_focus_event(),
974 Some(FocusEvent::FocusGained { id: 5 })
975 );
976 }
977
978 #[test]
979 fn apply_host_focus_gain_preserves_valid_current() {
980 let mut fm = FocusManager::new();
981 fm.graph_mut().insert(node(1, 0));
982 fm.graph_mut().insert(node(2, 1));
983 fm.focus(2);
984 let _ = fm.take_focus_event();
985
986 assert!(!fm.apply_host_focus(true));
987 assert_eq!(fm.current(), Some(2));
988 assert!(fm.take_focus_event().is_none());
989 }
990
991 #[test]
992 fn apply_host_focus_gain_clears_invalid_current_when_restore_fails() {
993 let mut fm = FocusManager::new();
994 fm.graph_mut().insert(node(1, 0));
995 fm.focus(1);
996 let _ = fm.take_focus_event();
997 let _ = fm.graph_mut().remove(1);
998
999 assert!(fm.apply_host_focus(true));
1000 assert_eq!(fm.current(), None);
1001 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1002 }
1003
1004 #[test]
1005 fn apply_host_focus_gain_respects_trap_order() {
1006 let mut fm = FocusManager::new();
1007 fm.graph_mut().insert(node(1, 0));
1008 fm.graph_mut().insert(node(2, 1));
1009 fm.graph_mut().insert(node(3, 2));
1010 fm.create_group(42, vec![2, 3]);
1011 fm.push_trap(42);
1012 let _ = fm.take_focus_event();
1013 fm.blur();
1014 let _ = fm.take_focus_event();
1015
1016 assert!(fm.apply_host_focus(true));
1017 assert_eq!(fm.current(), Some(2));
1018 }
1019
1020 #[test]
1021 fn apply_host_focus_gain_restores_previously_selected_trapped_focus() {
1022 let mut fm = FocusManager::new();
1023 fm.graph_mut().insert(node(1, 0));
1024 fm.graph_mut().insert(node(2, 1));
1025 fm.graph_mut().insert(node(3, 2));
1026 fm.create_group(42, vec![2, 3]);
1027 assert!(fm.push_trap(42));
1028 assert_eq!(fm.current(), Some(2));
1029 assert_eq!(fm.focus(3), Some(2));
1030 let _ = fm.take_focus_event();
1031
1032 assert!(fm.apply_host_focus(false));
1033 assert_eq!(fm.current(), None);
1034 let _ = fm.take_focus_event();
1035
1036 assert!(fm.apply_host_focus(true));
1037 assert_eq!(fm.current(), Some(3));
1038 assert_eq!(
1039 fm.take_focus_event(),
1040 Some(FocusEvent::FocusGained { id: 3 })
1041 );
1042 }
1043
1044 #[test]
1045 fn push_trap_while_host_blurred_without_prior_focus_restores_none_on_pop() {
1046 let mut fm = FocusManager::new();
1047 fm.graph_mut().insert(node(1, 0));
1048 fm.graph_mut().insert(node(2, 1));
1049 assert!(!fm.apply_host_focus(false));
1050
1051 fm.create_group(42, vec![2]);
1052 assert!(fm.push_trap(42));
1053 assert!(fm.apply_host_focus(true));
1054 assert_eq!(fm.current(), Some(2));
1055
1056 assert!(fm.pop_trap());
1057 assert_eq!(fm.current(), None);
1058 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 2 }));
1059 }
1060
1061 #[test]
1062 fn push_trap_does_not_autofocus_while_host_blurred() {
1063 let mut fm = FocusManager::new();
1064 fm.graph_mut().insert(node(1, 0));
1065 fm.graph_mut().insert(node(2, 1));
1066 fm.focus(1);
1067 assert!(fm.apply_host_focus(false));
1068
1069 fm.create_group(42, vec![2]);
1070 assert!(fm.push_trap(42));
1071 assert_eq!(fm.current(), None);
1072
1073 assert!(fm.apply_host_focus(true));
1074 assert_eq!(fm.current(), Some(2));
1075 }
1076
1077 #[test]
1078 fn focus_while_host_blurred_updates_deferred_target_without_restoring_current() {
1079 let mut fm = FocusManager::new();
1080 fm.graph_mut().insert(node(1, 0));
1081 fm.graph_mut().insert(node(2, 1));
1082 fm.graph_mut().insert(node(3, 2));
1083 fm.focus(1);
1084 let _ = fm.take_focus_event();
1085
1086 assert!(fm.apply_host_focus(false));
1087 assert_eq!(fm.current(), None);
1088 assert_eq!(fm.focus(3), Some(1));
1089 assert_eq!(fm.current(), None);
1090 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1091
1092 assert!(fm.apply_host_focus(true));
1093 assert_eq!(fm.current(), Some(3));
1094 assert_eq!(
1095 fm.take_focus_event(),
1096 Some(FocusEvent::FocusGained { id: 3 })
1097 );
1098 }
1099
1100 #[test]
1101 fn focus_next_while_host_blurred_advances_deferred_target() {
1102 let mut fm = FocusManager::new();
1103 fm.graph_mut().insert(node(1, 0));
1104 fm.graph_mut().insert(node(2, 1));
1105 fm.graph_mut().insert(node(3, 2));
1106 fm.focus(1);
1107 assert_eq!(fm.focus(2), Some(1));
1108 let _ = fm.take_focus_event();
1109
1110 assert!(fm.apply_host_focus(false));
1111 assert_eq!(fm.current(), None);
1112 assert!(fm.focus_next());
1113 assert_eq!(fm.current(), None);
1114 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 2 }));
1115
1116 assert!(fm.apply_host_focus(true));
1117 assert_eq!(fm.current(), Some(3));
1118 assert_eq!(
1119 fm.take_focus_event(),
1120 Some(FocusEvent::FocusGained { id: 3 })
1121 );
1122 }
1123
1124 #[test]
1125 fn focus_trap_push_pop() {
1126 let mut fm = FocusManager::new();
1127 fm.graph_mut().insert(node(1, 0));
1128 fm.graph_mut().insert(node(2, 1));
1129 fm.graph_mut().insert(node(3, 2));
1130
1131 fm.focus(3);
1132 fm.create_group(7, vec![1, 2]);
1133
1134 fm.push_trap(7);
1135 assert!(fm.is_trapped());
1136 assert_eq!(fm.current(), Some(1));
1137
1138 fm.pop_trap();
1139 assert!(!fm.is_trapped());
1140 assert_eq!(fm.current(), Some(3));
1141 }
1142
1143 #[test]
1144 fn focus_group_wrap_respected() {
1145 let mut fm = FocusManager::new();
1146 fm.graph_mut().insert(node(1, 0));
1147 fm.graph_mut().insert(node(2, 1));
1148 fm.create_group(9, vec![1, 2]);
1149 fm.groups.get_mut(&9).unwrap().wrap = false;
1150
1151 fm.push_trap(9);
1152 fm.focus(2);
1153 assert!(!fm.focus_next());
1154 assert_eq!(fm.current(), Some(2));
1155 }
1156
1157 #[test]
1158 fn focus_event_generation() {
1159 let mut fm = FocusManager::new();
1160 fm.graph_mut().insert(node(1, 0));
1161 fm.graph_mut().insert(node(2, 1));
1162
1163 fm.focus(1);
1164 assert_eq!(
1165 fm.take_focus_event(),
1166 Some(FocusEvent::FocusGained { id: 1 })
1167 );
1168
1169 fm.focus(2);
1170 assert_eq!(
1171 fm.take_focus_event(),
1172 Some(FocusEvent::FocusMoved { from: 1, to: 2 })
1173 );
1174
1175 fm.blur();
1176 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 2 }));
1177 }
1178
1179 #[test]
1180 fn trap_prevents_focus_outside_group() {
1181 let mut fm = FocusManager::new();
1182 fm.graph_mut().insert(node(1, 0));
1183 fm.graph_mut().insert(node(2, 1));
1184 fm.graph_mut().insert(node(3, 2));
1185 fm.create_group(5, vec![1, 2]);
1186
1187 fm.push_trap(5);
1188 assert_eq!(fm.current(), Some(1));
1189
1190 assert!(fm.focus(3).is_none());
1192 assert_ne!(fm.current(), Some(3));
1193 }
1194
1195 fn spatial_node(id: FocusId, x: u16, y: u16, w: u16, h: u16, tab: i32) -> FocusNode {
1198 FocusNode::new(id, Rect::new(x, y, w, h)).with_tab_index(tab)
1199 }
1200
1201 #[test]
1202 fn navigate_spatial_fallback() {
1203 let mut fm = FocusManager::new();
1204 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1206 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
1207
1208 fm.focus(1);
1209 assert!(fm.navigate(NavDirection::Right));
1210 assert_eq!(fm.current(), Some(2));
1211
1212 assert!(fm.navigate(NavDirection::Left));
1213 assert_eq!(fm.current(), Some(1));
1214 }
1215
1216 #[test]
1217 fn navigate_explicit_edge_overrides_spatial() {
1218 let mut fm = FocusManager::new();
1219 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1220 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);
1225
1226 fm.focus(1);
1227 assert!(fm.navigate(NavDirection::Right));
1228 assert_eq!(fm.current(), Some(3));
1229 }
1230
1231 #[test]
1232 fn navigate_spatial_respects_trap() {
1233 let mut fm = FocusManager::new();
1234 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1235 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
1236 fm.graph_mut().insert(spatial_node(3, 40, 0, 10, 3, 2));
1237
1238 fm.create_group(1, vec![1, 2]);
1240 fm.focus(2);
1241 fm.push_trap(1);
1242
1243 assert!(!fm.navigate(NavDirection::Right));
1245 assert_eq!(fm.current(), Some(2));
1246 }
1247
1248 #[test]
1249 fn navigate_spatial_grid_round_trip() {
1250 let mut fm = FocusManager::new();
1251 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1253 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
1254 fm.graph_mut().insert(spatial_node(3, 0, 6, 10, 3, 2));
1255 fm.graph_mut().insert(spatial_node(4, 20, 6, 10, 3, 3));
1256
1257 fm.focus(1);
1258
1259 assert!(fm.navigate(NavDirection::Right));
1261 assert_eq!(fm.current(), Some(2));
1262
1263 assert!(fm.navigate(NavDirection::Down));
1264 assert_eq!(fm.current(), Some(4));
1265
1266 assert!(fm.navigate(NavDirection::Left));
1267 assert_eq!(fm.current(), Some(3));
1268
1269 assert!(fm.navigate(NavDirection::Up));
1270 assert_eq!(fm.current(), Some(1));
1271 }
1272
1273 #[test]
1274 fn navigate_spatial_no_candidate() {
1275 let mut fm = FocusManager::new();
1276 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1277 fm.focus(1);
1278
1279 assert!(!fm.navigate(NavDirection::Right));
1281 assert!(!fm.navigate(NavDirection::Up));
1282 assert_eq!(fm.current(), Some(1));
1283 }
1284
1285 #[test]
1288 fn new_manager_has_no_focus() {
1289 let fm = FocusManager::new();
1290 assert_eq!(fm.current(), None);
1291 assert!(!fm.is_trapped());
1292 }
1293
1294 #[test]
1295 fn default_and_new_are_equivalent() {
1296 let a = FocusManager::new();
1297 let b = FocusManager::default();
1298 assert_eq!(a.current(), b.current());
1299 assert_eq!(a.is_trapped(), b.is_trapped());
1300 assert_eq!(a.host_focused(), b.host_focused());
1301 }
1302
1303 #[test]
1306 fn is_focused_returns_true_for_current() {
1307 let mut fm = FocusManager::new();
1308 fm.graph_mut().insert(node(1, 0));
1309 fm.focus(1);
1310 assert!(fm.is_focused(1));
1311 assert!(!fm.is_focused(2));
1312 }
1313
1314 #[test]
1315 fn is_focused_returns_false_when_no_focus() {
1316 let fm = FocusManager::new();
1317 assert!(!fm.is_focused(1));
1318 }
1319
1320 #[test]
1323 fn focus_non_existent_node_returns_none() {
1324 let mut fm = FocusManager::new();
1325 assert!(fm.focus(999).is_none());
1326 assert_eq!(fm.current(), None);
1327 }
1328
1329 #[test]
1330 fn focus_already_focused_returns_same_id() {
1331 let mut fm = FocusManager::new();
1332 fm.graph_mut().insert(node(1, 0));
1333 fm.focus(1);
1334 assert_eq!(fm.focus(1), Some(1));
1336 assert_eq!(fm.current(), Some(1));
1337 }
1338
1339 #[test]
1342 fn blur_when_no_focus_returns_none() {
1343 let mut fm = FocusManager::new();
1344 assert_eq!(fm.blur(), None);
1345 }
1346
1347 #[test]
1348 fn blur_generates_focus_lost_event() {
1349 let mut fm = FocusManager::new();
1350 fm.graph_mut().insert(node(1, 0));
1351 fm.focus(1);
1352 let _ = fm.take_focus_event(); fm.blur();
1354 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1355 }
1356
1357 #[test]
1360 fn focus_first_selects_lowest_tab_index() {
1361 let mut fm = FocusManager::new();
1362 fm.graph_mut().insert(node(3, 2));
1363 fm.graph_mut().insert(node(1, 0));
1364 fm.graph_mut().insert(node(2, 1));
1365
1366 assert!(fm.focus_first());
1367 assert_eq!(fm.current(), Some(1));
1368 }
1369
1370 #[test]
1371 fn focus_last_selects_highest_tab_index() {
1372 let mut fm = FocusManager::new();
1373 fm.graph_mut().insert(node(1, 0));
1374 fm.graph_mut().insert(node(2, 1));
1375 fm.graph_mut().insert(node(3, 2));
1376
1377 assert!(fm.focus_last());
1378 assert_eq!(fm.current(), Some(3));
1379 }
1380
1381 #[test]
1382 fn focus_first_on_empty_graph_returns_false() {
1383 let mut fm = FocusManager::new();
1384 assert!(!fm.focus_first());
1385 }
1386
1387 #[test]
1388 fn focus_last_on_empty_graph_returns_false() {
1389 let mut fm = FocusManager::new();
1390 assert!(!fm.focus_last());
1391 }
1392
1393 #[test]
1396 fn focus_next_wraps_at_end() {
1397 let mut fm = FocusManager::new();
1398 fm.graph_mut().insert(node(1, 0));
1399 fm.graph_mut().insert(node(2, 1));
1400
1401 fm.focus(2);
1402 assert!(fm.focus_next()); assert_eq!(fm.current(), Some(1));
1404 }
1405
1406 #[test]
1407 fn focus_prev_wraps_at_start() {
1408 let mut fm = FocusManager::new();
1409 fm.graph_mut().insert(node(1, 0));
1410 fm.graph_mut().insert(node(2, 1));
1411
1412 fm.focus(1);
1413 assert!(fm.focus_prev()); assert_eq!(fm.current(), Some(2));
1415 }
1416
1417 #[test]
1418 fn focus_next_with_no_current_selects_first() {
1419 let mut fm = FocusManager::new();
1420 fm.graph_mut().insert(node(1, 0));
1421 fm.graph_mut().insert(node(2, 1));
1422
1423 assert!(fm.focus_next());
1424 assert_eq!(fm.current(), Some(1));
1425 }
1426
1427 #[test]
1428 fn focus_prev_with_no_current_selects_last() {
1429 let mut fm = FocusManager::new();
1430 fm.graph_mut().insert(node(1, 0));
1431 fm.graph_mut().insert(node(2, 1));
1432
1433 assert!(fm.focus_prev());
1434 assert_eq!(fm.current(), Some(2));
1435 }
1436
1437 #[test]
1438 fn focus_prev_with_stale_current_selects_last() {
1439 let mut fm = FocusManager::new();
1440 fm.graph_mut().insert(node(1, 0));
1441 fm.graph_mut().insert(node(2, 1));
1442 fm.graph_mut().insert(node(3, 2));
1443
1444 fm.focus(2);
1445 let _ = fm.graph_mut().remove(2);
1446
1447 assert!(fm.focus_prev());
1448 assert_eq!(fm.current(), Some(3));
1449 }
1450
1451 #[test]
1452 fn focus_next_on_empty_returns_false() {
1453 let mut fm = FocusManager::new();
1454 assert!(!fm.focus_next());
1455 }
1456
1457 #[test]
1460 fn focus_back_on_empty_history_returns_false() {
1461 let mut fm = FocusManager::new();
1462 fm.graph_mut().insert(node(1, 0));
1463 fm.focus(1);
1464 assert!(!fm.focus_back());
1465 }
1466
1467 #[test]
1468 fn clear_history_prevents_back() {
1469 let mut fm = FocusManager::new();
1470 fm.graph_mut().insert(node(1, 0));
1471 fm.graph_mut().insert(node(2, 1));
1472
1473 fm.focus(1);
1474 fm.focus(2);
1475 fm.clear_history();
1476 assert!(!fm.focus_back());
1477 assert_eq!(fm.current(), Some(2));
1478 }
1479
1480 #[test]
1481 fn focus_back_skips_removed_nodes() {
1482 let mut fm = FocusManager::new();
1483 fm.graph_mut().insert(node(1, 0));
1484 fm.graph_mut().insert(node(2, 1));
1485 fm.graph_mut().insert(node(3, 2));
1486
1487 fm.focus(1);
1488 fm.focus(2);
1489 fm.focus(3);
1490
1491 let _ = fm.graph_mut().remove(2);
1493
1494 assert!(fm.focus_back());
1496 assert_eq!(fm.current(), Some(1));
1497 }
1498
1499 #[test]
1502 fn create_group_filters_non_focusable() {
1503 let mut fm = FocusManager::new();
1504 fm.graph_mut().insert(node(1, 0));
1505 fm.create_group(1, vec![1, 999]);
1507
1508 let group = fm.groups.get(&1).unwrap();
1509 assert_eq!(group.members.len(), 1);
1510 assert!(group.contains(1));
1511 }
1512
1513 #[test]
1514 fn add_to_group_creates_group_if_needed() {
1515 let mut fm = FocusManager::new();
1516 fm.graph_mut().insert(node(1, 0));
1517 fm.add_to_group(42, 1);
1518 assert!(fm.groups.contains_key(&42));
1519 assert!(fm.groups.get(&42).unwrap().contains(1));
1520 }
1521
1522 #[test]
1523 fn add_to_group_skips_unfocusable() {
1524 let mut fm = FocusManager::new();
1525 fm.add_to_group(1, 999); if let Some(group) = fm.groups.get(&1) {
1528 assert!(!group.contains(999));
1529 }
1530 }
1531
1532 #[test]
1533 fn add_to_group_no_duplicates() {
1534 let mut fm = FocusManager::new();
1535 fm.graph_mut().insert(node(1, 0));
1536 fm.add_to_group(1, 1);
1537 fm.add_to_group(1, 1);
1538 assert_eq!(fm.groups.get(&1).unwrap().members.len(), 1);
1539 }
1540
1541 #[test]
1542 fn remove_from_group() {
1543 let mut fm = FocusManager::new();
1544 fm.graph_mut().insert(node(1, 0));
1545 fm.graph_mut().insert(node(2, 1));
1546 fm.create_group(1, vec![1, 2]);
1547 fm.remove_from_group(1, 1);
1548 assert!(!fm.groups.get(&1).unwrap().contains(1));
1549 assert!(fm.groups.get(&1).unwrap().contains(2));
1550 }
1551
1552 #[test]
1553 fn removing_focused_member_from_active_trap_refocuses_remaining_member() {
1554 let mut fm = FocusManager::new();
1555 fm.graph_mut().insert(node(1, 0));
1556 fm.graph_mut().insert(node(2, 1));
1557 fm.graph_mut().insert(node(3, 2));
1558 fm.create_group(1, vec![1, 2]);
1559
1560 fm.focus(2);
1561 assert!(fm.push_trap(1));
1562 assert_eq!(fm.current(), Some(2));
1563
1564 fm.remove_from_group(1, 2);
1565 assert_eq!(fm.current(), Some(1));
1566 assert!(fm.is_trapped());
1567 assert!(fm.focus(3).is_none());
1568 assert_eq!(fm.current(), Some(1));
1569 }
1570
1571 #[test]
1572 fn removing_last_member_from_active_trap_allows_focus_escape() {
1573 let mut fm = FocusManager::new();
1574 fm.graph_mut().insert(node(1, 0));
1575 fm.graph_mut().insert(node(2, 1));
1576 fm.create_group(1, vec![1]);
1577
1578 fm.focus(1);
1579 assert!(fm.push_trap(1));
1580 assert!(fm.is_trapped());
1581
1582 fm.remove_from_group(1, 1);
1583 assert!(!fm.is_trapped());
1584 assert_eq!(fm.current(), Some(1));
1585 assert_eq!(fm.focus(2), Some(1));
1586 assert_eq!(fm.current(), Some(2));
1587 }
1588
1589 #[test]
1590 fn removing_active_inner_trap_member_falls_back_to_outer_trap() {
1591 let mut fm = FocusManager::new();
1592 fm.graph_mut().insert(node(1, 0));
1593 fm.graph_mut().insert(node(2, 1));
1594 fm.graph_mut().insert(node(3, 2));
1595 fm.create_group(10, vec![1, 2]);
1596 fm.create_group(20, vec![3]);
1597
1598 fm.focus(1);
1599 assert!(fm.push_trap(10));
1600 assert!(fm.push_trap(20));
1601 assert_eq!(fm.current(), Some(3));
1602
1603 fm.remove_from_group(20, 3);
1604 assert!(fm.is_trapped());
1605 assert_eq!(fm.current(), Some(1));
1606 assert!(fm.focus(3).is_none());
1607 assert_eq!(fm.focus(2), Some(1));
1608 assert_eq!(fm.current(), Some(2));
1609 }
1610
1611 #[test]
1612 fn adding_member_to_invalidated_trap_restores_confinement() {
1613 let mut fm = FocusManager::new();
1614 fm.graph_mut().insert(node(1, 0));
1615 fm.graph_mut().insert(node(2, 1));
1616 fm.create_group(1, vec![1]);
1617
1618 fm.focus(1);
1619 assert!(fm.push_trap(1));
1620
1621 fm.remove_from_group(1, 1);
1622 assert!(!fm.is_trapped());
1623
1624 fm.add_to_group(1, 2);
1625 assert!(fm.is_trapped());
1626 assert_eq!(fm.current(), Some(2));
1627 assert!(fm.focus(1).is_none());
1628 }
1629
1630 #[test]
1631 fn remove_from_nonexistent_group_is_noop() {
1632 let mut fm = FocusManager::new();
1633 fm.remove_from_group(999, 1); }
1635
1636 #[test]
1637 fn remove_group_deletes_group() {
1638 let mut fm = FocusManager::new();
1639 fm.graph_mut().insert(node(1, 0));
1640 fm.create_group(42, vec![1]);
1641
1642 fm.remove_group(42);
1643 assert!(!fm.groups.contains_key(&42));
1644 }
1645
1646 #[test]
1647 fn remove_group_from_active_inner_trap_falls_back_to_outer_trap() {
1648 let mut fm = FocusManager::new();
1649 fm.graph_mut().insert(node(1, 0));
1650 fm.graph_mut().insert(node(2, 1));
1651 fm.graph_mut().insert(node(3, 2));
1652 fm.create_group(10, vec![1, 2]);
1653 fm.create_group(20, vec![3]);
1654
1655 fm.focus(1);
1656 assert!(fm.push_trap(10));
1657 assert!(fm.push_trap(20));
1658 assert_eq!(fm.current(), Some(3));
1659
1660 fm.remove_group(20);
1661 assert!(fm.is_trapped());
1662 assert_eq!(fm.current(), Some(1));
1663 assert!(fm.focus(3).is_none());
1664 }
1665
1666 #[test]
1667 fn remove_group_clears_stale_trap_entries() {
1668 let mut fm = FocusManager::new();
1669 fm.graph_mut().insert(node(1, 0));
1670 fm.create_group(10, vec![1]);
1671
1672 fm.focus(1);
1673 assert!(fm.push_trap(10));
1674 assert!(fm.is_trapped());
1675
1676 fm.remove_group(10);
1677 assert!(!fm.is_trapped());
1678 assert!(!fm.pop_trap());
1679 }
1680
1681 #[test]
1682 fn remove_group_blurs_invalid_current_when_no_fallback_exists() {
1683 let mut fm = FocusManager::new();
1684 fm.graph_mut().insert(node(1, 0));
1685 fm.create_group(10, vec![1]);
1686 fm.focus(1);
1687
1688 let _ = fm.graph_mut().remove(1);
1689 fm.remove_group(10);
1690
1691 assert_eq!(fm.current(), None);
1692 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1693 }
1694
1695 #[test]
1698 fn focus_group_with_wrap() {
1699 let group = FocusGroup::new(1, vec![1, 2]).with_wrap(false);
1700 assert!(!group.wrap);
1701 }
1702
1703 #[test]
1704 fn focus_group_with_exit_key() {
1705 let group = FocusGroup::new(1, vec![]).with_exit_key(KeyCode::Escape);
1706 assert_eq!(group.exit_key, Some(KeyCode::Escape));
1707 }
1708
1709 #[test]
1710 fn focus_group_default_wraps() {
1711 let group = FocusGroup::new(1, vec![]);
1712 assert!(group.wrap);
1713 assert_eq!(group.exit_key, None);
1714 }
1715
1716 #[test]
1719 fn nested_traps() {
1720 let mut fm = FocusManager::new();
1721 fm.graph_mut().insert(node(1, 0));
1722 fm.graph_mut().insert(node(2, 1));
1723 fm.graph_mut().insert(node(3, 2));
1724 fm.graph_mut().insert(node(4, 3));
1725
1726 fm.create_group(10, vec![1, 2]);
1727 fm.create_group(20, vec![3, 4]);
1728
1729 fm.focus(1);
1730 fm.push_trap(10);
1731 assert!(fm.is_trapped());
1732
1733 fm.push_trap(20);
1734 assert_eq!(fm.current(), Some(3));
1736
1737 fm.pop_trap();
1739 assert!(fm.is_trapped());
1741
1742 fm.pop_trap();
1744 assert!(!fm.is_trapped());
1745 }
1746
1747 #[test]
1748 fn trap_push_pop_does_not_pollute_focus_history() {
1749 let mut fm = FocusManager::new();
1750 fm.graph_mut().insert(node(1, 0));
1751 fm.graph_mut().insert(node(2, 1));
1752 fm.graph_mut().insert(node(3, 2));
1753 fm.create_group(10, vec![2]);
1754
1755 fm.focus(1);
1756 fm.focus(3);
1757 assert_eq!(fm.current(), Some(3));
1758
1759 assert!(fm.push_trap(10));
1760 assert_eq!(fm.current(), Some(2));
1761
1762 assert!(fm.pop_trap());
1763 assert_eq!(fm.current(), Some(3));
1764
1765 assert!(fm.focus_back());
1766 assert_eq!(fm.current(), Some(1));
1767 assert!(!fm.focus_back());
1768 }
1769
1770 #[test]
1771 fn pop_trap_restores_none_when_modal_opened_without_focus() {
1772 let mut fm = FocusManager::new();
1773 fm.graph_mut().insert(node(1, 0));
1774 fm.create_group(10, vec![1]);
1775
1776 assert!(fm.push_trap(10));
1777 assert_eq!(fm.current(), Some(1));
1778
1779 assert!(fm.pop_trap());
1780 assert_eq!(fm.current(), None);
1781 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1782 }
1783
1784 #[test]
1785 fn pop_trap_on_empty_returns_false() {
1786 let mut fm = FocusManager::new();
1787 assert!(!fm.pop_trap());
1788 }
1789
1790 #[test]
1791 fn push_trap_rejects_missing_group() {
1792 let mut fm = FocusManager::new();
1793 fm.graph_mut().insert(node(1, 0));
1794 fm.focus(1);
1795
1796 assert!(!fm.push_trap(999));
1798 assert!(!fm.is_trapped());
1799 assert_eq!(fm.current(), Some(1));
1801 }
1802
1803 #[test]
1804 fn push_trap_rejects_empty_group() {
1805 let mut fm = FocusManager::new();
1806 fm.graph_mut().insert(node(1, 0));
1807 fm.focus(1);
1808
1809 fm.create_group(42, vec![]);
1811 assert!(!fm.push_trap(42));
1812 assert!(!fm.is_trapped());
1813 assert_eq!(fm.current(), Some(1));
1815 }
1816
1817 #[test]
1818 fn push_trap_autofocuses_negative_tabindex_member_when_group_has_no_tabbable_nodes() {
1819 let mut fm = FocusManager::new();
1820 fm.graph_mut().insert(node(1, 0));
1821 fm.graph_mut().insert(node(2, -1));
1822 fm.focus(1);
1823
1824 fm.create_group(42, vec![2]);
1825 assert!(fm.push_trap(42));
1826 assert!(fm.is_trapped());
1827 assert_eq!(fm.current(), Some(2));
1828 }
1829
1830 #[test]
1831 fn push_trap_blurred_restores_negative_tabindex_member_on_focus_gain() {
1832 let mut fm = FocusManager::new();
1833 fm.graph_mut().insert(node(1, 0));
1834 fm.graph_mut().insert(node(2, -1));
1835 fm.focus(1);
1836 assert!(fm.apply_host_focus(false));
1837
1838 fm.create_group(42, vec![2]);
1839 assert!(fm.push_trap(42));
1840 assert_eq!(fm.current(), None);
1841
1842 assert!(fm.apply_host_focus(true));
1843 assert_eq!(fm.current(), Some(2));
1844 }
1845
1846 #[test]
1847 fn push_trap_retargets_when_current_group_member_becomes_unfocusable() {
1848 let mut fm = FocusManager::new();
1849 fm.graph_mut().insert(node(1, 0));
1850 fm.graph_mut().insert(node(2, 1));
1851 fm.focus(1);
1852 fm.create_group(10, vec![1, 2]);
1853
1854 fm.graph_mut().insert(node(1, 0).with_focusable(false));
1855
1856 assert!(fm.push_trap(10));
1857 assert_eq!(fm.current(), Some(2));
1858 }
1859
1860 #[test]
1863 fn take_focus_event_clears_it() {
1864 let mut fm = FocusManager::new();
1865 fm.graph_mut().insert(node(1, 0));
1866 fm.focus(1);
1867
1868 assert!(fm.take_focus_event().is_some());
1869 assert!(fm.take_focus_event().is_none());
1870 }
1871
1872 #[test]
1873 fn focus_event_accessor() {
1874 let mut fm = FocusManager::new();
1875 fm.graph_mut().insert(node(1, 0));
1876 fm.focus(1);
1877
1878 assert_eq!(fm.focus_event(), Some(&FocusEvent::FocusGained { id: 1 }));
1879 }
1880
1881 #[test]
1884 fn navigate_direction_with_no_current_returns_false() {
1885 let mut fm = FocusManager::new();
1886 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1887 assert!(!fm.navigate(NavDirection::Right));
1888 }
1889
1890 #[test]
1893 fn graph_accessor_returns_reference() {
1894 let mut fm = FocusManager::new();
1895 fm.graph_mut().insert(node(1, 0));
1896 assert!(fm.graph().get(1).is_some());
1897 }
1898
1899 #[test]
1902 fn default_indicator_is_reverse() {
1903 let fm = FocusManager::new();
1904 assert!(fm.indicator().is_visible());
1905 assert_eq!(
1906 fm.indicator().kind(),
1907 crate::focus::FocusIndicatorKind::StyleOverlay
1908 );
1909 }
1910
1911 #[test]
1912 fn set_indicator() {
1913 let mut fm = FocusManager::new();
1914 fm.set_indicator(crate::focus::FocusIndicator::underline());
1915 assert_eq!(
1916 fm.indicator().kind(),
1917 crate::focus::FocusIndicatorKind::Underline
1918 );
1919 }
1920
1921 #[test]
1924 fn focus_change_count_increments() {
1925 let mut fm = FocusManager::new();
1926 fm.graph_mut().insert(node(1, 0));
1927 fm.graph_mut().insert(node(2, 1));
1928
1929 assert_eq!(fm.focus_change_count(), 0);
1930
1931 fm.focus(1);
1932 assert_eq!(fm.focus_change_count(), 1);
1933
1934 fm.focus(2);
1935 assert_eq!(fm.focus_change_count(), 2);
1936
1937 fm.blur();
1938 assert_eq!(fm.focus_change_count(), 3);
1939 }
1940
1941 #[test]
1942 fn focus_change_count_zero_on_no_op() {
1943 let mut fm = FocusManager::new();
1944 fm.graph_mut().insert(node(1, 0));
1945 fm.focus(1);
1946 assert_eq!(fm.focus_change_count(), 1);
1947
1948 fm.focus(1);
1950 assert_eq!(fm.focus_change_count(), 1);
1951 }
1952
1953 #[test]
1954 fn focus_back_increments_focus_change_count() {
1955 let mut fm = FocusManager::new();
1956 fm.graph_mut().insert(node(1, 0));
1957 fm.graph_mut().insert(node(2, 1));
1958
1959 fm.focus(1);
1960 fm.focus(2);
1961 assert_eq!(fm.focus_change_count(), 2);
1962
1963 assert!(fm.focus_back());
1964 assert_eq!(fm.current(), Some(1));
1965 assert_eq!(fm.focus_change_count(), 3);
1966 }
1967}