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(
227 &mut self,
228 from: WidgetId,
229 rect: Rect,
230 margin: f32,
231 align: crate::event::ScrollAlign,
232 motion: crate::event::ScrollMotion,
233 ops: &mut dyn crate::window::WindowOps,
234 ) {
235 let applied = std::sync::Arc::new(std::sync::Mutex::new(Point::ZERO));
241 let mut rect = rect;
242 let mut current = self.arena.parent(from);
243 let mut pending_align = align;
246 while let Some(ancestor_id) = current {
247 let to_parent = self
262 .arena
263 .get(ancestor_id)
264 .and_then(|n| n.transform_prop.as_ref())
265 .map(|t| t.get())
266 .filter(|t| !t.is_identity());
267 let content_transform = self
268 .arena
269 .get(ancestor_id)
270 .is_some_and(|n| n.content_transform);
271
272 if let Some(node) = self.arena.get(ancestor_id)
279 && node.clips_children
280 && (node.handlers.on_scroll.is_some() || node.external_handlers.on_scroll.is_some())
281 {
282 let viewport = node.bounds;
283 let against = match (content_transform, to_parent.as_ref()) {
290 (true, Some(t)) => t.apply_rect(rect),
291 _ => rect,
292 };
293 let align =
294 std::mem::replace(&mut pending_align, crate::event::ScrollAlign::Minimal);
295 let needs_scroll = matches!(align, crate::event::ScrollAlign::Fraction(_))
301 || node.judges_reveals_live
302 || against.y - margin < viewport.y
303 || against.bottom() + margin > viewport.bottom()
304 || against.x - margin < viewport.x
305 || against.right() + margin > viewport.right();
306
307 if needs_scroll {
308 *applied.lock().unwrap() = Point::ZERO;
309 self.dispatch_to_widget_direct(
323 ancestor_id,
324 &WidgetEvent::ScrollIntoView {
325 target_bounds: rect,
331 margin,
332 align,
333 motion,
334 applied_scroll: Some(applied.clone()),
335 },
336 &mut *ops,
337 );
338 let delta = *applied.lock().unwrap();
342 if delta != Point::ZERO {
343 rect =
344 Rect::new(rect.x - delta.x, rect.y - delta.y, rect.width, rect.height);
345 }
346 }
347 }
348 if let Some(t) = to_parent {
354 rect = t.apply_rect(rect);
355 }
356 current = self.arena.parent(ancestor_id);
357 }
358 }
359
360 pub fn focus(&mut self, id: WidgetId) {
385 let mut noop = crate::window::NoopWindowOps;
386 self.request_focus_with_ops(id, FocusRequestMode::Exact, &mut noop);
387 }
388
389 pub fn clear_focus(&mut self) {
407 let mut noop = crate::window::NoopWindowOps;
408 self.clear_focus_ops(&mut noop);
409 }
410
411 pub(crate) fn clear_focus_ops(&mut self, ops: &mut dyn crate::window::WindowOps) {
414 self.pending_focus_request = None;
415 let Some(old) = self.focused else {
416 return;
417 };
418 struct ClearOnDrop(std::rc::Rc<std::cell::Cell<bool>>);
421 impl Drop for ClearOnDrop {
422 fn drop(&mut self) {
423 self.0.set(false);
424 }
425 }
426 self.in_focus_dispatch.set(true);
427 let _focus_dispatch_guard = ClearOnDrop(self.in_focus_dispatch.clone());
428
429 self.set_focused(None);
433 self.focus_origin = None;
434 if self.focus_visible.get() {
435 self.focus_visible.set(false);
436 }
437 self.a11y_dirty = true;
438 self.update_focus_within_signals(Some(old), None);
439 self.update_view_focus_signals(Some(old), None);
440 self.dispatch_to_widget(old, &WidgetEvent::FocusLost, &mut *ops);
441 let now = self.focused;
444 self.dismiss_overlays_left_by_focus(Some(old), now, &mut *ops);
445 self.tooltip_focus_leave_outside(now, &mut *ops);
446 }
447
448 pub(crate) fn request_focus_with_ops(
461 &mut self,
462 id: WidgetId,
463 mode: FocusRequestMode,
464 ops: &mut dyn crate::window::WindowOps,
465 ) {
466 self.pending_focus_request = None;
468 if self.arena.get(id).is_some() && !self.arena.is_active(id) {
469 self.pending_focus_request = Some(PendingFocusRequest { id, mode });
470 return;
471 }
472 self.resolve_focus_request(id, mode, ops);
473 }
474
475 #[cfg(test)]
477 pub(crate) fn has_pending_focus_request(&self) -> bool {
478 self.pending_focus_request.is_some()
479 }
480
481 fn resolve_focus_request(
483 &mut self,
484 id: WidgetId,
485 mode: FocusRequestMode,
486 ops: &mut dyn crate::window::WindowOps,
487 ) {
488 match mode {
489 FocusRequestMode::Exact => {
490 let target = self.exact_focus_target(id);
491 self.focus_ops(target, ops);
492 }
493 FocusRequestMode::SelfOrDescendant => {
494 let target = self.first_focusable_descendant(id).unwrap_or(id);
495 self.focus_ops(target, ops);
496 }
497 FocusRequestMode::Into => {
498 if let Some(target) = self.first_focusable_descendant(id) {
499 self.focus_ops(target, ops);
500 }
501 }
502 }
503 }
504
505 pub(crate) fn exact_focus_target(&self, id: WidgetId) -> WidgetId {
517 let Some(node) = self.arena.get(id) else {
518 return id;
519 };
520 if self.is_node_focusable(node) || !self.arena.is_active(id) || !self.arena.is_enabled(id) {
521 return id;
522 }
523 self.first_focusable_descendant(id).unwrap_or(id)
524 }
525
526 pub(super) fn settle_pending_focus_request(&mut self, ops: &mut dyn crate::window::WindowOps) {
535 let Some(request) = self.pending_focus_request.take() else {
536 return;
537 };
538 if self.arena.is_active(request.id) {
541 self.resolve_focus_request(request.id, request.mode, ops);
542 }
543 }
544
545 pub fn focus_ops(&mut self, id: WidgetId, ops: &mut dyn crate::window::WindowOps) {
548 self.focus_with_origin_ops(id, crate::focus::FocusOrigin::Programmatic, ops);
549 }
550
551 pub fn focused(&self) -> Option<WidgetId> {
553 self.focused
554 }
555
556 pub fn ime_context_for_focused(&self) -> Option<crate::ime::ImeContext> {
562 self.focused.and_then(|id| self.arena.ime_context(id))
563 }
564
565 pub fn first_focusable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
570 if !self.arena.is_active(root) {
571 return None;
572 }
573 let mut entries = Vec::new();
574 self.collect_scope_entries(root, &mut entries);
575 sort_scope_entries(&mut entries);
576 let scope = ScopeNode {
577 policy: crate::focus::TraversalScopePolicy::Cycle,
578 entries,
579 };
580 enter_scope_edge(&scope, false)
581 }
582
583 pub(crate) fn tab_stops_in_order(&self, root: WidgetId) -> Vec<WidgetId> {
589 fn flatten(entries: &[ScopeEntry], out: &mut Vec<WidgetId>) {
590 for entry in entries {
591 match &entry.kind {
592 ScopeEntryKind::Focusable(id) => out.push(*id),
593 ScopeEntryKind::Scope(child) => flatten(&child.entries, out),
594 }
595 }
596 }
597 if !self.arena.is_active(root) {
598 return Vec::new();
599 }
600 let mut entries = Vec::new();
601 self.collect_scope_entries(root, &mut entries);
602 sort_scope_entries(&mut entries);
603 let mut out = Vec::new();
604 flatten(&entries, &mut out);
605 out
606 }
607
608 #[doc(hidden)]
615 pub fn focusables_in_tab_order(&self, root: WidgetId) -> Vec<WidgetId> {
616 fn flatten(entries: &[ScopeEntry], out: &mut Vec<WidgetId>) {
617 for entry in entries {
618 match &entry.kind {
619 ScopeEntryKind::Focusable(id) => out.push(*id),
620 ScopeEntryKind::Scope(scope) => flatten(&scope.entries, out),
621 }
622 }
623 }
624 let mut entries = Vec::new();
625 self.collect_scope_entries_with(root, &mut entries, false);
626 sort_scope_entries(&mut entries);
627 let mut out = Vec::new();
628 flatten(&entries, &mut out);
629 out
630 }
631
632 pub(crate) fn focusable_descendants(&self, root: WidgetId) -> Vec<WidgetId> {
642 fn walk(tree: &WidgetTree, id: WidgetId, out: &mut Vec<WidgetId>) {
643 if !tree.arena.is_active(id) {
644 return;
645 }
646 let Some(node) = tree.arena.get(id) else {
647 return;
648 };
649 if node.enabled_state.as_ref().is_some_and(|s| !s.get()) {
650 return;
651 }
652 if tree.is_node_focusable(node) {
653 out.push(id);
654 }
655 for &child in tree.arena.children(id) {
656 walk(tree, child, out);
657 }
658 }
659 let mut out = Vec::new();
660 walk(self, root, &mut out);
661 out
662 }
663
664 pub(crate) fn first_focus_capable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
668 if !self.arena.is_active(root) {
669 return None;
670 }
671 let node = self.arena.get(root)?;
672 if self.is_node_focusable(node) {
673 return Some(root);
674 }
675 node.children
676 .iter()
677 .find_map(|&child| self.first_focus_capable_descendant(child))
678 }
679
680 pub fn is_active(&self, id: WidgetId) -> bool {
686 self.arena.is_active(id)
687 }
688
689 pub fn widget_initial_focus_hint(&self, id: WidgetId) -> Option<WidgetId> {
705 if !self.arena.is_active(id) {
706 return None;
707 }
708 if let Some(node) = self.arena.get(id) {
709 if let Some(target) = node.widget.initial_focus_hint()
710 && self.arena.is_active(target)
711 && self.is_descendant_of(target, id)
712 {
713 return Some(target);
714 }
715 for &child in &node.children {
716 if let Some(found) = self.widget_initial_focus_hint(child) {
717 return Some(found);
718 }
719 }
720 }
721 None
722 }
723
724 pub fn focus_origin(&self) -> Option<crate::focus::FocusOrigin> {
726 self.focus_origin
727 }
728
729 pub fn focus_visible_signal(&self) -> crate::signal::Signal<bool> {
733 self.focus_visible.clone()
734 }
735
736 fn focus_scope_root(&self) -> ScopeNode {
742 let mut entries = Vec::new();
743 if let Some(modal_overlay) = self.overlay_manager.topmost_modal() {
744 let content_id = modal_overlay.content_id;
745 self.collect_scope_entries(content_id, &mut entries);
746 let modal_id = modal_overlay.id;
753 let above: Vec<WidgetId> = {
754 let stack = &self.overlay_manager.stack;
755 stack
756 .iter()
757 .position(|overlay| overlay.id == modal_id)
758 .map(|at| {
759 stack[at + 1..]
760 .iter()
761 .map(|overlay| overlay.content_id)
762 .collect()
763 })
764 .unwrap_or_default()
765 };
766 for overlay_content in above {
767 if self.tooltip_content_root(overlay_content).is_some() {
772 continue;
773 }
774 let holds_focus = self
775 .focused
776 .is_some_and(|f| self.is_descendant_of(f, overlay_content));
777 if holds_focus && !self.is_descendant_of(overlay_content, content_id) {
778 self.collect_scope_entries(overlay_content, &mut entries);
779 }
780 }
781 } else {
782 let roots = self.arena.roots();
783 for root in roots {
784 if self.tooltip_content_root(root).is_some() {
794 continue;
795 }
796 self.collect_scope_entries(root, &mut entries);
797 }
798 }
799 sort_scope_entries(&mut entries);
800 self.splice_sticky_tooltips_after_anchors(&mut entries);
801 ScopeNode {
802 policy: crate::focus::TraversalScopePolicy::Cycle,
803 entries,
804 }
805 }
806
807 pub(super) fn first_tab_stop_in_scope(&self) -> Option<WidgetId> {
810 enter_scope_edge(&self.focus_scope_root(), false)
811 }
812
813 pub(super) fn enabled_stop_around(&self, marker: WidgetId) -> Option<WidgetId> {
824 let mut entries = Vec::new();
825 let holder = self
826 .overlay_manager
827 .stack
828 .iter()
829 .rev()
830 .find(|o| self.is_descendant_of(marker, o.content_id))
831 .map(|o| o.content_id);
832 if let Some(content) = holder.or_else(|| {
833 self.overlay_manager
834 .topmost_modal()
835 .map(|modal| modal.content_id)
836 }) {
837 self.collect_marked(content, marker, false, &mut entries);
838 } else {
839 for root in self.arena.roots() {
840 if self.tooltip_content_root(root).is_some()
841 || self.overlay_manager.find_by_content(root).is_some()
842 {
843 continue;
844 }
845 self.collect_marked(root, marker, false, &mut entries);
846 }
847 }
848 sort_marked(&mut entries);
849 let scope = innermost_cycle_holding_marker(&entries).unwrap_or(&entries);
850 let mut flat = Vec::new();
851 flatten_marked(scope, &mut flat);
852 let at = flat.iter().position(Option::is_none)?;
853 flat[at + 1..]
854 .iter()
855 .find_map(|stop| *stop)
856 .or_else(|| flat[..at].iter().rev().find_map(|stop| *stop))
857 }
858
859 fn collect_marked(
863 &self,
864 id: WidgetId,
865 marker: WidgetId,
866 disabled_above: bool,
867 out: &mut Vec<(Option<i32>, Marked)>,
868 ) {
869 if !self.arena.is_active(id) {
870 return;
871 }
872 let Some(node) = self.arena.get(id) else {
873 return;
874 };
875 if id == marker {
876 out.push((node.node_tab_index, Marked::Marker));
877 return;
878 }
879 let disabled = disabled_above
880 || node
881 .enabled_state
882 .as_ref()
883 .is_some_and(|state| !state.get());
884 if disabled && !self.is_descendant_of(marker, id) {
885 return;
886 }
887 if let Some(policy) = node.node_traversal_scope {
888 let mut child_entries = Vec::new();
889 for &child in &node.children {
890 self.collect_marked(child, marker, disabled, &mut child_entries);
891 }
892 sort_marked(&mut child_entries);
893 out.push((node.node_tab_index, Marked::Scope(policy, child_entries)));
894 return;
895 }
896 if !disabled && self.is_node_focusable(node) && self.tab_stop_effective(id) {
897 out.push((node.node_tab_index, Marked::Stop(id)));
898 }
899 for &child in &node.children {
900 self.collect_marked(child, marker, disabled, out);
901 }
902 }
903
904 pub(super) fn cycle_focus(&mut self, reverse: bool, ops: &mut dyn crate::window::WindowOps) {
913 let root_scope = self.focus_scope_root();
914
915 if let StepResult::Found(next_id) = navigate_scope(&root_scope, self.focused, reverse, true)
916 {
917 self.focus_with_origin_ops(next_id, crate::focus::FocusOrigin::Keyboard, &mut *ops);
918 }
919 }
922
923 fn tooltip_content_root(&self, id: WidgetId) -> Option<usize> {
925 self.tooltips.iter().position(|e| e.content_id == id)
926 }
927
928 fn splice_sticky_tooltips_after_anchors(&self, entries: &mut Vec<ScopeEntry>) {
947 let sticky: Vec<(WidgetId, WidgetId)> = self
948 .tooltips
949 .iter()
950 .filter(|e| e.is_sticky && e.overlay_id.is_some())
951 .map(|e| (e.anchor_id, e.content_id))
952 .collect();
953
954 for (anchor_id, content_id) in sticky {
955 let mut panel = Vec::new();
956 self.collect_scope_entries(content_id, &mut panel);
957 if panel.is_empty() {
958 continue;
959 }
960 sort_scope_entries(&mut panel);
961
962 let stop = self
967 .find_focusable_at_or_above(anchor_id)
968 .unwrap_or(anchor_id);
969 let at = entries
970 .iter()
971 .position(|entry| scope_entry_contains(entry, stop))
972 .map_or(entries.len(), |i| i + 1);
977
978 for (offset, item) in panel.into_iter().enumerate() {
979 entries.insert(at + offset, item);
980 }
981 }
982 }
983
984 pub(super) fn tab_stop_effective(&self, id: WidgetId) -> bool {
992 let mut current = Some(id);
993 while let Some(cur) = current {
994 let Some(node) = self.arena.get(cur) else {
995 break;
996 };
997 if let Some(prop) = node.tab_stop.as_ref() {
998 return prop.get();
999 }
1000 current = node.parent;
1001 }
1002 true
1003 }
1004
1005 pub(super) fn is_node_focusable(&self, node: &crate::arena::WidgetNode) -> bool {
1007 node.node_focusable.unwrap_or(false)
1008 }
1009
1010 pub(super) fn find_focusable_at_or_above(&self, id: WidgetId) -> Option<WidgetId> {
1012 let mut current = Some(id);
1013 while let Some(current_id) = current {
1014 if let Some(node) = self.arena.get(current_id)
1015 && self.is_node_focusable(node)
1016 {
1017 return Some(current_id);
1018 }
1019 current = self.arena.parent(current_id);
1020 }
1021 None
1022 }
1023
1024 fn collect_scope_entries(&self, id: WidgetId, out: &mut Vec<ScopeEntry>) {
1040 self.collect_scope_entries_with(id, out, true);
1041 }
1042
1043 fn collect_scope_entries_with(&self, id: WidgetId, out: &mut Vec<ScopeEntry>, tab_stops: bool) {
1047 if !self.arena.is_active(id) {
1048 return;
1049 }
1050 let Some(node) = self.arena.get(id) else {
1051 return;
1052 };
1053 if node
1054 .enabled_state
1055 .as_ref()
1056 .map(|s| !s.get())
1057 .unwrap_or(false)
1058 {
1059 return;
1060 }
1061 if tab_stops && node.tab_gate.as_ref().is_some_and(|gate| !gate.get()) {
1064 return;
1065 }
1066
1067 if let Some(policy) = node.node_traversal_scope {
1070 let mut child_entries = Vec::new();
1071 for &child in &node.children {
1072 self.collect_scope_entries_with(child, &mut child_entries, tab_stops);
1073 }
1074 sort_scope_entries(&mut child_entries);
1075 out.push(ScopeEntry {
1076 tab_index: node.node_tab_index,
1077 kind: ScopeEntryKind::Scope(ScopeNode {
1078 policy,
1079 entries: child_entries,
1080 }),
1081 });
1082 return;
1083 }
1084
1085 if self.is_node_focusable(node) && (!tab_stops || self.tab_stop_effective(id)) {
1087 out.push(ScopeEntry {
1088 tab_index: node.node_tab_index,
1089 kind: ScopeEntryKind::Focusable(id),
1090 });
1091 }
1092 for &child in &node.children {
1093 self.collect_scope_entries_with(child, out, tab_stops);
1094 }
1095 }
1096
1097 pub(super) fn strict_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
1102 let mut chain = Vec::new();
1103 if let Some(start) = id {
1104 let mut current = self.arena.parent(start);
1105 while let Some(parent) = current {
1106 chain.push(parent);
1107 current = self.arena.parent(parent);
1108 }
1109 }
1110 chain
1111 }
1112
1113 pub(crate) fn update_focus_within_signals(
1118 &mut self,
1119 old: Option<WidgetId>,
1120 new: Option<WidgetId>,
1121 ) {
1122 let old_chain = self.strict_ancestors_of(old);
1123 let new_chain = self.strict_ancestors_of(new);
1124 for &id in &old_chain {
1126 if !new_chain.contains(&id)
1127 && let Some(node) = self.arena.get(id)
1128 && let Some(sig) = node.focus_within_signal.clone()
1129 {
1130 sig.set(false);
1131 }
1132 }
1133 for &id in &new_chain {
1135 if !old_chain.contains(&id)
1136 && let Some(node) = self.arena.get(id)
1137 && let Some(sig) = node.focus_within_signal.clone()
1138 {
1139 sig.set(true);
1140 }
1141 }
1142 }
1143
1144 pub(super) fn inclusive_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
1147 let mut chain = Vec::new();
1148 if let Some(start) = id {
1149 chain.push(start);
1150 chain.extend(self.strict_ancestors_of(Some(start)));
1151 }
1152 chain
1153 }
1154
1155 pub(crate) fn update_view_focus_signals(
1160 &mut self,
1161 old: Option<WidgetId>,
1162 new: Option<WidgetId>,
1163 ) {
1164 let old_chain = self.inclusive_ancestors_of(old);
1165 let new_chain = self.inclusive_ancestors_of(new);
1166 for &id in &old_chain {
1167 if !new_chain.contains(&id)
1168 && let Some(node) = self.arena.get(id)
1169 && let Some(sig) = node.view_focus_signal.clone()
1170 {
1171 sig.set(false);
1172 }
1173 }
1174 for &id in &new_chain {
1175 if !old_chain.contains(&id)
1176 && let Some(node) = self.arena.get(id)
1177 && let Some(sig) = node.view_focus_signal.clone()
1178 {
1179 sig.set(true);
1180 }
1181 }
1182 }
1183
1184 pub(crate) fn view_focus_signal_for(
1187 &mut self,
1188 node_id: WidgetId,
1189 ) -> crate::signal::Signal<bool> {
1190 if let Some(node) = self.arena.get(node_id)
1191 && let Some(sig) = node.view_focus_signal.clone()
1192 {
1193 return sig;
1194 }
1195 let active = self.inclusive_ancestors_of(self.focused).contains(&node_id);
1196 let sig = crate::signal::Signal::new(active);
1197 if let Some(node) = self.arena.get_mut(node_id) {
1198 node.view_focus_signal = Some(sig.clone());
1199 }
1200 sig
1201 }
1202
1203 pub fn view_focus_active_for(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
1209 match self.find_focusable_at_or_above(node_id) {
1210 Some(scope) => self.view_focus_signal_for(scope),
1211 None => crate::signal::Signal::new(true),
1212 }
1213 }
1214
1215 pub fn begin_view_focus(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
1220 let sig = self.view_focus_signal_for(node_id);
1221 self.view_focus_stack.push(sig.clone());
1222 sig
1223 }
1224
1225 pub fn end_view_focus(&mut self) {
1227 self.view_focus_stack.pop();
1228 }
1229
1230 pub fn current_view_focus(&self) -> Option<crate::signal::Signal<bool>> {
1232 self.view_focus_stack.last().cloned()
1233 }
1234
1235 pub(crate) fn set_hovered(&mut self, value: Option<WidgetId>) {
1249 if let Some(entry) = self.pointers.hover_owner_mut() {
1250 entry.hovered = value;
1251 }
1252 if self.hovered_signal.get() != value {
1253 self.hovered_signal.set(value);
1254 }
1255 }
1256
1257 pub(crate) fn set_focused(&mut self, value: Option<WidgetId>) {
1265 self.focused = value;
1266 if let Some(focused) = value {
1268 self.remember_region_focus(focused);
1269 }
1270 if self.focused_signal.get() != value {
1271 self.focused_signal.set(value);
1272 }
1273 }
1274
1275 pub(crate) fn update_hover_within_signals(
1278 &mut self,
1279 old: Option<WidgetId>,
1280 new: Option<WidgetId>,
1281 ) {
1282 let old_chain = self.strict_ancestors_of(old);
1283 let new_chain = self.strict_ancestors_of(new);
1284 if let Some(entry) = self.pointers.hover_owner_mut() {
1288 entry.hover_within = new_chain.clone();
1289 }
1290 for &id in &old_chain {
1291 if !new_chain.contains(&id)
1292 && let Some(node) = self.arena.get(id)
1293 && let Some(sig) = node.hover_within_signal.clone()
1294 {
1295 sig.set(false);
1296 }
1297 }
1298 for &id in &new_chain {
1299 if !old_chain.contains(&id)
1300 && let Some(node) = self.arena.get(id)
1301 && let Some(sig) = node.hover_within_signal.clone()
1302 {
1303 sig.set(true);
1304 }
1305 }
1306 }
1307}
1308
1309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1312pub(crate) enum FocusRequestMode {
1313 Exact,
1317 SelfOrDescendant,
1320 Into,
1323}
1324
1325#[derive(Debug, Clone, Copy)]
1328pub(crate) struct PendingFocusRequest {
1329 id: WidgetId,
1330 mode: FocusRequestMode,
1331}
1332
1333struct ScopeEntry {
1345 tab_index: Option<i32>,
1346 kind: ScopeEntryKind,
1347}
1348
1349enum ScopeEntryKind {
1350 Focusable(WidgetId),
1351 Scope(ScopeNode),
1352}
1353
1354struct ScopeNode {
1356 policy: crate::focus::TraversalScopePolicy,
1357 entries: Vec<ScopeEntry>,
1358}
1359
1360enum StepResult {
1362 Found(WidgetId),
1364 Escaped,
1368}
1369
1370fn scope_wraps(policy: crate::focus::TraversalScopePolicy, is_root: bool) -> bool {
1373 is_root || matches!(policy, crate::focus::TraversalScopePolicy::Cycle)
1374}
1375
1376fn sort_scope_entries(entries: &mut [ScopeEntry]) {
1379 entries.sort_by(|a, b| match (a.tab_index, b.tab_index) {
1380 (Some(ia), Some(ib)) => ia.cmp(&ib),
1381 (Some(_), None) => std::cmp::Ordering::Less,
1382 (None, Some(_)) => std::cmp::Ordering::Greater,
1383 (None, None) => std::cmp::Ordering::Equal,
1384 });
1385}
1386
1387fn scope_entry_contains(entry: &ScopeEntry, f: WidgetId) -> bool {
1389 match &entry.kind {
1390 ScopeEntryKind::Focusable(id) => *id == f,
1391 ScopeEntryKind::Scope(child) => child.entries.iter().any(|e| scope_entry_contains(e, f)),
1392 }
1393}
1394
1395fn enter_scope_edge(scope: &ScopeNode, reverse: bool) -> Option<WidgetId> {
1399 let n = scope.entries.len();
1400 for k in 0..n {
1401 let idx = if reverse { n - 1 - k } else { k };
1402 match &scope.entries[idx].kind {
1403 ScopeEntryKind::Focusable(id) => return Some(*id),
1404 ScopeEntryKind::Scope(child) => {
1405 if let Some(id) = enter_scope_edge(child, reverse) {
1406 return Some(id);
1407 }
1408 }
1409 }
1410 }
1411 None
1412}
1413
1414fn navigate_scope(
1418 scope: &ScopeNode,
1419 focused: Option<WidgetId>,
1420 reverse: bool,
1421 is_root: bool,
1422) -> StepResult {
1423 if scope.entries.is_empty() {
1424 return StepResult::Escaped;
1425 }
1426
1427 let cur = focused.and_then(|f| {
1428 scope
1429 .entries
1430 .iter()
1431 .position(|e| scope_entry_contains(e, f))
1432 });
1433
1434 let from = match cur {
1435 None => {
1436 return match enter_scope_edge(scope, reverse) {
1439 Some(id) => StepResult::Found(id),
1440 None => StepResult::Escaped,
1441 };
1442 }
1443 Some(i) => i,
1444 };
1445
1446 if let ScopeEntryKind::Scope(child) = &scope.entries[from].kind
1449 && let StepResult::Found(id) = navigate_scope(child, focused, reverse, false)
1450 {
1451 return StepResult::Found(id);
1452 }
1453
1454 step_to_sibling(scope, from, reverse, is_root)
1455}
1456
1457enum Marked {
1459 Stop(WidgetId),
1461 Marker,
1463 Scope(
1465 crate::focus::TraversalScopePolicy,
1466 Vec<(Option<i32>, Marked)>,
1467 ),
1468}
1469
1470fn sort_marked(entries: &mut [(Option<i32>, Marked)]) {
1472 entries.sort_by(|a, b| match (a.0, b.0) {
1473 (Some(ia), Some(ib)) => ia.cmp(&ib),
1474 (Some(_), None) => std::cmp::Ordering::Less,
1475 (None, Some(_)) => std::cmp::Ordering::Greater,
1476 (None, None) => std::cmp::Ordering::Equal,
1477 });
1478}
1479
1480fn marked_holds_marker(entry: &Marked) -> bool {
1481 match entry {
1482 Marked::Stop(_) => false,
1483 Marked::Marker => true,
1484 Marked::Scope(_, entries) => entries.iter().any(|(_, e)| marked_holds_marker(e)),
1485 }
1486}
1487
1488fn innermost_cycle_holding_marker(
1491 entries: &[(Option<i32>, Marked)],
1492) -> Option<&[(Option<i32>, Marked)]> {
1493 entries.iter().find_map(|(_, entry)| match entry {
1494 Marked::Scope(policy, inner) if marked_holds_marker(entry) => {
1495 innermost_cycle_holding_marker(inner).or_else(|| {
1496 matches!(policy, crate::focus::TraversalScopePolicy::Cycle)
1497 .then_some(inner.as_slice())
1498 })
1499 }
1500 _ => None,
1501 })
1502}
1503
1504fn flatten_marked(entries: &[(Option<i32>, Marked)], out: &mut Vec<Option<WidgetId>>) {
1506 for (_, entry) in entries {
1507 match entry {
1508 Marked::Stop(id) => out.push(Some(*id)),
1509 Marked::Marker => out.push(None),
1510 Marked::Scope(_, inner) => flatten_marked(inner, out),
1511 }
1512 }
1513}
1514
1515fn step_to_sibling(scope: &ScopeNode, from: usize, reverse: bool, is_root: bool) -> StepResult {
1519 let n = scope.entries.len();
1520 let mut idx = from;
1521 for _ in 0..n {
1522 let next = if reverse {
1523 if idx == 0 {
1524 if scope_wraps(scope.policy, is_root) {
1525 n - 1
1526 } else {
1527 return StepResult::Escaped;
1528 }
1529 } else {
1530 idx - 1
1531 }
1532 } else if idx + 1 >= n {
1533 if scope_wraps(scope.policy, is_root) {
1534 0
1535 } else {
1536 return StepResult::Escaped;
1537 }
1538 } else {
1539 idx + 1
1540 };
1541
1542 match &scope.entries[next].kind {
1543 ScopeEntryKind::Focusable(id) => return StepResult::Found(*id),
1544 ScopeEntryKind::Scope(child) => {
1545 if let Some(id) = enter_scope_edge(child, reverse) {
1546 return StepResult::Found(id);
1547 }
1548 idx = next;
1550 }
1551 }
1552 }
1553 StepResult::Escaped
1554}
1555
1556#[cfg(test)]
1557mod tests {
1558 use super::*;
1559 use crate::test_widgets::FillWidget;
1560 use crate::widget_builder::WidgetBuilder;
1561
1562 #[test]
1571 fn a_reveal_walks_the_ancestors_of_whoever_owns_the_rect() {
1572 use crate::event::{EventResponse, ScrollAlign, ScrollMotion};
1573 use std::cell::Cell;
1574 use std::rc::Rc;
1575
1576 let asked = Rc::new(Cell::new(0usize));
1577 let mut tree = WidgetTree::new();
1578 let viewport = {
1580 let asked = asked.clone();
1581 tree.add(
1582 FillWidget::new()
1585 .on_scroll(move |event, _ctx| {
1586 if matches!(event, WidgetEvent::ScrollIntoView { .. }) {
1587 asked.set(asked.get() + 1);
1588 }
1589 EventResponse::Ignored
1590 })
1591 .clips_children(true),
1592 )
1593 };
1594 let inside = tree.add_child(viewport, FillWidget::new());
1595 let button = tree.add(FillWidget::new());
1597 tree.layout(SizeProposal::exact(400.0, 300.0));
1598
1599 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
1601 let mut ops = crate::window::NoopWindowOps;
1602
1603 tree.scroll_rect_into_view(
1604 button,
1605 target,
1606 0.0,
1607 ScrollAlign::Minimal,
1608 ScrollMotion::Instant,
1609 &mut ops,
1610 );
1611 assert_eq!(
1612 asked.get(),
1613 0,
1614 "walking from the asker never reaches a viewport it is not inside"
1615 );
1616
1617 tree.scroll_rect_into_view(
1618 inside,
1619 target,
1620 0.0,
1621 ScrollAlign::Minimal,
1622 ScrollMotion::Instant,
1623 &mut ops,
1624 );
1625 assert_eq!(
1626 asked.get(),
1627 1,
1628 "walking from the rect's owner reaches the viewport enclosing it"
1629 );
1630 }
1631
1632 fn recorder(
1635 tree: &mut WidgetTree,
1636 child: WidgetId,
1637 seen: std::rc::Rc<std::cell::RefCell<Vec<Rect>>>,
1638 ) -> WidgetId {
1639 use crate::event::EventResponse;
1640 use crate::test_widgets::StackWidget;
1641 tree.add(
1642 StackWidget::new()
1643 .child(child)
1644 .on_scroll(move |event, _ctx| {
1645 if let WidgetEvent::ScrollIntoView { target_bounds, .. } = event {
1646 seen.borrow_mut().push(*target_bounds);
1647 }
1648 EventResponse::Ignored
1649 })
1650 .clips_children(true),
1651 )
1652 }
1653
1654 #[test]
1665 fn a_reveal_projects_the_rect_as_it_climbs_out_of_a_content_transform() {
1666 use crate::event::{ScrollAlign, ScrollMotion};
1667 use std::cell::RefCell;
1668 use std::rc::Rc;
1669 use teksilo_canvas::Transform2D;
1670
1671 let outer_seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1672 let view_seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1673
1674 let mut tree = WidgetTree::new();
1675 let card = tree.add(FillWidget::new());
1676 let view = recorder(&mut tree, card, view_seen.clone());
1677 let _outer = recorder(&mut tree, view, outer_seen.clone());
1678 tree.layout(SizeProposal::exact(400.0, 300.0));
1679 tree.set_content_transform(view, Transform2D::translate(-90.0, 0.0));
1682
1683 let card_rect = Rect::new(100.0, 100.0, 50.0, 50.0);
1684 let mut ops = crate::window::NoopWindowOps;
1685 tree.scroll_rect_into_view(
1686 card,
1687 card_rect,
1688 0.0,
1689 ScrollAlign::Minimal,
1690 ScrollMotion::Instant,
1691 &mut ops,
1692 );
1693
1694 assert!(
1695 view_seen.borrow().is_empty(),
1696 "the card paints at window x = 10, inside the 400×300 viewport — \
1697 comparing its scene rect against the viewport is what used to \
1698 produce a spurious verdict here, in either direction"
1699 );
1700 assert!(
1701 outer_seen.borrow().is_empty(),
1702 "and nothing outside it has a reason to scroll either"
1703 );
1704
1705 tree.set_content_transform(view, Transform2D::translate(-600.0, 0.0));
1709 tree.scroll_rect_into_view(
1710 card,
1711 card_rect,
1712 0.0,
1713 ScrollAlign::Minimal,
1714 ScrollMotion::Instant,
1715 &mut ops,
1716 );
1717 assert_eq!(
1718 view_seen.borrow().as_slice(),
1719 &[card_rect],
1720 "the view is asked to reveal the card, in the view's own content \
1721 space — the space `SceneView::ensure_visible` takes by signature"
1722 );
1723 assert_eq!(
1724 outer_seen.borrow().as_slice(),
1725 &[Rect::new(-500.0, 100.0, 50.0, 50.0)],
1726 "and everything further out is asked in window space, which is the \
1727 only space it can compare against its own viewport"
1728 );
1729 }
1730
1731 #[test]
1734 fn a_reveal_with_no_transform_in_the_chain_passes_the_rect_through() {
1735 use crate::event::{ScrollAlign, ScrollMotion};
1736 use std::cell::RefCell;
1737 use std::rc::Rc;
1738
1739 let seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1740 let mut tree = WidgetTree::new();
1741 let inside = tree.add(FillWidget::new());
1742 let _viewport = recorder(&mut tree, inside, seen.clone());
1743 tree.layout(SizeProposal::exact(400.0, 300.0));
1744
1745 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
1746 let mut ops = crate::window::NoopWindowOps;
1747 tree.scroll_rect_into_view(
1748 inside,
1749 target,
1750 0.0,
1751 ScrollAlign::Minimal,
1752 ScrollMotion::Instant,
1753 &mut ops,
1754 );
1755 assert_eq!(seen.borrow().as_slice(), &[target]);
1756 }
1757
1758 #[test]
1759 fn focus_widget() {
1760 let mut tree = WidgetTree::new();
1761 let widget = tree.add(FillWidget::new());
1762 tree.layout(SizeProposal::exact(100.0, 50.0));
1763 tree.focus(widget);
1764 assert_eq!(tree.focused(), Some(widget));
1765 }
1766
1767 #[test]
1768 fn focus_change() {
1769 let mut tree = WidgetTree::new();
1770 let a = tree.add(FillWidget::new());
1771 let b = tree.add(FillWidget::new());
1772 tree.layout(SizeProposal::exact(100.0, 50.0));
1773 tree.focus(a);
1774 assert_eq!(tree.focused(), Some(a));
1775 tree.focus(b);
1776 assert_eq!(tree.focused(), Some(b));
1777 }
1778
1779 #[derive(Debug)]
1783 struct RebuildingRows {
1784 epoch: crate::signal::Signal<u64>,
1785 rows: Vec<WidgetId>,
1786 }
1787
1788 impl Widget for RebuildingRows {
1789 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1790 let sid = ctx.self_id();
1791 let reg = ctx.binding_registry();
1792 self.epoch
1793 .bind_to(sid, reg, crate::binding::BindingLevel::Rebuild);
1794 self.rows = (0..3)
1795 .map(|_| ctx.add(FillWidget::new().focusable()))
1796 .collect();
1797 self.rows.clone()
1798 }
1799
1800 fn layout_response(
1801 &self,
1802 proposal: SizeProposal,
1803 _ctx: &LayoutContext,
1804 ) -> crate::widget::LayoutResponse {
1805 proposal.resolve(0.0, 0.0).into()
1806 }
1807
1808 fn children(&self) -> Vec<WidgetId> {
1809 self.rows.clone()
1810 }
1811 }
1812
1813 #[test]
1823 fn a_rebuild_keeps_focus_inside_the_subtree_that_had_it() {
1824 let mut tree = WidgetTree::new();
1825 let epoch = crate::signal::Signal::new(0u64);
1826 let root = tree.add(RebuildingRows {
1827 epoch: epoch.clone(),
1828 rows: Vec::new(),
1829 });
1830 tree.layout(SizeProposal::exact(100.0, 60.0));
1831
1832 let first_row = tree
1833 .first_focusable_descendant(root)
1834 .expect("the rows are focusable");
1835 tree.focus(first_row);
1836 assert_eq!(tree.focused(), Some(first_row));
1837
1838 epoch.set(1);
1840 tree.layout(SizeProposal::exact(100.0, 60.0));
1841
1842 let focused = tree
1843 .focused()
1844 .expect("a rebuild must not drop focus out of the rebuilt subtree");
1845 assert_ne!(
1846 focused, first_row,
1847 "the old row is dead — focus must have moved to a freshly built one"
1848 );
1849 assert!(
1850 tree.is_descendant_of(focused, root),
1851 "focus must land back inside the rebuilt subtree"
1852 );
1853 assert!(tree.is_active(focused), "the focused node must be live");
1854 }
1855
1856 #[test]
1864 fn a_rebuild_restores_focus_to_the_position_it_had() {
1865 let mut tree = WidgetTree::new();
1866 let epoch = crate::signal::Signal::new(0u64);
1867 let root = tree.add(RebuildingRows {
1868 epoch: epoch.clone(),
1869 rows: Vec::new(),
1870 });
1871 tree.layout(SizeProposal::exact(100.0, 60.0));
1872 let third = tree.children(root)[2];
1873 tree.focus(third);
1874
1875 epoch.set(1);
1876 tree.layout(SizeProposal::exact(100.0, 60.0));
1877
1878 let rows = tree.children(root);
1879 assert_ne!(rows[2], third, "precondition: the rows were rebuilt");
1880 assert_eq!(tree.focused(), Some(rows[2]), "the third row again");
1881 assert_eq!(
1882 tree.focus_origin(),
1883 Some(crate::focus::FocusOrigin::Restored)
1884 );
1885 }
1886
1887 #[test]
1892 fn a_rebuild_elsewhere_does_not_steal_focus() {
1893 let mut tree = WidgetTree::new();
1894 let epoch = crate::signal::Signal::new(0u64);
1895 let outsider = tree.add(FillWidget::new().focusable());
1896 let _rows = tree.add(RebuildingRows {
1897 epoch: epoch.clone(),
1898 rows: Vec::new(),
1899 });
1900 tree.layout(SizeProposal::exact(100.0, 60.0));
1901
1902 tree.focus(outsider);
1903 assert_eq!(tree.focused(), Some(outsider));
1904
1905 epoch.set(1);
1906 tree.layout(SizeProposal::exact(100.0, 60.0));
1907
1908 assert_eq!(
1909 tree.focused(),
1910 Some(outsider),
1911 "a rebuild that never held focus must not pull focus into itself"
1912 );
1913 }
1914
1915 #[derive(Debug)]
1919 struct KeepingGroups {
1920 epoch: crate::signal::Signal<u64>,
1921 group_epochs: Vec<crate::signal::Signal<u64>>,
1922 groups: Vec<WidgetId>,
1923 }
1924
1925 impl Widget for KeepingGroups {
1926 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1927 let sid = ctx.self_id();
1928 let reg = ctx.binding_registry();
1929 self.epoch
1930 .bind_to(sid, reg, crate::binding::BindingLevel::Rebuild);
1931 if self.groups.is_empty() {
1932 self.groups = self
1933 .group_epochs
1934 .iter()
1935 .map(|epoch| {
1936 ctx.add(RebuildingRows {
1937 epoch: epoch.clone(),
1938 rows: Vec::new(),
1939 })
1940 })
1941 .collect();
1942 }
1943 self.groups.clone()
1944 }
1945
1946 fn layout_response(
1947 &self,
1948 proposal: SizeProposal,
1949 _ctx: &LayoutContext,
1950 ) -> crate::widget::LayoutResponse {
1951 proposal.resolve(0.0, 0.0).into()
1952 }
1953
1954 fn children(&self) -> Vec<WidgetId> {
1955 self.groups.clone()
1956 }
1957
1958 fn preserves_children_on_rebuild(&self) -> bool {
1959 true
1960 }
1961 }
1962
1963 #[test]
1974 fn a_rebuild_restores_focus_into_the_innermost_ancestor_that_survived() {
1975 let mut tree = WidgetTree::new();
1976 let epoch = crate::signal::Signal::new(0u64);
1977 let group_epochs = vec![
1978 crate::signal::Signal::new(0u64),
1979 crate::signal::Signal::new(0u64),
1980 ];
1981 let root = tree.add(KeepingGroups {
1982 epoch: epoch.clone(),
1983 group_epochs: group_epochs.clone(),
1984 groups: Vec::new(),
1985 });
1986 tree.layout(SizeProposal::exact(100.0, 60.0));
1987
1988 let groups = tree.children(root);
1989 let second_group = groups[1];
1990 let row = tree
1991 .first_focusable_descendant(second_group)
1992 .expect("the second group's rows are focusable");
1993 tree.focus(row);
1994
1995 epoch.set(1);
1998 group_epochs[1].set(1);
1999 tree.layout(SizeProposal::exact(100.0, 60.0));
2000
2001 assert_eq!(
2002 tree.children(root),
2003 groups,
2004 "the container kept both groups (the premise of this test)"
2005 );
2006 let focused = tree.focused().expect("focus must not be dropped");
2007 assert_ne!(focused, row, "the old row is dead");
2008 assert!(
2009 tree.is_descendant_of(focused, second_group),
2010 "focus must land back in the group that held it, the innermost \
2011 ancestor that survived, not in the first group of the container"
2012 );
2013 }
2014
2015 #[test]
2016 fn first_focusable_descendant_prefers_first_focusable_child() {
2017 let mut tree = WidgetTree::new();
2018 let a = tree.add(FillWidget::new().focusable());
2019 let _not_focusable = tree.add(FillWidget::new());
2020 let b = tree.add(FillWidget::new().focusable());
2021 let root = tree.add(crate::test_widgets::StackWidget::new().child(a).child(b));
2022 tree.layout(SizeProposal::exact(100.0, 50.0));
2023
2024 assert_eq!(tree.first_focusable_descendant(root), Some(a));
2025 }
2026
2027 #[test]
2028 fn tab_cycles_through_focusable_widgets() {
2029 let mut tree = WidgetTree::new();
2030 let a = tree.add(FillWidget::new().focusable());
2031 let b = tree.add(FillWidget::new().focusable());
2032 let c = tree.add(FillWidget::new().focusable());
2033 tree.layout(SizeProposal::exact(100.0, 50.0));
2034
2035 assert_eq!(tree.focused(), None);
2036
2037 tree.press_key(Key::Tab, Modifiers::NONE);
2038 assert_eq!(tree.focused(), Some(a));
2039
2040 tree.press_key(Key::Tab, Modifiers::NONE);
2041 assert_eq!(tree.focused(), Some(b));
2042
2043 tree.press_key(Key::Tab, Modifiers::NONE);
2044 assert_eq!(tree.focused(), Some(c));
2045
2046 tree.press_key(Key::Tab, Modifiers::NONE);
2047 assert_eq!(tree.focused(), Some(a));
2048 }
2049
2050 #[test]
2051 fn tab_stop_on_composite_excludes_focusable_descendant() {
2052 let mut tree = WidgetTree::new();
2057 let leaf = tree.add(FillWidget::new().focusable());
2058 let composite = tree.add(crate::test_widgets::StackWidget::new().child(leaf));
2059 let other = tree.add(FillWidget::new().focusable());
2060 tree.layout(SizeProposal::exact(100.0, 50.0));
2061
2062 tree.set_tab_stop(composite, false);
2063
2064 tree.press_key(Key::Tab, Modifiers::NONE);
2065 assert_eq!(
2066 tree.focused(),
2067 Some(other),
2068 "Tab must skip the suppressed composite's inner leaf"
2069 );
2070 tree.press_key(Key::Tab, Modifiers::NONE);
2071 assert_eq!(
2072 tree.focused(),
2073 Some(other),
2074 "only the un-suppressed control participates in Tab"
2075 );
2076
2077 tree.set_tab_stop(composite, true);
2079 tree.set_focused(None);
2080 tree.press_key(Key::Tab, Modifiers::NONE);
2081 let first = tree.focused();
2082 tree.press_key(Key::Tab, Modifiers::NONE);
2083 let second = tree.focused();
2084 assert_ne!(
2085 first, second,
2086 "with the composite re-enabled, Tab visits both controls"
2087 );
2088 }
2089
2090 #[test]
2091 fn shift_tab_cycles_backwards() {
2092 let mut tree = WidgetTree::new();
2093 let a = tree.add(FillWidget::new().focusable());
2094 let b = tree.add(FillWidget::new().focusable());
2095 let c = tree.add(FillWidget::new().focusable());
2096 tree.layout(SizeProposal::exact(100.0, 50.0));
2097
2098 tree.press_key(Key::Tab, Modifiers::NONE);
2099 assert_eq!(tree.focused(), Some(a));
2100
2101 tree.press_key(Key::Tab, Modifiers::SHIFT);
2102 assert_eq!(tree.focused(), Some(c));
2103
2104 tree.press_key(Key::Tab, Modifiers::SHIFT);
2105 assert_eq!(tree.focused(), Some(b));
2106 }
2107
2108 #[test]
2109 fn tab_skips_non_focusable_widgets() {
2110 let mut tree = WidgetTree::new();
2111 let _not_focusable = tree.add(FillWidget::new());
2112 let a = tree.add(FillWidget::new().focusable());
2113 let _also_not = tree.add(FillWidget::new());
2114 let b = tree.add(FillWidget::new().focusable());
2115 tree.layout(SizeProposal::exact(100.0, 50.0));
2116
2117 tree.press_key(Key::Tab, Modifiers::NONE);
2118 assert_eq!(tree.focused(), Some(a));
2119
2120 tree.press_key(Key::Tab, Modifiers::NONE);
2121 assert_eq!(tree.focused(), Some(b));
2122 }
2123
2124 #[test]
2135 fn focus_does_not_check_that_the_node_is_focusable() {
2136 let mut tree = WidgetTree::new();
2137 let inert = tree.add(FillWidget::new()); tree.layout(SizeProposal::exact(100.0, 50.0));
2139 assert!(
2140 tree.tab_stops_within(inert).is_empty(),
2141 "the subject has to be unreachable for the point to be made"
2142 );
2143
2144 tree.focus(inert);
2145 assert_eq!(
2146 tree.focused(),
2147 Some(inert),
2148 "focus lands on a node Tab traversal can never offer"
2149 );
2150 }
2151
2152 #[test]
2153 fn tab_focus_has_keyboard_origin() {
2154 let mut tree = WidgetTree::new();
2155 tree.add(FillWidget::new().focusable());
2156 tree.layout(SizeProposal::exact(100.0, 50.0));
2157
2158 tree.press_key(Key::Tab, Modifiers::NONE);
2159 assert_eq!(
2160 tree.focus_origin(),
2161 Some(crate::focus::FocusOrigin::Keyboard)
2162 );
2163 }
2164
2165 #[test]
2166 fn tab_skips_focusable_inside_disabled_ancestor() {
2167 use crate::signal::Signal;
2168 use crate::test_widgets::StackWidget;
2169
2170 let mut tree = WidgetTree::new();
2171 let a = tree.add(FillWidget::new().focusable());
2172 let inner = tree.add(FillWidget::new().focusable());
2173 let disabled_container = tree.add(StackWidget::new().child(inner));
2174 let c = tree.add(FillWidget::new().focusable());
2175 tree.enabled_when(disabled_container, Signal::new(false));
2176 tree.layout(SizeProposal::exact(200.0, 100.0));
2177
2178 tree.press_key(Key::Tab, Modifiers::NONE);
2179 assert_eq!(tree.focused(), Some(a));
2180
2181 tree.press_key(Key::Tab, Modifiers::NONE);
2182 assert_eq!(
2183 tree.focused(),
2184 Some(c),
2185 "tab should skip the focusable widget nested inside the disabled container"
2186 );
2187 }
2188
2189 #[test]
2190 fn dormant_widget_not_in_focus_cycle() {
2191 let mut tree = WidgetTree::new();
2192 let a = tree.add(FillWidget::new().focusable());
2193 let b = tree.add(FillWidget::new().focusable());
2194 let c = tree.add(FillWidget::new().focusable());
2195 tree.layout(SizeProposal::exact(200.0, 100.0));
2196
2197 tree.focus(a);
2198 tree.set_dormant(b);
2199
2200 tree.press_key(Key::Tab, Modifiers::NONE);
2201 assert_eq!(tree.focused(), Some(c));
2202 }
2203
2204 #[test]
2209 fn revalidate_delivers_focus_lost_when_focused_widget_goes_dormant() {
2210 use std::cell::Cell;
2211 use std::rc::Rc;
2212
2213 let lost = Rc::new(Cell::new(0_u32));
2214 let gained = Rc::new(Cell::new(0_u32));
2215 let lost_c = lost.clone();
2216 let gained_c = gained.clone();
2217
2218 let mut tree = WidgetTree::new();
2219 let editor = tree.add(
2220 FillWidget::new()
2221 .focusable()
2222 .on_focus(move |is_gained, _ctx| {
2223 if is_gained {
2224 gained_c.set(gained_c.get() + 1);
2225 } else {
2226 lost_c.set(lost_c.get() + 1);
2227 }
2228 }),
2229 );
2230 let _other = tree.add(FillWidget::new().focusable());
2231 tree.layout(SizeProposal::exact(200.0, 100.0));
2232
2233 tree.focus(editor);
2234 assert_eq!(tree.focused(), Some(editor));
2235 assert_eq!(gained.get(), 1, "focus() delivers FocusGained");
2236 assert_eq!(lost.get(), 0);
2237
2238 tree.set_dormant(editor);
2240 let mut noop = crate::window::NoopWindowOps;
2242 tree.revalidate_interaction_state(&mut noop);
2243
2244 assert_eq!(
2245 tree.focused(),
2246 None,
2247 "tree focus must clear when the target is dormant"
2248 );
2249 assert_eq!(
2250 lost.get(),
2251 1,
2252 "FocusLost must reach the dormant widget so it can clear has_focus / caret blink"
2253 );
2254 }
2255
2256 #[test]
2257 fn tab_cycles_focus_in_tree_order() {
2258 let mut tree = WidgetTree::new();
2259 let a = tree.add(FillWidget::new().focusable());
2260 let b = tree.add(FillWidget::new().focusable());
2261 tree.layout(SizeProposal::exact(200.0, 80.0));
2262
2263 tree.focus(a);
2264 assert_eq!(tree.focused(), Some(a));
2265
2266 tree.press_key(Key::Tab, Modifiers::NONE);
2267 assert_eq!(tree.focused(), Some(b));
2268
2269 tree.press_key(Key::Tab, Modifiers::SHIFT);
2270 assert_eq!(tree.focused(), Some(a));
2271 }
2272
2273 #[test]
2274 fn focus_survives_theme_switch() {
2275 let mut tree = WidgetTree::new();
2276 let a = tree.add(FillWidget::new().focusable());
2277 let _b = tree.add(FillWidget::new().focusable());
2278 tree.layout(SizeProposal::exact(200.0, 80.0));
2279
2280 tree.focus(a);
2281 assert_eq!(tree.focused(), Some(a));
2282
2283 tree.set_theme(crate::presets::intui::dark());
2284 tree.layout(SizeProposal::exact(200.0, 80.0));
2285
2286 assert_eq!(
2287 tree.focused(),
2288 Some(a),
2289 "theme switch must not clobber focus"
2290 );
2291 }
2292
2293 #[test]
2294 fn focus_survives_locale_switch() {
2295 let mut tree = WidgetTree::new();
2296 let a = tree.add(FillWidget::new().focusable());
2297 tree.layout(SizeProposal::exact(200.0, 80.0));
2298
2299 tree.focus(a);
2300 tree.set_locale("fr-FR".to_string());
2301 tree.layout(SizeProposal::exact(200.0, 80.0));
2302
2303 assert_eq!(
2304 tree.focused(),
2305 Some(a),
2306 "locale switch must not clobber focus"
2307 );
2308 }
2309
2310 #[test]
2313 fn focus_within_flips_when_descendant_takes_focus() {
2314 use crate::signal::Signal;
2315 use crate::test_widgets::StackWidget;
2316 use crate::widget_builder::WidgetBuilder;
2317
2318 let halo = Signal::new(false);
2319
2320 let mut tree = WidgetTree::new();
2321 let leaf = tree.add(FillWidget::new().focusable());
2322 let mid = tree.add(StackWidget::new().child(leaf));
2323 let _outer = tree.add(StackWidget::new().child(mid).focus_within(halo.clone()));
2324
2325 tree.layout(SizeProposal::exact(100.0, 50.0));
2326 assert!(!halo.get(), "no focus yet → signal is false");
2327
2328 tree.focus(leaf);
2329 assert!(halo.get(), "leaf now focused, outer is its strict ancestor");
2330 }
2331
2332 #[test]
2333 fn focus_within_strict_ancestors_only() {
2334 use crate::signal::Signal;
2337 use crate::widget_builder::WidgetBuilder;
2338
2339 let halo = Signal::new(false);
2340
2341 let mut tree = WidgetTree::new();
2342 let widget = tree.add(FillWidget::new().focusable().focus_within(halo.clone()));
2343 tree.layout(SizeProposal::exact(100.0, 50.0));
2344 tree.focus(widget);
2345
2346 assert!(
2347 !halo.get(),
2348 "focusing the widget itself must not set its own focus_within"
2349 );
2350 }
2351
2352 #[test]
2353 fn view_focus_active_is_inclusive_for_the_view_itself() {
2354 use crate::test_widgets::StackWidget;
2360 use crate::widget_builder::WidgetBuilder;
2361
2362 let mut tree = WidgetTree::new();
2363 let item = tree.add(FillWidget::new()); let view = tree.add(StackWidget::new().child(item).focusable(true));
2365 let outside = tree.add(FillWidget::new().focusable());
2366 tree.layout(SizeProposal::exact(100.0, 50.0));
2367
2368 let active = tree.view_focus_active_for(item);
2369 assert!(!active.get(), "no focus yet → scope inactive");
2370
2371 tree.focus(view);
2372 assert!(
2373 active.get(),
2374 "view focused directly → scope active (focus_within would be false here)"
2375 );
2376
2377 tree.focus(outside);
2378 assert!(
2379 !active.get(),
2380 "focus moved outside the view → scope inactive"
2381 );
2382 }
2383
2384 #[test]
2385 fn begin_view_focus_for_keys_on_the_view_root_not_the_building_pane() {
2386 use crate::test_widgets::StackWidget;
2394 use crate::widget_builder::WidgetBuilder;
2395
2396 let mut tree = WidgetTree::new();
2397 let row = tree.add(FillWidget::new()); let pane = tree.add(StackWidget::new().child(row)); let root = tree.add(StackWidget::new().child(pane).focusable(true));
2400 tree.layout(SizeProposal::exact(100.0, 50.0));
2401
2402 let keyed_on_root = tree.begin_view_focus(root);
2404 tree.end_view_focus();
2405 let keyed_on_pane = tree.view_focus_signal_for(pane);
2407
2408 assert!(!keyed_on_root.get(), "no focus yet → inactive");
2409 tree.focus(root);
2410 assert!(keyed_on_root.get(), "view root focused → row scope active");
2411 assert!(
2412 !keyed_on_pane.get(),
2413 "pane-keyed scope stays false: the focused root is the pane's ancestor, not its descendant",
2414 );
2415 }
2416
2417 #[test]
2418 fn focus_visible_tracks_input_modality() {
2419 use crate::event::{Key, Modifiers};
2422 let mut tree = WidgetTree::new();
2423 let w = tree.add(FillWidget::new().focusable());
2424 tree.layout(SizeProposal::exact(100.0, 50.0));
2425 let vis = tree.focus_visible_signal();
2426 assert!(!vis.get(), "starts not focus-visible");
2427
2428 tree.press_key(Key::Tab, Modifiers::NONE);
2429 assert!(vis.get(), "keyboard input turns focus-visible ON");
2430
2431 tree.click(w);
2432 assert!(!vis.get(), "pointer input turns focus-visible OFF");
2433
2434 tree.press_key(Key::ArrowDown, Modifiers::NONE);
2435 assert!(vis.get(), "keyboard input turns it back ON");
2436 }
2437
2438 #[test]
2439 fn view_focus_active_without_focusable_ancestor_is_constant_true() {
2440 let mut tree = WidgetTree::new();
2443 let item = tree.add(FillWidget::new());
2444 tree.layout(SizeProposal::exact(100.0, 50.0));
2445 assert!(tree.view_focus_active_for(item).get());
2446 }
2447
2448 #[test]
2449 fn focus_within_diff_across_siblings() {
2450 use crate::signal::Signal;
2453 use crate::test_widgets::StackWidget;
2454 use crate::widget_builder::WidgetBuilder;
2455
2456 let sig_a = Signal::new(false);
2457 let sig_b = Signal::new(false);
2458
2459 let mut tree = WidgetTree::new();
2460 let leaf_a = tree.add(FillWidget::new().focusable());
2461 let leaf_b = tree.add(FillWidget::new().focusable());
2462 let mid_a = tree.add(StackWidget::new().child(leaf_a).focus_within(sig_a.clone()));
2463 let mid_b = tree.add(StackWidget::new().child(leaf_b).focus_within(sig_b.clone()));
2464 let _root = tree.add(StackWidget::new().child(mid_a).child(mid_b));
2465
2466 tree.layout(SizeProposal::exact(100.0, 50.0));
2467 tree.focus(leaf_a);
2468 assert!(sig_a.get(), "sig_a true after focusing leaf_a");
2469 assert!(!sig_b.get(), "sig_b false: leaf_a is not its descendant");
2470
2471 tree.focus(leaf_b);
2472 assert!(!sig_a.get(), "sig_a flipped to false on focus move");
2473 assert!(sig_b.get(), "sig_b flipped to true on focus move");
2474 }
2475
2476 #[test]
2477 fn focus_within_clears_when_focused_widget_destroyed() {
2478 use crate::signal::Signal;
2479 use crate::test_widgets::StackWidget;
2480 use crate::widget_builder::WidgetBuilder;
2481
2482 let halo = Signal::new(false);
2483
2484 let mut tree = WidgetTree::new();
2485 let leaf = tree.add(FillWidget::new().focusable());
2486 let _outer = tree.add(StackWidget::new().child(leaf).focus_within(halo.clone()));
2487
2488 tree.layout(SizeProposal::exact(100.0, 50.0));
2489 tree.focus(leaf);
2490 assert!(halo.get());
2491
2492 tree.destroy_subtree(leaf);
2493 assert!(
2494 !halo.get(),
2495 "destroying the focused widget must clear focus_within on its ancestors"
2496 );
2497 }
2498
2499 #[test]
2500 fn hover_within_flips_via_pointer_move() {
2501 use crate::signal::Signal;
2502 use crate::test_widgets::StackWidget;
2503 use crate::widget_builder::WidgetBuilder;
2504 use teksilo_canvas::Point;
2505
2506 let glow = Signal::new(false);
2507
2508 let mut tree = WidgetTree::new();
2509 let leaf = tree.add(FillWidget::new());
2510 let _outer = tree.add(StackWidget::new().child(leaf).hover_within(glow.clone()));
2511
2512 tree.layout(SizeProposal::exact(100.0, 50.0));
2513 assert!(!glow.get());
2514
2515 tree.pointer_move(Point::new(50.0, 25.0));
2516 assert!(glow.get(), "pointer over leaf → outer.hover_within = true");
2517
2518 tree.pointer_move(Point::new(500.0, 500.0));
2519 assert!(
2520 !glow.get(),
2521 "pointer moves outside the tree → hover_within clears"
2522 );
2523 }
2524
2525 #[test]
2526 fn hover_within_strict_ancestors_only() {
2527 use crate::signal::Signal;
2528 use crate::widget_builder::WidgetBuilder;
2529 use teksilo_canvas::Point;
2530
2531 let glow = Signal::new(false);
2532
2533 let mut tree = WidgetTree::new();
2534 let _w = tree.add(FillWidget::new().hover_within(glow.clone()));
2535 tree.layout(SizeProposal::exact(100.0, 50.0));
2536 tree.pointer_move(Point::new(50.0, 25.0));
2537
2538 assert!(
2539 !glow.get(),
2540 "hovering the widget itself must not set its own hover_within"
2541 );
2542 }
2543}
2544
2545#[cfg(test)]
2546mod tests_scope {
2547 use super::*;
2552 use crate::focus::TraversalScopePolicy::{Continue, Cycle};
2553 use crate::test_widgets::{FillWidget, StackWidget};
2554 use crate::widget_builder::WidgetBuilder;
2555
2556 fn indexed(tree: &mut WidgetTree, idx: i32) -> WidgetId {
2558 tree.add(FillWidget::new().focusable().tab_index(idx))
2559 }
2560
2561 fn tab(tree: &mut WidgetTree) {
2562 tree.press_key(Key::Tab, Modifiers::NONE);
2563 }
2564 fn shift_tab(tree: &mut WidgetTree) {
2565 tree.press_key(Key::Tab, Modifiers::SHIFT);
2566 }
2567
2568 #[test]
2569 fn a_closed_tab_gate_holds_its_subtree_out_of_tab_whatever_its_props_say() {
2570 use crate::signal::Signal;
2572 let mut tree = WidgetTree::new();
2573 let a = tree.add(FillWidget::new().focusable());
2574 let inner = tree.add(FillWidget::new().focusable());
2575 let gated = tree.add(StackWidget::new().child(inner));
2576 let b = tree.add(FillWidget::new().focusable());
2577 let root = tree.add(StackWidget::new().child(a).child(gated).child(b));
2578 tree.set_tab_stop(inner, true);
2579 let open = Signal::new(false);
2580 tree.set_tab_gate(gated, open.clone());
2581 tree.layout(SizeProposal::exact(100.0, 100.0));
2582
2583 assert_eq!(tree.tab_stops_within(root), vec![a, b]);
2584 assert_eq!(tree.first_focusable_descendant(gated), None);
2585 tab(&mut tree);
2586 tab(&mut tree);
2587 assert_eq!(tree.focused(), Some(b), "Tab steps over the gated subtree");
2588 assert_eq!(
2589 tree.focusables_in_tab_order(gated),
2590 vec![inner],
2591 "the listing that ignores Tab stops sees through the gate"
2592 );
2593 let snapshot = tree.accessibility_tree_snapshot();
2594 let node = crate::accessibility::widget_id_to_node_id(inner);
2595 assert!(
2596 snapshot.nodes.iter().any(|(id, _)| *id == node),
2597 "and assistive technology still sees the control"
2598 );
2599 tree.focus(inner);
2600 assert_eq!(tree.focused(), Some(inner), "a direct request still lands");
2601
2602 open.set(true);
2603 assert_eq!(tree.tab_stops_within(root), vec![a, inner, b]);
2604 }
2605
2606 #[test]
2607 fn focusables_in_tab_order_keeps_roving_items_and_changes_nothing() {
2608 use crate::signal::Signal;
2612 let mut tree = WidgetTree::new();
2613 let a2 = indexed(&mut tree, 2);
2614 let a1 = indexed(&mut tree, 1);
2615 let scope = tree.add(StackWidget::new().child(a2).child(a1));
2616 let off = tree.add(FillWidget::new().focusable());
2617 let d = tree.add(FillWidget::new().focusable());
2618 let disabled = tree.add(StackWidget::new().child(d));
2619 let b = tree.add(FillWidget::new().focusable());
2620 let root = tree.add(
2621 StackWidget::new()
2622 .child(scope)
2623 .child(off)
2624 .child(disabled)
2625 .child(b),
2626 );
2627 tree.set_traversal_scope(scope, Continue);
2628 tree.set_tab_stop(off, false);
2629 tree.enabled_when(disabled, Signal::new(false));
2630 tree.layout(SizeProposal::exact(100.0, 100.0));
2631
2632 assert_eq!(tree.focusables_in_tab_order(root), vec![a1, a2, off, b]);
2633 assert_eq!(
2634 tree.tab_stops_within(root),
2635 vec![a2, a1, b],
2636 "and the Tab stops are as they were"
2637 );
2638 }
2639
2640 #[test]
2641 fn overlapping_tab_index_in_sibling_scopes_does_not_interleave() {
2642 let mut tree = WidgetTree::new();
2643 let a1 = indexed(&mut tree, 1);
2644 let a2 = indexed(&mut tree, 2);
2645 let scope_a = tree.add(StackWidget::new().child(a1).child(a2));
2646 let b1 = indexed(&mut tree, 1);
2647 let b2 = indexed(&mut tree, 2);
2648 let scope_b = tree.add(StackWidget::new().child(b1).child(b2));
2649 let _root = tree.add(StackWidget::new().child(scope_a).child(scope_b));
2650 tree.set_traversal_scope(scope_a, Continue);
2651 tree.set_traversal_scope(scope_b, Continue);
2652 tree.layout(SizeProposal::exact(100.0, 100.0));
2653
2654 for expected in [a1, a2, b1, b2, a1] {
2656 tab(&mut tree);
2657 assert_eq!(tree.focused(), Some(expected));
2658 }
2659 }
2660
2661 #[test]
2662 fn continue_scope_flows_out_at_the_ends() {
2663 let mut tree = WidgetTree::new();
2665 let a = tree.add(FillWidget::new().focusable());
2666 let c1 = tree.add(FillWidget::new().focusable());
2667 let c2 = tree.add(FillWidget::new().focusable());
2668 let scope_c = tree.add(StackWidget::new().child(c1).child(c2));
2669 let b = tree.add(FillWidget::new().focusable());
2670 let _root = tree.add(StackWidget::new().child(a).child(scope_c).child(b));
2671 tree.set_traversal_scope(scope_c, Continue);
2672 tree.layout(SizeProposal::exact(100.0, 100.0));
2673
2674 for expected in [a, c1, c2, b, a] {
2675 tab(&mut tree);
2676 assert_eq!(tree.focused(), Some(expected));
2677 }
2678 tree.focus(c1);
2680 shift_tab(&mut tree);
2681 assert_eq!(tree.focused(), Some(a));
2682 }
2683
2684 #[test]
2685 fn cycle_scope_wraps_and_never_escapes() {
2686 let mut tree = WidgetTree::new();
2688 let a = tree.add(FillWidget::new().focusable());
2689 let d1 = tree.add(FillWidget::new().focusable());
2690 let d2 = tree.add(FillWidget::new().focusable());
2691 let scope_d = tree.add(StackWidget::new().child(d1).child(d2));
2692 let _root = tree.add(StackWidget::new().child(a).child(scope_d));
2693 tree.set_traversal_scope(scope_d, Cycle);
2694 tree.layout(SizeProposal::exact(100.0, 100.0));
2695
2696 tree.focus(d1);
2697 for _ in 0..10 {
2698 tab(&mut tree);
2699 let f = tree.focused();
2700 assert!(
2701 f == Some(d1) || f == Some(d2),
2702 "Cycle scope must trap Tab inside {{d1,d2}}, got {f:?}"
2703 );
2704 }
2705 tree.focus(d1);
2707 tab(&mut tree);
2708 assert_eq!(tree.focused(), Some(d2));
2709 shift_tab(&mut tree);
2710 assert_eq!(tree.focused(), Some(d1));
2711 shift_tab(&mut tree);
2712 assert_eq!(tree.focused(), Some(d2), "Shift+Tab at start wraps to last");
2713 }
2714
2715 #[test]
2716 fn empty_scope_is_skipped() {
2717 let mut tree = WidgetTree::new();
2719 let a = tree.add(FillWidget::new().focusable());
2720 let empty = tree.add(StackWidget::new());
2721 let b = tree.add(FillWidget::new().focusable());
2722 let _root = tree.add(StackWidget::new().child(a).child(empty).child(b));
2723 tree.set_traversal_scope(empty, Continue);
2724 tree.layout(SizeProposal::exact(100.0, 100.0));
2725
2726 for expected in [a, b, a] {
2727 tab(&mut tree);
2728 assert_eq!(tree.focused(), Some(expected));
2729 }
2730 }
2731
2732 #[test]
2733 fn single_member_cycle_scope_stays_put() {
2734 let mut tree = WidgetTree::new();
2735 let e = tree.add(FillWidget::new().focusable());
2736 let scope_e = tree.add(StackWidget::new().child(e));
2737 tree.set_traversal_scope(scope_e, Cycle);
2738 tree.layout(SizeProposal::exact(100.0, 50.0));
2739
2740 tree.focus(e);
2741 tab(&mut tree);
2742 assert_eq!(tree.focused(), Some(e));
2743 shift_tab(&mut tree);
2744 assert_eq!(tree.focused(), Some(e));
2745 }
2746
2747 #[test]
2748 fn nested_continue_in_cycle_flows_out_to_outer() {
2749 let mut tree = WidgetTree::new();
2751 let x = tree.add(FillWidget::new().focusable());
2752 let i1 = tree.add(FillWidget::new().focusable());
2753 let i2 = tree.add(FillWidget::new().focusable());
2754 let inner = tree.add(StackWidget::new().child(i1).child(i2));
2755 let y = tree.add(FillWidget::new().focusable());
2756 let outer = tree.add(StackWidget::new().child(x).child(inner).child(y));
2757 tree.set_traversal_scope(inner, Continue);
2758 tree.set_traversal_scope(outer, Cycle);
2759 tree.layout(SizeProposal::exact(100.0, 100.0));
2760
2761 for expected in [x, i1, i2, y, x] {
2762 tab(&mut tree);
2763 assert_eq!(tree.focused(), Some(expected));
2764 }
2765 tree.focus(i1);
2767 shift_tab(&mut tree);
2768 assert_eq!(tree.focused(), Some(x));
2769 }
2770
2771 #[test]
2772 fn nested_cycle_in_cycle_traps_in_the_inner_scope() {
2773 let mut tree = WidgetTree::new();
2776 let x = tree.add(FillWidget::new().focusable());
2777 let i1 = tree.add(FillWidget::new().focusable());
2778 let i2 = tree.add(FillWidget::new().focusable());
2779 let inner = tree.add(StackWidget::new().child(i1).child(i2));
2780 let y = tree.add(FillWidget::new().focusable());
2781 let outer = tree.add(StackWidget::new().child(x).child(inner).child(y));
2782 tree.set_traversal_scope(inner, Cycle);
2783 tree.set_traversal_scope(outer, Cycle);
2784 tree.layout(SizeProposal::exact(100.0, 100.0));
2785
2786 tree.focus(i1);
2787 for _ in 0..6 {
2788 tab(&mut tree);
2789 let f = tree.focused();
2790 assert!(
2791 f == Some(i1) || f == Some(i2),
2792 "inner Cycle must trap Tab; reached {f:?}"
2793 );
2794 }
2795 }
2796
2797 #[test]
2798 fn scoped_tab_index_orders_within_a_scope() {
2799 let mut tree = WidgetTree::new();
2801 let f3 = indexed(&mut tree, 3);
2802 let f1 = indexed(&mut tree, 1);
2803 let f2 = indexed(&mut tree, 2);
2804 let scope_f = tree.add(StackWidget::new().child(f3).child(f1).child(f2));
2805 tree.set_traversal_scope(scope_f, Cycle);
2806 tree.layout(SizeProposal::exact(100.0, 100.0));
2807
2808 for expected in [f1, f2, f3, f1] {
2809 tab(&mut tree);
2810 assert_eq!(tree.focused(), Some(expected));
2811 }
2812 }
2813
2814 #[test]
2815 fn destroyed_focused_widget_re_enters_at_first() {
2816 let mut tree = WidgetTree::new();
2817 let a = tree.add(FillWidget::new().focusable());
2818 let b = tree.add(FillWidget::new().focusable());
2819 tree.layout(SizeProposal::exact(100.0, 50.0));
2820
2821 tree.focus(b);
2822 tree.destroy_subtree(b);
2823 tab(&mut tree);
2824 assert_eq!(tree.focused(), Some(a), "Tab after destroy enters at first");
2825 }
2826
2827 #[test]
2828 fn set_traversal_scope_forces_node_non_focusable() {
2829 let mut tree = WidgetTree::new();
2832 let inner = tree.add(FillWidget::new().focusable());
2833 let scope = tree.add(StackWidget::new().child(inner).focusable(true));
2834 tree.set_traversal_scope(scope, Continue);
2835 tree.layout(SizeProposal::exact(100.0, 50.0));
2836
2837 tab(&mut tree);
2839 assert_eq!(tree.focused(), Some(inner));
2840 tab(&mut tree);
2841 assert_eq!(tree.focused(), Some(inner));
2842 }
2843
2844 #[test]
2845 fn no_scopes_behaves_like_a_flat_cycle() {
2846 let mut tree = WidgetTree::new();
2849 let a = indexed(&mut tree, 2);
2850 let b = indexed(&mut tree, 1);
2851 let c = tree.add(FillWidget::new().focusable());
2852 tree.layout(SizeProposal::exact(100.0, 50.0));
2853
2854 for expected in [b, a, c, b] {
2856 tab(&mut tree);
2857 assert_eq!(tree.focused(), Some(expected));
2858 }
2859 }
2860
2861 #[test]
2862 fn centered_modal_confines_tab_to_its_content() {
2863 let mut tree = WidgetTree::new();
2866 let outside1 = tree.add(FillWidget::new().focusable());
2867 let outside2 = tree.add(FillWidget::new().focusable());
2868 let m1 = tree.add(FillWidget::new().focusable());
2869 let m2 = tree.add(FillWidget::new().focusable());
2870 let content = tree.add(StackWidget::new().child(m1).child(m2));
2871 tree.layout(SizeProposal::exact(200.0, 100.0));
2872
2873 tree.show_overlay(crate::overlay::OverlayRequest::new(
2874 content,
2875 outside1,
2876 crate::overlay::OverlayPlacement::Centered,
2877 crate::overlay::DismissBehavior::Manual,
2878 ));
2879
2880 for _ in 0..6 {
2881 tab(&mut tree);
2882 let f = tree.focused();
2883 assert!(
2884 f == Some(m1) || f == Some(m2),
2885 "modal must trap Tab inside its content, reached {f:?}"
2886 );
2887 }
2888 assert_ne!(tree.focused(), Some(outside1));
2889 assert_ne!(tree.focused(), Some(outside2));
2890 }
2891
2892 #[test]
2896 fn a_sticky_tooltip_above_a_modal_does_not_trap_tab() {
2897 let mut tree = WidgetTree::new();
2898 tree.set_accessibility_preferences(false, true, 1.0);
2899 let outside = tree.add(FillWidget::new().focusable());
2900 let m1 = tree.add(FillWidget::new().focusable());
2901 let anchor = tree.add(FillWidget::new());
2902 let modal = tree.add(StackWidget::new().child(m1).child(anchor));
2903 let t_a = tree.add(FillWidget::new().focusable());
2904 let t_b = tree.add(FillWidget::new().focusable());
2905 let tip = tree.add(StackWidget::new().child(t_a).child(t_b));
2906 tree.layout(SizeProposal::exact(200.0, 100.0));
2907 tree.show_overlay(crate::overlay::OverlayRequest::new(
2908 modal,
2909 outside,
2910 crate::overlay::OverlayPlacement::Centered,
2911 crate::overlay::DismissBehavior::Manual,
2912 ));
2913 tree.attach_tooltip_with_sticky(
2914 anchor,
2915 tip,
2916 std::time::Duration::from_millis(500),
2917 Some(std::time::Duration::from_millis(100)),
2918 );
2919 tree.tooltip_pointer_enter(anchor);
2920 tree.advance_time(std::time::Duration::from_millis(550));
2921 tree.promote_tooltip_to_sticky(tip);
2922 tree.layout(SizeProposal::exact(200.0, 100.0));
2923 tree.focus(t_a);
2924 assert_eq!(tree.focused(), Some(t_a), "focus is in the sticky tooltip");
2925
2926 let mut reached = Vec::new();
2927 for _ in 0..4 {
2928 tab(&mut tree);
2929 reached.push(tree.focused());
2930 }
2931 assert!(
2932 reached.contains(&Some(m1)),
2933 "Tab leaves the tooltip for the modal, reached {reached:?}"
2934 );
2935 }
2936
2937 #[test]
2940 fn an_event_context_says_whether_a_modal_is_open() {
2941 let mut tree = WidgetTree::new();
2942 let outside = tree.add(FillWidget::new().focusable());
2943 let m1 = tree.add(FillWidget::new().focusable());
2944 let content = tree.add(StackWidget::new().child(m1));
2945 tree.layout(SizeProposal::exact(200.0, 100.0));
2946 let ask = |tree: &mut WidgetTree| {
2947 let seen = std::rc::Rc::new(std::cell::Cell::new(None));
2948 let out = seen.clone();
2949 tree.run_with_event_context(&mut crate::window::NoopWindowOps, move |ctx| {
2950 out.set(Some(ctx.is_modal_open()));
2951 });
2952 seen.get()
2953 };
2954 assert_eq!(ask(&mut tree), Some(false));
2955 tree.show_overlay(crate::overlay::OverlayRequest::new(
2956 content,
2957 outside,
2958 crate::overlay::OverlayPlacement::Centered,
2959 crate::overlay::DismissBehavior::Manual,
2960 ));
2961 assert_eq!(ask(&mut tree), Some(true));
2962 }
2963
2964 #[test]
2969 fn a_cycle_popup_above_a_modal_keeps_tab_inside_it() {
2970 let mut tree = WidgetTree::new();
2971 let outside = tree.add(FillWidget::new().focusable());
2972 let field = tree.add(FillWidget::new().focusable());
2973 let after = tree.add(FillWidget::new().focusable());
2974 let modal = tree.add(StackWidget::new().child(field).child(after));
2975 let p1 = tree.add(FillWidget::new().focusable());
2976 let p2 = tree.add(FillWidget::new().focusable());
2977 let popup = tree.add(StackWidget::new().child(p1).child(p2));
2978 tree.set_traversal_scope(popup, Cycle);
2979 tree.layout(SizeProposal::exact(200.0, 100.0));
2980 tree.show_overlay(crate::overlay::OverlayRequest::new(
2981 modal,
2982 outside,
2983 crate::overlay::OverlayPlacement::Centered,
2984 crate::overlay::DismissBehavior::Manual,
2985 ));
2986 tree.show_overlay(crate::overlay::OverlayRequest::new(
2987 popup,
2988 field,
2989 crate::overlay::OverlayPlacement::Below,
2990 crate::overlay::DismissBehavior::EscapeOrClickOutside,
2991 ));
2992 tree.layout(SizeProposal::exact(200.0, 100.0));
2993 tree.focus(p1);
2994
2995 for expected in [p2, p1, p2] {
2996 tab(&mut tree);
2997 assert_eq!(tree.focused(), Some(expected), "Tab wraps inside the popup");
2998 }
2999 tree.press_key(Key::Tab, Modifiers::SHIFT);
3000 assert_eq!(tree.focused(), Some(p1), "Shift+Tab too");
3001 assert_eq!(tree.active_overlays().len(), 2, "the popup is still open");
3002
3003 tree.focus(field);
3005 tab(&mut tree);
3006 assert_eq!(tree.focused(), Some(after));
3007 }
3008}
3009
3010#[cfg(test)]
3011mod tests_focus_out_dismissal {
3012 use super::*;
3019 use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
3020 use crate::test_widgets::{FillWidget, StackWidget};
3021
3022 fn tab(tree: &mut WidgetTree) {
3023 tree.press_key(Key::Tab, Modifiers::NONE);
3024 }
3025
3026 fn show_anchored(
3028 tree: &mut WidgetTree,
3029 content: WidgetId,
3030 anchor: WidgetId,
3031 parent: Option<crate::overlay::OverlayId>,
3032 ) -> crate::overlay::OverlayId {
3033 let mut request = OverlayRequest::new(
3034 content,
3035 anchor,
3036 OverlayPlacement::Below,
3037 DismissBehavior::EscapeOrClickOutside,
3038 );
3039 request.parent_overlay = parent;
3040 tree.show_overlay(request)
3041 }
3042
3043 #[test]
3044 fn tab_out_of_a_non_modal_overlay_dismisses_it() {
3045 let mut tree = WidgetTree::new();
3046 let anchor = tree.add(FillWidget::new().focusable());
3047 let after = tree.add(FillWidget::new().focusable());
3048 let inner = tree.add(FillWidget::new().focusable());
3049 let content = tree.add(StackWidget::new().child(inner));
3050 tree.layout(SizeProposal::exact(200.0, 100.0));
3051 show_anchored(&mut tree, content, anchor, None);
3052
3053 tree.focus(inner);
3054 tab(&mut tree);
3055
3056 assert_ne!(tree.focused(), Some(inner), "focus genuinely left");
3057 assert!(
3058 tree.active_overlays().is_empty(),
3059 "an overlay must not stay open over the focus ring that left it"
3060 );
3061 assert!(tree.focused() == Some(after) || tree.focused() == Some(anchor));
3062 }
3063
3064 #[test]
3066 fn a_centered_modal_is_never_dismissed_by_focus_moving() {
3067 let mut tree = WidgetTree::new();
3068 let outside = tree.add(FillWidget::new().focusable());
3069 let m1 = tree.add(FillWidget::new().focusable());
3070 let content = tree.add(StackWidget::new().child(m1));
3071 tree.layout(SizeProposal::exact(200.0, 100.0));
3072 tree.show_overlay(OverlayRequest::new(
3073 content,
3074 outside,
3075 OverlayPlacement::Centered,
3076 DismissBehavior::Manual,
3077 ));
3078
3079 tree.focus(m1);
3080 tree.focus(outside);
3084
3085 assert_eq!(
3086 tree.active_overlays().len(),
3087 1,
3088 "a modal is the one overlay that legitimately contains focus"
3089 );
3090 }
3091
3092 #[test]
3096 fn the_modal_scrim_survives_focus_moving_inside_the_modal() {
3097 let mut tree = WidgetTree::new();
3098 let opener = tree.add(FillWidget::new().focusable());
3099 let m1 = tree.add(FillWidget::new().focusable());
3100 let m2 = tree.add(FillWidget::new().focusable());
3101 let scrim = tree.add(FillWidget::new());
3102 let content = tree.add(StackWidget::new().child(m1).child(m2));
3103 tree.layout(SizeProposal::exact(200.0, 100.0));
3104
3105 tree.show_overlay(OverlayRequest::new(
3106 scrim,
3107 opener,
3108 OverlayPlacement::FullViewport,
3109 DismissBehavior::Manual,
3110 ));
3111 tree.show_overlay(OverlayRequest::new(
3112 content,
3113 opener,
3114 OverlayPlacement::Centered,
3115 DismissBehavior::Manual,
3116 ));
3117
3118 tree.focus(opener);
3119 tree.focus(m1);
3120 tab(&mut tree);
3121
3122 assert_eq!(
3123 tree.active_overlays().len(),
3124 2,
3125 "scrim and modal both stand while focus moves within the modal"
3126 );
3127 }
3128
3129 #[test]
3132 fn focus_moving_into_a_child_overlay_keeps_the_parent_open() {
3133 let mut tree = WidgetTree::new();
3134 let anchor = tree.add(FillWidget::new().focusable());
3135 let parent_item = tree.add(FillWidget::new().focusable());
3136 let parent_content = tree.add(StackWidget::new().child(parent_item));
3137 let child_item = tree.add(FillWidget::new().focusable());
3138 let child_content = tree.add(StackWidget::new().child(child_item));
3139 tree.layout(SizeProposal::exact(200.0, 100.0));
3140
3141 let parent = show_anchored(&mut tree, parent_content, anchor, None);
3142 show_anchored(&mut tree, child_content, parent_item, Some(parent));
3143
3144 tree.focus(parent_item);
3145 tree.focus(child_item);
3146
3147 assert_eq!(
3148 tree.active_overlays().len(),
3149 2,
3150 "opening a submenu is not leaving the menu that owns it"
3151 );
3152 }
3153
3154 #[test]
3157 fn focus_back_to_the_parent_overlay_closes_only_the_child() {
3158 let mut tree = WidgetTree::new();
3159 let anchor = tree.add(FillWidget::new().focusable());
3160 let parent_item = tree.add(FillWidget::new().focusable());
3161 let parent_content = tree.add(StackWidget::new().child(parent_item));
3162 let child_item = tree.add(FillWidget::new().focusable());
3163 let child_content = tree.add(StackWidget::new().child(child_item));
3164 tree.layout(SizeProposal::exact(200.0, 100.0));
3165
3166 let parent = show_anchored(&mut tree, parent_content, anchor, None);
3167 show_anchored(&mut tree, child_content, parent_item, Some(parent));
3168
3169 tree.focus(child_item);
3170 tree.focus(parent_item);
3171
3172 assert_eq!(
3173 tree.active_overlays(),
3174 vec![parent],
3175 "the submenu goes, the menu that owns it stays"
3176 );
3177 }
3178
3179 #[test]
3182 fn leaving_a_nested_cascade_closes_every_level() {
3183 let mut tree = WidgetTree::new();
3184 let anchor = tree.add(FillWidget::new().focusable());
3185 let away = tree.add(FillWidget::new().focusable());
3186 let parent_item = tree.add(FillWidget::new().focusable());
3187 let parent_content = tree.add(StackWidget::new().child(parent_item));
3188 let child_item = tree.add(FillWidget::new().focusable());
3189 let child_content = tree.add(StackWidget::new().child(child_item));
3190 tree.layout(SizeProposal::exact(200.0, 100.0));
3191
3192 let parent = show_anchored(&mut tree, parent_content, anchor, None);
3193 show_anchored(&mut tree, child_content, parent_item, Some(parent));
3194
3195 tree.focus(child_item);
3196 tree.focus(away);
3197
3198 assert!(
3199 tree.active_overlays().is_empty(),
3200 "one move out of the cascade must leave nothing behind"
3201 );
3202 }
3203
3204 #[test]
3214 fn a_dropdown_inside_a_modal_still_follows_focus_out() {
3215 let mut tree = WidgetTree::new();
3216 let opener = tree.add(FillWidget::new().focusable());
3217 let trigger = tree.add(FillWidget::new().focusable());
3218 let next = tree.add(FillWidget::new().focusable());
3219 let modal_content = tree.add(StackWidget::new().child(trigger).child(next));
3220 let panel = tree.add(StackWidget::new());
3221 tree.layout(SizeProposal::exact(200.0, 100.0));
3222
3223 tree.show_overlay(OverlayRequest::new(
3224 modal_content,
3225 opener,
3226 OverlayPlacement::Centered,
3227 DismissBehavior::Manual,
3228 ));
3229 show_anchored(&mut tree, panel, trigger, None);
3231
3232 tree.focus(trigger);
3233 assert_eq!(
3234 tree.active_overlays().len(),
3235 2,
3236 "precondition: modal + panel"
3237 );
3238
3239 tab(&mut tree);
3240
3241 assert_eq!(
3242 tree.focused(),
3243 Some(next),
3244 "Tab moves on within the modal, as it should"
3245 );
3246 assert_eq!(
3247 tree.active_overlays().len(),
3248 1,
3249 "the dropdown must go — only the modal hosting it stays"
3250 );
3251 }
3252
3253 #[test]
3261 fn a_viewport_placed_notification_ignores_focus_moving() {
3262 let mut tree = WidgetTree::new();
3263 let trigger = tree.add(FillWidget::new().focusable());
3264 let elsewhere = tree.add(FillWidget::new().focusable());
3265 let snack = tree.add(StackWidget::new());
3266 tree.layout(SizeProposal::exact(200.0, 100.0));
3267 tree.show_overlay(OverlayRequest::new(
3268 snack,
3269 trigger,
3270 OverlayPlacement::BottomCenter,
3271 DismissBehavior::Manual,
3272 ));
3273
3274 tree.focus(trigger);
3275 tab(&mut tree);
3276
3277 assert_eq!(
3278 tree.active_overlays().len(),
3279 1,
3280 "a snackbar outlives the keystroke that moved focus off its trigger"
3281 );
3282 assert_ne!(tree.focused(), Some(trigger), "and focus did move");
3283 assert_eq!(tree.focused(), Some(elsewhere));
3284 }
3285
3286 #[test]
3290 fn leaving_a_hosted_menu_spares_the_host() {
3291 let mut tree = WidgetTree::new();
3292 let opener = tree.add(FillWidget::new().focusable());
3293 let away = tree.add(FillWidget::new().focusable());
3294 let modal_item = tree.add(FillWidget::new().focusable());
3295 let modal_content = tree.add(StackWidget::new().child(modal_item));
3296 let menu_item = tree.add(FillWidget::new().focusable());
3297 let menu_content = tree.add(StackWidget::new().child(menu_item));
3298 tree.layout(SizeProposal::exact(200.0, 100.0));
3299
3300 let host = tree.show_overlay(OverlayRequest::new(
3302 modal_content,
3303 opener,
3304 OverlayPlacement::Centered,
3305 DismissBehavior::Manual,
3306 ));
3307 show_anchored(&mut tree, menu_content, modal_item, Some(host));
3308
3309 tree.focus(menu_item);
3310 tree.focus(away);
3311
3312 assert_eq!(
3313 tree.active_overlays(),
3314 vec![host],
3315 "the menu goes; the modal hosting it stays"
3316 );
3317 }
3318
3319 #[test]
3323 fn leaving_the_anchor_dismisses_a_panel_focus_never_entered() {
3324 let mut tree = WidgetTree::new();
3325 let trigger = tree.add(FillWidget::new().focusable());
3326 let after = tree.add(FillWidget::new().focusable());
3327 let content = tree.add(StackWidget::new());
3328 tree.layout(SizeProposal::exact(200.0, 100.0));
3329 show_anchored(&mut tree, content, trigger, None);
3330
3331 tree.focus(trigger);
3332 assert_eq!(tree.active_overlays().len(), 1, "precondition: panel is up");
3333
3334 tree.focus(after);
3335 assert!(
3336 tree.active_overlays().is_empty(),
3337 "leaving the trigger is leaving the dropdown it owns"
3338 );
3339 }
3340}