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_skips_closed_modal_focus_ids_when_background_focus_disappears() {
866        let mut modals = FocusAwareModalStack::new();
867        modals
868            .focus_manager_mut()
869            .graph_mut()
870            .insert(make_focus_node(1));
871        modals
872            .focus_manager_mut()
873            .graph_mut()
874            .insert(make_focus_node(50));
875        modals
876            .focus_manager_mut()
877            .graph_mut()
878            .insert(make_focus_node(100));
879
880        modals.focus_manager_mut().focus(100);
881        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
882        let _ = modals.focus_manager_mut().graph_mut().remove(100);
883
884        modals.pop();
885        assert_eq!(modals.focus_manager().current(), Some(50));
886        assert!(!modals.is_focus_trapped());
887    }
888
889    #[test]
890    fn nested_modals_restore_correctly() {
891        let mut modals = FocusAwareModalStack::new();
892
893        // Add focusable nodes
894        for id in 1..=6 {
895            modals
896                .focus_manager_mut()
897                .graph_mut()
898                .insert(make_focus_node(id));
899        }
900
901        // Initial focus
902        modals.focus_manager_mut().focus(1);
903
904        // First modal traps to nodes 2, 3
905        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
906        assert_eq!(modals.focus_manager().current(), Some(2));
907
908        // Second modal traps to nodes 4, 5, 6
909        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5, 6]);
910        assert_eq!(modals.focus_manager().current(), Some(4));
911
912        // Pop second modal - back to first modal's focus (node 2)
913        modals.pop();
914        assert_eq!(modals.focus_manager().current(), Some(2));
915
916        // Pop first modal - back to original focus (node 1)
917        modals.pop();
918        assert_eq!(modals.focus_manager().current(), Some(1));
919        assert!(!modals.is_focus_trapped());
920    }
921
922    #[test]
923    fn pop_restores_none_when_modal_opened_without_focus() {
924        let mut modals = FocusAwareModalStack::new();
925        modals
926            .focus_manager_mut()
927            .graph_mut()
928            .insert(make_focus_node(1));
929
930        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
931        assert_eq!(modals.focus_manager().current(), Some(1));
932
933        modals.pop();
934        assert_eq!(modals.focus_manager().current(), None);
935        assert!(!modals.is_focus_trapped());
936    }
937
938    #[test]
939    fn resync_focus_state_recovers_after_manual_stack_mutation() {
940        let mut modals = FocusAwareModalStack::new();
941        modals
942            .focus_manager_mut()
943            .graph_mut()
944            .insert(make_focus_node(1));
945        modals
946            .focus_manager_mut()
947            .graph_mut()
948            .insert(make_focus_node(2));
949        modals
950            .focus_manager_mut()
951            .graph_mut()
952            .insert(make_focus_node(100));
953
954        modals.focus_manager_mut().focus(100);
955        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
956        assert!(modals.is_focus_trapped());
957        assert_eq!(modals.focus_manager().current(), Some(1));
958
959        let result = modals.stack_mut().pop();
960        assert!(result.is_some());
961        assert!(modals.is_focus_trapped());
962
963        modals.resync_focus_state();
964        assert!(!modals.is_focus_trapped());
965        assert_eq!(modals.focus_manager().current(), Some(100));
966    }
967
968    #[test]
969    fn handle_event_escape_restores_focus() {
970        let mut modals = FocusAwareModalStack::new();
971
972        // Add focusable nodes
973        modals
974            .focus_manager_mut()
975            .graph_mut()
976            .insert(make_focus_node(1));
977        modals
978            .focus_manager_mut()
979            .graph_mut()
980            .insert(make_focus_node(2));
981
982        // Focus node 2
983        modals.focus_manager_mut().focus(2);
984
985        // Push modal
986        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
987        assert_eq!(modals.focus_manager().current(), Some(1));
988
989        // Escape closes modal
990        let escape = Event::Key(KeyEvent {
991            code: KeyCode::Escape,
992            modifiers: Modifiers::empty(),
993            kind: KeyEventKind::Press,
994        });
995
996        let result = modals.handle_event(&escape, None);
997        assert!(result.is_some());
998        assert_eq!(modals.focus_manager().current(), Some(2));
999    }
1000
1001    #[test]
1002    fn handle_event_focus_loss_blurs_current_focus() {
1003        let mut modals = FocusAwareModalStack::new();
1004        modals
1005            .focus_manager_mut()
1006            .graph_mut()
1007            .insert(make_focus_node(1));
1008        modals.focus_manager_mut().focus(1);
1009        let _ = modals.focus_manager_mut().take_focus_event();
1010
1011        let result = modals.handle_event(&Event::Focus(false), None);
1012        assert!(result.is_none());
1013        assert_eq!(modals.focus_manager().current(), None);
1014        assert_eq!(
1015            modals.focus_manager_mut().take_focus_event(),
1016            Some(crate::focus::FocusEvent::FocusLost { id: 1 })
1017        );
1018    }
1019
1020    #[test]
1021    fn handle_event_focus_gain_restores_trapped_focus() {
1022        let mut modals = FocusAwareModalStack::new();
1023        modals
1024            .focus_manager_mut()
1025            .graph_mut()
1026            .insert(make_focus_node(1));
1027        modals
1028            .focus_manager_mut()
1029            .graph_mut()
1030            .insert(make_focus_node(2));
1031        modals
1032            .focus_manager_mut()
1033            .graph_mut()
1034            .insert(make_focus_node(3));
1035        modals.focus_manager_mut().focus(3);
1036
1037        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
1038        assert_eq!(modals.focus_manager().current(), Some(1));
1039
1040        let _ = modals.handle_event(&Event::Focus(false), None);
1041        assert_eq!(modals.focus_manager().current(), None);
1042
1043        let result = modals.handle_event(&Event::Focus(true), None);
1044        assert!(result.is_none());
1045        assert_eq!(modals.focus_manager().current(), Some(1));
1046    }
1047
1048    #[test]
1049    fn push_with_trap_autofocuses_negative_tabindex_member_when_modal_has_no_tabbable_nodes() {
1050        let mut modals = FocusAwareModalStack::new();
1051
1052        modals
1053            .focus_manager_mut()
1054            .graph_mut()
1055            .insert(make_focus_node(1));
1056        modals
1057            .focus_manager_mut()
1058            .graph_mut()
1059            .insert(FocusNode::new(2, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
1060        modals.focus_manager_mut().focus(1);
1061
1062        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1063
1064        assert!(modals.is_focus_trapped());
1065        assert_eq!(modals.focus_manager().current(), Some(2));
1066    }
1067
1068    #[test]
1069    fn push_with_trap_blurred_restores_negative_tabindex_member_on_focus_gain() {
1070        let mut modals = FocusAwareModalStack::new();
1071
1072        modals
1073            .focus_manager_mut()
1074            .graph_mut()
1075            .insert(make_focus_node(1));
1076        modals
1077            .focus_manager_mut()
1078            .graph_mut()
1079            .insert(FocusNode::new(2, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
1080        modals.focus_manager_mut().focus(1);
1081        let _ = modals.handle_event(&Event::Focus(false), None);
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(), None);
1087
1088        let _ = modals.handle_event(&Event::Focus(true), None);
1089        assert_eq!(modals.focus_manager().current(), Some(2));
1090    }
1091
1092    #[test]
1093    fn push_without_trap_no_focus_change() {
1094        let mut modals = FocusAwareModalStack::new();
1095
1096        // Add focusable nodes
1097        modals
1098            .focus_manager_mut()
1099            .graph_mut()
1100            .insert(make_focus_node(1));
1101        modals
1102            .focus_manager_mut()
1103            .graph_mut()
1104            .insert(make_focus_node(2));
1105
1106        // Focus node 2
1107        modals.focus_manager_mut().focus(2);
1108
1109        // Push modal without trap
1110        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
1111
1112        // Focus should not change
1113        assert!(!modals.is_focus_trapped());
1114        assert_eq!(modals.focus_manager().current(), Some(2));
1115    }
1116
1117    #[test]
1118    fn pop_all_restores_all_focus() {
1119        let mut modals = FocusAwareModalStack::new();
1120
1121        // Add focusable nodes
1122        for id in 1..=4 {
1123            modals
1124                .focus_manager_mut()
1125                .graph_mut()
1126                .insert(make_focus_node(id));
1127        }
1128
1129        // Initial focus
1130        modals.focus_manager_mut().focus(1);
1131
1132        // Push multiple modals
1133        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1134        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1135        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
1136
1137        assert_eq!(modals.depth(), 3);
1138        assert_eq!(modals.focus_manager().current(), Some(4));
1139
1140        // Pop all
1141        let results = modals.pop_all();
1142        assert_eq!(results.len(), 3);
1143        assert!(modals.is_empty());
1144        assert!(!modals.is_focus_trapped());
1145        assert_eq!(modals.focus_manager().current(), Some(1));
1146    }
1147
1148    #[test]
1149    fn pop_all_restores_base_focus_without_intermediate_hop() {
1150        let mut modals = FocusAwareModalStack::new();
1151        modals.with_focus_graph_mut(|graph| {
1152            for id in 1..=5 {
1153                graph.insert(make_focus_node(id));
1154            }
1155        });
1156
1157        modals.focus(1);
1158        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1159        modals.focus(3);
1160        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
1161        modals.focus(5);
1162        let _ = modals.focus_manager_mut().take_focus_event();
1163        let before = modals.focus_manager().focus_change_count();
1164
1165        let results = modals.pop_all();
1166
1167        assert_eq!(results.len(), 2);
1168        assert_eq!(modals.focus_manager().current(), Some(1));
1169        assert_eq!(
1170            modals.focus_manager_mut().take_focus_event(),
1171            Some(crate::focus::FocusEvent::FocusMoved { from: 5, to: 1 })
1172        );
1173        assert_eq!(modals.focus_manager().focus_change_count(), before + 1);
1174        assert!(!modals.is_focus_trapped());
1175    }
1176
1177    #[test]
1178    fn pop_id_restores_none_when_last_modal_opened_without_focus() {
1179        let mut modals = FocusAwareModalStack::new();
1180        modals
1181            .focus_manager_mut()
1182            .graph_mut()
1183            .insert(make_focus_node(1));
1184
1185        let modal_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
1186        assert_eq!(modals.focus_manager().current(), Some(1));
1187
1188        let _ = modals.pop_id(modal_id);
1189        assert_eq!(modals.focus_manager().current(), None);
1190        assert!(!modals.is_focus_trapped());
1191    }
1192
1193    #[test]
1194    fn pop_id_rebuild_preserves_unfocused_base_state_for_remaining_modal() {
1195        let mut modals = FocusAwareModalStack::new();
1196        modals
1197            .focus_manager_mut()
1198            .graph_mut()
1199            .insert(make_focus_node(1));
1200        modals
1201            .focus_manager_mut()
1202            .graph_mut()
1203            .insert(make_focus_node(2));
1204
1205        let lower_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
1206        let upper_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1207        assert_eq!(modals.focus_manager().current(), Some(2));
1208
1209        let removed = modals.pop_id(lower_id);
1210        assert_eq!(removed.map(|result| result.id), Some(lower_id));
1211        assert_eq!(modals.focus_manager().current(), Some(2));
1212        assert!(modals.is_focus_trapped());
1213
1214        let closed = modals.pop();
1215        assert_eq!(closed.map(|result| result.id), Some(upper_id));
1216        assert_eq!(modals.focus_manager().current(), None);
1217        assert!(!modals.is_focus_trapped());
1218    }
1219
1220    #[test]
1221    fn tab_navigation_trapped_in_modal() {
1222        let mut modals = FocusAwareModalStack::new();
1223
1224        // Add focusable nodes
1225        for id in 1..=5 {
1226            modals
1227                .focus_manager_mut()
1228                .graph_mut()
1229                .insert(make_focus_node(id));
1230        }
1231
1232        // Push modal with nodes 2 and 3
1233        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1234
1235        // Focus should be on 2
1236        assert_eq!(modals.focus_manager().current(), Some(2));
1237
1238        // Tab forward should go to 3
1239        modals.focus_manager_mut().focus_next();
1240        assert_eq!(modals.focus_manager().current(), Some(3));
1241
1242        // Tab forward should wrap to 2 (trapped)
1243        modals.focus_manager_mut().focus_next();
1244        assert_eq!(modals.focus_manager().current(), Some(2));
1245
1246        // Attempt to focus outside trap should fail
1247        assert!(modals.focus_manager_mut().focus(5).is_none());
1248        assert_eq!(modals.focus_manager().current(), Some(2));
1249    }
1250
1251    #[test]
1252    fn empty_focus_group_no_panic() {
1253        let mut modals = FocusAwareModalStack::new();
1254
1255        // Push modal with empty focus group (edge case).
1256        // The trap is NOT pushed because the group has no focusable members,
1257        // preventing a deadlock where no widget could receive focus.
1258        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![]);
1259
1260        // Should not panic, and focus should NOT be trapped (empty group).
1261        assert!(!modals.is_focus_trapped());
1262
1263        // Pop should still work
1264        modals.pop();
1265        assert!(!modals.is_focus_trapped());
1266    }
1267
1268    #[test]
1269    fn rejected_empty_trap_does_not_leave_focus_group_behind() {
1270        let mut modals = FocusAwareModalStack::new();
1271        modals
1272            .focus_manager_mut()
1273            .graph_mut()
1274            .insert(make_focus_node(1));
1275        modals.focus_manager_mut().focus(1);
1276        let group_count_before = modals.focus_manager().group_count();
1277
1278        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![]);
1279
1280        assert!(!modals.is_focus_trapped());
1281        assert_eq!(modals.focus_manager().group_count(), group_count_before);
1282        assert_eq!(modals.focus_manager().current(), Some(1));
1283    }
1284
1285    #[test]
1286    fn late_registered_focus_ids_activate_modal_trap_and_restore_latest_background_selection() {
1287        let mut modals = FocusAwareModalStack::new();
1288        modals.with_focus_graph_mut(|graph| {
1289            graph.insert(make_focus_node(50));
1290            graph.insert(make_focus_node(100));
1291        });
1292
1293        modals.focus(100);
1294        let modal_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
1295        assert!(!modals.is_focus_trapped());
1296        assert_eq!(modals.focus_manager().current(), Some(100));
1297
1298        modals.focus(50);
1299        assert_eq!(modals.focus_manager().current(), Some(50));
1300
1301        modals.with_focus_graph_mut(|graph| {
1302            graph.insert(make_focus_node(1));
1303        });
1304        assert!(modals.is_focus_trapped());
1305        assert_eq!(modals.focus_manager().current(), Some(1));
1306
1307        assert!(modals.pop_id(modal_id).is_some());
1308        assert_eq!(modals.focus_manager().current(), Some(50));
1309        assert!(!modals.is_focus_trapped());
1310    }
1311
1312    #[test]
1313    fn blurred_pop_all_after_late_trap_activation_restores_background_focus_on_gain() {
1314        let mut modals = FocusAwareModalStack::new();
1315        modals.with_focus_graph_mut(|graph| {
1316            graph.insert(make_focus_node(50));
1317            graph.insert(make_focus_node(100));
1318        });
1319
1320        modals.focus(100);
1321        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
1322        modals.focus(50);
1323
1324        modals.with_focus_graph_mut(|graph| {
1325            graph.insert(make_focus_node(1));
1326        });
1327        assert_eq!(modals.focus_manager().current(), Some(1));
1328        assert!(modals.is_focus_trapped());
1329
1330        let _ = modals.handle_event(&Event::Focus(false), None);
1331        assert_eq!(modals.focus_manager().current(), None);
1332
1333        let results = modals.pop_all();
1334        assert_eq!(results.len(), 1);
1335        assert_eq!(modals.focus_manager().current(), None);
1336        assert!(!modals.is_focus_trapped());
1337
1338        let _ = modals.handle_event(&Event::Focus(true), None);
1339        assert_eq!(modals.focus_manager().current(), Some(50));
1340    }
1341
1342    #[test]
1343    fn push_with_trap_does_not_collide_with_existing_group_ids() {
1344        let mut modals = FocusAwareModalStack::new();
1345        modals
1346            .focus_manager_mut()
1347            .graph_mut()
1348            .insert(make_focus_node(1));
1349        modals
1350            .focus_manager_mut()
1351            .graph_mut()
1352            .insert(make_focus_node(99));
1353        modals
1354            .focus_manager_mut()
1355            .graph_mut()
1356            .insert(make_focus_node(100));
1357
1358        let reserved_group_id = FOCUS_GROUP_COUNTER.load(Ordering::Relaxed);
1359        modals
1360            .focus_manager_mut()
1361            .create_group(reserved_group_id, vec![99]);
1362        modals.focus_manager_mut().focus(100);
1363
1364        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
1365        let _ = modals.pop().unwrap();
1366
1367        assert!(modals.focus_manager_mut().push_trap(reserved_group_id));
1368        assert_eq!(modals.focus_manager().current(), Some(99));
1369    }
1370
1371    #[test]
1372    fn pop_id_non_top_modal_rebuilds_focus_traps() {
1373        let mut modals = FocusAwareModalStack::new();
1374
1375        // Add focusable nodes
1376        for id in 1..=6 {
1377            modals
1378                .focus_manager_mut()
1379                .graph_mut()
1380                .insert(make_focus_node(id));
1381        }
1382
1383        // Initial focus
1384        modals.focus_manager_mut().focus(1);
1385
1386        // Push three modals with focus traps.
1387        let id1 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1388        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1389        let _id3 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
1390
1391        // Focus should be on node 4 (top modal)
1392        assert_eq!(modals.focus_manager().current(), Some(4));
1393
1394        // Pop the BOTTOM modal (id1) by ID - this is non-LIFO.
1395        modals.pop_id(id1);
1396
1397        // Focus should still be on the top modal.
1398        assert_eq!(modals.focus_manager().current(), Some(4));
1399        assert_eq!(modals.depth(), 2);
1400        assert!(modals.is_focus_trapped());
1401
1402        // Pop remaining modals normally. Focus should restore as if the removed modal never
1403        // existed: top -> next modal -> original background focus.
1404        modals.pop();
1405        assert_eq!(modals.focus_manager().current(), Some(3));
1406
1407        modals.pop();
1408        assert_eq!(modals.focus_manager().current(), Some(1));
1409        assert!(modals.is_empty());
1410        assert!(!modals.is_focus_trapped());
1411    }
1412
1413    #[test]
1414    fn pop_id_middle_modal_retargets_upper_return_focus() {
1415        let mut modals = FocusAwareModalStack::new();
1416
1417        for id in 1..=6 {
1418            modals
1419                .focus_manager_mut()
1420                .graph_mut()
1421                .insert(make_focus_node(id));
1422        }
1423
1424        modals.focus_manager_mut().focus(1);
1425
1426        let _id1 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1427        let id2 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1428        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
1429
1430        assert_eq!(modals.focus_manager().current(), Some(4));
1431
1432        // Remove the middle modal. The top modal should now restore to modal1's focus.
1433        modals.pop_id(id2);
1434        assert_eq!(modals.focus_manager().current(), Some(4));
1435        assert_eq!(modals.depth(), 2);
1436
1437        modals.pop();
1438        assert_eq!(modals.focus_manager().current(), Some(2));
1439
1440        modals.pop();
1441        assert_eq!(modals.focus_manager().current(), Some(1));
1442        assert!(!modals.is_focus_trapped());
1443    }
1444
1445    #[test]
1446    fn pop_id_rebuild_does_not_pollute_focus_history() {
1447        let mut modals = FocusAwareModalStack::new();
1448
1449        for id in 1..=6 {
1450            modals
1451                .focus_manager_mut()
1452                .graph_mut()
1453                .insert(make_focus_node(id));
1454        }
1455
1456        modals.focus_manager_mut().focus(1);
1457        modals.focus_manager_mut().focus(6);
1458
1459        let id1 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1460        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1461
1462        modals.pop_id(id1);
1463        assert_eq!(modals.focus_manager().current(), Some(3));
1464
1465        modals.pop();
1466        assert_eq!(modals.focus_manager().current(), Some(6));
1467        assert!(modals.focus_manager_mut().focus_back());
1468        assert_eq!(modals.focus_manager().current(), Some(1));
1469        assert!(!modals.focus_manager_mut().focus_back());
1470    }
1471
1472    #[test]
1473    fn pop_id_top_modal_restores_focus_correctly() {
1474        let mut modals = FocusAwareModalStack::new();
1475
1476        // Add focusable nodes
1477        for id in 1..=4 {
1478            modals
1479                .focus_manager_mut()
1480                .graph_mut()
1481                .insert(make_focus_node(id));
1482        }
1483
1484        // Initial focus
1485        modals.focus_manager_mut().focus(1);
1486
1487        // Push two modals
1488        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1489        let id2 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1490
1491        assert_eq!(modals.focus_manager().current(), Some(3));
1492
1493        // Pop the TOP modal by ID - this should work correctly
1494        modals.pop_id(id2);
1495
1496        // Focus should restore to modal1's focus (2)
1497        assert_eq!(modals.focus_manager().current(), Some(2));
1498        assert!(modals.is_focus_trapped()); // Still in modal1's trap
1499
1500        // Pop the last modal
1501        modals.pop();
1502        assert_eq!(modals.focus_manager().current(), Some(1));
1503        assert!(!modals.is_focus_trapped());
1504    }
1505
1506    #[test]
1507    fn pop_id_top_modal_preserves_underlying_selected_control() {
1508        let mut modals = FocusAwareModalStack::new();
1509        modals.with_focus_graph_mut(|graph| {
1510            for id in 1..=5 {
1511                graph.insert(make_focus_node(id));
1512            }
1513        });
1514
1515        modals.focus(1);
1516        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1517        modals.focus(3);
1518        let upper_id =
1519            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
1520        modals.focus(5);
1521        let _ = modals.focus_manager_mut().take_focus_event();
1522        let before = modals.focus_manager().focus_change_count();
1523
1524        assert!(modals.pop_id(upper_id).is_some());
1525        assert_eq!(modals.focus_manager().current(), Some(3));
1526        assert_eq!(
1527            modals.focus_manager_mut().take_focus_event(),
1528            Some(crate::focus::FocusEvent::FocusMoved { from: 5, to: 3 })
1529        );
1530        assert_eq!(modals.focus_manager().focus_change_count(), before + 1);
1531        assert!(modals.is_focus_trapped());
1532
1533        let _ = modals.pop();
1534        assert_eq!(modals.focus_manager().current(), Some(1));
1535    }
1536
1537    #[test]
1538    fn pop_removes_closed_modal_focus_group() {
1539        let mut modals = FocusAwareModalStack::new();
1540        modals
1541            .focus_manager_mut()
1542            .graph_mut()
1543            .insert(make_focus_node(1));
1544        modals
1545            .focus_manager_mut()
1546            .graph_mut()
1547            .insert(make_focus_node(2));
1548
1549        modals.focus_manager_mut().focus(1);
1550        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1551
1552        let result = modals.pop().unwrap();
1553        let group_id = result.focus_group_id.unwrap();
1554
1555        assert!(!modals.focus_manager_mut().push_trap(group_id));
1556        assert!(!modals.is_focus_trapped());
1557        assert_eq!(modals.focus_manager().current(), Some(1));
1558    }
1559
1560    #[test]
1561    fn pop_last_modal_clears_invalid_stale_focus_when_no_fallback_exists() {
1562        let mut modals = FocusAwareModalStack::new();
1563        modals
1564            .focus_manager_mut()
1565            .graph_mut()
1566            .insert(make_focus_node(1));
1567        modals
1568            .focus_manager_mut()
1569            .graph_mut()
1570            .insert(make_focus_node(2));
1571
1572        modals.focus_manager_mut().focus(1);
1573        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1574        assert_eq!(modals.focus_manager().current(), Some(2));
1575
1576        let _ = modals.focus_manager_mut().graph_mut().remove(1);
1577        let _ = modals.focus_manager_mut().graph_mut().remove(2);
1578
1579        modals.pop();
1580        assert_eq!(modals.focus_manager().current(), None);
1581        assert!(!modals.is_focus_trapped());
1582    }
1583
1584    #[test]
1585    fn default_creates_empty_stack() {
1586        let modals = FocusAwareModalStack::default();
1587        assert!(modals.is_empty());
1588        assert_eq!(modals.depth(), 0);
1589        assert!(!modals.is_focus_trapped());
1590    }
1591
1592    #[test]
1593    fn with_focus_manager_uses_provided() {
1594        let mut fm = FocusManager::new();
1595        fm.graph_mut().insert(make_focus_node(42));
1596        fm.focus(42);
1597
1598        let modals = FocusAwareModalStack::with_focus_manager(fm);
1599        assert!(modals.is_empty());
1600        assert_eq!(modals.focus_manager().current(), Some(42));
1601    }
1602
1603    #[test]
1604    fn with_focus_manager_rejects_pretrapped_manager() {
1605        let mut fm = FocusManager::new();
1606        fm.graph_mut().insert(make_focus_node(1));
1607        fm.focus(1);
1608        fm.create_group(7, vec![1]);
1609        assert!(fm.push_trap(7));
1610
1611        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1612            let _ = FocusAwareModalStack::with_focus_manager(fm);
1613        }));
1614        assert!(result.is_err());
1615    }
1616
1617    #[test]
1618    fn stack_accessors() {
1619        let mut modals = FocusAwareModalStack::new();
1620        assert!(modals.stack().is_empty());
1621        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
1622        assert!(!modals.stack().is_empty());
1623        assert_eq!(modals.stack_mut().depth(), 1);
1624    }
1625
1626    #[test]
1627    fn with_focus_graph_mut_resyncs_after_panic() {
1628        let mut modals = FocusAwareModalStack::new();
1629
1630        modals.with_focus_graph_mut(|graph| {
1631            graph.insert(make_focus_node(1));
1632            graph.insert(make_focus_node(2));
1633        });
1634        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
1635        assert_eq!(modals.focus_manager().current(), Some(1));
1636
1637        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1638            modals.with_focus_graph_mut(|graph| {
1639                let _ = graph.remove(1);
1640                panic!("boom");
1641            });
1642        }));
1643        assert!(result.is_err());
1644        assert_eq!(modals.focus_manager().current(), Some(2));
1645        assert!(modals.is_focus_trapped());
1646    }
1647
1648    #[test]
1649    fn with_focus_graph_mut_repairs_invalid_focus_without_modals() {
1650        let mut modals = FocusAwareModalStack::new();
1651        modals.with_focus_graph_mut(|graph| {
1652            graph.insert(make_focus_node(1));
1653            graph.insert(make_focus_node(2));
1654        });
1655        modals.focus(2);
1656        assert_eq!(modals.focus_manager().current(), Some(2));
1657
1658        modals.with_focus_graph_mut(|graph| {
1659            let _ = graph.remove(2);
1660        });
1661
1662        assert_eq!(modals.focus_manager().current(), Some(1));
1663        assert!(!modals.is_focus_trapped());
1664    }
1665
1666    #[test]
1667    fn with_focus_graph_mut_does_not_restore_focus_while_host_blurred() {
1668        let mut modals = FocusAwareModalStack::new();
1669        modals.with_focus_graph_mut(|graph| {
1670            graph.insert(make_focus_node(1));
1671            graph.insert(make_focus_node(2));
1672        });
1673        modals.focus(2);
1674        let _ = modals.handle_event(&Event::Focus(false), None);
1675        assert_eq!(modals.focus_manager().current(), None);
1676
1677        modals.with_focus_graph_mut(|graph| {
1678            let _ = graph.remove(2);
1679        });
1680
1681        assert_eq!(modals.focus_manager().current(), None);
1682    }
1683
1684    #[test]
1685    fn focus_call_while_host_blurred_defers_until_focus_gain() {
1686        let mut modals = FocusAwareModalStack::new();
1687        modals.with_focus_graph_mut(|graph| {
1688            graph.insert(make_focus_node(1));
1689            graph.insert(make_focus_node(2));
1690            graph.insert(make_focus_node(3));
1691        });
1692        modals.focus(1);
1693        let _ = modals.focus_manager_mut().take_focus_event();
1694
1695        let _ = modals.handle_event(&Event::Focus(false), None);
1696        assert_eq!(modals.focus_manager().current(), None);
1697
1698        assert_eq!(modals.focus(3), Some(1));
1699        assert_eq!(modals.focus_manager().current(), None);
1700        assert_eq!(
1701            modals.focus_manager_mut().take_focus_event(),
1702            Some(crate::focus::FocusEvent::FocusLost { id: 1 })
1703        );
1704
1705        let _ = modals.handle_event(&Event::Focus(true), None);
1706        assert_eq!(modals.focus_manager().current(), Some(3));
1707        assert_eq!(
1708            modals.focus_manager_mut().take_focus_event(),
1709            Some(crate::focus::FocusEvent::FocusGained { id: 3 })
1710        );
1711    }
1712
1713    #[test]
1714    fn pop_while_host_blurred_defers_base_focus_restore_until_focus_gain() {
1715        let mut modals = FocusAwareModalStack::new();
1716        modals.with_focus_graph_mut(|graph| {
1717            graph.insert(make_focus_node(1));
1718            graph.insert(make_focus_node(2));
1719        });
1720        modals.focus(1);
1721        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1722        assert_eq!(modals.focus_manager().current(), Some(2));
1723
1724        let _ = modals.handle_event(&Event::Focus(false), None);
1725        assert_eq!(modals.focus_manager().current(), None);
1726
1727        let result = modals.pop();
1728        assert!(result.is_some());
1729        assert_eq!(modals.focus_manager().current(), None);
1730        assert!(!modals.is_focus_trapped());
1731
1732        let _ = modals.handle_event(&Event::Focus(true), None);
1733        assert_eq!(modals.focus_manager().current(), Some(1));
1734    }
1735
1736    #[test]
1737    fn pop_id_last_modal_while_host_blurred_restores_base_focus_on_focus_gain() {
1738        let mut modals = FocusAwareModalStack::new();
1739        modals.with_focus_graph_mut(|graph| {
1740            graph.insert(make_focus_node(1));
1741            graph.insert(make_focus_node(2));
1742        });
1743        modals.focus(1);
1744        let modal_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1745        assert_eq!(modals.focus_manager().current(), Some(2));
1746
1747        let _ = modals.handle_event(&Event::Focus(false), None);
1748        assert_eq!(modals.focus_manager().current(), None);
1749
1750        assert!(modals.pop_id(modal_id).is_some());
1751        assert_eq!(modals.focus_manager().current(), None);
1752        assert!(!modals.is_focus_trapped());
1753
1754        let _ = modals.handle_event(&Event::Focus(true), None);
1755        assert_eq!(modals.focus_manager().current(), Some(1));
1756    }
1757
1758    #[test]
1759    fn pop_id_top_modal_while_host_blurred_restores_underlying_modal_selection_on_focus_gain() {
1760        let mut modals = FocusAwareModalStack::new();
1761        modals.with_focus_graph_mut(|graph| {
1762            for id in 1..=5 {
1763                graph.insert(make_focus_node(id));
1764            }
1765        });
1766        modals.focus(1);
1767        let _lower_id =
1768            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1769        modals.focus(3);
1770        let upper_id =
1771            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
1772        modals.focus(5);
1773        let _ = modals.focus_manager_mut().take_focus_event();
1774
1775        let _ = modals.handle_event(&Event::Focus(false), None);
1776        assert_eq!(modals.focus_manager().current(), None);
1777
1778        assert!(modals.pop_id(upper_id).is_some());
1779        assert_eq!(modals.focus_manager().current(), None);
1780        assert!(modals.is_focus_trapped());
1781
1782        let _ = modals.handle_event(&Event::Focus(true), None);
1783        assert_eq!(modals.focus_manager().current(), Some(3));
1784    }
1785
1786    #[test]
1787    fn pop_all_while_host_blurred_restores_base_focus_on_focus_gain() {
1788        let mut modals = FocusAwareModalStack::new();
1789        modals.with_focus_graph_mut(|graph| {
1790            graph.insert(make_focus_node(1));
1791            graph.insert(make_focus_node(2));
1792            graph.insert(make_focus_node(3));
1793        });
1794        modals.focus(1);
1795        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1796        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
1797        assert_eq!(modals.focus_manager().current(), Some(3));
1798
1799        let _ = modals.handle_event(&Event::Focus(false), None);
1800        assert_eq!(modals.focus_manager().current(), None);
1801
1802        let results = modals.pop_all();
1803        assert_eq!(results.len(), 2);
1804        assert_eq!(modals.focus_manager().current(), None);
1805        assert!(!modals.is_focus_trapped());
1806
1807        let _ = modals.handle_event(&Event::Focus(true), None);
1808        assert_eq!(modals.focus_manager().current(), Some(1));
1809    }
1810
1811    #[test]
1812    fn focus_gain_after_blurred_pop_restores_base_focus_without_intermediate_hop() {
1813        let mut modals = FocusAwareModalStack::new();
1814        modals.with_focus_graph_mut(|graph| {
1815            graph.insert(make_focus_node(1));
1816            graph.insert(make_focus_node(5));
1817            graph.insert(make_focus_node(10));
1818        });
1819        modals.focus(5);
1820        let _ = modals.focus_manager_mut().take_focus_event();
1821        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![10]);
1822        let _ = modals.focus_manager_mut().take_focus_event();
1823
1824        let _ = modals.handle_event(&Event::Focus(false), None);
1825        let _ = modals.focus_manager_mut().take_focus_event();
1826
1827        let result = modals.pop();
1828        assert!(result.is_some());
1829        assert_eq!(modals.focus_manager().current(), None);
1830
1831        let before = modals.focus_manager().focus_change_count();
1832        let _ = modals.handle_event(&Event::Focus(true), None);
1833
1834        assert_eq!(modals.focus_manager().current(), Some(5));
1835        assert_eq!(
1836            modals.focus_manager_mut().take_focus_event(),
1837            Some(crate::focus::FocusEvent::FocusGained { id: 5 })
1838        );
1839        assert_eq!(modals.focus_manager().focus_change_count(), before + 1);
1840    }
1841
1842    #[test]
1843    fn blurred_background_focus_change_after_last_modal_pop_overrides_stale_base_focus() {
1844        let mut modals = FocusAwareModalStack::new();
1845        modals.with_focus_graph_mut(|graph| {
1846            graph.insert(make_focus_node(1));
1847            graph.insert(make_focus_node(2));
1848            graph.insert(make_focus_node(3));
1849        });
1850        modals.focus(1);
1851        let _ = modals.focus_manager_mut().take_focus_event();
1852
1853        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1854        let _ = modals.focus_manager_mut().take_focus_event();
1855        let _ = modals.handle_event(&Event::Focus(false), None);
1856        let _ = modals.focus_manager_mut().take_focus_event();
1857
1858        assert!(modals.pop().is_some());
1859        assert_eq!(modals.focus_manager().current(), None);
1860
1861        assert_eq!(modals.focus(3), Some(1));
1862        assert_eq!(modals.focus_manager().current(), None);
1863
1864        let _ = modals.handle_event(&Event::Focus(true), None);
1865        assert_eq!(modals.focus_manager().current(), Some(3));
1866        assert_eq!(
1867            modals.focus_manager_mut().take_focus_event(),
1868            Some(crate::focus::FocusEvent::FocusGained { id: 3 })
1869        );
1870    }
1871
1872    #[test]
1873    fn pop_id_middle_modal_preserves_top_selection_and_retargets_restore_chain() {
1874        let mut modals = FocusAwareModalStack::new();
1875        modals.with_focus_graph_mut(|graph| {
1876            for id in 1..=7 {
1877                graph.insert(make_focus_node(id));
1878            }
1879        });
1880
1881        modals.focus(1);
1882        let _lower_id =
1883            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1884        modals.focus(3);
1885        let middle_id =
1886            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
1887        modals.focus(5);
1888        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![6, 7]);
1889        modals.focus(7);
1890        let _ = modals.focus_manager_mut().take_focus_event();
1891
1892        let removed = modals.pop_id(middle_id);
1893        assert!(removed.is_some());
1894        assert_eq!(modals.focus_manager().current(), Some(7));
1895        assert!(modals.is_focus_trapped());
1896
1897        let _ = modals.pop();
1898        assert_eq!(modals.focus_manager().current(), Some(3));
1899
1900        let _ = modals.pop();
1901        assert_eq!(modals.focus_manager().current(), Some(1));
1902        assert!(!modals.is_focus_trapped());
1903    }
1904
1905    #[test]
1906    fn pop_id_bottom_modal_preserves_top_selection_and_retargets_to_base_focus() {
1907        let mut modals = FocusAwareModalStack::new();
1908        modals.with_focus_graph_mut(|graph| {
1909            for id in 1..=5 {
1910                graph.insert(make_focus_node(id));
1911            }
1912        });
1913
1914        modals.focus(1);
1915        let lower_id =
1916            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1917        modals.focus(3);
1918        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
1919        modals.focus(5);
1920        let _ = modals.focus_manager_mut().take_focus_event();
1921
1922        let removed = modals.pop_id(lower_id);
1923        assert!(removed.is_some());
1924        assert_eq!(modals.focus_manager().current(), Some(5));
1925        assert!(modals.is_focus_trapped());
1926
1927        let _ = modals.pop();
1928        assert_eq!(modals.focus_manager().current(), Some(1));
1929        assert!(!modals.is_focus_trapped());
1930    }
1931
1932    #[test]
1933    fn push_with_trap_while_host_blurred_defers_modal_focus_until_focus_gain() {
1934        let mut modals = FocusAwareModalStack::new();
1935        modals.with_focus_graph_mut(|graph| {
1936            graph.insert(make_focus_node(1));
1937            graph.insert(make_focus_node(2));
1938        });
1939        modals.focus(1);
1940        let _ = modals.handle_event(&Event::Focus(false), None);
1941        assert_eq!(modals.focus_manager().current(), None);
1942
1943        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
1944        assert_eq!(modals.focus_manager().current(), None);
1945        assert!(modals.is_focus_trapped());
1946
1947        let _ = modals.handle_event(&Event::Focus(true), None);
1948        assert_eq!(modals.focus_manager().current(), Some(2));
1949    }
1950
1951    #[test]
1952    fn nested_push_while_host_blurred_restores_underlying_modal_selection_on_close() {
1953        let mut modals = FocusAwareModalStack::new();
1954        modals.with_focus_graph_mut(|graph| {
1955            for id in 1..=4 {
1956                graph.insert(make_focus_node(id));
1957            }
1958        });
1959        modals.focus(1);
1960        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1961        modals.focus(3);
1962        let _ = modals.handle_event(&Event::Focus(false), None);
1963        assert_eq!(modals.focus_manager().current(), None);
1964
1965        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
1966        assert_eq!(modals.focus_manager().current(), None);
1967
1968        let _ = modals.handle_event(&Event::Focus(true), None);
1969        assert_eq!(modals.focus_manager().current(), Some(4));
1970
1971        let result = modals.pop();
1972        assert!(result.is_some());
1973        assert_eq!(modals.focus_manager().current(), Some(3));
1974    }
1975
1976    #[test]
1977    fn first_modal_opened_while_blurred_from_unfocused_base_restores_none() {
1978        let mut modals = FocusAwareModalStack::new();
1979        modals.with_focus_graph_mut(|graph| {
1980            graph.insert(make_focus_node(1));
1981            graph.insert(make_focus_node(2));
1982        });
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![2]);
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(2));
1991
1992        let result = modals.pop();
1993        assert!(result.is_some());
1994        assert_eq!(modals.focus_manager().current(), None);
1995        assert!(!modals.is_focus_trapped());
1996    }
1997
1998    #[test]
1999    fn pop_id_non_top_while_host_blurred_keeps_focus_cleared_until_focus_gain() {
2000        let mut modals = FocusAwareModalStack::new();
2001        for id in 1..=4 {
2002            modals
2003                .focus_manager_mut()
2004                .graph_mut()
2005                .insert(make_focus_node(id));
2006        }
2007
2008        modals.focus_manager_mut().focus(1);
2009        let id1 = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2010        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![3]);
2011        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2012        assert_eq!(modals.focus_manager().current(), Some(4));
2013
2014        let _ = modals.handle_event(&Event::Focus(false), None);
2015        assert_eq!(modals.focus_manager().current(), None);
2016
2017        let result = modals.pop_id(id1);
2018        assert!(result.is_some());
2019        assert_eq!(modals.focus_manager().current(), None);
2020        assert!(modals.is_focus_trapped());
2021
2022        let _ = modals.handle_event(&Event::Focus(true), None);
2023        assert_eq!(modals.focus_manager().current(), Some(4));
2024    }
2025
2026    #[test]
2027    fn pop_id_trapped_modal_while_blurred_with_only_non_trapped_modals_remaining_restores_base_focus()
2028     {
2029        let mut modals = FocusAwareModalStack::new();
2030        modals.with_focus_graph_mut(|graph| {
2031            graph.insert(make_focus_node(1));
2032            graph.insert(make_focus_node(2));
2033        });
2034
2035        modals.focus(1);
2036        let trapped_id =
2037            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2038        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
2039        assert_eq!(modals.focus_manager().current(), Some(2));
2040
2041        let _ = modals.handle_event(&Event::Focus(false), None);
2042        assert_eq!(modals.focus_manager().current(), None);
2043
2044        assert!(modals.pop_id(trapped_id).is_some());
2045        assert_eq!(modals.focus_manager().current(), None);
2046        assert!(!modals.is_focus_trapped());
2047
2048        let _ = modals.handle_event(&Event::Focus(true), None);
2049        assert_eq!(modals.focus_manager().current(), Some(1));
2050    }
2051
2052    #[test]
2053    fn pop_id_inactive_trapped_modal_with_only_non_trapped_modals_remaining_preserves_latest_background_focus()
2054     {
2055        let mut modals = FocusAwareModalStack::new();
2056        modals.with_focus_graph_mut(|graph| {
2057            graph.insert(make_focus_node(1));
2058            graph.insert(make_focus_node(2));
2059            graph.insert(make_focus_node(9));
2060        });
2061
2062        modals.focus(1);
2063        let trapped_id =
2064            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2065        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
2066        assert_eq!(modals.focus_manager().current(), Some(2));
2067
2068        modals.with_focus_graph_mut(|graph| {
2069            let _ = graph.remove(2);
2070        });
2071        assert_eq!(modals.focus_manager().current(), Some(1));
2072        assert!(!modals.is_focus_trapped());
2073
2074        assert_eq!(modals.focus(9), Some(1));
2075        assert_eq!(modals.focus_manager().current(), Some(9));
2076        assert!(!modals.is_focus_trapped());
2077
2078        assert!(modals.pop_id(trapped_id).is_some());
2079        assert_eq!(modals.focus_manager().current(), Some(9));
2080        assert!(!modals.is_focus_trapped());
2081    }
2082
2083    #[test]
2084    fn blurred_pop_id_inactive_trapped_modal_with_only_non_trapped_modals_remaining_preserves_latest_background_focus_on_focus_gain()
2085     {
2086        let mut modals = FocusAwareModalStack::new();
2087        modals.with_focus_graph_mut(|graph| {
2088            graph.insert(make_focus_node(1));
2089            graph.insert(make_focus_node(2));
2090            graph.insert(make_focus_node(9));
2091        });
2092
2093        modals.focus(1);
2094        let trapped_id =
2095            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2096        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
2097
2098        modals.with_focus_graph_mut(|graph| {
2099            let _ = graph.remove(2);
2100        });
2101        assert_eq!(modals.focus_manager().current(), Some(1));
2102        assert!(!modals.is_focus_trapped());
2103
2104        assert_eq!(modals.focus(9), Some(1));
2105        let _ = modals.handle_event(&Event::Focus(false), None);
2106        assert_eq!(modals.focus_manager().current(), None);
2107        assert!(!modals.is_focus_trapped());
2108
2109        assert!(modals.pop_id(trapped_id).is_some());
2110        assert_eq!(modals.focus_manager().current(), None);
2111        assert!(!modals.is_focus_trapped());
2112
2113        let _ = modals.handle_event(&Event::Focus(true), None);
2114        assert_eq!(modals.focus_manager().current(), Some(9));
2115        assert!(!modals.is_focus_trapped());
2116    }
2117
2118    #[test]
2119    fn blurred_pop_id_inactive_trapped_modal_preserves_latest_background_focus_when_trap_went_inactive_while_blurred()
2120     {
2121        let mut modals = FocusAwareModalStack::new();
2122        modals.with_focus_graph_mut(|graph| {
2123            graph.insert(make_focus_node(1));
2124            graph.insert(make_focus_node(2));
2125            graph.insert(make_focus_node(9));
2126        });
2127
2128        modals.focus(1);
2129        let trapped_id =
2130            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2131        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
2132        assert_eq!(modals.focus_manager().current(), Some(2));
2133
2134        let _ = modals.handle_event(&Event::Focus(false), None);
2135        assert_eq!(modals.focus_manager().current(), None);
2136        assert!(modals.is_focus_trapped());
2137
2138        modals.with_focus_graph_mut(|graph| {
2139            let _ = graph.remove(2);
2140        });
2141        assert_eq!(modals.focus_manager().current(), None);
2142        assert!(!modals.is_focus_trapped());
2143
2144        assert_eq!(modals.focus(9), Some(1));
2145        assert_eq!(modals.focus_manager().current(), None);
2146        assert!(!modals.is_focus_trapped());
2147
2148        assert!(modals.pop_id(trapped_id).is_some());
2149        assert_eq!(modals.focus_manager().current(), None);
2150        assert!(!modals.is_focus_trapped());
2151
2152        let _ = modals.handle_event(&Event::Focus(true), None);
2153        assert_eq!(modals.focus_manager().current(), Some(9));
2154        assert!(!modals.is_focus_trapped());
2155    }
2156
2157    #[test]
2158    fn focus_gain_refreshes_inactive_modal_restore_target_after_background_fallback() {
2159        let mut modals = FocusAwareModalStack::new();
2160        modals.with_focus_graph_mut(|graph| {
2161            graph.insert(make_focus_node(2));
2162            graph.insert(make_focus_node(9));
2163        });
2164
2165        let _ = modals.handle_event(&Event::Focus(false), None);
2166        assert_eq!(modals.focus_manager().current(), None);
2167
2168        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2169        assert_eq!(modals.focus_manager().current(), None);
2170        assert!(modals.is_focus_trapped());
2171
2172        modals.with_focus_graph_mut(|graph| {
2173            let _ = graph.remove(2);
2174        });
2175        assert_eq!(modals.focus_manager().current(), None);
2176        assert!(!modals.is_focus_trapped());
2177
2178        let _ = modals.handle_event(&Event::Focus(true), None);
2179        assert_eq!(modals.focus_manager().current(), Some(9));
2180        assert!(!modals.is_focus_trapped());
2181
2182        modals.with_focus_graph_mut(|graph| {
2183            graph.insert(make_focus_node(2));
2184        });
2185        assert_eq!(modals.focus_manager().current(), Some(2));
2186        assert!(modals.is_focus_trapped());
2187
2188        assert!(modals.pop().is_some());
2189        assert_eq!(modals.focus_manager().current(), Some(9));
2190        assert!(!modals.is_focus_trapped());
2191    }
2192
2193    #[test]
2194    fn with_focus_graph_mut_blurred_empty_trap_restores_base_focus_on_focus_gain() {
2195        let mut modals = FocusAwareModalStack::new();
2196        modals.with_focus_graph_mut(|graph| {
2197            graph.insert(make_focus_node(1));
2198            graph.insert(make_focus_node(2));
2199            graph.insert(make_focus_node(3));
2200        });
2201
2202        modals.focus(3);
2203        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2204        assert_eq!(modals.focus_manager().current(), Some(2));
2205
2206        let _ = modals.handle_event(&Event::Focus(false), None);
2207        assert_eq!(modals.focus_manager().current(), None);
2208
2209        modals.with_focus_graph_mut(|graph| {
2210            let _ = graph.remove(2);
2211        });
2212
2213        assert_eq!(modals.focus_manager().current(), None);
2214        let _ = modals.handle_event(&Event::Focus(true), None);
2215        assert_eq!(modals.focus_manager().current(), Some(3));
2216    }
2217
2218    #[test]
2219    fn with_focus_graph_mut_focused_empty_trap_restores_base_focus() {
2220        let mut modals = FocusAwareModalStack::new();
2221        modals.with_focus_graph_mut(|graph| {
2222            graph.insert(make_focus_node(1));
2223            graph.insert(make_focus_node(2));
2224            graph.insert(make_focus_node(3));
2225        });
2226
2227        modals.focus(3);
2228        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2229        assert_eq!(modals.focus_manager().current(), Some(2));
2230
2231        modals.with_focus_graph_mut(|graph| {
2232            let _ = graph.remove(2);
2233        });
2234
2235        assert_eq!(modals.focus_manager().current(), Some(3));
2236        assert!(!modals.is_focus_trapped());
2237    }
2238
2239    #[test]
2240    fn with_focus_graph_mut_focused_empty_top_trap_restores_underlying_selected_control() {
2241        let mut modals = FocusAwareModalStack::new();
2242        modals.with_focus_graph_mut(|graph| {
2243            for id in 1..=4 {
2244                graph.insert(make_focus_node(id));
2245            }
2246        });
2247
2248        modals.focus(1);
2249        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2250        modals.focus(3);
2251        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2252        assert_eq!(modals.focus_manager().current(), Some(4));
2253
2254        modals.with_focus_graph_mut(|graph| {
2255            let _ = graph.remove(4);
2256        });
2257
2258        assert_eq!(modals.focus_manager().current(), Some(3));
2259        assert!(modals.is_focus_trapped());
2260    }
2261
2262    #[test]
2263    fn with_focus_graph_mut_blurred_empty_top_trap_restores_underlying_selected_control_on_focus_gain()
2264     {
2265        let mut modals = FocusAwareModalStack::new();
2266        modals.with_focus_graph_mut(|graph| {
2267            for id in 1..=4 {
2268                graph.insert(make_focus_node(id));
2269            }
2270        });
2271
2272        modals.focus(1);
2273        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2274        modals.focus(3);
2275        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2276        let _ = modals.handle_event(&Event::Focus(false), None);
2277        assert_eq!(modals.focus_manager().current(), None);
2278
2279        modals.with_focus_graph_mut(|graph| {
2280            let _ = graph.remove(4);
2281        });
2282
2283        let _ = modals.handle_event(&Event::Focus(true), None);
2284        assert_eq!(modals.focus_manager().current(), Some(3));
2285        assert!(modals.is_focus_trapped());
2286    }
2287
2288    #[test]
2289    fn with_focus_graph_mut_empty_lower_trap_retargets_surviving_top_restore_to_base_focus() {
2290        let mut modals = FocusAwareModalStack::new();
2291        modals.with_focus_graph_mut(|graph| {
2292            for id in [1, 5, 8, 10] {
2293                graph.insert(make_focus_node(id));
2294            }
2295        });
2296
2297        modals.focus(10);
2298        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2299        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![8]);
2300        assert_eq!(modals.focus_manager().current(), Some(8));
2301
2302        modals.with_focus_graph_mut(|graph| {
2303            let _ = graph.remove(5);
2304        });
2305        assert_eq!(modals.focus_manager().current(), Some(8));
2306        assert!(modals.is_focus_trapped());
2307
2308        assert!(modals.pop().is_some());
2309        assert_eq!(modals.focus_manager().current(), Some(10));
2310        assert!(!modals.is_focus_trapped());
2311    }
2312
2313    #[test]
2314    fn pop_after_top_trap_becomes_empty_preserves_underlying_trap() {
2315        let mut modals = FocusAwareModalStack::new();
2316        modals.with_focus_graph_mut(|graph| {
2317            for id in 1..=4 {
2318                graph.insert(make_focus_node(id));
2319            }
2320        });
2321
2322        modals.focus(1);
2323        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2324        modals.focus(3);
2325        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2326
2327        modals.with_focus_graph_mut(|graph| {
2328            let _ = graph.remove(4);
2329        });
2330        assert_eq!(modals.focus_manager().current(), Some(3));
2331        assert!(modals.is_focus_trapped());
2332
2333        assert!(modals.pop().is_some());
2334        assert_eq!(modals.focus_manager().current(), Some(3));
2335        assert!(modals.is_focus_trapped());
2336        assert_eq!(modals.focus(1), None);
2337        assert_eq!(modals.focus_manager().current(), Some(3));
2338
2339        assert!(modals.pop().is_some());
2340        assert_eq!(modals.focus_manager().current(), Some(1));
2341        assert!(!modals.is_focus_trapped());
2342    }
2343
2344    #[test]
2345    fn blurred_pop_after_top_trap_becomes_empty_preserves_underlying_deferred_focus() {
2346        let mut modals = FocusAwareModalStack::new();
2347        modals.with_focus_graph_mut(|graph| {
2348            for id in 1..=4 {
2349                graph.insert(make_focus_node(id));
2350            }
2351        });
2352
2353        modals.focus(1);
2354        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2355        modals.focus(3);
2356        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2357
2358        modals.with_focus_graph_mut(|graph| {
2359            let _ = graph.remove(4);
2360        });
2361        let _ = modals.handle_event(&Event::Focus(false), None);
2362        assert_eq!(modals.focus_manager().current(), None);
2363        assert!(modals.is_focus_trapped());
2364
2365        assert!(modals.pop().is_some());
2366        assert_eq!(modals.focus_manager().current(), None);
2367        assert!(modals.is_focus_trapped());
2368
2369        let _ = modals.handle_event(&Event::Focus(true), None);
2370        assert_eq!(modals.focus_manager().current(), Some(3));
2371        assert!(modals.is_focus_trapped());
2372    }
2373
2374    #[test]
2375    fn pop_id_skips_stale_retarget_from_inactive_middle_modal() {
2376        let mut modals = FocusAwareModalStack::new();
2377        modals.with_focus_graph_mut(|graph| {
2378            for id in 1..=6 {
2379                graph.insert(make_focus_node(id));
2380            }
2381        });
2382
2383        modals.focus(1);
2384        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2385        modals.focus(3);
2386        let stale_middle_id =
2387            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2388
2389        modals.with_focus_graph_mut(|graph| {
2390            let _ = graph.remove(4);
2391        });
2392        assert_eq!(modals.focus_manager().current(), Some(3));
2393        modals.focus(2);
2394        assert_eq!(modals.focus_manager().current(), Some(2));
2395
2396        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5, 6]);
2397        assert_eq!(modals.focus_manager().current(), Some(5));
2398
2399        assert!(modals.pop_id(stale_middle_id).is_some());
2400        assert_eq!(modals.focus_manager().current(), Some(5));
2401
2402        assert!(modals.pop().is_some());
2403        assert_eq!(modals.focus_manager().current(), Some(2));
2404        assert!(modals.is_focus_trapped());
2405    }
2406
2407    #[test]
2408    fn blurred_pop_id_skips_stale_retarget_from_inactive_middle_modal() {
2409        let mut modals = FocusAwareModalStack::new();
2410        modals.with_focus_graph_mut(|graph| {
2411            for id in 1..=6 {
2412                graph.insert(make_focus_node(id));
2413            }
2414        });
2415
2416        modals.focus(1);
2417        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2418        modals.focus(3);
2419        let stale_middle_id =
2420            modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2421
2422        modals.with_focus_graph_mut(|graph| {
2423            let _ = graph.remove(4);
2424        });
2425        let _ = modals.handle_event(&Event::Focus(false), None);
2426        assert_eq!(modals.focus_manager().current(), None);
2427
2428        assert_eq!(modals.focus(2), Some(3));
2429        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5, 6]);
2430
2431        assert!(modals.pop_id(stale_middle_id).is_some());
2432        assert_eq!(modals.focus_manager().current(), None);
2433
2434        assert!(modals.pop().is_some());
2435        let _ = modals.handle_event(&Event::Focus(true), None);
2436        assert_eq!(modals.focus_manager().current(), Some(2));
2437        assert!(modals.is_focus_trapped());
2438    }
2439
2440    #[test]
2441    fn pop_id_inactive_lower_modal_preserves_surviving_upper_restore_to_base_focus() {
2442        let mut modals = FocusAwareModalStack::new();
2443        modals.with_focus_graph_mut(|graph| {
2444            for id in [5, 10, 20, 30] {
2445                graph.insert(make_focus_node(id));
2446            }
2447        });
2448
2449        modals.focus(10);
2450        let lower_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![20]);
2451        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![30]);
2452        assert_eq!(modals.focus_manager().current(), Some(30));
2453
2454        modals.with_focus_graph_mut(|graph| {
2455            let _ = graph.remove(20);
2456        });
2457        assert_eq!(modals.focus_manager().current(), Some(30));
2458
2459        assert!(modals.pop_id(lower_id).is_some());
2460        assert_eq!(modals.focus_manager().current(), Some(30));
2461        assert!(modals.is_focus_trapped());
2462
2463        assert!(modals.pop().is_some());
2464        assert_eq!(modals.focus_manager().current(), Some(10));
2465        assert!(!modals.is_focus_trapped());
2466    }
2467
2468    #[test]
2469    fn pop_id_active_lower_modal_propagates_none_restore_target_to_surviving_upper_modal() {
2470        let mut modals = FocusAwareModalStack::new();
2471        modals.with_focus_graph_mut(|graph| {
2472            graph.insert(make_focus_node(1));
2473            graph.insert(make_focus_node(2));
2474        });
2475
2476        let lower_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
2477        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2]);
2478        assert_eq!(modals.focus_manager().current(), Some(2));
2479        assert!(modals.is_focus_trapped());
2480
2481        assert!(modals.pop_id(lower_id).is_some());
2482        assert_eq!(modals.focus_manager().current(), Some(2));
2483        assert!(modals.is_focus_trapped());
2484
2485        assert!(modals.pop().is_some());
2486        assert_eq!(modals.focus_manager().current(), None);
2487        assert!(!modals.is_focus_trapped());
2488    }
2489
2490    #[test]
2491    fn blurred_pop_id_inactive_lower_modal_preserves_surviving_upper_restore_to_base_focus() {
2492        let mut modals = FocusAwareModalStack::new();
2493        modals.with_focus_graph_mut(|graph| {
2494            for id in [5, 10, 20, 30] {
2495                graph.insert(make_focus_node(id));
2496            }
2497        });
2498
2499        modals.focus(10);
2500        let lower_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![20]);
2501        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![30]);
2502
2503        modals.with_focus_graph_mut(|graph| {
2504            let _ = graph.remove(20);
2505        });
2506        assert!(modals.pop_id(lower_id).is_some());
2507
2508        let _ = modals.handle_event(&Event::Focus(false), None);
2509        assert_eq!(modals.focus_manager().current(), None);
2510
2511        assert!(modals.pop().is_some());
2512        assert_eq!(modals.focus_manager().current(), None);
2513
2514        let _ = modals.handle_event(&Event::Focus(true), None);
2515        assert_eq!(modals.focus_manager().current(), Some(10));
2516        assert!(!modals.is_focus_trapped());
2517    }
2518
2519    #[test]
2520    fn reactivated_top_modal_restores_latest_underlying_selection() {
2521        let mut modals = FocusAwareModalStack::new();
2522        modals.with_focus_graph_mut(|graph| {
2523            for id in 1..=4 {
2524                graph.insert(make_focus_node(id));
2525            }
2526        });
2527
2528        modals.focus(1);
2529        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2530        modals.focus(3);
2531        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2532
2533        modals.with_focus_graph_mut(|graph| {
2534            let _ = graph.remove(4);
2535        });
2536        assert_eq!(modals.focus_manager().current(), Some(3));
2537
2538        modals.focus(2);
2539        assert_eq!(modals.focus_manager().current(), Some(2));
2540
2541        modals.with_focus_graph_mut(|graph| {
2542            graph.insert(make_focus_node(4));
2543        });
2544        assert_eq!(modals.focus_manager().current(), Some(4));
2545        assert!(modals.is_focus_trapped());
2546
2547        assert!(modals.pop().is_some());
2548        assert_eq!(modals.focus_manager().current(), Some(2));
2549        assert!(modals.is_focus_trapped());
2550    }
2551
2552    #[test]
2553    fn reactivated_top_modal_tracks_graph_restored_selection_within_same_lower_group() {
2554        let mut modals = FocusAwareModalStack::new();
2555        modals.with_focus_graph_mut(|graph| {
2556            for id in 1..=4 {
2557                graph.insert(make_focus_node(id));
2558            }
2559        });
2560
2561        modals.focus(1);
2562        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2563        modals.focus(3);
2564        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2565
2566        modals.with_focus_graph_mut(|graph| {
2567            let _ = graph.remove(4);
2568        });
2569        assert_eq!(modals.focus_manager().current(), Some(3));
2570
2571        modals.with_focus_graph_mut(|graph| {
2572            let _ = graph.remove(3);
2573        });
2574        assert_eq!(modals.focus_manager().current(), Some(2));
2575        assert!(modals.is_focus_trapped());
2576
2577        modals.with_focus_graph_mut(|graph| {
2578            graph.insert(make_focus_node(4));
2579        });
2580        assert_eq!(modals.focus_manager().current(), Some(4));
2581        assert!(modals.is_focus_trapped());
2582
2583        assert!(modals.pop().is_some());
2584        assert_eq!(modals.focus_manager().current(), Some(2));
2585        assert!(modals.is_focus_trapped());
2586    }
2587
2588    #[test]
2589    fn blurred_reactivated_top_modal_tracks_graph_restored_selection_within_same_lower_group() {
2590        let mut modals = FocusAwareModalStack::new();
2591        modals.with_focus_graph_mut(|graph| {
2592            for id in 1..=4 {
2593                graph.insert(make_focus_node(id));
2594            }
2595        });
2596
2597        modals.focus(1);
2598        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2599        modals.focus(3);
2600        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2601
2602        modals.with_focus_graph_mut(|graph| {
2603            let _ = graph.remove(4);
2604        });
2605        let _ = modals.handle_event(&Event::Focus(false), None);
2606        assert_eq!(modals.focus_manager().current(), None);
2607
2608        modals.with_focus_graph_mut(|graph| {
2609            let _ = graph.remove(3);
2610        });
2611        assert_eq!(modals.focus_manager().current(), None);
2612        assert!(modals.is_focus_trapped());
2613
2614        modals.with_focus_graph_mut(|graph| {
2615            graph.insert(make_focus_node(4));
2616        });
2617        let _ = modals.handle_event(&Event::Focus(true), None);
2618        assert_eq!(modals.focus_manager().current(), Some(4));
2619        assert!(modals.is_focus_trapped());
2620
2621        assert!(modals.pop().is_some());
2622        assert_eq!(modals.focus_manager().current(), Some(2));
2623        assert!(modals.is_focus_trapped());
2624    }
2625
2626    #[test]
2627    fn blurred_reactivated_top_modal_restores_latest_underlying_selection() {
2628        let mut modals = FocusAwareModalStack::new();
2629        modals.with_focus_graph_mut(|graph| {
2630            for id in 1..=4 {
2631                graph.insert(make_focus_node(id));
2632            }
2633        });
2634
2635        modals.focus(1);
2636        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2637        modals.focus(3);
2638        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2639
2640        modals.with_focus_graph_mut(|graph| {
2641            let _ = graph.remove(4);
2642        });
2643        let _ = modals.handle_event(&Event::Focus(false), None);
2644        assert_eq!(modals.focus_manager().current(), None);
2645
2646        assert_eq!(modals.focus(2), Some(3));
2647
2648        modals.with_focus_graph_mut(|graph| {
2649            graph.insert(make_focus_node(4));
2650        });
2651
2652        let _ = modals.handle_event(&Event::Focus(true), None);
2653        assert_eq!(modals.focus_manager().current(), Some(4));
2654        assert!(modals.is_focus_trapped());
2655
2656        assert!(modals.pop().is_some());
2657        assert_eq!(modals.focus_manager().current(), Some(2));
2658        assert!(modals.is_focus_trapped());
2659    }
2660
2661    #[test]
2662    fn reactivated_inactive_top_modal_tracks_graph_restored_underlying_selection() {
2663        let mut modals = FocusAwareModalStack::new();
2664        modals.with_focus_graph_mut(|graph| {
2665            for id in 1..=7 {
2666                graph.insert(make_focus_node(id));
2667            }
2668        });
2669
2670        modals.focus(1);
2671        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2672        modals.focus(3);
2673        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
2674        modals.focus(5);
2675        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![6]);
2676        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![7]);
2677
2678        modals.with_focus_graph_mut(|graph| {
2679            let _ = graph.remove(7);
2680        });
2681        assert_eq!(modals.focus_manager().current(), Some(6));
2682
2683        modals.with_focus_graph_mut(|graph| {
2684            let _ = graph.remove(6);
2685        });
2686        assert_eq!(modals.focus_manager().current(), Some(5));
2687        assert!(modals.is_focus_trapped());
2688
2689        modals.with_focus_graph_mut(|graph| {
2690            graph.insert(make_focus_node(7));
2691        });
2692        assert_eq!(modals.focus_manager().current(), Some(7));
2693        assert!(modals.is_focus_trapped());
2694
2695        assert!(modals.pop().is_some());
2696        assert_eq!(modals.focus_manager().current(), Some(5));
2697        assert!(modals.is_focus_trapped());
2698    }
2699
2700    #[test]
2701    fn reactivated_inactive_modal_chain_tracks_graph_restored_underlying_selection() {
2702        let mut modals = FocusAwareModalStack::new();
2703        modals.with_focus_graph_mut(|graph| {
2704            for id in 1..=5 {
2705                graph.insert(make_focus_node(id));
2706            }
2707        });
2708
2709        modals.focus(1);
2710        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2711        modals.focus(3);
2712        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2713        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2714
2715        modals.with_focus_graph_mut(|graph| {
2716            let _ = graph.remove(5);
2717        });
2718        assert_eq!(modals.focus_manager().current(), Some(4));
2719
2720        modals.with_focus_graph_mut(|graph| {
2721            let _ = graph.remove(4);
2722        });
2723        assert_eq!(modals.focus_manager().current(), Some(3));
2724
2725        modals.with_focus_graph_mut(|graph| {
2726            let _ = graph.remove(3);
2727        });
2728        assert_eq!(modals.focus_manager().current(), Some(2));
2729        assert!(modals.is_focus_trapped());
2730
2731        modals.with_focus_graph_mut(|graph| {
2732            graph.insert(make_focus_node(4));
2733        });
2734        assert_eq!(modals.focus_manager().current(), Some(4));
2735        assert!(modals.is_focus_trapped());
2736
2737        modals.with_focus_graph_mut(|graph| {
2738            graph.insert(make_focus_node(5));
2739        });
2740        assert_eq!(modals.focus_manager().current(), Some(5));
2741        assert!(modals.is_focus_trapped());
2742
2743        assert!(modals.pop().is_some());
2744        assert_eq!(modals.focus_manager().current(), Some(4));
2745        assert!(modals.is_focus_trapped());
2746
2747        assert!(modals.pop().is_some());
2748        assert_eq!(modals.focus_manager().current(), Some(2));
2749        assert!(modals.is_focus_trapped());
2750    }
2751
2752    #[test]
2753    fn reactivated_lower_modal_refreshes_still_inactive_upper_restore_target() {
2754        let mut modals = FocusAwareModalStack::new();
2755        modals.with_focus_graph_mut(|graph| {
2756            for id in [1, 2, 4, 5] {
2757                graph.insert(make_focus_node(id));
2758            }
2759        });
2760
2761        modals.focus(1);
2762        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 4]);
2763        modals.focus(4);
2764        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2765
2766        modals.with_focus_graph_mut(|graph| {
2767            let _ = graph.remove(5);
2768        });
2769        assert_eq!(modals.focus_manager().current(), Some(4));
2770
2771        modals.with_focus_graph_mut(|graph| {
2772            let _ = graph.remove(2);
2773            let _ = graph.remove(4);
2774        });
2775        assert_eq!(modals.focus_manager().current(), Some(1));
2776        assert!(!modals.is_focus_trapped());
2777
2778        modals.with_focus_graph_mut(|graph| {
2779            graph.insert(make_focus_node(2));
2780            graph.insert(make_focus_node(4));
2781        });
2782        assert_eq!(modals.focus_manager().current(), Some(2));
2783        assert!(modals.is_focus_trapped());
2784
2785        modals.with_focus_graph_mut(|graph| {
2786            graph.insert(make_focus_node(5));
2787        });
2788        assert_eq!(modals.focus_manager().current(), Some(5));
2789        assert!(modals.is_focus_trapped());
2790
2791        assert!(modals.pop().is_some());
2792        assert_eq!(modals.focus_manager().current(), Some(2));
2793        assert!(modals.is_focus_trapped());
2794    }
2795
2796    #[test]
2797    fn reactivated_inactive_upper_modal_does_not_restore_stale_lower_selection_after_top_pop() {
2798        let mut modals = FocusAwareModalStack::new();
2799        modals.with_focus_graph_mut(|graph| {
2800            for id in 1..=5 {
2801                graph.insert(make_focus_node(id));
2802            }
2803        });
2804
2805        modals.focus(1);
2806        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2807        modals.focus(3);
2808        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2809
2810        modals.with_focus_graph_mut(|graph| {
2811            let _ = graph.remove(4);
2812        });
2813        assert_eq!(modals.focus_manager().current(), Some(3));
2814
2815        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2816        assert_eq!(modals.focus_manager().current(), Some(5));
2817
2818        modals.with_focus_graph_mut(|graph| {
2819            let _ = graph.remove(3);
2820        });
2821        assert_eq!(modals.focus_manager().current(), Some(5));
2822
2823        assert!(modals.pop().is_some());
2824        assert_eq!(modals.focus_manager().current(), Some(2));
2825        assert!(modals.is_focus_trapped());
2826
2827        modals.with_focus_graph_mut(|graph| {
2828            graph.insert(make_focus_node(4));
2829        });
2830        assert_eq!(modals.focus_manager().current(), Some(4));
2831        assert!(modals.is_focus_trapped());
2832
2833        modals.with_focus_graph_mut(|graph| {
2834            graph.insert(make_focus_node(3));
2835        });
2836        assert_eq!(modals.focus_manager().current(), Some(4));
2837        assert!(modals.is_focus_trapped());
2838
2839        assert!(modals.pop().is_some());
2840        assert_eq!(modals.focus_manager().current(), Some(2));
2841        assert!(modals.is_focus_trapped());
2842    }
2843
2844    #[test]
2845    fn blurred_reactivated_inactive_upper_modal_does_not_restore_stale_lower_selection_after_top_pop()
2846     {
2847        let mut modals = FocusAwareModalStack::new();
2848        modals.with_focus_graph_mut(|graph| {
2849            for id in 1..=5 {
2850                graph.insert(make_focus_node(id));
2851            }
2852        });
2853
2854        modals.focus(1);
2855        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2856        modals.focus(3);
2857        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2858
2859        modals.with_focus_graph_mut(|graph| {
2860            let _ = graph.remove(4);
2861        });
2862        assert_eq!(modals.focus_manager().current(), Some(3));
2863
2864        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2865        assert_eq!(modals.focus_manager().current(), Some(5));
2866
2867        let _ = modals.handle_event(&Event::Focus(false), None);
2868        assert_eq!(modals.focus_manager().current(), None);
2869
2870        modals.with_focus_graph_mut(|graph| {
2871            let _ = graph.remove(3);
2872        });
2873        assert_eq!(modals.focus_manager().current(), None);
2874
2875        assert!(modals.pop().is_some());
2876        assert_eq!(modals.focus_manager().current(), None);
2877        assert!(modals.is_focus_trapped());
2878
2879        modals.with_focus_graph_mut(|graph| {
2880            graph.insert(make_focus_node(4));
2881        });
2882        let _ = modals.handle_event(&Event::Focus(true), None);
2883        assert_eq!(modals.focus_manager().current(), Some(4));
2884        assert!(modals.is_focus_trapped());
2885
2886        modals.with_focus_graph_mut(|graph| {
2887            graph.insert(make_focus_node(3));
2888        });
2889        assert_eq!(modals.focus_manager().current(), Some(4));
2890        assert!(modals.is_focus_trapped());
2891
2892        assert!(modals.pop().is_some());
2893        assert_eq!(modals.focus_manager().current(), Some(2));
2894        assert!(modals.is_focus_trapped());
2895    }
2896
2897    #[test]
2898    fn reactivated_middle_modal_before_top_close_does_not_restore_stale_lower_selection() {
2899        let mut modals = FocusAwareModalStack::new();
2900        modals.with_focus_graph_mut(|graph| {
2901            for id in 1..=5 {
2902                graph.insert(make_focus_node(id));
2903            }
2904        });
2905
2906        modals.focus(1);
2907        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2908        modals.focus(3);
2909        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2910        assert_eq!(modals.focus_manager().current(), Some(4));
2911
2912        modals.with_focus_graph_mut(|graph| {
2913            let _ = graph.remove(4);
2914        });
2915        assert_eq!(modals.focus_manager().current(), Some(3));
2916
2917        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2918        assert_eq!(modals.focus_manager().current(), Some(5));
2919
2920        modals.with_focus_graph_mut(|graph| {
2921            let _ = graph.remove(3);
2922        });
2923        assert_eq!(modals.focus_manager().current(), Some(5));
2924
2925        modals.with_focus_graph_mut(|graph| {
2926            graph.insert(make_focus_node(4));
2927        });
2928        assert_eq!(modals.focus_manager().current(), Some(5));
2929
2930        assert!(modals.pop().is_some());
2931        assert_eq!(modals.focus_manager().current(), Some(4));
2932        assert!(modals.is_focus_trapped());
2933
2934        assert!(modals.pop().is_some());
2935        assert_eq!(modals.focus_manager().current(), Some(2));
2936        assert!(modals.is_focus_trapped());
2937    }
2938
2939    #[test]
2940    fn revalidated_stale_lower_target_before_top_close_does_not_win_on_middle_pop() {
2941        let mut modals = FocusAwareModalStack::new();
2942        modals.with_focus_graph_mut(|graph| {
2943            for id in 1..=5 {
2944                graph.insert(make_focus_node(id));
2945            }
2946        });
2947
2948        modals.focus(1);
2949        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2950        modals.focus(3);
2951        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2952
2953        modals.with_focus_graph_mut(|graph| {
2954            let _ = graph.remove(4);
2955        });
2956        assert_eq!(modals.focus_manager().current(), Some(3));
2957
2958        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
2959        assert_eq!(modals.focus_manager().current(), Some(5));
2960
2961        modals.with_focus_graph_mut(|graph| {
2962            let _ = graph.remove(3);
2963        });
2964        assert_eq!(modals.focus_manager().current(), Some(5));
2965
2966        modals.with_focus_graph_mut(|graph| {
2967            graph.insert(make_focus_node(4));
2968        });
2969        assert_eq!(modals.focus_manager().current(), Some(5));
2970
2971        modals.with_focus_graph_mut(|graph| {
2972            graph.insert(make_focus_node(3));
2973        });
2974        assert_eq!(modals.focus_manager().current(), Some(5));
2975
2976        assert!(modals.pop().is_some());
2977        assert_eq!(modals.focus_manager().current(), Some(4));
2978        assert!(modals.is_focus_trapped());
2979
2980        assert!(modals.pop().is_some());
2981        assert_eq!(modals.focus_manager().current(), Some(2));
2982        assert!(modals.is_focus_trapped());
2983    }
2984
2985    #[test]
2986    fn blurred_revalidated_stale_lower_target_before_top_close_does_not_win_on_middle_pop() {
2987        let mut modals = FocusAwareModalStack::new();
2988        modals.with_focus_graph_mut(|graph| {
2989            for id in 1..=5 {
2990                graph.insert(make_focus_node(id));
2991            }
2992        });
2993
2994        modals.focus(1);
2995        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
2996        modals.focus(3);
2997        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4]);
2998
2999        modals.with_focus_graph_mut(|graph| {
3000            let _ = graph.remove(4);
3001        });
3002        assert_eq!(modals.focus_manager().current(), Some(3));
3003
3004        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
3005        assert_eq!(modals.focus_manager().current(), Some(5));
3006
3007        let _ = modals.handle_event(&Event::Focus(false), None);
3008        assert_eq!(modals.focus_manager().current(), None);
3009
3010        modals.with_focus_graph_mut(|graph| {
3011            let _ = graph.remove(3);
3012        });
3013        assert_eq!(modals.focus_manager().current(), None);
3014
3015        modals.with_focus_graph_mut(|graph| {
3016            graph.insert(make_focus_node(4));
3017        });
3018        assert_eq!(modals.focus_manager().current(), None);
3019
3020        modals.with_focus_graph_mut(|graph| {
3021            graph.insert(make_focus_node(3));
3022        });
3023        assert_eq!(modals.focus_manager().current(), None);
3024
3025        assert!(modals.pop().is_some());
3026        assert_eq!(modals.focus_manager().current(), None);
3027        assert!(modals.is_focus_trapped());
3028
3029        let _ = modals.handle_event(&Event::Focus(true), None);
3030        assert_eq!(modals.focus_manager().current(), Some(4));
3031        assert!(modals.is_focus_trapped());
3032
3033        assert!(modals.pop().is_some());
3034        assert_eq!(modals.focus_manager().current(), Some(2));
3035        assert!(modals.is_focus_trapped());
3036    }
3037
3038    #[test]
3039    fn invalidated_lower_selection_retargets_upper_restore_using_group_tab_order() {
3040        let mut modals = FocusAwareModalStack::new();
3041        modals.with_focus_graph_mut(|graph| {
3042            for id in 1..=5 {
3043                graph.insert(make_focus_node(id));
3044            }
3045        });
3046
3047        modals.focus(1);
3048        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 3, 2]);
3049        assert_eq!(modals.focus_manager().current(), Some(2));
3050        modals.focus(4);
3051        assert_eq!(modals.focus_manager().current(), Some(4));
3052
3053        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
3054        assert_eq!(modals.focus_manager().current(), Some(5));
3055
3056        modals.with_focus_graph_mut(|graph| {
3057            let _ = graph.remove(4);
3058        });
3059        assert_eq!(modals.focus_manager().current(), Some(5));
3060
3061        assert!(modals.pop().is_some());
3062        assert_eq!(modals.focus_manager().current(), Some(2));
3063        assert!(modals.is_focus_trapped());
3064    }
3065
3066    #[test]
3067    fn blurred_invalidated_lower_selection_retargets_upper_restore_using_group_tab_order() {
3068        let mut modals = FocusAwareModalStack::new();
3069        modals.with_focus_graph_mut(|graph| {
3070            for id in 1..=5 {
3071                graph.insert(make_focus_node(id));
3072            }
3073        });
3074
3075        modals.focus(1);
3076        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 3, 2]);
3077        assert_eq!(modals.focus_manager().current(), Some(2));
3078        modals.focus(4);
3079        assert_eq!(modals.focus_manager().current(), Some(4));
3080
3081        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
3082        assert_eq!(modals.focus_manager().current(), Some(5));
3083
3084        let _ = modals.handle_event(&Event::Focus(false), None);
3085        assert_eq!(modals.focus_manager().current(), None);
3086
3087        modals.with_focus_graph_mut(|graph| {
3088            let _ = graph.remove(4);
3089        });
3090        assert_eq!(modals.focus_manager().current(), None);
3091
3092        assert!(modals.pop().is_some());
3093        assert_eq!(modals.focus_manager().current(), None);
3094        assert!(modals.is_focus_trapped());
3095
3096        let _ = modals.handle_event(&Event::Focus(true), None);
3097        assert_eq!(modals.focus_manager().current(), Some(2));
3098        assert!(modals.is_focus_trapped());
3099    }
3100
3101    #[test]
3102    fn invalidated_negative_tabindex_lower_selection_retargets_upper_restore_metadata() {
3103        let mut modals = FocusAwareModalStack::new();
3104        modals
3105            .focus_manager_mut()
3106            .graph_mut()
3107            .insert(make_focus_node(1));
3108        modals
3109            .focus_manager_mut()
3110            .graph_mut()
3111            .insert(FocusNode::new(3, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
3112        modals
3113            .focus_manager_mut()
3114            .graph_mut()
3115            .insert(FocusNode::new(4, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
3116        modals
3117            .focus_manager_mut()
3118            .graph_mut()
3119            .insert(make_focus_node(5));
3120
3121        modals.focus(1);
3122        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 3]);
3123        assert_eq!(modals.focus_manager().current(), Some(4));
3124
3125        let upper_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
3126        assert_eq!(modals.focus_manager().current(), Some(5));
3127
3128        modals.with_focus_graph_mut(|graph| {
3129            let _ = graph.remove(4);
3130        });
3131
3132        let upper_return_focus = modals
3133            .stack
3134            .focus_modal_specs_in_order()
3135            .into_iter()
3136            .find(|(modal_id, _)| *modal_id == upper_id)
3137            .map(|(_, spec)| spec.return_focus);
3138        assert_eq!(upper_return_focus, Some(Some(3)));
3139    }
3140
3141    #[test]
3142    fn blurred_invalidated_negative_tabindex_lower_selection_retargets_upper_restore_metadata() {
3143        let mut modals = FocusAwareModalStack::new();
3144        modals
3145            .focus_manager_mut()
3146            .graph_mut()
3147            .insert(make_focus_node(1));
3148        modals
3149            .focus_manager_mut()
3150            .graph_mut()
3151            .insert(FocusNode::new(3, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
3152        modals
3153            .focus_manager_mut()
3154            .graph_mut()
3155            .insert(FocusNode::new(4, Rect::new(0, 0, 10, 3)).with_tab_index(-1));
3156        modals
3157            .focus_manager_mut()
3158            .graph_mut()
3159            .insert(make_focus_node(5));
3160
3161        modals.focus(1);
3162        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 3]);
3163        assert_eq!(modals.focus_manager().current(), Some(4));
3164
3165        let upper_id = modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![5]);
3166        assert_eq!(modals.focus_manager().current(), Some(5));
3167
3168        let _ = modals.handle_event(&Event::Focus(false), None);
3169        assert_eq!(modals.focus_manager().current(), None);
3170
3171        modals.with_focus_graph_mut(|graph| {
3172            let _ = graph.remove(4);
3173        });
3174
3175        let upper_return_focus = modals
3176            .stack
3177            .focus_modal_specs_in_order()
3178            .into_iter()
3179            .find(|(modal_id, _)| *modal_id == upper_id)
3180            .map(|(_, spec)| spec.return_focus);
3181        assert_eq!(upper_return_focus, Some(Some(3)));
3182    }
3183
3184    #[test]
3185    fn blurred_reactivated_inactive_top_modal_tracks_graph_restored_underlying_selection() {
3186        let mut modals = FocusAwareModalStack::new();
3187        modals.with_focus_graph_mut(|graph| {
3188            for id in 1..=7 {
3189                graph.insert(make_focus_node(id));
3190            }
3191        });
3192
3193        modals.focus(1);
3194        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
3195        modals.focus(3);
3196        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5]);
3197        modals.focus(5);
3198        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![6]);
3199        modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![7]);
3200
3201        modals.with_focus_graph_mut(|graph| {
3202            let _ = graph.remove(7);
3203        });
3204        let _ = modals.handle_event(&Event::Focus(false), None);
3205        assert_eq!(modals.focus_manager().current(), None);
3206
3207        modals.with_focus_graph_mut(|graph| {
3208            let _ = graph.remove(6);
3209        });
3210        assert_eq!(modals.focus_manager().current(), None);
3211
3212        modals.with_focus_graph_mut(|graph| {
3213            graph.insert(make_focus_node(7));
3214        });
3215        let _ = modals.handle_event(&Event::Focus(true), None);
3216        assert_eq!(modals.focus_manager().current(), Some(7));
3217        assert!(modals.is_focus_trapped());
3218
3219        assert!(modals.pop().is_some());
3220        assert_eq!(modals.focus_manager().current(), Some(5));
3221        assert!(modals.is_focus_trapped());
3222    }
3223
3224    #[test]
3225    fn depth_tracks_push_pop() {
3226        let mut modals = FocusAwareModalStack::new();
3227        assert_eq!(modals.depth(), 0);
3228        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
3229        assert_eq!(modals.depth(), 1);
3230        modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
3231        assert_eq!(modals.depth(), 2);
3232        modals.pop();
3233        assert_eq!(modals.depth(), 1);
3234    }
3235
3236    #[test]
3237    fn pop_empty_stack_returns_none() {
3238        let mut modals = FocusAwareModalStack::new();
3239        assert!(modals.pop().is_none());
3240    }
3241}