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(
188 &mut self,
189 from: WidgetId,
190 rect: Rect,
191 margin: f32,
192 align: crate::event::ScrollAlign,
193 motion: crate::event::ScrollMotion,
194 ops: &mut dyn crate::window::WindowOps,
195 ) {
196 let applied = std::sync::Arc::new(std::sync::Mutex::new(Point::ZERO));
202 let mut rect = rect;
203 let mut current = self.arena.parent(from);
204 let mut pending_align = align;
207 while let Some(ancestor_id) = current {
208 if let Some(node) = self.arena.get(ancestor_id)
209 && node.clips_children
210 {
211 let viewport = node.bounds;
212 let align =
213 std::mem::replace(&mut pending_align, crate::event::ScrollAlign::Minimal);
214 let needs_scroll = matches!(align, crate::event::ScrollAlign::Fraction(_))
217 || rect.y - margin < viewport.y
218 || rect.bottom() + margin > viewport.bottom()
219 || rect.x - margin < viewport.x
220 || rect.right() + margin > viewport.right();
221
222 if needs_scroll {
223 *applied.lock().unwrap() = Point::ZERO;
224 self.dispatch_to_widget(
225 ancestor_id,
226 &WidgetEvent::ScrollIntoView {
227 target_bounds: rect,
228 margin,
229 align,
230 motion,
231 applied_scroll: Some(applied.clone()),
232 },
233 &mut *ops,
234 );
235 let delta = *applied.lock().unwrap();
238 if delta != Point::ZERO {
239 rect =
240 Rect::new(rect.x - delta.x, rect.y - delta.y, rect.width, rect.height);
241 }
242 }
243 }
244 current = self.arena.parent(ancestor_id);
245 }
246 }
247
248 pub fn focus(&mut self, id: WidgetId) {
250 self.focus_with_origin(id, crate::focus::FocusOrigin::Programmatic);
251 }
252
253 pub fn focus_ops(&mut self, id: WidgetId, ops: &mut dyn crate::window::WindowOps) {
256 self.focus_with_origin_ops(id, crate::focus::FocusOrigin::Programmatic, ops);
257 }
258
259 pub fn focused(&self) -> Option<WidgetId> {
261 self.focused
262 }
263
264 pub fn ime_context_for_focused(&self) -> Option<crate::ime::ImeContext> {
270 self.focused.and_then(|id| self.arena.ime_context(id))
271 }
272
273 pub fn first_focusable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
278 if !self.arena.is_active(root) {
279 return None;
280 }
281 let mut entries = Vec::new();
282 self.collect_scope_entries(root, &mut entries);
283 sort_scope_entries(&mut entries);
284 let scope = ScopeNode {
285 policy: crate::focus::TraversalScopePolicy::Cycle,
286 entries,
287 };
288 enter_scope_edge(&scope, false)
289 }
290
291 pub fn is_active(&self, id: WidgetId) -> bool {
297 self.arena.is_active(id)
298 }
299
300 pub fn widget_initial_focus_hint(&self, id: WidgetId) -> Option<WidgetId> {
316 if !self.arena.is_active(id) {
317 return None;
318 }
319 if let Some(node) = self.arena.get(id) {
320 if let Some(target) = node.widget.initial_focus_hint()
321 && self.arena.is_active(target)
322 && self.is_descendant_of(target, id)
323 {
324 return Some(target);
325 }
326 for &child in &node.children {
327 if let Some(found) = self.widget_initial_focus_hint(child) {
328 return Some(found);
329 }
330 }
331 }
332 None
333 }
334
335 pub fn focus_origin(&self) -> Option<crate::focus::FocusOrigin> {
337 self.focus_origin
338 }
339
340 pub fn focus_visible_signal(&self) -> crate::signal::Signal<bool> {
344 self.focus_visible.clone()
345 }
346
347 pub(super) fn cycle_focus(&mut self, reverse: bool, ops: &mut dyn crate::window::WindowOps) {
356 let mut entries = Vec::new();
357 if let Some(modal_overlay) = self.overlay_manager.topmost_centered() {
358 let content_id = modal_overlay.content_id;
359 self.collect_scope_entries(content_id, &mut entries);
360 } else {
361 let roots = self.arena.roots();
362 for root in roots {
363 if self.tooltip_content_root(root).is_some() {
373 continue;
374 }
375 self.collect_scope_entries(root, &mut entries);
376 }
377 }
378 sort_scope_entries(&mut entries);
379 self.splice_sticky_tooltips_after_anchors(&mut entries);
380 let root_scope = ScopeNode {
381 policy: crate::focus::TraversalScopePolicy::Cycle,
382 entries,
383 };
384
385 if let StepResult::Found(next_id) = navigate_scope(&root_scope, self.focused, reverse, true)
386 {
387 self.focus_with_origin_ops(next_id, crate::focus::FocusOrigin::Keyboard, &mut *ops);
388 }
389 }
392
393 fn tooltip_content_root(&self, id: WidgetId) -> Option<usize> {
395 self.tooltips.iter().position(|e| e.content_id == id)
396 }
397
398 fn splice_sticky_tooltips_after_anchors(&self, entries: &mut Vec<ScopeEntry>) {
416 let sticky: Vec<(WidgetId, WidgetId)> = self
417 .tooltips
418 .iter()
419 .filter(|e| e.is_sticky && e.overlay_id.is_some())
420 .map(|e| (e.anchor_id, e.content_id))
421 .collect();
422
423 for (anchor_id, content_id) in sticky {
424 let mut panel = Vec::new();
425 self.collect_scope_entries(content_id, &mut panel);
426 if panel.is_empty() {
427 continue;
428 }
429 sort_scope_entries(&mut panel);
430
431 let stop = self
436 .find_focusable_at_or_above(anchor_id)
437 .unwrap_or(anchor_id);
438 let at = entries
439 .iter()
440 .position(|entry| scope_entry_contains(entry, stop))
441 .map_or(entries.len(), |i| i + 1);
446
447 for (offset, item) in panel.into_iter().enumerate() {
448 entries.insert(at + offset, item);
449 }
450 }
451 }
452
453 pub(super) fn tab_stop_effective(&self, id: WidgetId) -> bool {
461 let mut current = Some(id);
462 while let Some(cur) = current {
463 let Some(node) = self.arena.get(cur) else {
464 break;
465 };
466 if let Some(prop) = node.tab_stop.as_ref() {
467 return prop.get();
468 }
469 current = node.parent;
470 }
471 true
472 }
473
474 pub(super) fn is_node_focusable(&self, node: &crate::arena::WidgetNode) -> bool {
476 node.node_focusable.unwrap_or(false)
477 }
478
479 pub(super) fn find_focusable_at_or_above(&self, id: WidgetId) -> Option<WidgetId> {
481 let mut current = Some(id);
482 while let Some(current_id) = current {
483 if let Some(node) = self.arena.get(current_id)
484 && self.is_node_focusable(node)
485 {
486 return Some(current_id);
487 }
488 current = self.arena.parent(current_id);
489 }
490 None
491 }
492
493 fn collect_scope_entries(&self, id: WidgetId, out: &mut Vec<ScopeEntry>) {
509 if !self.arena.is_active(id) {
510 return;
511 }
512 let Some(node) = self.arena.get(id) else {
513 return;
514 };
515 if node
516 .enabled_state
517 .as_ref()
518 .map(|s| !s.get())
519 .unwrap_or(false)
520 {
521 return;
522 }
523
524 if let Some(policy) = node.node_traversal_scope {
527 let mut child_entries = Vec::new();
528 for &child in &node.children {
529 self.collect_scope_entries(child, &mut child_entries);
530 }
531 sort_scope_entries(&mut child_entries);
532 out.push(ScopeEntry {
533 tab_index: node.node_tab_index,
534 kind: ScopeEntryKind::Scope(ScopeNode {
535 policy,
536 entries: child_entries,
537 }),
538 });
539 return;
540 }
541
542 if self.is_node_focusable(node) && self.tab_stop_effective(id) {
544 out.push(ScopeEntry {
545 tab_index: node.node_tab_index,
546 kind: ScopeEntryKind::Focusable(id),
547 });
548 }
549 for &child in &node.children {
550 self.collect_scope_entries(child, out);
551 }
552 }
553
554 pub(super) fn strict_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
559 let mut chain = Vec::new();
560 if let Some(start) = id {
561 let mut current = self.arena.parent(start);
562 while let Some(parent) = current {
563 chain.push(parent);
564 current = self.arena.parent(parent);
565 }
566 }
567 chain
568 }
569
570 pub(crate) fn update_focus_within_signals(
575 &mut self,
576 old: Option<WidgetId>,
577 new: Option<WidgetId>,
578 ) {
579 let old_chain = self.strict_ancestors_of(old);
580 let new_chain = self.strict_ancestors_of(new);
581 for &id in &old_chain {
583 if !new_chain.contains(&id)
584 && let Some(node) = self.arena.get(id)
585 && let Some(sig) = node.focus_within_signal.clone()
586 {
587 sig.set(false);
588 }
589 }
590 for &id in &new_chain {
592 if !old_chain.contains(&id)
593 && let Some(node) = self.arena.get(id)
594 && let Some(sig) = node.focus_within_signal.clone()
595 {
596 sig.set(true);
597 }
598 }
599 }
600
601 pub(super) fn inclusive_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
604 let mut chain = Vec::new();
605 if let Some(start) = id {
606 chain.push(start);
607 chain.extend(self.strict_ancestors_of(Some(start)));
608 }
609 chain
610 }
611
612 pub(crate) fn update_view_focus_signals(
617 &mut self,
618 old: Option<WidgetId>,
619 new: Option<WidgetId>,
620 ) {
621 let old_chain = self.inclusive_ancestors_of(old);
622 let new_chain = self.inclusive_ancestors_of(new);
623 for &id in &old_chain {
624 if !new_chain.contains(&id)
625 && let Some(node) = self.arena.get(id)
626 && let Some(sig) = node.view_focus_signal.clone()
627 {
628 sig.set(false);
629 }
630 }
631 for &id in &new_chain {
632 if !old_chain.contains(&id)
633 && let Some(node) = self.arena.get(id)
634 && let Some(sig) = node.view_focus_signal.clone()
635 {
636 sig.set(true);
637 }
638 }
639 }
640
641 pub(crate) fn view_focus_signal_for(
644 &mut self,
645 node_id: WidgetId,
646 ) -> crate::signal::Signal<bool> {
647 if let Some(node) = self.arena.get(node_id)
648 && let Some(sig) = node.view_focus_signal.clone()
649 {
650 return sig;
651 }
652 let active = self.inclusive_ancestors_of(self.focused).contains(&node_id);
653 let sig = crate::signal::Signal::new(active);
654 if let Some(node) = self.arena.get_mut(node_id) {
655 node.view_focus_signal = Some(sig.clone());
656 }
657 sig
658 }
659
660 pub fn view_focus_active_for(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
666 match self.find_focusable_at_or_above(node_id) {
667 Some(scope) => self.view_focus_signal_for(scope),
668 None => crate::signal::Signal::new(true),
669 }
670 }
671
672 pub fn begin_view_focus(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
677 let sig = self.view_focus_signal_for(node_id);
678 self.view_focus_stack.push(sig.clone());
679 sig
680 }
681
682 pub fn end_view_focus(&mut self) {
684 self.view_focus_stack.pop();
685 }
686
687 pub fn current_view_focus(&self) -> Option<crate::signal::Signal<bool>> {
689 self.view_focus_stack.last().cloned()
690 }
691
692 pub(crate) fn set_hovered(&mut self, value: Option<WidgetId>) {
706 if let Some(entry) = self.pointers.hover_owner_mut() {
707 entry.hovered = value;
708 }
709 if self.hovered_signal.get() != value {
710 self.hovered_signal.set(value);
711 }
712 }
713
714 pub(crate) fn set_focused(&mut self, value: Option<WidgetId>) {
721 self.focused = value;
722 if self.focused_signal.get() != value {
723 self.focused_signal.set(value);
724 }
725 }
726
727 pub(crate) fn update_hover_within_signals(
730 &mut self,
731 old: Option<WidgetId>,
732 new: Option<WidgetId>,
733 ) {
734 let old_chain = self.strict_ancestors_of(old);
735 let new_chain = self.strict_ancestors_of(new);
736 if let Some(entry) = self.pointers.hover_owner_mut() {
740 entry.hover_within = new_chain.clone();
741 }
742 for &id in &old_chain {
743 if !new_chain.contains(&id)
744 && let Some(node) = self.arena.get(id)
745 && let Some(sig) = node.hover_within_signal.clone()
746 {
747 sig.set(false);
748 }
749 }
750 for &id in &new_chain {
751 if !old_chain.contains(&id)
752 && let Some(node) = self.arena.get(id)
753 && let Some(sig) = node.hover_within_signal.clone()
754 {
755 sig.set(true);
756 }
757 }
758 }
759}
760
761struct ScopeEntry {
773 tab_index: Option<i32>,
774 kind: ScopeEntryKind,
775}
776
777enum ScopeEntryKind {
778 Focusable(WidgetId),
779 Scope(ScopeNode),
780}
781
782struct ScopeNode {
784 policy: crate::focus::TraversalScopePolicy,
785 entries: Vec<ScopeEntry>,
786}
787
788enum StepResult {
790 Found(WidgetId),
792 Escaped,
796}
797
798fn scope_wraps(policy: crate::focus::TraversalScopePolicy, is_root: bool) -> bool {
801 is_root || matches!(policy, crate::focus::TraversalScopePolicy::Cycle)
802}
803
804fn sort_scope_entries(entries: &mut [ScopeEntry]) {
807 entries.sort_by(|a, b| match (a.tab_index, b.tab_index) {
808 (Some(ia), Some(ib)) => ia.cmp(&ib),
809 (Some(_), None) => std::cmp::Ordering::Less,
810 (None, Some(_)) => std::cmp::Ordering::Greater,
811 (None, None) => std::cmp::Ordering::Equal,
812 });
813}
814
815fn scope_entry_contains(entry: &ScopeEntry, f: WidgetId) -> bool {
817 match &entry.kind {
818 ScopeEntryKind::Focusable(id) => *id == f,
819 ScopeEntryKind::Scope(child) => child.entries.iter().any(|e| scope_entry_contains(e, f)),
820 }
821}
822
823fn enter_scope_edge(scope: &ScopeNode, reverse: bool) -> Option<WidgetId> {
827 let n = scope.entries.len();
828 for k in 0..n {
829 let idx = if reverse { n - 1 - k } else { k };
830 match &scope.entries[idx].kind {
831 ScopeEntryKind::Focusable(id) => return Some(*id),
832 ScopeEntryKind::Scope(child) => {
833 if let Some(id) = enter_scope_edge(child, reverse) {
834 return Some(id);
835 }
836 }
837 }
838 }
839 None
840}
841
842fn navigate_scope(
846 scope: &ScopeNode,
847 focused: Option<WidgetId>,
848 reverse: bool,
849 is_root: bool,
850) -> StepResult {
851 if scope.entries.is_empty() {
852 return StepResult::Escaped;
853 }
854
855 let cur = focused.and_then(|f| {
856 scope
857 .entries
858 .iter()
859 .position(|e| scope_entry_contains(e, f))
860 });
861
862 let from = match cur {
863 None => {
864 return match enter_scope_edge(scope, reverse) {
867 Some(id) => StepResult::Found(id),
868 None => StepResult::Escaped,
869 };
870 }
871 Some(i) => i,
872 };
873
874 if let ScopeEntryKind::Scope(child) = &scope.entries[from].kind
877 && let StepResult::Found(id) = navigate_scope(child, focused, reverse, false)
878 {
879 return StepResult::Found(id);
880 }
881
882 step_to_sibling(scope, from, reverse, is_root)
883}
884
885fn step_to_sibling(scope: &ScopeNode, from: usize, reverse: bool, is_root: bool) -> StepResult {
889 let n = scope.entries.len();
890 let mut idx = from;
891 for _ in 0..n {
892 let next = if reverse {
893 if idx == 0 {
894 if scope_wraps(scope.policy, is_root) {
895 n - 1
896 } else {
897 return StepResult::Escaped;
898 }
899 } else {
900 idx - 1
901 }
902 } else if idx + 1 >= n {
903 if scope_wraps(scope.policy, is_root) {
904 0
905 } else {
906 return StepResult::Escaped;
907 }
908 } else {
909 idx + 1
910 };
911
912 match &scope.entries[next].kind {
913 ScopeEntryKind::Focusable(id) => return StepResult::Found(*id),
914 ScopeEntryKind::Scope(child) => {
915 if let Some(id) = enter_scope_edge(child, reverse) {
916 return StepResult::Found(id);
917 }
918 idx = next;
920 }
921 }
922 }
923 StepResult::Escaped
924}
925
926#[cfg(test)]
927mod tests {
928 use super::*;
929 use crate::test_widgets::FillWidget;
930 use crate::widget_builder::WidgetBuilder;
931
932 #[test]
941 fn a_reveal_walks_the_ancestors_of_whoever_owns_the_rect() {
942 use crate::event::{EventResponse, ScrollAlign, ScrollMotion};
943 use std::cell::Cell;
944 use std::rc::Rc;
945
946 let asked = Rc::new(Cell::new(0usize));
947 let mut tree = WidgetTree::new();
948 let viewport = {
950 let asked = asked.clone();
951 tree.add(
952 FillWidget::new()
955 .on_scroll(move |event, _ctx| {
956 if matches!(event, WidgetEvent::ScrollIntoView { .. }) {
957 asked.set(asked.get() + 1);
958 }
959 EventResponse::Ignored
960 })
961 .clips_children(true),
962 )
963 };
964 let inside = tree.add_child(viewport, FillWidget::new());
965 let button = tree.add(FillWidget::new());
967 tree.layout(SizeProposal::exact(400.0, 300.0));
968
969 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
971 let mut ops = crate::window::NoopWindowOps;
972
973 tree.scroll_rect_into_view(
974 button,
975 target,
976 0.0,
977 ScrollAlign::Minimal,
978 ScrollMotion::Instant,
979 &mut ops,
980 );
981 assert_eq!(
982 asked.get(),
983 0,
984 "walking from the asker never reaches a viewport it is not inside"
985 );
986
987 tree.scroll_rect_into_view(
988 inside,
989 target,
990 0.0,
991 ScrollAlign::Minimal,
992 ScrollMotion::Instant,
993 &mut ops,
994 );
995 assert_eq!(
996 asked.get(),
997 1,
998 "walking from the rect's owner reaches the viewport enclosing it"
999 );
1000 }
1001
1002 #[test]
1003 fn focus_widget() {
1004 let mut tree = WidgetTree::new();
1005 let widget = tree.add(FillWidget::new());
1006 tree.layout(SizeProposal::exact(100.0, 50.0));
1007 tree.focus(widget);
1008 assert_eq!(tree.focused(), Some(widget));
1009 }
1010
1011 #[test]
1012 fn focus_change() {
1013 let mut tree = WidgetTree::new();
1014 let a = tree.add(FillWidget::new());
1015 let b = tree.add(FillWidget::new());
1016 tree.layout(SizeProposal::exact(100.0, 50.0));
1017 tree.focus(a);
1018 assert_eq!(tree.focused(), Some(a));
1019 tree.focus(b);
1020 assert_eq!(tree.focused(), Some(b));
1021 }
1022
1023 #[derive(Debug)]
1027 struct RebuildingRows {
1028 epoch: crate::signal::Signal<u64>,
1029 rows: Vec<WidgetId>,
1030 }
1031
1032 impl Widget for RebuildingRows {
1033 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1034 let sid = ctx.self_id();
1035 let reg = ctx.binding_registry();
1036 self.epoch
1037 .bind_to(sid, reg, crate::binding::BindingLevel::Rebuild);
1038 self.rows = (0..3)
1039 .map(|_| ctx.add(FillWidget::new().focusable()))
1040 .collect();
1041 self.rows.clone()
1042 }
1043
1044 fn layout_response(
1045 &self,
1046 proposal: SizeProposal,
1047 _ctx: &LayoutContext,
1048 ) -> crate::widget::LayoutResponse {
1049 proposal.resolve(0.0, 0.0).into()
1050 }
1051
1052 fn children(&self) -> Vec<WidgetId> {
1053 self.rows.clone()
1054 }
1055 }
1056
1057 #[test]
1067 fn a_rebuild_keeps_focus_inside_the_subtree_that_had_it() {
1068 let mut tree = WidgetTree::new();
1069 let epoch = crate::signal::Signal::new(0u64);
1070 let root = tree.add(RebuildingRows {
1071 epoch: epoch.clone(),
1072 rows: Vec::new(),
1073 });
1074 tree.layout(SizeProposal::exact(100.0, 60.0));
1075
1076 let first_row = tree
1077 .first_focusable_descendant(root)
1078 .expect("the rows are focusable");
1079 tree.focus(first_row);
1080 assert_eq!(tree.focused(), Some(first_row));
1081
1082 epoch.set(1);
1084 tree.layout(SizeProposal::exact(100.0, 60.0));
1085
1086 let focused = tree
1087 .focused()
1088 .expect("a rebuild must not drop focus out of the rebuilt subtree");
1089 assert_ne!(
1090 focused, first_row,
1091 "the old row is dead — focus must have moved to a freshly built one"
1092 );
1093 assert!(
1094 tree.is_descendant_of(focused, root),
1095 "focus must land back inside the rebuilt subtree"
1096 );
1097 assert!(tree.is_active(focused), "the focused node must be live");
1098 }
1099
1100 #[test]
1105 fn a_rebuild_elsewhere_does_not_steal_focus() {
1106 let mut tree = WidgetTree::new();
1107 let epoch = crate::signal::Signal::new(0u64);
1108 let outsider = tree.add(FillWidget::new().focusable());
1109 let _rows = tree.add(RebuildingRows {
1110 epoch: epoch.clone(),
1111 rows: Vec::new(),
1112 });
1113 tree.layout(SizeProposal::exact(100.0, 60.0));
1114
1115 tree.focus(outsider);
1116 assert_eq!(tree.focused(), Some(outsider));
1117
1118 epoch.set(1);
1119 tree.layout(SizeProposal::exact(100.0, 60.0));
1120
1121 assert_eq!(
1122 tree.focused(),
1123 Some(outsider),
1124 "a rebuild that never held focus must not pull focus into itself"
1125 );
1126 }
1127
1128 #[test]
1129 fn first_focusable_descendant_prefers_first_focusable_child() {
1130 let mut tree = WidgetTree::new();
1131 let a = tree.add(FillWidget::new().focusable());
1132 let _not_focusable = tree.add(FillWidget::new());
1133 let b = tree.add(FillWidget::new().focusable());
1134 let root = tree.add(
1135 crate::test_widgets::StackWidget::new()
1136 .add_child(a)
1137 .add_child(b),
1138 );
1139 tree.layout(SizeProposal::exact(100.0, 50.0));
1140
1141 assert_eq!(tree.first_focusable_descendant(root), Some(a));
1142 }
1143
1144 #[test]
1145 fn tab_cycles_through_focusable_widgets() {
1146 let mut tree = WidgetTree::new();
1147 let a = tree.add(FillWidget::new().focusable());
1148 let b = tree.add(FillWidget::new().focusable());
1149 let c = tree.add(FillWidget::new().focusable());
1150 tree.layout(SizeProposal::exact(100.0, 50.0));
1151
1152 assert_eq!(tree.focused(), None);
1153
1154 tree.press_key(Key::Tab, Modifiers::NONE);
1155 assert_eq!(tree.focused(), Some(a));
1156
1157 tree.press_key(Key::Tab, Modifiers::NONE);
1158 assert_eq!(tree.focused(), Some(b));
1159
1160 tree.press_key(Key::Tab, Modifiers::NONE);
1161 assert_eq!(tree.focused(), Some(c));
1162
1163 tree.press_key(Key::Tab, Modifiers::NONE);
1164 assert_eq!(tree.focused(), Some(a));
1165 }
1166
1167 #[test]
1168 fn tab_stop_on_composite_excludes_focusable_descendant() {
1169 let mut tree = WidgetTree::new();
1174 let leaf = tree.add(FillWidget::new().focusable());
1175 let composite = tree.add(crate::test_widgets::StackWidget::new().add_child(leaf));
1176 let other = tree.add(FillWidget::new().focusable());
1177 tree.layout(SizeProposal::exact(100.0, 50.0));
1178
1179 tree.set_tab_stop(composite, false);
1180
1181 tree.press_key(Key::Tab, Modifiers::NONE);
1182 assert_eq!(
1183 tree.focused(),
1184 Some(other),
1185 "Tab must skip the suppressed composite's inner leaf"
1186 );
1187 tree.press_key(Key::Tab, Modifiers::NONE);
1188 assert_eq!(
1189 tree.focused(),
1190 Some(other),
1191 "only the un-suppressed control participates in Tab"
1192 );
1193
1194 tree.set_tab_stop(composite, true);
1196 tree.set_focused(None);
1197 tree.press_key(Key::Tab, Modifiers::NONE);
1198 let first = tree.focused();
1199 tree.press_key(Key::Tab, Modifiers::NONE);
1200 let second = tree.focused();
1201 assert_ne!(
1202 first, second,
1203 "with the composite re-enabled, Tab visits both controls"
1204 );
1205 }
1206
1207 #[test]
1208 fn shift_tab_cycles_backwards() {
1209 let mut tree = WidgetTree::new();
1210 let a = tree.add(FillWidget::new().focusable());
1211 let b = tree.add(FillWidget::new().focusable());
1212 let c = tree.add(FillWidget::new().focusable());
1213 tree.layout(SizeProposal::exact(100.0, 50.0));
1214
1215 tree.press_key(Key::Tab, Modifiers::NONE);
1216 assert_eq!(tree.focused(), Some(a));
1217
1218 tree.press_key(Key::Tab, Modifiers::SHIFT);
1219 assert_eq!(tree.focused(), Some(c));
1220
1221 tree.press_key(Key::Tab, Modifiers::SHIFT);
1222 assert_eq!(tree.focused(), Some(b));
1223 }
1224
1225 #[test]
1226 fn tab_skips_non_focusable_widgets() {
1227 let mut tree = WidgetTree::new();
1228 let _not_focusable = tree.add(FillWidget::new());
1229 let a = tree.add(FillWidget::new().focusable());
1230 let _also_not = tree.add(FillWidget::new());
1231 let b = tree.add(FillWidget::new().focusable());
1232 tree.layout(SizeProposal::exact(100.0, 50.0));
1233
1234 tree.press_key(Key::Tab, Modifiers::NONE);
1235 assert_eq!(tree.focused(), Some(a));
1236
1237 tree.press_key(Key::Tab, Modifiers::NONE);
1238 assert_eq!(tree.focused(), Some(b));
1239 }
1240
1241 #[test]
1252 fn focus_does_not_check_that_the_node_is_focusable() {
1253 let mut tree = WidgetTree::new();
1254 let inert = tree.add(FillWidget::new()); tree.layout(SizeProposal::exact(100.0, 50.0));
1256 assert!(
1257 tree.tab_stops_within(inert).is_empty(),
1258 "the subject has to be unreachable for the point to be made"
1259 );
1260
1261 tree.focus(inert);
1262 assert_eq!(
1263 tree.focused(),
1264 Some(inert),
1265 "focus lands on a node Tab traversal can never offer"
1266 );
1267 }
1268
1269 #[test]
1270 fn tab_focus_has_keyboard_origin() {
1271 let mut tree = WidgetTree::new();
1272 tree.add(FillWidget::new().focusable());
1273 tree.layout(SizeProposal::exact(100.0, 50.0));
1274
1275 tree.press_key(Key::Tab, Modifiers::NONE);
1276 assert_eq!(
1277 tree.focus_origin(),
1278 Some(crate::focus::FocusOrigin::Keyboard)
1279 );
1280 }
1281
1282 #[test]
1283 fn tab_skips_focusable_inside_disabled_ancestor() {
1284 use crate::signal::Signal;
1285 use crate::test_widgets::StackWidget;
1286
1287 let mut tree = WidgetTree::new();
1288 let a = tree.add(FillWidget::new().focusable());
1289 let inner = tree.add(FillWidget::new().focusable());
1290 let disabled_container = tree.add(StackWidget::new().add_child(inner));
1291 let c = tree.add(FillWidget::new().focusable());
1292 tree.enabled_when(disabled_container, Signal::new(false));
1293 tree.layout(SizeProposal::exact(200.0, 100.0));
1294
1295 tree.press_key(Key::Tab, Modifiers::NONE);
1296 assert_eq!(tree.focused(), Some(a));
1297
1298 tree.press_key(Key::Tab, Modifiers::NONE);
1299 assert_eq!(
1300 tree.focused(),
1301 Some(c),
1302 "tab should skip the focusable widget nested inside the disabled container"
1303 );
1304 }
1305
1306 #[test]
1307 fn dormant_widget_not_in_focus_cycle() {
1308 let mut tree = WidgetTree::new();
1309 let a = tree.add(FillWidget::new().focusable());
1310 let b = tree.add(FillWidget::new().focusable());
1311 let c = tree.add(FillWidget::new().focusable());
1312 tree.layout(SizeProposal::exact(200.0, 100.0));
1313
1314 tree.focus(a);
1315 tree.set_dormant(b);
1316
1317 tree.press_key(Key::Tab, Modifiers::NONE);
1318 assert_eq!(tree.focused(), Some(c));
1319 }
1320
1321 #[test]
1326 fn revalidate_delivers_focus_lost_when_focused_widget_goes_dormant() {
1327 use std::cell::Cell;
1328 use std::rc::Rc;
1329
1330 let lost = Rc::new(Cell::new(0_u32));
1331 let gained = Rc::new(Cell::new(0_u32));
1332 let lost_c = lost.clone();
1333 let gained_c = gained.clone();
1334
1335 let mut tree = WidgetTree::new();
1336 let editor = tree.add(
1337 FillWidget::new()
1338 .focusable()
1339 .on_focus(move |is_gained, _ctx| {
1340 if is_gained {
1341 gained_c.set(gained_c.get() + 1);
1342 } else {
1343 lost_c.set(lost_c.get() + 1);
1344 }
1345 }),
1346 );
1347 let _other = tree.add(FillWidget::new().focusable());
1348 tree.layout(SizeProposal::exact(200.0, 100.0));
1349
1350 tree.focus(editor);
1351 assert_eq!(tree.focused(), Some(editor));
1352 assert_eq!(gained.get(), 1, "focus() delivers FocusGained");
1353 assert_eq!(lost.get(), 0);
1354
1355 tree.set_dormant(editor);
1357 let mut noop = crate::window::NoopWindowOps;
1359 tree.revalidate_interaction_state(&mut noop);
1360
1361 assert_eq!(
1362 tree.focused(),
1363 None,
1364 "tree focus must clear when the target is dormant"
1365 );
1366 assert_eq!(
1367 lost.get(),
1368 1,
1369 "FocusLost must reach the dormant widget so it can clear has_focus / caret blink"
1370 );
1371 }
1372
1373 #[test]
1374 fn tab_cycles_focus_in_tree_order() {
1375 let mut tree = WidgetTree::new();
1376 let a = tree.add(FillWidget::new().focusable());
1377 let b = tree.add(FillWidget::new().focusable());
1378 tree.layout(SizeProposal::exact(200.0, 80.0));
1379
1380 tree.focus(a);
1381 assert_eq!(tree.focused(), Some(a));
1382
1383 tree.press_key(Key::Tab, Modifiers::NONE);
1384 assert_eq!(tree.focused(), Some(b));
1385
1386 tree.press_key(Key::Tab, Modifiers::SHIFT);
1387 assert_eq!(tree.focused(), Some(a));
1388 }
1389
1390 #[test]
1391 fn focus_survives_theme_switch() {
1392 let mut tree = WidgetTree::new();
1393 let a = tree.add(FillWidget::new().focusable());
1394 let _b = tree.add(FillWidget::new().focusable());
1395 tree.layout(SizeProposal::exact(200.0, 80.0));
1396
1397 tree.focus(a);
1398 assert_eq!(tree.focused(), Some(a));
1399
1400 tree.set_theme(crate::presets::intui::dark());
1401 tree.layout(SizeProposal::exact(200.0, 80.0));
1402
1403 assert_eq!(
1404 tree.focused(),
1405 Some(a),
1406 "theme switch must not clobber focus"
1407 );
1408 }
1409
1410 #[test]
1411 fn focus_survives_locale_switch() {
1412 let mut tree = WidgetTree::new();
1413 let a = tree.add(FillWidget::new().focusable());
1414 tree.layout(SizeProposal::exact(200.0, 80.0));
1415
1416 tree.focus(a);
1417 tree.set_locale("fr-FR".to_string());
1418 tree.layout(SizeProposal::exact(200.0, 80.0));
1419
1420 assert_eq!(
1421 tree.focused(),
1422 Some(a),
1423 "locale switch must not clobber focus"
1424 );
1425 }
1426
1427 #[test]
1430 fn focus_within_flips_when_descendant_takes_focus() {
1431 use crate::signal::Signal;
1432 use crate::test_widgets::StackWidget;
1433 use crate::widget_builder::WidgetBuilder;
1434
1435 let halo = Signal::new(false);
1436
1437 let mut tree = WidgetTree::new();
1438 let leaf = tree.add(FillWidget::new().focusable());
1439 let mid = tree.add(StackWidget::new().add_child(leaf));
1440 let _outer = tree.add(StackWidget::new().add_child(mid).focus_within(halo.clone()));
1441
1442 tree.layout(SizeProposal::exact(100.0, 50.0));
1443 assert!(!halo.get(), "no focus yet → signal is false");
1444
1445 tree.focus(leaf);
1446 assert!(halo.get(), "leaf now focused, outer is its strict ancestor");
1447 }
1448
1449 #[test]
1450 fn focus_within_strict_ancestors_only() {
1451 use crate::signal::Signal;
1454 use crate::widget_builder::WidgetBuilder;
1455
1456 let halo = Signal::new(false);
1457
1458 let mut tree = WidgetTree::new();
1459 let widget = tree.add(FillWidget::new().focusable().focus_within(halo.clone()));
1460 tree.layout(SizeProposal::exact(100.0, 50.0));
1461 tree.focus(widget);
1462
1463 assert!(
1464 !halo.get(),
1465 "focusing the widget itself must not set its own focus_within"
1466 );
1467 }
1468
1469 #[test]
1470 fn view_focus_active_is_inclusive_for_the_view_itself() {
1471 use crate::test_widgets::StackWidget;
1477 use crate::widget_builder::WidgetBuilder;
1478
1479 let mut tree = WidgetTree::new();
1480 let item = tree.add(FillWidget::new()); let view = tree.add(StackWidget::new().add_child(item).focusable(true));
1482 let outside = tree.add(FillWidget::new().focusable());
1483 tree.layout(SizeProposal::exact(100.0, 50.0));
1484
1485 let active = tree.view_focus_active_for(item);
1486 assert!(!active.get(), "no focus yet → scope inactive");
1487
1488 tree.focus(view);
1489 assert!(
1490 active.get(),
1491 "view focused directly → scope active (focus_within would be false here)"
1492 );
1493
1494 tree.focus(outside);
1495 assert!(
1496 !active.get(),
1497 "focus moved outside the view → scope inactive"
1498 );
1499 }
1500
1501 #[test]
1502 fn begin_view_focus_for_keys_on_the_view_root_not_the_building_pane() {
1503 use crate::test_widgets::StackWidget;
1511 use crate::widget_builder::WidgetBuilder;
1512
1513 let mut tree = WidgetTree::new();
1514 let row = tree.add(FillWidget::new()); let pane = tree.add(StackWidget::new().add_child(row)); let root = tree.add(StackWidget::new().add_child(pane).focusable(true));
1517 tree.layout(SizeProposal::exact(100.0, 50.0));
1518
1519 let keyed_on_root = tree.begin_view_focus(root);
1521 tree.end_view_focus();
1522 let keyed_on_pane = tree.view_focus_signal_for(pane);
1524
1525 assert!(!keyed_on_root.get(), "no focus yet → inactive");
1526 tree.focus(root);
1527 assert!(keyed_on_root.get(), "view root focused → row scope active");
1528 assert!(
1529 !keyed_on_pane.get(),
1530 "pane-keyed scope stays false: the focused root is the pane's ancestor, not its descendant",
1531 );
1532 }
1533
1534 #[test]
1535 fn focus_visible_tracks_input_modality() {
1536 use crate::event::{Key, Modifiers};
1539 let mut tree = WidgetTree::new();
1540 let w = tree.add(FillWidget::new().focusable());
1541 tree.layout(SizeProposal::exact(100.0, 50.0));
1542 let vis = tree.focus_visible_signal();
1543 assert!(!vis.get(), "starts not focus-visible");
1544
1545 tree.press_key(Key::Tab, Modifiers::NONE);
1546 assert!(vis.get(), "keyboard input turns focus-visible ON");
1547
1548 tree.click(w);
1549 assert!(!vis.get(), "pointer input turns focus-visible OFF");
1550
1551 tree.press_key(Key::ArrowDown, Modifiers::NONE);
1552 assert!(vis.get(), "keyboard input turns it back ON");
1553 }
1554
1555 #[test]
1556 fn view_focus_active_without_focusable_ancestor_is_constant_true() {
1557 let mut tree = WidgetTree::new();
1560 let item = tree.add(FillWidget::new());
1561 tree.layout(SizeProposal::exact(100.0, 50.0));
1562 assert!(tree.view_focus_active_for(item).get());
1563 }
1564
1565 #[test]
1566 fn focus_within_diff_across_siblings() {
1567 use crate::signal::Signal;
1570 use crate::test_widgets::StackWidget;
1571 use crate::widget_builder::WidgetBuilder;
1572
1573 let sig_a = Signal::new(false);
1574 let sig_b = Signal::new(false);
1575
1576 let mut tree = WidgetTree::new();
1577 let leaf_a = tree.add(FillWidget::new().focusable());
1578 let leaf_b = tree.add(FillWidget::new().focusable());
1579 let mid_a = tree.add(
1580 StackWidget::new()
1581 .add_child(leaf_a)
1582 .focus_within(sig_a.clone()),
1583 );
1584 let mid_b = tree.add(
1585 StackWidget::new()
1586 .add_child(leaf_b)
1587 .focus_within(sig_b.clone()),
1588 );
1589 let _root = tree.add(StackWidget::new().add_child(mid_a).add_child(mid_b));
1590
1591 tree.layout(SizeProposal::exact(100.0, 50.0));
1592 tree.focus(leaf_a);
1593 assert!(sig_a.get(), "sig_a true after focusing leaf_a");
1594 assert!(!sig_b.get(), "sig_b false: leaf_a is not its descendant");
1595
1596 tree.focus(leaf_b);
1597 assert!(!sig_a.get(), "sig_a flipped to false on focus move");
1598 assert!(sig_b.get(), "sig_b flipped to true on focus move");
1599 }
1600
1601 #[test]
1602 fn focus_within_clears_when_focused_widget_destroyed() {
1603 use crate::signal::Signal;
1604 use crate::test_widgets::StackWidget;
1605 use crate::widget_builder::WidgetBuilder;
1606
1607 let halo = Signal::new(false);
1608
1609 let mut tree = WidgetTree::new();
1610 let leaf = tree.add(FillWidget::new().focusable());
1611 let _outer = tree.add(
1612 StackWidget::new()
1613 .add_child(leaf)
1614 .focus_within(halo.clone()),
1615 );
1616
1617 tree.layout(SizeProposal::exact(100.0, 50.0));
1618 tree.focus(leaf);
1619 assert!(halo.get());
1620
1621 tree.destroy_subtree(leaf);
1622 assert!(
1623 !halo.get(),
1624 "destroying the focused widget must clear focus_within on its ancestors"
1625 );
1626 }
1627
1628 #[test]
1629 fn hover_within_flips_via_pointer_move() {
1630 use crate::signal::Signal;
1631 use crate::test_widgets::StackWidget;
1632 use crate::widget_builder::WidgetBuilder;
1633 use teksilo_canvas::Point;
1634
1635 let glow = Signal::new(false);
1636
1637 let mut tree = WidgetTree::new();
1638 let leaf = tree.add(FillWidget::new());
1639 let _outer = tree.add(
1640 StackWidget::new()
1641 .add_child(leaf)
1642 .hover_within(glow.clone()),
1643 );
1644
1645 tree.layout(SizeProposal::exact(100.0, 50.0));
1646 assert!(!glow.get());
1647
1648 tree.pointer_move(Point::new(50.0, 25.0));
1649 assert!(glow.get(), "pointer over leaf → outer.hover_within = true");
1650
1651 tree.pointer_move(Point::new(500.0, 500.0));
1652 assert!(
1653 !glow.get(),
1654 "pointer moves outside the tree → hover_within clears"
1655 );
1656 }
1657
1658 #[test]
1659 fn hover_within_strict_ancestors_only() {
1660 use crate::signal::Signal;
1661 use crate::widget_builder::WidgetBuilder;
1662 use teksilo_canvas::Point;
1663
1664 let glow = Signal::new(false);
1665
1666 let mut tree = WidgetTree::new();
1667 let _w = tree.add(FillWidget::new().hover_within(glow.clone()));
1668 tree.layout(SizeProposal::exact(100.0, 50.0));
1669 tree.pointer_move(Point::new(50.0, 25.0));
1670
1671 assert!(
1672 !glow.get(),
1673 "hovering the widget itself must not set its own hover_within"
1674 );
1675 }
1676}
1677
1678#[cfg(test)]
1679mod tests_scope {
1680 use super::*;
1685 use crate::focus::TraversalScopePolicy::{Continue, Cycle};
1686 use crate::test_widgets::{FillWidget, StackWidget};
1687 use crate::widget_builder::WidgetBuilder;
1688
1689 fn indexed(tree: &mut WidgetTree, idx: i32) -> WidgetId {
1691 tree.add(FillWidget::new().focusable().tab_index(idx))
1692 }
1693
1694 fn tab(tree: &mut WidgetTree) {
1695 tree.press_key(Key::Tab, Modifiers::NONE);
1696 }
1697 fn shift_tab(tree: &mut WidgetTree) {
1698 tree.press_key(Key::Tab, Modifiers::SHIFT);
1699 }
1700
1701 #[test]
1702 fn overlapping_tab_index_in_sibling_scopes_does_not_interleave() {
1703 let mut tree = WidgetTree::new();
1704 let a1 = indexed(&mut tree, 1);
1705 let a2 = indexed(&mut tree, 2);
1706 let scope_a = tree.add(StackWidget::new().add_child(a1).add_child(a2));
1707 let b1 = indexed(&mut tree, 1);
1708 let b2 = indexed(&mut tree, 2);
1709 let scope_b = tree.add(StackWidget::new().add_child(b1).add_child(b2));
1710 let _root = tree.add(StackWidget::new().add_child(scope_a).add_child(scope_b));
1711 tree.set_traversal_scope(scope_a, Continue);
1712 tree.set_traversal_scope(scope_b, Continue);
1713 tree.layout(SizeProposal::exact(100.0, 100.0));
1714
1715 for expected in [a1, a2, b1, b2, a1] {
1717 tab(&mut tree);
1718 assert_eq!(tree.focused(), Some(expected));
1719 }
1720 }
1721
1722 #[test]
1723 fn continue_scope_flows_out_at_the_ends() {
1724 let mut tree = WidgetTree::new();
1726 let a = tree.add(FillWidget::new().focusable());
1727 let c1 = tree.add(FillWidget::new().focusable());
1728 let c2 = tree.add(FillWidget::new().focusable());
1729 let scope_c = tree.add(StackWidget::new().add_child(c1).add_child(c2));
1730 let b = tree.add(FillWidget::new().focusable());
1731 let _root = tree.add(
1732 StackWidget::new()
1733 .add_child(a)
1734 .add_child(scope_c)
1735 .add_child(b),
1736 );
1737 tree.set_traversal_scope(scope_c, Continue);
1738 tree.layout(SizeProposal::exact(100.0, 100.0));
1739
1740 for expected in [a, c1, c2, b, a] {
1741 tab(&mut tree);
1742 assert_eq!(tree.focused(), Some(expected));
1743 }
1744 tree.focus(c1);
1746 shift_tab(&mut tree);
1747 assert_eq!(tree.focused(), Some(a));
1748 }
1749
1750 #[test]
1751 fn cycle_scope_wraps_and_never_escapes() {
1752 let mut tree = WidgetTree::new();
1754 let a = tree.add(FillWidget::new().focusable());
1755 let d1 = tree.add(FillWidget::new().focusable());
1756 let d2 = tree.add(FillWidget::new().focusable());
1757 let scope_d = tree.add(StackWidget::new().add_child(d1).add_child(d2));
1758 let _root = tree.add(StackWidget::new().add_child(a).add_child(scope_d));
1759 tree.set_traversal_scope(scope_d, Cycle);
1760 tree.layout(SizeProposal::exact(100.0, 100.0));
1761
1762 tree.focus(d1);
1763 for _ in 0..10 {
1764 tab(&mut tree);
1765 let f = tree.focused();
1766 assert!(
1767 f == Some(d1) || f == Some(d2),
1768 "Cycle scope must trap Tab inside {{d1,d2}}, got {f:?}"
1769 );
1770 }
1771 tree.focus(d1);
1773 tab(&mut tree);
1774 assert_eq!(tree.focused(), Some(d2));
1775 shift_tab(&mut tree);
1776 assert_eq!(tree.focused(), Some(d1));
1777 shift_tab(&mut tree);
1778 assert_eq!(tree.focused(), Some(d2), "Shift+Tab at start wraps to last");
1779 }
1780
1781 #[test]
1782 fn empty_scope_is_skipped() {
1783 let mut tree = WidgetTree::new();
1785 let a = tree.add(FillWidget::new().focusable());
1786 let empty = tree.add(StackWidget::new());
1787 let b = tree.add(FillWidget::new().focusable());
1788 let _root = tree.add(
1789 StackWidget::new()
1790 .add_child(a)
1791 .add_child(empty)
1792 .add_child(b),
1793 );
1794 tree.set_traversal_scope(empty, Continue);
1795 tree.layout(SizeProposal::exact(100.0, 100.0));
1796
1797 for expected in [a, b, a] {
1798 tab(&mut tree);
1799 assert_eq!(tree.focused(), Some(expected));
1800 }
1801 }
1802
1803 #[test]
1804 fn single_member_cycle_scope_stays_put() {
1805 let mut tree = WidgetTree::new();
1806 let e = tree.add(FillWidget::new().focusable());
1807 let scope_e = tree.add(StackWidget::new().add_child(e));
1808 tree.set_traversal_scope(scope_e, Cycle);
1809 tree.layout(SizeProposal::exact(100.0, 50.0));
1810
1811 tree.focus(e);
1812 tab(&mut tree);
1813 assert_eq!(tree.focused(), Some(e));
1814 shift_tab(&mut tree);
1815 assert_eq!(tree.focused(), Some(e));
1816 }
1817
1818 #[test]
1819 fn nested_continue_in_cycle_flows_out_to_outer() {
1820 let mut tree = WidgetTree::new();
1822 let x = tree.add(FillWidget::new().focusable());
1823 let i1 = tree.add(FillWidget::new().focusable());
1824 let i2 = tree.add(FillWidget::new().focusable());
1825 let inner = tree.add(StackWidget::new().add_child(i1).add_child(i2));
1826 let y = tree.add(FillWidget::new().focusable());
1827 let outer = tree.add(
1828 StackWidget::new()
1829 .add_child(x)
1830 .add_child(inner)
1831 .add_child(y),
1832 );
1833 tree.set_traversal_scope(inner, Continue);
1834 tree.set_traversal_scope(outer, Cycle);
1835 tree.layout(SizeProposal::exact(100.0, 100.0));
1836
1837 for expected in [x, i1, i2, y, x] {
1838 tab(&mut tree);
1839 assert_eq!(tree.focused(), Some(expected));
1840 }
1841 tree.focus(i1);
1843 shift_tab(&mut tree);
1844 assert_eq!(tree.focused(), Some(x));
1845 }
1846
1847 #[test]
1848 fn nested_cycle_in_cycle_traps_in_the_inner_scope() {
1849 let mut tree = WidgetTree::new();
1852 let x = tree.add(FillWidget::new().focusable());
1853 let i1 = tree.add(FillWidget::new().focusable());
1854 let i2 = tree.add(FillWidget::new().focusable());
1855 let inner = tree.add(StackWidget::new().add_child(i1).add_child(i2));
1856 let y = tree.add(FillWidget::new().focusable());
1857 let outer = tree.add(
1858 StackWidget::new()
1859 .add_child(x)
1860 .add_child(inner)
1861 .add_child(y),
1862 );
1863 tree.set_traversal_scope(inner, Cycle);
1864 tree.set_traversal_scope(outer, Cycle);
1865 tree.layout(SizeProposal::exact(100.0, 100.0));
1866
1867 tree.focus(i1);
1868 for _ in 0..6 {
1869 tab(&mut tree);
1870 let f = tree.focused();
1871 assert!(
1872 f == Some(i1) || f == Some(i2),
1873 "inner Cycle must trap Tab; reached {f:?}"
1874 );
1875 }
1876 }
1877
1878 #[test]
1879 fn scoped_tab_index_orders_within_a_scope() {
1880 let mut tree = WidgetTree::new();
1882 let f3 = indexed(&mut tree, 3);
1883 let f1 = indexed(&mut tree, 1);
1884 let f2 = indexed(&mut tree, 2);
1885 let scope_f = tree.add(StackWidget::new().add_child(f3).add_child(f1).add_child(f2));
1886 tree.set_traversal_scope(scope_f, Cycle);
1887 tree.layout(SizeProposal::exact(100.0, 100.0));
1888
1889 for expected in [f1, f2, f3, f1] {
1890 tab(&mut tree);
1891 assert_eq!(tree.focused(), Some(expected));
1892 }
1893 }
1894
1895 #[test]
1896 fn destroyed_focused_widget_re_enters_at_first() {
1897 let mut tree = WidgetTree::new();
1898 let a = tree.add(FillWidget::new().focusable());
1899 let b = tree.add(FillWidget::new().focusable());
1900 tree.layout(SizeProposal::exact(100.0, 50.0));
1901
1902 tree.focus(b);
1903 tree.destroy_subtree(b);
1904 tab(&mut tree);
1905 assert_eq!(tree.focused(), Some(a), "Tab after destroy enters at first");
1906 }
1907
1908 #[test]
1909 fn set_traversal_scope_forces_node_non_focusable() {
1910 let mut tree = WidgetTree::new();
1913 let inner = tree.add(FillWidget::new().focusable());
1914 let scope = tree.add(StackWidget::new().add_child(inner).focusable(true));
1915 tree.set_traversal_scope(scope, Continue);
1916 tree.layout(SizeProposal::exact(100.0, 50.0));
1917
1918 tab(&mut tree);
1920 assert_eq!(tree.focused(), Some(inner));
1921 tab(&mut tree);
1922 assert_eq!(tree.focused(), Some(inner));
1923 }
1924
1925 #[test]
1926 fn no_scopes_behaves_like_a_flat_cycle() {
1927 let mut tree = WidgetTree::new();
1930 let a = indexed(&mut tree, 2);
1931 let b = indexed(&mut tree, 1);
1932 let c = tree.add(FillWidget::new().focusable());
1933 tree.layout(SizeProposal::exact(100.0, 50.0));
1934
1935 for expected in [b, a, c, b] {
1937 tab(&mut tree);
1938 assert_eq!(tree.focused(), Some(expected));
1939 }
1940 }
1941
1942 #[test]
1943 fn centered_modal_confines_tab_to_its_content() {
1944 let mut tree = WidgetTree::new();
1947 let outside1 = tree.add(FillWidget::new().focusable());
1948 let outside2 = tree.add(FillWidget::new().focusable());
1949 let m1 = tree.add(FillWidget::new().focusable());
1950 let m2 = tree.add(FillWidget::new().focusable());
1951 let content = tree.add(StackWidget::new().add_child(m1).add_child(m2));
1952 tree.layout(SizeProposal::exact(200.0, 100.0));
1953
1954 tree.show_overlay(crate::overlay::OverlayRequest {
1955 content_id: content,
1956 anchor: outside1,
1957 placement: crate::overlay::OverlayPlacement::Centered,
1958 dismiss: crate::overlay::DismissBehavior::Manual,
1959 layer: crate::overlay::OverlayLayer::InTree,
1960 parent_overlay: None,
1961 on_dismiss: None,
1962 fade_duration: None,
1963 });
1964
1965 for _ in 0..6 {
1966 tab(&mut tree);
1967 let f = tree.focused();
1968 assert!(
1969 f == Some(m1) || f == Some(m2),
1970 "modal must trap Tab inside its content, reached {f:?}"
1971 );
1972 }
1973 assert_ne!(tree.focused(), Some(outside1));
1974 assert_ne!(tree.focused(), Some(outside2));
1975 }
1976}
1977
1978#[cfg(test)]
1979mod tests_focus_out_dismissal {
1980 use super::*;
1987 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
1988 use crate::test_widgets::{FillWidget, StackWidget};
1989
1990 fn tab(tree: &mut WidgetTree) {
1991 tree.press_key(Key::Tab, Modifiers::NONE);
1992 }
1993
1994 fn show_anchored(
1996 tree: &mut WidgetTree,
1997 content: WidgetId,
1998 anchor: WidgetId,
1999 parent: Option<crate::overlay::OverlayId>,
2000 ) -> crate::overlay::OverlayId {
2001 tree.show_overlay(OverlayRequest {
2002 content_id: content,
2003 anchor,
2004 placement: OverlayPlacement::Below,
2005 dismiss: DismissBehavior::EscapeOrClickOutside,
2006 layer: OverlayLayer::InTree,
2007 parent_overlay: parent,
2008 on_dismiss: None,
2009 fade_duration: None,
2010 })
2011 }
2012
2013 #[test]
2014 fn tab_out_of_a_non_modal_overlay_dismisses_it() {
2015 let mut tree = WidgetTree::new();
2016 let anchor = tree.add(FillWidget::new().focusable());
2017 let after = tree.add(FillWidget::new().focusable());
2018 let inner = tree.add(FillWidget::new().focusable());
2019 let content = tree.add(StackWidget::new().add_child(inner));
2020 tree.layout(SizeProposal::exact(200.0, 100.0));
2021 show_anchored(&mut tree, content, anchor, None);
2022
2023 tree.focus(inner);
2024 tab(&mut tree);
2025
2026 assert_ne!(tree.focused(), Some(inner), "focus genuinely left");
2027 assert!(
2028 tree.active_overlays().is_empty(),
2029 "an overlay must not stay open over the focus ring that left it"
2030 );
2031 assert!(tree.focused() == Some(after) || tree.focused() == Some(anchor));
2032 }
2033
2034 #[test]
2036 fn a_centered_modal_is_never_dismissed_by_focus_moving() {
2037 let mut tree = WidgetTree::new();
2038 let outside = tree.add(FillWidget::new().focusable());
2039 let m1 = tree.add(FillWidget::new().focusable());
2040 let content = tree.add(StackWidget::new().add_child(m1));
2041 tree.layout(SizeProposal::exact(200.0, 100.0));
2042 tree.show_overlay(OverlayRequest {
2043 content_id: content,
2044 anchor: outside,
2045 placement: OverlayPlacement::Centered,
2046 dismiss: DismissBehavior::Manual,
2047 layer: OverlayLayer::InTree,
2048 parent_overlay: None,
2049 on_dismiss: None,
2050 fade_duration: None,
2051 });
2052
2053 tree.focus(m1);
2054 tree.focus(outside);
2057
2058 assert_eq!(
2059 tree.active_overlays().len(),
2060 1,
2061 "a modal is the one overlay that legitimately contains focus"
2062 );
2063 }
2064
2065 #[test]
2069 fn the_modal_scrim_survives_focus_moving_inside_the_modal() {
2070 let mut tree = WidgetTree::new();
2071 let opener = tree.add(FillWidget::new().focusable());
2072 let m1 = tree.add(FillWidget::new().focusable());
2073 let m2 = tree.add(FillWidget::new().focusable());
2074 let scrim = tree.add(FillWidget::new());
2075 let content = tree.add(StackWidget::new().add_child(m1).add_child(m2));
2076 tree.layout(SizeProposal::exact(200.0, 100.0));
2077
2078 tree.show_overlay(OverlayRequest {
2079 content_id: scrim,
2080 anchor: opener,
2081 placement: OverlayPlacement::FullViewport,
2082 dismiss: DismissBehavior::Manual,
2083 layer: OverlayLayer::InTree,
2084 parent_overlay: None,
2085 on_dismiss: None,
2086 fade_duration: None,
2087 });
2088 tree.show_overlay(OverlayRequest {
2089 content_id: content,
2090 anchor: opener,
2091 placement: OverlayPlacement::Centered,
2092 dismiss: DismissBehavior::Manual,
2093 layer: OverlayLayer::InTree,
2094 parent_overlay: None,
2095 on_dismiss: None,
2096 fade_duration: None,
2097 });
2098
2099 tree.focus(opener);
2100 tree.focus(m1);
2101 tab(&mut tree);
2102
2103 assert_eq!(
2104 tree.active_overlays().len(),
2105 2,
2106 "scrim and modal both stand while focus moves within the modal"
2107 );
2108 }
2109
2110 #[test]
2113 fn focus_moving_into_a_child_overlay_keeps_the_parent_open() {
2114 let mut tree = WidgetTree::new();
2115 let anchor = tree.add(FillWidget::new().focusable());
2116 let parent_item = tree.add(FillWidget::new().focusable());
2117 let parent_content = tree.add(StackWidget::new().add_child(parent_item));
2118 let child_item = tree.add(FillWidget::new().focusable());
2119 let child_content = tree.add(StackWidget::new().add_child(child_item));
2120 tree.layout(SizeProposal::exact(200.0, 100.0));
2121
2122 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2123 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2124
2125 tree.focus(parent_item);
2126 tree.focus(child_item);
2127
2128 assert_eq!(
2129 tree.active_overlays().len(),
2130 2,
2131 "opening a submenu is not leaving the menu that owns it"
2132 );
2133 }
2134
2135 #[test]
2138 fn focus_back_to_the_parent_overlay_closes_only_the_child() {
2139 let mut tree = WidgetTree::new();
2140 let anchor = tree.add(FillWidget::new().focusable());
2141 let parent_item = tree.add(FillWidget::new().focusable());
2142 let parent_content = tree.add(StackWidget::new().add_child(parent_item));
2143 let child_item = tree.add(FillWidget::new().focusable());
2144 let child_content = tree.add(StackWidget::new().add_child(child_item));
2145 tree.layout(SizeProposal::exact(200.0, 100.0));
2146
2147 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2148 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2149
2150 tree.focus(child_item);
2151 tree.focus(parent_item);
2152
2153 assert_eq!(
2154 tree.active_overlays(),
2155 vec![parent],
2156 "the submenu goes, the menu that owns it stays"
2157 );
2158 }
2159
2160 #[test]
2163 fn leaving_a_nested_cascade_closes_every_level() {
2164 let mut tree = WidgetTree::new();
2165 let anchor = tree.add(FillWidget::new().focusable());
2166 let away = tree.add(FillWidget::new().focusable());
2167 let parent_item = tree.add(FillWidget::new().focusable());
2168 let parent_content = tree.add(StackWidget::new().add_child(parent_item));
2169 let child_item = tree.add(FillWidget::new().focusable());
2170 let child_content = tree.add(StackWidget::new().add_child(child_item));
2171 tree.layout(SizeProposal::exact(200.0, 100.0));
2172
2173 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2174 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2175
2176 tree.focus(child_item);
2177 tree.focus(away);
2178
2179 assert!(
2180 tree.active_overlays().is_empty(),
2181 "one move out of the cascade must leave nothing behind"
2182 );
2183 }
2184
2185 #[test]
2195 fn a_dropdown_inside_a_modal_still_follows_focus_out() {
2196 let mut tree = WidgetTree::new();
2197 let opener = tree.add(FillWidget::new().focusable());
2198 let trigger = tree.add(FillWidget::new().focusable());
2199 let next = tree.add(FillWidget::new().focusable());
2200 let modal_content = tree.add(StackWidget::new().add_child(trigger).add_child(next));
2201 let panel = tree.add(StackWidget::new());
2202 tree.layout(SizeProposal::exact(200.0, 100.0));
2203
2204 tree.show_overlay(OverlayRequest {
2205 content_id: modal_content,
2206 anchor: opener,
2207 placement: OverlayPlacement::Centered,
2208 dismiss: DismissBehavior::Manual,
2209 layer: OverlayLayer::InTree,
2210 parent_overlay: None,
2211 on_dismiss: None,
2212 fade_duration: None,
2213 });
2214 show_anchored(&mut tree, panel, trigger, None);
2216
2217 tree.focus(trigger);
2218 assert_eq!(
2219 tree.active_overlays().len(),
2220 2,
2221 "precondition: modal + panel"
2222 );
2223
2224 tab(&mut tree);
2225
2226 assert_eq!(
2227 tree.focused(),
2228 Some(next),
2229 "Tab moves on within the modal, as it should"
2230 );
2231 assert_eq!(
2232 tree.active_overlays().len(),
2233 1,
2234 "the dropdown must go — only the modal hosting it stays"
2235 );
2236 }
2237
2238 #[test]
2246 fn a_viewport_placed_notification_ignores_focus_moving() {
2247 let mut tree = WidgetTree::new();
2248 let trigger = tree.add(FillWidget::new().focusable());
2249 let elsewhere = tree.add(FillWidget::new().focusable());
2250 let snack = tree.add(StackWidget::new());
2251 tree.layout(SizeProposal::exact(200.0, 100.0));
2252 tree.show_overlay(OverlayRequest {
2253 content_id: snack,
2254 anchor: trigger,
2255 placement: OverlayPlacement::BottomCenter,
2256 dismiss: DismissBehavior::Manual,
2257 layer: OverlayLayer::InTree,
2258 parent_overlay: None,
2259 on_dismiss: None,
2260 fade_duration: None,
2261 });
2262
2263 tree.focus(trigger);
2264 tab(&mut tree);
2265
2266 assert_eq!(
2267 tree.active_overlays().len(),
2268 1,
2269 "a snackbar outlives the keystroke that moved focus off its trigger"
2270 );
2271 assert_ne!(tree.focused(), Some(trigger), "and focus did move");
2272 assert_eq!(tree.focused(), Some(elsewhere));
2273 }
2274
2275 #[test]
2279 fn leaving_a_hosted_menu_spares_the_host() {
2280 let mut tree = WidgetTree::new();
2281 let opener = tree.add(FillWidget::new().focusable());
2282 let away = tree.add(FillWidget::new().focusable());
2283 let modal_item = tree.add(FillWidget::new().focusable());
2284 let modal_content = tree.add(StackWidget::new().add_child(modal_item));
2285 let menu_item = tree.add(FillWidget::new().focusable());
2286 let menu_content = tree.add(StackWidget::new().add_child(menu_item));
2287 tree.layout(SizeProposal::exact(200.0, 100.0));
2288
2289 let host = tree.show_overlay(OverlayRequest {
2291 content_id: modal_content,
2292 anchor: opener,
2293 placement: OverlayPlacement::Centered,
2294 dismiss: DismissBehavior::Manual,
2295 layer: OverlayLayer::InTree,
2296 parent_overlay: None,
2297 on_dismiss: None,
2298 fade_duration: None,
2299 });
2300 show_anchored(&mut tree, menu_content, modal_item, Some(host));
2301
2302 tree.focus(menu_item);
2303 tree.focus(away);
2304
2305 assert_eq!(
2306 tree.active_overlays(),
2307 vec![host],
2308 "the menu goes; the modal hosting it stays"
2309 );
2310 }
2311
2312 #[test]
2316 fn leaving_the_anchor_dismisses_a_panel_focus_never_entered() {
2317 let mut tree = WidgetTree::new();
2318 let trigger = tree.add(FillWidget::new().focusable());
2319 let after = tree.add(FillWidget::new().focusable());
2320 let content = tree.add(StackWidget::new());
2321 tree.layout(SizeProposal::exact(200.0, 100.0));
2322 show_anchored(&mut tree, content, trigger, None);
2323
2324 tree.focus(trigger);
2325 assert_eq!(tree.active_overlays().len(), 1, "precondition: panel is up");
2326
2327 tree.focus(after);
2328 assert!(
2329 tree.active_overlays().is_empty(),
2330 "leaving the trigger is leaving the dropdown it owns"
2331 );
2332 }
2333}