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