1#![forbid(unsafe_code)]
2
3use 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
58static FOCUS_GROUP_COUNTER: AtomicU32 = AtomicU32::new(1_000_000);
60
61pub(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
533pub 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 pub fn new() -> Self {
559 Self {
560 stack: ModalStack::new(),
561 focus_manager: FocusManager::new(),
562 base_focus: None,
563 }
564 }
565
566 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 pub fn push(&mut self, modal: Box<dyn StackModal>) -> ModalId {
593 self.stack.push(modal)
594 }
595
596 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 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 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 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 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 pub fn render(&self, frame: &mut Frame, screen: Rect) {
673 self.stack.render(frame, screen);
674 }
675
676 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 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 #[inline]
746 pub fn is_empty(&self) -> bool {
747 self.stack.is_empty()
748 }
749
750 #[inline]
752 pub fn depth(&self) -> usize {
753 self.stack.depth()
754 }
755
756 #[inline]
758 pub fn is_focus_trapped(&self) -> bool {
759 self.focus_manager.is_trapped()
760 }
761
762 pub fn stack(&self) -> &ModalStack {
764 &self.stack
765 }
766
767 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 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 modals.focus_manager_mut().focus(3);
823 assert_eq!(modals.focus_manager().current(), Some(3));
824
825 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
827
828 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 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 modals.focus_manager_mut().focus(3);
853
854 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1, 2]);
856 assert_eq!(modals.focus_manager().current(), Some(1));
857
858 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 for id in 1..=6 {
895 modals
896 .focus_manager_mut()
897 .graph_mut()
898 .insert(make_focus_node(id));
899 }
900
901 modals.focus_manager_mut().focus(1);
903
904 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
906 assert_eq!(modals.focus_manager().current(), Some(2));
907
908 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![4, 5, 6]);
910 assert_eq!(modals.focus_manager().current(), Some(4));
911
912 modals.pop();
914 assert_eq!(modals.focus_manager().current(), Some(2));
915
916 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 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 modals.focus_manager_mut().focus(2);
984
985 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![1]);
987 assert_eq!(modals.focus_manager().current(), Some(1));
988
989 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 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 modals.focus_manager_mut().focus(2);
1108
1109 modals.push(Box::new(WidgetModalEntry::new(StubWidget)));
1111
1112 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 for id in 1..=4 {
1123 modals
1124 .focus_manager_mut()
1125 .graph_mut()
1126 .insert(make_focus_node(id));
1127 }
1128
1129 modals.focus_manager_mut().focus(1);
1131
1132 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 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 for id in 1..=5 {
1226 modals
1227 .focus_manager_mut()
1228 .graph_mut()
1229 .insert(make_focus_node(id));
1230 }
1231
1232 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![2, 3]);
1234
1235 assert_eq!(modals.focus_manager().current(), Some(2));
1237
1238 modals.focus_manager_mut().focus_next();
1240 assert_eq!(modals.focus_manager().current(), Some(3));
1241
1242 modals.focus_manager_mut().focus_next();
1244 assert_eq!(modals.focus_manager().current(), Some(2));
1245
1246 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 modals.push_with_trap(Box::new(WidgetModalEntry::new(StubWidget)), vec![]);
1259
1260 assert!(!modals.is_focus_trapped());
1262
1263 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 for id in 1..=6 {
1377 modals
1378 .focus_manager_mut()
1379 .graph_mut()
1380 .insert(make_focus_node(id));
1381 }
1382
1383 modals.focus_manager_mut().focus(1);
1385
1386 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 assert_eq!(modals.focus_manager().current(), Some(4));
1393
1394 modals.pop_id(id1);
1396
1397 assert_eq!(modals.focus_manager().current(), Some(4));
1399 assert_eq!(modals.depth(), 2);
1400 assert!(modals.is_focus_trapped());
1401
1402 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 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 for id in 1..=4 {
1478 modals
1479 .focus_manager_mut()
1480 .graph_mut()
1481 .insert(make_focus_node(id));
1482 }
1483
1484 modals.focus_manager_mut().focus(1);
1486
1487 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 modals.pop_id(id2);
1495
1496 assert_eq!(modals.focus_manager().current(), Some(2));
1498 assert!(modals.is_focus_trapped()); 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}