Skip to main content

ftui_widgets/focus/
manager.rs

1#![forbid(unsafe_code)]
2
3//! Focus manager coordinating focus traversal, history, and traps.
4//!
5//! The manager tracks the current focus, maintains a navigation history,
6//! and enforces focus traps for modal dialogs. It also provides a
7//! configurable [`FocusIndicator`] for styling the focused widget.
8
9use ahash::AHashMap;
10
11use ftui_core::event::KeyCode;
12
13use super::indicator::FocusIndicator;
14use super::spatial;
15use super::{FocusGraph, FocusId, NavDirection};
16
17/// Focus change events emitted by the manager.
18#[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/// Group of focusable widgets for tab traversal.
26#[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/// Active focus trap (e.g., modal).
63#[derive(Debug, Clone, Copy)]
64pub struct FocusTrap {
65    pub group_id: u32,
66    pub return_focus: Option<FocusId>,
67}
68
69/// Central focus coordinator.
70///
71/// Tracks focus state, navigation history, focus traps (for modals),
72/// and focus indicator styling. Emits [`FocusEvent`]s on focus changes.
73#[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    /// Running count of focus changes for metrics.
85    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    /// Create a new focus manager.
107    #[must_use]
108    pub fn new() -> Self {
109        Self::default()
110    }
111
112    /// Access the underlying focus graph.
113    #[must_use]
114    pub fn graph(&self) -> &FocusGraph {
115        &self.graph
116    }
117
118    /// Mutably access the underlying focus graph.
119    pub fn graph_mut(&mut self) -> &mut FocusGraph {
120        &mut self.graph
121    }
122
123    /// Get currently focused widget.
124    #[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    /// Check if a widget is focused.
143    #[must_use]
144    pub fn is_focused(&self, id: FocusId) -> bool {
145        self.current == Some(id)
146    }
147
148    /// Set focus to widget, returns previous focus.
149    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    /// Remove focus from current widget.
162    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    /// Apply host/window focus state to the widget focus graph.
174    ///
175    /// Deterministic policy:
176    /// - `focused = false` clears current focus.
177    /// - `focused = true` restores the last valid logical focus target when
178    ///   possible, otherwise falls back to the first allowed node (respecting
179    ///   active traps).
180    ///
181    /// Returns `true` when focus state changed.
182    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    /// Move focus in direction.
227    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                // Explicit edges take precedence; fall back to spatial navigation.
236                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    /// Move to next in tab order.
252    pub fn focus_next(&mut self) -> bool {
253        self.move_in_tab_order(true)
254    }
255
256    /// Move to previous in tab order.
257    pub fn focus_prev(&mut self) -> bool {
258        self.move_in_tab_order(false)
259    }
260
261    /// Focus first focusable widget.
262    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    /// Focus last focusable widget.
271    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    /// Go back to previous focus.
280    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                // Set focus directly without pushing current to history
291                // (going back shouldn't create a forward entry).
292                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    /// Clear focus history.
306    pub fn clear_history(&mut self) {
307        self.history.clear();
308    }
309
310    /// Push focus trap (for modals).
311    ///
312    /// If the group doesn't exist or has no focusable members, the trap is
313    /// **not** pushed and the method returns `false`. This prevents a deadlock
314    /// where `allowed_by_trap` would deny focus to every widget because the
315    /// group is empty/missing.
316    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    /// Pop focus trap, restore previous focus.
337    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    /// Check if focus is currently trapped.
395    #[must_use]
396    pub fn is_trapped(&self) -> bool {
397        self.active_trap_group().is_some()
398    }
399
400    /// Remove all active focus traps without changing focus groups.
401    pub fn clear_traps(&mut self) {
402        self.trap_stack.clear();
403    }
404
405    /// Create focus group.
406    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    /// Add widget to group.
419    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    /// Remove widget from group.
434    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    /// Remove an entire focus group.
443    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    /// Get the last focus event.
452    #[must_use]
453    pub fn focus_event(&self) -> Option<&FocusEvent> {
454        self.last_event.as_ref()
455    }
456
457    /// Take and clear the last focus event.
458    #[must_use]
459    pub fn take_focus_event(&mut self) -> Option<FocusEvent> {
460        self.last_event.take()
461    }
462
463    /// Get the focus indicator configuration.
464    #[inline]
465    #[must_use]
466    pub fn indicator(&self) -> &FocusIndicator {
467        &self.indicator
468    }
469
470    /// Set the focus indicator configuration.
471    pub fn set_indicator(&mut self, indicator: FocusIndicator) {
472        self.indicator = indicator;
473    }
474
475    /// Total number of focus changes since creation (for metrics).
476    #[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// =========================================================================
864// Tests
865// =========================================================================
866
867#[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        // Attempt to focus outside trap should fail.
1191        assert!(fm.focus(3).is_none());
1192        assert_ne!(fm.current(), Some(3));
1193    }
1194
1195    // --- Spatial navigation integration ---
1196
1197    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        // Two nodes side by side — no explicit edges.
1205        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)); // spatially right
1221        fm.graph_mut().insert(spatial_node(3, 40, 0, 10, 3, 2)); // further right
1222
1223        // Explicit edge overrides spatial: Right from 1 goes to 3, not 2.
1224        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        // Trap to group containing only 1 and 2.
1239        fm.create_group(1, vec![1, 2]);
1240        fm.focus(2);
1241        fm.push_trap(1);
1242
1243        // Spatial would find 3 to the right of 2, but trap blocks it.
1244        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        // 2x2 grid.
1252        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        // Navigate around the grid: right, down, left, up — back to start.
1260        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        // No other nodes, spatial should return false.
1280        assert!(!fm.navigate(NavDirection::Right));
1281        assert!(!fm.navigate(NavDirection::Up));
1282        assert_eq!(fm.current(), Some(1));
1283    }
1284
1285    // --- FocusManager construction ---
1286
1287    #[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    // --- is_focused ---
1304
1305    #[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    // --- focus edge cases ---
1321
1322    #[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        // Focusing same node returns current (early exit)
1335        assert_eq!(fm.focus(1), Some(1));
1336        assert_eq!(fm.current(), Some(1));
1337    }
1338
1339    // --- blur ---
1340
1341    #[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(); // clear
1353        fm.blur();
1354        assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1355    }
1356
1357    // --- focus_first / focus_last ---
1358
1359    #[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    // --- Tab wrapping ---
1394
1395    #[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()); // wraps
1403        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()); // wraps
1414        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    // --- History ---
1458
1459    #[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        // Remove node 2 from graph
1492        let _ = fm.graph_mut().remove(2);
1493
1494        // focus_back should skip 2 and go to 1
1495        assert!(fm.focus_back());
1496        assert_eq!(fm.current(), Some(1));
1497    }
1498
1499    // --- Groups ---
1500
1501    #[test]
1502    fn create_group_filters_non_focusable() {
1503        let mut fm = FocusManager::new();
1504        fm.graph_mut().insert(node(1, 0));
1505        // Node 999 doesn't exist in the graph
1506        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); // 999 not in graph
1526        // Group may or may not exist, but if it does, 999 is not in it
1527        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); // should not panic
1634    }
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    // --- FocusGroup ---
1696
1697    #[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    // --- Trap stack ---
1717
1718    #[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        // Should be in inner trap, focused on first of group 20
1735        assert_eq!(fm.current(), Some(3));
1736
1737        // Pop inner trap
1738        fm.pop_trap();
1739        // Should still be trapped (in group 10)
1740        assert!(fm.is_trapped());
1741
1742        // Pop outer trap
1743        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        // Group 999 doesn't exist — push_trap must refuse.
1797        assert!(!fm.push_trap(999));
1798        assert!(!fm.is_trapped());
1799        // Focus should remain unchanged (no deadlock).
1800        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        // Create group with no members.
1810        fm.create_group(42, vec![]);
1811        assert!(!fm.push_trap(42));
1812        assert!(!fm.is_trapped());
1813        // Focus should remain unchanged (no deadlock).
1814        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    // --- Focus events ---
1861
1862    #[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    // --- Navigate with no current ---
1882
1883    #[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    // --- graph accessors ---
1891
1892    #[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    // --- Focus indicator ---
1900
1901    #[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    // --- Focus change count ---
1922
1923    #[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        // Focusing the same widget is a no-op
1949        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}