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 if self.focused == Some(id) {
32 return;
33 }
34 struct ClearOnDrop(std::rc::Rc<std::cell::Cell<bool>>);
40 impl Drop for ClearOnDrop {
41 fn drop(&mut self) {
42 self.0.set(false);
43 }
44 }
45 self.in_focus_dispatch.set(true);
46 let _focus_dispatch_guard = ClearOnDrop(self.in_focus_dispatch.clone());
47
48 let previously_focused = self.focused;
49 if let Some(old) = self.focused {
50 let old_overlay = self.overlay_ancestor_for_widget(old);
51 let new_overlay = self.overlay_ancestor_for_widget(id);
52 let moving_into_descendant_overlay = match (old_overlay, new_overlay) {
53 (Some(old_overlay), Some(new_overlay)) => self
54 .overlay_manager
55 .is_descendant_of(new_overlay, old_overlay),
56 _ => false,
57 };
58
59 if moving_into_descendant_overlay {
60 self.dispatch_to_widget_direct(old, &WidgetEvent::FocusLost, &mut *ops);
61 } else {
62 self.dispatch_to_widget(old, &WidgetEvent::FocusLost, &mut *ops);
63 }
64 }
65 self.set_focused(Some(id));
66 self.focus_origin = Some(origin);
67 if let Some(visible) = origin.focus_visible()
76 && self.focus_visible.get() != visible
77 {
78 self.focus_visible.set(visible);
79 }
80 self.a11y_dirty = true;
81 self.update_focus_within_signals(previously_focused, Some(id));
82 self.update_view_focus_signals(previously_focused, Some(id));
83 self.dispatch_to_widget(id, &WidgetEvent::FocusGained { origin }, &mut *ops);
84 self.dismiss_overlays_left_by_focus(previously_focused, id, &mut *ops);
94 self.tooltip_focus_leave_outside(Some(id), &mut *ops);
101 self.tooltip_focus_enter(id);
102 if !origin.is_pointer() {
109 self.scroll_focused_into_view(id, &mut *ops);
110 }
111 }
112
113 pub fn focus_with_origin(&mut self, id: WidgetId, origin: crate::focus::FocusOrigin) {
117 let mut noop = crate::window::NoopWindowOps;
118 self.focus_with_origin_ops(id, origin, &mut noop);
119 }
120
121 fn scroll_focused_into_view(
124 &mut self,
125 focused_id: WidgetId,
126 ops: &mut dyn crate::window::WindowOps,
127 ) {
128 let focused_bounds = self.arena.bounds(focused_id);
129 let reveal = self
134 .arena
135 .get(focused_id)
136 .and_then(|node| node.widget.focus_reveal_rect(focused_bounds))
137 .unwrap_or(focused_bounds);
138 self.scroll_rect_into_view(
139 focused_id,
140 reveal,
141 0.0,
142 crate::event::ScrollAlign::Minimal,
143 crate::event::ScrollMotion::Instant,
144 &mut *ops,
145 );
146 }
147
148 pub(super) fn scroll_rect_into_view(
208 &mut self,
209 from: WidgetId,
210 rect: Rect,
211 margin: f32,
212 align: crate::event::ScrollAlign,
213 motion: crate::event::ScrollMotion,
214 ops: &mut dyn crate::window::WindowOps,
215 ) {
216 let applied = std::sync::Arc::new(std::sync::Mutex::new(Point::ZERO));
222 let mut rect = rect;
223 let mut current = self.arena.parent(from);
224 let mut pending_align = align;
227 while let Some(ancestor_id) = current {
228 let to_parent = self
243 .arena
244 .get(ancestor_id)
245 .and_then(|n| n.transform_prop.as_ref())
246 .map(|t| t.get())
247 .filter(|t| !t.is_identity());
248 let content_transform = self
249 .arena
250 .get(ancestor_id)
251 .is_some_and(|n| n.content_transform);
252
253 if let Some(node) = self.arena.get(ancestor_id)
260 && node.clips_children
261 && (node.handlers.on_scroll.is_some() || node.external_handlers.on_scroll.is_some())
262 {
263 let viewport = node.bounds;
264 let against = match (content_transform, to_parent.as_ref()) {
271 (true, Some(t)) => t.apply_rect(rect),
272 _ => rect,
273 };
274 let align =
275 std::mem::replace(&mut pending_align, crate::event::ScrollAlign::Minimal);
276 let needs_scroll = matches!(align, crate::event::ScrollAlign::Fraction(_))
279 || against.y - margin < viewport.y
280 || against.bottom() + margin > viewport.bottom()
281 || against.x - margin < viewport.x
282 || against.right() + margin > viewport.right();
283
284 if needs_scroll {
285 *applied.lock().unwrap() = Point::ZERO;
286 self.dispatch_to_widget_direct(
300 ancestor_id,
301 &WidgetEvent::ScrollIntoView {
302 target_bounds: rect,
308 margin,
309 align,
310 motion,
311 applied_scroll: Some(applied.clone()),
312 },
313 &mut *ops,
314 );
315 let delta = *applied.lock().unwrap();
319 if delta != Point::ZERO {
320 rect =
321 Rect::new(rect.x - delta.x, rect.y - delta.y, rect.width, rect.height);
322 }
323 }
324 }
325 if let Some(t) = to_parent {
331 rect = t.apply_rect(rect);
332 }
333 current = self.arena.parent(ancestor_id);
334 }
335 }
336
337 pub fn focus(&mut self, id: WidgetId) {
339 self.focus_with_origin(id, crate::focus::FocusOrigin::Programmatic);
340 }
341
342 pub fn focus_ops(&mut self, id: WidgetId, ops: &mut dyn crate::window::WindowOps) {
345 self.focus_with_origin_ops(id, crate::focus::FocusOrigin::Programmatic, ops);
346 }
347
348 pub fn focused(&self) -> Option<WidgetId> {
350 self.focused
351 }
352
353 pub fn ime_context_for_focused(&self) -> Option<crate::ime::ImeContext> {
359 self.focused.and_then(|id| self.arena.ime_context(id))
360 }
361
362 pub fn first_focusable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
367 if !self.arena.is_active(root) {
368 return None;
369 }
370 let mut entries = Vec::new();
371 self.collect_scope_entries(root, &mut entries);
372 sort_scope_entries(&mut entries);
373 let scope = ScopeNode {
374 policy: crate::focus::TraversalScopePolicy::Cycle,
375 entries,
376 };
377 enter_scope_edge(&scope, false)
378 }
379
380 pub(crate) fn first_focus_capable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
384 if !self.arena.is_active(root) {
385 return None;
386 }
387 let node = self.arena.get(root)?;
388 if self.is_node_focusable(node) {
389 return Some(root);
390 }
391 node.children
392 .iter()
393 .find_map(|&child| self.first_focus_capable_descendant(child))
394 }
395
396 pub fn is_active(&self, id: WidgetId) -> bool {
402 self.arena.is_active(id)
403 }
404
405 pub fn widget_initial_focus_hint(&self, id: WidgetId) -> Option<WidgetId> {
421 if !self.arena.is_active(id) {
422 return None;
423 }
424 if let Some(node) = self.arena.get(id) {
425 if let Some(target) = node.widget.initial_focus_hint()
426 && self.arena.is_active(target)
427 && self.is_descendant_of(target, id)
428 {
429 return Some(target);
430 }
431 for &child in &node.children {
432 if let Some(found) = self.widget_initial_focus_hint(child) {
433 return Some(found);
434 }
435 }
436 }
437 None
438 }
439
440 pub fn focus_origin(&self) -> Option<crate::focus::FocusOrigin> {
442 self.focus_origin
443 }
444
445 pub fn focus_visible_signal(&self) -> crate::signal::Signal<bool> {
449 self.focus_visible.clone()
450 }
451
452 pub(super) fn cycle_focus(&mut self, reverse: bool, ops: &mut dyn crate::window::WindowOps) {
461 let mut entries = Vec::new();
462 if let Some(modal_overlay) = self.overlay_manager.topmost_centered() {
463 let content_id = modal_overlay.content_id;
464 self.collect_scope_entries(content_id, &mut entries);
465 } else {
466 let roots = self.arena.roots();
467 for root in roots {
468 if self.tooltip_content_root(root).is_some() {
478 continue;
479 }
480 self.collect_scope_entries(root, &mut entries);
481 }
482 }
483 sort_scope_entries(&mut entries);
484 self.splice_sticky_tooltips_after_anchors(&mut entries);
485 let root_scope = ScopeNode {
486 policy: crate::focus::TraversalScopePolicy::Cycle,
487 entries,
488 };
489
490 if let StepResult::Found(next_id) = navigate_scope(&root_scope, self.focused, reverse, true)
491 {
492 self.focus_with_origin_ops(next_id, crate::focus::FocusOrigin::Keyboard, &mut *ops);
493 }
494 }
497
498 fn tooltip_content_root(&self, id: WidgetId) -> Option<usize> {
500 self.tooltips.iter().position(|e| e.content_id == id)
501 }
502
503 fn splice_sticky_tooltips_after_anchors(&self, entries: &mut Vec<ScopeEntry>) {
521 let sticky: Vec<(WidgetId, WidgetId)> = self
522 .tooltips
523 .iter()
524 .filter(|e| e.is_sticky && e.overlay_id.is_some())
525 .map(|e| (e.anchor_id, e.content_id))
526 .collect();
527
528 for (anchor_id, content_id) in sticky {
529 let mut panel = Vec::new();
530 self.collect_scope_entries(content_id, &mut panel);
531 if panel.is_empty() {
532 continue;
533 }
534 sort_scope_entries(&mut panel);
535
536 let stop = self
541 .find_focusable_at_or_above(anchor_id)
542 .unwrap_or(anchor_id);
543 let at = entries
544 .iter()
545 .position(|entry| scope_entry_contains(entry, stop))
546 .map_or(entries.len(), |i| i + 1);
551
552 for (offset, item) in panel.into_iter().enumerate() {
553 entries.insert(at + offset, item);
554 }
555 }
556 }
557
558 pub(super) fn tab_stop_effective(&self, id: WidgetId) -> bool {
566 let mut current = Some(id);
567 while let Some(cur) = current {
568 let Some(node) = self.arena.get(cur) else {
569 break;
570 };
571 if let Some(prop) = node.tab_stop.as_ref() {
572 return prop.get();
573 }
574 current = node.parent;
575 }
576 true
577 }
578
579 pub(super) fn is_node_focusable(&self, node: &crate::arena::WidgetNode) -> bool {
581 node.node_focusable.unwrap_or(false)
582 }
583
584 pub(super) fn find_focusable_at_or_above(&self, id: WidgetId) -> Option<WidgetId> {
586 let mut current = Some(id);
587 while let Some(current_id) = current {
588 if let Some(node) = self.arena.get(current_id)
589 && self.is_node_focusable(node)
590 {
591 return Some(current_id);
592 }
593 current = self.arena.parent(current_id);
594 }
595 None
596 }
597
598 fn collect_scope_entries(&self, id: WidgetId, out: &mut Vec<ScopeEntry>) {
614 if !self.arena.is_active(id) {
615 return;
616 }
617 let Some(node) = self.arena.get(id) else {
618 return;
619 };
620 if node
621 .enabled_state
622 .as_ref()
623 .map(|s| !s.get())
624 .unwrap_or(false)
625 {
626 return;
627 }
628
629 if let Some(policy) = node.node_traversal_scope {
632 let mut child_entries = Vec::new();
633 for &child in &node.children {
634 self.collect_scope_entries(child, &mut child_entries);
635 }
636 sort_scope_entries(&mut child_entries);
637 out.push(ScopeEntry {
638 tab_index: node.node_tab_index,
639 kind: ScopeEntryKind::Scope(ScopeNode {
640 policy,
641 entries: child_entries,
642 }),
643 });
644 return;
645 }
646
647 if self.is_node_focusable(node) && self.tab_stop_effective(id) {
649 out.push(ScopeEntry {
650 tab_index: node.node_tab_index,
651 kind: ScopeEntryKind::Focusable(id),
652 });
653 }
654 for &child in &node.children {
655 self.collect_scope_entries(child, out);
656 }
657 }
658
659 pub(super) fn strict_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
664 let mut chain = Vec::new();
665 if let Some(start) = id {
666 let mut current = self.arena.parent(start);
667 while let Some(parent) = current {
668 chain.push(parent);
669 current = self.arena.parent(parent);
670 }
671 }
672 chain
673 }
674
675 pub(crate) fn update_focus_within_signals(
680 &mut self,
681 old: Option<WidgetId>,
682 new: Option<WidgetId>,
683 ) {
684 let old_chain = self.strict_ancestors_of(old);
685 let new_chain = self.strict_ancestors_of(new);
686 for &id in &old_chain {
688 if !new_chain.contains(&id)
689 && let Some(node) = self.arena.get(id)
690 && let Some(sig) = node.focus_within_signal.clone()
691 {
692 sig.set(false);
693 }
694 }
695 for &id in &new_chain {
697 if !old_chain.contains(&id)
698 && let Some(node) = self.arena.get(id)
699 && let Some(sig) = node.focus_within_signal.clone()
700 {
701 sig.set(true);
702 }
703 }
704 }
705
706 pub(super) fn inclusive_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
709 let mut chain = Vec::new();
710 if let Some(start) = id {
711 chain.push(start);
712 chain.extend(self.strict_ancestors_of(Some(start)));
713 }
714 chain
715 }
716
717 pub(crate) fn update_view_focus_signals(
722 &mut self,
723 old: Option<WidgetId>,
724 new: Option<WidgetId>,
725 ) {
726 let old_chain = self.inclusive_ancestors_of(old);
727 let new_chain = self.inclusive_ancestors_of(new);
728 for &id in &old_chain {
729 if !new_chain.contains(&id)
730 && let Some(node) = self.arena.get(id)
731 && let Some(sig) = node.view_focus_signal.clone()
732 {
733 sig.set(false);
734 }
735 }
736 for &id in &new_chain {
737 if !old_chain.contains(&id)
738 && let Some(node) = self.arena.get(id)
739 && let Some(sig) = node.view_focus_signal.clone()
740 {
741 sig.set(true);
742 }
743 }
744 }
745
746 pub(crate) fn view_focus_signal_for(
749 &mut self,
750 node_id: WidgetId,
751 ) -> crate::signal::Signal<bool> {
752 if let Some(node) = self.arena.get(node_id)
753 && let Some(sig) = node.view_focus_signal.clone()
754 {
755 return sig;
756 }
757 let active = self.inclusive_ancestors_of(self.focused).contains(&node_id);
758 let sig = crate::signal::Signal::new(active);
759 if let Some(node) = self.arena.get_mut(node_id) {
760 node.view_focus_signal = Some(sig.clone());
761 }
762 sig
763 }
764
765 pub fn view_focus_active_for(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
771 match self.find_focusable_at_or_above(node_id) {
772 Some(scope) => self.view_focus_signal_for(scope),
773 None => crate::signal::Signal::new(true),
774 }
775 }
776
777 pub fn begin_view_focus(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
782 let sig = self.view_focus_signal_for(node_id);
783 self.view_focus_stack.push(sig.clone());
784 sig
785 }
786
787 pub fn end_view_focus(&mut self) {
789 self.view_focus_stack.pop();
790 }
791
792 pub fn current_view_focus(&self) -> Option<crate::signal::Signal<bool>> {
794 self.view_focus_stack.last().cloned()
795 }
796
797 pub(crate) fn set_hovered(&mut self, value: Option<WidgetId>) {
811 if let Some(entry) = self.pointers.hover_owner_mut() {
812 entry.hovered = value;
813 }
814 if self.hovered_signal.get() != value {
815 self.hovered_signal.set(value);
816 }
817 }
818
819 pub(crate) fn set_focused(&mut self, value: Option<WidgetId>) {
826 self.focused = value;
827 if self.focused_signal.get() != value {
828 self.focused_signal.set(value);
829 }
830 }
831
832 pub(crate) fn update_hover_within_signals(
835 &mut self,
836 old: Option<WidgetId>,
837 new: Option<WidgetId>,
838 ) {
839 let old_chain = self.strict_ancestors_of(old);
840 let new_chain = self.strict_ancestors_of(new);
841 if let Some(entry) = self.pointers.hover_owner_mut() {
845 entry.hover_within = new_chain.clone();
846 }
847 for &id in &old_chain {
848 if !new_chain.contains(&id)
849 && let Some(node) = self.arena.get(id)
850 && let Some(sig) = node.hover_within_signal.clone()
851 {
852 sig.set(false);
853 }
854 }
855 for &id in &new_chain {
856 if !old_chain.contains(&id)
857 && let Some(node) = self.arena.get(id)
858 && let Some(sig) = node.hover_within_signal.clone()
859 {
860 sig.set(true);
861 }
862 }
863 }
864}
865
866struct ScopeEntry {
878 tab_index: Option<i32>,
879 kind: ScopeEntryKind,
880}
881
882enum ScopeEntryKind {
883 Focusable(WidgetId),
884 Scope(ScopeNode),
885}
886
887struct ScopeNode {
889 policy: crate::focus::TraversalScopePolicy,
890 entries: Vec<ScopeEntry>,
891}
892
893enum StepResult {
895 Found(WidgetId),
897 Escaped,
901}
902
903fn scope_wraps(policy: crate::focus::TraversalScopePolicy, is_root: bool) -> bool {
906 is_root || matches!(policy, crate::focus::TraversalScopePolicy::Cycle)
907}
908
909fn sort_scope_entries(entries: &mut [ScopeEntry]) {
912 entries.sort_by(|a, b| match (a.tab_index, b.tab_index) {
913 (Some(ia), Some(ib)) => ia.cmp(&ib),
914 (Some(_), None) => std::cmp::Ordering::Less,
915 (None, Some(_)) => std::cmp::Ordering::Greater,
916 (None, None) => std::cmp::Ordering::Equal,
917 });
918}
919
920fn scope_entry_contains(entry: &ScopeEntry, f: WidgetId) -> bool {
922 match &entry.kind {
923 ScopeEntryKind::Focusable(id) => *id == f,
924 ScopeEntryKind::Scope(child) => child.entries.iter().any(|e| scope_entry_contains(e, f)),
925 }
926}
927
928fn enter_scope_edge(scope: &ScopeNode, reverse: bool) -> Option<WidgetId> {
932 let n = scope.entries.len();
933 for k in 0..n {
934 let idx = if reverse { n - 1 - k } else { k };
935 match &scope.entries[idx].kind {
936 ScopeEntryKind::Focusable(id) => return Some(*id),
937 ScopeEntryKind::Scope(child) => {
938 if let Some(id) = enter_scope_edge(child, reverse) {
939 return Some(id);
940 }
941 }
942 }
943 }
944 None
945}
946
947fn navigate_scope(
951 scope: &ScopeNode,
952 focused: Option<WidgetId>,
953 reverse: bool,
954 is_root: bool,
955) -> StepResult {
956 if scope.entries.is_empty() {
957 return StepResult::Escaped;
958 }
959
960 let cur = focused.and_then(|f| {
961 scope
962 .entries
963 .iter()
964 .position(|e| scope_entry_contains(e, f))
965 });
966
967 let from = match cur {
968 None => {
969 return match enter_scope_edge(scope, reverse) {
972 Some(id) => StepResult::Found(id),
973 None => StepResult::Escaped,
974 };
975 }
976 Some(i) => i,
977 };
978
979 if let ScopeEntryKind::Scope(child) = &scope.entries[from].kind
982 && let StepResult::Found(id) = navigate_scope(child, focused, reverse, false)
983 {
984 return StepResult::Found(id);
985 }
986
987 step_to_sibling(scope, from, reverse, is_root)
988}
989
990fn step_to_sibling(scope: &ScopeNode, from: usize, reverse: bool, is_root: bool) -> StepResult {
994 let n = scope.entries.len();
995 let mut idx = from;
996 for _ in 0..n {
997 let next = if reverse {
998 if idx == 0 {
999 if scope_wraps(scope.policy, is_root) {
1000 n - 1
1001 } else {
1002 return StepResult::Escaped;
1003 }
1004 } else {
1005 idx - 1
1006 }
1007 } else if idx + 1 >= n {
1008 if scope_wraps(scope.policy, is_root) {
1009 0
1010 } else {
1011 return StepResult::Escaped;
1012 }
1013 } else {
1014 idx + 1
1015 };
1016
1017 match &scope.entries[next].kind {
1018 ScopeEntryKind::Focusable(id) => return StepResult::Found(*id),
1019 ScopeEntryKind::Scope(child) => {
1020 if let Some(id) = enter_scope_edge(child, reverse) {
1021 return StepResult::Found(id);
1022 }
1023 idx = next;
1025 }
1026 }
1027 }
1028 StepResult::Escaped
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::*;
1034 use crate::test_widgets::FillWidget;
1035 use crate::widget_builder::WidgetBuilder;
1036
1037 #[test]
1046 fn a_reveal_walks_the_ancestors_of_whoever_owns_the_rect() {
1047 use crate::event::{EventResponse, ScrollAlign, ScrollMotion};
1048 use std::cell::Cell;
1049 use std::rc::Rc;
1050
1051 let asked = Rc::new(Cell::new(0usize));
1052 let mut tree = WidgetTree::new();
1053 let viewport = {
1055 let asked = asked.clone();
1056 tree.add(
1057 FillWidget::new()
1060 .on_scroll(move |event, _ctx| {
1061 if matches!(event, WidgetEvent::ScrollIntoView { .. }) {
1062 asked.set(asked.get() + 1);
1063 }
1064 EventResponse::Ignored
1065 })
1066 .clips_children(true),
1067 )
1068 };
1069 let inside = tree.add_child(viewport, FillWidget::new());
1070 let button = tree.add(FillWidget::new());
1072 tree.layout(SizeProposal::exact(400.0, 300.0));
1073
1074 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
1076 let mut ops = crate::window::NoopWindowOps;
1077
1078 tree.scroll_rect_into_view(
1079 button,
1080 target,
1081 0.0,
1082 ScrollAlign::Minimal,
1083 ScrollMotion::Instant,
1084 &mut ops,
1085 );
1086 assert_eq!(
1087 asked.get(),
1088 0,
1089 "walking from the asker never reaches a viewport it is not inside"
1090 );
1091
1092 tree.scroll_rect_into_view(
1093 inside,
1094 target,
1095 0.0,
1096 ScrollAlign::Minimal,
1097 ScrollMotion::Instant,
1098 &mut ops,
1099 );
1100 assert_eq!(
1101 asked.get(),
1102 1,
1103 "walking from the rect's owner reaches the viewport enclosing it"
1104 );
1105 }
1106
1107 fn recorder(
1110 tree: &mut WidgetTree,
1111 child: WidgetId,
1112 seen: std::rc::Rc<std::cell::RefCell<Vec<Rect>>>,
1113 ) -> WidgetId {
1114 use crate::event::EventResponse;
1115 use crate::test_widgets::StackWidget;
1116 tree.add(
1117 StackWidget::new()
1118 .child(child)
1119 .on_scroll(move |event, _ctx| {
1120 if let WidgetEvent::ScrollIntoView { target_bounds, .. } = event {
1121 seen.borrow_mut().push(*target_bounds);
1122 }
1123 EventResponse::Ignored
1124 })
1125 .clips_children(true),
1126 )
1127 }
1128
1129 #[test]
1140 fn a_reveal_projects_the_rect_as_it_climbs_out_of_a_content_transform() {
1141 use crate::event::{ScrollAlign, ScrollMotion};
1142 use std::cell::RefCell;
1143 use std::rc::Rc;
1144 use teksilo_canvas::Transform2D;
1145
1146 let outer_seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1147 let view_seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1148
1149 let mut tree = WidgetTree::new();
1150 let card = tree.add(FillWidget::new());
1151 let view = recorder(&mut tree, card, view_seen.clone());
1152 let _outer = recorder(&mut tree, view, outer_seen.clone());
1153 tree.layout(SizeProposal::exact(400.0, 300.0));
1154 tree.set_content_transform(view, Transform2D::translate(-90.0, 0.0));
1157
1158 let card_rect = Rect::new(100.0, 100.0, 50.0, 50.0);
1159 let mut ops = crate::window::NoopWindowOps;
1160 tree.scroll_rect_into_view(
1161 card,
1162 card_rect,
1163 0.0,
1164 ScrollAlign::Minimal,
1165 ScrollMotion::Instant,
1166 &mut ops,
1167 );
1168
1169 assert!(
1170 view_seen.borrow().is_empty(),
1171 "the card paints at window x = 10, inside the 400×300 viewport — \
1172 comparing its scene rect against the viewport is what used to \
1173 produce a spurious verdict here, in either direction"
1174 );
1175 assert!(
1176 outer_seen.borrow().is_empty(),
1177 "and nothing outside it has a reason to scroll either"
1178 );
1179
1180 tree.set_content_transform(view, Transform2D::translate(-600.0, 0.0));
1184 tree.scroll_rect_into_view(
1185 card,
1186 card_rect,
1187 0.0,
1188 ScrollAlign::Minimal,
1189 ScrollMotion::Instant,
1190 &mut ops,
1191 );
1192 assert_eq!(
1193 view_seen.borrow().as_slice(),
1194 &[card_rect],
1195 "the view is asked to reveal the card, in the view's own content \
1196 space — the space `SceneView::ensure_visible` takes by signature"
1197 );
1198 assert_eq!(
1199 outer_seen.borrow().as_slice(),
1200 &[Rect::new(-500.0, 100.0, 50.0, 50.0)],
1201 "and everything further out is asked in window space, which is the \
1202 only space it can compare against its own viewport"
1203 );
1204 }
1205
1206 #[test]
1209 fn a_reveal_with_no_transform_in_the_chain_passes_the_rect_through() {
1210 use crate::event::{ScrollAlign, ScrollMotion};
1211 use std::cell::RefCell;
1212 use std::rc::Rc;
1213
1214 let seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1215 let mut tree = WidgetTree::new();
1216 let inside = tree.add(FillWidget::new());
1217 let _viewport = recorder(&mut tree, inside, seen.clone());
1218 tree.layout(SizeProposal::exact(400.0, 300.0));
1219
1220 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
1221 let mut ops = crate::window::NoopWindowOps;
1222 tree.scroll_rect_into_view(
1223 inside,
1224 target,
1225 0.0,
1226 ScrollAlign::Minimal,
1227 ScrollMotion::Instant,
1228 &mut ops,
1229 );
1230 assert_eq!(seen.borrow().as_slice(), &[target]);
1231 }
1232
1233 #[test]
1234 fn focus_widget() {
1235 let mut tree = WidgetTree::new();
1236 let widget = tree.add(FillWidget::new());
1237 tree.layout(SizeProposal::exact(100.0, 50.0));
1238 tree.focus(widget);
1239 assert_eq!(tree.focused(), Some(widget));
1240 }
1241
1242 #[test]
1243 fn focus_change() {
1244 let mut tree = WidgetTree::new();
1245 let a = tree.add(FillWidget::new());
1246 let b = tree.add(FillWidget::new());
1247 tree.layout(SizeProposal::exact(100.0, 50.0));
1248 tree.focus(a);
1249 assert_eq!(tree.focused(), Some(a));
1250 tree.focus(b);
1251 assert_eq!(tree.focused(), Some(b));
1252 }
1253
1254 #[derive(Debug)]
1258 struct RebuildingRows {
1259 epoch: crate::signal::Signal<u64>,
1260 rows: Vec<WidgetId>,
1261 }
1262
1263 impl Widget for RebuildingRows {
1264 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1265 let sid = ctx.self_id();
1266 let reg = ctx.binding_registry();
1267 self.epoch
1268 .bind_to(sid, reg, crate::binding::BindingLevel::Rebuild);
1269 self.rows = (0..3)
1270 .map(|_| ctx.add(FillWidget::new().focusable()))
1271 .collect();
1272 self.rows.clone()
1273 }
1274
1275 fn layout_response(
1276 &self,
1277 proposal: SizeProposal,
1278 _ctx: &LayoutContext,
1279 ) -> crate::widget::LayoutResponse {
1280 proposal.resolve(0.0, 0.0).into()
1281 }
1282
1283 fn children(&self) -> Vec<WidgetId> {
1284 self.rows.clone()
1285 }
1286 }
1287
1288 #[test]
1298 fn a_rebuild_keeps_focus_inside_the_subtree_that_had_it() {
1299 let mut tree = WidgetTree::new();
1300 let epoch = crate::signal::Signal::new(0u64);
1301 let root = tree.add(RebuildingRows {
1302 epoch: epoch.clone(),
1303 rows: Vec::new(),
1304 });
1305 tree.layout(SizeProposal::exact(100.0, 60.0));
1306
1307 let first_row = tree
1308 .first_focusable_descendant(root)
1309 .expect("the rows are focusable");
1310 tree.focus(first_row);
1311 assert_eq!(tree.focused(), Some(first_row));
1312
1313 epoch.set(1);
1315 tree.layout(SizeProposal::exact(100.0, 60.0));
1316
1317 let focused = tree
1318 .focused()
1319 .expect("a rebuild must not drop focus out of the rebuilt subtree");
1320 assert_ne!(
1321 focused, first_row,
1322 "the old row is dead — focus must have moved to a freshly built one"
1323 );
1324 assert!(
1325 tree.is_descendant_of(focused, root),
1326 "focus must land back inside the rebuilt subtree"
1327 );
1328 assert!(tree.is_active(focused), "the focused node must be live");
1329 }
1330
1331 #[test]
1336 fn a_rebuild_elsewhere_does_not_steal_focus() {
1337 let mut tree = WidgetTree::new();
1338 let epoch = crate::signal::Signal::new(0u64);
1339 let outsider = tree.add(FillWidget::new().focusable());
1340 let _rows = tree.add(RebuildingRows {
1341 epoch: epoch.clone(),
1342 rows: Vec::new(),
1343 });
1344 tree.layout(SizeProposal::exact(100.0, 60.0));
1345
1346 tree.focus(outsider);
1347 assert_eq!(tree.focused(), Some(outsider));
1348
1349 epoch.set(1);
1350 tree.layout(SizeProposal::exact(100.0, 60.0));
1351
1352 assert_eq!(
1353 tree.focused(),
1354 Some(outsider),
1355 "a rebuild that never held focus must not pull focus into itself"
1356 );
1357 }
1358
1359 #[derive(Debug)]
1363 struct KeepingGroups {
1364 epoch: crate::signal::Signal<u64>,
1365 group_epochs: Vec<crate::signal::Signal<u64>>,
1366 groups: Vec<WidgetId>,
1367 }
1368
1369 impl Widget for KeepingGroups {
1370 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1371 let sid = ctx.self_id();
1372 let reg = ctx.binding_registry();
1373 self.epoch
1374 .bind_to(sid, reg, crate::binding::BindingLevel::Rebuild);
1375 if self.groups.is_empty() {
1376 self.groups = self
1377 .group_epochs
1378 .iter()
1379 .map(|epoch| {
1380 ctx.add(RebuildingRows {
1381 epoch: epoch.clone(),
1382 rows: Vec::new(),
1383 })
1384 })
1385 .collect();
1386 }
1387 self.groups.clone()
1388 }
1389
1390 fn layout_response(
1391 &self,
1392 proposal: SizeProposal,
1393 _ctx: &LayoutContext,
1394 ) -> crate::widget::LayoutResponse {
1395 proposal.resolve(0.0, 0.0).into()
1396 }
1397
1398 fn children(&self) -> Vec<WidgetId> {
1399 self.groups.clone()
1400 }
1401
1402 fn preserves_children_on_rebuild(&self) -> bool {
1403 true
1404 }
1405 }
1406
1407 #[test]
1418 fn a_rebuild_restores_focus_into_the_innermost_ancestor_that_survived() {
1419 let mut tree = WidgetTree::new();
1420 let epoch = crate::signal::Signal::new(0u64);
1421 let group_epochs = vec![
1422 crate::signal::Signal::new(0u64),
1423 crate::signal::Signal::new(0u64),
1424 ];
1425 let root = tree.add(KeepingGroups {
1426 epoch: epoch.clone(),
1427 group_epochs: group_epochs.clone(),
1428 groups: Vec::new(),
1429 });
1430 tree.layout(SizeProposal::exact(100.0, 60.0));
1431
1432 let groups = tree.children(root);
1433 let second_group = groups[1];
1434 let row = tree
1435 .first_focusable_descendant(second_group)
1436 .expect("the second group's rows are focusable");
1437 tree.focus(row);
1438
1439 epoch.set(1);
1442 group_epochs[1].set(1);
1443 tree.layout(SizeProposal::exact(100.0, 60.0));
1444
1445 assert_eq!(
1446 tree.children(root),
1447 groups,
1448 "the container kept both groups (the premise of this test)"
1449 );
1450 let focused = tree.focused().expect("focus must not be dropped");
1451 assert_ne!(focused, row, "the old row is dead");
1452 assert!(
1453 tree.is_descendant_of(focused, second_group),
1454 "focus must land back in the group that held it, the innermost \
1455 ancestor that survived, not in the first group of the container"
1456 );
1457 }
1458
1459 #[test]
1460 fn first_focusable_descendant_prefers_first_focusable_child() {
1461 let mut tree = WidgetTree::new();
1462 let a = tree.add(FillWidget::new().focusable());
1463 let _not_focusable = tree.add(FillWidget::new());
1464 let b = tree.add(FillWidget::new().focusable());
1465 let root = tree.add(crate::test_widgets::StackWidget::new().child(a).child(b));
1466 tree.layout(SizeProposal::exact(100.0, 50.0));
1467
1468 assert_eq!(tree.first_focusable_descendant(root), Some(a));
1469 }
1470
1471 #[test]
1472 fn tab_cycles_through_focusable_widgets() {
1473 let mut tree = WidgetTree::new();
1474 let a = tree.add(FillWidget::new().focusable());
1475 let b = tree.add(FillWidget::new().focusable());
1476 let c = tree.add(FillWidget::new().focusable());
1477 tree.layout(SizeProposal::exact(100.0, 50.0));
1478
1479 assert_eq!(tree.focused(), None);
1480
1481 tree.press_key(Key::Tab, Modifiers::NONE);
1482 assert_eq!(tree.focused(), Some(a));
1483
1484 tree.press_key(Key::Tab, Modifiers::NONE);
1485 assert_eq!(tree.focused(), Some(b));
1486
1487 tree.press_key(Key::Tab, Modifiers::NONE);
1488 assert_eq!(tree.focused(), Some(c));
1489
1490 tree.press_key(Key::Tab, Modifiers::NONE);
1491 assert_eq!(tree.focused(), Some(a));
1492 }
1493
1494 #[test]
1495 fn tab_stop_on_composite_excludes_focusable_descendant() {
1496 let mut tree = WidgetTree::new();
1501 let leaf = tree.add(FillWidget::new().focusable());
1502 let composite = tree.add(crate::test_widgets::StackWidget::new().child(leaf));
1503 let other = tree.add(FillWidget::new().focusable());
1504 tree.layout(SizeProposal::exact(100.0, 50.0));
1505
1506 tree.set_tab_stop(composite, false);
1507
1508 tree.press_key(Key::Tab, Modifiers::NONE);
1509 assert_eq!(
1510 tree.focused(),
1511 Some(other),
1512 "Tab must skip the suppressed composite's inner leaf"
1513 );
1514 tree.press_key(Key::Tab, Modifiers::NONE);
1515 assert_eq!(
1516 tree.focused(),
1517 Some(other),
1518 "only the un-suppressed control participates in Tab"
1519 );
1520
1521 tree.set_tab_stop(composite, true);
1523 tree.set_focused(None);
1524 tree.press_key(Key::Tab, Modifiers::NONE);
1525 let first = tree.focused();
1526 tree.press_key(Key::Tab, Modifiers::NONE);
1527 let second = tree.focused();
1528 assert_ne!(
1529 first, second,
1530 "with the composite re-enabled, Tab visits both controls"
1531 );
1532 }
1533
1534 #[test]
1535 fn shift_tab_cycles_backwards() {
1536 let mut tree = WidgetTree::new();
1537 let a = tree.add(FillWidget::new().focusable());
1538 let b = tree.add(FillWidget::new().focusable());
1539 let c = tree.add(FillWidget::new().focusable());
1540 tree.layout(SizeProposal::exact(100.0, 50.0));
1541
1542 tree.press_key(Key::Tab, Modifiers::NONE);
1543 assert_eq!(tree.focused(), Some(a));
1544
1545 tree.press_key(Key::Tab, Modifiers::SHIFT);
1546 assert_eq!(tree.focused(), Some(c));
1547
1548 tree.press_key(Key::Tab, Modifiers::SHIFT);
1549 assert_eq!(tree.focused(), Some(b));
1550 }
1551
1552 #[test]
1553 fn tab_skips_non_focusable_widgets() {
1554 let mut tree = WidgetTree::new();
1555 let _not_focusable = tree.add(FillWidget::new());
1556 let a = tree.add(FillWidget::new().focusable());
1557 let _also_not = tree.add(FillWidget::new());
1558 let b = tree.add(FillWidget::new().focusable());
1559 tree.layout(SizeProposal::exact(100.0, 50.0));
1560
1561 tree.press_key(Key::Tab, Modifiers::NONE);
1562 assert_eq!(tree.focused(), Some(a));
1563
1564 tree.press_key(Key::Tab, Modifiers::NONE);
1565 assert_eq!(tree.focused(), Some(b));
1566 }
1567
1568 #[test]
1579 fn focus_does_not_check_that_the_node_is_focusable() {
1580 let mut tree = WidgetTree::new();
1581 let inert = tree.add(FillWidget::new()); tree.layout(SizeProposal::exact(100.0, 50.0));
1583 assert!(
1584 tree.tab_stops_within(inert).is_empty(),
1585 "the subject has to be unreachable for the point to be made"
1586 );
1587
1588 tree.focus(inert);
1589 assert_eq!(
1590 tree.focused(),
1591 Some(inert),
1592 "focus lands on a node Tab traversal can never offer"
1593 );
1594 }
1595
1596 #[test]
1597 fn tab_focus_has_keyboard_origin() {
1598 let mut tree = WidgetTree::new();
1599 tree.add(FillWidget::new().focusable());
1600 tree.layout(SizeProposal::exact(100.0, 50.0));
1601
1602 tree.press_key(Key::Tab, Modifiers::NONE);
1603 assert_eq!(
1604 tree.focus_origin(),
1605 Some(crate::focus::FocusOrigin::Keyboard)
1606 );
1607 }
1608
1609 #[test]
1610 fn tab_skips_focusable_inside_disabled_ancestor() {
1611 use crate::signal::Signal;
1612 use crate::test_widgets::StackWidget;
1613
1614 let mut tree = WidgetTree::new();
1615 let a = tree.add(FillWidget::new().focusable());
1616 let inner = tree.add(FillWidget::new().focusable());
1617 let disabled_container = tree.add(StackWidget::new().child(inner));
1618 let c = tree.add(FillWidget::new().focusable());
1619 tree.enabled_when(disabled_container, Signal::new(false));
1620 tree.layout(SizeProposal::exact(200.0, 100.0));
1621
1622 tree.press_key(Key::Tab, Modifiers::NONE);
1623 assert_eq!(tree.focused(), Some(a));
1624
1625 tree.press_key(Key::Tab, Modifiers::NONE);
1626 assert_eq!(
1627 tree.focused(),
1628 Some(c),
1629 "tab should skip the focusable widget nested inside the disabled container"
1630 );
1631 }
1632
1633 #[test]
1634 fn dormant_widget_not_in_focus_cycle() {
1635 let mut tree = WidgetTree::new();
1636 let a = tree.add(FillWidget::new().focusable());
1637 let b = tree.add(FillWidget::new().focusable());
1638 let c = tree.add(FillWidget::new().focusable());
1639 tree.layout(SizeProposal::exact(200.0, 100.0));
1640
1641 tree.focus(a);
1642 tree.set_dormant(b);
1643
1644 tree.press_key(Key::Tab, Modifiers::NONE);
1645 assert_eq!(tree.focused(), Some(c));
1646 }
1647
1648 #[test]
1653 fn revalidate_delivers_focus_lost_when_focused_widget_goes_dormant() {
1654 use std::cell::Cell;
1655 use std::rc::Rc;
1656
1657 let lost = Rc::new(Cell::new(0_u32));
1658 let gained = Rc::new(Cell::new(0_u32));
1659 let lost_c = lost.clone();
1660 let gained_c = gained.clone();
1661
1662 let mut tree = WidgetTree::new();
1663 let editor = tree.add(
1664 FillWidget::new()
1665 .focusable()
1666 .on_focus(move |is_gained, _ctx| {
1667 if is_gained {
1668 gained_c.set(gained_c.get() + 1);
1669 } else {
1670 lost_c.set(lost_c.get() + 1);
1671 }
1672 }),
1673 );
1674 let _other = tree.add(FillWidget::new().focusable());
1675 tree.layout(SizeProposal::exact(200.0, 100.0));
1676
1677 tree.focus(editor);
1678 assert_eq!(tree.focused(), Some(editor));
1679 assert_eq!(gained.get(), 1, "focus() delivers FocusGained");
1680 assert_eq!(lost.get(), 0);
1681
1682 tree.set_dormant(editor);
1684 let mut noop = crate::window::NoopWindowOps;
1686 tree.revalidate_interaction_state(&mut noop);
1687
1688 assert_eq!(
1689 tree.focused(),
1690 None,
1691 "tree focus must clear when the target is dormant"
1692 );
1693 assert_eq!(
1694 lost.get(),
1695 1,
1696 "FocusLost must reach the dormant widget so it can clear has_focus / caret blink"
1697 );
1698 }
1699
1700 #[test]
1701 fn tab_cycles_focus_in_tree_order() {
1702 let mut tree = WidgetTree::new();
1703 let a = tree.add(FillWidget::new().focusable());
1704 let b = tree.add(FillWidget::new().focusable());
1705 tree.layout(SizeProposal::exact(200.0, 80.0));
1706
1707 tree.focus(a);
1708 assert_eq!(tree.focused(), Some(a));
1709
1710 tree.press_key(Key::Tab, Modifiers::NONE);
1711 assert_eq!(tree.focused(), Some(b));
1712
1713 tree.press_key(Key::Tab, Modifiers::SHIFT);
1714 assert_eq!(tree.focused(), Some(a));
1715 }
1716
1717 #[test]
1718 fn focus_survives_theme_switch() {
1719 let mut tree = WidgetTree::new();
1720 let a = tree.add(FillWidget::new().focusable());
1721 let _b = tree.add(FillWidget::new().focusable());
1722 tree.layout(SizeProposal::exact(200.0, 80.0));
1723
1724 tree.focus(a);
1725 assert_eq!(tree.focused(), Some(a));
1726
1727 tree.set_theme(crate::presets::intui::dark());
1728 tree.layout(SizeProposal::exact(200.0, 80.0));
1729
1730 assert_eq!(
1731 tree.focused(),
1732 Some(a),
1733 "theme switch must not clobber focus"
1734 );
1735 }
1736
1737 #[test]
1738 fn focus_survives_locale_switch() {
1739 let mut tree = WidgetTree::new();
1740 let a = tree.add(FillWidget::new().focusable());
1741 tree.layout(SizeProposal::exact(200.0, 80.0));
1742
1743 tree.focus(a);
1744 tree.set_locale("fr-FR".to_string());
1745 tree.layout(SizeProposal::exact(200.0, 80.0));
1746
1747 assert_eq!(
1748 tree.focused(),
1749 Some(a),
1750 "locale switch must not clobber focus"
1751 );
1752 }
1753
1754 #[test]
1757 fn focus_within_flips_when_descendant_takes_focus() {
1758 use crate::signal::Signal;
1759 use crate::test_widgets::StackWidget;
1760 use crate::widget_builder::WidgetBuilder;
1761
1762 let halo = Signal::new(false);
1763
1764 let mut tree = WidgetTree::new();
1765 let leaf = tree.add(FillWidget::new().focusable());
1766 let mid = tree.add(StackWidget::new().child(leaf));
1767 let _outer = tree.add(StackWidget::new().child(mid).focus_within(halo.clone()));
1768
1769 tree.layout(SizeProposal::exact(100.0, 50.0));
1770 assert!(!halo.get(), "no focus yet → signal is false");
1771
1772 tree.focus(leaf);
1773 assert!(halo.get(), "leaf now focused, outer is its strict ancestor");
1774 }
1775
1776 #[test]
1777 fn focus_within_strict_ancestors_only() {
1778 use crate::signal::Signal;
1781 use crate::widget_builder::WidgetBuilder;
1782
1783 let halo = Signal::new(false);
1784
1785 let mut tree = WidgetTree::new();
1786 let widget = tree.add(FillWidget::new().focusable().focus_within(halo.clone()));
1787 tree.layout(SizeProposal::exact(100.0, 50.0));
1788 tree.focus(widget);
1789
1790 assert!(
1791 !halo.get(),
1792 "focusing the widget itself must not set its own focus_within"
1793 );
1794 }
1795
1796 #[test]
1797 fn view_focus_active_is_inclusive_for_the_view_itself() {
1798 use crate::test_widgets::StackWidget;
1804 use crate::widget_builder::WidgetBuilder;
1805
1806 let mut tree = WidgetTree::new();
1807 let item = tree.add(FillWidget::new()); let view = tree.add(StackWidget::new().child(item).focusable(true));
1809 let outside = tree.add(FillWidget::new().focusable());
1810 tree.layout(SizeProposal::exact(100.0, 50.0));
1811
1812 let active = tree.view_focus_active_for(item);
1813 assert!(!active.get(), "no focus yet → scope inactive");
1814
1815 tree.focus(view);
1816 assert!(
1817 active.get(),
1818 "view focused directly → scope active (focus_within would be false here)"
1819 );
1820
1821 tree.focus(outside);
1822 assert!(
1823 !active.get(),
1824 "focus moved outside the view → scope inactive"
1825 );
1826 }
1827
1828 #[test]
1829 fn begin_view_focus_for_keys_on_the_view_root_not_the_building_pane() {
1830 use crate::test_widgets::StackWidget;
1838 use crate::widget_builder::WidgetBuilder;
1839
1840 let mut tree = WidgetTree::new();
1841 let row = tree.add(FillWidget::new()); let pane = tree.add(StackWidget::new().child(row)); let root = tree.add(StackWidget::new().child(pane).focusable(true));
1844 tree.layout(SizeProposal::exact(100.0, 50.0));
1845
1846 let keyed_on_root = tree.begin_view_focus(root);
1848 tree.end_view_focus();
1849 let keyed_on_pane = tree.view_focus_signal_for(pane);
1851
1852 assert!(!keyed_on_root.get(), "no focus yet → inactive");
1853 tree.focus(root);
1854 assert!(keyed_on_root.get(), "view root focused → row scope active");
1855 assert!(
1856 !keyed_on_pane.get(),
1857 "pane-keyed scope stays false: the focused root is the pane's ancestor, not its descendant",
1858 );
1859 }
1860
1861 #[test]
1862 fn focus_visible_tracks_input_modality() {
1863 use crate::event::{Key, Modifiers};
1866 let mut tree = WidgetTree::new();
1867 let w = tree.add(FillWidget::new().focusable());
1868 tree.layout(SizeProposal::exact(100.0, 50.0));
1869 let vis = tree.focus_visible_signal();
1870 assert!(!vis.get(), "starts not focus-visible");
1871
1872 tree.press_key(Key::Tab, Modifiers::NONE);
1873 assert!(vis.get(), "keyboard input turns focus-visible ON");
1874
1875 tree.click(w);
1876 assert!(!vis.get(), "pointer input turns focus-visible OFF");
1877
1878 tree.press_key(Key::ArrowDown, Modifiers::NONE);
1879 assert!(vis.get(), "keyboard input turns it back ON");
1880 }
1881
1882 #[test]
1883 fn view_focus_active_without_focusable_ancestor_is_constant_true() {
1884 let mut tree = WidgetTree::new();
1887 let item = tree.add(FillWidget::new());
1888 tree.layout(SizeProposal::exact(100.0, 50.0));
1889 assert!(tree.view_focus_active_for(item).get());
1890 }
1891
1892 #[test]
1893 fn focus_within_diff_across_siblings() {
1894 use crate::signal::Signal;
1897 use crate::test_widgets::StackWidget;
1898 use crate::widget_builder::WidgetBuilder;
1899
1900 let sig_a = Signal::new(false);
1901 let sig_b = Signal::new(false);
1902
1903 let mut tree = WidgetTree::new();
1904 let leaf_a = tree.add(FillWidget::new().focusable());
1905 let leaf_b = tree.add(FillWidget::new().focusable());
1906 let mid_a = tree.add(StackWidget::new().child(leaf_a).focus_within(sig_a.clone()));
1907 let mid_b = tree.add(StackWidget::new().child(leaf_b).focus_within(sig_b.clone()));
1908 let _root = tree.add(StackWidget::new().child(mid_a).child(mid_b));
1909
1910 tree.layout(SizeProposal::exact(100.0, 50.0));
1911 tree.focus(leaf_a);
1912 assert!(sig_a.get(), "sig_a true after focusing leaf_a");
1913 assert!(!sig_b.get(), "sig_b false: leaf_a is not its descendant");
1914
1915 tree.focus(leaf_b);
1916 assert!(!sig_a.get(), "sig_a flipped to false on focus move");
1917 assert!(sig_b.get(), "sig_b flipped to true on focus move");
1918 }
1919
1920 #[test]
1921 fn focus_within_clears_when_focused_widget_destroyed() {
1922 use crate::signal::Signal;
1923 use crate::test_widgets::StackWidget;
1924 use crate::widget_builder::WidgetBuilder;
1925
1926 let halo = Signal::new(false);
1927
1928 let mut tree = WidgetTree::new();
1929 let leaf = tree.add(FillWidget::new().focusable());
1930 let _outer = tree.add(StackWidget::new().child(leaf).focus_within(halo.clone()));
1931
1932 tree.layout(SizeProposal::exact(100.0, 50.0));
1933 tree.focus(leaf);
1934 assert!(halo.get());
1935
1936 tree.destroy_subtree(leaf);
1937 assert!(
1938 !halo.get(),
1939 "destroying the focused widget must clear focus_within on its ancestors"
1940 );
1941 }
1942
1943 #[test]
1944 fn hover_within_flips_via_pointer_move() {
1945 use crate::signal::Signal;
1946 use crate::test_widgets::StackWidget;
1947 use crate::widget_builder::WidgetBuilder;
1948 use teksilo_canvas::Point;
1949
1950 let glow = Signal::new(false);
1951
1952 let mut tree = WidgetTree::new();
1953 let leaf = tree.add(FillWidget::new());
1954 let _outer = tree.add(StackWidget::new().child(leaf).hover_within(glow.clone()));
1955
1956 tree.layout(SizeProposal::exact(100.0, 50.0));
1957 assert!(!glow.get());
1958
1959 tree.pointer_move(Point::new(50.0, 25.0));
1960 assert!(glow.get(), "pointer over leaf → outer.hover_within = true");
1961
1962 tree.pointer_move(Point::new(500.0, 500.0));
1963 assert!(
1964 !glow.get(),
1965 "pointer moves outside the tree → hover_within clears"
1966 );
1967 }
1968
1969 #[test]
1970 fn hover_within_strict_ancestors_only() {
1971 use crate::signal::Signal;
1972 use crate::widget_builder::WidgetBuilder;
1973 use teksilo_canvas::Point;
1974
1975 let glow = Signal::new(false);
1976
1977 let mut tree = WidgetTree::new();
1978 let _w = tree.add(FillWidget::new().hover_within(glow.clone()));
1979 tree.layout(SizeProposal::exact(100.0, 50.0));
1980 tree.pointer_move(Point::new(50.0, 25.0));
1981
1982 assert!(
1983 !glow.get(),
1984 "hovering the widget itself must not set its own hover_within"
1985 );
1986 }
1987}
1988
1989#[cfg(test)]
1990mod tests_scope {
1991 use super::*;
1996 use crate::focus::TraversalScopePolicy::{Continue, Cycle};
1997 use crate::test_widgets::{FillWidget, StackWidget};
1998 use crate::widget_builder::WidgetBuilder;
1999
2000 fn indexed(tree: &mut WidgetTree, idx: i32) -> WidgetId {
2002 tree.add(FillWidget::new().focusable().tab_index(idx))
2003 }
2004
2005 fn tab(tree: &mut WidgetTree) {
2006 tree.press_key(Key::Tab, Modifiers::NONE);
2007 }
2008 fn shift_tab(tree: &mut WidgetTree) {
2009 tree.press_key(Key::Tab, Modifiers::SHIFT);
2010 }
2011
2012 #[test]
2013 fn overlapping_tab_index_in_sibling_scopes_does_not_interleave() {
2014 let mut tree = WidgetTree::new();
2015 let a1 = indexed(&mut tree, 1);
2016 let a2 = indexed(&mut tree, 2);
2017 let scope_a = tree.add(StackWidget::new().child(a1).child(a2));
2018 let b1 = indexed(&mut tree, 1);
2019 let b2 = indexed(&mut tree, 2);
2020 let scope_b = tree.add(StackWidget::new().child(b1).child(b2));
2021 let _root = tree.add(StackWidget::new().child(scope_a).child(scope_b));
2022 tree.set_traversal_scope(scope_a, Continue);
2023 tree.set_traversal_scope(scope_b, Continue);
2024 tree.layout(SizeProposal::exact(100.0, 100.0));
2025
2026 for expected in [a1, a2, b1, b2, a1] {
2028 tab(&mut tree);
2029 assert_eq!(tree.focused(), Some(expected));
2030 }
2031 }
2032
2033 #[test]
2034 fn continue_scope_flows_out_at_the_ends() {
2035 let mut tree = WidgetTree::new();
2037 let a = tree.add(FillWidget::new().focusable());
2038 let c1 = tree.add(FillWidget::new().focusable());
2039 let c2 = tree.add(FillWidget::new().focusable());
2040 let scope_c = tree.add(StackWidget::new().child(c1).child(c2));
2041 let b = tree.add(FillWidget::new().focusable());
2042 let _root = tree.add(StackWidget::new().child(a).child(scope_c).child(b));
2043 tree.set_traversal_scope(scope_c, Continue);
2044 tree.layout(SizeProposal::exact(100.0, 100.0));
2045
2046 for expected in [a, c1, c2, b, a] {
2047 tab(&mut tree);
2048 assert_eq!(tree.focused(), Some(expected));
2049 }
2050 tree.focus(c1);
2052 shift_tab(&mut tree);
2053 assert_eq!(tree.focused(), Some(a));
2054 }
2055
2056 #[test]
2057 fn cycle_scope_wraps_and_never_escapes() {
2058 let mut tree = WidgetTree::new();
2060 let a = tree.add(FillWidget::new().focusable());
2061 let d1 = tree.add(FillWidget::new().focusable());
2062 let d2 = tree.add(FillWidget::new().focusable());
2063 let scope_d = tree.add(StackWidget::new().child(d1).child(d2));
2064 let _root = tree.add(StackWidget::new().child(a).child(scope_d));
2065 tree.set_traversal_scope(scope_d, Cycle);
2066 tree.layout(SizeProposal::exact(100.0, 100.0));
2067
2068 tree.focus(d1);
2069 for _ in 0..10 {
2070 tab(&mut tree);
2071 let f = tree.focused();
2072 assert!(
2073 f == Some(d1) || f == Some(d2),
2074 "Cycle scope must trap Tab inside {{d1,d2}}, got {f:?}"
2075 );
2076 }
2077 tree.focus(d1);
2079 tab(&mut tree);
2080 assert_eq!(tree.focused(), Some(d2));
2081 shift_tab(&mut tree);
2082 assert_eq!(tree.focused(), Some(d1));
2083 shift_tab(&mut tree);
2084 assert_eq!(tree.focused(), Some(d2), "Shift+Tab at start wraps to last");
2085 }
2086
2087 #[test]
2088 fn empty_scope_is_skipped() {
2089 let mut tree = WidgetTree::new();
2091 let a = tree.add(FillWidget::new().focusable());
2092 let empty = tree.add(StackWidget::new());
2093 let b = tree.add(FillWidget::new().focusable());
2094 let _root = tree.add(StackWidget::new().child(a).child(empty).child(b));
2095 tree.set_traversal_scope(empty, Continue);
2096 tree.layout(SizeProposal::exact(100.0, 100.0));
2097
2098 for expected in [a, b, a] {
2099 tab(&mut tree);
2100 assert_eq!(tree.focused(), Some(expected));
2101 }
2102 }
2103
2104 #[test]
2105 fn single_member_cycle_scope_stays_put() {
2106 let mut tree = WidgetTree::new();
2107 let e = tree.add(FillWidget::new().focusable());
2108 let scope_e = tree.add(StackWidget::new().child(e));
2109 tree.set_traversal_scope(scope_e, Cycle);
2110 tree.layout(SizeProposal::exact(100.0, 50.0));
2111
2112 tree.focus(e);
2113 tab(&mut tree);
2114 assert_eq!(tree.focused(), Some(e));
2115 shift_tab(&mut tree);
2116 assert_eq!(tree.focused(), Some(e));
2117 }
2118
2119 #[test]
2120 fn nested_continue_in_cycle_flows_out_to_outer() {
2121 let mut tree = WidgetTree::new();
2123 let x = tree.add(FillWidget::new().focusable());
2124 let i1 = tree.add(FillWidget::new().focusable());
2125 let i2 = tree.add(FillWidget::new().focusable());
2126 let inner = tree.add(StackWidget::new().child(i1).child(i2));
2127 let y = tree.add(FillWidget::new().focusable());
2128 let outer = tree.add(StackWidget::new().child(x).child(inner).child(y));
2129 tree.set_traversal_scope(inner, Continue);
2130 tree.set_traversal_scope(outer, Cycle);
2131 tree.layout(SizeProposal::exact(100.0, 100.0));
2132
2133 for expected in [x, i1, i2, y, x] {
2134 tab(&mut tree);
2135 assert_eq!(tree.focused(), Some(expected));
2136 }
2137 tree.focus(i1);
2139 shift_tab(&mut tree);
2140 assert_eq!(tree.focused(), Some(x));
2141 }
2142
2143 #[test]
2144 fn nested_cycle_in_cycle_traps_in_the_inner_scope() {
2145 let mut tree = WidgetTree::new();
2148 let x = tree.add(FillWidget::new().focusable());
2149 let i1 = tree.add(FillWidget::new().focusable());
2150 let i2 = tree.add(FillWidget::new().focusable());
2151 let inner = tree.add(StackWidget::new().child(i1).child(i2));
2152 let y = tree.add(FillWidget::new().focusable());
2153 let outer = tree.add(StackWidget::new().child(x).child(inner).child(y));
2154 tree.set_traversal_scope(inner, Cycle);
2155 tree.set_traversal_scope(outer, Cycle);
2156 tree.layout(SizeProposal::exact(100.0, 100.0));
2157
2158 tree.focus(i1);
2159 for _ in 0..6 {
2160 tab(&mut tree);
2161 let f = tree.focused();
2162 assert!(
2163 f == Some(i1) || f == Some(i2),
2164 "inner Cycle must trap Tab; reached {f:?}"
2165 );
2166 }
2167 }
2168
2169 #[test]
2170 fn scoped_tab_index_orders_within_a_scope() {
2171 let mut tree = WidgetTree::new();
2173 let f3 = indexed(&mut tree, 3);
2174 let f1 = indexed(&mut tree, 1);
2175 let f2 = indexed(&mut tree, 2);
2176 let scope_f = tree.add(StackWidget::new().child(f3).child(f1).child(f2));
2177 tree.set_traversal_scope(scope_f, Cycle);
2178 tree.layout(SizeProposal::exact(100.0, 100.0));
2179
2180 for expected in [f1, f2, f3, f1] {
2181 tab(&mut tree);
2182 assert_eq!(tree.focused(), Some(expected));
2183 }
2184 }
2185
2186 #[test]
2187 fn destroyed_focused_widget_re_enters_at_first() {
2188 let mut tree = WidgetTree::new();
2189 let a = tree.add(FillWidget::new().focusable());
2190 let b = tree.add(FillWidget::new().focusable());
2191 tree.layout(SizeProposal::exact(100.0, 50.0));
2192
2193 tree.focus(b);
2194 tree.destroy_subtree(b);
2195 tab(&mut tree);
2196 assert_eq!(tree.focused(), Some(a), "Tab after destroy enters at first");
2197 }
2198
2199 #[test]
2200 fn set_traversal_scope_forces_node_non_focusable() {
2201 let mut tree = WidgetTree::new();
2204 let inner = tree.add(FillWidget::new().focusable());
2205 let scope = tree.add(StackWidget::new().child(inner).focusable(true));
2206 tree.set_traversal_scope(scope, Continue);
2207 tree.layout(SizeProposal::exact(100.0, 50.0));
2208
2209 tab(&mut tree);
2211 assert_eq!(tree.focused(), Some(inner));
2212 tab(&mut tree);
2213 assert_eq!(tree.focused(), Some(inner));
2214 }
2215
2216 #[test]
2217 fn no_scopes_behaves_like_a_flat_cycle() {
2218 let mut tree = WidgetTree::new();
2221 let a = indexed(&mut tree, 2);
2222 let b = indexed(&mut tree, 1);
2223 let c = tree.add(FillWidget::new().focusable());
2224 tree.layout(SizeProposal::exact(100.0, 50.0));
2225
2226 for expected in [b, a, c, b] {
2228 tab(&mut tree);
2229 assert_eq!(tree.focused(), Some(expected));
2230 }
2231 }
2232
2233 #[test]
2234 fn centered_modal_confines_tab_to_its_content() {
2235 let mut tree = WidgetTree::new();
2238 let outside1 = tree.add(FillWidget::new().focusable());
2239 let outside2 = tree.add(FillWidget::new().focusable());
2240 let m1 = tree.add(FillWidget::new().focusable());
2241 let m2 = tree.add(FillWidget::new().focusable());
2242 let content = tree.add(StackWidget::new().child(m1).child(m2));
2243 tree.layout(SizeProposal::exact(200.0, 100.0));
2244
2245 tree.show_overlay(crate::overlay::OverlayRequest {
2246 content_id: content,
2247 anchor: outside1,
2248 placement: crate::overlay::OverlayPlacement::Centered,
2249 dismiss: crate::overlay::DismissBehavior::Manual,
2250 layer: crate::overlay::OverlayLayer::InTree,
2251 parent_overlay: None,
2252 on_dismiss: None,
2253 fade_duration: None,
2254 });
2255
2256 for _ in 0..6 {
2257 tab(&mut tree);
2258 let f = tree.focused();
2259 assert!(
2260 f == Some(m1) || f == Some(m2),
2261 "modal must trap Tab inside its content, reached {f:?}"
2262 );
2263 }
2264 assert_ne!(tree.focused(), Some(outside1));
2265 assert_ne!(tree.focused(), Some(outside2));
2266 }
2267}
2268
2269#[cfg(test)]
2270mod tests_focus_out_dismissal {
2271 use super::*;
2278 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
2279 use crate::test_widgets::{FillWidget, StackWidget};
2280
2281 fn tab(tree: &mut WidgetTree) {
2282 tree.press_key(Key::Tab, Modifiers::NONE);
2283 }
2284
2285 fn show_anchored(
2287 tree: &mut WidgetTree,
2288 content: WidgetId,
2289 anchor: WidgetId,
2290 parent: Option<crate::overlay::OverlayId>,
2291 ) -> crate::overlay::OverlayId {
2292 tree.show_overlay(OverlayRequest {
2293 content_id: content,
2294 anchor,
2295 placement: OverlayPlacement::Below,
2296 dismiss: DismissBehavior::EscapeOrClickOutside,
2297 layer: OverlayLayer::InTree,
2298 parent_overlay: parent,
2299 on_dismiss: None,
2300 fade_duration: None,
2301 })
2302 }
2303
2304 #[test]
2305 fn tab_out_of_a_non_modal_overlay_dismisses_it() {
2306 let mut tree = WidgetTree::new();
2307 let anchor = tree.add(FillWidget::new().focusable());
2308 let after = tree.add(FillWidget::new().focusable());
2309 let inner = tree.add(FillWidget::new().focusable());
2310 let content = tree.add(StackWidget::new().child(inner));
2311 tree.layout(SizeProposal::exact(200.0, 100.0));
2312 show_anchored(&mut tree, content, anchor, None);
2313
2314 tree.focus(inner);
2315 tab(&mut tree);
2316
2317 assert_ne!(tree.focused(), Some(inner), "focus genuinely left");
2318 assert!(
2319 tree.active_overlays().is_empty(),
2320 "an overlay must not stay open over the focus ring that left it"
2321 );
2322 assert!(tree.focused() == Some(after) || tree.focused() == Some(anchor));
2323 }
2324
2325 #[test]
2327 fn a_centered_modal_is_never_dismissed_by_focus_moving() {
2328 let mut tree = WidgetTree::new();
2329 let outside = tree.add(FillWidget::new().focusable());
2330 let m1 = tree.add(FillWidget::new().focusable());
2331 let content = tree.add(StackWidget::new().child(m1));
2332 tree.layout(SizeProposal::exact(200.0, 100.0));
2333 tree.show_overlay(OverlayRequest {
2334 content_id: content,
2335 anchor: outside,
2336 placement: OverlayPlacement::Centered,
2337 dismiss: DismissBehavior::Manual,
2338 layer: OverlayLayer::InTree,
2339 parent_overlay: None,
2340 on_dismiss: None,
2341 fade_duration: None,
2342 });
2343
2344 tree.focus(m1);
2345 tree.focus(outside);
2349
2350 assert_eq!(
2351 tree.active_overlays().len(),
2352 1,
2353 "a modal is the one overlay that legitimately contains focus"
2354 );
2355 }
2356
2357 #[test]
2361 fn the_modal_scrim_survives_focus_moving_inside_the_modal() {
2362 let mut tree = WidgetTree::new();
2363 let opener = tree.add(FillWidget::new().focusable());
2364 let m1 = tree.add(FillWidget::new().focusable());
2365 let m2 = tree.add(FillWidget::new().focusable());
2366 let scrim = tree.add(FillWidget::new());
2367 let content = tree.add(StackWidget::new().child(m1).child(m2));
2368 tree.layout(SizeProposal::exact(200.0, 100.0));
2369
2370 tree.show_overlay(OverlayRequest {
2371 content_id: scrim,
2372 anchor: opener,
2373 placement: OverlayPlacement::FullViewport,
2374 dismiss: DismissBehavior::Manual,
2375 layer: OverlayLayer::InTree,
2376 parent_overlay: None,
2377 on_dismiss: None,
2378 fade_duration: None,
2379 });
2380 tree.show_overlay(OverlayRequest {
2381 content_id: content,
2382 anchor: opener,
2383 placement: OverlayPlacement::Centered,
2384 dismiss: DismissBehavior::Manual,
2385 layer: OverlayLayer::InTree,
2386 parent_overlay: None,
2387 on_dismiss: None,
2388 fade_duration: None,
2389 });
2390
2391 tree.focus(opener);
2392 tree.focus(m1);
2393 tab(&mut tree);
2394
2395 assert_eq!(
2396 tree.active_overlays().len(),
2397 2,
2398 "scrim and modal both stand while focus moves within the modal"
2399 );
2400 }
2401
2402 #[test]
2405 fn focus_moving_into_a_child_overlay_keeps_the_parent_open() {
2406 let mut tree = WidgetTree::new();
2407 let anchor = tree.add(FillWidget::new().focusable());
2408 let parent_item = tree.add(FillWidget::new().focusable());
2409 let parent_content = tree.add(StackWidget::new().child(parent_item));
2410 let child_item = tree.add(FillWidget::new().focusable());
2411 let child_content = tree.add(StackWidget::new().child(child_item));
2412 tree.layout(SizeProposal::exact(200.0, 100.0));
2413
2414 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2415 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2416
2417 tree.focus(parent_item);
2418 tree.focus(child_item);
2419
2420 assert_eq!(
2421 tree.active_overlays().len(),
2422 2,
2423 "opening a submenu is not leaving the menu that owns it"
2424 );
2425 }
2426
2427 #[test]
2430 fn focus_back_to_the_parent_overlay_closes_only_the_child() {
2431 let mut tree = WidgetTree::new();
2432 let anchor = tree.add(FillWidget::new().focusable());
2433 let parent_item = tree.add(FillWidget::new().focusable());
2434 let parent_content = tree.add(StackWidget::new().child(parent_item));
2435 let child_item = tree.add(FillWidget::new().focusable());
2436 let child_content = tree.add(StackWidget::new().child(child_item));
2437 tree.layout(SizeProposal::exact(200.0, 100.0));
2438
2439 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2440 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2441
2442 tree.focus(child_item);
2443 tree.focus(parent_item);
2444
2445 assert_eq!(
2446 tree.active_overlays(),
2447 vec![parent],
2448 "the submenu goes, the menu that owns it stays"
2449 );
2450 }
2451
2452 #[test]
2455 fn leaving_a_nested_cascade_closes_every_level() {
2456 let mut tree = WidgetTree::new();
2457 let anchor = tree.add(FillWidget::new().focusable());
2458 let away = tree.add(FillWidget::new().focusable());
2459 let parent_item = tree.add(FillWidget::new().focusable());
2460 let parent_content = tree.add(StackWidget::new().child(parent_item));
2461 let child_item = tree.add(FillWidget::new().focusable());
2462 let child_content = tree.add(StackWidget::new().child(child_item));
2463 tree.layout(SizeProposal::exact(200.0, 100.0));
2464
2465 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2466 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2467
2468 tree.focus(child_item);
2469 tree.focus(away);
2470
2471 assert!(
2472 tree.active_overlays().is_empty(),
2473 "one move out of the cascade must leave nothing behind"
2474 );
2475 }
2476
2477 #[test]
2487 fn a_dropdown_inside_a_modal_still_follows_focus_out() {
2488 let mut tree = WidgetTree::new();
2489 let opener = tree.add(FillWidget::new().focusable());
2490 let trigger = tree.add(FillWidget::new().focusable());
2491 let next = tree.add(FillWidget::new().focusable());
2492 let modal_content = tree.add(StackWidget::new().child(trigger).child(next));
2493 let panel = tree.add(StackWidget::new());
2494 tree.layout(SizeProposal::exact(200.0, 100.0));
2495
2496 tree.show_overlay(OverlayRequest {
2497 content_id: modal_content,
2498 anchor: opener,
2499 placement: OverlayPlacement::Centered,
2500 dismiss: DismissBehavior::Manual,
2501 layer: OverlayLayer::InTree,
2502 parent_overlay: None,
2503 on_dismiss: None,
2504 fade_duration: None,
2505 });
2506 show_anchored(&mut tree, panel, trigger, None);
2508
2509 tree.focus(trigger);
2510 assert_eq!(
2511 tree.active_overlays().len(),
2512 2,
2513 "precondition: modal + panel"
2514 );
2515
2516 tab(&mut tree);
2517
2518 assert_eq!(
2519 tree.focused(),
2520 Some(next),
2521 "Tab moves on within the modal, as it should"
2522 );
2523 assert_eq!(
2524 tree.active_overlays().len(),
2525 1,
2526 "the dropdown must go — only the modal hosting it stays"
2527 );
2528 }
2529
2530 #[test]
2538 fn a_viewport_placed_notification_ignores_focus_moving() {
2539 let mut tree = WidgetTree::new();
2540 let trigger = tree.add(FillWidget::new().focusable());
2541 let elsewhere = tree.add(FillWidget::new().focusable());
2542 let snack = tree.add(StackWidget::new());
2543 tree.layout(SizeProposal::exact(200.0, 100.0));
2544 tree.show_overlay(OverlayRequest {
2545 content_id: snack,
2546 anchor: trigger,
2547 placement: OverlayPlacement::BottomCenter,
2548 dismiss: DismissBehavior::Manual,
2549 layer: OverlayLayer::InTree,
2550 parent_overlay: None,
2551 on_dismiss: None,
2552 fade_duration: None,
2553 });
2554
2555 tree.focus(trigger);
2556 tab(&mut tree);
2557
2558 assert_eq!(
2559 tree.active_overlays().len(),
2560 1,
2561 "a snackbar outlives the keystroke that moved focus off its trigger"
2562 );
2563 assert_ne!(tree.focused(), Some(trigger), "and focus did move");
2564 assert_eq!(tree.focused(), Some(elsewhere));
2565 }
2566
2567 #[test]
2571 fn leaving_a_hosted_menu_spares_the_host() {
2572 let mut tree = WidgetTree::new();
2573 let opener = tree.add(FillWidget::new().focusable());
2574 let away = tree.add(FillWidget::new().focusable());
2575 let modal_item = tree.add(FillWidget::new().focusable());
2576 let modal_content = tree.add(StackWidget::new().child(modal_item));
2577 let menu_item = tree.add(FillWidget::new().focusable());
2578 let menu_content = tree.add(StackWidget::new().child(menu_item));
2579 tree.layout(SizeProposal::exact(200.0, 100.0));
2580
2581 let host = tree.show_overlay(OverlayRequest {
2583 content_id: modal_content,
2584 anchor: opener,
2585 placement: OverlayPlacement::Centered,
2586 dismiss: DismissBehavior::Manual,
2587 layer: OverlayLayer::InTree,
2588 parent_overlay: None,
2589 on_dismiss: None,
2590 fade_duration: None,
2591 });
2592 show_anchored(&mut tree, menu_content, modal_item, Some(host));
2593
2594 tree.focus(menu_item);
2595 tree.focus(away);
2596
2597 assert_eq!(
2598 tree.active_overlays(),
2599 vec![host],
2600 "the menu goes; the modal hosting it stays"
2601 );
2602 }
2603
2604 #[test]
2608 fn leaving_the_anchor_dismisses_a_panel_focus_never_entered() {
2609 let mut tree = WidgetTree::new();
2610 let trigger = tree.add(FillWidget::new().focusable());
2611 let after = tree.add(FillWidget::new().focusable());
2612 let content = tree.add(StackWidget::new());
2613 tree.layout(SizeProposal::exact(200.0, 100.0));
2614 show_anchored(&mut tree, content, trigger, None);
2615
2616 tree.focus(trigger);
2617 assert_eq!(tree.active_overlays().len(), 1, "precondition: panel is up");
2618
2619 tree.focus(after);
2620 assert!(
2621 tree.active_overlays().is_empty(),
2622 "leaving the trigger is leaving the dropdown it owns"
2623 );
2624 }
2625}