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        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// =========================================================================
866// Tests
867// =========================================================================
868
869#[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        // Attempt to focus outside trap should fail.
1193        assert!(fm.focus(3).is_none());
1194        assert_ne!(fm.current(), Some(3));
1195    }
1196
1197    // --- Spatial navigation integration ---
1198
1199    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        // Two nodes side by side — no explicit edges.
1207        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)); // spatially right
1223        fm.graph_mut().insert(spatial_node(3, 40, 0, 10, 3, 2)); // further right
1224
1225        // Explicit edge overrides spatial: Right from 1 goes to 3, not 2.
1226        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        // Trap to group containing only 1 and 2.
1241        fm.create_group(1, vec![1, 2]);
1242        fm.focus(2);
1243        fm.push_trap(1);
1244
1245        // Spatial would find 3 to the right of 2, but trap blocks it.
1246        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        // 2x2 grid.
1254        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        // Navigate around the grid: right, down, left, up — back to start.
1262        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        // No other nodes, spatial should return false.
1282        assert!(!fm.navigate(NavDirection::Right));
1283        assert!(!fm.navigate(NavDirection::Up));
1284        assert_eq!(fm.current(), Some(1));
1285    }
1286
1287    // --- FocusManager construction ---
1288
1289    #[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    // --- is_focused ---
1306
1307    #[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    // --- focus edge cases ---
1323
1324    #[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        // Focusing same node returns current (early exit)
1337        assert_eq!(fm.focus(1), Some(1));
1338        assert_eq!(fm.current(), Some(1));
1339    }
1340
1341    // --- blur ---
1342
1343    #[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(); // clear
1355        fm.blur();
1356        assert_eq!(fm.take_focus_event(), Some(FocusEvent::FocusLost { id: 1 }));
1357    }
1358
1359    // --- focus_first / focus_last ---
1360
1361    #[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    // --- Tab wrapping ---
1396
1397    #[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()); // wraps
1405        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()); // wraps
1416        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    // --- History ---
1460
1461    #[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        // Remove node 2 from graph
1494        let _ = fm.graph_mut().remove(2);
1495
1496        // focus_back should skip 2 and go to 1
1497        assert!(fm.focus_back());
1498        assert_eq!(fm.current(), Some(1));
1499    }
1500
1501    // --- Groups ---
1502
1503    #[test]
1504    fn create_group_filters_non_focusable() {
1505        let mut fm = FocusManager::new();
1506        fm.graph_mut().insert(node(1, 0));
1507        // Node 999 doesn't exist in the graph
1508        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); // 999 not in graph
1528        // Group may or may not exist, but if it does, 999 is not in it
1529        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); // should not panic
1636    }
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    // --- FocusGroup ---
1698
1699    #[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    // --- Trap stack ---
1719
1720    #[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        // Should be in inner trap, focused on first of group 20
1737        assert_eq!(fm.current(), Some(3));
1738
1739        // Pop inner trap
1740        fm.pop_trap();
1741        // Should still be trapped (in group 10)
1742        assert!(fm.is_trapped());
1743
1744        // Pop outer trap
1745        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        // Group 999 doesn't exist — push_trap must refuse.
1799        assert!(!fm.push_trap(999));
1800        assert!(!fm.is_trapped());
1801        // Focus should remain unchanged (no deadlock).
1802        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        // Create group with no members.
1812        fm.create_group(42, vec![]);
1813        assert!(!fm.push_trap(42));
1814        assert!(!fm.is_trapped());
1815        // Focus should remain unchanged (no deadlock).
1816        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    // --- Focus events ---
1863
1864    #[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    // --- Navigate with no current ---
1884
1885    #[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    // --- graph accessors ---
1893
1894    #[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    // --- Focus indicator ---
1902
1903    #[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    // --- Focus change count ---
1924
1925    #[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        // Focusing the same widget is a no-op
1951        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}