1use super::*;
5
6impl WidgetTree {
7 pub fn focus_with_origin_ops(
26 &mut self,
27 id: WidgetId,
28 origin: crate::focus::FocusOrigin,
29 ops: &mut dyn crate::window::WindowOps,
30 ) {
31 self.pending_focus_request = None;
36 if self.focused == Some(id) {
37 return;
38 }
39 struct ClearOnDrop(std::rc::Rc<std::cell::Cell<bool>>);
45 impl Drop for ClearOnDrop {
46 fn drop(&mut self) {
47 self.0.set(false);
48 }
49 }
50 self.in_focus_dispatch.set(true);
51 let _focus_dispatch_guard = ClearOnDrop(self.in_focus_dispatch.clone());
52
53 let previously_focused = self.focused;
54 if let Some(old) = self.focused {
55 let old_overlay = self.overlay_ancestor_for_widget(old);
56 let new_overlay = self.overlay_ancestor_for_widget(id);
57 let moving_into_descendant_overlay = match (old_overlay, new_overlay) {
58 (Some(old_overlay), Some(new_overlay)) => self
59 .overlay_manager
60 .is_descendant_of(new_overlay, old_overlay),
61 _ => false,
62 };
63
64 if moving_into_descendant_overlay {
65 self.dispatch_to_widget_direct(old, &WidgetEvent::FocusLost, &mut *ops);
66 } else {
67 self.dispatch_to_widget(old, &WidgetEvent::FocusLost, &mut *ops);
68 }
69 }
70 self.set_focused(Some(id));
71 self.focus_origin = Some(origin);
72 if let Some(visible) = origin.focus_visible()
81 && self.focus_visible.get() != visible
82 {
83 self.focus_visible.set(visible);
84 }
85 self.a11y_dirty = true;
86 self.update_focus_within_signals(previously_focused, Some(id));
87 self.update_view_focus_signals(previously_focused, Some(id));
88 self.dispatch_to_widget(id, &WidgetEvent::FocusGained { origin }, &mut *ops);
89 self.dismiss_overlays_left_by_focus(previously_focused, Some(id), &mut *ops);
99 self.tooltip_focus_leave_outside(Some(id), &mut *ops);
106 self.tooltip_focus_enter(id);
107 if !origin.is_pointer() {
114 self.scroll_focused_into_view(id, &mut *ops);
115 }
116 }
117
118 pub fn focus_with_origin(&mut self, id: WidgetId, origin: crate::focus::FocusOrigin) {
122 let mut noop = crate::window::NoopWindowOps;
123 self.focus_with_origin_ops(id, origin, &mut noop);
124 }
125
126 fn scroll_focused_into_view(
129 &mut self,
130 focused_id: WidgetId,
131 ops: &mut dyn crate::window::WindowOps,
132 ) {
133 let focused_bounds = self.arena.bounds(focused_id);
134 let reveal = self
139 .arena
140 .get(focused_id)
141 .and_then(|node| node.widget.focus_reveal_rect(focused_bounds))
142 .unwrap_or(focused_bounds);
143 self.scroll_rect_into_view(
144 focused_id,
145 reveal,
146 0.0,
147 crate::event::ScrollAlign::Minimal,
148 crate::event::ScrollMotion::Instant,
149 &mut *ops,
150 );
151 }
152
153 pub(super) fn scroll_rect_into_view(
213 &mut self,
214 from: WidgetId,
215 rect: Rect,
216 margin: f32,
217 align: crate::event::ScrollAlign,
218 motion: crate::event::ScrollMotion,
219 ops: &mut dyn crate::window::WindowOps,
220 ) {
221 let applied = std::sync::Arc::new(std::sync::Mutex::new(Point::ZERO));
227 let mut rect = rect;
228 let mut current = self.arena.parent(from);
229 let mut pending_align = align;
232 while let Some(ancestor_id) = current {
233 let to_parent = self
248 .arena
249 .get(ancestor_id)
250 .and_then(|n| n.transform_prop.as_ref())
251 .map(|t| t.get())
252 .filter(|t| !t.is_identity());
253 let content_transform = self
254 .arena
255 .get(ancestor_id)
256 .is_some_and(|n| n.content_transform);
257
258 if let Some(node) = self.arena.get(ancestor_id)
265 && node.clips_children
266 && (node.handlers.on_scroll.is_some() || node.external_handlers.on_scroll.is_some())
267 {
268 let viewport = node.bounds;
269 let against = match (content_transform, to_parent.as_ref()) {
276 (true, Some(t)) => t.apply_rect(rect),
277 _ => rect,
278 };
279 let align =
280 std::mem::replace(&mut pending_align, crate::event::ScrollAlign::Minimal);
281 let needs_scroll = matches!(align, crate::event::ScrollAlign::Fraction(_))
284 || against.y - margin < viewport.y
285 || against.bottom() + margin > viewport.bottom()
286 || against.x - margin < viewport.x
287 || against.right() + margin > viewport.right();
288
289 if needs_scroll {
290 *applied.lock().unwrap() = Point::ZERO;
291 self.dispatch_to_widget_direct(
305 ancestor_id,
306 &WidgetEvent::ScrollIntoView {
307 target_bounds: rect,
313 margin,
314 align,
315 motion,
316 applied_scroll: Some(applied.clone()),
317 },
318 &mut *ops,
319 );
320 let delta = *applied.lock().unwrap();
324 if delta != Point::ZERO {
325 rect =
326 Rect::new(rect.x - delta.x, rect.y - delta.y, rect.width, rect.height);
327 }
328 }
329 }
330 if let Some(t) = to_parent {
336 rect = t.apply_rect(rect);
337 }
338 current = self.arena.parent(ancestor_id);
339 }
340 }
341
342 pub fn focus(&mut self, id: WidgetId) {
352 let mut noop = crate::window::NoopWindowOps;
353 self.request_focus_with_ops(id, FocusRequestMode::Exact, &mut noop);
354 }
355
356 pub fn clear_focus(&mut self) {
374 let mut noop = crate::window::NoopWindowOps;
375 self.clear_focus_ops(&mut noop);
376 }
377
378 pub(crate) fn clear_focus_ops(&mut self, ops: &mut dyn crate::window::WindowOps) {
381 self.pending_focus_request = None;
382 let Some(old) = self.focused else {
383 return;
384 };
385 struct ClearOnDrop(std::rc::Rc<std::cell::Cell<bool>>);
388 impl Drop for ClearOnDrop {
389 fn drop(&mut self) {
390 self.0.set(false);
391 }
392 }
393 self.in_focus_dispatch.set(true);
394 let _focus_dispatch_guard = ClearOnDrop(self.in_focus_dispatch.clone());
395
396 self.set_focused(None);
400 self.focus_origin = None;
401 if self.focus_visible.get() {
402 self.focus_visible.set(false);
403 }
404 self.a11y_dirty = true;
405 self.update_focus_within_signals(Some(old), None);
406 self.update_view_focus_signals(Some(old), None);
407 self.dispatch_to_widget(old, &WidgetEvent::FocusLost, &mut *ops);
408 let now = self.focused;
411 self.dismiss_overlays_left_by_focus(Some(old), now, &mut *ops);
412 self.tooltip_focus_leave_outside(now, &mut *ops);
413 }
414
415 pub(crate) fn request_focus_with_ops(
428 &mut self,
429 id: WidgetId,
430 mode: FocusRequestMode,
431 ops: &mut dyn crate::window::WindowOps,
432 ) {
433 self.pending_focus_request = None;
435 if self.arena.get(id).is_some() && !self.arena.is_active(id) {
436 self.pending_focus_request = Some(PendingFocusRequest { id, mode });
437 return;
438 }
439 self.resolve_focus_request(id, mode, ops);
440 }
441
442 #[cfg(test)]
444 pub(crate) fn has_pending_focus_request(&self) -> bool {
445 self.pending_focus_request.is_some()
446 }
447
448 fn resolve_focus_request(
450 &mut self,
451 id: WidgetId,
452 mode: FocusRequestMode,
453 ops: &mut dyn crate::window::WindowOps,
454 ) {
455 match mode {
456 FocusRequestMode::Exact => self.focus_ops(id, ops),
457 FocusRequestMode::SelfOrDescendant => {
458 let target = self.first_focusable_descendant(id).unwrap_or(id);
459 self.focus_ops(target, ops);
460 }
461 FocusRequestMode::Into => {
462 if let Some(target) = self.first_focusable_descendant(id) {
463 self.focus_ops(target, ops);
464 }
465 }
466 }
467 }
468
469 pub(super) fn settle_pending_focus_request(&mut self, ops: &mut dyn crate::window::WindowOps) {
478 let Some(request) = self.pending_focus_request.take() else {
479 return;
480 };
481 if self.arena.is_active(request.id) {
484 self.resolve_focus_request(request.id, request.mode, ops);
485 }
486 }
487
488 pub fn focus_ops(&mut self, id: WidgetId, ops: &mut dyn crate::window::WindowOps) {
491 self.focus_with_origin_ops(id, crate::focus::FocusOrigin::Programmatic, ops);
492 }
493
494 pub fn focused(&self) -> Option<WidgetId> {
496 self.focused
497 }
498
499 pub fn ime_context_for_focused(&self) -> Option<crate::ime::ImeContext> {
505 self.focused.and_then(|id| self.arena.ime_context(id))
506 }
507
508 pub fn first_focusable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
513 if !self.arena.is_active(root) {
514 return None;
515 }
516 let mut entries = Vec::new();
517 self.collect_scope_entries(root, &mut entries);
518 sort_scope_entries(&mut entries);
519 let scope = ScopeNode {
520 policy: crate::focus::TraversalScopePolicy::Cycle,
521 entries,
522 };
523 enter_scope_edge(&scope, false)
524 }
525
526 pub(crate) fn first_focus_capable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
530 if !self.arena.is_active(root) {
531 return None;
532 }
533 let node = self.arena.get(root)?;
534 if self.is_node_focusable(node) {
535 return Some(root);
536 }
537 node.children
538 .iter()
539 .find_map(|&child| self.first_focus_capable_descendant(child))
540 }
541
542 pub fn is_active(&self, id: WidgetId) -> bool {
548 self.arena.is_active(id)
549 }
550
551 pub fn widget_initial_focus_hint(&self, id: WidgetId) -> Option<WidgetId> {
567 if !self.arena.is_active(id) {
568 return None;
569 }
570 if let Some(node) = self.arena.get(id) {
571 if let Some(target) = node.widget.initial_focus_hint()
572 && self.arena.is_active(target)
573 && self.is_descendant_of(target, id)
574 {
575 return Some(target);
576 }
577 for &child in &node.children {
578 if let Some(found) = self.widget_initial_focus_hint(child) {
579 return Some(found);
580 }
581 }
582 }
583 None
584 }
585
586 pub fn focus_origin(&self) -> Option<crate::focus::FocusOrigin> {
588 self.focus_origin
589 }
590
591 pub fn focus_visible_signal(&self) -> crate::signal::Signal<bool> {
595 self.focus_visible.clone()
596 }
597
598 pub(super) fn cycle_focus(&mut self, reverse: bool, ops: &mut dyn crate::window::WindowOps) {
607 let mut entries = Vec::new();
608 if let Some(modal_overlay) = self.overlay_manager.topmost_centered() {
609 let content_id = modal_overlay.content_id;
610 self.collect_scope_entries(content_id, &mut entries);
611 } else {
612 let roots = self.arena.roots();
613 for root in roots {
614 if self.tooltip_content_root(root).is_some() {
624 continue;
625 }
626 self.collect_scope_entries(root, &mut entries);
627 }
628 }
629 sort_scope_entries(&mut entries);
630 self.splice_sticky_tooltips_after_anchors(&mut entries);
631 let root_scope = ScopeNode {
632 policy: crate::focus::TraversalScopePolicy::Cycle,
633 entries,
634 };
635
636 if let StepResult::Found(next_id) = navigate_scope(&root_scope, self.focused, reverse, true)
637 {
638 self.focus_with_origin_ops(next_id, crate::focus::FocusOrigin::Keyboard, &mut *ops);
639 }
640 }
643
644 fn tooltip_content_root(&self, id: WidgetId) -> Option<usize> {
646 self.tooltips.iter().position(|e| e.content_id == id)
647 }
648
649 fn splice_sticky_tooltips_after_anchors(&self, entries: &mut Vec<ScopeEntry>) {
667 let sticky: Vec<(WidgetId, WidgetId)> = self
668 .tooltips
669 .iter()
670 .filter(|e| e.is_sticky && e.overlay_id.is_some())
671 .map(|e| (e.anchor_id, e.content_id))
672 .collect();
673
674 for (anchor_id, content_id) in sticky {
675 let mut panel = Vec::new();
676 self.collect_scope_entries(content_id, &mut panel);
677 if panel.is_empty() {
678 continue;
679 }
680 sort_scope_entries(&mut panel);
681
682 let stop = self
687 .find_focusable_at_or_above(anchor_id)
688 .unwrap_or(anchor_id);
689 let at = entries
690 .iter()
691 .position(|entry| scope_entry_contains(entry, stop))
692 .map_or(entries.len(), |i| i + 1);
697
698 for (offset, item) in panel.into_iter().enumerate() {
699 entries.insert(at + offset, item);
700 }
701 }
702 }
703
704 pub(super) fn tab_stop_effective(&self, id: WidgetId) -> bool {
712 let mut current = Some(id);
713 while let Some(cur) = current {
714 let Some(node) = self.arena.get(cur) else {
715 break;
716 };
717 if let Some(prop) = node.tab_stop.as_ref() {
718 return prop.get();
719 }
720 current = node.parent;
721 }
722 true
723 }
724
725 pub(super) fn is_node_focusable(&self, node: &crate::arena::WidgetNode) -> bool {
727 node.node_focusable.unwrap_or(false)
728 }
729
730 pub(super) fn find_focusable_at_or_above(&self, id: WidgetId) -> Option<WidgetId> {
732 let mut current = Some(id);
733 while let Some(current_id) = current {
734 if let Some(node) = self.arena.get(current_id)
735 && self.is_node_focusable(node)
736 {
737 return Some(current_id);
738 }
739 current = self.arena.parent(current_id);
740 }
741 None
742 }
743
744 fn collect_scope_entries(&self, id: WidgetId, out: &mut Vec<ScopeEntry>) {
760 if !self.arena.is_active(id) {
761 return;
762 }
763 let Some(node) = self.arena.get(id) else {
764 return;
765 };
766 if node
767 .enabled_state
768 .as_ref()
769 .map(|s| !s.get())
770 .unwrap_or(false)
771 {
772 return;
773 }
774
775 if let Some(policy) = node.node_traversal_scope {
778 let mut child_entries = Vec::new();
779 for &child in &node.children {
780 self.collect_scope_entries(child, &mut child_entries);
781 }
782 sort_scope_entries(&mut child_entries);
783 out.push(ScopeEntry {
784 tab_index: node.node_tab_index,
785 kind: ScopeEntryKind::Scope(ScopeNode {
786 policy,
787 entries: child_entries,
788 }),
789 });
790 return;
791 }
792
793 if self.is_node_focusable(node) && self.tab_stop_effective(id) {
795 out.push(ScopeEntry {
796 tab_index: node.node_tab_index,
797 kind: ScopeEntryKind::Focusable(id),
798 });
799 }
800 for &child in &node.children {
801 self.collect_scope_entries(child, out);
802 }
803 }
804
805 pub(super) fn strict_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
810 let mut chain = Vec::new();
811 if let Some(start) = id {
812 let mut current = self.arena.parent(start);
813 while let Some(parent) = current {
814 chain.push(parent);
815 current = self.arena.parent(parent);
816 }
817 }
818 chain
819 }
820
821 pub(crate) fn update_focus_within_signals(
826 &mut self,
827 old: Option<WidgetId>,
828 new: Option<WidgetId>,
829 ) {
830 let old_chain = self.strict_ancestors_of(old);
831 let new_chain = self.strict_ancestors_of(new);
832 for &id in &old_chain {
834 if !new_chain.contains(&id)
835 && let Some(node) = self.arena.get(id)
836 && let Some(sig) = node.focus_within_signal.clone()
837 {
838 sig.set(false);
839 }
840 }
841 for &id in &new_chain {
843 if !old_chain.contains(&id)
844 && let Some(node) = self.arena.get(id)
845 && let Some(sig) = node.focus_within_signal.clone()
846 {
847 sig.set(true);
848 }
849 }
850 }
851
852 pub(super) fn inclusive_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
855 let mut chain = Vec::new();
856 if let Some(start) = id {
857 chain.push(start);
858 chain.extend(self.strict_ancestors_of(Some(start)));
859 }
860 chain
861 }
862
863 pub(crate) fn update_view_focus_signals(
868 &mut self,
869 old: Option<WidgetId>,
870 new: Option<WidgetId>,
871 ) {
872 let old_chain = self.inclusive_ancestors_of(old);
873 let new_chain = self.inclusive_ancestors_of(new);
874 for &id in &old_chain {
875 if !new_chain.contains(&id)
876 && let Some(node) = self.arena.get(id)
877 && let Some(sig) = node.view_focus_signal.clone()
878 {
879 sig.set(false);
880 }
881 }
882 for &id in &new_chain {
883 if !old_chain.contains(&id)
884 && let Some(node) = self.arena.get(id)
885 && let Some(sig) = node.view_focus_signal.clone()
886 {
887 sig.set(true);
888 }
889 }
890 }
891
892 pub(crate) fn view_focus_signal_for(
895 &mut self,
896 node_id: WidgetId,
897 ) -> crate::signal::Signal<bool> {
898 if let Some(node) = self.arena.get(node_id)
899 && let Some(sig) = node.view_focus_signal.clone()
900 {
901 return sig;
902 }
903 let active = self.inclusive_ancestors_of(self.focused).contains(&node_id);
904 let sig = crate::signal::Signal::new(active);
905 if let Some(node) = self.arena.get_mut(node_id) {
906 node.view_focus_signal = Some(sig.clone());
907 }
908 sig
909 }
910
911 pub fn view_focus_active_for(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
917 match self.find_focusable_at_or_above(node_id) {
918 Some(scope) => self.view_focus_signal_for(scope),
919 None => crate::signal::Signal::new(true),
920 }
921 }
922
923 pub fn begin_view_focus(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
928 let sig = self.view_focus_signal_for(node_id);
929 self.view_focus_stack.push(sig.clone());
930 sig
931 }
932
933 pub fn end_view_focus(&mut self) {
935 self.view_focus_stack.pop();
936 }
937
938 pub fn current_view_focus(&self) -> Option<crate::signal::Signal<bool>> {
940 self.view_focus_stack.last().cloned()
941 }
942
943 pub(crate) fn set_hovered(&mut self, value: Option<WidgetId>) {
957 if let Some(entry) = self.pointers.hover_owner_mut() {
958 entry.hovered = value;
959 }
960 if self.hovered_signal.get() != value {
961 self.hovered_signal.set(value);
962 }
963 }
964
965 pub(crate) fn set_focused(&mut self, value: Option<WidgetId>) {
972 self.focused = value;
973 if self.focused_signal.get() != value {
974 self.focused_signal.set(value);
975 }
976 }
977
978 pub(crate) fn update_hover_within_signals(
981 &mut self,
982 old: Option<WidgetId>,
983 new: Option<WidgetId>,
984 ) {
985 let old_chain = self.strict_ancestors_of(old);
986 let new_chain = self.strict_ancestors_of(new);
987 if let Some(entry) = self.pointers.hover_owner_mut() {
991 entry.hover_within = new_chain.clone();
992 }
993 for &id in &old_chain {
994 if !new_chain.contains(&id)
995 && let Some(node) = self.arena.get(id)
996 && let Some(sig) = node.hover_within_signal.clone()
997 {
998 sig.set(false);
999 }
1000 }
1001 for &id in &new_chain {
1002 if !old_chain.contains(&id)
1003 && let Some(node) = self.arena.get(id)
1004 && let Some(sig) = node.hover_within_signal.clone()
1005 {
1006 sig.set(true);
1007 }
1008 }
1009 }
1010}
1011
1012#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1015pub(crate) enum FocusRequestMode {
1016 Exact,
1018 SelfOrDescendant,
1021 Into,
1024}
1025
1026#[derive(Debug, Clone, Copy)]
1029pub(crate) struct PendingFocusRequest {
1030 id: WidgetId,
1031 mode: FocusRequestMode,
1032}
1033
1034struct ScopeEntry {
1046 tab_index: Option<i32>,
1047 kind: ScopeEntryKind,
1048}
1049
1050enum ScopeEntryKind {
1051 Focusable(WidgetId),
1052 Scope(ScopeNode),
1053}
1054
1055struct ScopeNode {
1057 policy: crate::focus::TraversalScopePolicy,
1058 entries: Vec<ScopeEntry>,
1059}
1060
1061enum StepResult {
1063 Found(WidgetId),
1065 Escaped,
1069}
1070
1071fn scope_wraps(policy: crate::focus::TraversalScopePolicy, is_root: bool) -> bool {
1074 is_root || matches!(policy, crate::focus::TraversalScopePolicy::Cycle)
1075}
1076
1077fn sort_scope_entries(entries: &mut [ScopeEntry]) {
1080 entries.sort_by(|a, b| match (a.tab_index, b.tab_index) {
1081 (Some(ia), Some(ib)) => ia.cmp(&ib),
1082 (Some(_), None) => std::cmp::Ordering::Less,
1083 (None, Some(_)) => std::cmp::Ordering::Greater,
1084 (None, None) => std::cmp::Ordering::Equal,
1085 });
1086}
1087
1088fn scope_entry_contains(entry: &ScopeEntry, f: WidgetId) -> bool {
1090 match &entry.kind {
1091 ScopeEntryKind::Focusable(id) => *id == f,
1092 ScopeEntryKind::Scope(child) => child.entries.iter().any(|e| scope_entry_contains(e, f)),
1093 }
1094}
1095
1096fn enter_scope_edge(scope: &ScopeNode, reverse: bool) -> Option<WidgetId> {
1100 let n = scope.entries.len();
1101 for k in 0..n {
1102 let idx = if reverse { n - 1 - k } else { k };
1103 match &scope.entries[idx].kind {
1104 ScopeEntryKind::Focusable(id) => return Some(*id),
1105 ScopeEntryKind::Scope(child) => {
1106 if let Some(id) = enter_scope_edge(child, reverse) {
1107 return Some(id);
1108 }
1109 }
1110 }
1111 }
1112 None
1113}
1114
1115fn navigate_scope(
1119 scope: &ScopeNode,
1120 focused: Option<WidgetId>,
1121 reverse: bool,
1122 is_root: bool,
1123) -> StepResult {
1124 if scope.entries.is_empty() {
1125 return StepResult::Escaped;
1126 }
1127
1128 let cur = focused.and_then(|f| {
1129 scope
1130 .entries
1131 .iter()
1132 .position(|e| scope_entry_contains(e, f))
1133 });
1134
1135 let from = match cur {
1136 None => {
1137 return match enter_scope_edge(scope, reverse) {
1140 Some(id) => StepResult::Found(id),
1141 None => StepResult::Escaped,
1142 };
1143 }
1144 Some(i) => i,
1145 };
1146
1147 if let ScopeEntryKind::Scope(child) = &scope.entries[from].kind
1150 && let StepResult::Found(id) = navigate_scope(child, focused, reverse, false)
1151 {
1152 return StepResult::Found(id);
1153 }
1154
1155 step_to_sibling(scope, from, reverse, is_root)
1156}
1157
1158fn step_to_sibling(scope: &ScopeNode, from: usize, reverse: bool, is_root: bool) -> StepResult {
1162 let n = scope.entries.len();
1163 let mut idx = from;
1164 for _ in 0..n {
1165 let next = if reverse {
1166 if idx == 0 {
1167 if scope_wraps(scope.policy, is_root) {
1168 n - 1
1169 } else {
1170 return StepResult::Escaped;
1171 }
1172 } else {
1173 idx - 1
1174 }
1175 } else if idx + 1 >= n {
1176 if scope_wraps(scope.policy, is_root) {
1177 0
1178 } else {
1179 return StepResult::Escaped;
1180 }
1181 } else {
1182 idx + 1
1183 };
1184
1185 match &scope.entries[next].kind {
1186 ScopeEntryKind::Focusable(id) => return StepResult::Found(*id),
1187 ScopeEntryKind::Scope(child) => {
1188 if let Some(id) = enter_scope_edge(child, reverse) {
1189 return StepResult::Found(id);
1190 }
1191 idx = next;
1193 }
1194 }
1195 }
1196 StepResult::Escaped
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201 use super::*;
1202 use crate::test_widgets::FillWidget;
1203 use crate::widget_builder::WidgetBuilder;
1204
1205 #[test]
1214 fn a_reveal_walks_the_ancestors_of_whoever_owns_the_rect() {
1215 use crate::event::{EventResponse, ScrollAlign, ScrollMotion};
1216 use std::cell::Cell;
1217 use std::rc::Rc;
1218
1219 let asked = Rc::new(Cell::new(0usize));
1220 let mut tree = WidgetTree::new();
1221 let viewport = {
1223 let asked = asked.clone();
1224 tree.add(
1225 FillWidget::new()
1228 .on_scroll(move |event, _ctx| {
1229 if matches!(event, WidgetEvent::ScrollIntoView { .. }) {
1230 asked.set(asked.get() + 1);
1231 }
1232 EventResponse::Ignored
1233 })
1234 .clips_children(true),
1235 )
1236 };
1237 let inside = tree.add_child(viewport, FillWidget::new());
1238 let button = tree.add(FillWidget::new());
1240 tree.layout(SizeProposal::exact(400.0, 300.0));
1241
1242 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
1244 let mut ops = crate::window::NoopWindowOps;
1245
1246 tree.scroll_rect_into_view(
1247 button,
1248 target,
1249 0.0,
1250 ScrollAlign::Minimal,
1251 ScrollMotion::Instant,
1252 &mut ops,
1253 );
1254 assert_eq!(
1255 asked.get(),
1256 0,
1257 "walking from the asker never reaches a viewport it is not inside"
1258 );
1259
1260 tree.scroll_rect_into_view(
1261 inside,
1262 target,
1263 0.0,
1264 ScrollAlign::Minimal,
1265 ScrollMotion::Instant,
1266 &mut ops,
1267 );
1268 assert_eq!(
1269 asked.get(),
1270 1,
1271 "walking from the rect's owner reaches the viewport enclosing it"
1272 );
1273 }
1274
1275 fn recorder(
1278 tree: &mut WidgetTree,
1279 child: WidgetId,
1280 seen: std::rc::Rc<std::cell::RefCell<Vec<Rect>>>,
1281 ) -> WidgetId {
1282 use crate::event::EventResponse;
1283 use crate::test_widgets::StackWidget;
1284 tree.add(
1285 StackWidget::new()
1286 .child(child)
1287 .on_scroll(move |event, _ctx| {
1288 if let WidgetEvent::ScrollIntoView { target_bounds, .. } = event {
1289 seen.borrow_mut().push(*target_bounds);
1290 }
1291 EventResponse::Ignored
1292 })
1293 .clips_children(true),
1294 )
1295 }
1296
1297 #[test]
1308 fn a_reveal_projects_the_rect_as_it_climbs_out_of_a_content_transform() {
1309 use crate::event::{ScrollAlign, ScrollMotion};
1310 use std::cell::RefCell;
1311 use std::rc::Rc;
1312 use teksilo_canvas::Transform2D;
1313
1314 let outer_seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1315 let view_seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1316
1317 let mut tree = WidgetTree::new();
1318 let card = tree.add(FillWidget::new());
1319 let view = recorder(&mut tree, card, view_seen.clone());
1320 let _outer = recorder(&mut tree, view, outer_seen.clone());
1321 tree.layout(SizeProposal::exact(400.0, 300.0));
1322 tree.set_content_transform(view, Transform2D::translate(-90.0, 0.0));
1325
1326 let card_rect = Rect::new(100.0, 100.0, 50.0, 50.0);
1327 let mut ops = crate::window::NoopWindowOps;
1328 tree.scroll_rect_into_view(
1329 card,
1330 card_rect,
1331 0.0,
1332 ScrollAlign::Minimal,
1333 ScrollMotion::Instant,
1334 &mut ops,
1335 );
1336
1337 assert!(
1338 view_seen.borrow().is_empty(),
1339 "the card paints at window x = 10, inside the 400×300 viewport — \
1340 comparing its scene rect against the viewport is what used to \
1341 produce a spurious verdict here, in either direction"
1342 );
1343 assert!(
1344 outer_seen.borrow().is_empty(),
1345 "and nothing outside it has a reason to scroll either"
1346 );
1347
1348 tree.set_content_transform(view, Transform2D::translate(-600.0, 0.0));
1352 tree.scroll_rect_into_view(
1353 card,
1354 card_rect,
1355 0.0,
1356 ScrollAlign::Minimal,
1357 ScrollMotion::Instant,
1358 &mut ops,
1359 );
1360 assert_eq!(
1361 view_seen.borrow().as_slice(),
1362 &[card_rect],
1363 "the view is asked to reveal the card, in the view's own content \
1364 space — the space `SceneView::ensure_visible` takes by signature"
1365 );
1366 assert_eq!(
1367 outer_seen.borrow().as_slice(),
1368 &[Rect::new(-500.0, 100.0, 50.0, 50.0)],
1369 "and everything further out is asked in window space, which is the \
1370 only space it can compare against its own viewport"
1371 );
1372 }
1373
1374 #[test]
1377 fn a_reveal_with_no_transform_in_the_chain_passes_the_rect_through() {
1378 use crate::event::{ScrollAlign, ScrollMotion};
1379 use std::cell::RefCell;
1380 use std::rc::Rc;
1381
1382 let seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1383 let mut tree = WidgetTree::new();
1384 let inside = tree.add(FillWidget::new());
1385 let _viewport = recorder(&mut tree, inside, seen.clone());
1386 tree.layout(SizeProposal::exact(400.0, 300.0));
1387
1388 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
1389 let mut ops = crate::window::NoopWindowOps;
1390 tree.scroll_rect_into_view(
1391 inside,
1392 target,
1393 0.0,
1394 ScrollAlign::Minimal,
1395 ScrollMotion::Instant,
1396 &mut ops,
1397 );
1398 assert_eq!(seen.borrow().as_slice(), &[target]);
1399 }
1400
1401 #[test]
1402 fn focus_widget() {
1403 let mut tree = WidgetTree::new();
1404 let widget = tree.add(FillWidget::new());
1405 tree.layout(SizeProposal::exact(100.0, 50.0));
1406 tree.focus(widget);
1407 assert_eq!(tree.focused(), Some(widget));
1408 }
1409
1410 #[test]
1411 fn focus_change() {
1412 let mut tree = WidgetTree::new();
1413 let a = tree.add(FillWidget::new());
1414 let b = tree.add(FillWidget::new());
1415 tree.layout(SizeProposal::exact(100.0, 50.0));
1416 tree.focus(a);
1417 assert_eq!(tree.focused(), Some(a));
1418 tree.focus(b);
1419 assert_eq!(tree.focused(), Some(b));
1420 }
1421
1422 #[derive(Debug)]
1426 struct RebuildingRows {
1427 epoch: crate::signal::Signal<u64>,
1428 rows: Vec<WidgetId>,
1429 }
1430
1431 impl Widget for RebuildingRows {
1432 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1433 let sid = ctx.self_id();
1434 let reg = ctx.binding_registry();
1435 self.epoch
1436 .bind_to(sid, reg, crate::binding::BindingLevel::Rebuild);
1437 self.rows = (0..3)
1438 .map(|_| ctx.add(FillWidget::new().focusable()))
1439 .collect();
1440 self.rows.clone()
1441 }
1442
1443 fn layout_response(
1444 &self,
1445 proposal: SizeProposal,
1446 _ctx: &LayoutContext,
1447 ) -> crate::widget::LayoutResponse {
1448 proposal.resolve(0.0, 0.0).into()
1449 }
1450
1451 fn children(&self) -> Vec<WidgetId> {
1452 self.rows.clone()
1453 }
1454 }
1455
1456 #[test]
1466 fn a_rebuild_keeps_focus_inside_the_subtree_that_had_it() {
1467 let mut tree = WidgetTree::new();
1468 let epoch = crate::signal::Signal::new(0u64);
1469 let root = tree.add(RebuildingRows {
1470 epoch: epoch.clone(),
1471 rows: Vec::new(),
1472 });
1473 tree.layout(SizeProposal::exact(100.0, 60.0));
1474
1475 let first_row = tree
1476 .first_focusable_descendant(root)
1477 .expect("the rows are focusable");
1478 tree.focus(first_row);
1479 assert_eq!(tree.focused(), Some(first_row));
1480
1481 epoch.set(1);
1483 tree.layout(SizeProposal::exact(100.0, 60.0));
1484
1485 let focused = tree
1486 .focused()
1487 .expect("a rebuild must not drop focus out of the rebuilt subtree");
1488 assert_ne!(
1489 focused, first_row,
1490 "the old row is dead — focus must have moved to a freshly built one"
1491 );
1492 assert!(
1493 tree.is_descendant_of(focused, root),
1494 "focus must land back inside the rebuilt subtree"
1495 );
1496 assert!(tree.is_active(focused), "the focused node must be live");
1497 }
1498
1499 #[test]
1504 fn a_rebuild_elsewhere_does_not_steal_focus() {
1505 let mut tree = WidgetTree::new();
1506 let epoch = crate::signal::Signal::new(0u64);
1507 let outsider = tree.add(FillWidget::new().focusable());
1508 let _rows = tree.add(RebuildingRows {
1509 epoch: epoch.clone(),
1510 rows: Vec::new(),
1511 });
1512 tree.layout(SizeProposal::exact(100.0, 60.0));
1513
1514 tree.focus(outsider);
1515 assert_eq!(tree.focused(), Some(outsider));
1516
1517 epoch.set(1);
1518 tree.layout(SizeProposal::exact(100.0, 60.0));
1519
1520 assert_eq!(
1521 tree.focused(),
1522 Some(outsider),
1523 "a rebuild that never held focus must not pull focus into itself"
1524 );
1525 }
1526
1527 #[derive(Debug)]
1531 struct KeepingGroups {
1532 epoch: crate::signal::Signal<u64>,
1533 group_epochs: Vec<crate::signal::Signal<u64>>,
1534 groups: Vec<WidgetId>,
1535 }
1536
1537 impl Widget for KeepingGroups {
1538 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1539 let sid = ctx.self_id();
1540 let reg = ctx.binding_registry();
1541 self.epoch
1542 .bind_to(sid, reg, crate::binding::BindingLevel::Rebuild);
1543 if self.groups.is_empty() {
1544 self.groups = self
1545 .group_epochs
1546 .iter()
1547 .map(|epoch| {
1548 ctx.add(RebuildingRows {
1549 epoch: epoch.clone(),
1550 rows: Vec::new(),
1551 })
1552 })
1553 .collect();
1554 }
1555 self.groups.clone()
1556 }
1557
1558 fn layout_response(
1559 &self,
1560 proposal: SizeProposal,
1561 _ctx: &LayoutContext,
1562 ) -> crate::widget::LayoutResponse {
1563 proposal.resolve(0.0, 0.0).into()
1564 }
1565
1566 fn children(&self) -> Vec<WidgetId> {
1567 self.groups.clone()
1568 }
1569
1570 fn preserves_children_on_rebuild(&self) -> bool {
1571 true
1572 }
1573 }
1574
1575 #[test]
1586 fn a_rebuild_restores_focus_into_the_innermost_ancestor_that_survived() {
1587 let mut tree = WidgetTree::new();
1588 let epoch = crate::signal::Signal::new(0u64);
1589 let group_epochs = vec![
1590 crate::signal::Signal::new(0u64),
1591 crate::signal::Signal::new(0u64),
1592 ];
1593 let root = tree.add(KeepingGroups {
1594 epoch: epoch.clone(),
1595 group_epochs: group_epochs.clone(),
1596 groups: Vec::new(),
1597 });
1598 tree.layout(SizeProposal::exact(100.0, 60.0));
1599
1600 let groups = tree.children(root);
1601 let second_group = groups[1];
1602 let row = tree
1603 .first_focusable_descendant(second_group)
1604 .expect("the second group's rows are focusable");
1605 tree.focus(row);
1606
1607 epoch.set(1);
1610 group_epochs[1].set(1);
1611 tree.layout(SizeProposal::exact(100.0, 60.0));
1612
1613 assert_eq!(
1614 tree.children(root),
1615 groups,
1616 "the container kept both groups (the premise of this test)"
1617 );
1618 let focused = tree.focused().expect("focus must not be dropped");
1619 assert_ne!(focused, row, "the old row is dead");
1620 assert!(
1621 tree.is_descendant_of(focused, second_group),
1622 "focus must land back in the group that held it, the innermost \
1623 ancestor that survived, not in the first group of the container"
1624 );
1625 }
1626
1627 #[test]
1628 fn first_focusable_descendant_prefers_first_focusable_child() {
1629 let mut tree = WidgetTree::new();
1630 let a = tree.add(FillWidget::new().focusable());
1631 let _not_focusable = tree.add(FillWidget::new());
1632 let b = tree.add(FillWidget::new().focusable());
1633 let root = tree.add(crate::test_widgets::StackWidget::new().child(a).child(b));
1634 tree.layout(SizeProposal::exact(100.0, 50.0));
1635
1636 assert_eq!(tree.first_focusable_descendant(root), Some(a));
1637 }
1638
1639 #[test]
1640 fn tab_cycles_through_focusable_widgets() {
1641 let mut tree = WidgetTree::new();
1642 let a = tree.add(FillWidget::new().focusable());
1643 let b = tree.add(FillWidget::new().focusable());
1644 let c = tree.add(FillWidget::new().focusable());
1645 tree.layout(SizeProposal::exact(100.0, 50.0));
1646
1647 assert_eq!(tree.focused(), None);
1648
1649 tree.press_key(Key::Tab, Modifiers::NONE);
1650 assert_eq!(tree.focused(), Some(a));
1651
1652 tree.press_key(Key::Tab, Modifiers::NONE);
1653 assert_eq!(tree.focused(), Some(b));
1654
1655 tree.press_key(Key::Tab, Modifiers::NONE);
1656 assert_eq!(tree.focused(), Some(c));
1657
1658 tree.press_key(Key::Tab, Modifiers::NONE);
1659 assert_eq!(tree.focused(), Some(a));
1660 }
1661
1662 #[test]
1663 fn tab_stop_on_composite_excludes_focusable_descendant() {
1664 let mut tree = WidgetTree::new();
1669 let leaf = tree.add(FillWidget::new().focusable());
1670 let composite = tree.add(crate::test_widgets::StackWidget::new().child(leaf));
1671 let other = tree.add(FillWidget::new().focusable());
1672 tree.layout(SizeProposal::exact(100.0, 50.0));
1673
1674 tree.set_tab_stop(composite, false);
1675
1676 tree.press_key(Key::Tab, Modifiers::NONE);
1677 assert_eq!(
1678 tree.focused(),
1679 Some(other),
1680 "Tab must skip the suppressed composite's inner leaf"
1681 );
1682 tree.press_key(Key::Tab, Modifiers::NONE);
1683 assert_eq!(
1684 tree.focused(),
1685 Some(other),
1686 "only the un-suppressed control participates in Tab"
1687 );
1688
1689 tree.set_tab_stop(composite, true);
1691 tree.set_focused(None);
1692 tree.press_key(Key::Tab, Modifiers::NONE);
1693 let first = tree.focused();
1694 tree.press_key(Key::Tab, Modifiers::NONE);
1695 let second = tree.focused();
1696 assert_ne!(
1697 first, second,
1698 "with the composite re-enabled, Tab visits both controls"
1699 );
1700 }
1701
1702 #[test]
1703 fn shift_tab_cycles_backwards() {
1704 let mut tree = WidgetTree::new();
1705 let a = tree.add(FillWidget::new().focusable());
1706 let b = tree.add(FillWidget::new().focusable());
1707 let c = tree.add(FillWidget::new().focusable());
1708 tree.layout(SizeProposal::exact(100.0, 50.0));
1709
1710 tree.press_key(Key::Tab, Modifiers::NONE);
1711 assert_eq!(tree.focused(), Some(a));
1712
1713 tree.press_key(Key::Tab, Modifiers::SHIFT);
1714 assert_eq!(tree.focused(), Some(c));
1715
1716 tree.press_key(Key::Tab, Modifiers::SHIFT);
1717 assert_eq!(tree.focused(), Some(b));
1718 }
1719
1720 #[test]
1721 fn tab_skips_non_focusable_widgets() {
1722 let mut tree = WidgetTree::new();
1723 let _not_focusable = tree.add(FillWidget::new());
1724 let a = tree.add(FillWidget::new().focusable());
1725 let _also_not = tree.add(FillWidget::new());
1726 let b = tree.add(FillWidget::new().focusable());
1727 tree.layout(SizeProposal::exact(100.0, 50.0));
1728
1729 tree.press_key(Key::Tab, Modifiers::NONE);
1730 assert_eq!(tree.focused(), Some(a));
1731
1732 tree.press_key(Key::Tab, Modifiers::NONE);
1733 assert_eq!(tree.focused(), Some(b));
1734 }
1735
1736 #[test]
1747 fn focus_does_not_check_that_the_node_is_focusable() {
1748 let mut tree = WidgetTree::new();
1749 let inert = tree.add(FillWidget::new()); tree.layout(SizeProposal::exact(100.0, 50.0));
1751 assert!(
1752 tree.tab_stops_within(inert).is_empty(),
1753 "the subject has to be unreachable for the point to be made"
1754 );
1755
1756 tree.focus(inert);
1757 assert_eq!(
1758 tree.focused(),
1759 Some(inert),
1760 "focus lands on a node Tab traversal can never offer"
1761 );
1762 }
1763
1764 #[test]
1765 fn tab_focus_has_keyboard_origin() {
1766 let mut tree = WidgetTree::new();
1767 tree.add(FillWidget::new().focusable());
1768 tree.layout(SizeProposal::exact(100.0, 50.0));
1769
1770 tree.press_key(Key::Tab, Modifiers::NONE);
1771 assert_eq!(
1772 tree.focus_origin(),
1773 Some(crate::focus::FocusOrigin::Keyboard)
1774 );
1775 }
1776
1777 #[test]
1778 fn tab_skips_focusable_inside_disabled_ancestor() {
1779 use crate::signal::Signal;
1780 use crate::test_widgets::StackWidget;
1781
1782 let mut tree = WidgetTree::new();
1783 let a = tree.add(FillWidget::new().focusable());
1784 let inner = tree.add(FillWidget::new().focusable());
1785 let disabled_container = tree.add(StackWidget::new().child(inner));
1786 let c = tree.add(FillWidget::new().focusable());
1787 tree.enabled_when(disabled_container, Signal::new(false));
1788 tree.layout(SizeProposal::exact(200.0, 100.0));
1789
1790 tree.press_key(Key::Tab, Modifiers::NONE);
1791 assert_eq!(tree.focused(), Some(a));
1792
1793 tree.press_key(Key::Tab, Modifiers::NONE);
1794 assert_eq!(
1795 tree.focused(),
1796 Some(c),
1797 "tab should skip the focusable widget nested inside the disabled container"
1798 );
1799 }
1800
1801 #[test]
1802 fn dormant_widget_not_in_focus_cycle() {
1803 let mut tree = WidgetTree::new();
1804 let a = tree.add(FillWidget::new().focusable());
1805 let b = tree.add(FillWidget::new().focusable());
1806 let c = tree.add(FillWidget::new().focusable());
1807 tree.layout(SizeProposal::exact(200.0, 100.0));
1808
1809 tree.focus(a);
1810 tree.set_dormant(b);
1811
1812 tree.press_key(Key::Tab, Modifiers::NONE);
1813 assert_eq!(tree.focused(), Some(c));
1814 }
1815
1816 #[test]
1821 fn revalidate_delivers_focus_lost_when_focused_widget_goes_dormant() {
1822 use std::cell::Cell;
1823 use std::rc::Rc;
1824
1825 let lost = Rc::new(Cell::new(0_u32));
1826 let gained = Rc::new(Cell::new(0_u32));
1827 let lost_c = lost.clone();
1828 let gained_c = gained.clone();
1829
1830 let mut tree = WidgetTree::new();
1831 let editor = tree.add(
1832 FillWidget::new()
1833 .focusable()
1834 .on_focus(move |is_gained, _ctx| {
1835 if is_gained {
1836 gained_c.set(gained_c.get() + 1);
1837 } else {
1838 lost_c.set(lost_c.get() + 1);
1839 }
1840 }),
1841 );
1842 let _other = tree.add(FillWidget::new().focusable());
1843 tree.layout(SizeProposal::exact(200.0, 100.0));
1844
1845 tree.focus(editor);
1846 assert_eq!(tree.focused(), Some(editor));
1847 assert_eq!(gained.get(), 1, "focus() delivers FocusGained");
1848 assert_eq!(lost.get(), 0);
1849
1850 tree.set_dormant(editor);
1852 let mut noop = crate::window::NoopWindowOps;
1854 tree.revalidate_interaction_state(&mut noop);
1855
1856 assert_eq!(
1857 tree.focused(),
1858 None,
1859 "tree focus must clear when the target is dormant"
1860 );
1861 assert_eq!(
1862 lost.get(),
1863 1,
1864 "FocusLost must reach the dormant widget so it can clear has_focus / caret blink"
1865 );
1866 }
1867
1868 #[test]
1869 fn tab_cycles_focus_in_tree_order() {
1870 let mut tree = WidgetTree::new();
1871 let a = tree.add(FillWidget::new().focusable());
1872 let b = tree.add(FillWidget::new().focusable());
1873 tree.layout(SizeProposal::exact(200.0, 80.0));
1874
1875 tree.focus(a);
1876 assert_eq!(tree.focused(), Some(a));
1877
1878 tree.press_key(Key::Tab, Modifiers::NONE);
1879 assert_eq!(tree.focused(), Some(b));
1880
1881 tree.press_key(Key::Tab, Modifiers::SHIFT);
1882 assert_eq!(tree.focused(), Some(a));
1883 }
1884
1885 #[test]
1886 fn focus_survives_theme_switch() {
1887 let mut tree = WidgetTree::new();
1888 let a = tree.add(FillWidget::new().focusable());
1889 let _b = tree.add(FillWidget::new().focusable());
1890 tree.layout(SizeProposal::exact(200.0, 80.0));
1891
1892 tree.focus(a);
1893 assert_eq!(tree.focused(), Some(a));
1894
1895 tree.set_theme(crate::presets::intui::dark());
1896 tree.layout(SizeProposal::exact(200.0, 80.0));
1897
1898 assert_eq!(
1899 tree.focused(),
1900 Some(a),
1901 "theme switch must not clobber focus"
1902 );
1903 }
1904
1905 #[test]
1906 fn focus_survives_locale_switch() {
1907 let mut tree = WidgetTree::new();
1908 let a = tree.add(FillWidget::new().focusable());
1909 tree.layout(SizeProposal::exact(200.0, 80.0));
1910
1911 tree.focus(a);
1912 tree.set_locale("fr-FR".to_string());
1913 tree.layout(SizeProposal::exact(200.0, 80.0));
1914
1915 assert_eq!(
1916 tree.focused(),
1917 Some(a),
1918 "locale switch must not clobber focus"
1919 );
1920 }
1921
1922 #[test]
1925 fn focus_within_flips_when_descendant_takes_focus() {
1926 use crate::signal::Signal;
1927 use crate::test_widgets::StackWidget;
1928 use crate::widget_builder::WidgetBuilder;
1929
1930 let halo = Signal::new(false);
1931
1932 let mut tree = WidgetTree::new();
1933 let leaf = tree.add(FillWidget::new().focusable());
1934 let mid = tree.add(StackWidget::new().child(leaf));
1935 let _outer = tree.add(StackWidget::new().child(mid).focus_within(halo.clone()));
1936
1937 tree.layout(SizeProposal::exact(100.0, 50.0));
1938 assert!(!halo.get(), "no focus yet → signal is false");
1939
1940 tree.focus(leaf);
1941 assert!(halo.get(), "leaf now focused, outer is its strict ancestor");
1942 }
1943
1944 #[test]
1945 fn focus_within_strict_ancestors_only() {
1946 use crate::signal::Signal;
1949 use crate::widget_builder::WidgetBuilder;
1950
1951 let halo = Signal::new(false);
1952
1953 let mut tree = WidgetTree::new();
1954 let widget = tree.add(FillWidget::new().focusable().focus_within(halo.clone()));
1955 tree.layout(SizeProposal::exact(100.0, 50.0));
1956 tree.focus(widget);
1957
1958 assert!(
1959 !halo.get(),
1960 "focusing the widget itself must not set its own focus_within"
1961 );
1962 }
1963
1964 #[test]
1965 fn view_focus_active_is_inclusive_for_the_view_itself() {
1966 use crate::test_widgets::StackWidget;
1972 use crate::widget_builder::WidgetBuilder;
1973
1974 let mut tree = WidgetTree::new();
1975 let item = tree.add(FillWidget::new()); let view = tree.add(StackWidget::new().child(item).focusable(true));
1977 let outside = tree.add(FillWidget::new().focusable());
1978 tree.layout(SizeProposal::exact(100.0, 50.0));
1979
1980 let active = tree.view_focus_active_for(item);
1981 assert!(!active.get(), "no focus yet → scope inactive");
1982
1983 tree.focus(view);
1984 assert!(
1985 active.get(),
1986 "view focused directly → scope active (focus_within would be false here)"
1987 );
1988
1989 tree.focus(outside);
1990 assert!(
1991 !active.get(),
1992 "focus moved outside the view → scope inactive"
1993 );
1994 }
1995
1996 #[test]
1997 fn begin_view_focus_for_keys_on_the_view_root_not_the_building_pane() {
1998 use crate::test_widgets::StackWidget;
2006 use crate::widget_builder::WidgetBuilder;
2007
2008 let mut tree = WidgetTree::new();
2009 let row = tree.add(FillWidget::new()); let pane = tree.add(StackWidget::new().child(row)); let root = tree.add(StackWidget::new().child(pane).focusable(true));
2012 tree.layout(SizeProposal::exact(100.0, 50.0));
2013
2014 let keyed_on_root = tree.begin_view_focus(root);
2016 tree.end_view_focus();
2017 let keyed_on_pane = tree.view_focus_signal_for(pane);
2019
2020 assert!(!keyed_on_root.get(), "no focus yet → inactive");
2021 tree.focus(root);
2022 assert!(keyed_on_root.get(), "view root focused → row scope active");
2023 assert!(
2024 !keyed_on_pane.get(),
2025 "pane-keyed scope stays false: the focused root is the pane's ancestor, not its descendant",
2026 );
2027 }
2028
2029 #[test]
2030 fn focus_visible_tracks_input_modality() {
2031 use crate::event::{Key, Modifiers};
2034 let mut tree = WidgetTree::new();
2035 let w = tree.add(FillWidget::new().focusable());
2036 tree.layout(SizeProposal::exact(100.0, 50.0));
2037 let vis = tree.focus_visible_signal();
2038 assert!(!vis.get(), "starts not focus-visible");
2039
2040 tree.press_key(Key::Tab, Modifiers::NONE);
2041 assert!(vis.get(), "keyboard input turns focus-visible ON");
2042
2043 tree.click(w);
2044 assert!(!vis.get(), "pointer input turns focus-visible OFF");
2045
2046 tree.press_key(Key::ArrowDown, Modifiers::NONE);
2047 assert!(vis.get(), "keyboard input turns it back ON");
2048 }
2049
2050 #[test]
2051 fn view_focus_active_without_focusable_ancestor_is_constant_true() {
2052 let mut tree = WidgetTree::new();
2055 let item = tree.add(FillWidget::new());
2056 tree.layout(SizeProposal::exact(100.0, 50.0));
2057 assert!(tree.view_focus_active_for(item).get());
2058 }
2059
2060 #[test]
2061 fn focus_within_diff_across_siblings() {
2062 use crate::signal::Signal;
2065 use crate::test_widgets::StackWidget;
2066 use crate::widget_builder::WidgetBuilder;
2067
2068 let sig_a = Signal::new(false);
2069 let sig_b = Signal::new(false);
2070
2071 let mut tree = WidgetTree::new();
2072 let leaf_a = tree.add(FillWidget::new().focusable());
2073 let leaf_b = tree.add(FillWidget::new().focusable());
2074 let mid_a = tree.add(StackWidget::new().child(leaf_a).focus_within(sig_a.clone()));
2075 let mid_b = tree.add(StackWidget::new().child(leaf_b).focus_within(sig_b.clone()));
2076 let _root = tree.add(StackWidget::new().child(mid_a).child(mid_b));
2077
2078 tree.layout(SizeProposal::exact(100.0, 50.0));
2079 tree.focus(leaf_a);
2080 assert!(sig_a.get(), "sig_a true after focusing leaf_a");
2081 assert!(!sig_b.get(), "sig_b false: leaf_a is not its descendant");
2082
2083 tree.focus(leaf_b);
2084 assert!(!sig_a.get(), "sig_a flipped to false on focus move");
2085 assert!(sig_b.get(), "sig_b flipped to true on focus move");
2086 }
2087
2088 #[test]
2089 fn focus_within_clears_when_focused_widget_destroyed() {
2090 use crate::signal::Signal;
2091 use crate::test_widgets::StackWidget;
2092 use crate::widget_builder::WidgetBuilder;
2093
2094 let halo = Signal::new(false);
2095
2096 let mut tree = WidgetTree::new();
2097 let leaf = tree.add(FillWidget::new().focusable());
2098 let _outer = tree.add(StackWidget::new().child(leaf).focus_within(halo.clone()));
2099
2100 tree.layout(SizeProposal::exact(100.0, 50.0));
2101 tree.focus(leaf);
2102 assert!(halo.get());
2103
2104 tree.destroy_subtree(leaf);
2105 assert!(
2106 !halo.get(),
2107 "destroying the focused widget must clear focus_within on its ancestors"
2108 );
2109 }
2110
2111 #[test]
2112 fn hover_within_flips_via_pointer_move() {
2113 use crate::signal::Signal;
2114 use crate::test_widgets::StackWidget;
2115 use crate::widget_builder::WidgetBuilder;
2116 use teksilo_canvas::Point;
2117
2118 let glow = Signal::new(false);
2119
2120 let mut tree = WidgetTree::new();
2121 let leaf = tree.add(FillWidget::new());
2122 let _outer = tree.add(StackWidget::new().child(leaf).hover_within(glow.clone()));
2123
2124 tree.layout(SizeProposal::exact(100.0, 50.0));
2125 assert!(!glow.get());
2126
2127 tree.pointer_move(Point::new(50.0, 25.0));
2128 assert!(glow.get(), "pointer over leaf → outer.hover_within = true");
2129
2130 tree.pointer_move(Point::new(500.0, 500.0));
2131 assert!(
2132 !glow.get(),
2133 "pointer moves outside the tree → hover_within clears"
2134 );
2135 }
2136
2137 #[test]
2138 fn hover_within_strict_ancestors_only() {
2139 use crate::signal::Signal;
2140 use crate::widget_builder::WidgetBuilder;
2141 use teksilo_canvas::Point;
2142
2143 let glow = Signal::new(false);
2144
2145 let mut tree = WidgetTree::new();
2146 let _w = tree.add(FillWidget::new().hover_within(glow.clone()));
2147 tree.layout(SizeProposal::exact(100.0, 50.0));
2148 tree.pointer_move(Point::new(50.0, 25.0));
2149
2150 assert!(
2151 !glow.get(),
2152 "hovering the widget itself must not set its own hover_within"
2153 );
2154 }
2155}
2156
2157#[cfg(test)]
2158mod tests_scope {
2159 use super::*;
2164 use crate::focus::TraversalScopePolicy::{Continue, Cycle};
2165 use crate::test_widgets::{FillWidget, StackWidget};
2166 use crate::widget_builder::WidgetBuilder;
2167
2168 fn indexed(tree: &mut WidgetTree, idx: i32) -> WidgetId {
2170 tree.add(FillWidget::new().focusable().tab_index(idx))
2171 }
2172
2173 fn tab(tree: &mut WidgetTree) {
2174 tree.press_key(Key::Tab, Modifiers::NONE);
2175 }
2176 fn shift_tab(tree: &mut WidgetTree) {
2177 tree.press_key(Key::Tab, Modifiers::SHIFT);
2178 }
2179
2180 #[test]
2181 fn overlapping_tab_index_in_sibling_scopes_does_not_interleave() {
2182 let mut tree = WidgetTree::new();
2183 let a1 = indexed(&mut tree, 1);
2184 let a2 = indexed(&mut tree, 2);
2185 let scope_a = tree.add(StackWidget::new().child(a1).child(a2));
2186 let b1 = indexed(&mut tree, 1);
2187 let b2 = indexed(&mut tree, 2);
2188 let scope_b = tree.add(StackWidget::new().child(b1).child(b2));
2189 let _root = tree.add(StackWidget::new().child(scope_a).child(scope_b));
2190 tree.set_traversal_scope(scope_a, Continue);
2191 tree.set_traversal_scope(scope_b, Continue);
2192 tree.layout(SizeProposal::exact(100.0, 100.0));
2193
2194 for expected in [a1, a2, b1, b2, a1] {
2196 tab(&mut tree);
2197 assert_eq!(tree.focused(), Some(expected));
2198 }
2199 }
2200
2201 #[test]
2202 fn continue_scope_flows_out_at_the_ends() {
2203 let mut tree = WidgetTree::new();
2205 let a = tree.add(FillWidget::new().focusable());
2206 let c1 = tree.add(FillWidget::new().focusable());
2207 let c2 = tree.add(FillWidget::new().focusable());
2208 let scope_c = tree.add(StackWidget::new().child(c1).child(c2));
2209 let b = tree.add(FillWidget::new().focusable());
2210 let _root = tree.add(StackWidget::new().child(a).child(scope_c).child(b));
2211 tree.set_traversal_scope(scope_c, Continue);
2212 tree.layout(SizeProposal::exact(100.0, 100.0));
2213
2214 for expected in [a, c1, c2, b, a] {
2215 tab(&mut tree);
2216 assert_eq!(tree.focused(), Some(expected));
2217 }
2218 tree.focus(c1);
2220 shift_tab(&mut tree);
2221 assert_eq!(tree.focused(), Some(a));
2222 }
2223
2224 #[test]
2225 fn cycle_scope_wraps_and_never_escapes() {
2226 let mut tree = WidgetTree::new();
2228 let a = tree.add(FillWidget::new().focusable());
2229 let d1 = tree.add(FillWidget::new().focusable());
2230 let d2 = tree.add(FillWidget::new().focusable());
2231 let scope_d = tree.add(StackWidget::new().child(d1).child(d2));
2232 let _root = tree.add(StackWidget::new().child(a).child(scope_d));
2233 tree.set_traversal_scope(scope_d, Cycle);
2234 tree.layout(SizeProposal::exact(100.0, 100.0));
2235
2236 tree.focus(d1);
2237 for _ in 0..10 {
2238 tab(&mut tree);
2239 let f = tree.focused();
2240 assert!(
2241 f == Some(d1) || f == Some(d2),
2242 "Cycle scope must trap Tab inside {{d1,d2}}, got {f:?}"
2243 );
2244 }
2245 tree.focus(d1);
2247 tab(&mut tree);
2248 assert_eq!(tree.focused(), Some(d2));
2249 shift_tab(&mut tree);
2250 assert_eq!(tree.focused(), Some(d1));
2251 shift_tab(&mut tree);
2252 assert_eq!(tree.focused(), Some(d2), "Shift+Tab at start wraps to last");
2253 }
2254
2255 #[test]
2256 fn empty_scope_is_skipped() {
2257 let mut tree = WidgetTree::new();
2259 let a = tree.add(FillWidget::new().focusable());
2260 let empty = tree.add(StackWidget::new());
2261 let b = tree.add(FillWidget::new().focusable());
2262 let _root = tree.add(StackWidget::new().child(a).child(empty).child(b));
2263 tree.set_traversal_scope(empty, Continue);
2264 tree.layout(SizeProposal::exact(100.0, 100.0));
2265
2266 for expected in [a, b, a] {
2267 tab(&mut tree);
2268 assert_eq!(tree.focused(), Some(expected));
2269 }
2270 }
2271
2272 #[test]
2273 fn single_member_cycle_scope_stays_put() {
2274 let mut tree = WidgetTree::new();
2275 let e = tree.add(FillWidget::new().focusable());
2276 let scope_e = tree.add(StackWidget::new().child(e));
2277 tree.set_traversal_scope(scope_e, Cycle);
2278 tree.layout(SizeProposal::exact(100.0, 50.0));
2279
2280 tree.focus(e);
2281 tab(&mut tree);
2282 assert_eq!(tree.focused(), Some(e));
2283 shift_tab(&mut tree);
2284 assert_eq!(tree.focused(), Some(e));
2285 }
2286
2287 #[test]
2288 fn nested_continue_in_cycle_flows_out_to_outer() {
2289 let mut tree = WidgetTree::new();
2291 let x = tree.add(FillWidget::new().focusable());
2292 let i1 = tree.add(FillWidget::new().focusable());
2293 let i2 = tree.add(FillWidget::new().focusable());
2294 let inner = tree.add(StackWidget::new().child(i1).child(i2));
2295 let y = tree.add(FillWidget::new().focusable());
2296 let outer = tree.add(StackWidget::new().child(x).child(inner).child(y));
2297 tree.set_traversal_scope(inner, Continue);
2298 tree.set_traversal_scope(outer, Cycle);
2299 tree.layout(SizeProposal::exact(100.0, 100.0));
2300
2301 for expected in [x, i1, i2, y, x] {
2302 tab(&mut tree);
2303 assert_eq!(tree.focused(), Some(expected));
2304 }
2305 tree.focus(i1);
2307 shift_tab(&mut tree);
2308 assert_eq!(tree.focused(), Some(x));
2309 }
2310
2311 #[test]
2312 fn nested_cycle_in_cycle_traps_in_the_inner_scope() {
2313 let mut tree = WidgetTree::new();
2316 let x = tree.add(FillWidget::new().focusable());
2317 let i1 = tree.add(FillWidget::new().focusable());
2318 let i2 = tree.add(FillWidget::new().focusable());
2319 let inner = tree.add(StackWidget::new().child(i1).child(i2));
2320 let y = tree.add(FillWidget::new().focusable());
2321 let outer = tree.add(StackWidget::new().child(x).child(inner).child(y));
2322 tree.set_traversal_scope(inner, Cycle);
2323 tree.set_traversal_scope(outer, Cycle);
2324 tree.layout(SizeProposal::exact(100.0, 100.0));
2325
2326 tree.focus(i1);
2327 for _ in 0..6 {
2328 tab(&mut tree);
2329 let f = tree.focused();
2330 assert!(
2331 f == Some(i1) || f == Some(i2),
2332 "inner Cycle must trap Tab; reached {f:?}"
2333 );
2334 }
2335 }
2336
2337 #[test]
2338 fn scoped_tab_index_orders_within_a_scope() {
2339 let mut tree = WidgetTree::new();
2341 let f3 = indexed(&mut tree, 3);
2342 let f1 = indexed(&mut tree, 1);
2343 let f2 = indexed(&mut tree, 2);
2344 let scope_f = tree.add(StackWidget::new().child(f3).child(f1).child(f2));
2345 tree.set_traversal_scope(scope_f, Cycle);
2346 tree.layout(SizeProposal::exact(100.0, 100.0));
2347
2348 for expected in [f1, f2, f3, f1] {
2349 tab(&mut tree);
2350 assert_eq!(tree.focused(), Some(expected));
2351 }
2352 }
2353
2354 #[test]
2355 fn destroyed_focused_widget_re_enters_at_first() {
2356 let mut tree = WidgetTree::new();
2357 let a = tree.add(FillWidget::new().focusable());
2358 let b = tree.add(FillWidget::new().focusable());
2359 tree.layout(SizeProposal::exact(100.0, 50.0));
2360
2361 tree.focus(b);
2362 tree.destroy_subtree(b);
2363 tab(&mut tree);
2364 assert_eq!(tree.focused(), Some(a), "Tab after destroy enters at first");
2365 }
2366
2367 #[test]
2368 fn set_traversal_scope_forces_node_non_focusable() {
2369 let mut tree = WidgetTree::new();
2372 let inner = tree.add(FillWidget::new().focusable());
2373 let scope = tree.add(StackWidget::new().child(inner).focusable(true));
2374 tree.set_traversal_scope(scope, Continue);
2375 tree.layout(SizeProposal::exact(100.0, 50.0));
2376
2377 tab(&mut tree);
2379 assert_eq!(tree.focused(), Some(inner));
2380 tab(&mut tree);
2381 assert_eq!(tree.focused(), Some(inner));
2382 }
2383
2384 #[test]
2385 fn no_scopes_behaves_like_a_flat_cycle() {
2386 let mut tree = WidgetTree::new();
2389 let a = indexed(&mut tree, 2);
2390 let b = indexed(&mut tree, 1);
2391 let c = tree.add(FillWidget::new().focusable());
2392 tree.layout(SizeProposal::exact(100.0, 50.0));
2393
2394 for expected in [b, a, c, b] {
2396 tab(&mut tree);
2397 assert_eq!(tree.focused(), Some(expected));
2398 }
2399 }
2400
2401 #[test]
2402 fn centered_modal_confines_tab_to_its_content() {
2403 let mut tree = WidgetTree::new();
2406 let outside1 = tree.add(FillWidget::new().focusable());
2407 let outside2 = tree.add(FillWidget::new().focusable());
2408 let m1 = tree.add(FillWidget::new().focusable());
2409 let m2 = tree.add(FillWidget::new().focusable());
2410 let content = tree.add(StackWidget::new().child(m1).child(m2));
2411 tree.layout(SizeProposal::exact(200.0, 100.0));
2412
2413 tree.show_overlay(crate::overlay::OverlayRequest::new(
2414 content,
2415 outside1,
2416 crate::overlay::OverlayPlacement::Centered,
2417 crate::overlay::DismissBehavior::Manual,
2418 ));
2419
2420 for _ in 0..6 {
2421 tab(&mut tree);
2422 let f = tree.focused();
2423 assert!(
2424 f == Some(m1) || f == Some(m2),
2425 "modal must trap Tab inside its content, reached {f:?}"
2426 );
2427 }
2428 assert_ne!(tree.focused(), Some(outside1));
2429 assert_ne!(tree.focused(), Some(outside2));
2430 }
2431}
2432
2433#[cfg(test)]
2434mod tests_focus_out_dismissal {
2435 use super::*;
2442 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
2443 use crate::test_widgets::{FillWidget, StackWidget};
2444
2445 fn tab(tree: &mut WidgetTree) {
2446 tree.press_key(Key::Tab, Modifiers::NONE);
2447 }
2448
2449 fn show_anchored(
2451 tree: &mut WidgetTree,
2452 content: WidgetId,
2453 anchor: WidgetId,
2454 parent: Option<crate::overlay::OverlayId>,
2455 ) -> crate::overlay::OverlayId {
2456 let mut request = OverlayRequest::new(
2457 content,
2458 anchor,
2459 OverlayPlacement::Below,
2460 DismissBehavior::EscapeOrClickOutside,
2461 );
2462 request.parent_overlay = parent;
2463 tree.show_overlay(request)
2464 }
2465
2466 #[test]
2467 fn tab_out_of_a_non_modal_overlay_dismisses_it() {
2468 let mut tree = WidgetTree::new();
2469 let anchor = tree.add(FillWidget::new().focusable());
2470 let after = tree.add(FillWidget::new().focusable());
2471 let inner = tree.add(FillWidget::new().focusable());
2472 let content = tree.add(StackWidget::new().child(inner));
2473 tree.layout(SizeProposal::exact(200.0, 100.0));
2474 show_anchored(&mut tree, content, anchor, None);
2475
2476 tree.focus(inner);
2477 tab(&mut tree);
2478
2479 assert_ne!(tree.focused(), Some(inner), "focus genuinely left");
2480 assert!(
2481 tree.active_overlays().is_empty(),
2482 "an overlay must not stay open over the focus ring that left it"
2483 );
2484 assert!(tree.focused() == Some(after) || tree.focused() == Some(anchor));
2485 }
2486
2487 #[test]
2489 fn a_centered_modal_is_never_dismissed_by_focus_moving() {
2490 let mut tree = WidgetTree::new();
2491 let outside = tree.add(FillWidget::new().focusable());
2492 let m1 = tree.add(FillWidget::new().focusable());
2493 let content = tree.add(StackWidget::new().child(m1));
2494 tree.layout(SizeProposal::exact(200.0, 100.0));
2495 tree.show_overlay(OverlayRequest::new(
2496 content,
2497 outside,
2498 OverlayPlacement::Centered,
2499 DismissBehavior::Manual,
2500 ));
2501
2502 tree.focus(m1);
2503 tree.focus(outside);
2507
2508 assert_eq!(
2509 tree.active_overlays().len(),
2510 1,
2511 "a modal is the one overlay that legitimately contains focus"
2512 );
2513 }
2514
2515 #[test]
2519 fn the_modal_scrim_survives_focus_moving_inside_the_modal() {
2520 let mut tree = WidgetTree::new();
2521 let opener = tree.add(FillWidget::new().focusable());
2522 let m1 = tree.add(FillWidget::new().focusable());
2523 let m2 = tree.add(FillWidget::new().focusable());
2524 let scrim = tree.add(FillWidget::new());
2525 let content = tree.add(StackWidget::new().child(m1).child(m2));
2526 tree.layout(SizeProposal::exact(200.0, 100.0));
2527
2528 tree.show_overlay(OverlayRequest::new(
2529 scrim,
2530 opener,
2531 OverlayPlacement::FullViewport,
2532 DismissBehavior::Manual,
2533 ));
2534 tree.show_overlay(OverlayRequest::new(
2535 content,
2536 opener,
2537 OverlayPlacement::Centered,
2538 DismissBehavior::Manual,
2539 ));
2540
2541 tree.focus(opener);
2542 tree.focus(m1);
2543 tab(&mut tree);
2544
2545 assert_eq!(
2546 tree.active_overlays().len(),
2547 2,
2548 "scrim and modal both stand while focus moves within the modal"
2549 );
2550 }
2551
2552 #[test]
2555 fn focus_moving_into_a_child_overlay_keeps_the_parent_open() {
2556 let mut tree = WidgetTree::new();
2557 let anchor = tree.add(FillWidget::new().focusable());
2558 let parent_item = tree.add(FillWidget::new().focusable());
2559 let parent_content = tree.add(StackWidget::new().child(parent_item));
2560 let child_item = tree.add(FillWidget::new().focusable());
2561 let child_content = tree.add(StackWidget::new().child(child_item));
2562 tree.layout(SizeProposal::exact(200.0, 100.0));
2563
2564 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2565 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2566
2567 tree.focus(parent_item);
2568 tree.focus(child_item);
2569
2570 assert_eq!(
2571 tree.active_overlays().len(),
2572 2,
2573 "opening a submenu is not leaving the menu that owns it"
2574 );
2575 }
2576
2577 #[test]
2580 fn focus_back_to_the_parent_overlay_closes_only_the_child() {
2581 let mut tree = WidgetTree::new();
2582 let anchor = tree.add(FillWidget::new().focusable());
2583 let parent_item = tree.add(FillWidget::new().focusable());
2584 let parent_content = tree.add(StackWidget::new().child(parent_item));
2585 let child_item = tree.add(FillWidget::new().focusable());
2586 let child_content = tree.add(StackWidget::new().child(child_item));
2587 tree.layout(SizeProposal::exact(200.0, 100.0));
2588
2589 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2590 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2591
2592 tree.focus(child_item);
2593 tree.focus(parent_item);
2594
2595 assert_eq!(
2596 tree.active_overlays(),
2597 vec![parent],
2598 "the submenu goes, the menu that owns it stays"
2599 );
2600 }
2601
2602 #[test]
2605 fn leaving_a_nested_cascade_closes_every_level() {
2606 let mut tree = WidgetTree::new();
2607 let anchor = tree.add(FillWidget::new().focusable());
2608 let away = tree.add(FillWidget::new().focusable());
2609 let parent_item = tree.add(FillWidget::new().focusable());
2610 let parent_content = tree.add(StackWidget::new().child(parent_item));
2611 let child_item = tree.add(FillWidget::new().focusable());
2612 let child_content = tree.add(StackWidget::new().child(child_item));
2613 tree.layout(SizeProposal::exact(200.0, 100.0));
2614
2615 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2616 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2617
2618 tree.focus(child_item);
2619 tree.focus(away);
2620
2621 assert!(
2622 tree.active_overlays().is_empty(),
2623 "one move out of the cascade must leave nothing behind"
2624 );
2625 }
2626
2627 #[test]
2637 fn a_dropdown_inside_a_modal_still_follows_focus_out() {
2638 let mut tree = WidgetTree::new();
2639 let opener = tree.add(FillWidget::new().focusable());
2640 let trigger = tree.add(FillWidget::new().focusable());
2641 let next = tree.add(FillWidget::new().focusable());
2642 let modal_content = tree.add(StackWidget::new().child(trigger).child(next));
2643 let panel = tree.add(StackWidget::new());
2644 tree.layout(SizeProposal::exact(200.0, 100.0));
2645
2646 tree.show_overlay(OverlayRequest::new(
2647 modal_content,
2648 opener,
2649 OverlayPlacement::Centered,
2650 DismissBehavior::Manual,
2651 ));
2652 show_anchored(&mut tree, panel, trigger, None);
2654
2655 tree.focus(trigger);
2656 assert_eq!(
2657 tree.active_overlays().len(),
2658 2,
2659 "precondition: modal + panel"
2660 );
2661
2662 tab(&mut tree);
2663
2664 assert_eq!(
2665 tree.focused(),
2666 Some(next),
2667 "Tab moves on within the modal, as it should"
2668 );
2669 assert_eq!(
2670 tree.active_overlays().len(),
2671 1,
2672 "the dropdown must go — only the modal hosting it stays"
2673 );
2674 }
2675
2676 #[test]
2684 fn a_viewport_placed_notification_ignores_focus_moving() {
2685 let mut tree = WidgetTree::new();
2686 let trigger = tree.add(FillWidget::new().focusable());
2687 let elsewhere = tree.add(FillWidget::new().focusable());
2688 let snack = tree.add(StackWidget::new());
2689 tree.layout(SizeProposal::exact(200.0, 100.0));
2690 tree.show_overlay(OverlayRequest::new(
2691 snack,
2692 trigger,
2693 OverlayPlacement::BottomCenter,
2694 DismissBehavior::Manual,
2695 ));
2696
2697 tree.focus(trigger);
2698 tab(&mut tree);
2699
2700 assert_eq!(
2701 tree.active_overlays().len(),
2702 1,
2703 "a snackbar outlives the keystroke that moved focus off its trigger"
2704 );
2705 assert_ne!(tree.focused(), Some(trigger), "and focus did move");
2706 assert_eq!(tree.focused(), Some(elsewhere));
2707 }
2708
2709 #[test]
2713 fn leaving_a_hosted_menu_spares_the_host() {
2714 let mut tree = WidgetTree::new();
2715 let opener = tree.add(FillWidget::new().focusable());
2716 let away = tree.add(FillWidget::new().focusable());
2717 let modal_item = tree.add(FillWidget::new().focusable());
2718 let modal_content = tree.add(StackWidget::new().child(modal_item));
2719 let menu_item = tree.add(FillWidget::new().focusable());
2720 let menu_content = tree.add(StackWidget::new().child(menu_item));
2721 tree.layout(SizeProposal::exact(200.0, 100.0));
2722
2723 let host = tree.show_overlay(OverlayRequest::new(
2725 modal_content,
2726 opener,
2727 OverlayPlacement::Centered,
2728 DismissBehavior::Manual,
2729 ));
2730 show_anchored(&mut tree, menu_content, modal_item, Some(host));
2731
2732 tree.focus(menu_item);
2733 tree.focus(away);
2734
2735 assert_eq!(
2736 tree.active_overlays(),
2737 vec![host],
2738 "the menu goes; the modal hosting it stays"
2739 );
2740 }
2741
2742 #[test]
2746 fn leaving_the_anchor_dismisses_a_panel_focus_never_entered() {
2747 let mut tree = WidgetTree::new();
2748 let trigger = tree.add(FillWidget::new().focusable());
2749 let after = tree.add(FillWidget::new().focusable());
2750 let content = tree.add(StackWidget::new());
2751 tree.layout(SizeProposal::exact(200.0, 100.0));
2752 show_anchored(&mut tree, content, trigger, None);
2753
2754 tree.focus(trigger);
2755 assert_eq!(tree.active_overlays().len(), 1, "precondition: panel is up");
2756
2757 tree.focus(after);
2758 assert!(
2759 tree.active_overlays().is_empty(),
2760 "leaving the trigger is leaving the dropdown it owns"
2761 );
2762 }
2763}