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 focusable_descendants(&self, root: WidgetId) -> Vec<WidgetId> {
536 fn walk(tree: &WidgetTree, id: WidgetId, out: &mut Vec<WidgetId>) {
537 if !tree.arena.is_active(id) {
538 return;
539 }
540 let Some(node) = tree.arena.get(id) else {
541 return;
542 };
543 if node.enabled_state.as_ref().is_some_and(|s| !s.get()) {
544 return;
545 }
546 if tree.is_node_focusable(node) {
547 out.push(id);
548 }
549 for &child in tree.arena.children(id) {
550 walk(tree, child, out);
551 }
552 }
553 let mut out = Vec::new();
554 walk(self, root, &mut out);
555 out
556 }
557
558 pub(crate) fn first_focus_capable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
562 if !self.arena.is_active(root) {
563 return None;
564 }
565 let node = self.arena.get(root)?;
566 if self.is_node_focusable(node) {
567 return Some(root);
568 }
569 node.children
570 .iter()
571 .find_map(|&child| self.first_focus_capable_descendant(child))
572 }
573
574 pub fn is_active(&self, id: WidgetId) -> bool {
580 self.arena.is_active(id)
581 }
582
583 pub fn widget_initial_focus_hint(&self, id: WidgetId) -> Option<WidgetId> {
599 if !self.arena.is_active(id) {
600 return None;
601 }
602 if let Some(node) = self.arena.get(id) {
603 if let Some(target) = node.widget.initial_focus_hint()
604 && self.arena.is_active(target)
605 && self.is_descendant_of(target, id)
606 {
607 return Some(target);
608 }
609 for &child in &node.children {
610 if let Some(found) = self.widget_initial_focus_hint(child) {
611 return Some(found);
612 }
613 }
614 }
615 None
616 }
617
618 pub fn focus_origin(&self) -> Option<crate::focus::FocusOrigin> {
620 self.focus_origin
621 }
622
623 pub fn focus_visible_signal(&self) -> crate::signal::Signal<bool> {
627 self.focus_visible.clone()
628 }
629
630 pub(super) fn cycle_focus(&mut self, reverse: bool, ops: &mut dyn crate::window::WindowOps) {
639 let mut entries = Vec::new();
640 if let Some(modal_overlay) = self.overlay_manager.topmost_modal() {
641 let content_id = modal_overlay.content_id;
642 self.collect_scope_entries(content_id, &mut entries);
643 } else {
644 let roots = self.arena.roots();
645 for root in roots {
646 if self.tooltip_content_root(root).is_some() {
656 continue;
657 }
658 self.collect_scope_entries(root, &mut entries);
659 }
660 }
661 sort_scope_entries(&mut entries);
662 self.splice_sticky_tooltips_after_anchors(&mut entries);
663 let root_scope = ScopeNode {
664 policy: crate::focus::TraversalScopePolicy::Cycle,
665 entries,
666 };
667
668 if let StepResult::Found(next_id) = navigate_scope(&root_scope, self.focused, reverse, true)
669 {
670 self.focus_with_origin_ops(next_id, crate::focus::FocusOrigin::Keyboard, &mut *ops);
671 }
672 }
675
676 fn tooltip_content_root(&self, id: WidgetId) -> Option<usize> {
678 self.tooltips.iter().position(|e| e.content_id == id)
679 }
680
681 fn splice_sticky_tooltips_after_anchors(&self, entries: &mut Vec<ScopeEntry>) {
699 let sticky: Vec<(WidgetId, WidgetId)> = self
700 .tooltips
701 .iter()
702 .filter(|e| e.is_sticky && e.overlay_id.is_some())
703 .map(|e| (e.anchor_id, e.content_id))
704 .collect();
705
706 for (anchor_id, content_id) in sticky {
707 let mut panel = Vec::new();
708 self.collect_scope_entries(content_id, &mut panel);
709 if panel.is_empty() {
710 continue;
711 }
712 sort_scope_entries(&mut panel);
713
714 let stop = self
719 .find_focusable_at_or_above(anchor_id)
720 .unwrap_or(anchor_id);
721 let at = entries
722 .iter()
723 .position(|entry| scope_entry_contains(entry, stop))
724 .map_or(entries.len(), |i| i + 1);
729
730 for (offset, item) in panel.into_iter().enumerate() {
731 entries.insert(at + offset, item);
732 }
733 }
734 }
735
736 pub(super) fn tab_stop_effective(&self, id: WidgetId) -> bool {
744 let mut current = Some(id);
745 while let Some(cur) = current {
746 let Some(node) = self.arena.get(cur) else {
747 break;
748 };
749 if let Some(prop) = node.tab_stop.as_ref() {
750 return prop.get();
751 }
752 current = node.parent;
753 }
754 true
755 }
756
757 pub(super) fn is_node_focusable(&self, node: &crate::arena::WidgetNode) -> bool {
759 node.node_focusable.unwrap_or(false)
760 }
761
762 pub(super) fn find_focusable_at_or_above(&self, id: WidgetId) -> Option<WidgetId> {
764 let mut current = Some(id);
765 while let Some(current_id) = current {
766 if let Some(node) = self.arena.get(current_id)
767 && self.is_node_focusable(node)
768 {
769 return Some(current_id);
770 }
771 current = self.arena.parent(current_id);
772 }
773 None
774 }
775
776 fn collect_scope_entries(&self, id: WidgetId, out: &mut Vec<ScopeEntry>) {
792 if !self.arena.is_active(id) {
793 return;
794 }
795 let Some(node) = self.arena.get(id) else {
796 return;
797 };
798 if node
799 .enabled_state
800 .as_ref()
801 .map(|s| !s.get())
802 .unwrap_or(false)
803 {
804 return;
805 }
806
807 if let Some(policy) = node.node_traversal_scope {
810 let mut child_entries = Vec::new();
811 for &child in &node.children {
812 self.collect_scope_entries(child, &mut child_entries);
813 }
814 sort_scope_entries(&mut child_entries);
815 out.push(ScopeEntry {
816 tab_index: node.node_tab_index,
817 kind: ScopeEntryKind::Scope(ScopeNode {
818 policy,
819 entries: child_entries,
820 }),
821 });
822 return;
823 }
824
825 if self.is_node_focusable(node) && self.tab_stop_effective(id) {
827 out.push(ScopeEntry {
828 tab_index: node.node_tab_index,
829 kind: ScopeEntryKind::Focusable(id),
830 });
831 }
832 for &child in &node.children {
833 self.collect_scope_entries(child, out);
834 }
835 }
836
837 pub(super) fn strict_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
842 let mut chain = Vec::new();
843 if let Some(start) = id {
844 let mut current = self.arena.parent(start);
845 while let Some(parent) = current {
846 chain.push(parent);
847 current = self.arena.parent(parent);
848 }
849 }
850 chain
851 }
852
853 pub(crate) fn update_focus_within_signals(
858 &mut self,
859 old: Option<WidgetId>,
860 new: Option<WidgetId>,
861 ) {
862 let old_chain = self.strict_ancestors_of(old);
863 let new_chain = self.strict_ancestors_of(new);
864 for &id in &old_chain {
866 if !new_chain.contains(&id)
867 && let Some(node) = self.arena.get(id)
868 && let Some(sig) = node.focus_within_signal.clone()
869 {
870 sig.set(false);
871 }
872 }
873 for &id in &new_chain {
875 if !old_chain.contains(&id)
876 && let Some(node) = self.arena.get(id)
877 && let Some(sig) = node.focus_within_signal.clone()
878 {
879 sig.set(true);
880 }
881 }
882 }
883
884 pub(super) fn inclusive_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
887 let mut chain = Vec::new();
888 if let Some(start) = id {
889 chain.push(start);
890 chain.extend(self.strict_ancestors_of(Some(start)));
891 }
892 chain
893 }
894
895 pub(crate) fn update_view_focus_signals(
900 &mut self,
901 old: Option<WidgetId>,
902 new: Option<WidgetId>,
903 ) {
904 let old_chain = self.inclusive_ancestors_of(old);
905 let new_chain = self.inclusive_ancestors_of(new);
906 for &id in &old_chain {
907 if !new_chain.contains(&id)
908 && let Some(node) = self.arena.get(id)
909 && let Some(sig) = node.view_focus_signal.clone()
910 {
911 sig.set(false);
912 }
913 }
914 for &id in &new_chain {
915 if !old_chain.contains(&id)
916 && let Some(node) = self.arena.get(id)
917 && let Some(sig) = node.view_focus_signal.clone()
918 {
919 sig.set(true);
920 }
921 }
922 }
923
924 pub(crate) fn view_focus_signal_for(
927 &mut self,
928 node_id: WidgetId,
929 ) -> crate::signal::Signal<bool> {
930 if let Some(node) = self.arena.get(node_id)
931 && let Some(sig) = node.view_focus_signal.clone()
932 {
933 return sig;
934 }
935 let active = self.inclusive_ancestors_of(self.focused).contains(&node_id);
936 let sig = crate::signal::Signal::new(active);
937 if let Some(node) = self.arena.get_mut(node_id) {
938 node.view_focus_signal = Some(sig.clone());
939 }
940 sig
941 }
942
943 pub fn view_focus_active_for(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
949 match self.find_focusable_at_or_above(node_id) {
950 Some(scope) => self.view_focus_signal_for(scope),
951 None => crate::signal::Signal::new(true),
952 }
953 }
954
955 pub fn begin_view_focus(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
960 let sig = self.view_focus_signal_for(node_id);
961 self.view_focus_stack.push(sig.clone());
962 sig
963 }
964
965 pub fn end_view_focus(&mut self) {
967 self.view_focus_stack.pop();
968 }
969
970 pub fn current_view_focus(&self) -> Option<crate::signal::Signal<bool>> {
972 self.view_focus_stack.last().cloned()
973 }
974
975 pub(crate) fn set_hovered(&mut self, value: Option<WidgetId>) {
989 if let Some(entry) = self.pointers.hover_owner_mut() {
990 entry.hovered = value;
991 }
992 if self.hovered_signal.get() != value {
993 self.hovered_signal.set(value);
994 }
995 }
996
997 pub(crate) fn set_focused(&mut self, value: Option<WidgetId>) {
1004 self.focused = value;
1005 if self.focused_signal.get() != value {
1006 self.focused_signal.set(value);
1007 }
1008 }
1009
1010 pub(crate) fn update_hover_within_signals(
1013 &mut self,
1014 old: Option<WidgetId>,
1015 new: Option<WidgetId>,
1016 ) {
1017 let old_chain = self.strict_ancestors_of(old);
1018 let new_chain = self.strict_ancestors_of(new);
1019 if let Some(entry) = self.pointers.hover_owner_mut() {
1023 entry.hover_within = new_chain.clone();
1024 }
1025 for &id in &old_chain {
1026 if !new_chain.contains(&id)
1027 && let Some(node) = self.arena.get(id)
1028 && let Some(sig) = node.hover_within_signal.clone()
1029 {
1030 sig.set(false);
1031 }
1032 }
1033 for &id in &new_chain {
1034 if !old_chain.contains(&id)
1035 && let Some(node) = self.arena.get(id)
1036 && let Some(sig) = node.hover_within_signal.clone()
1037 {
1038 sig.set(true);
1039 }
1040 }
1041 }
1042}
1043
1044#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1047pub(crate) enum FocusRequestMode {
1048 Exact,
1050 SelfOrDescendant,
1053 Into,
1056}
1057
1058#[derive(Debug, Clone, Copy)]
1061pub(crate) struct PendingFocusRequest {
1062 id: WidgetId,
1063 mode: FocusRequestMode,
1064}
1065
1066struct ScopeEntry {
1078 tab_index: Option<i32>,
1079 kind: ScopeEntryKind,
1080}
1081
1082enum ScopeEntryKind {
1083 Focusable(WidgetId),
1084 Scope(ScopeNode),
1085}
1086
1087struct ScopeNode {
1089 policy: crate::focus::TraversalScopePolicy,
1090 entries: Vec<ScopeEntry>,
1091}
1092
1093enum StepResult {
1095 Found(WidgetId),
1097 Escaped,
1101}
1102
1103fn scope_wraps(policy: crate::focus::TraversalScopePolicy, is_root: bool) -> bool {
1106 is_root || matches!(policy, crate::focus::TraversalScopePolicy::Cycle)
1107}
1108
1109fn sort_scope_entries(entries: &mut [ScopeEntry]) {
1112 entries.sort_by(|a, b| match (a.tab_index, b.tab_index) {
1113 (Some(ia), Some(ib)) => ia.cmp(&ib),
1114 (Some(_), None) => std::cmp::Ordering::Less,
1115 (None, Some(_)) => std::cmp::Ordering::Greater,
1116 (None, None) => std::cmp::Ordering::Equal,
1117 });
1118}
1119
1120fn scope_entry_contains(entry: &ScopeEntry, f: WidgetId) -> bool {
1122 match &entry.kind {
1123 ScopeEntryKind::Focusable(id) => *id == f,
1124 ScopeEntryKind::Scope(child) => child.entries.iter().any(|e| scope_entry_contains(e, f)),
1125 }
1126}
1127
1128fn enter_scope_edge(scope: &ScopeNode, reverse: bool) -> Option<WidgetId> {
1132 let n = scope.entries.len();
1133 for k in 0..n {
1134 let idx = if reverse { n - 1 - k } else { k };
1135 match &scope.entries[idx].kind {
1136 ScopeEntryKind::Focusable(id) => return Some(*id),
1137 ScopeEntryKind::Scope(child) => {
1138 if let Some(id) = enter_scope_edge(child, reverse) {
1139 return Some(id);
1140 }
1141 }
1142 }
1143 }
1144 None
1145}
1146
1147fn navigate_scope(
1151 scope: &ScopeNode,
1152 focused: Option<WidgetId>,
1153 reverse: bool,
1154 is_root: bool,
1155) -> StepResult {
1156 if scope.entries.is_empty() {
1157 return StepResult::Escaped;
1158 }
1159
1160 let cur = focused.and_then(|f| {
1161 scope
1162 .entries
1163 .iter()
1164 .position(|e| scope_entry_contains(e, f))
1165 });
1166
1167 let from = match cur {
1168 None => {
1169 return match enter_scope_edge(scope, reverse) {
1172 Some(id) => StepResult::Found(id),
1173 None => StepResult::Escaped,
1174 };
1175 }
1176 Some(i) => i,
1177 };
1178
1179 if let ScopeEntryKind::Scope(child) = &scope.entries[from].kind
1182 && let StepResult::Found(id) = navigate_scope(child, focused, reverse, false)
1183 {
1184 return StepResult::Found(id);
1185 }
1186
1187 step_to_sibling(scope, from, reverse, is_root)
1188}
1189
1190fn step_to_sibling(scope: &ScopeNode, from: usize, reverse: bool, is_root: bool) -> StepResult {
1194 let n = scope.entries.len();
1195 let mut idx = from;
1196 for _ in 0..n {
1197 let next = if reverse {
1198 if idx == 0 {
1199 if scope_wraps(scope.policy, is_root) {
1200 n - 1
1201 } else {
1202 return StepResult::Escaped;
1203 }
1204 } else {
1205 idx - 1
1206 }
1207 } else if idx + 1 >= n {
1208 if scope_wraps(scope.policy, is_root) {
1209 0
1210 } else {
1211 return StepResult::Escaped;
1212 }
1213 } else {
1214 idx + 1
1215 };
1216
1217 match &scope.entries[next].kind {
1218 ScopeEntryKind::Focusable(id) => return StepResult::Found(*id),
1219 ScopeEntryKind::Scope(child) => {
1220 if let Some(id) = enter_scope_edge(child, reverse) {
1221 return StepResult::Found(id);
1222 }
1223 idx = next;
1225 }
1226 }
1227 }
1228 StepResult::Escaped
1229}
1230
1231#[cfg(test)]
1232mod tests {
1233 use super::*;
1234 use crate::test_widgets::FillWidget;
1235 use crate::widget_builder::WidgetBuilder;
1236
1237 #[test]
1246 fn a_reveal_walks_the_ancestors_of_whoever_owns_the_rect() {
1247 use crate::event::{EventResponse, ScrollAlign, ScrollMotion};
1248 use std::cell::Cell;
1249 use std::rc::Rc;
1250
1251 let asked = Rc::new(Cell::new(0usize));
1252 let mut tree = WidgetTree::new();
1253 let viewport = {
1255 let asked = asked.clone();
1256 tree.add(
1257 FillWidget::new()
1260 .on_scroll(move |event, _ctx| {
1261 if matches!(event, WidgetEvent::ScrollIntoView { .. }) {
1262 asked.set(asked.get() + 1);
1263 }
1264 EventResponse::Ignored
1265 })
1266 .clips_children(true),
1267 )
1268 };
1269 let inside = tree.add_child(viewport, FillWidget::new());
1270 let button = tree.add(FillWidget::new());
1272 tree.layout(SizeProposal::exact(400.0, 300.0));
1273
1274 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
1276 let mut ops = crate::window::NoopWindowOps;
1277
1278 tree.scroll_rect_into_view(
1279 button,
1280 target,
1281 0.0,
1282 ScrollAlign::Minimal,
1283 ScrollMotion::Instant,
1284 &mut ops,
1285 );
1286 assert_eq!(
1287 asked.get(),
1288 0,
1289 "walking from the asker never reaches a viewport it is not inside"
1290 );
1291
1292 tree.scroll_rect_into_view(
1293 inside,
1294 target,
1295 0.0,
1296 ScrollAlign::Minimal,
1297 ScrollMotion::Instant,
1298 &mut ops,
1299 );
1300 assert_eq!(
1301 asked.get(),
1302 1,
1303 "walking from the rect's owner reaches the viewport enclosing it"
1304 );
1305 }
1306
1307 fn recorder(
1310 tree: &mut WidgetTree,
1311 child: WidgetId,
1312 seen: std::rc::Rc<std::cell::RefCell<Vec<Rect>>>,
1313 ) -> WidgetId {
1314 use crate::event::EventResponse;
1315 use crate::test_widgets::StackWidget;
1316 tree.add(
1317 StackWidget::new()
1318 .child(child)
1319 .on_scroll(move |event, _ctx| {
1320 if let WidgetEvent::ScrollIntoView { target_bounds, .. } = event {
1321 seen.borrow_mut().push(*target_bounds);
1322 }
1323 EventResponse::Ignored
1324 })
1325 .clips_children(true),
1326 )
1327 }
1328
1329 #[test]
1340 fn a_reveal_projects_the_rect_as_it_climbs_out_of_a_content_transform() {
1341 use crate::event::{ScrollAlign, ScrollMotion};
1342 use std::cell::RefCell;
1343 use std::rc::Rc;
1344 use teksilo_canvas::Transform2D;
1345
1346 let outer_seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1347 let view_seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1348
1349 let mut tree = WidgetTree::new();
1350 let card = tree.add(FillWidget::new());
1351 let view = recorder(&mut tree, card, view_seen.clone());
1352 let _outer = recorder(&mut tree, view, outer_seen.clone());
1353 tree.layout(SizeProposal::exact(400.0, 300.0));
1354 tree.set_content_transform(view, Transform2D::translate(-90.0, 0.0));
1357
1358 let card_rect = Rect::new(100.0, 100.0, 50.0, 50.0);
1359 let mut ops = crate::window::NoopWindowOps;
1360 tree.scroll_rect_into_view(
1361 card,
1362 card_rect,
1363 0.0,
1364 ScrollAlign::Minimal,
1365 ScrollMotion::Instant,
1366 &mut ops,
1367 );
1368
1369 assert!(
1370 view_seen.borrow().is_empty(),
1371 "the card paints at window x = 10, inside the 400×300 viewport — \
1372 comparing its scene rect against the viewport is what used to \
1373 produce a spurious verdict here, in either direction"
1374 );
1375 assert!(
1376 outer_seen.borrow().is_empty(),
1377 "and nothing outside it has a reason to scroll either"
1378 );
1379
1380 tree.set_content_transform(view, Transform2D::translate(-600.0, 0.0));
1384 tree.scroll_rect_into_view(
1385 card,
1386 card_rect,
1387 0.0,
1388 ScrollAlign::Minimal,
1389 ScrollMotion::Instant,
1390 &mut ops,
1391 );
1392 assert_eq!(
1393 view_seen.borrow().as_slice(),
1394 &[card_rect],
1395 "the view is asked to reveal the card, in the view's own content \
1396 space — the space `SceneView::ensure_visible` takes by signature"
1397 );
1398 assert_eq!(
1399 outer_seen.borrow().as_slice(),
1400 &[Rect::new(-500.0, 100.0, 50.0, 50.0)],
1401 "and everything further out is asked in window space, which is the \
1402 only space it can compare against its own viewport"
1403 );
1404 }
1405
1406 #[test]
1409 fn a_reveal_with_no_transform_in_the_chain_passes_the_rect_through() {
1410 use crate::event::{ScrollAlign, ScrollMotion};
1411 use std::cell::RefCell;
1412 use std::rc::Rc;
1413
1414 let seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1415 let mut tree = WidgetTree::new();
1416 let inside = tree.add(FillWidget::new());
1417 let _viewport = recorder(&mut tree, inside, seen.clone());
1418 tree.layout(SizeProposal::exact(400.0, 300.0));
1419
1420 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
1421 let mut ops = crate::window::NoopWindowOps;
1422 tree.scroll_rect_into_view(
1423 inside,
1424 target,
1425 0.0,
1426 ScrollAlign::Minimal,
1427 ScrollMotion::Instant,
1428 &mut ops,
1429 );
1430 assert_eq!(seen.borrow().as_slice(), &[target]);
1431 }
1432
1433 #[test]
1434 fn focus_widget() {
1435 let mut tree = WidgetTree::new();
1436 let widget = tree.add(FillWidget::new());
1437 tree.layout(SizeProposal::exact(100.0, 50.0));
1438 tree.focus(widget);
1439 assert_eq!(tree.focused(), Some(widget));
1440 }
1441
1442 #[test]
1443 fn focus_change() {
1444 let mut tree = WidgetTree::new();
1445 let a = tree.add(FillWidget::new());
1446 let b = tree.add(FillWidget::new());
1447 tree.layout(SizeProposal::exact(100.0, 50.0));
1448 tree.focus(a);
1449 assert_eq!(tree.focused(), Some(a));
1450 tree.focus(b);
1451 assert_eq!(tree.focused(), Some(b));
1452 }
1453
1454 #[derive(Debug)]
1458 struct RebuildingRows {
1459 epoch: crate::signal::Signal<u64>,
1460 rows: Vec<WidgetId>,
1461 }
1462
1463 impl Widget for RebuildingRows {
1464 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1465 let sid = ctx.self_id();
1466 let reg = ctx.binding_registry();
1467 self.epoch
1468 .bind_to(sid, reg, crate::binding::BindingLevel::Rebuild);
1469 self.rows = (0..3)
1470 .map(|_| ctx.add(FillWidget::new().focusable()))
1471 .collect();
1472 self.rows.clone()
1473 }
1474
1475 fn layout_response(
1476 &self,
1477 proposal: SizeProposal,
1478 _ctx: &LayoutContext,
1479 ) -> crate::widget::LayoutResponse {
1480 proposal.resolve(0.0, 0.0).into()
1481 }
1482
1483 fn children(&self) -> Vec<WidgetId> {
1484 self.rows.clone()
1485 }
1486 }
1487
1488 #[test]
1498 fn a_rebuild_keeps_focus_inside_the_subtree_that_had_it() {
1499 let mut tree = WidgetTree::new();
1500 let epoch = crate::signal::Signal::new(0u64);
1501 let root = tree.add(RebuildingRows {
1502 epoch: epoch.clone(),
1503 rows: Vec::new(),
1504 });
1505 tree.layout(SizeProposal::exact(100.0, 60.0));
1506
1507 let first_row = tree
1508 .first_focusable_descendant(root)
1509 .expect("the rows are focusable");
1510 tree.focus(first_row);
1511 assert_eq!(tree.focused(), Some(first_row));
1512
1513 epoch.set(1);
1515 tree.layout(SizeProposal::exact(100.0, 60.0));
1516
1517 let focused = tree
1518 .focused()
1519 .expect("a rebuild must not drop focus out of the rebuilt subtree");
1520 assert_ne!(
1521 focused, first_row,
1522 "the old row is dead — focus must have moved to a freshly built one"
1523 );
1524 assert!(
1525 tree.is_descendant_of(focused, root),
1526 "focus must land back inside the rebuilt subtree"
1527 );
1528 assert!(tree.is_active(focused), "the focused node must be live");
1529 }
1530
1531 #[test]
1539 fn a_rebuild_restores_focus_to_the_position_it_had() {
1540 let mut tree = WidgetTree::new();
1541 let epoch = crate::signal::Signal::new(0u64);
1542 let root = tree.add(RebuildingRows {
1543 epoch: epoch.clone(),
1544 rows: Vec::new(),
1545 });
1546 tree.layout(SizeProposal::exact(100.0, 60.0));
1547 let third = tree.children(root)[2];
1548 tree.focus(third);
1549
1550 epoch.set(1);
1551 tree.layout(SizeProposal::exact(100.0, 60.0));
1552
1553 let rows = tree.children(root);
1554 assert_ne!(rows[2], third, "precondition: the rows were rebuilt");
1555 assert_eq!(tree.focused(), Some(rows[2]), "the third row again");
1556 assert_eq!(
1557 tree.focus_origin(),
1558 Some(crate::focus::FocusOrigin::Restored)
1559 );
1560 }
1561
1562 #[test]
1567 fn a_rebuild_elsewhere_does_not_steal_focus() {
1568 let mut tree = WidgetTree::new();
1569 let epoch = crate::signal::Signal::new(0u64);
1570 let outsider = tree.add(FillWidget::new().focusable());
1571 let _rows = tree.add(RebuildingRows {
1572 epoch: epoch.clone(),
1573 rows: Vec::new(),
1574 });
1575 tree.layout(SizeProposal::exact(100.0, 60.0));
1576
1577 tree.focus(outsider);
1578 assert_eq!(tree.focused(), Some(outsider));
1579
1580 epoch.set(1);
1581 tree.layout(SizeProposal::exact(100.0, 60.0));
1582
1583 assert_eq!(
1584 tree.focused(),
1585 Some(outsider),
1586 "a rebuild that never held focus must not pull focus into itself"
1587 );
1588 }
1589
1590 #[derive(Debug)]
1594 struct KeepingGroups {
1595 epoch: crate::signal::Signal<u64>,
1596 group_epochs: Vec<crate::signal::Signal<u64>>,
1597 groups: Vec<WidgetId>,
1598 }
1599
1600 impl Widget for KeepingGroups {
1601 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1602 let sid = ctx.self_id();
1603 let reg = ctx.binding_registry();
1604 self.epoch
1605 .bind_to(sid, reg, crate::binding::BindingLevel::Rebuild);
1606 if self.groups.is_empty() {
1607 self.groups = self
1608 .group_epochs
1609 .iter()
1610 .map(|epoch| {
1611 ctx.add(RebuildingRows {
1612 epoch: epoch.clone(),
1613 rows: Vec::new(),
1614 })
1615 })
1616 .collect();
1617 }
1618 self.groups.clone()
1619 }
1620
1621 fn layout_response(
1622 &self,
1623 proposal: SizeProposal,
1624 _ctx: &LayoutContext,
1625 ) -> crate::widget::LayoutResponse {
1626 proposal.resolve(0.0, 0.0).into()
1627 }
1628
1629 fn children(&self) -> Vec<WidgetId> {
1630 self.groups.clone()
1631 }
1632
1633 fn preserves_children_on_rebuild(&self) -> bool {
1634 true
1635 }
1636 }
1637
1638 #[test]
1649 fn a_rebuild_restores_focus_into_the_innermost_ancestor_that_survived() {
1650 let mut tree = WidgetTree::new();
1651 let epoch = crate::signal::Signal::new(0u64);
1652 let group_epochs = vec![
1653 crate::signal::Signal::new(0u64),
1654 crate::signal::Signal::new(0u64),
1655 ];
1656 let root = tree.add(KeepingGroups {
1657 epoch: epoch.clone(),
1658 group_epochs: group_epochs.clone(),
1659 groups: Vec::new(),
1660 });
1661 tree.layout(SizeProposal::exact(100.0, 60.0));
1662
1663 let groups = tree.children(root);
1664 let second_group = groups[1];
1665 let row = tree
1666 .first_focusable_descendant(second_group)
1667 .expect("the second group's rows are focusable");
1668 tree.focus(row);
1669
1670 epoch.set(1);
1673 group_epochs[1].set(1);
1674 tree.layout(SizeProposal::exact(100.0, 60.0));
1675
1676 assert_eq!(
1677 tree.children(root),
1678 groups,
1679 "the container kept both groups (the premise of this test)"
1680 );
1681 let focused = tree.focused().expect("focus must not be dropped");
1682 assert_ne!(focused, row, "the old row is dead");
1683 assert!(
1684 tree.is_descendant_of(focused, second_group),
1685 "focus must land back in the group that held it, the innermost \
1686 ancestor that survived, not in the first group of the container"
1687 );
1688 }
1689
1690 #[test]
1691 fn first_focusable_descendant_prefers_first_focusable_child() {
1692 let mut tree = WidgetTree::new();
1693 let a = tree.add(FillWidget::new().focusable());
1694 let _not_focusable = tree.add(FillWidget::new());
1695 let b = tree.add(FillWidget::new().focusable());
1696 let root = tree.add(crate::test_widgets::StackWidget::new().child(a).child(b));
1697 tree.layout(SizeProposal::exact(100.0, 50.0));
1698
1699 assert_eq!(tree.first_focusable_descendant(root), Some(a));
1700 }
1701
1702 #[test]
1703 fn tab_cycles_through_focusable_widgets() {
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 assert_eq!(tree.focused(), None);
1711
1712 tree.press_key(Key::Tab, Modifiers::NONE);
1713 assert_eq!(tree.focused(), Some(a));
1714
1715 tree.press_key(Key::Tab, Modifiers::NONE);
1716 assert_eq!(tree.focused(), Some(b));
1717
1718 tree.press_key(Key::Tab, Modifiers::NONE);
1719 assert_eq!(tree.focused(), Some(c));
1720
1721 tree.press_key(Key::Tab, Modifiers::NONE);
1722 assert_eq!(tree.focused(), Some(a));
1723 }
1724
1725 #[test]
1726 fn tab_stop_on_composite_excludes_focusable_descendant() {
1727 let mut tree = WidgetTree::new();
1732 let leaf = tree.add(FillWidget::new().focusable());
1733 let composite = tree.add(crate::test_widgets::StackWidget::new().child(leaf));
1734 let other = tree.add(FillWidget::new().focusable());
1735 tree.layout(SizeProposal::exact(100.0, 50.0));
1736
1737 tree.set_tab_stop(composite, false);
1738
1739 tree.press_key(Key::Tab, Modifiers::NONE);
1740 assert_eq!(
1741 tree.focused(),
1742 Some(other),
1743 "Tab must skip the suppressed composite's inner leaf"
1744 );
1745 tree.press_key(Key::Tab, Modifiers::NONE);
1746 assert_eq!(
1747 tree.focused(),
1748 Some(other),
1749 "only the un-suppressed control participates in Tab"
1750 );
1751
1752 tree.set_tab_stop(composite, true);
1754 tree.set_focused(None);
1755 tree.press_key(Key::Tab, Modifiers::NONE);
1756 let first = tree.focused();
1757 tree.press_key(Key::Tab, Modifiers::NONE);
1758 let second = tree.focused();
1759 assert_ne!(
1760 first, second,
1761 "with the composite re-enabled, Tab visits both controls"
1762 );
1763 }
1764
1765 #[test]
1766 fn shift_tab_cycles_backwards() {
1767 let mut tree = WidgetTree::new();
1768 let a = tree.add(FillWidget::new().focusable());
1769 let b = tree.add(FillWidget::new().focusable());
1770 let c = tree.add(FillWidget::new().focusable());
1771 tree.layout(SizeProposal::exact(100.0, 50.0));
1772
1773 tree.press_key(Key::Tab, Modifiers::NONE);
1774 assert_eq!(tree.focused(), Some(a));
1775
1776 tree.press_key(Key::Tab, Modifiers::SHIFT);
1777 assert_eq!(tree.focused(), Some(c));
1778
1779 tree.press_key(Key::Tab, Modifiers::SHIFT);
1780 assert_eq!(tree.focused(), Some(b));
1781 }
1782
1783 #[test]
1784 fn tab_skips_non_focusable_widgets() {
1785 let mut tree = WidgetTree::new();
1786 let _not_focusable = tree.add(FillWidget::new());
1787 let a = tree.add(FillWidget::new().focusable());
1788 let _also_not = tree.add(FillWidget::new());
1789 let b = tree.add(FillWidget::new().focusable());
1790 tree.layout(SizeProposal::exact(100.0, 50.0));
1791
1792 tree.press_key(Key::Tab, Modifiers::NONE);
1793 assert_eq!(tree.focused(), Some(a));
1794
1795 tree.press_key(Key::Tab, Modifiers::NONE);
1796 assert_eq!(tree.focused(), Some(b));
1797 }
1798
1799 #[test]
1810 fn focus_does_not_check_that_the_node_is_focusable() {
1811 let mut tree = WidgetTree::new();
1812 let inert = tree.add(FillWidget::new()); tree.layout(SizeProposal::exact(100.0, 50.0));
1814 assert!(
1815 tree.tab_stops_within(inert).is_empty(),
1816 "the subject has to be unreachable for the point to be made"
1817 );
1818
1819 tree.focus(inert);
1820 assert_eq!(
1821 tree.focused(),
1822 Some(inert),
1823 "focus lands on a node Tab traversal can never offer"
1824 );
1825 }
1826
1827 #[test]
1828 fn tab_focus_has_keyboard_origin() {
1829 let mut tree = WidgetTree::new();
1830 tree.add(FillWidget::new().focusable());
1831 tree.layout(SizeProposal::exact(100.0, 50.0));
1832
1833 tree.press_key(Key::Tab, Modifiers::NONE);
1834 assert_eq!(
1835 tree.focus_origin(),
1836 Some(crate::focus::FocusOrigin::Keyboard)
1837 );
1838 }
1839
1840 #[test]
1841 fn tab_skips_focusable_inside_disabled_ancestor() {
1842 use crate::signal::Signal;
1843 use crate::test_widgets::StackWidget;
1844
1845 let mut tree = WidgetTree::new();
1846 let a = tree.add(FillWidget::new().focusable());
1847 let inner = tree.add(FillWidget::new().focusable());
1848 let disabled_container = tree.add(StackWidget::new().child(inner));
1849 let c = tree.add(FillWidget::new().focusable());
1850 tree.enabled_when(disabled_container, Signal::new(false));
1851 tree.layout(SizeProposal::exact(200.0, 100.0));
1852
1853 tree.press_key(Key::Tab, Modifiers::NONE);
1854 assert_eq!(tree.focused(), Some(a));
1855
1856 tree.press_key(Key::Tab, Modifiers::NONE);
1857 assert_eq!(
1858 tree.focused(),
1859 Some(c),
1860 "tab should skip the focusable widget nested inside the disabled container"
1861 );
1862 }
1863
1864 #[test]
1865 fn dormant_widget_not_in_focus_cycle() {
1866 let mut tree = WidgetTree::new();
1867 let a = tree.add(FillWidget::new().focusable());
1868 let b = tree.add(FillWidget::new().focusable());
1869 let c = tree.add(FillWidget::new().focusable());
1870 tree.layout(SizeProposal::exact(200.0, 100.0));
1871
1872 tree.focus(a);
1873 tree.set_dormant(b);
1874
1875 tree.press_key(Key::Tab, Modifiers::NONE);
1876 assert_eq!(tree.focused(), Some(c));
1877 }
1878
1879 #[test]
1884 fn revalidate_delivers_focus_lost_when_focused_widget_goes_dormant() {
1885 use std::cell::Cell;
1886 use std::rc::Rc;
1887
1888 let lost = Rc::new(Cell::new(0_u32));
1889 let gained = Rc::new(Cell::new(0_u32));
1890 let lost_c = lost.clone();
1891 let gained_c = gained.clone();
1892
1893 let mut tree = WidgetTree::new();
1894 let editor = tree.add(
1895 FillWidget::new()
1896 .focusable()
1897 .on_focus(move |is_gained, _ctx| {
1898 if is_gained {
1899 gained_c.set(gained_c.get() + 1);
1900 } else {
1901 lost_c.set(lost_c.get() + 1);
1902 }
1903 }),
1904 );
1905 let _other = tree.add(FillWidget::new().focusable());
1906 tree.layout(SizeProposal::exact(200.0, 100.0));
1907
1908 tree.focus(editor);
1909 assert_eq!(tree.focused(), Some(editor));
1910 assert_eq!(gained.get(), 1, "focus() delivers FocusGained");
1911 assert_eq!(lost.get(), 0);
1912
1913 tree.set_dormant(editor);
1915 let mut noop = crate::window::NoopWindowOps;
1917 tree.revalidate_interaction_state(&mut noop);
1918
1919 assert_eq!(
1920 tree.focused(),
1921 None,
1922 "tree focus must clear when the target is dormant"
1923 );
1924 assert_eq!(
1925 lost.get(),
1926 1,
1927 "FocusLost must reach the dormant widget so it can clear has_focus / caret blink"
1928 );
1929 }
1930
1931 #[test]
1932 fn tab_cycles_focus_in_tree_order() {
1933 let mut tree = WidgetTree::new();
1934 let a = tree.add(FillWidget::new().focusable());
1935 let b = tree.add(FillWidget::new().focusable());
1936 tree.layout(SizeProposal::exact(200.0, 80.0));
1937
1938 tree.focus(a);
1939 assert_eq!(tree.focused(), Some(a));
1940
1941 tree.press_key(Key::Tab, Modifiers::NONE);
1942 assert_eq!(tree.focused(), Some(b));
1943
1944 tree.press_key(Key::Tab, Modifiers::SHIFT);
1945 assert_eq!(tree.focused(), Some(a));
1946 }
1947
1948 #[test]
1949 fn focus_survives_theme_switch() {
1950 let mut tree = WidgetTree::new();
1951 let a = tree.add(FillWidget::new().focusable());
1952 let _b = tree.add(FillWidget::new().focusable());
1953 tree.layout(SizeProposal::exact(200.0, 80.0));
1954
1955 tree.focus(a);
1956 assert_eq!(tree.focused(), Some(a));
1957
1958 tree.set_theme(crate::presets::intui::dark());
1959 tree.layout(SizeProposal::exact(200.0, 80.0));
1960
1961 assert_eq!(
1962 tree.focused(),
1963 Some(a),
1964 "theme switch must not clobber focus"
1965 );
1966 }
1967
1968 #[test]
1969 fn focus_survives_locale_switch() {
1970 let mut tree = WidgetTree::new();
1971 let a = tree.add(FillWidget::new().focusable());
1972 tree.layout(SizeProposal::exact(200.0, 80.0));
1973
1974 tree.focus(a);
1975 tree.set_locale("fr-FR".to_string());
1976 tree.layout(SizeProposal::exact(200.0, 80.0));
1977
1978 assert_eq!(
1979 tree.focused(),
1980 Some(a),
1981 "locale switch must not clobber focus"
1982 );
1983 }
1984
1985 #[test]
1988 fn focus_within_flips_when_descendant_takes_focus() {
1989 use crate::signal::Signal;
1990 use crate::test_widgets::StackWidget;
1991 use crate::widget_builder::WidgetBuilder;
1992
1993 let halo = Signal::new(false);
1994
1995 let mut tree = WidgetTree::new();
1996 let leaf = tree.add(FillWidget::new().focusable());
1997 let mid = tree.add(StackWidget::new().child(leaf));
1998 let _outer = tree.add(StackWidget::new().child(mid).focus_within(halo.clone()));
1999
2000 tree.layout(SizeProposal::exact(100.0, 50.0));
2001 assert!(!halo.get(), "no focus yet → signal is false");
2002
2003 tree.focus(leaf);
2004 assert!(halo.get(), "leaf now focused, outer is its strict ancestor");
2005 }
2006
2007 #[test]
2008 fn focus_within_strict_ancestors_only() {
2009 use crate::signal::Signal;
2012 use crate::widget_builder::WidgetBuilder;
2013
2014 let halo = Signal::new(false);
2015
2016 let mut tree = WidgetTree::new();
2017 let widget = tree.add(FillWidget::new().focusable().focus_within(halo.clone()));
2018 tree.layout(SizeProposal::exact(100.0, 50.0));
2019 tree.focus(widget);
2020
2021 assert!(
2022 !halo.get(),
2023 "focusing the widget itself must not set its own focus_within"
2024 );
2025 }
2026
2027 #[test]
2028 fn view_focus_active_is_inclusive_for_the_view_itself() {
2029 use crate::test_widgets::StackWidget;
2035 use crate::widget_builder::WidgetBuilder;
2036
2037 let mut tree = WidgetTree::new();
2038 let item = tree.add(FillWidget::new()); let view = tree.add(StackWidget::new().child(item).focusable(true));
2040 let outside = tree.add(FillWidget::new().focusable());
2041 tree.layout(SizeProposal::exact(100.0, 50.0));
2042
2043 let active = tree.view_focus_active_for(item);
2044 assert!(!active.get(), "no focus yet → scope inactive");
2045
2046 tree.focus(view);
2047 assert!(
2048 active.get(),
2049 "view focused directly → scope active (focus_within would be false here)"
2050 );
2051
2052 tree.focus(outside);
2053 assert!(
2054 !active.get(),
2055 "focus moved outside the view → scope inactive"
2056 );
2057 }
2058
2059 #[test]
2060 fn begin_view_focus_for_keys_on_the_view_root_not_the_building_pane() {
2061 use crate::test_widgets::StackWidget;
2069 use crate::widget_builder::WidgetBuilder;
2070
2071 let mut tree = WidgetTree::new();
2072 let row = tree.add(FillWidget::new()); let pane = tree.add(StackWidget::new().child(row)); let root = tree.add(StackWidget::new().child(pane).focusable(true));
2075 tree.layout(SizeProposal::exact(100.0, 50.0));
2076
2077 let keyed_on_root = tree.begin_view_focus(root);
2079 tree.end_view_focus();
2080 let keyed_on_pane = tree.view_focus_signal_for(pane);
2082
2083 assert!(!keyed_on_root.get(), "no focus yet → inactive");
2084 tree.focus(root);
2085 assert!(keyed_on_root.get(), "view root focused → row scope active");
2086 assert!(
2087 !keyed_on_pane.get(),
2088 "pane-keyed scope stays false: the focused root is the pane's ancestor, not its descendant",
2089 );
2090 }
2091
2092 #[test]
2093 fn focus_visible_tracks_input_modality() {
2094 use crate::event::{Key, Modifiers};
2097 let mut tree = WidgetTree::new();
2098 let w = tree.add(FillWidget::new().focusable());
2099 tree.layout(SizeProposal::exact(100.0, 50.0));
2100 let vis = tree.focus_visible_signal();
2101 assert!(!vis.get(), "starts not focus-visible");
2102
2103 tree.press_key(Key::Tab, Modifiers::NONE);
2104 assert!(vis.get(), "keyboard input turns focus-visible ON");
2105
2106 tree.click(w);
2107 assert!(!vis.get(), "pointer input turns focus-visible OFF");
2108
2109 tree.press_key(Key::ArrowDown, Modifiers::NONE);
2110 assert!(vis.get(), "keyboard input turns it back ON");
2111 }
2112
2113 #[test]
2114 fn view_focus_active_without_focusable_ancestor_is_constant_true() {
2115 let mut tree = WidgetTree::new();
2118 let item = tree.add(FillWidget::new());
2119 tree.layout(SizeProposal::exact(100.0, 50.0));
2120 assert!(tree.view_focus_active_for(item).get());
2121 }
2122
2123 #[test]
2124 fn focus_within_diff_across_siblings() {
2125 use crate::signal::Signal;
2128 use crate::test_widgets::StackWidget;
2129 use crate::widget_builder::WidgetBuilder;
2130
2131 let sig_a = Signal::new(false);
2132 let sig_b = Signal::new(false);
2133
2134 let mut tree = WidgetTree::new();
2135 let leaf_a = tree.add(FillWidget::new().focusable());
2136 let leaf_b = tree.add(FillWidget::new().focusable());
2137 let mid_a = tree.add(StackWidget::new().child(leaf_a).focus_within(sig_a.clone()));
2138 let mid_b = tree.add(StackWidget::new().child(leaf_b).focus_within(sig_b.clone()));
2139 let _root = tree.add(StackWidget::new().child(mid_a).child(mid_b));
2140
2141 tree.layout(SizeProposal::exact(100.0, 50.0));
2142 tree.focus(leaf_a);
2143 assert!(sig_a.get(), "sig_a true after focusing leaf_a");
2144 assert!(!sig_b.get(), "sig_b false: leaf_a is not its descendant");
2145
2146 tree.focus(leaf_b);
2147 assert!(!sig_a.get(), "sig_a flipped to false on focus move");
2148 assert!(sig_b.get(), "sig_b flipped to true on focus move");
2149 }
2150
2151 #[test]
2152 fn focus_within_clears_when_focused_widget_destroyed() {
2153 use crate::signal::Signal;
2154 use crate::test_widgets::StackWidget;
2155 use crate::widget_builder::WidgetBuilder;
2156
2157 let halo = Signal::new(false);
2158
2159 let mut tree = WidgetTree::new();
2160 let leaf = tree.add(FillWidget::new().focusable());
2161 let _outer = tree.add(StackWidget::new().child(leaf).focus_within(halo.clone()));
2162
2163 tree.layout(SizeProposal::exact(100.0, 50.0));
2164 tree.focus(leaf);
2165 assert!(halo.get());
2166
2167 tree.destroy_subtree(leaf);
2168 assert!(
2169 !halo.get(),
2170 "destroying the focused widget must clear focus_within on its ancestors"
2171 );
2172 }
2173
2174 #[test]
2175 fn hover_within_flips_via_pointer_move() {
2176 use crate::signal::Signal;
2177 use crate::test_widgets::StackWidget;
2178 use crate::widget_builder::WidgetBuilder;
2179 use teksilo_canvas::Point;
2180
2181 let glow = Signal::new(false);
2182
2183 let mut tree = WidgetTree::new();
2184 let leaf = tree.add(FillWidget::new());
2185 let _outer = tree.add(StackWidget::new().child(leaf).hover_within(glow.clone()));
2186
2187 tree.layout(SizeProposal::exact(100.0, 50.0));
2188 assert!(!glow.get());
2189
2190 tree.pointer_move(Point::new(50.0, 25.0));
2191 assert!(glow.get(), "pointer over leaf → outer.hover_within = true");
2192
2193 tree.pointer_move(Point::new(500.0, 500.0));
2194 assert!(
2195 !glow.get(),
2196 "pointer moves outside the tree → hover_within clears"
2197 );
2198 }
2199
2200 #[test]
2201 fn hover_within_strict_ancestors_only() {
2202 use crate::signal::Signal;
2203 use crate::widget_builder::WidgetBuilder;
2204 use teksilo_canvas::Point;
2205
2206 let glow = Signal::new(false);
2207
2208 let mut tree = WidgetTree::new();
2209 let _w = tree.add(FillWidget::new().hover_within(glow.clone()));
2210 tree.layout(SizeProposal::exact(100.0, 50.0));
2211 tree.pointer_move(Point::new(50.0, 25.0));
2212
2213 assert!(
2214 !glow.get(),
2215 "hovering the widget itself must not set its own hover_within"
2216 );
2217 }
2218}
2219
2220#[cfg(test)]
2221mod tests_scope {
2222 use super::*;
2227 use crate::focus::TraversalScopePolicy::{Continue, Cycle};
2228 use crate::test_widgets::{FillWidget, StackWidget};
2229 use crate::widget_builder::WidgetBuilder;
2230
2231 fn indexed(tree: &mut WidgetTree, idx: i32) -> WidgetId {
2233 tree.add(FillWidget::new().focusable().tab_index(idx))
2234 }
2235
2236 fn tab(tree: &mut WidgetTree) {
2237 tree.press_key(Key::Tab, Modifiers::NONE);
2238 }
2239 fn shift_tab(tree: &mut WidgetTree) {
2240 tree.press_key(Key::Tab, Modifiers::SHIFT);
2241 }
2242
2243 #[test]
2244 fn overlapping_tab_index_in_sibling_scopes_does_not_interleave() {
2245 let mut tree = WidgetTree::new();
2246 let a1 = indexed(&mut tree, 1);
2247 let a2 = indexed(&mut tree, 2);
2248 let scope_a = tree.add(StackWidget::new().child(a1).child(a2));
2249 let b1 = indexed(&mut tree, 1);
2250 let b2 = indexed(&mut tree, 2);
2251 let scope_b = tree.add(StackWidget::new().child(b1).child(b2));
2252 let _root = tree.add(StackWidget::new().child(scope_a).child(scope_b));
2253 tree.set_traversal_scope(scope_a, Continue);
2254 tree.set_traversal_scope(scope_b, Continue);
2255 tree.layout(SizeProposal::exact(100.0, 100.0));
2256
2257 for expected in [a1, a2, b1, b2, a1] {
2259 tab(&mut tree);
2260 assert_eq!(tree.focused(), Some(expected));
2261 }
2262 }
2263
2264 #[test]
2265 fn continue_scope_flows_out_at_the_ends() {
2266 let mut tree = WidgetTree::new();
2268 let a = tree.add(FillWidget::new().focusable());
2269 let c1 = tree.add(FillWidget::new().focusable());
2270 let c2 = tree.add(FillWidget::new().focusable());
2271 let scope_c = tree.add(StackWidget::new().child(c1).child(c2));
2272 let b = tree.add(FillWidget::new().focusable());
2273 let _root = tree.add(StackWidget::new().child(a).child(scope_c).child(b));
2274 tree.set_traversal_scope(scope_c, Continue);
2275 tree.layout(SizeProposal::exact(100.0, 100.0));
2276
2277 for expected in [a, c1, c2, b, a] {
2278 tab(&mut tree);
2279 assert_eq!(tree.focused(), Some(expected));
2280 }
2281 tree.focus(c1);
2283 shift_tab(&mut tree);
2284 assert_eq!(tree.focused(), Some(a));
2285 }
2286
2287 #[test]
2288 fn cycle_scope_wraps_and_never_escapes() {
2289 let mut tree = WidgetTree::new();
2291 let a = tree.add(FillWidget::new().focusable());
2292 let d1 = tree.add(FillWidget::new().focusable());
2293 let d2 = tree.add(FillWidget::new().focusable());
2294 let scope_d = tree.add(StackWidget::new().child(d1).child(d2));
2295 let _root = tree.add(StackWidget::new().child(a).child(scope_d));
2296 tree.set_traversal_scope(scope_d, Cycle);
2297 tree.layout(SizeProposal::exact(100.0, 100.0));
2298
2299 tree.focus(d1);
2300 for _ in 0..10 {
2301 tab(&mut tree);
2302 let f = tree.focused();
2303 assert!(
2304 f == Some(d1) || f == Some(d2),
2305 "Cycle scope must trap Tab inside {{d1,d2}}, got {f:?}"
2306 );
2307 }
2308 tree.focus(d1);
2310 tab(&mut tree);
2311 assert_eq!(tree.focused(), Some(d2));
2312 shift_tab(&mut tree);
2313 assert_eq!(tree.focused(), Some(d1));
2314 shift_tab(&mut tree);
2315 assert_eq!(tree.focused(), Some(d2), "Shift+Tab at start wraps to last");
2316 }
2317
2318 #[test]
2319 fn empty_scope_is_skipped() {
2320 let mut tree = WidgetTree::new();
2322 let a = tree.add(FillWidget::new().focusable());
2323 let empty = tree.add(StackWidget::new());
2324 let b = tree.add(FillWidget::new().focusable());
2325 let _root = tree.add(StackWidget::new().child(a).child(empty).child(b));
2326 tree.set_traversal_scope(empty, Continue);
2327 tree.layout(SizeProposal::exact(100.0, 100.0));
2328
2329 for expected in [a, b, a] {
2330 tab(&mut tree);
2331 assert_eq!(tree.focused(), Some(expected));
2332 }
2333 }
2334
2335 #[test]
2336 fn single_member_cycle_scope_stays_put() {
2337 let mut tree = WidgetTree::new();
2338 let e = tree.add(FillWidget::new().focusable());
2339 let scope_e = tree.add(StackWidget::new().child(e));
2340 tree.set_traversal_scope(scope_e, Cycle);
2341 tree.layout(SizeProposal::exact(100.0, 50.0));
2342
2343 tree.focus(e);
2344 tab(&mut tree);
2345 assert_eq!(tree.focused(), Some(e));
2346 shift_tab(&mut tree);
2347 assert_eq!(tree.focused(), Some(e));
2348 }
2349
2350 #[test]
2351 fn nested_continue_in_cycle_flows_out_to_outer() {
2352 let mut tree = WidgetTree::new();
2354 let x = tree.add(FillWidget::new().focusable());
2355 let i1 = tree.add(FillWidget::new().focusable());
2356 let i2 = tree.add(FillWidget::new().focusable());
2357 let inner = tree.add(StackWidget::new().child(i1).child(i2));
2358 let y = tree.add(FillWidget::new().focusable());
2359 let outer = tree.add(StackWidget::new().child(x).child(inner).child(y));
2360 tree.set_traversal_scope(inner, Continue);
2361 tree.set_traversal_scope(outer, Cycle);
2362 tree.layout(SizeProposal::exact(100.0, 100.0));
2363
2364 for expected in [x, i1, i2, y, x] {
2365 tab(&mut tree);
2366 assert_eq!(tree.focused(), Some(expected));
2367 }
2368 tree.focus(i1);
2370 shift_tab(&mut tree);
2371 assert_eq!(tree.focused(), Some(x));
2372 }
2373
2374 #[test]
2375 fn nested_cycle_in_cycle_traps_in_the_inner_scope() {
2376 let mut tree = WidgetTree::new();
2379 let x = tree.add(FillWidget::new().focusable());
2380 let i1 = tree.add(FillWidget::new().focusable());
2381 let i2 = tree.add(FillWidget::new().focusable());
2382 let inner = tree.add(StackWidget::new().child(i1).child(i2));
2383 let y = tree.add(FillWidget::new().focusable());
2384 let outer = tree.add(StackWidget::new().child(x).child(inner).child(y));
2385 tree.set_traversal_scope(inner, Cycle);
2386 tree.set_traversal_scope(outer, Cycle);
2387 tree.layout(SizeProposal::exact(100.0, 100.0));
2388
2389 tree.focus(i1);
2390 for _ in 0..6 {
2391 tab(&mut tree);
2392 let f = tree.focused();
2393 assert!(
2394 f == Some(i1) || f == Some(i2),
2395 "inner Cycle must trap Tab; reached {f:?}"
2396 );
2397 }
2398 }
2399
2400 #[test]
2401 fn scoped_tab_index_orders_within_a_scope() {
2402 let mut tree = WidgetTree::new();
2404 let f3 = indexed(&mut tree, 3);
2405 let f1 = indexed(&mut tree, 1);
2406 let f2 = indexed(&mut tree, 2);
2407 let scope_f = tree.add(StackWidget::new().child(f3).child(f1).child(f2));
2408 tree.set_traversal_scope(scope_f, Cycle);
2409 tree.layout(SizeProposal::exact(100.0, 100.0));
2410
2411 for expected in [f1, f2, f3, f1] {
2412 tab(&mut tree);
2413 assert_eq!(tree.focused(), Some(expected));
2414 }
2415 }
2416
2417 #[test]
2418 fn destroyed_focused_widget_re_enters_at_first() {
2419 let mut tree = WidgetTree::new();
2420 let a = tree.add(FillWidget::new().focusable());
2421 let b = tree.add(FillWidget::new().focusable());
2422 tree.layout(SizeProposal::exact(100.0, 50.0));
2423
2424 tree.focus(b);
2425 tree.destroy_subtree(b);
2426 tab(&mut tree);
2427 assert_eq!(tree.focused(), Some(a), "Tab after destroy enters at first");
2428 }
2429
2430 #[test]
2431 fn set_traversal_scope_forces_node_non_focusable() {
2432 let mut tree = WidgetTree::new();
2435 let inner = tree.add(FillWidget::new().focusable());
2436 let scope = tree.add(StackWidget::new().child(inner).focusable(true));
2437 tree.set_traversal_scope(scope, Continue);
2438 tree.layout(SizeProposal::exact(100.0, 50.0));
2439
2440 tab(&mut tree);
2442 assert_eq!(tree.focused(), Some(inner));
2443 tab(&mut tree);
2444 assert_eq!(tree.focused(), Some(inner));
2445 }
2446
2447 #[test]
2448 fn no_scopes_behaves_like_a_flat_cycle() {
2449 let mut tree = WidgetTree::new();
2452 let a = indexed(&mut tree, 2);
2453 let b = indexed(&mut tree, 1);
2454 let c = tree.add(FillWidget::new().focusable());
2455 tree.layout(SizeProposal::exact(100.0, 50.0));
2456
2457 for expected in [b, a, c, b] {
2459 tab(&mut tree);
2460 assert_eq!(tree.focused(), Some(expected));
2461 }
2462 }
2463
2464 #[test]
2465 fn centered_modal_confines_tab_to_its_content() {
2466 let mut tree = WidgetTree::new();
2469 let outside1 = tree.add(FillWidget::new().focusable());
2470 let outside2 = tree.add(FillWidget::new().focusable());
2471 let m1 = tree.add(FillWidget::new().focusable());
2472 let m2 = tree.add(FillWidget::new().focusable());
2473 let content = tree.add(StackWidget::new().child(m1).child(m2));
2474 tree.layout(SizeProposal::exact(200.0, 100.0));
2475
2476 tree.show_overlay(crate::overlay::OverlayRequest::new(
2477 content,
2478 outside1,
2479 crate::overlay::OverlayPlacement::Centered,
2480 crate::overlay::DismissBehavior::Manual,
2481 ));
2482
2483 for _ in 0..6 {
2484 tab(&mut tree);
2485 let f = tree.focused();
2486 assert!(
2487 f == Some(m1) || f == Some(m2),
2488 "modal must trap Tab inside its content, reached {f:?}"
2489 );
2490 }
2491 assert_ne!(tree.focused(), Some(outside1));
2492 assert_ne!(tree.focused(), Some(outside2));
2493 }
2494}
2495
2496#[cfg(test)]
2497mod tests_focus_out_dismissal {
2498 use super::*;
2505 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
2506 use crate::test_widgets::{FillWidget, StackWidget};
2507
2508 fn tab(tree: &mut WidgetTree) {
2509 tree.press_key(Key::Tab, Modifiers::NONE);
2510 }
2511
2512 fn show_anchored(
2514 tree: &mut WidgetTree,
2515 content: WidgetId,
2516 anchor: WidgetId,
2517 parent: Option<crate::overlay::OverlayId>,
2518 ) -> crate::overlay::OverlayId {
2519 let mut request = OverlayRequest::new(
2520 content,
2521 anchor,
2522 OverlayPlacement::Below,
2523 DismissBehavior::EscapeOrClickOutside,
2524 );
2525 request.parent_overlay = parent;
2526 tree.show_overlay(request)
2527 }
2528
2529 #[test]
2530 fn tab_out_of_a_non_modal_overlay_dismisses_it() {
2531 let mut tree = WidgetTree::new();
2532 let anchor = tree.add(FillWidget::new().focusable());
2533 let after = tree.add(FillWidget::new().focusable());
2534 let inner = tree.add(FillWidget::new().focusable());
2535 let content = tree.add(StackWidget::new().child(inner));
2536 tree.layout(SizeProposal::exact(200.0, 100.0));
2537 show_anchored(&mut tree, content, anchor, None);
2538
2539 tree.focus(inner);
2540 tab(&mut tree);
2541
2542 assert_ne!(tree.focused(), Some(inner), "focus genuinely left");
2543 assert!(
2544 tree.active_overlays().is_empty(),
2545 "an overlay must not stay open over the focus ring that left it"
2546 );
2547 assert!(tree.focused() == Some(after) || tree.focused() == Some(anchor));
2548 }
2549
2550 #[test]
2552 fn a_centered_modal_is_never_dismissed_by_focus_moving() {
2553 let mut tree = WidgetTree::new();
2554 let outside = tree.add(FillWidget::new().focusable());
2555 let m1 = tree.add(FillWidget::new().focusable());
2556 let content = tree.add(StackWidget::new().child(m1));
2557 tree.layout(SizeProposal::exact(200.0, 100.0));
2558 tree.show_overlay(OverlayRequest::new(
2559 content,
2560 outside,
2561 OverlayPlacement::Centered,
2562 DismissBehavior::Manual,
2563 ));
2564
2565 tree.focus(m1);
2566 tree.focus(outside);
2570
2571 assert_eq!(
2572 tree.active_overlays().len(),
2573 1,
2574 "a modal is the one overlay that legitimately contains focus"
2575 );
2576 }
2577
2578 #[test]
2582 fn the_modal_scrim_survives_focus_moving_inside_the_modal() {
2583 let mut tree = WidgetTree::new();
2584 let opener = tree.add(FillWidget::new().focusable());
2585 let m1 = tree.add(FillWidget::new().focusable());
2586 let m2 = tree.add(FillWidget::new().focusable());
2587 let scrim = tree.add(FillWidget::new());
2588 let content = tree.add(StackWidget::new().child(m1).child(m2));
2589 tree.layout(SizeProposal::exact(200.0, 100.0));
2590
2591 tree.show_overlay(OverlayRequest::new(
2592 scrim,
2593 opener,
2594 OverlayPlacement::FullViewport,
2595 DismissBehavior::Manual,
2596 ));
2597 tree.show_overlay(OverlayRequest::new(
2598 content,
2599 opener,
2600 OverlayPlacement::Centered,
2601 DismissBehavior::Manual,
2602 ));
2603
2604 tree.focus(opener);
2605 tree.focus(m1);
2606 tab(&mut tree);
2607
2608 assert_eq!(
2609 tree.active_overlays().len(),
2610 2,
2611 "scrim and modal both stand while focus moves within the modal"
2612 );
2613 }
2614
2615 #[test]
2618 fn focus_moving_into_a_child_overlay_keeps_the_parent_open() {
2619 let mut tree = WidgetTree::new();
2620 let anchor = tree.add(FillWidget::new().focusable());
2621 let parent_item = tree.add(FillWidget::new().focusable());
2622 let parent_content = tree.add(StackWidget::new().child(parent_item));
2623 let child_item = tree.add(FillWidget::new().focusable());
2624 let child_content = tree.add(StackWidget::new().child(child_item));
2625 tree.layout(SizeProposal::exact(200.0, 100.0));
2626
2627 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2628 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2629
2630 tree.focus(parent_item);
2631 tree.focus(child_item);
2632
2633 assert_eq!(
2634 tree.active_overlays().len(),
2635 2,
2636 "opening a submenu is not leaving the menu that owns it"
2637 );
2638 }
2639
2640 #[test]
2643 fn focus_back_to_the_parent_overlay_closes_only_the_child() {
2644 let mut tree = WidgetTree::new();
2645 let anchor = tree.add(FillWidget::new().focusable());
2646 let parent_item = tree.add(FillWidget::new().focusable());
2647 let parent_content = tree.add(StackWidget::new().child(parent_item));
2648 let child_item = tree.add(FillWidget::new().focusable());
2649 let child_content = tree.add(StackWidget::new().child(child_item));
2650 tree.layout(SizeProposal::exact(200.0, 100.0));
2651
2652 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2653 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2654
2655 tree.focus(child_item);
2656 tree.focus(parent_item);
2657
2658 assert_eq!(
2659 tree.active_overlays(),
2660 vec![parent],
2661 "the submenu goes, the menu that owns it stays"
2662 );
2663 }
2664
2665 #[test]
2668 fn leaving_a_nested_cascade_closes_every_level() {
2669 let mut tree = WidgetTree::new();
2670 let anchor = tree.add(FillWidget::new().focusable());
2671 let away = tree.add(FillWidget::new().focusable());
2672 let parent_item = tree.add(FillWidget::new().focusable());
2673 let parent_content = tree.add(StackWidget::new().child(parent_item));
2674 let child_item = tree.add(FillWidget::new().focusable());
2675 let child_content = tree.add(StackWidget::new().child(child_item));
2676 tree.layout(SizeProposal::exact(200.0, 100.0));
2677
2678 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2679 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2680
2681 tree.focus(child_item);
2682 tree.focus(away);
2683
2684 assert!(
2685 tree.active_overlays().is_empty(),
2686 "one move out of the cascade must leave nothing behind"
2687 );
2688 }
2689
2690 #[test]
2700 fn a_dropdown_inside_a_modal_still_follows_focus_out() {
2701 let mut tree = WidgetTree::new();
2702 let opener = tree.add(FillWidget::new().focusable());
2703 let trigger = tree.add(FillWidget::new().focusable());
2704 let next = tree.add(FillWidget::new().focusable());
2705 let modal_content = tree.add(StackWidget::new().child(trigger).child(next));
2706 let panel = tree.add(StackWidget::new());
2707 tree.layout(SizeProposal::exact(200.0, 100.0));
2708
2709 tree.show_overlay(OverlayRequest::new(
2710 modal_content,
2711 opener,
2712 OverlayPlacement::Centered,
2713 DismissBehavior::Manual,
2714 ));
2715 show_anchored(&mut tree, panel, trigger, None);
2717
2718 tree.focus(trigger);
2719 assert_eq!(
2720 tree.active_overlays().len(),
2721 2,
2722 "precondition: modal + panel"
2723 );
2724
2725 tab(&mut tree);
2726
2727 assert_eq!(
2728 tree.focused(),
2729 Some(next),
2730 "Tab moves on within the modal, as it should"
2731 );
2732 assert_eq!(
2733 tree.active_overlays().len(),
2734 1,
2735 "the dropdown must go — only the modal hosting it stays"
2736 );
2737 }
2738
2739 #[test]
2747 fn a_viewport_placed_notification_ignores_focus_moving() {
2748 let mut tree = WidgetTree::new();
2749 let trigger = tree.add(FillWidget::new().focusable());
2750 let elsewhere = tree.add(FillWidget::new().focusable());
2751 let snack = tree.add(StackWidget::new());
2752 tree.layout(SizeProposal::exact(200.0, 100.0));
2753 tree.show_overlay(OverlayRequest::new(
2754 snack,
2755 trigger,
2756 OverlayPlacement::BottomCenter,
2757 DismissBehavior::Manual,
2758 ));
2759
2760 tree.focus(trigger);
2761 tab(&mut tree);
2762
2763 assert_eq!(
2764 tree.active_overlays().len(),
2765 1,
2766 "a snackbar outlives the keystroke that moved focus off its trigger"
2767 );
2768 assert_ne!(tree.focused(), Some(trigger), "and focus did move");
2769 assert_eq!(tree.focused(), Some(elsewhere));
2770 }
2771
2772 #[test]
2776 fn leaving_a_hosted_menu_spares_the_host() {
2777 let mut tree = WidgetTree::new();
2778 let opener = tree.add(FillWidget::new().focusable());
2779 let away = tree.add(FillWidget::new().focusable());
2780 let modal_item = tree.add(FillWidget::new().focusable());
2781 let modal_content = tree.add(StackWidget::new().child(modal_item));
2782 let menu_item = tree.add(FillWidget::new().focusable());
2783 let menu_content = tree.add(StackWidget::new().child(menu_item));
2784 tree.layout(SizeProposal::exact(200.0, 100.0));
2785
2786 let host = tree.show_overlay(OverlayRequest::new(
2788 modal_content,
2789 opener,
2790 OverlayPlacement::Centered,
2791 DismissBehavior::Manual,
2792 ));
2793 show_anchored(&mut tree, menu_content, modal_item, Some(host));
2794
2795 tree.focus(menu_item);
2796 tree.focus(away);
2797
2798 assert_eq!(
2799 tree.active_overlays(),
2800 vec![host],
2801 "the menu goes; the modal hosting it stays"
2802 );
2803 }
2804
2805 #[test]
2809 fn leaving_the_anchor_dismisses_a_panel_focus_never_entered() {
2810 let mut tree = WidgetTree::new();
2811 let trigger = tree.add(FillWidget::new().focusable());
2812 let after = tree.add(FillWidget::new().focusable());
2813 let content = tree.add(StackWidget::new());
2814 tree.layout(SizeProposal::exact(200.0, 100.0));
2815 show_anchored(&mut tree, content, trigger, None);
2816
2817 tree.focus(trigger);
2818 assert_eq!(tree.active_overlays().len(), 1, "precondition: panel is up");
2819
2820 tree.focus(after);
2821 assert!(
2822 tree.active_overlays().is_empty(),
2823 "leaving the trigger is leaving the dropdown it owns"
2824 );
2825 }
2826}