1use super::*;
5
6impl WidgetTree {
7 pub fn focus_with_origin_ops(
26 &mut self,
27 id: WidgetId,
28 origin: crate::focus::FocusOrigin,
29 ops: &mut dyn crate::window::WindowOps,
30 ) {
31 if self.focused == Some(id) {
32 return;
33 }
34 struct ClearOnDrop(std::rc::Rc<std::cell::Cell<bool>>);
40 impl Drop for ClearOnDrop {
41 fn drop(&mut self) {
42 self.0.set(false);
43 }
44 }
45 self.in_focus_dispatch.set(true);
46 let _focus_dispatch_guard = ClearOnDrop(self.in_focus_dispatch.clone());
47
48 let previously_focused = self.focused;
49 if let Some(old) = self.focused {
50 let old_overlay = self.overlay_ancestor_for_widget(old);
51 let new_overlay = self.overlay_ancestor_for_widget(id);
52 let moving_into_descendant_overlay = match (old_overlay, new_overlay) {
53 (Some(old_overlay), Some(new_overlay)) => self
54 .overlay_manager
55 .is_descendant_of(new_overlay, old_overlay),
56 _ => false,
57 };
58
59 if moving_into_descendant_overlay {
60 self.dispatch_to_widget_direct(old, &WidgetEvent::FocusLost, &mut *ops);
61 } else {
62 self.dispatch_to_widget(old, &WidgetEvent::FocusLost, &mut *ops);
63 }
64 }
65 self.set_focused(Some(id));
66 self.focus_origin = Some(origin);
67 if let Some(visible) = origin.focus_visible()
76 && self.focus_visible.get() != visible
77 {
78 self.focus_visible.set(visible);
79 }
80 self.a11y_dirty = true;
81 self.update_focus_within_signals(previously_focused, Some(id));
82 self.update_view_focus_signals(previously_focused, Some(id));
83 self.dispatch_to_widget(id, &WidgetEvent::FocusGained { origin }, &mut *ops);
84 self.dismiss_overlays_left_by_focus(previously_focused, id, &mut *ops);
94 self.tooltip_focus_leave_outside(Some(id), &mut *ops);
101 self.tooltip_focus_enter(id);
102 if !origin.is_pointer() {
109 self.scroll_focused_into_view(id, &mut *ops);
110 }
111 }
112
113 pub fn focus_with_origin(&mut self, id: WidgetId, origin: crate::focus::FocusOrigin) {
117 let mut noop = crate::window::NoopWindowOps;
118 self.focus_with_origin_ops(id, origin, &mut noop);
119 }
120
121 fn scroll_focused_into_view(
124 &mut self,
125 focused_id: WidgetId,
126 ops: &mut dyn crate::window::WindowOps,
127 ) {
128 let focused_bounds = self.arena.bounds(focused_id);
129 let reveal = self
134 .arena
135 .get(focused_id)
136 .and_then(|node| node.widget.focus_reveal_rect(focused_bounds))
137 .unwrap_or(focused_bounds);
138 self.scroll_rect_into_view(
139 focused_id,
140 reveal,
141 0.0,
142 crate::event::ScrollAlign::Minimal,
143 crate::event::ScrollMotion::Instant,
144 &mut *ops,
145 );
146 }
147
148 pub(super) fn scroll_rect_into_view(
201 &mut self,
202 from: WidgetId,
203 rect: Rect,
204 margin: f32,
205 align: crate::event::ScrollAlign,
206 motion: crate::event::ScrollMotion,
207 ops: &mut dyn crate::window::WindowOps,
208 ) {
209 let applied = std::sync::Arc::new(std::sync::Mutex::new(Point::ZERO));
215 let mut rect = rect;
216 let mut current = self.arena.parent(from);
217 let mut pending_align = align;
220 while let Some(ancestor_id) = current {
221 let to_parent = self
236 .arena
237 .get(ancestor_id)
238 .and_then(|n| n.transform_prop.as_ref())
239 .map(|t| t.get())
240 .filter(|t| !t.is_identity());
241 let content_transform = self
242 .arena
243 .get(ancestor_id)
244 .is_some_and(|n| n.content_transform);
245
246 if let Some(node) = self.arena.get(ancestor_id)
247 && node.clips_children
248 {
249 let viewport = node.bounds;
250 let against = match (content_transform, to_parent.as_ref()) {
257 (true, Some(t)) => t.apply_rect(rect),
258 _ => rect,
259 };
260 let align =
261 std::mem::replace(&mut pending_align, crate::event::ScrollAlign::Minimal);
262 let needs_scroll = matches!(align, crate::event::ScrollAlign::Fraction(_))
265 || against.y - margin < viewport.y
266 || against.bottom() + margin > viewport.bottom()
267 || against.x - margin < viewport.x
268 || against.right() + margin > viewport.right();
269
270 if needs_scroll {
271 *applied.lock().unwrap() = Point::ZERO;
272 self.dispatch_to_widget_direct(
286 ancestor_id,
287 &WidgetEvent::ScrollIntoView {
288 target_bounds: rect,
294 margin,
295 align,
296 motion,
297 applied_scroll: Some(applied.clone()),
298 },
299 &mut *ops,
300 );
301 let delta = *applied.lock().unwrap();
305 if delta != Point::ZERO {
306 rect =
307 Rect::new(rect.x - delta.x, rect.y - delta.y, rect.width, rect.height);
308 }
309 }
310 }
311 if let Some(t) = to_parent {
317 rect = t.apply_rect(rect);
318 }
319 current = self.arena.parent(ancestor_id);
320 }
321 }
322
323 pub fn focus(&mut self, id: WidgetId) {
325 self.focus_with_origin(id, crate::focus::FocusOrigin::Programmatic);
326 }
327
328 pub fn focus_ops(&mut self, id: WidgetId, ops: &mut dyn crate::window::WindowOps) {
331 self.focus_with_origin_ops(id, crate::focus::FocusOrigin::Programmatic, ops);
332 }
333
334 pub fn focused(&self) -> Option<WidgetId> {
336 self.focused
337 }
338
339 pub fn ime_context_for_focused(&self) -> Option<crate::ime::ImeContext> {
345 self.focused.and_then(|id| self.arena.ime_context(id))
346 }
347
348 pub fn first_focusable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
353 if !self.arena.is_active(root) {
354 return None;
355 }
356 let mut entries = Vec::new();
357 self.collect_scope_entries(root, &mut entries);
358 sort_scope_entries(&mut entries);
359 let scope = ScopeNode {
360 policy: crate::focus::TraversalScopePolicy::Cycle,
361 entries,
362 };
363 enter_scope_edge(&scope, false)
364 }
365
366 pub fn is_active(&self, id: WidgetId) -> bool {
372 self.arena.is_active(id)
373 }
374
375 pub fn widget_initial_focus_hint(&self, id: WidgetId) -> Option<WidgetId> {
391 if !self.arena.is_active(id) {
392 return None;
393 }
394 if let Some(node) = self.arena.get(id) {
395 if let Some(target) = node.widget.initial_focus_hint()
396 && self.arena.is_active(target)
397 && self.is_descendant_of(target, id)
398 {
399 return Some(target);
400 }
401 for &child in &node.children {
402 if let Some(found) = self.widget_initial_focus_hint(child) {
403 return Some(found);
404 }
405 }
406 }
407 None
408 }
409
410 pub fn focus_origin(&self) -> Option<crate::focus::FocusOrigin> {
412 self.focus_origin
413 }
414
415 pub fn focus_visible_signal(&self) -> crate::signal::Signal<bool> {
419 self.focus_visible.clone()
420 }
421
422 pub(super) fn cycle_focus(&mut self, reverse: bool, ops: &mut dyn crate::window::WindowOps) {
431 let mut entries = Vec::new();
432 if let Some(modal_overlay) = self.overlay_manager.topmost_centered() {
433 let content_id = modal_overlay.content_id;
434 self.collect_scope_entries(content_id, &mut entries);
435 } else {
436 let roots = self.arena.roots();
437 for root in roots {
438 if self.tooltip_content_root(root).is_some() {
448 continue;
449 }
450 self.collect_scope_entries(root, &mut entries);
451 }
452 }
453 sort_scope_entries(&mut entries);
454 self.splice_sticky_tooltips_after_anchors(&mut entries);
455 let root_scope = ScopeNode {
456 policy: crate::focus::TraversalScopePolicy::Cycle,
457 entries,
458 };
459
460 if let StepResult::Found(next_id) = navigate_scope(&root_scope, self.focused, reverse, true)
461 {
462 self.focus_with_origin_ops(next_id, crate::focus::FocusOrigin::Keyboard, &mut *ops);
463 }
464 }
467
468 fn tooltip_content_root(&self, id: WidgetId) -> Option<usize> {
470 self.tooltips.iter().position(|e| e.content_id == id)
471 }
472
473 fn splice_sticky_tooltips_after_anchors(&self, entries: &mut Vec<ScopeEntry>) {
491 let sticky: Vec<(WidgetId, WidgetId)> = self
492 .tooltips
493 .iter()
494 .filter(|e| e.is_sticky && e.overlay_id.is_some())
495 .map(|e| (e.anchor_id, e.content_id))
496 .collect();
497
498 for (anchor_id, content_id) in sticky {
499 let mut panel = Vec::new();
500 self.collect_scope_entries(content_id, &mut panel);
501 if panel.is_empty() {
502 continue;
503 }
504 sort_scope_entries(&mut panel);
505
506 let stop = self
511 .find_focusable_at_or_above(anchor_id)
512 .unwrap_or(anchor_id);
513 let at = entries
514 .iter()
515 .position(|entry| scope_entry_contains(entry, stop))
516 .map_or(entries.len(), |i| i + 1);
521
522 for (offset, item) in panel.into_iter().enumerate() {
523 entries.insert(at + offset, item);
524 }
525 }
526 }
527
528 pub(super) fn tab_stop_effective(&self, id: WidgetId) -> bool {
536 let mut current = Some(id);
537 while let Some(cur) = current {
538 let Some(node) = self.arena.get(cur) else {
539 break;
540 };
541 if let Some(prop) = node.tab_stop.as_ref() {
542 return prop.get();
543 }
544 current = node.parent;
545 }
546 true
547 }
548
549 pub(super) fn is_node_focusable(&self, node: &crate::arena::WidgetNode) -> bool {
551 node.node_focusable.unwrap_or(false)
552 }
553
554 pub(super) fn find_focusable_at_or_above(&self, id: WidgetId) -> Option<WidgetId> {
556 let mut current = Some(id);
557 while let Some(current_id) = current {
558 if let Some(node) = self.arena.get(current_id)
559 && self.is_node_focusable(node)
560 {
561 return Some(current_id);
562 }
563 current = self.arena.parent(current_id);
564 }
565 None
566 }
567
568 fn collect_scope_entries(&self, id: WidgetId, out: &mut Vec<ScopeEntry>) {
584 if !self.arena.is_active(id) {
585 return;
586 }
587 let Some(node) = self.arena.get(id) else {
588 return;
589 };
590 if node
591 .enabled_state
592 .as_ref()
593 .map(|s| !s.get())
594 .unwrap_or(false)
595 {
596 return;
597 }
598
599 if let Some(policy) = node.node_traversal_scope {
602 let mut child_entries = Vec::new();
603 for &child in &node.children {
604 self.collect_scope_entries(child, &mut child_entries);
605 }
606 sort_scope_entries(&mut child_entries);
607 out.push(ScopeEntry {
608 tab_index: node.node_tab_index,
609 kind: ScopeEntryKind::Scope(ScopeNode {
610 policy,
611 entries: child_entries,
612 }),
613 });
614 return;
615 }
616
617 if self.is_node_focusable(node) && self.tab_stop_effective(id) {
619 out.push(ScopeEntry {
620 tab_index: node.node_tab_index,
621 kind: ScopeEntryKind::Focusable(id),
622 });
623 }
624 for &child in &node.children {
625 self.collect_scope_entries(child, out);
626 }
627 }
628
629 pub(super) fn strict_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
634 let mut chain = Vec::new();
635 if let Some(start) = id {
636 let mut current = self.arena.parent(start);
637 while let Some(parent) = current {
638 chain.push(parent);
639 current = self.arena.parent(parent);
640 }
641 }
642 chain
643 }
644
645 pub(crate) fn update_focus_within_signals(
650 &mut self,
651 old: Option<WidgetId>,
652 new: Option<WidgetId>,
653 ) {
654 let old_chain = self.strict_ancestors_of(old);
655 let new_chain = self.strict_ancestors_of(new);
656 for &id in &old_chain {
658 if !new_chain.contains(&id)
659 && let Some(node) = self.arena.get(id)
660 && let Some(sig) = node.focus_within_signal.clone()
661 {
662 sig.set(false);
663 }
664 }
665 for &id in &new_chain {
667 if !old_chain.contains(&id)
668 && let Some(node) = self.arena.get(id)
669 && let Some(sig) = node.focus_within_signal.clone()
670 {
671 sig.set(true);
672 }
673 }
674 }
675
676 pub(super) fn inclusive_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
679 let mut chain = Vec::new();
680 if let Some(start) = id {
681 chain.push(start);
682 chain.extend(self.strict_ancestors_of(Some(start)));
683 }
684 chain
685 }
686
687 pub(crate) fn update_view_focus_signals(
692 &mut self,
693 old: Option<WidgetId>,
694 new: Option<WidgetId>,
695 ) {
696 let old_chain = self.inclusive_ancestors_of(old);
697 let new_chain = self.inclusive_ancestors_of(new);
698 for &id in &old_chain {
699 if !new_chain.contains(&id)
700 && let Some(node) = self.arena.get(id)
701 && let Some(sig) = node.view_focus_signal.clone()
702 {
703 sig.set(false);
704 }
705 }
706 for &id in &new_chain {
707 if !old_chain.contains(&id)
708 && let Some(node) = self.arena.get(id)
709 && let Some(sig) = node.view_focus_signal.clone()
710 {
711 sig.set(true);
712 }
713 }
714 }
715
716 pub(crate) fn view_focus_signal_for(
719 &mut self,
720 node_id: WidgetId,
721 ) -> crate::signal::Signal<bool> {
722 if let Some(node) = self.arena.get(node_id)
723 && let Some(sig) = node.view_focus_signal.clone()
724 {
725 return sig;
726 }
727 let active = self.inclusive_ancestors_of(self.focused).contains(&node_id);
728 let sig = crate::signal::Signal::new(active);
729 if let Some(node) = self.arena.get_mut(node_id) {
730 node.view_focus_signal = Some(sig.clone());
731 }
732 sig
733 }
734
735 pub fn view_focus_active_for(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
741 match self.find_focusable_at_or_above(node_id) {
742 Some(scope) => self.view_focus_signal_for(scope),
743 None => crate::signal::Signal::new(true),
744 }
745 }
746
747 pub fn begin_view_focus(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
752 let sig = self.view_focus_signal_for(node_id);
753 self.view_focus_stack.push(sig.clone());
754 sig
755 }
756
757 pub fn end_view_focus(&mut self) {
759 self.view_focus_stack.pop();
760 }
761
762 pub fn current_view_focus(&self) -> Option<crate::signal::Signal<bool>> {
764 self.view_focus_stack.last().cloned()
765 }
766
767 pub(crate) fn set_hovered(&mut self, value: Option<WidgetId>) {
781 if let Some(entry) = self.pointers.hover_owner_mut() {
782 entry.hovered = value;
783 }
784 if self.hovered_signal.get() != value {
785 self.hovered_signal.set(value);
786 }
787 }
788
789 pub(crate) fn set_focused(&mut self, value: Option<WidgetId>) {
796 self.focused = value;
797 if self.focused_signal.get() != value {
798 self.focused_signal.set(value);
799 }
800 }
801
802 pub(crate) fn update_hover_within_signals(
805 &mut self,
806 old: Option<WidgetId>,
807 new: Option<WidgetId>,
808 ) {
809 let old_chain = self.strict_ancestors_of(old);
810 let new_chain = self.strict_ancestors_of(new);
811 if let Some(entry) = self.pointers.hover_owner_mut() {
815 entry.hover_within = new_chain.clone();
816 }
817 for &id in &old_chain {
818 if !new_chain.contains(&id)
819 && let Some(node) = self.arena.get(id)
820 && let Some(sig) = node.hover_within_signal.clone()
821 {
822 sig.set(false);
823 }
824 }
825 for &id in &new_chain {
826 if !old_chain.contains(&id)
827 && let Some(node) = self.arena.get(id)
828 && let Some(sig) = node.hover_within_signal.clone()
829 {
830 sig.set(true);
831 }
832 }
833 }
834}
835
836struct ScopeEntry {
848 tab_index: Option<i32>,
849 kind: ScopeEntryKind,
850}
851
852enum ScopeEntryKind {
853 Focusable(WidgetId),
854 Scope(ScopeNode),
855}
856
857struct ScopeNode {
859 policy: crate::focus::TraversalScopePolicy,
860 entries: Vec<ScopeEntry>,
861}
862
863enum StepResult {
865 Found(WidgetId),
867 Escaped,
871}
872
873fn scope_wraps(policy: crate::focus::TraversalScopePolicy, is_root: bool) -> bool {
876 is_root || matches!(policy, crate::focus::TraversalScopePolicy::Cycle)
877}
878
879fn sort_scope_entries(entries: &mut [ScopeEntry]) {
882 entries.sort_by(|a, b| match (a.tab_index, b.tab_index) {
883 (Some(ia), Some(ib)) => ia.cmp(&ib),
884 (Some(_), None) => std::cmp::Ordering::Less,
885 (None, Some(_)) => std::cmp::Ordering::Greater,
886 (None, None) => std::cmp::Ordering::Equal,
887 });
888}
889
890fn scope_entry_contains(entry: &ScopeEntry, f: WidgetId) -> bool {
892 match &entry.kind {
893 ScopeEntryKind::Focusable(id) => *id == f,
894 ScopeEntryKind::Scope(child) => child.entries.iter().any(|e| scope_entry_contains(e, f)),
895 }
896}
897
898fn enter_scope_edge(scope: &ScopeNode, reverse: bool) -> Option<WidgetId> {
902 let n = scope.entries.len();
903 for k in 0..n {
904 let idx = if reverse { n - 1 - k } else { k };
905 match &scope.entries[idx].kind {
906 ScopeEntryKind::Focusable(id) => return Some(*id),
907 ScopeEntryKind::Scope(child) => {
908 if let Some(id) = enter_scope_edge(child, reverse) {
909 return Some(id);
910 }
911 }
912 }
913 }
914 None
915}
916
917fn navigate_scope(
921 scope: &ScopeNode,
922 focused: Option<WidgetId>,
923 reverse: bool,
924 is_root: bool,
925) -> StepResult {
926 if scope.entries.is_empty() {
927 return StepResult::Escaped;
928 }
929
930 let cur = focused.and_then(|f| {
931 scope
932 .entries
933 .iter()
934 .position(|e| scope_entry_contains(e, f))
935 });
936
937 let from = match cur {
938 None => {
939 return match enter_scope_edge(scope, reverse) {
942 Some(id) => StepResult::Found(id),
943 None => StepResult::Escaped,
944 };
945 }
946 Some(i) => i,
947 };
948
949 if let ScopeEntryKind::Scope(child) = &scope.entries[from].kind
952 && let StepResult::Found(id) = navigate_scope(child, focused, reverse, false)
953 {
954 return StepResult::Found(id);
955 }
956
957 step_to_sibling(scope, from, reverse, is_root)
958}
959
960fn step_to_sibling(scope: &ScopeNode, from: usize, reverse: bool, is_root: bool) -> StepResult {
964 let n = scope.entries.len();
965 let mut idx = from;
966 for _ in 0..n {
967 let next = if reverse {
968 if idx == 0 {
969 if scope_wraps(scope.policy, is_root) {
970 n - 1
971 } else {
972 return StepResult::Escaped;
973 }
974 } else {
975 idx - 1
976 }
977 } else if idx + 1 >= n {
978 if scope_wraps(scope.policy, is_root) {
979 0
980 } else {
981 return StepResult::Escaped;
982 }
983 } else {
984 idx + 1
985 };
986
987 match &scope.entries[next].kind {
988 ScopeEntryKind::Focusable(id) => return StepResult::Found(*id),
989 ScopeEntryKind::Scope(child) => {
990 if let Some(id) = enter_scope_edge(child, reverse) {
991 return StepResult::Found(id);
992 }
993 idx = next;
995 }
996 }
997 }
998 StepResult::Escaped
999}
1000
1001#[cfg(test)]
1002mod tests {
1003 use super::*;
1004 use crate::test_widgets::FillWidget;
1005 use crate::widget_builder::WidgetBuilder;
1006
1007 #[test]
1016 fn a_reveal_walks_the_ancestors_of_whoever_owns_the_rect() {
1017 use crate::event::{EventResponse, ScrollAlign, ScrollMotion};
1018 use std::cell::Cell;
1019 use std::rc::Rc;
1020
1021 let asked = Rc::new(Cell::new(0usize));
1022 let mut tree = WidgetTree::new();
1023 let viewport = {
1025 let asked = asked.clone();
1026 tree.add(
1027 FillWidget::new()
1030 .on_scroll(move |event, _ctx| {
1031 if matches!(event, WidgetEvent::ScrollIntoView { .. }) {
1032 asked.set(asked.get() + 1);
1033 }
1034 EventResponse::Ignored
1035 })
1036 .clips_children(true),
1037 )
1038 };
1039 let inside = tree.add_child(viewport, FillWidget::new());
1040 let button = tree.add(FillWidget::new());
1042 tree.layout(SizeProposal::exact(400.0, 300.0));
1043
1044 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
1046 let mut ops = crate::window::NoopWindowOps;
1047
1048 tree.scroll_rect_into_view(
1049 button,
1050 target,
1051 0.0,
1052 ScrollAlign::Minimal,
1053 ScrollMotion::Instant,
1054 &mut ops,
1055 );
1056 assert_eq!(
1057 asked.get(),
1058 0,
1059 "walking from the asker never reaches a viewport it is not inside"
1060 );
1061
1062 tree.scroll_rect_into_view(
1063 inside,
1064 target,
1065 0.0,
1066 ScrollAlign::Minimal,
1067 ScrollMotion::Instant,
1068 &mut ops,
1069 );
1070 assert_eq!(
1071 asked.get(),
1072 1,
1073 "walking from the rect's owner reaches the viewport enclosing it"
1074 );
1075 }
1076
1077 fn recorder(
1080 tree: &mut WidgetTree,
1081 child: WidgetId,
1082 seen: std::rc::Rc<std::cell::RefCell<Vec<Rect>>>,
1083 ) -> WidgetId {
1084 use crate::event::EventResponse;
1085 use crate::test_widgets::StackWidget;
1086 tree.add(
1087 StackWidget::new()
1088 .child(child)
1089 .on_scroll(move |event, _ctx| {
1090 if let WidgetEvent::ScrollIntoView { target_bounds, .. } = event {
1091 seen.borrow_mut().push(*target_bounds);
1092 }
1093 EventResponse::Ignored
1094 })
1095 .clips_children(true),
1096 )
1097 }
1098
1099 #[test]
1110 fn a_reveal_projects_the_rect_as_it_climbs_out_of_a_content_transform() {
1111 use crate::event::{ScrollAlign, ScrollMotion};
1112 use std::cell::RefCell;
1113 use std::rc::Rc;
1114 use teksilo_canvas::Transform2D;
1115
1116 let outer_seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1117 let view_seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1118
1119 let mut tree = WidgetTree::new();
1120 let card = tree.add(FillWidget::new());
1121 let view = recorder(&mut tree, card, view_seen.clone());
1122 let _outer = recorder(&mut tree, view, outer_seen.clone());
1123 tree.layout(SizeProposal::exact(400.0, 300.0));
1124 tree.set_content_transform(view, Transform2D::translate(-90.0, 0.0));
1127
1128 let card_rect = Rect::new(100.0, 100.0, 50.0, 50.0);
1129 let mut ops = crate::window::NoopWindowOps;
1130 tree.scroll_rect_into_view(
1131 card,
1132 card_rect,
1133 0.0,
1134 ScrollAlign::Minimal,
1135 ScrollMotion::Instant,
1136 &mut ops,
1137 );
1138
1139 assert!(
1140 view_seen.borrow().is_empty(),
1141 "the card paints at window x = 10, inside the 400×300 viewport — \
1142 comparing its scene rect against the viewport is what used to \
1143 produce a spurious verdict here, in either direction"
1144 );
1145 assert!(
1146 outer_seen.borrow().is_empty(),
1147 "and nothing outside it has a reason to scroll either"
1148 );
1149
1150 tree.set_content_transform(view, Transform2D::translate(-600.0, 0.0));
1154 tree.scroll_rect_into_view(
1155 card,
1156 card_rect,
1157 0.0,
1158 ScrollAlign::Minimal,
1159 ScrollMotion::Instant,
1160 &mut ops,
1161 );
1162 assert_eq!(
1163 view_seen.borrow().as_slice(),
1164 &[card_rect],
1165 "the view is asked to reveal the card, in the view's own content \
1166 space — the space `SceneView::ensure_visible` takes by signature"
1167 );
1168 assert_eq!(
1169 outer_seen.borrow().as_slice(),
1170 &[Rect::new(-500.0, 100.0, 50.0, 50.0)],
1171 "and everything further out is asked in window space, which is the \
1172 only space it can compare against its own viewport"
1173 );
1174 }
1175
1176 #[test]
1179 fn a_reveal_with_no_transform_in_the_chain_passes_the_rect_through() {
1180 use crate::event::{ScrollAlign, ScrollMotion};
1181 use std::cell::RefCell;
1182 use std::rc::Rc;
1183
1184 let seen: Rc<RefCell<Vec<Rect>>> = Rc::new(RefCell::new(Vec::new()));
1185 let mut tree = WidgetTree::new();
1186 let inside = tree.add(FillWidget::new());
1187 let _viewport = recorder(&mut tree, inside, seen.clone());
1188 tree.layout(SizeProposal::exact(400.0, 300.0));
1189
1190 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
1191 let mut ops = crate::window::NoopWindowOps;
1192 tree.scroll_rect_into_view(
1193 inside,
1194 target,
1195 0.0,
1196 ScrollAlign::Minimal,
1197 ScrollMotion::Instant,
1198 &mut ops,
1199 );
1200 assert_eq!(seen.borrow().as_slice(), &[target]);
1201 }
1202
1203 #[test]
1204 fn focus_widget() {
1205 let mut tree = WidgetTree::new();
1206 let widget = tree.add(FillWidget::new());
1207 tree.layout(SizeProposal::exact(100.0, 50.0));
1208 tree.focus(widget);
1209 assert_eq!(tree.focused(), Some(widget));
1210 }
1211
1212 #[test]
1213 fn focus_change() {
1214 let mut tree = WidgetTree::new();
1215 let a = tree.add(FillWidget::new());
1216 let b = tree.add(FillWidget::new());
1217 tree.layout(SizeProposal::exact(100.0, 50.0));
1218 tree.focus(a);
1219 assert_eq!(tree.focused(), Some(a));
1220 tree.focus(b);
1221 assert_eq!(tree.focused(), Some(b));
1222 }
1223
1224 #[derive(Debug)]
1228 struct RebuildingRows {
1229 epoch: crate::signal::Signal<u64>,
1230 rows: Vec<WidgetId>,
1231 }
1232
1233 impl Widget for RebuildingRows {
1234 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1235 let sid = ctx.self_id();
1236 let reg = ctx.binding_registry();
1237 self.epoch
1238 .bind_to(sid, reg, crate::binding::BindingLevel::Rebuild);
1239 self.rows = (0..3)
1240 .map(|_| ctx.add(FillWidget::new().focusable()))
1241 .collect();
1242 self.rows.clone()
1243 }
1244
1245 fn layout_response(
1246 &self,
1247 proposal: SizeProposal,
1248 _ctx: &LayoutContext,
1249 ) -> crate::widget::LayoutResponse {
1250 proposal.resolve(0.0, 0.0).into()
1251 }
1252
1253 fn children(&self) -> Vec<WidgetId> {
1254 self.rows.clone()
1255 }
1256 }
1257
1258 #[test]
1268 fn a_rebuild_keeps_focus_inside_the_subtree_that_had_it() {
1269 let mut tree = WidgetTree::new();
1270 let epoch = crate::signal::Signal::new(0u64);
1271 let root = tree.add(RebuildingRows {
1272 epoch: epoch.clone(),
1273 rows: Vec::new(),
1274 });
1275 tree.layout(SizeProposal::exact(100.0, 60.0));
1276
1277 let first_row = tree
1278 .first_focusable_descendant(root)
1279 .expect("the rows are focusable");
1280 tree.focus(first_row);
1281 assert_eq!(tree.focused(), Some(first_row));
1282
1283 epoch.set(1);
1285 tree.layout(SizeProposal::exact(100.0, 60.0));
1286
1287 let focused = tree
1288 .focused()
1289 .expect("a rebuild must not drop focus out of the rebuilt subtree");
1290 assert_ne!(
1291 focused, first_row,
1292 "the old row is dead — focus must have moved to a freshly built one"
1293 );
1294 assert!(
1295 tree.is_descendant_of(focused, root),
1296 "focus must land back inside the rebuilt subtree"
1297 );
1298 assert!(tree.is_active(focused), "the focused node must be live");
1299 }
1300
1301 #[test]
1306 fn a_rebuild_elsewhere_does_not_steal_focus() {
1307 let mut tree = WidgetTree::new();
1308 let epoch = crate::signal::Signal::new(0u64);
1309 let outsider = tree.add(FillWidget::new().focusable());
1310 let _rows = tree.add(RebuildingRows {
1311 epoch: epoch.clone(),
1312 rows: Vec::new(),
1313 });
1314 tree.layout(SizeProposal::exact(100.0, 60.0));
1315
1316 tree.focus(outsider);
1317 assert_eq!(tree.focused(), Some(outsider));
1318
1319 epoch.set(1);
1320 tree.layout(SizeProposal::exact(100.0, 60.0));
1321
1322 assert_eq!(
1323 tree.focused(),
1324 Some(outsider),
1325 "a rebuild that never held focus must not pull focus into itself"
1326 );
1327 }
1328
1329 #[test]
1330 fn first_focusable_descendant_prefers_first_focusable_child() {
1331 let mut tree = WidgetTree::new();
1332 let a = tree.add(FillWidget::new().focusable());
1333 let _not_focusable = tree.add(FillWidget::new());
1334 let b = tree.add(FillWidget::new().focusable());
1335 let root = tree.add(crate::test_widgets::StackWidget::new().child(a).child(b));
1336 tree.layout(SizeProposal::exact(100.0, 50.0));
1337
1338 assert_eq!(tree.first_focusable_descendant(root), Some(a));
1339 }
1340
1341 #[test]
1342 fn tab_cycles_through_focusable_widgets() {
1343 let mut tree = WidgetTree::new();
1344 let a = tree.add(FillWidget::new().focusable());
1345 let b = tree.add(FillWidget::new().focusable());
1346 let c = tree.add(FillWidget::new().focusable());
1347 tree.layout(SizeProposal::exact(100.0, 50.0));
1348
1349 assert_eq!(tree.focused(), None);
1350
1351 tree.press_key(Key::Tab, Modifiers::NONE);
1352 assert_eq!(tree.focused(), Some(a));
1353
1354 tree.press_key(Key::Tab, Modifiers::NONE);
1355 assert_eq!(tree.focused(), Some(b));
1356
1357 tree.press_key(Key::Tab, Modifiers::NONE);
1358 assert_eq!(tree.focused(), Some(c));
1359
1360 tree.press_key(Key::Tab, Modifiers::NONE);
1361 assert_eq!(tree.focused(), Some(a));
1362 }
1363
1364 #[test]
1365 fn tab_stop_on_composite_excludes_focusable_descendant() {
1366 let mut tree = WidgetTree::new();
1371 let leaf = tree.add(FillWidget::new().focusable());
1372 let composite = tree.add(crate::test_widgets::StackWidget::new().child(leaf));
1373 let other = tree.add(FillWidget::new().focusable());
1374 tree.layout(SizeProposal::exact(100.0, 50.0));
1375
1376 tree.set_tab_stop(composite, false);
1377
1378 tree.press_key(Key::Tab, Modifiers::NONE);
1379 assert_eq!(
1380 tree.focused(),
1381 Some(other),
1382 "Tab must skip the suppressed composite's inner leaf"
1383 );
1384 tree.press_key(Key::Tab, Modifiers::NONE);
1385 assert_eq!(
1386 tree.focused(),
1387 Some(other),
1388 "only the un-suppressed control participates in Tab"
1389 );
1390
1391 tree.set_tab_stop(composite, true);
1393 tree.set_focused(None);
1394 tree.press_key(Key::Tab, Modifiers::NONE);
1395 let first = tree.focused();
1396 tree.press_key(Key::Tab, Modifiers::NONE);
1397 let second = tree.focused();
1398 assert_ne!(
1399 first, second,
1400 "with the composite re-enabled, Tab visits both controls"
1401 );
1402 }
1403
1404 #[test]
1405 fn shift_tab_cycles_backwards() {
1406 let mut tree = WidgetTree::new();
1407 let a = tree.add(FillWidget::new().focusable());
1408 let b = tree.add(FillWidget::new().focusable());
1409 let c = tree.add(FillWidget::new().focusable());
1410 tree.layout(SizeProposal::exact(100.0, 50.0));
1411
1412 tree.press_key(Key::Tab, Modifiers::NONE);
1413 assert_eq!(tree.focused(), Some(a));
1414
1415 tree.press_key(Key::Tab, Modifiers::SHIFT);
1416 assert_eq!(tree.focused(), Some(c));
1417
1418 tree.press_key(Key::Tab, Modifiers::SHIFT);
1419 assert_eq!(tree.focused(), Some(b));
1420 }
1421
1422 #[test]
1423 fn tab_skips_non_focusable_widgets() {
1424 let mut tree = WidgetTree::new();
1425 let _not_focusable = tree.add(FillWidget::new());
1426 let a = tree.add(FillWidget::new().focusable());
1427 let _also_not = tree.add(FillWidget::new());
1428 let b = tree.add(FillWidget::new().focusable());
1429 tree.layout(SizeProposal::exact(100.0, 50.0));
1430
1431 tree.press_key(Key::Tab, Modifiers::NONE);
1432 assert_eq!(tree.focused(), Some(a));
1433
1434 tree.press_key(Key::Tab, Modifiers::NONE);
1435 assert_eq!(tree.focused(), Some(b));
1436 }
1437
1438 #[test]
1449 fn focus_does_not_check_that_the_node_is_focusable() {
1450 let mut tree = WidgetTree::new();
1451 let inert = tree.add(FillWidget::new()); tree.layout(SizeProposal::exact(100.0, 50.0));
1453 assert!(
1454 tree.tab_stops_within(inert).is_empty(),
1455 "the subject has to be unreachable for the point to be made"
1456 );
1457
1458 tree.focus(inert);
1459 assert_eq!(
1460 tree.focused(),
1461 Some(inert),
1462 "focus lands on a node Tab traversal can never offer"
1463 );
1464 }
1465
1466 #[test]
1467 fn tab_focus_has_keyboard_origin() {
1468 let mut tree = WidgetTree::new();
1469 tree.add(FillWidget::new().focusable());
1470 tree.layout(SizeProposal::exact(100.0, 50.0));
1471
1472 tree.press_key(Key::Tab, Modifiers::NONE);
1473 assert_eq!(
1474 tree.focus_origin(),
1475 Some(crate::focus::FocusOrigin::Keyboard)
1476 );
1477 }
1478
1479 #[test]
1480 fn tab_skips_focusable_inside_disabled_ancestor() {
1481 use crate::signal::Signal;
1482 use crate::test_widgets::StackWidget;
1483
1484 let mut tree = WidgetTree::new();
1485 let a = tree.add(FillWidget::new().focusable());
1486 let inner = tree.add(FillWidget::new().focusable());
1487 let disabled_container = tree.add(StackWidget::new().child(inner));
1488 let c = tree.add(FillWidget::new().focusable());
1489 tree.enabled_when(disabled_container, Signal::new(false));
1490 tree.layout(SizeProposal::exact(200.0, 100.0));
1491
1492 tree.press_key(Key::Tab, Modifiers::NONE);
1493 assert_eq!(tree.focused(), Some(a));
1494
1495 tree.press_key(Key::Tab, Modifiers::NONE);
1496 assert_eq!(
1497 tree.focused(),
1498 Some(c),
1499 "tab should skip the focusable widget nested inside the disabled container"
1500 );
1501 }
1502
1503 #[test]
1504 fn dormant_widget_not_in_focus_cycle() {
1505 let mut tree = WidgetTree::new();
1506 let a = tree.add(FillWidget::new().focusable());
1507 let b = tree.add(FillWidget::new().focusable());
1508 let c = tree.add(FillWidget::new().focusable());
1509 tree.layout(SizeProposal::exact(200.0, 100.0));
1510
1511 tree.focus(a);
1512 tree.set_dormant(b);
1513
1514 tree.press_key(Key::Tab, Modifiers::NONE);
1515 assert_eq!(tree.focused(), Some(c));
1516 }
1517
1518 #[test]
1523 fn revalidate_delivers_focus_lost_when_focused_widget_goes_dormant() {
1524 use std::cell::Cell;
1525 use std::rc::Rc;
1526
1527 let lost = Rc::new(Cell::new(0_u32));
1528 let gained = Rc::new(Cell::new(0_u32));
1529 let lost_c = lost.clone();
1530 let gained_c = gained.clone();
1531
1532 let mut tree = WidgetTree::new();
1533 let editor = tree.add(
1534 FillWidget::new()
1535 .focusable()
1536 .on_focus(move |is_gained, _ctx| {
1537 if is_gained {
1538 gained_c.set(gained_c.get() + 1);
1539 } else {
1540 lost_c.set(lost_c.get() + 1);
1541 }
1542 }),
1543 );
1544 let _other = tree.add(FillWidget::new().focusable());
1545 tree.layout(SizeProposal::exact(200.0, 100.0));
1546
1547 tree.focus(editor);
1548 assert_eq!(tree.focused(), Some(editor));
1549 assert_eq!(gained.get(), 1, "focus() delivers FocusGained");
1550 assert_eq!(lost.get(), 0);
1551
1552 tree.set_dormant(editor);
1554 let mut noop = crate::window::NoopWindowOps;
1556 tree.revalidate_interaction_state(&mut noop);
1557
1558 assert_eq!(
1559 tree.focused(),
1560 None,
1561 "tree focus must clear when the target is dormant"
1562 );
1563 assert_eq!(
1564 lost.get(),
1565 1,
1566 "FocusLost must reach the dormant widget so it can clear has_focus / caret blink"
1567 );
1568 }
1569
1570 #[test]
1571 fn tab_cycles_focus_in_tree_order() {
1572 let mut tree = WidgetTree::new();
1573 let a = tree.add(FillWidget::new().focusable());
1574 let b = tree.add(FillWidget::new().focusable());
1575 tree.layout(SizeProposal::exact(200.0, 80.0));
1576
1577 tree.focus(a);
1578 assert_eq!(tree.focused(), Some(a));
1579
1580 tree.press_key(Key::Tab, Modifiers::NONE);
1581 assert_eq!(tree.focused(), Some(b));
1582
1583 tree.press_key(Key::Tab, Modifiers::SHIFT);
1584 assert_eq!(tree.focused(), Some(a));
1585 }
1586
1587 #[test]
1588 fn focus_survives_theme_switch() {
1589 let mut tree = WidgetTree::new();
1590 let a = tree.add(FillWidget::new().focusable());
1591 let _b = tree.add(FillWidget::new().focusable());
1592 tree.layout(SizeProposal::exact(200.0, 80.0));
1593
1594 tree.focus(a);
1595 assert_eq!(tree.focused(), Some(a));
1596
1597 tree.set_theme(crate::presets::intui::dark());
1598 tree.layout(SizeProposal::exact(200.0, 80.0));
1599
1600 assert_eq!(
1601 tree.focused(),
1602 Some(a),
1603 "theme switch must not clobber focus"
1604 );
1605 }
1606
1607 #[test]
1608 fn focus_survives_locale_switch() {
1609 let mut tree = WidgetTree::new();
1610 let a = tree.add(FillWidget::new().focusable());
1611 tree.layout(SizeProposal::exact(200.0, 80.0));
1612
1613 tree.focus(a);
1614 tree.set_locale("fr-FR".to_string());
1615 tree.layout(SizeProposal::exact(200.0, 80.0));
1616
1617 assert_eq!(
1618 tree.focused(),
1619 Some(a),
1620 "locale switch must not clobber focus"
1621 );
1622 }
1623
1624 #[test]
1627 fn focus_within_flips_when_descendant_takes_focus() {
1628 use crate::signal::Signal;
1629 use crate::test_widgets::StackWidget;
1630 use crate::widget_builder::WidgetBuilder;
1631
1632 let halo = Signal::new(false);
1633
1634 let mut tree = WidgetTree::new();
1635 let leaf = tree.add(FillWidget::new().focusable());
1636 let mid = tree.add(StackWidget::new().child(leaf));
1637 let _outer = tree.add(StackWidget::new().child(mid).focus_within(halo.clone()));
1638
1639 tree.layout(SizeProposal::exact(100.0, 50.0));
1640 assert!(!halo.get(), "no focus yet → signal is false");
1641
1642 tree.focus(leaf);
1643 assert!(halo.get(), "leaf now focused, outer is its strict ancestor");
1644 }
1645
1646 #[test]
1647 fn focus_within_strict_ancestors_only() {
1648 use crate::signal::Signal;
1651 use crate::widget_builder::WidgetBuilder;
1652
1653 let halo = Signal::new(false);
1654
1655 let mut tree = WidgetTree::new();
1656 let widget = tree.add(FillWidget::new().focusable().focus_within(halo.clone()));
1657 tree.layout(SizeProposal::exact(100.0, 50.0));
1658 tree.focus(widget);
1659
1660 assert!(
1661 !halo.get(),
1662 "focusing the widget itself must not set its own focus_within"
1663 );
1664 }
1665
1666 #[test]
1667 fn view_focus_active_is_inclusive_for_the_view_itself() {
1668 use crate::test_widgets::StackWidget;
1674 use crate::widget_builder::WidgetBuilder;
1675
1676 let mut tree = WidgetTree::new();
1677 let item = tree.add(FillWidget::new()); let view = tree.add(StackWidget::new().child(item).focusable(true));
1679 let outside = tree.add(FillWidget::new().focusable());
1680 tree.layout(SizeProposal::exact(100.0, 50.0));
1681
1682 let active = tree.view_focus_active_for(item);
1683 assert!(!active.get(), "no focus yet → scope inactive");
1684
1685 tree.focus(view);
1686 assert!(
1687 active.get(),
1688 "view focused directly → scope active (focus_within would be false here)"
1689 );
1690
1691 tree.focus(outside);
1692 assert!(
1693 !active.get(),
1694 "focus moved outside the view → scope inactive"
1695 );
1696 }
1697
1698 #[test]
1699 fn begin_view_focus_for_keys_on_the_view_root_not_the_building_pane() {
1700 use crate::test_widgets::StackWidget;
1708 use crate::widget_builder::WidgetBuilder;
1709
1710 let mut tree = WidgetTree::new();
1711 let row = tree.add(FillWidget::new()); let pane = tree.add(StackWidget::new().child(row)); let root = tree.add(StackWidget::new().child(pane).focusable(true));
1714 tree.layout(SizeProposal::exact(100.0, 50.0));
1715
1716 let keyed_on_root = tree.begin_view_focus(root);
1718 tree.end_view_focus();
1719 let keyed_on_pane = tree.view_focus_signal_for(pane);
1721
1722 assert!(!keyed_on_root.get(), "no focus yet → inactive");
1723 tree.focus(root);
1724 assert!(keyed_on_root.get(), "view root focused → row scope active");
1725 assert!(
1726 !keyed_on_pane.get(),
1727 "pane-keyed scope stays false: the focused root is the pane's ancestor, not its descendant",
1728 );
1729 }
1730
1731 #[test]
1732 fn focus_visible_tracks_input_modality() {
1733 use crate::event::{Key, Modifiers};
1736 let mut tree = WidgetTree::new();
1737 let w = tree.add(FillWidget::new().focusable());
1738 tree.layout(SizeProposal::exact(100.0, 50.0));
1739 let vis = tree.focus_visible_signal();
1740 assert!(!vis.get(), "starts not focus-visible");
1741
1742 tree.press_key(Key::Tab, Modifiers::NONE);
1743 assert!(vis.get(), "keyboard input turns focus-visible ON");
1744
1745 tree.click(w);
1746 assert!(!vis.get(), "pointer input turns focus-visible OFF");
1747
1748 tree.press_key(Key::ArrowDown, Modifiers::NONE);
1749 assert!(vis.get(), "keyboard input turns it back ON");
1750 }
1751
1752 #[test]
1753 fn view_focus_active_without_focusable_ancestor_is_constant_true() {
1754 let mut tree = WidgetTree::new();
1757 let item = tree.add(FillWidget::new());
1758 tree.layout(SizeProposal::exact(100.0, 50.0));
1759 assert!(tree.view_focus_active_for(item).get());
1760 }
1761
1762 #[test]
1763 fn focus_within_diff_across_siblings() {
1764 use crate::signal::Signal;
1767 use crate::test_widgets::StackWidget;
1768 use crate::widget_builder::WidgetBuilder;
1769
1770 let sig_a = Signal::new(false);
1771 let sig_b = Signal::new(false);
1772
1773 let mut tree = WidgetTree::new();
1774 let leaf_a = tree.add(FillWidget::new().focusable());
1775 let leaf_b = tree.add(FillWidget::new().focusable());
1776 let mid_a = tree.add(StackWidget::new().child(leaf_a).focus_within(sig_a.clone()));
1777 let mid_b = tree.add(StackWidget::new().child(leaf_b).focus_within(sig_b.clone()));
1778 let _root = tree.add(StackWidget::new().child(mid_a).child(mid_b));
1779
1780 tree.layout(SizeProposal::exact(100.0, 50.0));
1781 tree.focus(leaf_a);
1782 assert!(sig_a.get(), "sig_a true after focusing leaf_a");
1783 assert!(!sig_b.get(), "sig_b false: leaf_a is not its descendant");
1784
1785 tree.focus(leaf_b);
1786 assert!(!sig_a.get(), "sig_a flipped to false on focus move");
1787 assert!(sig_b.get(), "sig_b flipped to true on focus move");
1788 }
1789
1790 #[test]
1791 fn focus_within_clears_when_focused_widget_destroyed() {
1792 use crate::signal::Signal;
1793 use crate::test_widgets::StackWidget;
1794 use crate::widget_builder::WidgetBuilder;
1795
1796 let halo = Signal::new(false);
1797
1798 let mut tree = WidgetTree::new();
1799 let leaf = tree.add(FillWidget::new().focusable());
1800 let _outer = tree.add(StackWidget::new().child(leaf).focus_within(halo.clone()));
1801
1802 tree.layout(SizeProposal::exact(100.0, 50.0));
1803 tree.focus(leaf);
1804 assert!(halo.get());
1805
1806 tree.destroy_subtree(leaf);
1807 assert!(
1808 !halo.get(),
1809 "destroying the focused widget must clear focus_within on its ancestors"
1810 );
1811 }
1812
1813 #[test]
1814 fn hover_within_flips_via_pointer_move() {
1815 use crate::signal::Signal;
1816 use crate::test_widgets::StackWidget;
1817 use crate::widget_builder::WidgetBuilder;
1818 use teksilo_canvas::Point;
1819
1820 let glow = Signal::new(false);
1821
1822 let mut tree = WidgetTree::new();
1823 let leaf = tree.add(FillWidget::new());
1824 let _outer = tree.add(StackWidget::new().child(leaf).hover_within(glow.clone()));
1825
1826 tree.layout(SizeProposal::exact(100.0, 50.0));
1827 assert!(!glow.get());
1828
1829 tree.pointer_move(Point::new(50.0, 25.0));
1830 assert!(glow.get(), "pointer over leaf → outer.hover_within = true");
1831
1832 tree.pointer_move(Point::new(500.0, 500.0));
1833 assert!(
1834 !glow.get(),
1835 "pointer moves outside the tree → hover_within clears"
1836 );
1837 }
1838
1839 #[test]
1840 fn hover_within_strict_ancestors_only() {
1841 use crate::signal::Signal;
1842 use crate::widget_builder::WidgetBuilder;
1843 use teksilo_canvas::Point;
1844
1845 let glow = Signal::new(false);
1846
1847 let mut tree = WidgetTree::new();
1848 let _w = tree.add(FillWidget::new().hover_within(glow.clone()));
1849 tree.layout(SizeProposal::exact(100.0, 50.0));
1850 tree.pointer_move(Point::new(50.0, 25.0));
1851
1852 assert!(
1853 !glow.get(),
1854 "hovering the widget itself must not set its own hover_within"
1855 );
1856 }
1857}
1858
1859#[cfg(test)]
1860mod tests_scope {
1861 use super::*;
1866 use crate::focus::TraversalScopePolicy::{Continue, Cycle};
1867 use crate::test_widgets::{FillWidget, StackWidget};
1868 use crate::widget_builder::WidgetBuilder;
1869
1870 fn indexed(tree: &mut WidgetTree, idx: i32) -> WidgetId {
1872 tree.add(FillWidget::new().focusable().tab_index(idx))
1873 }
1874
1875 fn tab(tree: &mut WidgetTree) {
1876 tree.press_key(Key::Tab, Modifiers::NONE);
1877 }
1878 fn shift_tab(tree: &mut WidgetTree) {
1879 tree.press_key(Key::Tab, Modifiers::SHIFT);
1880 }
1881
1882 #[test]
1883 fn overlapping_tab_index_in_sibling_scopes_does_not_interleave() {
1884 let mut tree = WidgetTree::new();
1885 let a1 = indexed(&mut tree, 1);
1886 let a2 = indexed(&mut tree, 2);
1887 let scope_a = tree.add(StackWidget::new().child(a1).child(a2));
1888 let b1 = indexed(&mut tree, 1);
1889 let b2 = indexed(&mut tree, 2);
1890 let scope_b = tree.add(StackWidget::new().child(b1).child(b2));
1891 let _root = tree.add(StackWidget::new().child(scope_a).child(scope_b));
1892 tree.set_traversal_scope(scope_a, Continue);
1893 tree.set_traversal_scope(scope_b, Continue);
1894 tree.layout(SizeProposal::exact(100.0, 100.0));
1895
1896 for expected in [a1, a2, b1, b2, a1] {
1898 tab(&mut tree);
1899 assert_eq!(tree.focused(), Some(expected));
1900 }
1901 }
1902
1903 #[test]
1904 fn continue_scope_flows_out_at_the_ends() {
1905 let mut tree = WidgetTree::new();
1907 let a = tree.add(FillWidget::new().focusable());
1908 let c1 = tree.add(FillWidget::new().focusable());
1909 let c2 = tree.add(FillWidget::new().focusable());
1910 let scope_c = tree.add(StackWidget::new().child(c1).child(c2));
1911 let b = tree.add(FillWidget::new().focusable());
1912 let _root = tree.add(StackWidget::new().child(a).child(scope_c).child(b));
1913 tree.set_traversal_scope(scope_c, Continue);
1914 tree.layout(SizeProposal::exact(100.0, 100.0));
1915
1916 for expected in [a, c1, c2, b, a] {
1917 tab(&mut tree);
1918 assert_eq!(tree.focused(), Some(expected));
1919 }
1920 tree.focus(c1);
1922 shift_tab(&mut tree);
1923 assert_eq!(tree.focused(), Some(a));
1924 }
1925
1926 #[test]
1927 fn cycle_scope_wraps_and_never_escapes() {
1928 let mut tree = WidgetTree::new();
1930 let a = tree.add(FillWidget::new().focusable());
1931 let d1 = tree.add(FillWidget::new().focusable());
1932 let d2 = tree.add(FillWidget::new().focusable());
1933 let scope_d = tree.add(StackWidget::new().child(d1).child(d2));
1934 let _root = tree.add(StackWidget::new().child(a).child(scope_d));
1935 tree.set_traversal_scope(scope_d, Cycle);
1936 tree.layout(SizeProposal::exact(100.0, 100.0));
1937
1938 tree.focus(d1);
1939 for _ in 0..10 {
1940 tab(&mut tree);
1941 let f = tree.focused();
1942 assert!(
1943 f == Some(d1) || f == Some(d2),
1944 "Cycle scope must trap Tab inside {{d1,d2}}, got {f:?}"
1945 );
1946 }
1947 tree.focus(d1);
1949 tab(&mut tree);
1950 assert_eq!(tree.focused(), Some(d2));
1951 shift_tab(&mut tree);
1952 assert_eq!(tree.focused(), Some(d1));
1953 shift_tab(&mut tree);
1954 assert_eq!(tree.focused(), Some(d2), "Shift+Tab at start wraps to last");
1955 }
1956
1957 #[test]
1958 fn empty_scope_is_skipped() {
1959 let mut tree = WidgetTree::new();
1961 let a = tree.add(FillWidget::new().focusable());
1962 let empty = tree.add(StackWidget::new());
1963 let b = tree.add(FillWidget::new().focusable());
1964 let _root = tree.add(StackWidget::new().child(a).child(empty).child(b));
1965 tree.set_traversal_scope(empty, Continue);
1966 tree.layout(SizeProposal::exact(100.0, 100.0));
1967
1968 for expected in [a, b, a] {
1969 tab(&mut tree);
1970 assert_eq!(tree.focused(), Some(expected));
1971 }
1972 }
1973
1974 #[test]
1975 fn single_member_cycle_scope_stays_put() {
1976 let mut tree = WidgetTree::new();
1977 let e = tree.add(FillWidget::new().focusable());
1978 let scope_e = tree.add(StackWidget::new().child(e));
1979 tree.set_traversal_scope(scope_e, Cycle);
1980 tree.layout(SizeProposal::exact(100.0, 50.0));
1981
1982 tree.focus(e);
1983 tab(&mut tree);
1984 assert_eq!(tree.focused(), Some(e));
1985 shift_tab(&mut tree);
1986 assert_eq!(tree.focused(), Some(e));
1987 }
1988
1989 #[test]
1990 fn nested_continue_in_cycle_flows_out_to_outer() {
1991 let mut tree = WidgetTree::new();
1993 let x = tree.add(FillWidget::new().focusable());
1994 let i1 = tree.add(FillWidget::new().focusable());
1995 let i2 = tree.add(FillWidget::new().focusable());
1996 let inner = tree.add(StackWidget::new().child(i1).child(i2));
1997 let y = tree.add(FillWidget::new().focusable());
1998 let outer = tree.add(StackWidget::new().child(x).child(inner).child(y));
1999 tree.set_traversal_scope(inner, Continue);
2000 tree.set_traversal_scope(outer, Cycle);
2001 tree.layout(SizeProposal::exact(100.0, 100.0));
2002
2003 for expected in [x, i1, i2, y, x] {
2004 tab(&mut tree);
2005 assert_eq!(tree.focused(), Some(expected));
2006 }
2007 tree.focus(i1);
2009 shift_tab(&mut tree);
2010 assert_eq!(tree.focused(), Some(x));
2011 }
2012
2013 #[test]
2014 fn nested_cycle_in_cycle_traps_in_the_inner_scope() {
2015 let mut tree = WidgetTree::new();
2018 let x = tree.add(FillWidget::new().focusable());
2019 let i1 = tree.add(FillWidget::new().focusable());
2020 let i2 = tree.add(FillWidget::new().focusable());
2021 let inner = tree.add(StackWidget::new().child(i1).child(i2));
2022 let y = tree.add(FillWidget::new().focusable());
2023 let outer = tree.add(StackWidget::new().child(x).child(inner).child(y));
2024 tree.set_traversal_scope(inner, Cycle);
2025 tree.set_traversal_scope(outer, Cycle);
2026 tree.layout(SizeProposal::exact(100.0, 100.0));
2027
2028 tree.focus(i1);
2029 for _ in 0..6 {
2030 tab(&mut tree);
2031 let f = tree.focused();
2032 assert!(
2033 f == Some(i1) || f == Some(i2),
2034 "inner Cycle must trap Tab; reached {f:?}"
2035 );
2036 }
2037 }
2038
2039 #[test]
2040 fn scoped_tab_index_orders_within_a_scope() {
2041 let mut tree = WidgetTree::new();
2043 let f3 = indexed(&mut tree, 3);
2044 let f1 = indexed(&mut tree, 1);
2045 let f2 = indexed(&mut tree, 2);
2046 let scope_f = tree.add(StackWidget::new().child(f3).child(f1).child(f2));
2047 tree.set_traversal_scope(scope_f, Cycle);
2048 tree.layout(SizeProposal::exact(100.0, 100.0));
2049
2050 for expected in [f1, f2, f3, f1] {
2051 tab(&mut tree);
2052 assert_eq!(tree.focused(), Some(expected));
2053 }
2054 }
2055
2056 #[test]
2057 fn destroyed_focused_widget_re_enters_at_first() {
2058 let mut tree = WidgetTree::new();
2059 let a = tree.add(FillWidget::new().focusable());
2060 let b = tree.add(FillWidget::new().focusable());
2061 tree.layout(SizeProposal::exact(100.0, 50.0));
2062
2063 tree.focus(b);
2064 tree.destroy_subtree(b);
2065 tab(&mut tree);
2066 assert_eq!(tree.focused(), Some(a), "Tab after destroy enters at first");
2067 }
2068
2069 #[test]
2070 fn set_traversal_scope_forces_node_non_focusable() {
2071 let mut tree = WidgetTree::new();
2074 let inner = tree.add(FillWidget::new().focusable());
2075 let scope = tree.add(StackWidget::new().child(inner).focusable(true));
2076 tree.set_traversal_scope(scope, Continue);
2077 tree.layout(SizeProposal::exact(100.0, 50.0));
2078
2079 tab(&mut tree);
2081 assert_eq!(tree.focused(), Some(inner));
2082 tab(&mut tree);
2083 assert_eq!(tree.focused(), Some(inner));
2084 }
2085
2086 #[test]
2087 fn no_scopes_behaves_like_a_flat_cycle() {
2088 let mut tree = WidgetTree::new();
2091 let a = indexed(&mut tree, 2);
2092 let b = indexed(&mut tree, 1);
2093 let c = tree.add(FillWidget::new().focusable());
2094 tree.layout(SizeProposal::exact(100.0, 50.0));
2095
2096 for expected in [b, a, c, b] {
2098 tab(&mut tree);
2099 assert_eq!(tree.focused(), Some(expected));
2100 }
2101 }
2102
2103 #[test]
2104 fn centered_modal_confines_tab_to_its_content() {
2105 let mut tree = WidgetTree::new();
2108 let outside1 = tree.add(FillWidget::new().focusable());
2109 let outside2 = tree.add(FillWidget::new().focusable());
2110 let m1 = tree.add(FillWidget::new().focusable());
2111 let m2 = tree.add(FillWidget::new().focusable());
2112 let content = tree.add(StackWidget::new().child(m1).child(m2));
2113 tree.layout(SizeProposal::exact(200.0, 100.0));
2114
2115 tree.show_overlay(crate::overlay::OverlayRequest {
2116 content_id: content,
2117 anchor: outside1,
2118 placement: crate::overlay::OverlayPlacement::Centered,
2119 dismiss: crate::overlay::DismissBehavior::Manual,
2120 layer: crate::overlay::OverlayLayer::InTree,
2121 parent_overlay: None,
2122 on_dismiss: None,
2123 fade_duration: None,
2124 });
2125
2126 for _ in 0..6 {
2127 tab(&mut tree);
2128 let f = tree.focused();
2129 assert!(
2130 f == Some(m1) || f == Some(m2),
2131 "modal must trap Tab inside its content, reached {f:?}"
2132 );
2133 }
2134 assert_ne!(tree.focused(), Some(outside1));
2135 assert_ne!(tree.focused(), Some(outside2));
2136 }
2137}
2138
2139#[cfg(test)]
2140mod tests_focus_out_dismissal {
2141 use super::*;
2148 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
2149 use crate::test_widgets::{FillWidget, StackWidget};
2150
2151 fn tab(tree: &mut WidgetTree) {
2152 tree.press_key(Key::Tab, Modifiers::NONE);
2153 }
2154
2155 fn show_anchored(
2157 tree: &mut WidgetTree,
2158 content: WidgetId,
2159 anchor: WidgetId,
2160 parent: Option<crate::overlay::OverlayId>,
2161 ) -> crate::overlay::OverlayId {
2162 tree.show_overlay(OverlayRequest {
2163 content_id: content,
2164 anchor,
2165 placement: OverlayPlacement::Below,
2166 dismiss: DismissBehavior::EscapeOrClickOutside,
2167 layer: OverlayLayer::InTree,
2168 parent_overlay: parent,
2169 on_dismiss: None,
2170 fade_duration: None,
2171 })
2172 }
2173
2174 #[test]
2175 fn tab_out_of_a_non_modal_overlay_dismisses_it() {
2176 let mut tree = WidgetTree::new();
2177 let anchor = tree.add(FillWidget::new().focusable());
2178 let after = tree.add(FillWidget::new().focusable());
2179 let inner = tree.add(FillWidget::new().focusable());
2180 let content = tree.add(StackWidget::new().child(inner));
2181 tree.layout(SizeProposal::exact(200.0, 100.0));
2182 show_anchored(&mut tree, content, anchor, None);
2183
2184 tree.focus(inner);
2185 tab(&mut tree);
2186
2187 assert_ne!(tree.focused(), Some(inner), "focus genuinely left");
2188 assert!(
2189 tree.active_overlays().is_empty(),
2190 "an overlay must not stay open over the focus ring that left it"
2191 );
2192 assert!(tree.focused() == Some(after) || tree.focused() == Some(anchor));
2193 }
2194
2195 #[test]
2197 fn a_centered_modal_is_never_dismissed_by_focus_moving() {
2198 let mut tree = WidgetTree::new();
2199 let outside = tree.add(FillWidget::new().focusable());
2200 let m1 = tree.add(FillWidget::new().focusable());
2201 let content = tree.add(StackWidget::new().child(m1));
2202 tree.layout(SizeProposal::exact(200.0, 100.0));
2203 tree.show_overlay(OverlayRequest {
2204 content_id: content,
2205 anchor: outside,
2206 placement: OverlayPlacement::Centered,
2207 dismiss: DismissBehavior::Manual,
2208 layer: OverlayLayer::InTree,
2209 parent_overlay: None,
2210 on_dismiss: None,
2211 fade_duration: None,
2212 });
2213
2214 tree.focus(m1);
2215 tree.focus(outside);
2218
2219 assert_eq!(
2220 tree.active_overlays().len(),
2221 1,
2222 "a modal is the one overlay that legitimately contains focus"
2223 );
2224 }
2225
2226 #[test]
2230 fn the_modal_scrim_survives_focus_moving_inside_the_modal() {
2231 let mut tree = WidgetTree::new();
2232 let opener = tree.add(FillWidget::new().focusable());
2233 let m1 = tree.add(FillWidget::new().focusable());
2234 let m2 = tree.add(FillWidget::new().focusable());
2235 let scrim = tree.add(FillWidget::new());
2236 let content = tree.add(StackWidget::new().child(m1).child(m2));
2237 tree.layout(SizeProposal::exact(200.0, 100.0));
2238
2239 tree.show_overlay(OverlayRequest {
2240 content_id: scrim,
2241 anchor: opener,
2242 placement: OverlayPlacement::FullViewport,
2243 dismiss: DismissBehavior::Manual,
2244 layer: OverlayLayer::InTree,
2245 parent_overlay: None,
2246 on_dismiss: None,
2247 fade_duration: None,
2248 });
2249 tree.show_overlay(OverlayRequest {
2250 content_id: content,
2251 anchor: opener,
2252 placement: OverlayPlacement::Centered,
2253 dismiss: DismissBehavior::Manual,
2254 layer: OverlayLayer::InTree,
2255 parent_overlay: None,
2256 on_dismiss: None,
2257 fade_duration: None,
2258 });
2259
2260 tree.focus(opener);
2261 tree.focus(m1);
2262 tab(&mut tree);
2263
2264 assert_eq!(
2265 tree.active_overlays().len(),
2266 2,
2267 "scrim and modal both stand while focus moves within the modal"
2268 );
2269 }
2270
2271 #[test]
2274 fn focus_moving_into_a_child_overlay_keeps_the_parent_open() {
2275 let mut tree = WidgetTree::new();
2276 let anchor = tree.add(FillWidget::new().focusable());
2277 let parent_item = tree.add(FillWidget::new().focusable());
2278 let parent_content = tree.add(StackWidget::new().child(parent_item));
2279 let child_item = tree.add(FillWidget::new().focusable());
2280 let child_content = tree.add(StackWidget::new().child(child_item));
2281 tree.layout(SizeProposal::exact(200.0, 100.0));
2282
2283 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2284 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2285
2286 tree.focus(parent_item);
2287 tree.focus(child_item);
2288
2289 assert_eq!(
2290 tree.active_overlays().len(),
2291 2,
2292 "opening a submenu is not leaving the menu that owns it"
2293 );
2294 }
2295
2296 #[test]
2299 fn focus_back_to_the_parent_overlay_closes_only_the_child() {
2300 let mut tree = WidgetTree::new();
2301 let anchor = tree.add(FillWidget::new().focusable());
2302 let parent_item = tree.add(FillWidget::new().focusable());
2303 let parent_content = tree.add(StackWidget::new().child(parent_item));
2304 let child_item = tree.add(FillWidget::new().focusable());
2305 let child_content = tree.add(StackWidget::new().child(child_item));
2306 tree.layout(SizeProposal::exact(200.0, 100.0));
2307
2308 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2309 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2310
2311 tree.focus(child_item);
2312 tree.focus(parent_item);
2313
2314 assert_eq!(
2315 tree.active_overlays(),
2316 vec![parent],
2317 "the submenu goes, the menu that owns it stays"
2318 );
2319 }
2320
2321 #[test]
2324 fn leaving_a_nested_cascade_closes_every_level() {
2325 let mut tree = WidgetTree::new();
2326 let anchor = tree.add(FillWidget::new().focusable());
2327 let away = tree.add(FillWidget::new().focusable());
2328 let parent_item = tree.add(FillWidget::new().focusable());
2329 let parent_content = tree.add(StackWidget::new().child(parent_item));
2330 let child_item = tree.add(FillWidget::new().focusable());
2331 let child_content = tree.add(StackWidget::new().child(child_item));
2332 tree.layout(SizeProposal::exact(200.0, 100.0));
2333
2334 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2335 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2336
2337 tree.focus(child_item);
2338 tree.focus(away);
2339
2340 assert!(
2341 tree.active_overlays().is_empty(),
2342 "one move out of the cascade must leave nothing behind"
2343 );
2344 }
2345
2346 #[test]
2356 fn a_dropdown_inside_a_modal_still_follows_focus_out() {
2357 let mut tree = WidgetTree::new();
2358 let opener = tree.add(FillWidget::new().focusable());
2359 let trigger = tree.add(FillWidget::new().focusable());
2360 let next = tree.add(FillWidget::new().focusable());
2361 let modal_content = tree.add(StackWidget::new().child(trigger).child(next));
2362 let panel = tree.add(StackWidget::new());
2363 tree.layout(SizeProposal::exact(200.0, 100.0));
2364
2365 tree.show_overlay(OverlayRequest {
2366 content_id: modal_content,
2367 anchor: opener,
2368 placement: OverlayPlacement::Centered,
2369 dismiss: DismissBehavior::Manual,
2370 layer: OverlayLayer::InTree,
2371 parent_overlay: None,
2372 on_dismiss: None,
2373 fade_duration: None,
2374 });
2375 show_anchored(&mut tree, panel, trigger, None);
2377
2378 tree.focus(trigger);
2379 assert_eq!(
2380 tree.active_overlays().len(),
2381 2,
2382 "precondition: modal + panel"
2383 );
2384
2385 tab(&mut tree);
2386
2387 assert_eq!(
2388 tree.focused(),
2389 Some(next),
2390 "Tab moves on within the modal, as it should"
2391 );
2392 assert_eq!(
2393 tree.active_overlays().len(),
2394 1,
2395 "the dropdown must go — only the modal hosting it stays"
2396 );
2397 }
2398
2399 #[test]
2407 fn a_viewport_placed_notification_ignores_focus_moving() {
2408 let mut tree = WidgetTree::new();
2409 let trigger = tree.add(FillWidget::new().focusable());
2410 let elsewhere = tree.add(FillWidget::new().focusable());
2411 let snack = tree.add(StackWidget::new());
2412 tree.layout(SizeProposal::exact(200.0, 100.0));
2413 tree.show_overlay(OverlayRequest {
2414 content_id: snack,
2415 anchor: trigger,
2416 placement: OverlayPlacement::BottomCenter,
2417 dismiss: DismissBehavior::Manual,
2418 layer: OverlayLayer::InTree,
2419 parent_overlay: None,
2420 on_dismiss: None,
2421 fade_duration: None,
2422 });
2423
2424 tree.focus(trigger);
2425 tab(&mut tree);
2426
2427 assert_eq!(
2428 tree.active_overlays().len(),
2429 1,
2430 "a snackbar outlives the keystroke that moved focus off its trigger"
2431 );
2432 assert_ne!(tree.focused(), Some(trigger), "and focus did move");
2433 assert_eq!(tree.focused(), Some(elsewhere));
2434 }
2435
2436 #[test]
2440 fn leaving_a_hosted_menu_spares_the_host() {
2441 let mut tree = WidgetTree::new();
2442 let opener = tree.add(FillWidget::new().focusable());
2443 let away = tree.add(FillWidget::new().focusable());
2444 let modal_item = tree.add(FillWidget::new().focusable());
2445 let modal_content = tree.add(StackWidget::new().child(modal_item));
2446 let menu_item = tree.add(FillWidget::new().focusable());
2447 let menu_content = tree.add(StackWidget::new().child(menu_item));
2448 tree.layout(SizeProposal::exact(200.0, 100.0));
2449
2450 let host = tree.show_overlay(OverlayRequest {
2452 content_id: modal_content,
2453 anchor: opener,
2454 placement: OverlayPlacement::Centered,
2455 dismiss: DismissBehavior::Manual,
2456 layer: OverlayLayer::InTree,
2457 parent_overlay: None,
2458 on_dismiss: None,
2459 fade_duration: None,
2460 });
2461 show_anchored(&mut tree, menu_content, modal_item, Some(host));
2462
2463 tree.focus(menu_item);
2464 tree.focus(away);
2465
2466 assert_eq!(
2467 tree.active_overlays(),
2468 vec![host],
2469 "the menu goes; the modal hosting it stays"
2470 );
2471 }
2472
2473 #[test]
2477 fn leaving_the_anchor_dismisses_a_panel_focus_never_entered() {
2478 let mut tree = WidgetTree::new();
2479 let trigger = tree.add(FillWidget::new().focusable());
2480 let after = tree.add(FillWidget::new().focusable());
2481 let content = tree.add(StackWidget::new());
2482 tree.layout(SizeProposal::exact(200.0, 100.0));
2483 show_anchored(&mut tree, content, trigger, None);
2484
2485 tree.focus(trigger);
2486 assert_eq!(tree.active_overlays().len(), 1, "precondition: panel is up");
2487
2488 tree.focus(after);
2489 assert!(
2490 tree.active_overlays().is_empty(),
2491 "leaving the trigger is leaving the dropdown it owns"
2492 );
2493 }
2494}