Skip to main content

ftui_widgets/modal/
focus_integration.rs

1#![forbid(unsafe_code)]
2
3//! Focus-aware modal integration for automatic focus trap management.
4//!
5//! This module provides `FocusAwareModalStack`, which combines [`ModalStack`]
6//! with [`FocusManager`] integration for automatic focus trapping when modals
7//! are opened and focus restoration when they close.
8//!
9//! # Invariants
10//!
11//! 1. **Auto-focus**: When a modal opens with a focus group, focus moves to the
12//!    first focusable element in that group.
13//! 2. **Focus trap**: Tab navigation is constrained to the modal's focus group.
14//! 3. **Focus restoration**: When a modal closes, focus returns to where it was
15//!    before the modal opened.
16//! 4. **LIFO ordering**: Focus traps follow modal stack ordering (nested modals
17//!    restore to the correct previous state).
18//!
19//! # Failure Modes
20//!
21//! - If the focus group has no focusable members, focus remains unchanged.
22//! - If the original focus target is removed during modal display, focus moves
23//!   to the first available focusable element.
24//! - Focus trap with an empty group allows focus to escape (graceful degradation).
25//!
26//! # Example
27//!
28//! ```ignore
29//! use ftui_widgets::focus::FocusManager;
30//! use ftui_widgets::modal::{ModalStack, WidgetModalEntry};
31//! use ftui_widgets::modal::focus_integration::FocusAwareModalStack;
32//!
33//! let mut modals = FocusAwareModalStack::new();
34//!
35//! // Push modal with focus group members
36//! let focus_ids = vec![ok_button_id, cancel_button_id];
37//! let modal_id = modals.push_with_trap(
38//!     Box::new(WidgetModalEntry::new(dialog)),
39//!     focus_ids,
40//! );
41//!
42//! // Handle event (focus trap active, Escape closes and restores focus)
43//! if let Some(result) = modals.handle_event(&event, None) {
44//!     // Modal closed, focus already restored
45//! }
46//! ```
47
48use std::sync::atomic::{AtomicU32, Ordering};
49
50use ftui_core::event::Event;
51use ftui_core::geometry::Rect;
52use ftui_render::frame::{Frame, HitTestResult};
53
54use crate::focus::{FocusId, FocusManager};
55use crate::modal::stack::FocusTrapSpec;
56use crate::modal::{ModalId, ModalResult, ModalStack, StackModal};
57
58/// Global counter for unique focus group IDs.
59static FOCUS_GROUP_COUNTER: AtomicU32 = AtomicU32::new(1_000_000);
60
61/// Generate a unique focus group ID.
62pub(super) fn next_focus_group_id(focus_manager: &FocusManager) -> u32 {
63    loop {
64        let group_id = FOCUS_GROUP_COUNTER.fetch_add(1, Ordering::Relaxed);
65        if !focus_manager.has_group(group_id) {
66            return group_id;
67        }
68    }
69}
70
71pub(super) struct ModalFocusCoordinator<'a> {
72    stack: &'a mut ModalStack,
73    focus_manager: &'a mut FocusManager,
74    base_focus: &'a mut Option<Option<FocusId>>,
75}
76
77impl<'a> ModalFocusCoordinator<'a> {
78    pub(super) fn new(
79        stack: &'a mut ModalStack,
80        focus_manager: &'a mut FocusManager,
81        base_focus: &'a mut Option<Option<FocusId>>,
82    ) -> Self {
83        Self {
84            stack,
85            focus_manager,
86            base_focus,
87        }
88    }
89
90    pub(super) fn push_modal_with_trap<F>(
91        &mut self,
92        modal: Box<dyn StackModal>,
93        focusable_ids: Option<Vec<FocusId>>,
94        trap_enabled: bool,
95        allocate_group_id: F,
96    ) -> ModalId
97    where
98        F: FnOnce(&FocusManager) -> u32,
99    {
100        let base_focus = if self.focus_manager.host_focused() {
101            self.focus_manager.current()
102        } else {
103            self.focus_manager.deferred_focus_target()
104        };
105        let was_trapped = self.focus_manager.is_trapped();
106        let focus_group_id = if trap_enabled {
107            if let Some(ids) = focusable_ids {
108                let group_id = allocate_group_id(self.focus_manager);
109                let has_declared_members = !ids.is_empty();
110                self.focus_manager
111                    .create_group_preserving_members(group_id, ids);
112                let trapped = self.focus_manager.push_trap(group_id);
113                if !trapped && !has_declared_members {
114                    self.focus_manager.remove_group(group_id);
115                    None
116                } else {
117                    if !was_trapped && trapped {
118                        *self.base_focus = Some(base_focus);
119                    }
120                    Some(group_id)
121                }
122            } else {
123                None
124            }
125        } else {
126            None
127        };
128
129        let modal_id = self.stack.push_with_focus(modal, focus_group_id);
130        if focus_group_id.is_some() {
131            let _ = self.stack.set_focus_return_focus(modal_id, base_focus);
132        }
133        modal_id
134    }
135
136    pub(super) fn pop_modal(&mut self) -> Option<ModalResult> {
137        let result = self.stack.pop()?;
138        self.handle_closed_result(&result);
139        Some(result)
140    }
141
142    pub(super) fn pop_modal_by_id(&mut self, id: ModalId) -> Option<ModalResult> {
143        if self.stack.top_id() == Some(id) {
144            return self.pop_modal();
145        }
146
147        if let Some(group_id) = self.stack.focus_group_id(id) {
148            let removed_members = self.focus_manager.group_members(group_id);
149            let removed_group_active = self.group_has_focusable_member(group_id);
150            let removed_effective_return_focus = self
151                .effective_focus_return_focuses_in_order_skipping(None)
152                .into_iter()
153                .find_map(|(candidate_group_id, return_focus)| {
154                    (candidate_group_id == group_id).then_some(return_focus)
155                });
156
157            if let Some((upper_modal_id, _)) = self.stack.next_focus_modal_after(id) {
158                let should_retarget = if removed_group_active {
159                    true
160                } else {
161                    let upper_return_focus = self
162                        .stack
163                        .focus_modal_specs_in_order()
164                        .into_iter()
165                        .find_map(|(modal_id, trap)| {
166                            (modal_id == upper_modal_id).then_some(trap.return_focus)
167                        })
168                        .flatten();
169                    !self.return_focus_remains_valid_after_removing_group(
170                        upper_modal_id,
171                        upper_return_focus,
172                        group_id,
173                        &removed_members,
174                    )
175                };
176
177                if should_retarget && let Some(return_focus) = removed_effective_return_focus {
178                    let _ = self
179                        .stack
180                        .set_focus_return_focus(upper_modal_id, return_focus);
181                }
182            }
183        }
184
185        let result = self.stack.pop_id_with_restore_retarget(id, false)?;
186        if let Some(group_id) = result.focus_group_id {
187            let closing_members = self.focus_manager.group_members(group_id);
188            self.focus_manager.remove_group_without_repair(group_id);
189            self.focus_manager
190                .clear_deferred_focus_if_excluded(&closing_members);
191            self.rebuild_focus_traps();
192            self.focus_manager
193                .repair_focus_after_excluding_ids(&closing_members);
194            self.refresh_inactive_modal_return_focus_targets();
195        }
196        Some(result)
197    }
198
199    pub(super) fn pop_all_modals(&mut self) -> Vec<ModalResult> {
200        let results = self.stack.pop_all();
201        let mut removed_group = false;
202        let mut removed_members = Vec::new();
203        for result in &results {
204            if let Some(group_id) = result.focus_group_id {
205                removed_members.extend(self.focus_manager.group_members(group_id));
206                self.focus_manager.remove_group_without_repair(group_id);
207                removed_group = true;
208            }
209        }
210        if removed_group {
211            self.focus_manager
212                .clear_deferred_focus_if_excluded(&removed_members);
213            self.rebuild_focus_traps();
214            self.focus_manager
215                .repair_focus_after_excluding_ids(&removed_members);
216            self.refresh_inactive_modal_return_focus_targets();
217        }
218        results
219    }
220
221    pub(super) fn handle_modal_event(
222        &mut self,
223        event: &Event,
224        hit: Option<HitTestResult>,
225    ) -> Option<ModalResult> {
226        if let Event::Focus(focused) = event {
227            if *focused && self.stack.is_empty() && self.base_focus.is_some() {
228                let deferred_focus = self.focus_manager.deferred_focus_target();
229                self.focus_manager.set_host_focused(true);
230                if let Some(id) = deferred_focus {
231                    *self.base_focus = Some(Some(id));
232                }
233                self.rebuild_focus_traps();
234            } else {
235                self.focus_manager.apply_host_focus(*focused);
236            }
237            if *focused {
238                self.refresh_inactive_modal_return_focus_targets();
239            }
240        }
241        let result = self.stack.handle_event(event, hit)?;
242        self.handle_closed_result(&result);
243        Some(result)
244    }
245
246    pub(super) fn rebuild_focus_traps(&mut self) {
247        let (trap_specs, trailing_failed_restore) = self.collapsed_focus_trap_specs();
248        let had_active_trap_before = self.focus_manager.is_trapped();
249        let preserved_logical_target = self.focus_manager.logical_focus_target();
250        let activation_base_focus = if self.focus_manager.host_focused() {
251            self.focus_manager.current()
252        } else {
253            self.focus_manager.deferred_focus_target()
254        };
255        self.focus_manager.clear_traps();
256
257        if !self.focus_manager.host_focused() {
258            let mut has_active_trap = false;
259            if self.focus_manager.current().is_some() {
260                let _ = self.focus_manager.blur();
261            }
262
263            for trap in trap_specs.iter().copied() {
264                has_active_trap |= self
265                    .focus_manager
266                    .push_trap_with_return_focus(trap.group_id, trap.return_focus);
267            }
268
269            if has_active_trap && !had_active_trap_before && self.base_focus.is_none() {
270                *self.base_focus = Some(activation_base_focus);
271            }
272
273            if !has_active_trap {
274                let restore_target = (!had_active_trap_before)
275                    .then_some(preserved_logical_target)
276                    .flatten()
277                    .map(Some)
278                    .or(trailing_failed_restore)
279                    .or(*self.base_focus);
280                if let Some(target) = restore_target {
281                    self.focus_manager.replace_deferred_focus_target(target);
282                }
283            } else if self.focus_manager.logical_focus_target().is_none()
284                && let Some(target) = trailing_failed_restore
285            {
286                self.focus_manager.replace_deferred_focus_target(target);
287            }
288            return;
289        }
290
291        let mut has_active_trap = false;
292        for trap in trap_specs.iter().copied() {
293            has_active_trap |= self
294                .focus_manager
295                .push_trap_with_return_focus(trap.group_id, trap.return_focus);
296        }
297
298        if has_active_trap && !had_active_trap_before && self.base_focus.is_none() {
299            *self.base_focus = Some(activation_base_focus);
300        }
301
302        if !has_active_trap {
303            let restore_target = (!had_active_trap_before)
304                .then_some(preserved_logical_target)
305                .flatten()
306                .map(Some)
307                .or(trailing_failed_restore)
308                .or(*self.base_focus);
309            match restore_target {
310                Some(Some(base_focus)) => {
311                    let _ = self.focus_manager.focus_without_history(base_focus);
312                }
313                Some(None) if self.focus_manager.current().is_some() => {
314                    let _ = self.focus_manager.blur();
315                }
316                Some(None) => {}
317                None => {}
318            }
319
320            if matches!(restore_target, Some(Some(base_focus)) if self.focus_manager.current() != Some(base_focus))
321            {
322                self.focus_manager.focus_first_without_history_for_restore();
323            }
324            if self.focus_manager.current().is_some_and(|id| {
325                self.focus_manager
326                    .graph()
327                    .get(id)
328                    .map(|node| !node.is_focusable)
329                    .unwrap_or(true)
330            }) {
331                let _ = self.focus_manager.blur();
332            }
333            *self.base_focus = None;
334            return;
335        }
336
337        if self.focus_manager.logical_focus_target().is_none()
338            && let Some(Some(target)) = trailing_failed_restore
339        {
340            let _ = self.focus_manager.focus_without_history(target);
341        }
342
343        let _ = self.focus_manager.apply_host_focus(true);
344    }
345
346    fn return_focus_remains_valid_after_removing_group(
347        &self,
348        upper_modal_id: ModalId,
349        return_focus: Option<FocusId>,
350        removed_group_id: u32,
351        removed_members: &[FocusId],
352    ) -> bool {
353        let mut surviving_lower_active_group = None;
354        for (modal_id, trap) in self.stack.focus_modal_specs_in_order() {
355            if modal_id == upper_modal_id {
356                break;
357            }
358            if trap.group_id == removed_group_id {
359                continue;
360            }
361            if self.group_has_focusable_member(trap.group_id) {
362                surviving_lower_active_group = Some(trap.group_id);
363            }
364        }
365
366        if let Some(group_id) = surviving_lower_active_group {
367            return self.focus_target_in_group(return_focus, group_id);
368        }
369
370        match return_focus {
371            None => true,
372            Some(id) => self.focus_target_is_focusable(Some(id)) && !removed_members.contains(&id),
373        }
374    }
375
376    fn effective_focus_return_focuses_in_order_skipping(
377        &self,
378        skipped_modal_id: Option<ModalId>,
379    ) -> Vec<(u32, Option<FocusId>)> {
380        let mut effective = Vec::new();
381        let mut lower_active_group = None;
382        let mut lower_fallback_return_focus = None;
383
384        for (_, trap) in self
385            .stack
386            .focus_modal_specs_in_order()
387            .into_iter()
388            .filter(|(modal_id, _)| Some(*modal_id) != skipped_modal_id)
389        {
390            let effective_return_focus = if let Some(group_id) = lower_active_group {
391                if self.focus_target_in_group(trap.return_focus, group_id) {
392                    trap.return_focus
393                } else {
394                    lower_fallback_return_focus.unwrap_or(trap.return_focus)
395                }
396            } else if self.focus_target_is_focusable(trap.return_focus) {
397                trap.return_focus
398            } else {
399                lower_fallback_return_focus.unwrap_or(trap.return_focus)
400            };
401
402            effective.push((trap.group_id, effective_return_focus));
403            lower_fallback_return_focus = Some(effective_return_focus);
404            if self.group_has_focusable_member(trap.group_id) {
405                lower_active_group = Some(trap.group_id);
406            }
407        }
408
409        effective
410    }
411
412    fn collapsed_focus_trap_specs(&self) -> (Vec<FocusTrapSpec>, Option<Option<FocusId>>) {
413        let mut collapsed = Vec::new();
414        let mut trailing_failed_restore = None;
415
416        for (group_id, effective_return_focus) in
417            self.effective_focus_return_focuses_in_order_skipping(None)
418        {
419            if self.group_has_focusable_member(group_id) {
420                collapsed.push(FocusTrapSpec {
421                    group_id,
422                    return_focus: effective_return_focus,
423                });
424                trailing_failed_restore = None;
425            } else {
426                trailing_failed_restore = Some(effective_return_focus);
427            }
428        }
429
430        (collapsed, trailing_failed_restore)
431    }
432
433    fn group_has_focusable_member(&self, group_id: u32) -> bool {
434        self.focus_manager
435            .group_members(group_id)
436            .into_iter()
437            .any(|id| self.focus_target_is_focusable(Some(id)))
438    }
439
440    fn focus_target_is_focusable(&self, target: Option<FocusId>) -> bool {
441        target.is_some_and(|id| {
442            self.focus_manager
443                .graph()
444                .get(id)
445                .map(|node| node.is_focusable)
446                .unwrap_or(false)
447        })
448    }
449
450    fn focus_target_in_group(&self, target: Option<FocusId>, group_id: u32) -> bool {
451        let Some(target) = target else {
452            return false;
453        };
454        self.focus_target_is_focusable(Some(target))
455            && self.focus_manager.group_members(group_id).contains(&target)
456    }
457
458    fn handle_closed_result(&mut self, result: &ModalResult) {
459        if let Some(group_id) = result.focus_group_id {
460            self.close_focus_group(group_id);
461        }
462    }
463
464    fn close_focus_group(&mut self, group_id: u32) {
465        let closing_members = self.focus_manager.group_members(group_id);
466        if self.group_has_focusable_member(group_id) {
467            self.focus_manager.pop_trap();
468            self.focus_manager.remove_group(group_id);
469        } else {
470            self.focus_manager.remove_group_without_repair(group_id);
471        }
472        self.focus_manager
473            .repair_focus_after_excluding_ids(&closing_members);
474        if !self.focus_manager.is_trapped() && self.focus_manager.host_focused() {
475            *self.base_focus = None;
476        }
477        self.refresh_inactive_modal_return_focus_targets();
478    }
479
480    pub(super) fn refresh_inactive_modal_return_focus_targets(&mut self) {
481        let logical_target = self.focus_manager.logical_focus_target();
482        let focus_modals = self.stack.focus_modal_specs_in_order();
483
484        let topmost_active_index = focus_modals
485            .iter()
486            .rposition(|(_, trap)| self.group_has_focusable_member(trap.group_id));
487
488        let start_index = topmost_active_index.map_or(0, |index| index + 1);
489        for (modal_id, trap) in focus_modals.into_iter().skip(start_index) {
490            if self.group_has_focusable_member(trap.group_id) {
491                continue;
492            }
493            let _ = self.stack.set_focus_return_focus(modal_id, logical_target);
494        }
495
496        self.refresh_active_modal_return_focus_targets_for_invalid_lower_selections(
497            &self.stack.focus_modal_specs_in_order(),
498            topmost_active_index,
499        );
500    }
501
502    fn refresh_active_modal_return_focus_targets_for_invalid_lower_selections(
503        &mut self,
504        focus_modals: &[(ModalId, FocusTrapSpec)],
505        topmost_active_index: Option<usize>,
506    ) {
507        let Some(topmost_active_index) = topmost_active_index else {
508            return;
509        };
510
511        for upper_idx in 1..=topmost_active_index {
512            let (_, lower_trap) = focus_modals[upper_idx - 1];
513            let (upper_modal_id, upper_trap) = focus_modals[upper_idx];
514
515            if !self.group_has_focusable_member(lower_trap.group_id)
516                || self.focus_target_in_group(upper_trap.return_focus, lower_trap.group_id)
517            {
518                continue;
519            }
520
521            let replacement = self.first_focusable_in_group(lower_trap.group_id);
522            let _ = self
523                .stack
524                .set_focus_return_focus(upper_modal_id, replacement);
525        }
526    }
527
528    fn first_focusable_in_group(&self, group_id: u32) -> Option<FocusId> {
529        self.focus_manager.group_primary_focus_target(group_id)
530    }
531}
532
533/// Modal stack with integrated focus management.
534///
535/// This wrapper provides automatic focus trapping when modals open and
536/// focus restoration when they close. It manages both the modal stack
537/// and focus manager in a coordinated way.
538///
539/// # Invariants
540///
541/// - Focus trap stack depth equals the number of modals with focus groups.
542/// - Each modal's focus group ID is unique and not reused.
543/// - Pop operations always call `pop_trap` for modals with focus groups.
544pub struct FocusAwareModalStack {
545    stack: ModalStack,
546    focus_manager: FocusManager,
547    base_focus: Option<Option<FocusId>>,
548}
549
550impl Default for FocusAwareModalStack {
551    fn default() -> Self {
552        Self::new()
553    }
554}
555
556impl FocusAwareModalStack {
557    /// Create a new focus-aware modal stack.
558    pub fn new() -> Self {
559        Self {
560            stack: ModalStack::new(),
561            focus_manager: FocusManager::new(),
562            base_focus: None,
563        }
564    }
565
566    /// Create from existing stack and focus manager.
567    ///
568    /// Use this when you already have a `FocusManager` in your application
569    /// and want to integrate modal focus trapping.
570    ///
571    /// The provided manager must not already have active modal traps. This
572    /// wrapper only tracks traps for modals it owns, so starting from an
573    /// already-trapped manager would make later rebuild/pop operations
574    /// silently corrupt unrelated trap state.
575    pub fn with_focus_manager(focus_manager: FocusManager) -> Self {
576        assert!(
577            !focus_manager.is_trapped(),
578            "FocusAwareModalStack requires a FocusManager without active traps",
579        );
580        Self {
581            stack: ModalStack::new(),
582            focus_manager,
583            base_focus: None,
584        }
585    }
586
587    // --- Modal Stack Delegation ---
588
589    /// Push a modal without focus trapping.
590    ///
591    /// The modal will be rendered and receive events, but focus is not managed.
592    pub fn push(&mut self, modal: Box<dyn StackModal>) -> ModalId {
593        self.stack.push(modal)
594    }
595
596    /// Push a modal with automatic focus trapping.
597    ///
598    /// # Parameters
599    /// - `modal`: The modal content
600    /// - `focusable_ids`: The focus IDs of elements inside the modal
601    ///
602    /// # Behavior
603    /// 1. Creates a focus group with the provided IDs
604    /// 2. Pushes a focus trap (saving current focus)
605    /// 3. Moves focus to the first element in the group
606    pub fn push_with_trap(
607        &mut self,
608        modal: Box<dyn StackModal>,
609        focusable_ids: Vec<FocusId>,
610    ) -> ModalId {
611        ModalFocusCoordinator::new(
612            &mut self.stack,
613            &mut self.focus_manager,
614            &mut self.base_focus,
615        )
616        .push_modal_with_trap(modal, Some(focusable_ids), true, next_focus_group_id)
617    }
618
619    /// Pop the top modal.
620    ///
621    /// If the modal had a focus group, the focus trap is popped and
622    /// focus is restored to where it was before the modal opened.
623    pub fn pop(&mut self) -> Option<ModalResult> {
624        ModalFocusCoordinator::new(
625            &mut self.stack,
626            &mut self.focus_manager,
627            &mut self.base_focus,
628        )
629        .pop_modal()
630    }
631
632    /// Pop a specific modal by ID.
633    ///
634    pub fn pop_id(&mut self, id: ModalId) -> Option<ModalResult> {
635        ModalFocusCoordinator::new(
636            &mut self.stack,
637            &mut self.focus_manager,
638            &mut self.base_focus,
639        )
640        .pop_modal_by_id(id)
641    }
642
643    /// Pop all modals, restoring focus to the original state.
644    pub fn pop_all(&mut self) -> Vec<ModalResult> {
645        ModalFocusCoordinator::new(
646            &mut self.stack,
647            &mut self.focus_manager,
648            &mut self.base_focus,
649        )
650        .pop_all_modals()
651    }
652
653    /// Handle an event, routing to the top modal.
654    ///
655    /// If the modal closes (via Escape, backdrop click, etc.), the focus
656    /// trap is automatically popped and focus is restored. For mouse events,
657    /// pass the provenance-aware result from [`Frame::hit_test_detailed`].
658    pub fn handle_event(
659        &mut self,
660        event: &Event,
661        hit: Option<HitTestResult>,
662    ) -> Option<ModalResult> {
663        ModalFocusCoordinator::new(
664            &mut self.stack,
665            &mut self.focus_manager,
666            &mut self.base_focus,
667        )
668        .handle_modal_event(event, hit)
669    }
670
671    /// Render all modals.
672    pub fn render(&self, frame: &mut Frame, screen: Rect) {
673        self.stack.render(frame, screen);
674    }
675
676    /// Perform a direct focus-graph mutation and automatically resynchronize modal focus state.
677    pub fn with_focus_graph_mut<R>(
678        &mut self,
679        f: impl FnOnce(&mut crate::focus::FocusGraph) -> R,
680    ) -> R {
681        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
682            f(self.focus_manager.graph_mut())
683        }));
684        let had_invalid_current = self.focus_manager.current().is_some_and(|id| {
685            self.focus_manager
686                .graph()
687                .get(id)
688                .map(|node| !node.is_focusable)
689                .unwrap_or(true)
690        });
691        self.resync_focus_state();
692        let needs_post_resync_restore = had_invalid_current
693            && self.focus_manager.host_focused()
694            && self.focus_manager.current().is_none_or(|id| {
695                self.focus_manager
696                    .graph()
697                    .get(id)
698                    .map(|node| !node.is_focusable)
699                    .unwrap_or(true)
700            });
701        if needs_post_resync_restore {
702            self.focus_manager.restore_focus_after_invalid_current();
703            self.resync_inactive_modal_return_focus_targets();
704        }
705        match result {
706            Ok(result) => result,
707            Err(payload) => std::panic::resume_unwind(payload),
708        }
709    }
710
711    /// Focus a specific target through the wrapped focus manager.
712    pub fn focus(&mut self, id: FocusId) -> Option<FocusId> {
713        let previous = self.focus_manager.focus(id);
714        if previous.is_some()
715            || self.focus_manager.current() == Some(id)
716            || self.focus_manager.logical_focus_target() == Some(id)
717        {
718            self.resync_inactive_modal_return_focus_targets();
719        }
720        previous
721    }
722
723    fn resync_focus_state(&mut self) {
724        let mut coordinator = ModalFocusCoordinator::new(
725            &mut self.stack,
726            &mut self.focus_manager,
727            &mut self.base_focus,
728        );
729        coordinator.rebuild_focus_traps();
730        coordinator.refresh_inactive_modal_return_focus_targets();
731    }
732
733    fn resync_inactive_modal_return_focus_targets(&mut self) {
734        ModalFocusCoordinator::new(
735            &mut self.stack,
736            &mut self.focus_manager,
737            &mut self.base_focus,
738        )
739        .refresh_inactive_modal_return_focus_targets();
740    }
741
742    // --- State Queries ---
743
744    /// Check if the modal stack is empty.
745    #[inline]
746    pub fn is_empty(&self) -> bool {
747        self.stack.is_empty()
748    }
749
750    /// Get the number of open modals.
751    #[inline]
752    pub fn depth(&self) -> usize {
753        self.stack.depth()
754    }
755
756    /// Check if focus is currently trapped in a modal.
757    #[inline]
758    pub fn is_focus_trapped(&self) -> bool {
759        self.focus_manager.is_trapped()
760    }
761
762    /// Get a reference to the underlying modal stack.
763    pub fn stack(&self) -> &ModalStack {
764        &self.stack
765    }
766
767    /// Get a reference to the focus manager.
768    pub fn focus_manager(&self) -> &FocusManager {
769        &self.focus_manager
770    }
771
772    #[cfg(test)]
773    fn stack_mut(&mut self) -> &mut ModalStack {
774        &mut self.stack
775    }
776
777    #[cfg(test)]
778    fn focus_manager_mut(&mut self) -> &mut FocusManager {
779        &mut self.focus_manager
780    }
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786    use crate::Widget;
787    use crate::focus::FocusNode;
788    use crate::modal::WidgetModalEntry;
789    use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
790    use ftui_core::geometry::Rect;
791
792    #[derive(Debug, Clone)]
793    struct StubWidget;
794
795    impl Widget for StubWidget {
796        fn render(&self, _area: Rect, _frame: &mut Frame) {}
797    }
798
799    fn make_focus_node(id: FocusId) -> FocusNode {
800        FocusNode::new(id, Rect::new(0, 0, 10, 3)).with_tab_index(id as i32)
801    }
802
803    #[test]
804    fn push_with_trap_creates_focus_trap() {
805        let mut modals = FocusAwareModalStack::new();
806
807        // Add focusable nodes
808        modals
809            .focus_manager_mut()
810            .graph_mut()
811            .insert(make_focus_node(1));
812        modals
813            .focus_manager_mut()
814            .graph_mut()
815            .insert(make_focus_node(2));
816        modals
817            .focus_manager_mut()
818            .graph_mut()
819            .insert(make_focus_node(3));
820
821        // Focus node 3 before opening modal
822        modals.focus_manager_mut().focus(3);
823        assert_eq!(modals.focus_manager().current(), Some(3));
824
825        // Push modal with trap containing nodes 1 and 2
826        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
827
828        // Focus should now be on node 1 (first in group)
829        assert!(modals.is_focus_trapped());
830        assert_eq!(modals.focus_manager().current(), Some(1));
831    }
832
833    #[test]
834    fn pop_restores_focus() {
835        let mut modals = FocusAwareModalStack::new();
836
837        // Add focusable nodes
838        modals
839            .focus_manager_mut()
840            .graph_mut()
841            .insert(make_focus_node(1));
842        modals
843            .focus_manager_mut()
844            .graph_mut()
845            .insert(make_focus_node(2));
846        modals
847            .focus_manager_mut()
848            .graph_mut()
849            .insert(make_focus_node(3));
850
851        // Focus node 3 before opening modal
852        modals.focus_manager_mut().focus(3);
853
854        // Push modal with trap
855        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
856        assert_eq!(modals.focus_manager().current(), Some(1));
857
858        // Pop modal - focus should return to node 3
859        modals.pop();
860        assert!(!modals.is_focus_trapped());
861        assert_eq!(modals.focus_manager().current(), Some(3));
862    }
863
864    #[test]
865    fn pop_discards_closed_modal_focus_history() {
866        let mut modals = FocusAwareModalStack::new();
867        for id in 1..=3 {
868            modals
869                .focus_manager_mut()
870                .graph_mut()
871                .insert(make_focus_node(id));
872        }
873
874        modals.focus(1);
875        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
876        assert_eq!(modals.focus(3), Some(2));
877
878        let result = modals.pop();
879        assert!(result.is_some());
880        assert_eq!(modals.focus_manager().current(), Some(1));
881        assert!(!modals.focus_manager_mut().focus_back());
882        assert_eq!(modals.focus_manager().current(), Some(1));
883    }
884
885    #[test]
886    fn pop_skips_closed_modal_focus_ids_when_background_focus_disappears() {
887        let mut modals = FocusAwareModalStack::new();
888        modals
889            .focus_manager_mut()
890            .graph_mut()
891            .insert(make_focus_node(1));
892        modals
893            .focus_manager_mut()
894            .graph_mut()
895            .insert(make_focus_node(50));
896        modals
897            .focus_manager_mut()
898            .graph_mut()
899            .insert(make_focus_node(100));
900
901        modals.focus_manager_mut().focus(100);
902        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
903        let _ = modals.focus_manager_mut().graph_mut().remove(100);
904
905        modals.pop();
906        assert_eq!(modals.focus_manager().current(), Some(50));
907        assert!(!modals.is_focus_trapped());
908    }
909
910    #[test]
911    fn nested_modals_restore_correctly() {
912        let mut modals = FocusAwareModalStack::new();
913
914        // Add focusable nodes
915        for id in 1..=6 {
916            modals
917                .focus_manager_mut()
918                .graph_mut()
919                .insert(make_focus_node(id));
920        }
921
922        // Initial focus
923        modals.focus_manager_mut().focus(1);
924
925        // First modal traps to nodes 2, 3
926        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
927        assert_eq!(modals.focus_manager().current(), Some(2));
928
929        // Second modal traps to nodes 4, 5, 6
930        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5, 6]);
931        assert_eq!(modals.focus_manager().current(), Some(4));
932
933        // Pop second modal - back to first modal's focus (node 2)
934        modals.pop();
935        assert_eq!(modals.focus_manager().current(), Some(2));
936
937        // Pop first modal - back to original focus (node 1)
938        modals.pop();
939        assert_eq!(modals.focus_manager().current(), Some(1));
940        assert!(!modals.is_focus_trapped());
941    }
942
943    #[test]
944    fn pop_restores_none_when_modal_opened_without_focus() {
945        let mut modals = FocusAwareModalStack::new();
946        modals
947            .focus_manager_mut()
948            .graph_mut()
949            .insert(make_focus_node(1));
950
951        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
952        assert_eq!(modals.focus_manager().current(), Some(1));
953
954        modals.pop();
955        assert_eq!(modals.focus_manager().current(), None);
956        assert!(!modals.is_focus_trapped());
957    }
958
959    #[test]
960    fn resync_focus_state_recovers_after_manual_stack_mutation() {
961        let mut modals = FocusAwareModalStack::new();
962        modals
963            .focus_manager_mut()
964            .graph_mut()
965            .insert(make_focus_node(1));
966        modals
967            .focus_manager_mut()
968            .graph_mut()
969            .insert(make_focus_node(2));
970        modals
971            .focus_manager_mut()
972            .graph_mut()
973            .insert(make_focus_node(100));
974
975        modals.focus_manager_mut().focus(100);
976        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
977        assert!(modals.is_focus_trapped());
978        assert_eq!(modals.focus_manager().current(), Some(1));
979
980        let result = modals.stack_mut().pop();
981        assert!(result.is_some());
982        assert!(modals.is_focus_trapped());
983
984        modals.resync_focus_state();
985        assert!(!modals.is_focus_trapped());
986        assert_eq!(modals.focus_manager().current(), Some(100));
987    }
988
989    #[test]
990    fn handle_event_escape_restores_focus() {
991        let mut modals = FocusAwareModalStack::new();
992
993        // Add focusable nodes
994        modals
995            .focus_manager_mut()
996            .graph_mut()
997            .insert(make_focus_node(1));
998        modals
999            .focus_manager_mut()
1000            .graph_mut()
1001            .insert(make_focus_node(2));
1002
1003        // Focus node 2
1004        modals.focus_manager_mut().focus(2);
1005
1006        // Push modal
1007        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
1008        assert_eq!(modals.focus_manager().current(), Some(1));
1009
1010        // Escape closes modal
1011        let escape = Event::Key(KeyEvent {
1012            code: KeyCode::Escape,
1013            modifiers: Modifiers::empty(),
1014            kind: KeyEventKind::Press,
1015        });
1016
1017        let result = modals.handle_event(&escape, None);
1018        assert!(result.is_some());
1019        assert_eq!(modals.focus_manager().current(), Some(2));
1020    }
1021
1022    #[test]
1023    fn handle_event_focus_loss_blurs_current_focus() {
1024        let mut modals = FocusAwareModalStack::new();
1025        modals
1026            .focus_manager_mut()
1027            .graph_mut()
1028            .insert(make_focus_node(1));
1029        modals.focus_manager_mut().focus(1);
1030        let _ = modals.focus_manager_mut().take_focus_event();
1031
1032        let result = modals.handle_event(&Event::Focus(false), None);
1033        assert!(result.is_none());
1034        assert_eq!(modals.focus_manager().current(), None);
1035        assert_eq!(
1036            modals.focus_manager_mut().take_focus_event(),
1037            Some(crate::focus::FocusEvent::FocusLost { id: 1 })
1038        );
1039    }
1040
1041    #[test]
1042    fn handle_event_focus_gain_restores_trapped_focus() {
1043        let mut modals = FocusAwareModalStack::new();
1044        modals
1045            .focus_manager_mut()
1046            .graph_mut()
1047            .insert(make_focus_node(1));
1048        modals
1049            .focus_manager_mut()
1050            .graph_mut()
1051            .insert(make_focus_node(2));
1052        modals
1053            .focus_manager_mut()
1054            .graph_mut()
1055            .insert(make_focus_node(3));
1056        modals.focus_manager_mut().focus(3);
1057
1058        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
1059        assert_eq!(modals.focus_manager().current(), Some(1));
1060
1061        let _ = modals.handle_event(&Event::Focus(false), None);
1062        assert_eq!(modals.focus_manager().current(), None);
1063
1064        let result = modals.handle_event(&Event::Focus(true), None);
1065        assert!(result.is_none());
1066        assert_eq!(modals.focus_manager().current(), Some(1));
1067    }
1068
1069    #[test]
1070    fn push_with_trap_autofocuses_negative_tabindex_member_when_modal_has_no_tabbable_nodes() {
1071        let mut modals = FocusAwareModalStack::new();
1072
1073        modals
1074            .focus_manager_mut()
1075            .graph_mut()
1076            .insert(make_focus_node(1));
1077        modals
1078            .focus_manager_mut()
1079            .graph_mut()
1080            .insert(FocusNode::new(2, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
1081        modals.focus_manager_mut().focus(1);
1082
1083        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1084
1085        assert!(modals.is_focus_trapped());
1086        assert_eq!(modals.focus_manager().current(), Some(2));
1087    }
1088
1089    #[test]
1090    fn push_with_trap_blurred_restores_negative_tabindex_member_on_focus_gain() {
1091        let mut modals = FocusAwareModalStack::new();
1092
1093        modals
1094            .focus_manager_mut()
1095            .graph_mut()
1096            .insert(make_focus_node(1));
1097        modals
1098            .focus_manager_mut()
1099            .graph_mut()
1100            .insert(FocusNode::new(2, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
1101        modals.focus_manager_mut().focus(1);
1102        let _ = modals.handle_event(&Event::Focus(false), None);
1103
1104        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1105
1106        assert!(modals.is_focus_trapped());
1107        assert_eq!(modals.focus_manager().current(), None);
1108
1109        let _ = modals.handle_event(&Event::Focus(true), None);
1110        assert_eq!(modals.focus_manager().current(), Some(2));
1111    }
1112
1113    #[test]
1114    fn push_without_trap_no_focus_change() {
1115        let mut modals = FocusAwareModalStack::new();
1116
1117        // Add focusable nodes
1118        modals
1119            .focus_manager_mut()
1120            .graph_mut()
1121            .insert(make_focus_node(1));
1122        modals
1123            .focus_manager_mut()
1124            .graph_mut()
1125            .insert(make_focus_node(2));
1126
1127        // Focus node 2
1128        modals.focus_manager_mut().focus(2);
1129
1130        // Push modal without trap
1131        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
1132
1133        // Focus should not change
1134        assert!(!modals.is_focus_trapped());
1135        assert_eq!(modals.focus_manager().current(), Some(2));
1136    }
1137
1138    #[test]
1139    fn pop_all_restores_all_focus() {
1140        let mut modals = FocusAwareModalStack::new();
1141
1142        // Add focusable nodes
1143        for id in 1..=4 {
1144            modals
1145                .focus_manager_mut()
1146                .graph_mut()
1147                .insert(make_focus_node(id));
1148        }
1149
1150        // Initial focus
1151        modals.focus_manager_mut().focus(1);
1152
1153        // Push multiple modals
1154        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1155        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1156        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
1157
1158        assert_eq!(modals.depth(), 3);
1159        assert_eq!(modals.focus_manager().current(), Some(4));
1160
1161        // Pop all
1162        let results = modals.pop_all();
1163        assert_eq!(results.len(), 3);
1164        assert!(modals.is_empty());
1165        assert!(!modals.is_focus_trapped());
1166        assert_eq!(modals.focus_manager().current(), Some(1));
1167    }
1168
1169    #[test]
1170    fn pop_all_restores_base_focus_without_intermediate_hop() {
1171        let mut modals = FocusAwareModalStack::new();
1172        modals.with_focus_graph_mut(|graph| {
1173            for id in 1..=5 {
1174                graph.insert(make_focus_node(id));
1175            }
1176        });
1177
1178        modals.focus(1);
1179        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1180        modals.focus(3);
1181        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
1182        modals.focus(5);
1183        let _ = modals.focus_manager_mut().take_focus_event();
1184        let before = modals.focus_manager().focus_change_count();
1185
1186        let results = modals.pop_all();
1187
1188        assert_eq!(results.len(), 2);
1189        assert_eq!(modals.focus_manager().current(), Some(1));
1190        assert_eq!(
1191            modals.focus_manager_mut().take_focus_event(),
1192            Some(crate::focus::FocusEvent::FocusMoved { from: 5, to: 1 })
1193        );
1194        assert_eq!(modals.focus_manager().focus_change_count(), before + 1);
1195        assert!(!modals.is_focus_trapped());
1196    }
1197
1198    #[test]
1199    fn pop_id_restores_none_when_last_modal_opened_without_focus() {
1200        let mut modals = FocusAwareModalStack::new();
1201        modals
1202            .focus_manager_mut()
1203            .graph_mut()
1204            .insert(make_focus_node(1));
1205
1206        let modal_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
1207        assert_eq!(modals.focus_manager().current(), Some(1));
1208
1209        let _ = modals.pop_id(modal_id);
1210        assert_eq!(modals.focus_manager().current(), None);
1211        assert!(!modals.is_focus_trapped());
1212    }
1213
1214    #[test]
1215    fn pop_id_rebuild_preserves_unfocused_base_state_for_remaining_modal() {
1216        let mut modals = FocusAwareModalStack::new();
1217        modals
1218            .focus_manager_mut()
1219            .graph_mut()
1220            .insert(make_focus_node(1));
1221        modals
1222            .focus_manager_mut()
1223            .graph_mut()
1224            .insert(make_focus_node(2));
1225
1226        let lower_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
1227        let upper_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1228        assert_eq!(modals.focus_manager().current(), Some(2));
1229
1230        let removed = modals.pop_id(lower_id);
1231        assert_eq!(removed.map(|result| result.id), Some(lower_id));
1232        assert_eq!(modals.focus_manager().current(), Some(2));
1233        assert!(modals.is_focus_trapped());
1234
1235        let closed = modals.pop();
1236        assert_eq!(closed.map(|result| result.id), Some(upper_id));
1237        assert_eq!(modals.focus_manager().current(), None);
1238        assert!(!modals.is_focus_trapped());
1239    }
1240
1241    #[test]
1242    fn tab_navigation_trapped_in_modal() {
1243        let mut modals = FocusAwareModalStack::new();
1244
1245        // Add focusable nodes
1246        for id in 1..=5 {
1247            modals
1248                .focus_manager_mut()
1249                .graph_mut()
1250                .insert(make_focus_node(id));
1251        }
1252
1253        // Push modal with nodes 2 and 3
1254        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1255
1256        // Focus should be on 2
1257        assert_eq!(modals.focus_manager().current(), Some(2));
1258
1259        // Tab forward should go to 3
1260        modals.focus_manager_mut().focus_next();
1261        assert_eq!(modals.focus_manager().current(), Some(3));
1262
1263        // Tab forward should wrap to 2 (trapped)
1264        modals.focus_manager_mut().focus_next();
1265        assert_eq!(modals.focus_manager().current(), Some(2));
1266
1267        // Attempt to focus outside trap should fail
1268        assert!(modals.focus_manager_mut().focus(5).is_none());
1269        assert_eq!(modals.focus_manager().current(), Some(2));
1270    }
1271
1272    #[test]
1273    fn empty_focus_group_no_panic() {
1274        let mut modals = FocusAwareModalStack::new();
1275
1276        // Push modal with empty focus group (edge case).
1277        // The trap is NOT pushed because the group has no focusable members,
1278        // preventing a deadlock where no widget could receive focus.
1279        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![]);
1280
1281        // Should not panic, and focus should NOT be trapped (empty group).
1282        assert!(!modals.is_focus_trapped());
1283
1284        // Pop should still work
1285        modals.pop();
1286        assert!(!modals.is_focus_trapped());
1287    }
1288
1289    #[test]
1290    fn rejected_empty_trap_does_not_leave_focus_group_behind() {
1291        let mut modals = FocusAwareModalStack::new();
1292        modals
1293            .focus_manager_mut()
1294            .graph_mut()
1295            .insert(make_focus_node(1));
1296        modals.focus_manager_mut().focus(1);
1297        let group_count_before = modals.focus_manager().group_count();
1298
1299        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![]);
1300
1301        assert!(!modals.is_focus_trapped());
1302        assert_eq!(modals.focus_manager().group_count(), group_count_before);
1303        assert_eq!(modals.focus_manager().current(), Some(1));
1304    }
1305
1306    #[test]
1307    fn late_registered_focus_ids_activate_modal_trap_and_restore_latest_background_selection() {
1308        let mut modals = FocusAwareModalStack::new();
1309        modals.with_focus_graph_mut(|graph| {
1310            graph.insert(make_focus_node(50));
1311            graph.insert(make_focus_node(100));
1312        });
1313
1314        modals.focus(100);
1315        let modal_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
1316        assert!(!modals.is_focus_trapped());
1317        assert_eq!(modals.focus_manager().current(), Some(100));
1318
1319        modals.focus(50);
1320        assert_eq!(modals.focus_manager().current(), Some(50));
1321
1322        modals.with_focus_graph_mut(|graph| {
1323            graph.insert(make_focus_node(1));
1324        });
1325        assert!(modals.is_focus_trapped());
1326        assert_eq!(modals.focus_manager().current(), Some(1));
1327
1328        assert!(modals.pop_id(modal_id).is_some());
1329        assert_eq!(modals.focus_manager().current(), Some(50));
1330        assert!(!modals.is_focus_trapped());
1331    }
1332
1333    #[test]
1334    fn blurred_pop_all_after_late_trap_activation_restores_background_focus_on_gain() {
1335        let mut modals = FocusAwareModalStack::new();
1336        modals.with_focus_graph_mut(|graph| {
1337            graph.insert(make_focus_node(50));
1338            graph.insert(make_focus_node(100));
1339        });
1340
1341        modals.focus(100);
1342        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
1343        modals.focus(50);
1344
1345        modals.with_focus_graph_mut(|graph| {
1346            graph.insert(make_focus_node(1));
1347        });
1348        assert_eq!(modals.focus_manager().current(), Some(1));
1349        assert!(modals.is_focus_trapped());
1350
1351        let _ = modals.handle_event(&Event::Focus(false), None);
1352        assert_eq!(modals.focus_manager().current(), None);
1353
1354        let results = modals.pop_all();
1355        assert_eq!(results.len(), 1);
1356        assert_eq!(modals.focus_manager().current(), None);
1357        assert!(!modals.is_focus_trapped());
1358
1359        let _ = modals.handle_event(&Event::Focus(true), None);
1360        assert_eq!(modals.focus_manager().current(), Some(50));
1361    }
1362
1363    #[test]
1364    fn push_with_trap_does_not_collide_with_existing_group_ids() {
1365        let mut modals = FocusAwareModalStack::new();
1366        modals
1367            .focus_manager_mut()
1368            .graph_mut()
1369            .insert(make_focus_node(1));
1370        modals
1371            .focus_manager_mut()
1372            .graph_mut()
1373            .insert(make_focus_node(99));
1374        modals
1375            .focus_manager_mut()
1376            .graph_mut()
1377            .insert(make_focus_node(100));
1378
1379        let reserved_group_id = FOCUS_GROUP_COUNTER.load(Ordering::Relaxed);
1380        modals
1381            .focus_manager_mut()
1382            .create_group(reserved_group_id, vec![99]);
1383        modals.focus_manager_mut().focus(100);
1384
1385        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
1386        let _ = modals.pop().unwrap();
1387
1388        assert!(modals.focus_manager_mut().push_trap(reserved_group_id));
1389        assert_eq!(modals.focus_manager().current(), Some(99));
1390    }
1391
1392    #[test]
1393    fn pop_id_non_top_modal_rebuilds_focus_traps() {
1394        let mut modals = FocusAwareModalStack::new();
1395
1396        // Add focusable nodes
1397        for id in 1..=6 {
1398            modals
1399                .focus_manager_mut()
1400                .graph_mut()
1401                .insert(make_focus_node(id));
1402        }
1403
1404        // Initial focus
1405        modals.focus_manager_mut().focus(1);
1406
1407        // Push three modals with focus traps.
1408        let id1 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1409        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1410        let _id3 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
1411
1412        // Focus should be on node 4 (top modal)
1413        assert_eq!(modals.focus_manager().current(), Some(4));
1414
1415        // Pop the BOTTOM modal (id1) by ID - this is non-LIFO.
1416        modals.pop_id(id1);
1417
1418        // Focus should still be on the top modal.
1419        assert_eq!(modals.focus_manager().current(), Some(4));
1420        assert_eq!(modals.depth(), 2);
1421        assert!(modals.is_focus_trapped());
1422
1423        // Pop remaining modals normally. Focus should restore as if the removed modal never
1424        // existed: top -> next modal -> original background focus.
1425        modals.pop();
1426        assert_eq!(modals.focus_manager().current(), Some(3));
1427
1428        modals.pop();
1429        assert_eq!(modals.focus_manager().current(), Some(1));
1430        assert!(modals.is_empty());
1431        assert!(!modals.is_focus_trapped());
1432    }
1433
1434    #[test]
1435    fn pop_id_middle_modal_retargets_upper_return_focus() {
1436        let mut modals = FocusAwareModalStack::new();
1437
1438        for id in 1..=6 {
1439            modals
1440                .focus_manager_mut()
1441                .graph_mut()
1442                .insert(make_focus_node(id));
1443        }
1444
1445        modals.focus_manager_mut().focus(1);
1446
1447        let _id1 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1448        let id2 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1449        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
1450
1451        assert_eq!(modals.focus_manager().current(), Some(4));
1452
1453        // Remove the middle modal. The top modal should now restore to modal1's focus.
1454        modals.pop_id(id2);
1455        assert_eq!(modals.focus_manager().current(), Some(4));
1456        assert_eq!(modals.depth(), 2);
1457
1458        modals.pop();
1459        assert_eq!(modals.focus_manager().current(), Some(2));
1460
1461        modals.pop();
1462        assert_eq!(modals.focus_manager().current(), Some(1));
1463        assert!(!modals.is_focus_trapped());
1464    }
1465
1466    #[test]
1467    fn pop_id_rebuild_does_not_pollute_focus_history() {
1468        let mut modals = FocusAwareModalStack::new();
1469
1470        for id in 1..=6 {
1471            modals
1472                .focus_manager_mut()
1473                .graph_mut()
1474                .insert(make_focus_node(id));
1475        }
1476
1477        modals.focus_manager_mut().focus(1);
1478        modals.focus_manager_mut().focus(6);
1479
1480        let id1 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1481        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1482
1483        modals.pop_id(id1);
1484        assert_eq!(modals.focus_manager().current(), Some(3));
1485
1486        modals.pop();
1487        assert_eq!(modals.focus_manager().current(), Some(6));
1488        assert!(modals.focus_manager_mut().focus_back());
1489        assert_eq!(modals.focus_manager().current(), Some(1));
1490        assert!(!modals.focus_manager_mut().focus_back());
1491    }
1492
1493    #[test]
1494    fn pop_id_top_modal_restores_focus_correctly() {
1495        let mut modals = FocusAwareModalStack::new();
1496
1497        // Add focusable nodes
1498        for id in 1..=4 {
1499            modals
1500                .focus_manager_mut()
1501                .graph_mut()
1502                .insert(make_focus_node(id));
1503        }
1504
1505        // Initial focus
1506        modals.focus_manager_mut().focus(1);
1507
1508        // Push two modals
1509        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1510        let id2 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1511
1512        assert_eq!(modals.focus_manager().current(), Some(3));
1513
1514        // Pop the TOP modal by ID - this should work correctly
1515        modals.pop_id(id2);
1516
1517        // Focus should restore to modal1's focus (2)
1518        assert_eq!(modals.focus_manager().current(), Some(2));
1519        assert!(modals.is_focus_trapped()); // Still in modal1's trap
1520
1521        // Pop the last modal
1522        modals.pop();
1523        assert_eq!(modals.focus_manager().current(), Some(1));
1524        assert!(!modals.is_focus_trapped());
1525    }
1526
1527    #[test]
1528    fn pop_id_top_modal_preserves_underlying_selected_control() {
1529        let mut modals = FocusAwareModalStack::new();
1530        modals.with_focus_graph_mut(|graph| {
1531            for id in 1..=5 {
1532                graph.insert(make_focus_node(id));
1533            }
1534        });
1535
1536        modals.focus(1);
1537        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1538        modals.focus(3);
1539        let upper_id =
1540            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
1541        modals.focus(5);
1542        let _ = modals.focus_manager_mut().take_focus_event();
1543        let before = modals.focus_manager().focus_change_count();
1544
1545        assert!(modals.pop_id(upper_id).is_some());
1546        assert_eq!(modals.focus_manager().current(), Some(3));
1547        assert_eq!(
1548            modals.focus_manager_mut().take_focus_event(),
1549            Some(crate::focus::FocusEvent::FocusMoved { from: 5, to: 3 })
1550        );
1551        assert_eq!(modals.focus_manager().focus_change_count(), before + 1);
1552        assert!(modals.is_focus_trapped());
1553
1554        let _ = modals.pop();
1555        assert_eq!(modals.focus_manager().current(), Some(1));
1556    }
1557
1558    #[test]
1559    fn pop_removes_closed_modal_focus_group() {
1560        let mut modals = FocusAwareModalStack::new();
1561        modals
1562            .focus_manager_mut()
1563            .graph_mut()
1564            .insert(make_focus_node(1));
1565        modals
1566            .focus_manager_mut()
1567            .graph_mut()
1568            .insert(make_focus_node(2));
1569
1570        modals.focus_manager_mut().focus(1);
1571        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1572
1573        let result = modals.pop().unwrap();
1574        let group_id = result.focus_group_id.unwrap();
1575
1576        assert!(!modals.focus_manager_mut().push_trap(group_id));
1577        assert!(!modals.is_focus_trapped());
1578        assert_eq!(modals.focus_manager().current(), Some(1));
1579    }
1580
1581    #[test]
1582    fn pop_last_modal_clears_invalid_stale_focus_when_no_fallback_exists() {
1583        let mut modals = FocusAwareModalStack::new();
1584        modals
1585            .focus_manager_mut()
1586            .graph_mut()
1587            .insert(make_focus_node(1));
1588        modals
1589            .focus_manager_mut()
1590            .graph_mut()
1591            .insert(make_focus_node(2));
1592
1593        modals.focus_manager_mut().focus(1);
1594        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1595        assert_eq!(modals.focus_manager().current(), Some(2));
1596
1597        let _ = modals.focus_manager_mut().graph_mut().remove(1);
1598        let _ = modals.focus_manager_mut().graph_mut().remove(2);
1599
1600        modals.pop();
1601        assert_eq!(modals.focus_manager().current(), None);
1602        assert!(!modals.is_focus_trapped());
1603    }
1604
1605    #[test]
1606    fn default_creates_empty_stack() {
1607        let modals = FocusAwareModalStack::default();
1608        assert!(modals.is_empty());
1609        assert_eq!(modals.depth(), 0);
1610        assert!(!modals.is_focus_trapped());
1611    }
1612
1613    #[test]
1614    fn with_focus_manager_uses_provided() {
1615        let mut fm = FocusManager::new();
1616        fm.graph_mut().insert(make_focus_node(42));
1617        fm.focus(42);
1618
1619        let modals = FocusAwareModalStack::with_focus_manager(fm);
1620        assert!(modals.is_empty());
1621        assert_eq!(modals.focus_manager().current(), Some(42));
1622    }
1623
1624    #[test]
1625    fn with_focus_manager_rejects_pretrapped_manager() {
1626        let mut fm = FocusManager::new();
1627        fm.graph_mut().insert(make_focus_node(1));
1628        fm.focus(1);
1629        fm.create_group(7, vec![1]);
1630        assert!(fm.push_trap(7));
1631
1632        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1633            let _ = FocusAwareModalStack::with_focus_manager(fm);
1634        }));
1635        assert!(result.is_err());
1636    }
1637
1638    #[test]
1639    fn stack_accessors() {
1640        let mut modals = FocusAwareModalStack::new();
1641        assert!(modals.stack().is_empty());
1642        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
1643        assert!(!modals.stack().is_empty());
1644        assert_eq!(modals.stack_mut().depth(), 1);
1645    }
1646
1647    #[test]
1648    fn with_focus_graph_mut_resyncs_after_panic() {
1649        let mut modals = FocusAwareModalStack::new();
1650
1651        modals.with_focus_graph_mut(|graph| {
1652            graph.insert(make_focus_node(1));
1653            graph.insert(make_focus_node(2));
1654        });
1655        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
1656        assert_eq!(modals.focus_manager().current(), Some(1));
1657
1658        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1659            modals.with_focus_graph_mut(|graph| {
1660                let _ = graph.remove(1);
1661                panic!("boom");
1662            });
1663        }));
1664        assert!(result.is_err());
1665        assert_eq!(modals.focus_manager().current(), Some(2));
1666        assert!(modals.is_focus_trapped());
1667    }
1668
1669    #[test]
1670    fn with_focus_graph_mut_repairs_invalid_focus_without_modals() {
1671        let mut modals = FocusAwareModalStack::new();
1672        modals.with_focus_graph_mut(|graph| {
1673            graph.insert(make_focus_node(1));
1674            graph.insert(make_focus_node(2));
1675        });
1676        modals.focus(2);
1677        assert_eq!(modals.focus_manager().current(), Some(2));
1678
1679        modals.with_focus_graph_mut(|graph| {
1680            let _ = graph.remove(2);
1681        });
1682
1683        assert_eq!(modals.focus_manager().current(), Some(1));
1684        assert!(!modals.is_focus_trapped());
1685    }
1686
1687    #[test]
1688    fn with_focus_graph_mut_does_not_restore_focus_while_host_blurred() {
1689        let mut modals = FocusAwareModalStack::new();
1690        modals.with_focus_graph_mut(|graph| {
1691            graph.insert(make_focus_node(1));
1692            graph.insert(make_focus_node(2));
1693        });
1694        modals.focus(2);
1695        let _ = modals.handle_event(&Event::Focus(false), None);
1696        assert_eq!(modals.focus_manager().current(), None);
1697
1698        modals.with_focus_graph_mut(|graph| {
1699            let _ = graph.remove(2);
1700        });
1701
1702        assert_eq!(modals.focus_manager().current(), None);
1703    }
1704
1705    #[test]
1706    fn focus_call_while_host_blurred_defers_until_focus_gain() {
1707        let mut modals = FocusAwareModalStack::new();
1708        modals.with_focus_graph_mut(|graph| {
1709            graph.insert(make_focus_node(1));
1710            graph.insert(make_focus_node(2));
1711            graph.insert(make_focus_node(3));
1712        });
1713        modals.focus(1);
1714        let _ = modals.focus_manager_mut().take_focus_event();
1715
1716        let _ = modals.handle_event(&Event::Focus(false), None);
1717        assert_eq!(modals.focus_manager().current(), None);
1718
1719        assert_eq!(modals.focus(3), Some(1));
1720        assert_eq!(modals.focus_manager().current(), None);
1721        assert_eq!(
1722            modals.focus_manager_mut().take_focus_event(),
1723            Some(crate::focus::FocusEvent::FocusLost { id: 1 })
1724        );
1725
1726        let _ = modals.handle_event(&Event::Focus(true), None);
1727        assert_eq!(modals.focus_manager().current(), Some(3));
1728        assert_eq!(
1729            modals.focus_manager_mut().take_focus_event(),
1730            Some(crate::focus::FocusEvent::FocusGained { id: 3 })
1731        );
1732    }
1733
1734    #[test]
1735    fn pop_while_host_blurred_defers_base_focus_restore_until_focus_gain() {
1736        let mut modals = FocusAwareModalStack::new();
1737        modals.with_focus_graph_mut(|graph| {
1738            graph.insert(make_focus_node(1));
1739            graph.insert(make_focus_node(2));
1740        });
1741        modals.focus(1);
1742        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1743        assert_eq!(modals.focus_manager().current(), Some(2));
1744
1745        let _ = modals.handle_event(&Event::Focus(false), None);
1746        assert_eq!(modals.focus_manager().current(), None);
1747
1748        let result = modals.pop();
1749        assert!(result.is_some());
1750        assert_eq!(modals.focus_manager().current(), None);
1751        assert!(!modals.is_focus_trapped());
1752
1753        let _ = modals.handle_event(&Event::Focus(true), None);
1754        assert_eq!(modals.focus_manager().current(), Some(1));
1755    }
1756
1757    #[test]
1758    fn pop_id_last_modal_while_host_blurred_restores_base_focus_on_focus_gain() {
1759        let mut modals = FocusAwareModalStack::new();
1760        modals.with_focus_graph_mut(|graph| {
1761            graph.insert(make_focus_node(1));
1762            graph.insert(make_focus_node(2));
1763        });
1764        modals.focus(1);
1765        let modal_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1766        assert_eq!(modals.focus_manager().current(), Some(2));
1767
1768        let _ = modals.handle_event(&Event::Focus(false), None);
1769        assert_eq!(modals.focus_manager().current(), None);
1770
1771        assert!(modals.pop_id(modal_id).is_some());
1772        assert_eq!(modals.focus_manager().current(), None);
1773        assert!(!modals.is_focus_trapped());
1774
1775        let _ = modals.handle_event(&Event::Focus(true), None);
1776        assert_eq!(modals.focus_manager().current(), Some(1));
1777    }
1778
1779    #[test]
1780    fn pop_id_top_modal_while_host_blurred_restores_underlying_modal_selection_on_focus_gain() {
1781        let mut modals = FocusAwareModalStack::new();
1782        modals.with_focus_graph_mut(|graph| {
1783            for id in 1..=5 {
1784                graph.insert(make_focus_node(id));
1785            }
1786        });
1787        modals.focus(1);
1788        let _lower_id =
1789            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1790        modals.focus(3);
1791        let upper_id =
1792            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
1793        modals.focus(5);
1794        let _ = modals.focus_manager_mut().take_focus_event();
1795
1796        let _ = modals.handle_event(&Event::Focus(false), None);
1797        assert_eq!(modals.focus_manager().current(), None);
1798
1799        assert!(modals.pop_id(upper_id).is_some());
1800        assert_eq!(modals.focus_manager().current(), None);
1801        assert!(modals.is_focus_trapped());
1802
1803        let _ = modals.handle_event(&Event::Focus(true), None);
1804        assert_eq!(modals.focus_manager().current(), Some(3));
1805    }
1806
1807    #[test]
1808    fn pop_all_while_host_blurred_restores_base_focus_on_focus_gain() {
1809        let mut modals = FocusAwareModalStack::new();
1810        modals.with_focus_graph_mut(|graph| {
1811            graph.insert(make_focus_node(1));
1812            graph.insert(make_focus_node(2));
1813            graph.insert(make_focus_node(3));
1814        });
1815        modals.focus(1);
1816        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1817        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1818        assert_eq!(modals.focus_manager().current(), Some(3));
1819
1820        let _ = modals.handle_event(&Event::Focus(false), None);
1821        assert_eq!(modals.focus_manager().current(), None);
1822
1823        let results = modals.pop_all();
1824        assert_eq!(results.len(), 2);
1825        assert_eq!(modals.focus_manager().current(), None);
1826        assert!(!modals.is_focus_trapped());
1827
1828        let _ = modals.handle_event(&Event::Focus(true), None);
1829        assert_eq!(modals.focus_manager().current(), Some(1));
1830    }
1831
1832    #[test]
1833    fn focus_gain_after_blurred_pop_restores_base_focus_without_intermediate_hop() {
1834        let mut modals = FocusAwareModalStack::new();
1835        modals.with_focus_graph_mut(|graph| {
1836            graph.insert(make_focus_node(1));
1837            graph.insert(make_focus_node(5));
1838            graph.insert(make_focus_node(10));
1839        });
1840        modals.focus(5);
1841        let _ = modals.focus_manager_mut().take_focus_event();
1842        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![10]);
1843        let _ = modals.focus_manager_mut().take_focus_event();
1844
1845        let _ = modals.handle_event(&Event::Focus(false), None);
1846        let _ = modals.focus_manager_mut().take_focus_event();
1847
1848        let result = modals.pop();
1849        assert!(result.is_some());
1850        assert_eq!(modals.focus_manager().current(), None);
1851
1852        let before = modals.focus_manager().focus_change_count();
1853        let _ = modals.handle_event(&Event::Focus(true), None);
1854
1855        assert_eq!(modals.focus_manager().current(), Some(5));
1856        assert_eq!(
1857            modals.focus_manager_mut().take_focus_event(),
1858            Some(crate::focus::FocusEvent::FocusGained { id: 5 })
1859        );
1860        assert_eq!(modals.focus_manager().focus_change_count(), before + 1);
1861    }
1862
1863    #[test]
1864    fn blurred_background_focus_change_after_last_modal_pop_overrides_stale_base_focus() {
1865        let mut modals = FocusAwareModalStack::new();
1866        modals.with_focus_graph_mut(|graph| {
1867            graph.insert(make_focus_node(1));
1868            graph.insert(make_focus_node(2));
1869            graph.insert(make_focus_node(3));
1870        });
1871        modals.focus(1);
1872        let _ = modals.focus_manager_mut().take_focus_event();
1873
1874        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1875        let _ = modals.focus_manager_mut().take_focus_event();
1876        let _ = modals.handle_event(&Event::Focus(false), None);
1877        let _ = modals.focus_manager_mut().take_focus_event();
1878
1879        assert!(modals.pop().is_some());
1880        assert_eq!(modals.focus_manager().current(), None);
1881
1882        assert_eq!(modals.focus(3), Some(1));
1883        assert_eq!(modals.focus_manager().current(), None);
1884
1885        let _ = modals.handle_event(&Event::Focus(true), None);
1886        assert_eq!(modals.focus_manager().current(), Some(3));
1887        assert_eq!(
1888            modals.focus_manager_mut().take_focus_event(),
1889            Some(crate::focus::FocusEvent::FocusGained { id: 3 })
1890        );
1891    }
1892
1893    #[test]
1894    fn pop_id_middle_modal_preserves_top_selection_and_retargets_restore_chain() {
1895        let mut modals = FocusAwareModalStack::new();
1896        modals.with_focus_graph_mut(|graph| {
1897            for id in 1..=7 {
1898                graph.insert(make_focus_node(id));
1899            }
1900        });
1901
1902        modals.focus(1);
1903        let _lower_id =
1904            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1905        modals.focus(3);
1906        let middle_id =
1907            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
1908        modals.focus(5);
1909        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![6, 7]);
1910        modals.focus(7);
1911        let _ = modals.focus_manager_mut().take_focus_event();
1912
1913        let removed = modals.pop_id(middle_id);
1914        assert!(removed.is_some());
1915        assert_eq!(modals.focus_manager().current(), Some(7));
1916        assert!(modals.is_focus_trapped());
1917
1918        let _ = modals.pop();
1919        assert_eq!(modals.focus_manager().current(), Some(3));
1920
1921        let _ = modals.pop();
1922        assert_eq!(modals.focus_manager().current(), Some(1));
1923        assert!(!modals.is_focus_trapped());
1924    }
1925
1926    #[test]
1927    fn pop_id_bottom_modal_preserves_top_selection_and_retargets_to_base_focus() {
1928        let mut modals = FocusAwareModalStack::new();
1929        modals.with_focus_graph_mut(|graph| {
1930            for id in 1..=5 {
1931                graph.insert(make_focus_node(id));
1932            }
1933        });
1934
1935        modals.focus(1);
1936        let lower_id =
1937            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1938        modals.focus(3);
1939        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
1940        modals.focus(5);
1941        let _ = modals.focus_manager_mut().take_focus_event();
1942
1943        let removed = modals.pop_id(lower_id);
1944        assert!(removed.is_some());
1945        assert_eq!(modals.focus_manager().current(), Some(5));
1946        assert!(modals.is_focus_trapped());
1947
1948        let _ = modals.pop();
1949        assert_eq!(modals.focus_manager().current(), Some(1));
1950        assert!(!modals.is_focus_trapped());
1951    }
1952
1953    #[test]
1954    fn push_with_trap_while_host_blurred_defers_modal_focus_until_focus_gain() {
1955        let mut modals = FocusAwareModalStack::new();
1956        modals.with_focus_graph_mut(|graph| {
1957            graph.insert(make_focus_node(1));
1958            graph.insert(make_focus_node(2));
1959        });
1960        modals.focus(1);
1961        let _ = modals.handle_event(&Event::Focus(false), None);
1962        assert_eq!(modals.focus_manager().current(), None);
1963
1964        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1965        assert_eq!(modals.focus_manager().current(), None);
1966        assert!(modals.is_focus_trapped());
1967
1968        let _ = modals.handle_event(&Event::Focus(true), None);
1969        assert_eq!(modals.focus_manager().current(), Some(2));
1970    }
1971
1972    #[test]
1973    fn nested_push_while_host_blurred_restores_underlying_modal_selection_on_close() {
1974        let mut modals = FocusAwareModalStack::new();
1975        modals.with_focus_graph_mut(|graph| {
1976            for id in 1..=4 {
1977                graph.insert(make_focus_node(id));
1978            }
1979        });
1980        modals.focus(1);
1981        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1982        modals.focus(3);
1983        let _ = modals.handle_event(&Event::Focus(false), None);
1984        assert_eq!(modals.focus_manager().current(), None);
1985
1986        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
1987        assert_eq!(modals.focus_manager().current(), None);
1988
1989        let _ = modals.handle_event(&Event::Focus(true), None);
1990        assert_eq!(modals.focus_manager().current(), Some(4));
1991
1992        let result = modals.pop();
1993        assert!(result.is_some());
1994        assert_eq!(modals.focus_manager().current(), Some(3));
1995    }
1996
1997    #[test]
1998    fn first_modal_opened_while_blurred_from_unfocused_base_restores_none() {
1999        let mut modals = FocusAwareModalStack::new();
2000        modals.with_focus_graph_mut(|graph| {
2001            graph.insert(make_focus_node(1));
2002            graph.insert(make_focus_node(2));
2003        });
2004        let _ = modals.handle_event(&Event::Focus(false), None);
2005        assert_eq!(modals.focus_manager().current(), None);
2006
2007        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2008        assert_eq!(modals.focus_manager().current(), None);
2009
2010        let _ = modals.handle_event(&Event::Focus(true), None);
2011        assert_eq!(modals.focus_manager().current(), Some(2));
2012
2013        let result = modals.pop();
2014        assert!(result.is_some());
2015        assert_eq!(modals.focus_manager().current(), None);
2016        assert!(!modals.is_focus_trapped());
2017    }
2018
2019    #[test]
2020    fn pop_id_non_top_while_host_blurred_keeps_focus_cleared_until_focus_gain() {
2021        let mut modals = FocusAwareModalStack::new();
2022        for id in 1..=4 {
2023            modals
2024                .focus_manager_mut()
2025                .graph_mut()
2026                .insert(make_focus_node(id));
2027        }
2028
2029        modals.focus_manager_mut().focus(1);
2030        let id1 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2031        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
2032        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2033        assert_eq!(modals.focus_manager().current(), Some(4));
2034
2035        let _ = modals.handle_event(&Event::Focus(false), None);
2036        assert_eq!(modals.focus_manager().current(), None);
2037
2038        let result = modals.pop_id(id1);
2039        assert!(result.is_some());
2040        assert_eq!(modals.focus_manager().current(), None);
2041        assert!(modals.is_focus_trapped());
2042
2043        let _ = modals.handle_event(&Event::Focus(true), None);
2044        assert_eq!(modals.focus_manager().current(), Some(4));
2045    }
2046
2047    #[test]
2048    fn pop_id_trapped_modal_while_blurred_with_only_non_trapped_modals_remaining_restores_base_focus()
2049     {
2050        let mut modals = FocusAwareModalStack::new();
2051        modals.with_focus_graph_mut(|graph| {
2052            graph.insert(make_focus_node(1));
2053            graph.insert(make_focus_node(2));
2054        });
2055
2056        modals.focus(1);
2057        let trapped_id =
2058            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2059        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
2060        assert_eq!(modals.focus_manager().current(), Some(2));
2061
2062        let _ = modals.handle_event(&Event::Focus(false), None);
2063        assert_eq!(modals.focus_manager().current(), None);
2064
2065        assert!(modals.pop_id(trapped_id).is_some());
2066        assert_eq!(modals.focus_manager().current(), None);
2067        assert!(!modals.is_focus_trapped());
2068
2069        let _ = modals.handle_event(&Event::Focus(true), None);
2070        assert_eq!(modals.focus_manager().current(), Some(1));
2071    }
2072
2073    #[test]
2074    fn pop_id_inactive_trapped_modal_with_only_non_trapped_modals_remaining_preserves_latest_background_focus()
2075     {
2076        let mut modals = FocusAwareModalStack::new();
2077        modals.with_focus_graph_mut(|graph| {
2078            graph.insert(make_focus_node(1));
2079            graph.insert(make_focus_node(2));
2080            graph.insert(make_focus_node(9));
2081        });
2082
2083        modals.focus(1);
2084        let trapped_id =
2085            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2086        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
2087        assert_eq!(modals.focus_manager().current(), Some(2));
2088
2089        modals.with_focus_graph_mut(|graph| {
2090            let _ = graph.remove(2);
2091        });
2092        assert_eq!(modals.focus_manager().current(), Some(1));
2093        assert!(!modals.is_focus_trapped());
2094
2095        assert_eq!(modals.focus(9), Some(1));
2096        assert_eq!(modals.focus_manager().current(), Some(9));
2097        assert!(!modals.is_focus_trapped());
2098
2099        assert!(modals.pop_id(trapped_id).is_some());
2100        assert_eq!(modals.focus_manager().current(), Some(9));
2101        assert!(!modals.is_focus_trapped());
2102    }
2103
2104    #[test]
2105    fn blurred_pop_id_inactive_trapped_modal_with_only_non_trapped_modals_remaining_preserves_latest_background_focus_on_focus_gain()
2106     {
2107        let mut modals = FocusAwareModalStack::new();
2108        modals.with_focus_graph_mut(|graph| {
2109            graph.insert(make_focus_node(1));
2110            graph.insert(make_focus_node(2));
2111            graph.insert(make_focus_node(9));
2112        });
2113
2114        modals.focus(1);
2115        let trapped_id =
2116            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2117        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
2118
2119        modals.with_focus_graph_mut(|graph| {
2120            let _ = graph.remove(2);
2121        });
2122        assert_eq!(modals.focus_manager().current(), Some(1));
2123        assert!(!modals.is_focus_trapped());
2124
2125        assert_eq!(modals.focus(9), Some(1));
2126        let _ = modals.handle_event(&Event::Focus(false), None);
2127        assert_eq!(modals.focus_manager().current(), None);
2128        assert!(!modals.is_focus_trapped());
2129
2130        assert!(modals.pop_id(trapped_id).is_some());
2131        assert_eq!(modals.focus_manager().current(), None);
2132        assert!(!modals.is_focus_trapped());
2133
2134        let _ = modals.handle_event(&Event::Focus(true), None);
2135        assert_eq!(modals.focus_manager().current(), Some(9));
2136        assert!(!modals.is_focus_trapped());
2137    }
2138
2139    #[test]
2140    fn blurred_pop_id_inactive_trapped_modal_preserves_latest_background_focus_when_trap_went_inactive_while_blurred()
2141     {
2142        let mut modals = FocusAwareModalStack::new();
2143        modals.with_focus_graph_mut(|graph| {
2144            graph.insert(make_focus_node(1));
2145            graph.insert(make_focus_node(2));
2146            graph.insert(make_focus_node(9));
2147        });
2148
2149        modals.focus(1);
2150        let trapped_id =
2151            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2152        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
2153        assert_eq!(modals.focus_manager().current(), Some(2));
2154
2155        let _ = modals.handle_event(&Event::Focus(false), None);
2156        assert_eq!(modals.focus_manager().current(), None);
2157        assert!(modals.is_focus_trapped());
2158
2159        modals.with_focus_graph_mut(|graph| {
2160            let _ = graph.remove(2);
2161        });
2162        assert_eq!(modals.focus_manager().current(), None);
2163        assert!(!modals.is_focus_trapped());
2164
2165        assert_eq!(modals.focus(9), Some(1));
2166        assert_eq!(modals.focus_manager().current(), None);
2167        assert!(!modals.is_focus_trapped());
2168
2169        assert!(modals.pop_id(trapped_id).is_some());
2170        assert_eq!(modals.focus_manager().current(), None);
2171        assert!(!modals.is_focus_trapped());
2172
2173        let _ = modals.handle_event(&Event::Focus(true), None);
2174        assert_eq!(modals.focus_manager().current(), Some(9));
2175        assert!(!modals.is_focus_trapped());
2176    }
2177
2178    #[test]
2179    fn focus_gain_refreshes_inactive_modal_restore_target_after_background_fallback() {
2180        let mut modals = FocusAwareModalStack::new();
2181        modals.with_focus_graph_mut(|graph| {
2182            graph.insert(make_focus_node(2));
2183            graph.insert(make_focus_node(9));
2184        });
2185
2186        let _ = modals.handle_event(&Event::Focus(false), None);
2187        assert_eq!(modals.focus_manager().current(), None);
2188
2189        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2190        assert_eq!(modals.focus_manager().current(), None);
2191        assert!(modals.is_focus_trapped());
2192
2193        modals.with_focus_graph_mut(|graph| {
2194            let _ = graph.remove(2);
2195        });
2196        assert_eq!(modals.focus_manager().current(), None);
2197        assert!(!modals.is_focus_trapped());
2198
2199        let _ = modals.handle_event(&Event::Focus(true), None);
2200        assert_eq!(modals.focus_manager().current(), Some(9));
2201        assert!(!modals.is_focus_trapped());
2202
2203        modals.with_focus_graph_mut(|graph| {
2204            graph.insert(make_focus_node(2));
2205        });
2206        assert_eq!(modals.focus_manager().current(), Some(2));
2207        assert!(modals.is_focus_trapped());
2208
2209        assert!(modals.pop().is_some());
2210        assert_eq!(modals.focus_manager().current(), Some(9));
2211        assert!(!modals.is_focus_trapped());
2212    }
2213
2214    #[test]
2215    fn with_focus_graph_mut_blurred_empty_trap_restores_base_focus_on_focus_gain() {
2216        let mut modals = FocusAwareModalStack::new();
2217        modals.with_focus_graph_mut(|graph| {
2218            graph.insert(make_focus_node(1));
2219            graph.insert(make_focus_node(2));
2220            graph.insert(make_focus_node(3));
2221        });
2222
2223        modals.focus(3);
2224        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2225        assert_eq!(modals.focus_manager().current(), Some(2));
2226
2227        let _ = modals.handle_event(&Event::Focus(false), None);
2228        assert_eq!(modals.focus_manager().current(), None);
2229
2230        modals.with_focus_graph_mut(|graph| {
2231            let _ = graph.remove(2);
2232        });
2233
2234        assert_eq!(modals.focus_manager().current(), None);
2235        let _ = modals.handle_event(&Event::Focus(true), None);
2236        assert_eq!(modals.focus_manager().current(), Some(3));
2237    }
2238
2239    #[test]
2240    fn with_focus_graph_mut_focused_empty_trap_restores_base_focus() {
2241        let mut modals = FocusAwareModalStack::new();
2242        modals.with_focus_graph_mut(|graph| {
2243            graph.insert(make_focus_node(1));
2244            graph.insert(make_focus_node(2));
2245            graph.insert(make_focus_node(3));
2246        });
2247
2248        modals.focus(3);
2249        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2250        assert_eq!(modals.focus_manager().current(), Some(2));
2251
2252        modals.with_focus_graph_mut(|graph| {
2253            let _ = graph.remove(2);
2254        });
2255
2256        assert_eq!(modals.focus_manager().current(), Some(3));
2257        assert!(!modals.is_focus_trapped());
2258    }
2259
2260    #[test]
2261    fn with_focus_graph_mut_focused_empty_top_trap_restores_underlying_selected_control() {
2262        let mut modals = FocusAwareModalStack::new();
2263        modals.with_focus_graph_mut(|graph| {
2264            for id in 1..=4 {
2265                graph.insert(make_focus_node(id));
2266            }
2267        });
2268
2269        modals.focus(1);
2270        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2271        modals.focus(3);
2272        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2273        assert_eq!(modals.focus_manager().current(), Some(4));
2274
2275        modals.with_focus_graph_mut(|graph| {
2276            let _ = graph.remove(4);
2277        });
2278
2279        assert_eq!(modals.focus_manager().current(), Some(3));
2280        assert!(modals.is_focus_trapped());
2281    }
2282
2283    #[test]
2284    fn with_focus_graph_mut_blurred_empty_top_trap_restores_underlying_selected_control_on_focus_gain()
2285     {
2286        let mut modals = FocusAwareModalStack::new();
2287        modals.with_focus_graph_mut(|graph| {
2288            for id in 1..=4 {
2289                graph.insert(make_focus_node(id));
2290            }
2291        });
2292
2293        modals.focus(1);
2294        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2295        modals.focus(3);
2296        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2297        let _ = modals.handle_event(&Event::Focus(false), None);
2298        assert_eq!(modals.focus_manager().current(), None);
2299
2300        modals.with_focus_graph_mut(|graph| {
2301            let _ = graph.remove(4);
2302        });
2303
2304        let _ = modals.handle_event(&Event::Focus(true), None);
2305        assert_eq!(modals.focus_manager().current(), Some(3));
2306        assert!(modals.is_focus_trapped());
2307    }
2308
2309    #[test]
2310    fn with_focus_graph_mut_empty_lower_trap_retargets_surviving_top_restore_to_base_focus() {
2311        let mut modals = FocusAwareModalStack::new();
2312        modals.with_focus_graph_mut(|graph| {
2313            for id in [1, 5, 8, 10] {
2314                graph.insert(make_focus_node(id));
2315            }
2316        });
2317
2318        modals.focus(10);
2319        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2320        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![8]);
2321        assert_eq!(modals.focus_manager().current(), Some(8));
2322
2323        modals.with_focus_graph_mut(|graph| {
2324            let _ = graph.remove(5);
2325        });
2326        assert_eq!(modals.focus_manager().current(), Some(8));
2327        assert!(modals.is_focus_trapped());
2328
2329        assert!(modals.pop().is_some());
2330        assert_eq!(modals.focus_manager().current(), Some(10));
2331        assert!(!modals.is_focus_trapped());
2332    }
2333
2334    #[test]
2335    fn pop_after_top_trap_becomes_empty_preserves_underlying_trap() {
2336        let mut modals = FocusAwareModalStack::new();
2337        modals.with_focus_graph_mut(|graph| {
2338            for id in 1..=4 {
2339                graph.insert(make_focus_node(id));
2340            }
2341        });
2342
2343        modals.focus(1);
2344        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2345        modals.focus(3);
2346        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2347
2348        modals.with_focus_graph_mut(|graph| {
2349            let _ = graph.remove(4);
2350        });
2351        assert_eq!(modals.focus_manager().current(), Some(3));
2352        assert!(modals.is_focus_trapped());
2353
2354        assert!(modals.pop().is_some());
2355        assert_eq!(modals.focus_manager().current(), Some(3));
2356        assert!(modals.is_focus_trapped());
2357        assert_eq!(modals.focus(1), None);
2358        assert_eq!(modals.focus_manager().current(), Some(3));
2359
2360        assert!(modals.pop().is_some());
2361        assert_eq!(modals.focus_manager().current(), Some(1));
2362        assert!(!modals.is_focus_trapped());
2363    }
2364
2365    #[test]
2366    fn blurred_pop_after_top_trap_becomes_empty_preserves_underlying_deferred_focus() {
2367        let mut modals = FocusAwareModalStack::new();
2368        modals.with_focus_graph_mut(|graph| {
2369            for id in 1..=4 {
2370                graph.insert(make_focus_node(id));
2371            }
2372        });
2373
2374        modals.focus(1);
2375        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2376        modals.focus(3);
2377        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2378
2379        modals.with_focus_graph_mut(|graph| {
2380            let _ = graph.remove(4);
2381        });
2382        let _ = modals.handle_event(&Event::Focus(false), None);
2383        assert_eq!(modals.focus_manager().current(), None);
2384        assert!(modals.is_focus_trapped());
2385
2386        assert!(modals.pop().is_some());
2387        assert_eq!(modals.focus_manager().current(), None);
2388        assert!(modals.is_focus_trapped());
2389
2390        let _ = modals.handle_event(&Event::Focus(true), None);
2391        assert_eq!(modals.focus_manager().current(), Some(3));
2392        assert!(modals.is_focus_trapped());
2393    }
2394
2395    #[test]
2396    fn pop_id_skips_stale_retarget_from_inactive_middle_modal() {
2397        let mut modals = FocusAwareModalStack::new();
2398        modals.with_focus_graph_mut(|graph| {
2399            for id in 1..=6 {
2400                graph.insert(make_focus_node(id));
2401            }
2402        });
2403
2404        modals.focus(1);
2405        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2406        modals.focus(3);
2407        let stale_middle_id =
2408            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2409
2410        modals.with_focus_graph_mut(|graph| {
2411            let _ = graph.remove(4);
2412        });
2413        assert_eq!(modals.focus_manager().current(), Some(3));
2414        modals.focus(2);
2415        assert_eq!(modals.focus_manager().current(), Some(2));
2416
2417        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5, 6]);
2418        assert_eq!(modals.focus_manager().current(), Some(5));
2419
2420        assert!(modals.pop_id(stale_middle_id).is_some());
2421        assert_eq!(modals.focus_manager().current(), Some(5));
2422
2423        assert!(modals.pop().is_some());
2424        assert_eq!(modals.focus_manager().current(), Some(2));
2425        assert!(modals.is_focus_trapped());
2426    }
2427
2428    #[test]
2429    fn blurred_pop_id_skips_stale_retarget_from_inactive_middle_modal() {
2430        let mut modals = FocusAwareModalStack::new();
2431        modals.with_focus_graph_mut(|graph| {
2432            for id in 1..=6 {
2433                graph.insert(make_focus_node(id));
2434            }
2435        });
2436
2437        modals.focus(1);
2438        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2439        modals.focus(3);
2440        let stale_middle_id =
2441            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2442
2443        modals.with_focus_graph_mut(|graph| {
2444            let _ = graph.remove(4);
2445        });
2446        let _ = modals.handle_event(&Event::Focus(false), None);
2447        assert_eq!(modals.focus_manager().current(), None);
2448
2449        assert_eq!(modals.focus(2), Some(3));
2450        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5, 6]);
2451
2452        assert!(modals.pop_id(stale_middle_id).is_some());
2453        assert_eq!(modals.focus_manager().current(), None);
2454
2455        assert!(modals.pop().is_some());
2456        let _ = modals.handle_event(&Event::Focus(true), None);
2457        assert_eq!(modals.focus_manager().current(), Some(2));
2458        assert!(modals.is_focus_trapped());
2459    }
2460
2461    #[test]
2462    fn pop_id_inactive_lower_modal_preserves_surviving_upper_restore_to_base_focus() {
2463        let mut modals = FocusAwareModalStack::new();
2464        modals.with_focus_graph_mut(|graph| {
2465            for id in [5, 10, 20, 30] {
2466                graph.insert(make_focus_node(id));
2467            }
2468        });
2469
2470        modals.focus(10);
2471        let lower_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![20]);
2472        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![30]);
2473        assert_eq!(modals.focus_manager().current(), Some(30));
2474
2475        modals.with_focus_graph_mut(|graph| {
2476            let _ = graph.remove(20);
2477        });
2478        assert_eq!(modals.focus_manager().current(), Some(30));
2479
2480        assert!(modals.pop_id(lower_id).is_some());
2481        assert_eq!(modals.focus_manager().current(), Some(30));
2482        assert!(modals.is_focus_trapped());
2483
2484        assert!(modals.pop().is_some());
2485        assert_eq!(modals.focus_manager().current(), Some(10));
2486        assert!(!modals.is_focus_trapped());
2487    }
2488
2489    #[test]
2490    fn pop_id_active_lower_modal_propagates_none_restore_target_to_surviving_upper_modal() {
2491        let mut modals = FocusAwareModalStack::new();
2492        modals.with_focus_graph_mut(|graph| {
2493            graph.insert(make_focus_node(1));
2494            graph.insert(make_focus_node(2));
2495        });
2496
2497        let lower_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
2498        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2499        assert_eq!(modals.focus_manager().current(), Some(2));
2500        assert!(modals.is_focus_trapped());
2501
2502        assert!(modals.pop_id(lower_id).is_some());
2503        assert_eq!(modals.focus_manager().current(), Some(2));
2504        assert!(modals.is_focus_trapped());
2505
2506        assert!(modals.pop().is_some());
2507        assert_eq!(modals.focus_manager().current(), None);
2508        assert!(!modals.is_focus_trapped());
2509    }
2510
2511    #[test]
2512    fn blurred_pop_id_inactive_lower_modal_preserves_surviving_upper_restore_to_base_focus() {
2513        let mut modals = FocusAwareModalStack::new();
2514        modals.with_focus_graph_mut(|graph| {
2515            for id in [5, 10, 20, 30] {
2516                graph.insert(make_focus_node(id));
2517            }
2518        });
2519
2520        modals.focus(10);
2521        let lower_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![20]);
2522        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![30]);
2523
2524        modals.with_focus_graph_mut(|graph| {
2525            let _ = graph.remove(20);
2526        });
2527        assert!(modals.pop_id(lower_id).is_some());
2528
2529        let _ = modals.handle_event(&Event::Focus(false), None);
2530        assert_eq!(modals.focus_manager().current(), None);
2531
2532        assert!(modals.pop().is_some());
2533        assert_eq!(modals.focus_manager().current(), None);
2534
2535        let _ = modals.handle_event(&Event::Focus(true), None);
2536        assert_eq!(modals.focus_manager().current(), Some(10));
2537        assert!(!modals.is_focus_trapped());
2538    }
2539
2540    #[test]
2541    fn reactivated_top_modal_restores_latest_underlying_selection() {
2542        let mut modals = FocusAwareModalStack::new();
2543        modals.with_focus_graph_mut(|graph| {
2544            for id in 1..=4 {
2545                graph.insert(make_focus_node(id));
2546            }
2547        });
2548
2549        modals.focus(1);
2550        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2551        modals.focus(3);
2552        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2553
2554        modals.with_focus_graph_mut(|graph| {
2555            let _ = graph.remove(4);
2556        });
2557        assert_eq!(modals.focus_manager().current(), Some(3));
2558
2559        modals.focus(2);
2560        assert_eq!(modals.focus_manager().current(), Some(2));
2561
2562        modals.with_focus_graph_mut(|graph| {
2563            graph.insert(make_focus_node(4));
2564        });
2565        assert_eq!(modals.focus_manager().current(), Some(4));
2566        assert!(modals.is_focus_trapped());
2567
2568        assert!(modals.pop().is_some());
2569        assert_eq!(modals.focus_manager().current(), Some(2));
2570        assert!(modals.is_focus_trapped());
2571    }
2572
2573    #[test]
2574    fn reactivated_top_modal_tracks_graph_restored_selection_within_same_lower_group() {
2575        let mut modals = FocusAwareModalStack::new();
2576        modals.with_focus_graph_mut(|graph| {
2577            for id in 1..=4 {
2578                graph.insert(make_focus_node(id));
2579            }
2580        });
2581
2582        modals.focus(1);
2583        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2584        modals.focus(3);
2585        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2586
2587        modals.with_focus_graph_mut(|graph| {
2588            let _ = graph.remove(4);
2589        });
2590        assert_eq!(modals.focus_manager().current(), Some(3));
2591
2592        modals.with_focus_graph_mut(|graph| {
2593            let _ = graph.remove(3);
2594        });
2595        assert_eq!(modals.focus_manager().current(), Some(2));
2596        assert!(modals.is_focus_trapped());
2597
2598        modals.with_focus_graph_mut(|graph| {
2599            graph.insert(make_focus_node(4));
2600        });
2601        assert_eq!(modals.focus_manager().current(), Some(4));
2602        assert!(modals.is_focus_trapped());
2603
2604        assert!(modals.pop().is_some());
2605        assert_eq!(modals.focus_manager().current(), Some(2));
2606        assert!(modals.is_focus_trapped());
2607    }
2608
2609    #[test]
2610    fn blurred_reactivated_top_modal_tracks_graph_restored_selection_within_same_lower_group() {
2611        let mut modals = FocusAwareModalStack::new();
2612        modals.with_focus_graph_mut(|graph| {
2613            for id in 1..=4 {
2614                graph.insert(make_focus_node(id));
2615            }
2616        });
2617
2618        modals.focus(1);
2619        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2620        modals.focus(3);
2621        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2622
2623        modals.with_focus_graph_mut(|graph| {
2624            let _ = graph.remove(4);
2625        });
2626        let _ = modals.handle_event(&Event::Focus(false), None);
2627        assert_eq!(modals.focus_manager().current(), None);
2628
2629        modals.with_focus_graph_mut(|graph| {
2630            let _ = graph.remove(3);
2631        });
2632        assert_eq!(modals.focus_manager().current(), None);
2633        assert!(modals.is_focus_trapped());
2634
2635        modals.with_focus_graph_mut(|graph| {
2636            graph.insert(make_focus_node(4));
2637        });
2638        let _ = modals.handle_event(&Event::Focus(true), None);
2639        assert_eq!(modals.focus_manager().current(), Some(4));
2640        assert!(modals.is_focus_trapped());
2641
2642        assert!(modals.pop().is_some());
2643        assert_eq!(modals.focus_manager().current(), Some(2));
2644        assert!(modals.is_focus_trapped());
2645    }
2646
2647    #[test]
2648    fn blurred_reactivated_top_modal_restores_latest_underlying_selection() {
2649        let mut modals = FocusAwareModalStack::new();
2650        modals.with_focus_graph_mut(|graph| {
2651            for id in 1..=4 {
2652                graph.insert(make_focus_node(id));
2653            }
2654        });
2655
2656        modals.focus(1);
2657        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2658        modals.focus(3);
2659        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2660
2661        modals.with_focus_graph_mut(|graph| {
2662            let _ = graph.remove(4);
2663        });
2664        let _ = modals.handle_event(&Event::Focus(false), None);
2665        assert_eq!(modals.focus_manager().current(), None);
2666
2667        assert_eq!(modals.focus(2), Some(3));
2668
2669        modals.with_focus_graph_mut(|graph| {
2670            graph.insert(make_focus_node(4));
2671        });
2672
2673        let _ = modals.handle_event(&Event::Focus(true), None);
2674        assert_eq!(modals.focus_manager().current(), Some(4));
2675        assert!(modals.is_focus_trapped());
2676
2677        assert!(modals.pop().is_some());
2678        assert_eq!(modals.focus_manager().current(), Some(2));
2679        assert!(modals.is_focus_trapped());
2680    }
2681
2682    #[test]
2683    fn reactivated_inactive_top_modal_tracks_graph_restored_underlying_selection() {
2684        let mut modals = FocusAwareModalStack::new();
2685        modals.with_focus_graph_mut(|graph| {
2686            for id in 1..=7 {
2687                graph.insert(make_focus_node(id));
2688            }
2689        });
2690
2691        modals.focus(1);
2692        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2693        modals.focus(3);
2694        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
2695        modals.focus(5);
2696        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![6]);
2697        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![7]);
2698
2699        modals.with_focus_graph_mut(|graph| {
2700            let _ = graph.remove(7);
2701        });
2702        assert_eq!(modals.focus_manager().current(), Some(6));
2703
2704        modals.with_focus_graph_mut(|graph| {
2705            let _ = graph.remove(6);
2706        });
2707        assert_eq!(modals.focus_manager().current(), Some(5));
2708        assert!(modals.is_focus_trapped());
2709
2710        modals.with_focus_graph_mut(|graph| {
2711            graph.insert(make_focus_node(7));
2712        });
2713        assert_eq!(modals.focus_manager().current(), Some(7));
2714        assert!(modals.is_focus_trapped());
2715
2716        assert!(modals.pop().is_some());
2717        assert_eq!(modals.focus_manager().current(), Some(5));
2718        assert!(modals.is_focus_trapped());
2719    }
2720
2721    #[test]
2722    fn reactivated_inactive_modal_chain_tracks_graph_restored_underlying_selection() {
2723        let mut modals = FocusAwareModalStack::new();
2724        modals.with_focus_graph_mut(|graph| {
2725            for id in 1..=5 {
2726                graph.insert(make_focus_node(id));
2727            }
2728        });
2729
2730        modals.focus(1);
2731        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2732        modals.focus(3);
2733        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2734        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2735
2736        modals.with_focus_graph_mut(|graph| {
2737            let _ = graph.remove(5);
2738        });
2739        assert_eq!(modals.focus_manager().current(), Some(4));
2740
2741        modals.with_focus_graph_mut(|graph| {
2742            let _ = graph.remove(4);
2743        });
2744        assert_eq!(modals.focus_manager().current(), Some(3));
2745
2746        modals.with_focus_graph_mut(|graph| {
2747            let _ = graph.remove(3);
2748        });
2749        assert_eq!(modals.focus_manager().current(), Some(2));
2750        assert!(modals.is_focus_trapped());
2751
2752        modals.with_focus_graph_mut(|graph| {
2753            graph.insert(make_focus_node(4));
2754        });
2755        assert_eq!(modals.focus_manager().current(), Some(4));
2756        assert!(modals.is_focus_trapped());
2757
2758        modals.with_focus_graph_mut(|graph| {
2759            graph.insert(make_focus_node(5));
2760        });
2761        assert_eq!(modals.focus_manager().current(), Some(5));
2762        assert!(modals.is_focus_trapped());
2763
2764        assert!(modals.pop().is_some());
2765        assert_eq!(modals.focus_manager().current(), Some(4));
2766        assert!(modals.is_focus_trapped());
2767
2768        assert!(modals.pop().is_some());
2769        assert_eq!(modals.focus_manager().current(), Some(2));
2770        assert!(modals.is_focus_trapped());
2771    }
2772
2773    #[test]
2774    fn reactivated_lower_modal_refreshes_still_inactive_upper_restore_target() {
2775        let mut modals = FocusAwareModalStack::new();
2776        modals.with_focus_graph_mut(|graph| {
2777            for id in [1, 2, 4, 5] {
2778                graph.insert(make_focus_node(id));
2779            }
2780        });
2781
2782        modals.focus(1);
2783        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 4]);
2784        modals.focus(4);
2785        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2786
2787        modals.with_focus_graph_mut(|graph| {
2788            let _ = graph.remove(5);
2789        });
2790        assert_eq!(modals.focus_manager().current(), Some(4));
2791
2792        modals.with_focus_graph_mut(|graph| {
2793            let _ = graph.remove(2);
2794            let _ = graph.remove(4);
2795        });
2796        assert_eq!(modals.focus_manager().current(), Some(1));
2797        assert!(!modals.is_focus_trapped());
2798
2799        modals.with_focus_graph_mut(|graph| {
2800            graph.insert(make_focus_node(2));
2801            graph.insert(make_focus_node(4));
2802        });
2803        assert_eq!(modals.focus_manager().current(), Some(2));
2804        assert!(modals.is_focus_trapped());
2805
2806        modals.with_focus_graph_mut(|graph| {
2807            graph.insert(make_focus_node(5));
2808        });
2809        assert_eq!(modals.focus_manager().current(), Some(5));
2810        assert!(modals.is_focus_trapped());
2811
2812        assert!(modals.pop().is_some());
2813        assert_eq!(modals.focus_manager().current(), Some(2));
2814        assert!(modals.is_focus_trapped());
2815    }
2816
2817    #[test]
2818    fn reactivated_inactive_upper_modal_does_not_restore_stale_lower_selection_after_top_pop() {
2819        let mut modals = FocusAwareModalStack::new();
2820        modals.with_focus_graph_mut(|graph| {
2821            for id in 1..=5 {
2822                graph.insert(make_focus_node(id));
2823            }
2824        });
2825
2826        modals.focus(1);
2827        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2828        modals.focus(3);
2829        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2830
2831        modals.with_focus_graph_mut(|graph| {
2832            let _ = graph.remove(4);
2833        });
2834        assert_eq!(modals.focus_manager().current(), Some(3));
2835
2836        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2837        assert_eq!(modals.focus_manager().current(), Some(5));
2838
2839        modals.with_focus_graph_mut(|graph| {
2840            let _ = graph.remove(3);
2841        });
2842        assert_eq!(modals.focus_manager().current(), Some(5));
2843
2844        assert!(modals.pop().is_some());
2845        assert_eq!(modals.focus_manager().current(), Some(2));
2846        assert!(modals.is_focus_trapped());
2847
2848        modals.with_focus_graph_mut(|graph| {
2849            graph.insert(make_focus_node(4));
2850        });
2851        assert_eq!(modals.focus_manager().current(), Some(4));
2852        assert!(modals.is_focus_trapped());
2853
2854        modals.with_focus_graph_mut(|graph| {
2855            graph.insert(make_focus_node(3));
2856        });
2857        assert_eq!(modals.focus_manager().current(), Some(4));
2858        assert!(modals.is_focus_trapped());
2859
2860        assert!(modals.pop().is_some());
2861        assert_eq!(modals.focus_manager().current(), Some(2));
2862        assert!(modals.is_focus_trapped());
2863    }
2864
2865    #[test]
2866    fn blurred_reactivated_inactive_upper_modal_does_not_restore_stale_lower_selection_after_top_pop()
2867     {
2868        let mut modals = FocusAwareModalStack::new();
2869        modals.with_focus_graph_mut(|graph| {
2870            for id in 1..=5 {
2871                graph.insert(make_focus_node(id));
2872            }
2873        });
2874
2875        modals.focus(1);
2876        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2877        modals.focus(3);
2878        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2879
2880        modals.with_focus_graph_mut(|graph| {
2881            let _ = graph.remove(4);
2882        });
2883        assert_eq!(modals.focus_manager().current(), Some(3));
2884
2885        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2886        assert_eq!(modals.focus_manager().current(), Some(5));
2887
2888        let _ = modals.handle_event(&Event::Focus(false), None);
2889        assert_eq!(modals.focus_manager().current(), None);
2890
2891        modals.with_focus_graph_mut(|graph| {
2892            let _ = graph.remove(3);
2893        });
2894        assert_eq!(modals.focus_manager().current(), None);
2895
2896        assert!(modals.pop().is_some());
2897        assert_eq!(modals.focus_manager().current(), None);
2898        assert!(modals.is_focus_trapped());
2899
2900        modals.with_focus_graph_mut(|graph| {
2901            graph.insert(make_focus_node(4));
2902        });
2903        let _ = modals.handle_event(&Event::Focus(true), None);
2904        assert_eq!(modals.focus_manager().current(), Some(4));
2905        assert!(modals.is_focus_trapped());
2906
2907        modals.with_focus_graph_mut(|graph| {
2908            graph.insert(make_focus_node(3));
2909        });
2910        assert_eq!(modals.focus_manager().current(), Some(4));
2911        assert!(modals.is_focus_trapped());
2912
2913        assert!(modals.pop().is_some());
2914        assert_eq!(modals.focus_manager().current(), Some(2));
2915        assert!(modals.is_focus_trapped());
2916    }
2917
2918    #[test]
2919    fn reactivated_middle_modal_before_top_close_does_not_restore_stale_lower_selection() {
2920        let mut modals = FocusAwareModalStack::new();
2921        modals.with_focus_graph_mut(|graph| {
2922            for id in 1..=5 {
2923                graph.insert(make_focus_node(id));
2924            }
2925        });
2926
2927        modals.focus(1);
2928        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2929        modals.focus(3);
2930        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2931        assert_eq!(modals.focus_manager().current(), Some(4));
2932
2933        modals.with_focus_graph_mut(|graph| {
2934            let _ = graph.remove(4);
2935        });
2936        assert_eq!(modals.focus_manager().current(), Some(3));
2937
2938        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2939        assert_eq!(modals.focus_manager().current(), Some(5));
2940
2941        modals.with_focus_graph_mut(|graph| {
2942            let _ = graph.remove(3);
2943        });
2944        assert_eq!(modals.focus_manager().current(), Some(5));
2945
2946        modals.with_focus_graph_mut(|graph| {
2947            graph.insert(make_focus_node(4));
2948        });
2949        assert_eq!(modals.focus_manager().current(), Some(5));
2950
2951        assert!(modals.pop().is_some());
2952        assert_eq!(modals.focus_manager().current(), Some(4));
2953        assert!(modals.is_focus_trapped());
2954
2955        assert!(modals.pop().is_some());
2956        assert_eq!(modals.focus_manager().current(), Some(2));
2957        assert!(modals.is_focus_trapped());
2958    }
2959
2960    #[test]
2961    fn revalidated_stale_lower_target_before_top_close_does_not_win_on_middle_pop() {
2962        let mut modals = FocusAwareModalStack::new();
2963        modals.with_focus_graph_mut(|graph| {
2964            for id in 1..=5 {
2965                graph.insert(make_focus_node(id));
2966            }
2967        });
2968
2969        modals.focus(1);
2970        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2971        modals.focus(3);
2972        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2973
2974        modals.with_focus_graph_mut(|graph| {
2975            let _ = graph.remove(4);
2976        });
2977        assert_eq!(modals.focus_manager().current(), Some(3));
2978
2979        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2980        assert_eq!(modals.focus_manager().current(), Some(5));
2981
2982        modals.with_focus_graph_mut(|graph| {
2983            let _ = graph.remove(3);
2984        });
2985        assert_eq!(modals.focus_manager().current(), Some(5));
2986
2987        modals.with_focus_graph_mut(|graph| {
2988            graph.insert(make_focus_node(4));
2989        });
2990        assert_eq!(modals.focus_manager().current(), Some(5));
2991
2992        modals.with_focus_graph_mut(|graph| {
2993            graph.insert(make_focus_node(3));
2994        });
2995        assert_eq!(modals.focus_manager().current(), Some(5));
2996
2997        assert!(modals.pop().is_some());
2998        assert_eq!(modals.focus_manager().current(), Some(4));
2999        assert!(modals.is_focus_trapped());
3000
3001        assert!(modals.pop().is_some());
3002        assert_eq!(modals.focus_manager().current(), Some(2));
3003        assert!(modals.is_focus_trapped());
3004    }
3005
3006    #[test]
3007    fn blurred_revalidated_stale_lower_target_before_top_close_does_not_win_on_middle_pop() {
3008        let mut modals = FocusAwareModalStack::new();
3009        modals.with_focus_graph_mut(|graph| {
3010            for id in 1..=5 {
3011                graph.insert(make_focus_node(id));
3012            }
3013        });
3014
3015        modals.focus(1);
3016        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
3017        modals.focus(3);
3018        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
3019
3020        modals.with_focus_graph_mut(|graph| {
3021            let _ = graph.remove(4);
3022        });
3023        assert_eq!(modals.focus_manager().current(), Some(3));
3024
3025        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
3026        assert_eq!(modals.focus_manager().current(), Some(5));
3027
3028        let _ = modals.handle_event(&Event::Focus(false), None);
3029        assert_eq!(modals.focus_manager().current(), None);
3030
3031        modals.with_focus_graph_mut(|graph| {
3032            let _ = graph.remove(3);
3033        });
3034        assert_eq!(modals.focus_manager().current(), None);
3035
3036        modals.with_focus_graph_mut(|graph| {
3037            graph.insert(make_focus_node(4));
3038        });
3039        assert_eq!(modals.focus_manager().current(), None);
3040
3041        modals.with_focus_graph_mut(|graph| {
3042            graph.insert(make_focus_node(3));
3043        });
3044        assert_eq!(modals.focus_manager().current(), None);
3045
3046        assert!(modals.pop().is_some());
3047        assert_eq!(modals.focus_manager().current(), None);
3048        assert!(modals.is_focus_trapped());
3049
3050        let _ = modals.handle_event(&Event::Focus(true), None);
3051        assert_eq!(modals.focus_manager().current(), Some(4));
3052        assert!(modals.is_focus_trapped());
3053
3054        assert!(modals.pop().is_some());
3055        assert_eq!(modals.focus_manager().current(), Some(2));
3056        assert!(modals.is_focus_trapped());
3057    }
3058
3059    #[test]
3060    fn invalidated_lower_selection_retargets_upper_restore_using_group_tab_order() {
3061        let mut modals = FocusAwareModalStack::new();
3062        modals.with_focus_graph_mut(|graph| {
3063            for id in 1..=5 {
3064                graph.insert(make_focus_node(id));
3065            }
3066        });
3067
3068        modals.focus(1);
3069        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 3, 2]);
3070        assert_eq!(modals.focus_manager().current(), Some(2));
3071        modals.focus(4);
3072        assert_eq!(modals.focus_manager().current(), Some(4));
3073
3074        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
3075        assert_eq!(modals.focus_manager().current(), Some(5));
3076
3077        modals.with_focus_graph_mut(|graph| {
3078            let _ = graph.remove(4);
3079        });
3080        assert_eq!(modals.focus_manager().current(), Some(5));
3081
3082        assert!(modals.pop().is_some());
3083        assert_eq!(modals.focus_manager().current(), Some(2));
3084        assert!(modals.is_focus_trapped());
3085    }
3086
3087    #[test]
3088    fn blurred_invalidated_lower_selection_retargets_upper_restore_using_group_tab_order() {
3089        let mut modals = FocusAwareModalStack::new();
3090        modals.with_focus_graph_mut(|graph| {
3091            for id in 1..=5 {
3092                graph.insert(make_focus_node(id));
3093            }
3094        });
3095
3096        modals.focus(1);
3097        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 3, 2]);
3098        assert_eq!(modals.focus_manager().current(), Some(2));
3099        modals.focus(4);
3100        assert_eq!(modals.focus_manager().current(), Some(4));
3101
3102        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
3103        assert_eq!(modals.focus_manager().current(), Some(5));
3104
3105        let _ = modals.handle_event(&Event::Focus(false), None);
3106        assert_eq!(modals.focus_manager().current(), None);
3107
3108        modals.with_focus_graph_mut(|graph| {
3109            let _ = graph.remove(4);
3110        });
3111        assert_eq!(modals.focus_manager().current(), None);
3112
3113        assert!(modals.pop().is_some());
3114        assert_eq!(modals.focus_manager().current(), None);
3115        assert!(modals.is_focus_trapped());
3116
3117        let _ = modals.handle_event(&Event::Focus(true), None);
3118        assert_eq!(modals.focus_manager().current(), Some(2));
3119        assert!(modals.is_focus_trapped());
3120    }
3121
3122    #[test]
3123    fn invalidated_negative_tabindex_lower_selection_retargets_upper_restore_metadata() {
3124        let mut modals = FocusAwareModalStack::new();
3125        modals
3126            .focus_manager_mut()
3127            .graph_mut()
3128            .insert(make_focus_node(1));
3129        modals
3130            .focus_manager_mut()
3131            .graph_mut()
3132            .insert(FocusNode::new(3, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
3133        modals
3134            .focus_manager_mut()
3135            .graph_mut()
3136            .insert(FocusNode::new(4, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
3137        modals
3138            .focus_manager_mut()
3139            .graph_mut()
3140            .insert(make_focus_node(5));
3141
3142        modals.focus(1);
3143        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 3]);
3144        assert_eq!(modals.focus_manager().current(), Some(4));
3145
3146        let upper_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
3147        assert_eq!(modals.focus_manager().current(), Some(5));
3148
3149        modals.with_focus_graph_mut(|graph| {
3150            let _ = graph.remove(4);
3151        });
3152
3153        let upper_return_focus = modals
3154            .stack
3155            .focus_modal_specs_in_order()
3156            .into_iter()
3157            .find(|(modal_id, _)| *modal_id == upper_id)
3158            .map(|(_, spec)| spec.return_focus);
3159        assert_eq!(upper_return_focus, Some(Some(3)));
3160    }
3161
3162    #[test]
3163    fn blurred_invalidated_negative_tabindex_lower_selection_retargets_upper_restore_metadata() {
3164        let mut modals = FocusAwareModalStack::new();
3165        modals
3166            .focus_manager_mut()
3167            .graph_mut()
3168            .insert(make_focus_node(1));
3169        modals
3170            .focus_manager_mut()
3171            .graph_mut()
3172            .insert(FocusNode::new(3, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
3173        modals
3174            .focus_manager_mut()
3175            .graph_mut()
3176            .insert(FocusNode::new(4, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
3177        modals
3178            .focus_manager_mut()
3179            .graph_mut()
3180            .insert(make_focus_node(5));
3181
3182        modals.focus(1);
3183        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 3]);
3184        assert_eq!(modals.focus_manager().current(), Some(4));
3185
3186        let upper_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
3187        assert_eq!(modals.focus_manager().current(), Some(5));
3188
3189        let _ = modals.handle_event(&Event::Focus(false), None);
3190        assert_eq!(modals.focus_manager().current(), None);
3191
3192        modals.with_focus_graph_mut(|graph| {
3193            let _ = graph.remove(4);
3194        });
3195
3196        let upper_return_focus = modals
3197            .stack
3198            .focus_modal_specs_in_order()
3199            .into_iter()
3200            .find(|(modal_id, _)| *modal_id == upper_id)
3201            .map(|(_, spec)| spec.return_focus);
3202        assert_eq!(upper_return_focus, Some(Some(3)));
3203    }
3204
3205    #[test]
3206    fn blurred_reactivated_inactive_top_modal_tracks_graph_restored_underlying_selection() {
3207        let mut modals = FocusAwareModalStack::new();
3208        modals.with_focus_graph_mut(|graph| {
3209            for id in 1..=7 {
3210                graph.insert(make_focus_node(id));
3211            }
3212        });
3213
3214        modals.focus(1);
3215        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
3216        modals.focus(3);
3217        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
3218        modals.focus(5);
3219        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![6]);
3220        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![7]);
3221
3222        modals.with_focus_graph_mut(|graph| {
3223            let _ = graph.remove(7);
3224        });
3225        let _ = modals.handle_event(&Event::Focus(false), None);
3226        assert_eq!(modals.focus_manager().current(), None);
3227
3228        modals.with_focus_graph_mut(|graph| {
3229            let _ = graph.remove(6);
3230        });
3231        assert_eq!(modals.focus_manager().current(), None);
3232
3233        modals.with_focus_graph_mut(|graph| {
3234            graph.insert(make_focus_node(7));
3235        });
3236        let _ = modals.handle_event(&Event::Focus(true), None);
3237        assert_eq!(modals.focus_manager().current(), Some(7));
3238        assert!(modals.is_focus_trapped());
3239
3240        assert!(modals.pop().is_some());
3241        assert_eq!(modals.focus_manager().current(), Some(5));
3242        assert!(modals.is_focus_trapped());
3243    }
3244
3245    #[test]
3246    fn depth_tracks_push_pop() {
3247        let mut modals = FocusAwareModalStack::new();
3248        assert_eq!(modals.depth(), 0);
3249        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
3250        assert_eq!(modals.depth(), 1);
3251        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
3252        assert_eq!(modals.depth(), 2);
3253        modals.pop();
3254        assert_eq!(modals.depth(), 1);
3255    }
3256
3257    #[test]
3258    fn pop_empty_stack_returns_none() {
3259        let mut modals = FocusAwareModalStack::new();
3260        assert!(modals.pop().is_none());
3261    }
3262}