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 self.history.retain(|id| !excluded.contains(id));
568
569 if !self.host_focused {
570 if self.current.is_some_and(|id| excluded.contains(&id)) {
571 let _ = self.blur();
572 }
573 return;
574 }
575
576 if self.is_trapped() || !self.current.is_some_and(|id| excluded.contains(&id)) {
577 return;
578 }
579
580 for id in self.graph.tab_order() {
581 if excluded.contains(&id) {
582 continue;
583 }
584 if self.set_focus_without_history(id) {
585 return;
586 }
587 }
588
589 let _ = self.blur();
590 }
591
592 pub(crate) fn clear_deferred_focus_if_excluded(&mut self, excluded: &[FocusId]) {
593 if self
594 .pending_focus_on_host_gain
595 .is_some_and(|id| excluded.contains(&id))
596 {
597 self.pending_focus_on_host_gain = None;
598 }
599 }
600
601 pub(crate) fn restore_focus_after_invalid_current(&mut self) {
602 if !self.host_focused {
603 return;
604 }
605
606 if let Some(group_id) = self.active_trap_group()
607 && self.focus_first_in_group_without_history(group_id)
608 {
609 return;
610 }
611
612 let _ = self.focus_first_without_history();
613 }
614
615 fn set_focus(&mut self, id: FocusId) -> bool {
616 self.set_focus_target(id, true)
617 }
618
619 fn set_focus_without_history(&mut self, id: FocusId) -> bool {
620 self.set_focus_target(id, false)
621 }
622
623 fn set_focus_target(&mut self, id: FocusId, record_history: bool) -> bool {
624 if !self.host_focused {
625 return self.set_pending_focus_target(id);
626 }
627 self.set_focus_internal(id, record_history)
628 }
629
630 fn set_focus_internal(&mut self, id: FocusId, record_history: bool) -> bool {
631 if !self.can_focus(id) || !self.allowed_by_trap(id) {
632 return false;
633 }
634 if self.current == Some(id) {
635 return false;
636 }
637
638 let prev = self.current;
639 if let Some(prev_id) = prev {
640 if record_history && Some(prev_id) != self.history.last().copied() {
641 self.history.push(prev_id);
642 }
643 let event = FocusEvent::FocusMoved {
644 from: prev_id,
645 to: id,
646 };
647 #[cfg(feature = "tracing")]
648 tracing::debug!(
649 from_widget = prev_id,
650 to_widget = id,
651 trigger = "navigate",
652 "focus.change"
653 );
654 self.last_event = Some(event);
655 } else {
656 #[cfg(feature = "tracing")]
657 tracing::debug!(to_widget = id, trigger = "initial", "focus.change");
658 self.last_event = Some(FocusEvent::FocusGained { id });
659 }
660
661 self.current = Some(id);
662 self.focus_change_count += 1;
663 true
664 }
665
666 fn can_focus(&self, id: FocusId) -> bool {
667 self.graph.get(id).map(|n| n.is_focusable).unwrap_or(false)
668 }
669
670 fn active_focus_target(&self) -> Option<FocusId> {
671 if let Some(current) = self.current
672 && self.can_focus(current)
673 && self.allowed_by_trap(current)
674 {
675 return Some(current);
676 }
677
678 if self.host_focused {
679 return None;
680 }
681
682 self.pending_focus_on_host_gain
683 .filter(|id| self.can_focus(*id) && self.allowed_by_trap(*id))
684 }
685
686 fn set_pending_focus_target(&mut self, id: FocusId) -> bool {
687 if !self.can_focus(id) || !self.allowed_by_trap(id) {
688 return false;
689 }
690
691 let prev = self.active_focus_target();
692 self.current = None;
693 if prev == Some(id) {
694 return false;
695 }
696
697 self.pending_focus_on_host_gain = Some(id);
698 true
699 }
700
701 fn active_trap_group(&self) -> Option<u32> {
702 self.trap_stack
703 .iter()
704 .rev()
705 .find(|trap| self.group_has_focusable_member(trap.group_id))
706 .map(|trap| trap.group_id)
707 }
708
709 fn allowed_by_trap(&self, id: FocusId) -> bool {
710 let Some(group_id) = self.active_trap_group() else {
711 return true;
712 };
713 self.groups
714 .get(&group_id)
715 .map(|g| g.contains(id))
716 .unwrap_or(false)
717 }
718
719 fn group_has_focusable_member(&self, group_id: u32) -> bool {
720 self.groups
721 .get(&group_id)
722 .is_some_and(|group| group.members.iter().any(|id| self.can_focus(*id)))
723 }
724
725 fn repair_focus_after_group_change(&mut self) {
726 if !self.host_focused {
727 if self.current.is_some() {
728 let _ = self.blur();
729 }
730 return;
731 }
732
733 match self.active_trap_group() {
734 Some(group_id) => {
735 let current_allowed = self
736 .current
737 .is_some_and(|id| self.can_focus(id) && self.allowed_by_trap(id));
738 if !current_allowed {
739 let _ = self.focus_first_in_group_without_history(group_id);
740 }
741 }
742 None => {
743 if self.current.is_some_and(|id| !self.can_focus(id))
744 && !self.focus_first_without_history()
745 {
746 let _ = self.blur();
747 }
748 }
749 }
750 }
751
752 fn is_current_focusable_in_group(&self, group_id: u32) -> bool {
753 let Some(current) = self.current else {
754 return false;
755 };
756 self.can_focus(current)
757 && self
758 .groups
759 .get(&group_id)
760 .map(|g| g.contains(current))
761 .unwrap_or(false)
762 }
763
764 fn active_tab_order(&self) -> Vec<FocusId> {
765 if let Some(group_id) = self.active_trap_group() {
766 return self.group_tab_order(group_id);
767 }
768 self.graph.tab_order()
769 }
770
771 fn group_tab_order(&self, group_id: u32) -> Vec<FocusId> {
772 let Some(group) = self.groups.get(&group_id) else {
773 return Vec::new();
774 };
775 let order = self.graph.tab_order();
776 order.into_iter().filter(|id| group.contains(*id)).collect()
777 }
778
779 pub(crate) fn group_primary_focus_target(&self, group_id: u32) -> Option<FocusId> {
780 self.group_tab_order(group_id).first().copied().or_else(|| {
781 self.groups
782 .get(&group_id)
783 .and_then(|group| group.members.iter().copied().find(|id| self.can_focus(*id)))
784 })
785 }
786
787 fn focus_first_in_group_without_history(&mut self, group_id: u32) -> bool {
788 let Some(first) = self.group_primary_focus_target(group_id) else {
789 return false;
790 };
791 self.set_focus_without_history(first)
792 }
793
794 fn focus_first_without_history(&mut self) -> bool {
795 let order = self.active_tab_order();
796 let Some(first) = order.first().copied() else {
797 return false;
798 };
799 self.set_focus_without_history(first)
800 }
801
802 fn move_in_tab_order(&mut self, forward: bool) -> bool {
803 let order = self.active_tab_order();
804 if order.is_empty() {
805 return false;
806 }
807 let first = order[0];
808 let last = order[order.len() - 1];
809 let fallback = if forward { first } else { last };
810
811 let wrap = self
812 .active_trap_group()
813 .and_then(|id| self.groups.get(&id).map(|g| g.wrap))
814 .unwrap_or(true);
815
816 let next = match self.active_focus_target() {
817 None => fallback,
818 Some(current) => {
819 let pos = order.iter().position(|id| *id == current);
820 match pos {
821 None => fallback,
822 Some(idx) if forward => {
823 if idx + 1 < order.len() {
824 order[idx + 1]
825 } else if wrap {
826 order[0]
827 } else {
828 return false;
829 }
830 }
831 Some(idx) => {
832 if idx > 0 {
833 order[idx - 1]
834 } else if wrap {
835 last
836 } else {
837 return false;
838 }
839 }
840 }
841 }
842 };
843
844 self.set_focus(next)
845 }
846
847 fn dedup_members(&self, ids: Vec<FocusId>) -> Vec<FocusId> {
848 let mut out = Vec::new();
849 for id in ids {
850 if !out.contains(&id) {
851 out.push(id);
852 }
853 }
854 out
855 }
856
857 fn filter_focusable(&self, ids: Vec<FocusId>) -> Vec<FocusId> {
858 self.dedup_members(ids)
859 .into_iter()
860 .filter(|id| self.can_focus(*id))
861 .collect()
862 }
863}
864
865#[cfg(test)]
870mod tests {
871 use super::*;
872 use crate::focus::FocusNode;
873 use ftui_core::geometry::Rect;
874
875 fn node(id: FocusId, tab: i32) -> FocusNode {
876 FocusNode::new(id, Rect::new(0, 0, 1, 1)).with_tab_index(tab)
877 }
878
879 #[test]
880 fn focus_basic() {
881 let mut fm = FocusManager::new();
882 fm.graph_mut().insert(node(1, 0));
883 fm.graph_mut().insert(node(2, 1));
884
885 assert!(fm.focus(1).is_none());
886 assert_eq!(fm.current(), Some(1));
887
888 assert_eq!(fm.focus(2), Some(1));
889 assert_eq!(fm.current(), Some(2));
890
891 assert_eq!(fm.blur(), Some(2));
892 assert_eq!(fm.current(), None);
893 }
894
895 #[test]
896 fn focus_history_back() {
897 let mut fm = FocusManager::new();
898 fm.graph_mut().insert(node(1, 0));
899 fm.graph_mut().insert(node(2, 1));
900 fm.graph_mut().insert(node(3, 2));
901
902 fm.focus(1);
903 fm.focus(2);
904 fm.focus(3);
905
906 assert!(fm.focus_back());
907 assert_eq!(fm.current(), Some(2));
908
909 assert!(fm.focus_back());
910 assert_eq!(fm.current(), Some(1));
911 }
912
913 #[test]
914 fn focus_back_skips_current_id_in_history() {
915 let mut fm = FocusManager::new();
916 fm.graph_mut().insert(node(1, 0));
917 fm.graph_mut().insert(node(2, 1));
918
919 fm.focus(1);
920 fm.focus(2);
921 assert_eq!(fm.current(), Some(2));
922
923 assert_eq!(fm.blur(), Some(2));
924 assert_eq!(fm.current(), None);
925
926 fm.focus(1);
927 assert_eq!(fm.current(), Some(1));
928 let _ = fm.take_focus_event();
929 let before = fm.focus_change_count();
930
931 assert!(!fm.focus_back());
932 assert_eq!(fm.current(), Some(1));
933 assert!(fm.take_focus_event().is_none());
934 assert_eq!(fm.focus_change_count(), before);
935 }
936
937 #[test]
938 fn focus_next_prev() {
939 let mut fm = FocusManager::new();
940 fm.graph_mut().insert(node(1, 0));
941 fm.graph_mut().insert(node(2, 1));
942 fm.graph_mut().insert(node(3, 2));
943
944 assert!(fm.focus_next());
945 assert_eq!(fm.current(), Some(1));
946
947 assert!(fm.focus_next());
948 assert_eq!(fm.current(), Some(2));
949
950 assert!(fm.focus_prev());
951 assert_eq!(fm.current(), Some(1));
952 }
953
954 #[test]
955 fn apply_host_focus_loss_blurs_current() {
956 let mut fm = FocusManager::new();
957 fm.graph_mut().insert(node(1, 0));
958 fm.focus(1);
959 let _ = fm.take_focus_event();
960
961 assert!(fm.apply_host_focus(false));
962 assert_eq!(fm.current(), None);
963 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
964 }
965
966 #[test]
967 fn apply_host_focus_gain_focuses_first_when_unfocused() {
968 let mut fm = FocusManager::new();
969 fm.graph_mut().insert(node(10, 1));
970 fm.graph_mut().insert(node(5, 0));
971
972 assert!(fm.apply_host_focus(true));
973 assert_eq!(fm.current(), Some(5));
974 assert_eq!(
975 fm.take_focus_event(),
976 Some(FocusEvent::FocusGained { id: 5 })
977 );
978 }
979
980 #[test]
981 fn apply_host_focus_gain_preserves_valid_current() {
982 let mut fm = FocusManager::new();
983 fm.graph_mut().insert(node(1, 0));
984 fm.graph_mut().insert(node(2, 1));
985 fm.focus(2);
986 let _ = fm.take_focus_event();
987
988 assert!(!fm.apply_host_focus(true));
989 assert_eq!(fm.current(), Some(2));
990 assert!(fm.take_focus_event().is_none());
991 }
992
993 #[test]
994 fn apply_host_focus_gain_clears_invalid_current_when_restore_fails() {
995 let mut fm = FocusManager::new();
996 fm.graph_mut().insert(node(1, 0));
997 fm.focus(1);
998 let _ = fm.take_focus_event();
999 let _ = fm.graph_mut().remove(1);
1000
1001 assert!(fm.apply_host_focus(true));
1002 assert_eq!(fm.current(), None);
1003 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1004 }
1005
1006 #[test]
1007 fn apply_host_focus_gain_respects_trap_order() {
1008 let mut fm = FocusManager::new();
1009 fm.graph_mut().insert(node(1, 0));
1010 fm.graph_mut().insert(node(2, 1));
1011 fm.graph_mut().insert(node(3, 2));
1012 fm.create_group(42, vec![2, 3]);
1013 fm.push_trap(42);
1014 let _ = fm.take_focus_event();
1015 fm.blur();
1016 let _ = fm.take_focus_event();
1017
1018 assert!(fm.apply_host_focus(true));
1019 assert_eq!(fm.current(), Some(2));
1020 }
1021
1022 #[test]
1023 fn apply_host_focus_gain_restores_previously_selected_trapped_focus() {
1024 let mut fm = FocusManager::new();
1025 fm.graph_mut().insert(node(1, 0));
1026 fm.graph_mut().insert(node(2, 1));
1027 fm.graph_mut().insert(node(3, 2));
1028 fm.create_group(42, vec![2, 3]);
1029 assert!(fm.push_trap(42));
1030 assert_eq!(fm.current(), Some(2));
1031 assert_eq!(fm.focus(3), Some(2));
1032 let _ = fm.take_focus_event();
1033
1034 assert!(fm.apply_host_focus(false));
1035 assert_eq!(fm.current(), None);
1036 let _ = fm.take_focus_event();
1037
1038 assert!(fm.apply_host_focus(true));
1039 assert_eq!(fm.current(), Some(3));
1040 assert_eq!(
1041 fm.take_focus_event(),
1042 Some(FocusEvent::FocusGained { id: 3 })
1043 );
1044 }
1045
1046 #[test]
1047 fn push_trap_while_host_blurred_without_prior_focus_restores_none_on_pop() {
1048 let mut fm = FocusManager::new();
1049 fm.graph_mut().insert(node(1, 0));
1050 fm.graph_mut().insert(node(2, 1));
1051 assert!(!fm.apply_host_focus(false));
1052
1053 fm.create_group(42, vec![2]);
1054 assert!(fm.push_trap(42));
1055 assert!(fm.apply_host_focus(true));
1056 assert_eq!(fm.current(), Some(2));
1057
1058 assert!(fm.pop_trap());
1059 assert_eq!(fm.current(), None);
1060 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 2 }));
1061 }
1062
1063 #[test]
1064 fn push_trap_does_not_autofocus_while_host_blurred() {
1065 let mut fm = FocusManager::new();
1066 fm.graph_mut().insert(node(1, 0));
1067 fm.graph_mut().insert(node(2, 1));
1068 fm.focus(1);
1069 assert!(fm.apply_host_focus(false));
1070
1071 fm.create_group(42, vec![2]);
1072 assert!(fm.push_trap(42));
1073 assert_eq!(fm.current(), None);
1074
1075 assert!(fm.apply_host_focus(true));
1076 assert_eq!(fm.current(), Some(2));
1077 }
1078
1079 #[test]
1080 fn focus_while_host_blurred_updates_deferred_target_without_restoring_current() {
1081 let mut fm = FocusManager::new();
1082 fm.graph_mut().insert(node(1, 0));
1083 fm.graph_mut().insert(node(2, 1));
1084 fm.graph_mut().insert(node(3, 2));
1085 fm.focus(1);
1086 let _ = fm.take_focus_event();
1087
1088 assert!(fm.apply_host_focus(false));
1089 assert_eq!(fm.current(), None);
1090 assert_eq!(fm.focus(3), Some(1));
1091 assert_eq!(fm.current(), None);
1092 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1093
1094 assert!(fm.apply_host_focus(true));
1095 assert_eq!(fm.current(), Some(3));
1096 assert_eq!(
1097 fm.take_focus_event(),
1098 Some(FocusEvent::FocusGained { id: 3 })
1099 );
1100 }
1101
1102 #[test]
1103 fn focus_next_while_host_blurred_advances_deferred_target() {
1104 let mut fm = FocusManager::new();
1105 fm.graph_mut().insert(node(1, 0));
1106 fm.graph_mut().insert(node(2, 1));
1107 fm.graph_mut().insert(node(3, 2));
1108 fm.focus(1);
1109 assert_eq!(fm.focus(2), Some(1));
1110 let _ = fm.take_focus_event();
1111
1112 assert!(fm.apply_host_focus(false));
1113 assert_eq!(fm.current(), None);
1114 assert!(fm.focus_next());
1115 assert_eq!(fm.current(), None);
1116 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 2 }));
1117
1118 assert!(fm.apply_host_focus(true));
1119 assert_eq!(fm.current(), Some(3));
1120 assert_eq!(
1121 fm.take_focus_event(),
1122 Some(FocusEvent::FocusGained { id: 3 })
1123 );
1124 }
1125
1126 #[test]
1127 fn focus_trap_push_pop() {
1128 let mut fm = FocusManager::new();
1129 fm.graph_mut().insert(node(1, 0));
1130 fm.graph_mut().insert(node(2, 1));
1131 fm.graph_mut().insert(node(3, 2));
1132
1133 fm.focus(3);
1134 fm.create_group(7, vec![1, 2]);
1135
1136 fm.push_trap(7);
1137 assert!(fm.is_trapped());
1138 assert_eq!(fm.current(), Some(1));
1139
1140 fm.pop_trap();
1141 assert!(!fm.is_trapped());
1142 assert_eq!(fm.current(), Some(3));
1143 }
1144
1145 #[test]
1146 fn focus_group_wrap_respected() {
1147 let mut fm = FocusManager::new();
1148 fm.graph_mut().insert(node(1, 0));
1149 fm.graph_mut().insert(node(2, 1));
1150 fm.create_group(9, vec![1, 2]);
1151 fm.groups.get_mut(&9).unwrap().wrap = false;
1152
1153 fm.push_trap(9);
1154 fm.focus(2);
1155 assert!(!fm.focus_next());
1156 assert_eq!(fm.current(), Some(2));
1157 }
1158
1159 #[test]
1160 fn focus_event_generation() {
1161 let mut fm = FocusManager::new();
1162 fm.graph_mut().insert(node(1, 0));
1163 fm.graph_mut().insert(node(2, 1));
1164
1165 fm.focus(1);
1166 assert_eq!(
1167 fm.take_focus_event(),
1168 Some(FocusEvent::FocusGained { id: 1 })
1169 );
1170
1171 fm.focus(2);
1172 assert_eq!(
1173 fm.take_focus_event(),
1174 Some(FocusEvent::FocusMoved { from: 1, to: 2 })
1175 );
1176
1177 fm.blur();
1178 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 2 }));
1179 }
1180
1181 #[test]
1182 fn trap_prevents_focus_outside_group() {
1183 let mut fm = FocusManager::new();
1184 fm.graph_mut().insert(node(1, 0));
1185 fm.graph_mut().insert(node(2, 1));
1186 fm.graph_mut().insert(node(3, 2));
1187 fm.create_group(5, vec![1, 2]);
1188
1189 fm.push_trap(5);
1190 assert_eq!(fm.current(), Some(1));
1191
1192 assert!(fm.focus(3).is_none());
1194 assert_ne!(fm.current(), Some(3));
1195 }
1196
1197 fn spatial_node(id: FocusId, x: u16, y: u16, w: u16, h: u16, tab: i32) -> FocusNode {
1200 FocusNode::new(id, Rect::new(x, y, w, h)).with_tab_index(tab)
1201 }
1202
1203 #[test]
1204 fn navigate_spatial_fallback() {
1205 let mut fm = FocusManager::new();
1206 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1208 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
1209
1210 fm.focus(1);
1211 assert!(fm.navigate(NavDirection::Right));
1212 assert_eq!(fm.current(), Some(2));
1213
1214 assert!(fm.navigate(NavDirection::Left));
1215 assert_eq!(fm.current(), Some(1));
1216 }
1217
1218 #[test]
1219 fn navigate_explicit_edge_overrides_spatial() {
1220 let mut fm = FocusManager::new();
1221 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1222 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);
1227
1228 fm.focus(1);
1229 assert!(fm.navigate(NavDirection::Right));
1230 assert_eq!(fm.current(), Some(3));
1231 }
1232
1233 #[test]
1234 fn navigate_spatial_respects_trap() {
1235 let mut fm = FocusManager::new();
1236 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1237 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
1238 fm.graph_mut().insert(spatial_node(3, 40, 0, 10, 3, 2));
1239
1240 fm.create_group(1, vec![1, 2]);
1242 fm.focus(2);
1243 fm.push_trap(1);
1244
1245 assert!(!fm.navigate(NavDirection::Right));
1247 assert_eq!(fm.current(), Some(2));
1248 }
1249
1250 #[test]
1251 fn navigate_spatial_grid_round_trip() {
1252 let mut fm = FocusManager::new();
1253 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1255 fm.graph_mut().insert(spatial_node(2, 20, 0, 10, 3, 1));
1256 fm.graph_mut().insert(spatial_node(3, 0, 6, 10, 3, 2));
1257 fm.graph_mut().insert(spatial_node(4, 20, 6, 10, 3, 3));
1258
1259 fm.focus(1);
1260
1261 assert!(fm.navigate(NavDirection::Right));
1263 assert_eq!(fm.current(), Some(2));
1264
1265 assert!(fm.navigate(NavDirection::Down));
1266 assert_eq!(fm.current(), Some(4));
1267
1268 assert!(fm.navigate(NavDirection::Left));
1269 assert_eq!(fm.current(), Some(3));
1270
1271 assert!(fm.navigate(NavDirection::Up));
1272 assert_eq!(fm.current(), Some(1));
1273 }
1274
1275 #[test]
1276 fn navigate_spatial_no_candidate() {
1277 let mut fm = FocusManager::new();
1278 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1279 fm.focus(1);
1280
1281 assert!(!fm.navigate(NavDirection::Right));
1283 assert!(!fm.navigate(NavDirection::Up));
1284 assert_eq!(fm.current(), Some(1));
1285 }
1286
1287 #[test]
1290 fn new_manager_has_no_focus() {
1291 let fm = FocusManager::new();
1292 assert_eq!(fm.current(), None);
1293 assert!(!fm.is_trapped());
1294 }
1295
1296 #[test]
1297 fn default_and_new_are_equivalent() {
1298 let a = FocusManager::new();
1299 let b = FocusManager::default();
1300 assert_eq!(a.current(), b.current());
1301 assert_eq!(a.is_trapped(), b.is_trapped());
1302 assert_eq!(a.host_focused(), b.host_focused());
1303 }
1304
1305 #[test]
1308 fn is_focused_returns_true_for_current() {
1309 let mut fm = FocusManager::new();
1310 fm.graph_mut().insert(node(1, 0));
1311 fm.focus(1);
1312 assert!(fm.is_focused(1));
1313 assert!(!fm.is_focused(2));
1314 }
1315
1316 #[test]
1317 fn is_focused_returns_false_when_no_focus() {
1318 let fm = FocusManager::new();
1319 assert!(!fm.is_focused(1));
1320 }
1321
1322 #[test]
1325 fn focus_non_existent_node_returns_none() {
1326 let mut fm = FocusManager::new();
1327 assert!(fm.focus(999).is_none());
1328 assert_eq!(fm.current(), None);
1329 }
1330
1331 #[test]
1332 fn focus_already_focused_returns_same_id() {
1333 let mut fm = FocusManager::new();
1334 fm.graph_mut().insert(node(1, 0));
1335 fm.focus(1);
1336 assert_eq!(fm.focus(1), Some(1));
1338 assert_eq!(fm.current(), Some(1));
1339 }
1340
1341 #[test]
1344 fn blur_when_no_focus_returns_none() {
1345 let mut fm = FocusManager::new();
1346 assert_eq!(fm.blur(), None);
1347 }
1348
1349 #[test]
1350 fn blur_generates_focus_lost_event() {
1351 let mut fm = FocusManager::new();
1352 fm.graph_mut().insert(node(1, 0));
1353 fm.focus(1);
1354 let _ = fm.take_focus_event(); fm.blur();
1356 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1357 }
1358
1359 #[test]
1362 fn focus_first_selects_lowest_tab_index() {
1363 let mut fm = FocusManager::new();
1364 fm.graph_mut().insert(node(3, 2));
1365 fm.graph_mut().insert(node(1, 0));
1366 fm.graph_mut().insert(node(2, 1));
1367
1368 assert!(fm.focus_first());
1369 assert_eq!(fm.current(), Some(1));
1370 }
1371
1372 #[test]
1373 fn focus_last_selects_highest_tab_index() {
1374 let mut fm = FocusManager::new();
1375 fm.graph_mut().insert(node(1, 0));
1376 fm.graph_mut().insert(node(2, 1));
1377 fm.graph_mut().insert(node(3, 2));
1378
1379 assert!(fm.focus_last());
1380 assert_eq!(fm.current(), Some(3));
1381 }
1382
1383 #[test]
1384 fn focus_first_on_empty_graph_returns_false() {
1385 let mut fm = FocusManager::new();
1386 assert!(!fm.focus_first());
1387 }
1388
1389 #[test]
1390 fn focus_last_on_empty_graph_returns_false() {
1391 let mut fm = FocusManager::new();
1392 assert!(!fm.focus_last());
1393 }
1394
1395 #[test]
1398 fn focus_next_wraps_at_end() {
1399 let mut fm = FocusManager::new();
1400 fm.graph_mut().insert(node(1, 0));
1401 fm.graph_mut().insert(node(2, 1));
1402
1403 fm.focus(2);
1404 assert!(fm.focus_next()); assert_eq!(fm.current(), Some(1));
1406 }
1407
1408 #[test]
1409 fn focus_prev_wraps_at_start() {
1410 let mut fm = FocusManager::new();
1411 fm.graph_mut().insert(node(1, 0));
1412 fm.graph_mut().insert(node(2, 1));
1413
1414 fm.focus(1);
1415 assert!(fm.focus_prev()); assert_eq!(fm.current(), Some(2));
1417 }
1418
1419 #[test]
1420 fn focus_next_with_no_current_selects_first() {
1421 let mut fm = FocusManager::new();
1422 fm.graph_mut().insert(node(1, 0));
1423 fm.graph_mut().insert(node(2, 1));
1424
1425 assert!(fm.focus_next());
1426 assert_eq!(fm.current(), Some(1));
1427 }
1428
1429 #[test]
1430 fn focus_prev_with_no_current_selects_last() {
1431 let mut fm = FocusManager::new();
1432 fm.graph_mut().insert(node(1, 0));
1433 fm.graph_mut().insert(node(2, 1));
1434
1435 assert!(fm.focus_prev());
1436 assert_eq!(fm.current(), Some(2));
1437 }
1438
1439 #[test]
1440 fn focus_prev_with_stale_current_selects_last() {
1441 let mut fm = FocusManager::new();
1442 fm.graph_mut().insert(node(1, 0));
1443 fm.graph_mut().insert(node(2, 1));
1444 fm.graph_mut().insert(node(3, 2));
1445
1446 fm.focus(2);
1447 let _ = fm.graph_mut().remove(2);
1448
1449 assert!(fm.focus_prev());
1450 assert_eq!(fm.current(), Some(3));
1451 }
1452
1453 #[test]
1454 fn focus_next_on_empty_returns_false() {
1455 let mut fm = FocusManager::new();
1456 assert!(!fm.focus_next());
1457 }
1458
1459 #[test]
1462 fn focus_back_on_empty_history_returns_false() {
1463 let mut fm = FocusManager::new();
1464 fm.graph_mut().insert(node(1, 0));
1465 fm.focus(1);
1466 assert!(!fm.focus_back());
1467 }
1468
1469 #[test]
1470 fn clear_history_prevents_back() {
1471 let mut fm = FocusManager::new();
1472 fm.graph_mut().insert(node(1, 0));
1473 fm.graph_mut().insert(node(2, 1));
1474
1475 fm.focus(1);
1476 fm.focus(2);
1477 fm.clear_history();
1478 assert!(!fm.focus_back());
1479 assert_eq!(fm.current(), Some(2));
1480 }
1481
1482 #[test]
1483 fn focus_back_skips_removed_nodes() {
1484 let mut fm = FocusManager::new();
1485 fm.graph_mut().insert(node(1, 0));
1486 fm.graph_mut().insert(node(2, 1));
1487 fm.graph_mut().insert(node(3, 2));
1488
1489 fm.focus(1);
1490 fm.focus(2);
1491 fm.focus(3);
1492
1493 let _ = fm.graph_mut().remove(2);
1495
1496 assert!(fm.focus_back());
1498 assert_eq!(fm.current(), Some(1));
1499 }
1500
1501 #[test]
1504 fn create_group_filters_non_focusable() {
1505 let mut fm = FocusManager::new();
1506 fm.graph_mut().insert(node(1, 0));
1507 fm.create_group(1, vec![1, 999]);
1509
1510 let group = fm.groups.get(&1).unwrap();
1511 assert_eq!(group.members.len(), 1);
1512 assert!(group.contains(1));
1513 }
1514
1515 #[test]
1516 fn add_to_group_creates_group_if_needed() {
1517 let mut fm = FocusManager::new();
1518 fm.graph_mut().insert(node(1, 0));
1519 fm.add_to_group(42, 1);
1520 assert!(fm.groups.contains_key(&42));
1521 assert!(fm.groups.get(&42).unwrap().contains(1));
1522 }
1523
1524 #[test]
1525 fn add_to_group_skips_unfocusable() {
1526 let mut fm = FocusManager::new();
1527 fm.add_to_group(1, 999); if let Some(group) = fm.groups.get(&1) {
1530 assert!(!group.contains(999));
1531 }
1532 }
1533
1534 #[test]
1535 fn add_to_group_no_duplicates() {
1536 let mut fm = FocusManager::new();
1537 fm.graph_mut().insert(node(1, 0));
1538 fm.add_to_group(1, 1);
1539 fm.add_to_group(1, 1);
1540 assert_eq!(fm.groups.get(&1).unwrap().members.len(), 1);
1541 }
1542
1543 #[test]
1544 fn remove_from_group() {
1545 let mut fm = FocusManager::new();
1546 fm.graph_mut().insert(node(1, 0));
1547 fm.graph_mut().insert(node(2, 1));
1548 fm.create_group(1, vec![1, 2]);
1549 fm.remove_from_group(1, 1);
1550 assert!(!fm.groups.get(&1).unwrap().contains(1));
1551 assert!(fm.groups.get(&1).unwrap().contains(2));
1552 }
1553
1554 #[test]
1555 fn removing_focused_member_from_active_trap_refocuses_remaining_member() {
1556 let mut fm = FocusManager::new();
1557 fm.graph_mut().insert(node(1, 0));
1558 fm.graph_mut().insert(node(2, 1));
1559 fm.graph_mut().insert(node(3, 2));
1560 fm.create_group(1, vec![1, 2]);
1561
1562 fm.focus(2);
1563 assert!(fm.push_trap(1));
1564 assert_eq!(fm.current(), Some(2));
1565
1566 fm.remove_from_group(1, 2);
1567 assert_eq!(fm.current(), Some(1));
1568 assert!(fm.is_trapped());
1569 assert!(fm.focus(3).is_none());
1570 assert_eq!(fm.current(), Some(1));
1571 }
1572
1573 #[test]
1574 fn removing_last_member_from_active_trap_allows_focus_escape() {
1575 let mut fm = FocusManager::new();
1576 fm.graph_mut().insert(node(1, 0));
1577 fm.graph_mut().insert(node(2, 1));
1578 fm.create_group(1, vec![1]);
1579
1580 fm.focus(1);
1581 assert!(fm.push_trap(1));
1582 assert!(fm.is_trapped());
1583
1584 fm.remove_from_group(1, 1);
1585 assert!(!fm.is_trapped());
1586 assert_eq!(fm.current(), Some(1));
1587 assert_eq!(fm.focus(2), Some(1));
1588 assert_eq!(fm.current(), Some(2));
1589 }
1590
1591 #[test]
1592 fn removing_active_inner_trap_member_falls_back_to_outer_trap() {
1593 let mut fm = FocusManager::new();
1594 fm.graph_mut().insert(node(1, 0));
1595 fm.graph_mut().insert(node(2, 1));
1596 fm.graph_mut().insert(node(3, 2));
1597 fm.create_group(10, vec![1, 2]);
1598 fm.create_group(20, vec![3]);
1599
1600 fm.focus(1);
1601 assert!(fm.push_trap(10));
1602 assert!(fm.push_trap(20));
1603 assert_eq!(fm.current(), Some(3));
1604
1605 fm.remove_from_group(20, 3);
1606 assert!(fm.is_trapped());
1607 assert_eq!(fm.current(), Some(1));
1608 assert!(fm.focus(3).is_none());
1609 assert_eq!(fm.focus(2), Some(1));
1610 assert_eq!(fm.current(), Some(2));
1611 }
1612
1613 #[test]
1614 fn adding_member_to_invalidated_trap_restores_confinement() {
1615 let mut fm = FocusManager::new();
1616 fm.graph_mut().insert(node(1, 0));
1617 fm.graph_mut().insert(node(2, 1));
1618 fm.create_group(1, vec![1]);
1619
1620 fm.focus(1);
1621 assert!(fm.push_trap(1));
1622
1623 fm.remove_from_group(1, 1);
1624 assert!(!fm.is_trapped());
1625
1626 fm.add_to_group(1, 2);
1627 assert!(fm.is_trapped());
1628 assert_eq!(fm.current(), Some(2));
1629 assert!(fm.focus(1).is_none());
1630 }
1631
1632 #[test]
1633 fn remove_from_nonexistent_group_is_noop() {
1634 let mut fm = FocusManager::new();
1635 fm.remove_from_group(999, 1); }
1637
1638 #[test]
1639 fn remove_group_deletes_group() {
1640 let mut fm = FocusManager::new();
1641 fm.graph_mut().insert(node(1, 0));
1642 fm.create_group(42, vec![1]);
1643
1644 fm.remove_group(42);
1645 assert!(!fm.groups.contains_key(&42));
1646 }
1647
1648 #[test]
1649 fn remove_group_from_active_inner_trap_falls_back_to_outer_trap() {
1650 let mut fm = FocusManager::new();
1651 fm.graph_mut().insert(node(1, 0));
1652 fm.graph_mut().insert(node(2, 1));
1653 fm.graph_mut().insert(node(3, 2));
1654 fm.create_group(10, vec![1, 2]);
1655 fm.create_group(20, vec![3]);
1656
1657 fm.focus(1);
1658 assert!(fm.push_trap(10));
1659 assert!(fm.push_trap(20));
1660 assert_eq!(fm.current(), Some(3));
1661
1662 fm.remove_group(20);
1663 assert!(fm.is_trapped());
1664 assert_eq!(fm.current(), Some(1));
1665 assert!(fm.focus(3).is_none());
1666 }
1667
1668 #[test]
1669 fn remove_group_clears_stale_trap_entries() {
1670 let mut fm = FocusManager::new();
1671 fm.graph_mut().insert(node(1, 0));
1672 fm.create_group(10, vec![1]);
1673
1674 fm.focus(1);
1675 assert!(fm.push_trap(10));
1676 assert!(fm.is_trapped());
1677
1678 fm.remove_group(10);
1679 assert!(!fm.is_trapped());
1680 assert!(!fm.pop_trap());
1681 }
1682
1683 #[test]
1684 fn remove_group_blurs_invalid_current_when_no_fallback_exists() {
1685 let mut fm = FocusManager::new();
1686 fm.graph_mut().insert(node(1, 0));
1687 fm.create_group(10, vec![1]);
1688 fm.focus(1);
1689
1690 let _ = fm.graph_mut().remove(1);
1691 fm.remove_group(10);
1692
1693 assert_eq!(fm.current(), None);
1694 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1695 }
1696
1697 #[test]
1700 fn focus_group_with_wrap() {
1701 let group = FocusGroup::new(1, vec![1, 2]).with_wrap(false);
1702 assert!(!group.wrap);
1703 }
1704
1705 #[test]
1706 fn focus_group_with_exit_key() {
1707 let group = FocusGroup::new(1, vec![]).with_exit_key(KeyCode::Escape);
1708 assert_eq!(group.exit_key, Some(KeyCode::Escape));
1709 }
1710
1711 #[test]
1712 fn focus_group_default_wraps() {
1713 let group = FocusGroup::new(1, vec![]);
1714 assert!(group.wrap);
1715 assert_eq!(group.exit_key, None);
1716 }
1717
1718 #[test]
1721 fn nested_traps() {
1722 let mut fm = FocusManager::new();
1723 fm.graph_mut().insert(node(1, 0));
1724 fm.graph_mut().insert(node(2, 1));
1725 fm.graph_mut().insert(node(3, 2));
1726 fm.graph_mut().insert(node(4, 3));
1727
1728 fm.create_group(10, vec![1, 2]);
1729 fm.create_group(20, vec![3, 4]);
1730
1731 fm.focus(1);
1732 fm.push_trap(10);
1733 assert!(fm.is_trapped());
1734
1735 fm.push_trap(20);
1736 assert_eq!(fm.current(), Some(3));
1738
1739 fm.pop_trap();
1741 assert!(fm.is_trapped());
1743
1744 fm.pop_trap();
1746 assert!(!fm.is_trapped());
1747 }
1748
1749 #[test]
1750 fn trap_push_pop_does_not_pollute_focus_history() {
1751 let mut fm = FocusManager::new();
1752 fm.graph_mut().insert(node(1, 0));
1753 fm.graph_mut().insert(node(2, 1));
1754 fm.graph_mut().insert(node(3, 2));
1755 fm.create_group(10, vec![2]);
1756
1757 fm.focus(1);
1758 fm.focus(3);
1759 assert_eq!(fm.current(), Some(3));
1760
1761 assert!(fm.push_trap(10));
1762 assert_eq!(fm.current(), Some(2));
1763
1764 assert!(fm.pop_trap());
1765 assert_eq!(fm.current(), Some(3));
1766
1767 assert!(fm.focus_back());
1768 assert_eq!(fm.current(), Some(1));
1769 assert!(!fm.focus_back());
1770 }
1771
1772 #[test]
1773 fn pop_trap_restores_none_when_modal_opened_without_focus() {
1774 let mut fm = FocusManager::new();
1775 fm.graph_mut().insert(node(1, 0));
1776 fm.create_group(10, vec![1]);
1777
1778 assert!(fm.push_trap(10));
1779 assert_eq!(fm.current(), Some(1));
1780
1781 assert!(fm.pop_trap());
1782 assert_eq!(fm.current(), None);
1783 assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1784 }
1785
1786 #[test]
1787 fn pop_trap_on_empty_returns_false() {
1788 let mut fm = FocusManager::new();
1789 assert!(!fm.pop_trap());
1790 }
1791
1792 #[test]
1793 fn push_trap_rejects_missing_group() {
1794 let mut fm = FocusManager::new();
1795 fm.graph_mut().insert(node(1, 0));
1796 fm.focus(1);
1797
1798 assert!(!fm.push_trap(999));
1800 assert!(!fm.is_trapped());
1801 assert_eq!(fm.current(), Some(1));
1803 }
1804
1805 #[test]
1806 fn push_trap_rejects_empty_group() {
1807 let mut fm = FocusManager::new();
1808 fm.graph_mut().insert(node(1, 0));
1809 fm.focus(1);
1810
1811 fm.create_group(42, vec![]);
1813 assert!(!fm.push_trap(42));
1814 assert!(!fm.is_trapped());
1815 assert_eq!(fm.current(), Some(1));
1817 }
1818
1819 #[test]
1820 fn push_trap_autofocuses_negative_tabindex_member_when_group_has_no_tabbable_nodes() {
1821 let mut fm = FocusManager::new();
1822 fm.graph_mut().insert(node(1, 0));
1823 fm.graph_mut().insert(node(2, -1));
1824 fm.focus(1);
1825
1826 fm.create_group(42, vec![2]);
1827 assert!(fm.push_trap(42));
1828 assert!(fm.is_trapped());
1829 assert_eq!(fm.current(), Some(2));
1830 }
1831
1832 #[test]
1833 fn push_trap_blurred_restores_negative_tabindex_member_on_focus_gain() {
1834 let mut fm = FocusManager::new();
1835 fm.graph_mut().insert(node(1, 0));
1836 fm.graph_mut().insert(node(2, -1));
1837 fm.focus(1);
1838 assert!(fm.apply_host_focus(false));
1839
1840 fm.create_group(42, vec![2]);
1841 assert!(fm.push_trap(42));
1842 assert_eq!(fm.current(), None);
1843
1844 assert!(fm.apply_host_focus(true));
1845 assert_eq!(fm.current(), Some(2));
1846 }
1847
1848 #[test]
1849 fn push_trap_retargets_when_current_group_member_becomes_unfocusable() {
1850 let mut fm = FocusManager::new();
1851 fm.graph_mut().insert(node(1, 0));
1852 fm.graph_mut().insert(node(2, 1));
1853 fm.focus(1);
1854 fm.create_group(10, vec![1, 2]);
1855
1856 fm.graph_mut().insert(node(1, 0).with_focusable(false));
1857
1858 assert!(fm.push_trap(10));
1859 assert_eq!(fm.current(), Some(2));
1860 }
1861
1862 #[test]
1865 fn take_focus_event_clears_it() {
1866 let mut fm = FocusManager::new();
1867 fm.graph_mut().insert(node(1, 0));
1868 fm.focus(1);
1869
1870 assert!(fm.take_focus_event().is_some());
1871 assert!(fm.take_focus_event().is_none());
1872 }
1873
1874 #[test]
1875 fn focus_event_accessor() {
1876 let mut fm = FocusManager::new();
1877 fm.graph_mut().insert(node(1, 0));
1878 fm.focus(1);
1879
1880 assert_eq!(fm.focus_event(), Some(&FocusEvent::FocusGained { id: 1 }));
1881 }
1882
1883 #[test]
1886 fn navigate_direction_with_no_current_returns_false() {
1887 let mut fm = FocusManager::new();
1888 fm.graph_mut().insert(spatial_node(1, 0, 0, 10, 3, 0));
1889 assert!(!fm.navigate(NavDirection::Right));
1890 }
1891
1892 #[test]
1895 fn graph_accessor_returns_reference() {
1896 let mut fm = FocusManager::new();
1897 fm.graph_mut().insert(node(1, 0));
1898 assert!(fm.graph().get(1).is_some());
1899 }
1900
1901 #[test]
1904 fn default_indicator_is_reverse() {
1905 let fm = FocusManager::new();
1906 assert!(fm.indicator().is_visible());
1907 assert_eq!(
1908 fm.indicator().kind(),
1909 crate::focus::FocusIndicatorKind::StyleOverlay
1910 );
1911 }
1912
1913 #[test]
1914 fn set_indicator() {
1915 let mut fm = FocusManager::new();
1916 fm.set_indicator(crate::focus::FocusIndicator::underline());
1917 assert_eq!(
1918 fm.indicator().kind(),
1919 crate::focus::FocusIndicatorKind::Underline
1920 );
1921 }
1922
1923 #[test]
1926 fn focus_change_count_increments() {
1927 let mut fm = FocusManager::new();
1928 fm.graph_mut().insert(node(1, 0));
1929 fm.graph_mut().insert(node(2, 1));
1930
1931 assert_eq!(fm.focus_change_count(), 0);
1932
1933 fm.focus(1);
1934 assert_eq!(fm.focus_change_count(), 1);
1935
1936 fm.focus(2);
1937 assert_eq!(fm.focus_change_count(), 2);
1938
1939 fm.blur();
1940 assert_eq!(fm.focus_change_count(), 3);
1941 }
1942
1943 #[test]
1944 fn focus_change_count_zero_on_no_op() {
1945 let mut fm = FocusManager::new();
1946 fm.graph_mut().insert(node(1, 0));
1947 fm.focus(1);
1948 assert_eq!(fm.focus_change_count(), 1);
1949
1950 fm.focus(1);
1952 assert_eq!(fm.focus_change_count(), 1);
1953 }
1954
1955 #[test]
1956 fn focus_back_increments_focus_change_count() {
1957 let mut fm = FocusManager::new();
1958 fm.graph_mut().insert(node(1, 0));
1959 fm.graph_mut().insert(node(2, 1));
1960
1961 fm.focus(1);
1962 fm.focus(2);
1963 assert_eq!(fm.focus_change_count(), 2);
1964
1965 assert!(fm.focus_back());
1966 assert_eq!(fm.current(), Some(1));
1967 assert_eq!(fm.focus_change_count(), 3);
1968 }
1969}