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 self.a11y_dirty = true;
68 self.update_focus_within_signals(previously_focused, Some(id));
69 self.update_view_focus_signals(previously_focused, Some(id));
70 self.dispatch_to_widget(id, &WidgetEvent::FocusGained { origin }, &mut *ops);
71 self.dismiss_overlays_left_by_focus(previously_focused, id, &mut *ops);
81 self.tooltip_focus_leave_outside(Some(id), &mut *ops);
88 self.tooltip_focus_enter(id);
89 if origin != crate::focus::FocusOrigin::Pointer {
96 self.scroll_focused_into_view(id, &mut *ops);
97 }
98 }
99
100 pub fn focus_with_origin(&mut self, id: WidgetId, origin: crate::focus::FocusOrigin) {
104 let mut noop = crate::window::NoopWindowOps;
105 self.focus_with_origin_ops(id, origin, &mut noop);
106 }
107
108 fn scroll_focused_into_view(
111 &mut self,
112 focused_id: WidgetId,
113 ops: &mut dyn crate::window::WindowOps,
114 ) {
115 let focused_bounds = self.arena.bounds(focused_id);
116 let reveal = self
121 .arena
122 .get(focused_id)
123 .and_then(|node| node.widget.focus_reveal_rect(focused_bounds))
124 .unwrap_or(focused_bounds);
125 self.scroll_rect_into_view(
126 focused_id,
127 reveal,
128 0.0,
129 crate::event::ScrollAlign::Minimal,
130 crate::event::ScrollMotion::Instant,
131 &mut *ops,
132 );
133 }
134
135 pub(super) fn scroll_rect_into_view(
175 &mut self,
176 from: WidgetId,
177 rect: Rect,
178 margin: f32,
179 align: crate::event::ScrollAlign,
180 motion: crate::event::ScrollMotion,
181 ops: &mut dyn crate::window::WindowOps,
182 ) {
183 let applied = std::sync::Arc::new(std::sync::Mutex::new(Point::ZERO));
189 let mut rect = rect;
190 let mut current = self.arena.parent(from);
191 let mut pending_align = align;
194 while let Some(ancestor_id) = current {
195 if let Some(node) = self.arena.get(ancestor_id)
196 && node.clips_children
197 {
198 let viewport = node.bounds;
199 let align =
200 std::mem::replace(&mut pending_align, crate::event::ScrollAlign::Minimal);
201 let needs_scroll = matches!(align, crate::event::ScrollAlign::Fraction(_))
204 || rect.y - margin < viewport.y
205 || rect.bottom() + margin > viewport.bottom()
206 || rect.x - margin < viewport.x
207 || rect.right() + margin > viewport.right();
208
209 if needs_scroll {
210 *applied.lock().unwrap() = Point::ZERO;
211 self.dispatch_to_widget(
212 ancestor_id,
213 &WidgetEvent::ScrollIntoView {
214 target_bounds: rect,
215 margin,
216 align,
217 motion,
218 applied_scroll: Some(applied.clone()),
219 },
220 &mut *ops,
221 );
222 let delta = *applied.lock().unwrap();
225 if delta != Point::ZERO {
226 rect =
227 Rect::new(rect.x - delta.x, rect.y - delta.y, rect.width, rect.height);
228 }
229 }
230 }
231 current = self.arena.parent(ancestor_id);
232 }
233 }
234
235 pub fn focus(&mut self, id: WidgetId) {
237 self.focus_with_origin(id, crate::focus::FocusOrigin::Programmatic);
238 }
239
240 pub fn focus_ops(&mut self, id: WidgetId, ops: &mut dyn crate::window::WindowOps) {
243 self.focus_with_origin_ops(id, crate::focus::FocusOrigin::Programmatic, ops);
244 }
245
246 pub fn focused(&self) -> Option<WidgetId> {
248 self.focused
249 }
250
251 pub fn ime_context_for_focused(&self) -> Option<crate::ime::ImeContext> {
257 self.focused.and_then(|id| self.arena.ime_context(id))
258 }
259
260 pub fn first_focusable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
265 if !self.arena.is_active(root) {
266 return None;
267 }
268 let mut entries = Vec::new();
269 self.collect_scope_entries(root, &mut entries);
270 sort_scope_entries(&mut entries);
271 let scope = ScopeNode {
272 policy: crate::focus::TraversalScopePolicy::Cycle,
273 entries,
274 };
275 enter_scope_edge(&scope, false)
276 }
277
278 pub fn is_active(&self, id: WidgetId) -> bool {
284 self.arena.is_active(id)
285 }
286
287 pub fn widget_initial_focus_hint(&self, id: WidgetId) -> Option<WidgetId> {
303 if !self.arena.is_active(id) {
304 return None;
305 }
306 if let Some(node) = self.arena.get(id) {
307 if let Some(target) = node.widget.initial_focus_hint()
308 && self.arena.is_active(target)
309 && self.is_descendant_of(target, id)
310 {
311 return Some(target);
312 }
313 for &child in &node.children {
314 if let Some(found) = self.widget_initial_focus_hint(child) {
315 return Some(found);
316 }
317 }
318 }
319 None
320 }
321
322 pub fn focus_origin(&self) -> Option<crate::focus::FocusOrigin> {
324 self.focus_origin
325 }
326
327 pub fn focus_visible_signal(&self) -> crate::signal::Signal<bool> {
331 self.focus_visible.clone()
332 }
333
334 pub(super) fn cycle_focus(&mut self, reverse: bool, ops: &mut dyn crate::window::WindowOps) {
343 let mut entries = Vec::new();
344 if let Some(modal_overlay) = self.overlay_manager.topmost_centered() {
345 let content_id = modal_overlay.content_id;
346 self.collect_scope_entries(content_id, &mut entries);
347 } else {
348 let roots = self.arena.roots();
349 for root in roots {
350 if self.tooltip_content_root(root).is_some() {
360 continue;
361 }
362 self.collect_scope_entries(root, &mut entries);
363 }
364 }
365 sort_scope_entries(&mut entries);
366 self.splice_sticky_tooltips_after_anchors(&mut entries);
367 let root_scope = ScopeNode {
368 policy: crate::focus::TraversalScopePolicy::Cycle,
369 entries,
370 };
371
372 if let StepResult::Found(next_id) = navigate_scope(&root_scope, self.focused, reverse, true)
373 {
374 self.focus_with_origin_ops(next_id, crate::focus::FocusOrigin::Keyboard, &mut *ops);
375 }
376 }
379
380 fn tooltip_content_root(&self, id: WidgetId) -> Option<usize> {
382 self.tooltips.iter().position(|e| e.content_id == id)
383 }
384
385 fn splice_sticky_tooltips_after_anchors(&self, entries: &mut Vec<ScopeEntry>) {
403 let sticky: Vec<(WidgetId, WidgetId)> = self
404 .tooltips
405 .iter()
406 .filter(|e| e.is_sticky && e.overlay_id.is_some())
407 .map(|e| (e.anchor_id, e.content_id))
408 .collect();
409
410 for (anchor_id, content_id) in sticky {
411 let mut panel = Vec::new();
412 self.collect_scope_entries(content_id, &mut panel);
413 if panel.is_empty() {
414 continue;
415 }
416 sort_scope_entries(&mut panel);
417
418 let stop = self
423 .find_focusable_at_or_above(anchor_id)
424 .unwrap_or(anchor_id);
425 let at = entries
426 .iter()
427 .position(|entry| scope_entry_contains(entry, stop))
428 .map_or(entries.len(), |i| i + 1);
433
434 for (offset, item) in panel.into_iter().enumerate() {
435 entries.insert(at + offset, item);
436 }
437 }
438 }
439
440 fn tab_stop_effective(&self, id: WidgetId) -> bool {
448 let mut current = Some(id);
449 while let Some(cur) = current {
450 let Some(node) = self.arena.get(cur) else {
451 break;
452 };
453 if let Some(prop) = node.tab_stop.as_ref() {
454 return prop.get();
455 }
456 current = node.parent;
457 }
458 true
459 }
460
461 fn is_node_focusable(&self, node: &crate::arena::WidgetNode) -> bool {
463 node.node_focusable.unwrap_or(false)
464 }
465
466 pub(super) fn find_focusable_at_or_above(&self, id: WidgetId) -> Option<WidgetId> {
468 let mut current = Some(id);
469 while let Some(current_id) = current {
470 if let Some(node) = self.arena.get(current_id)
471 && self.is_node_focusable(node)
472 {
473 return Some(current_id);
474 }
475 current = self.arena.parent(current_id);
476 }
477 None
478 }
479
480 fn collect_scope_entries(&self, id: WidgetId, out: &mut Vec<ScopeEntry>) {
496 if !self.arena.is_active(id) {
497 return;
498 }
499 let Some(node) = self.arena.get(id) else {
500 return;
501 };
502 if node
503 .enabled_state
504 .as_ref()
505 .map(|s| !s.get())
506 .unwrap_or(false)
507 {
508 return;
509 }
510
511 if let Some(policy) = node.node_traversal_scope {
514 let mut child_entries = Vec::new();
515 for &child in &node.children {
516 self.collect_scope_entries(child, &mut child_entries);
517 }
518 sort_scope_entries(&mut child_entries);
519 out.push(ScopeEntry {
520 tab_index: node.node_tab_index,
521 kind: ScopeEntryKind::Scope(ScopeNode {
522 policy,
523 entries: child_entries,
524 }),
525 });
526 return;
527 }
528
529 if self.is_node_focusable(node) && self.tab_stop_effective(id) {
531 out.push(ScopeEntry {
532 tab_index: node.node_tab_index,
533 kind: ScopeEntryKind::Focusable(id),
534 });
535 }
536 for &child in &node.children {
537 self.collect_scope_entries(child, out);
538 }
539 }
540
541 pub(super) fn strict_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
546 let mut chain = Vec::new();
547 if let Some(start) = id {
548 let mut current = self.arena.parent(start);
549 while let Some(parent) = current {
550 chain.push(parent);
551 current = self.arena.parent(parent);
552 }
553 }
554 chain
555 }
556
557 pub(crate) fn update_focus_within_signals(
562 &mut self,
563 old: Option<WidgetId>,
564 new: Option<WidgetId>,
565 ) {
566 let old_chain = self.strict_ancestors_of(old);
567 let new_chain = self.strict_ancestors_of(new);
568 for &id in &old_chain {
570 if !new_chain.contains(&id)
571 && let Some(node) = self.arena.get(id)
572 && let Some(sig) = node.focus_within_signal.clone()
573 {
574 sig.set(false);
575 }
576 }
577 for &id in &new_chain {
579 if !old_chain.contains(&id)
580 && let Some(node) = self.arena.get(id)
581 && let Some(sig) = node.focus_within_signal.clone()
582 {
583 sig.set(true);
584 }
585 }
586 }
587
588 pub(super) fn inclusive_ancestors_of(&self, id: Option<WidgetId>) -> Vec<WidgetId> {
591 let mut chain = Vec::new();
592 if let Some(start) = id {
593 chain.push(start);
594 chain.extend(self.strict_ancestors_of(Some(start)));
595 }
596 chain
597 }
598
599 pub(crate) fn update_view_focus_signals(
604 &mut self,
605 old: Option<WidgetId>,
606 new: Option<WidgetId>,
607 ) {
608 let old_chain = self.inclusive_ancestors_of(old);
609 let new_chain = self.inclusive_ancestors_of(new);
610 for &id in &old_chain {
611 if !new_chain.contains(&id)
612 && let Some(node) = self.arena.get(id)
613 && let Some(sig) = node.view_focus_signal.clone()
614 {
615 sig.set(false);
616 }
617 }
618 for &id in &new_chain {
619 if !old_chain.contains(&id)
620 && let Some(node) = self.arena.get(id)
621 && let Some(sig) = node.view_focus_signal.clone()
622 {
623 sig.set(true);
624 }
625 }
626 }
627
628 pub(crate) fn view_focus_signal_for(
631 &mut self,
632 node_id: WidgetId,
633 ) -> crate::signal::Signal<bool> {
634 if let Some(node) = self.arena.get(node_id)
635 && let Some(sig) = node.view_focus_signal.clone()
636 {
637 return sig;
638 }
639 let active = self.inclusive_ancestors_of(self.focused).contains(&node_id);
640 let sig = crate::signal::Signal::new(active);
641 if let Some(node) = self.arena.get_mut(node_id) {
642 node.view_focus_signal = Some(sig.clone());
643 }
644 sig
645 }
646
647 pub fn view_focus_active_for(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
653 match self.find_focusable_at_or_above(node_id) {
654 Some(scope) => self.view_focus_signal_for(scope),
655 None => crate::signal::Signal::new(true),
656 }
657 }
658
659 pub fn begin_view_focus(&mut self, node_id: WidgetId) -> crate::signal::Signal<bool> {
664 let sig = self.view_focus_signal_for(node_id);
665 self.view_focus_stack.push(sig.clone());
666 sig
667 }
668
669 pub fn end_view_focus(&mut self) {
671 self.view_focus_stack.pop();
672 }
673
674 pub fn current_view_focus(&self) -> Option<crate::signal::Signal<bool>> {
676 self.view_focus_stack.last().cloned()
677 }
678
679 pub(crate) fn set_hovered(&mut self, value: Option<WidgetId>) {
686 self.hovered = value;
687 if self.hovered_signal.get() != value {
688 self.hovered_signal.set(value);
689 }
690 }
691
692 pub(crate) fn set_focused(&mut self, value: Option<WidgetId>) {
699 self.focused = value;
700 if self.focused_signal.get() != value {
701 self.focused_signal.set(value);
702 }
703 }
704
705 pub(crate) fn update_hover_within_signals(
708 &mut self,
709 old: Option<WidgetId>,
710 new: Option<WidgetId>,
711 ) {
712 let old_chain = self.strict_ancestors_of(old);
713 let new_chain = self.strict_ancestors_of(new);
714 for &id in &old_chain {
715 if !new_chain.contains(&id)
716 && let Some(node) = self.arena.get(id)
717 && let Some(sig) = node.hover_within_signal.clone()
718 {
719 sig.set(false);
720 }
721 }
722 for &id in &new_chain {
723 if !old_chain.contains(&id)
724 && let Some(node) = self.arena.get(id)
725 && let Some(sig) = node.hover_within_signal.clone()
726 {
727 sig.set(true);
728 }
729 }
730 }
731}
732
733struct ScopeEntry {
745 tab_index: Option<i32>,
746 kind: ScopeEntryKind,
747}
748
749enum ScopeEntryKind {
750 Focusable(WidgetId),
751 Scope(ScopeNode),
752}
753
754struct ScopeNode {
756 policy: crate::focus::TraversalScopePolicy,
757 entries: Vec<ScopeEntry>,
758}
759
760enum StepResult {
762 Found(WidgetId),
764 Escaped,
768}
769
770fn scope_wraps(policy: crate::focus::TraversalScopePolicy, is_root: bool) -> bool {
773 is_root || matches!(policy, crate::focus::TraversalScopePolicy::Cycle)
774}
775
776fn sort_scope_entries(entries: &mut [ScopeEntry]) {
779 entries.sort_by(|a, b| match (a.tab_index, b.tab_index) {
780 (Some(ia), Some(ib)) => ia.cmp(&ib),
781 (Some(_), None) => std::cmp::Ordering::Less,
782 (None, Some(_)) => std::cmp::Ordering::Greater,
783 (None, None) => std::cmp::Ordering::Equal,
784 });
785}
786
787fn scope_entry_contains(entry: &ScopeEntry, f: WidgetId) -> bool {
789 match &entry.kind {
790 ScopeEntryKind::Focusable(id) => *id == f,
791 ScopeEntryKind::Scope(child) => child.entries.iter().any(|e| scope_entry_contains(e, f)),
792 }
793}
794
795fn enter_scope_edge(scope: &ScopeNode, reverse: bool) -> Option<WidgetId> {
799 let n = scope.entries.len();
800 for k in 0..n {
801 let idx = if reverse { n - 1 - k } else { k };
802 match &scope.entries[idx].kind {
803 ScopeEntryKind::Focusable(id) => return Some(*id),
804 ScopeEntryKind::Scope(child) => {
805 if let Some(id) = enter_scope_edge(child, reverse) {
806 return Some(id);
807 }
808 }
809 }
810 }
811 None
812}
813
814fn navigate_scope(
818 scope: &ScopeNode,
819 focused: Option<WidgetId>,
820 reverse: bool,
821 is_root: bool,
822) -> StepResult {
823 if scope.entries.is_empty() {
824 return StepResult::Escaped;
825 }
826
827 let cur = focused.and_then(|f| {
828 scope
829 .entries
830 .iter()
831 .position(|e| scope_entry_contains(e, f))
832 });
833
834 let from = match cur {
835 None => {
836 return match enter_scope_edge(scope, reverse) {
839 Some(id) => StepResult::Found(id),
840 None => StepResult::Escaped,
841 };
842 }
843 Some(i) => i,
844 };
845
846 if let ScopeEntryKind::Scope(child) = &scope.entries[from].kind
849 && let StepResult::Found(id) = navigate_scope(child, focused, reverse, false)
850 {
851 return StepResult::Found(id);
852 }
853
854 step_to_sibling(scope, from, reverse, is_root)
855}
856
857fn step_to_sibling(scope: &ScopeNode, from: usize, reverse: bool, is_root: bool) -> StepResult {
861 let n = scope.entries.len();
862 let mut idx = from;
863 for _ in 0..n {
864 let next = if reverse {
865 if idx == 0 {
866 if scope_wraps(scope.policy, is_root) {
867 n - 1
868 } else {
869 return StepResult::Escaped;
870 }
871 } else {
872 idx - 1
873 }
874 } else if idx + 1 >= n {
875 if scope_wraps(scope.policy, is_root) {
876 0
877 } else {
878 return StepResult::Escaped;
879 }
880 } else {
881 idx + 1
882 };
883
884 match &scope.entries[next].kind {
885 ScopeEntryKind::Focusable(id) => return StepResult::Found(*id),
886 ScopeEntryKind::Scope(child) => {
887 if let Some(id) = enter_scope_edge(child, reverse) {
888 return StepResult::Found(id);
889 }
890 idx = next;
892 }
893 }
894 }
895 StepResult::Escaped
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901 use crate::test_widgets::FillWidget;
902 use crate::widget_builder::WidgetBuilder;
903
904 #[test]
913 fn a_reveal_walks_the_ancestors_of_whoever_owns_the_rect() {
914 use crate::event::{EventResponse, ScrollAlign, ScrollMotion};
915 use std::cell::Cell;
916 use std::rc::Rc;
917
918 let asked = Rc::new(Cell::new(0usize));
919 let mut tree = WidgetTree::new();
920 let viewport = {
922 let asked = asked.clone();
923 tree.add(
924 FillWidget::new()
927 .on_scroll(move |event, _ctx| {
928 if matches!(event, WidgetEvent::ScrollIntoView { .. }) {
929 asked.set(asked.get() + 1);
930 }
931 EventResponse::Ignored
932 })
933 .clips_children(true),
934 )
935 };
936 let inside = tree.add_child(viewport, FillWidget::new());
937 let button = tree.add(FillWidget::new());
939 tree.layout(SizeProposal::exact(400.0, 300.0));
940
941 let target = Rect::new(0.0, 5_000.0, 10.0, 10.0);
943 let mut ops = crate::window::NoopWindowOps;
944
945 tree.scroll_rect_into_view(
946 button,
947 target,
948 0.0,
949 ScrollAlign::Minimal,
950 ScrollMotion::Instant,
951 &mut ops,
952 );
953 assert_eq!(
954 asked.get(),
955 0,
956 "walking from the asker never reaches a viewport it is not inside"
957 );
958
959 tree.scroll_rect_into_view(
960 inside,
961 target,
962 0.0,
963 ScrollAlign::Minimal,
964 ScrollMotion::Instant,
965 &mut ops,
966 );
967 assert_eq!(
968 asked.get(),
969 1,
970 "walking from the rect's owner reaches the viewport enclosing it"
971 );
972 }
973
974 #[test]
975 fn focus_widget() {
976 let mut tree = WidgetTree::new();
977 let widget = tree.add(FillWidget::new());
978 tree.layout(SizeProposal::exact(100.0, 50.0));
979 tree.focus(widget);
980 assert_eq!(tree.focused(), Some(widget));
981 }
982
983 #[test]
984 fn focus_change() {
985 let mut tree = WidgetTree::new();
986 let a = tree.add(FillWidget::new());
987 let b = tree.add(FillWidget::new());
988 tree.layout(SizeProposal::exact(100.0, 50.0));
989 tree.focus(a);
990 assert_eq!(tree.focused(), Some(a));
991 tree.focus(b);
992 assert_eq!(tree.focused(), Some(b));
993 }
994
995 #[derive(Debug)]
999 struct RebuildingRows {
1000 epoch: crate::signal::Signal<u64>,
1001 rows: Vec<WidgetId>,
1002 }
1003
1004 impl Widget for RebuildingRows {
1005 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1006 let sid = ctx.self_id();
1007 let reg = ctx.binding_registry();
1008 self.epoch
1009 .bind_to(sid, reg, crate::binding::BindingLevel::Rebuild);
1010 self.rows = (0..3)
1011 .map(|_| ctx.add(FillWidget::new().focusable()))
1012 .collect();
1013 self.rows.clone()
1014 }
1015
1016 fn layout_response(
1017 &self,
1018 proposal: SizeProposal,
1019 _ctx: &LayoutContext,
1020 ) -> crate::widget::LayoutResponse {
1021 proposal.resolve(0.0, 0.0).into()
1022 }
1023
1024 fn children(&self) -> Vec<WidgetId> {
1025 self.rows.clone()
1026 }
1027 }
1028
1029 #[test]
1039 fn a_rebuild_keeps_focus_inside_the_subtree_that_had_it() {
1040 let mut tree = WidgetTree::new();
1041 let epoch = crate::signal::Signal::new(0u64);
1042 let root = tree.add(RebuildingRows {
1043 epoch: epoch.clone(),
1044 rows: Vec::new(),
1045 });
1046 tree.layout(SizeProposal::exact(100.0, 60.0));
1047
1048 let first_row = tree
1049 .first_focusable_descendant(root)
1050 .expect("the rows are focusable");
1051 tree.focus(first_row);
1052 assert_eq!(tree.focused(), Some(first_row));
1053
1054 epoch.set(1);
1056 tree.layout(SizeProposal::exact(100.0, 60.0));
1057
1058 let focused = tree
1059 .focused()
1060 .expect("a rebuild must not drop focus out of the rebuilt subtree");
1061 assert_ne!(
1062 focused, first_row,
1063 "the old row is dead — focus must have moved to a freshly built one"
1064 );
1065 assert!(
1066 tree.is_descendant_of(focused, root),
1067 "focus must land back inside the rebuilt subtree"
1068 );
1069 assert!(tree.is_active(focused), "the focused node must be live");
1070 }
1071
1072 #[test]
1077 fn a_rebuild_elsewhere_does_not_steal_focus() {
1078 let mut tree = WidgetTree::new();
1079 let epoch = crate::signal::Signal::new(0u64);
1080 let outsider = tree.add(FillWidget::new().focusable());
1081 let _rows = tree.add(RebuildingRows {
1082 epoch: epoch.clone(),
1083 rows: Vec::new(),
1084 });
1085 tree.layout(SizeProposal::exact(100.0, 60.0));
1086
1087 tree.focus(outsider);
1088 assert_eq!(tree.focused(), Some(outsider));
1089
1090 epoch.set(1);
1091 tree.layout(SizeProposal::exact(100.0, 60.0));
1092
1093 assert_eq!(
1094 tree.focused(),
1095 Some(outsider),
1096 "a rebuild that never held focus must not pull focus into itself"
1097 );
1098 }
1099
1100 #[test]
1101 fn first_focusable_descendant_prefers_first_focusable_child() {
1102 let mut tree = WidgetTree::new();
1103 let a = tree.add(FillWidget::new().focusable());
1104 let _not_focusable = tree.add(FillWidget::new());
1105 let b = tree.add(FillWidget::new().focusable());
1106 let root = tree.add(
1107 crate::test_widgets::StackWidget::new()
1108 .add_child(a)
1109 .add_child(b),
1110 );
1111 tree.layout(SizeProposal::exact(100.0, 50.0));
1112
1113 assert_eq!(tree.first_focusable_descendant(root), Some(a));
1114 }
1115
1116 #[test]
1117 fn tab_cycles_through_focusable_widgets() {
1118 let mut tree = WidgetTree::new();
1119 let a = tree.add(FillWidget::new().focusable());
1120 let b = tree.add(FillWidget::new().focusable());
1121 let c = tree.add(FillWidget::new().focusable());
1122 tree.layout(SizeProposal::exact(100.0, 50.0));
1123
1124 assert_eq!(tree.focused(), None);
1125
1126 tree.press_key(Key::Tab, Modifiers::NONE);
1127 assert_eq!(tree.focused(), Some(a));
1128
1129 tree.press_key(Key::Tab, Modifiers::NONE);
1130 assert_eq!(tree.focused(), Some(b));
1131
1132 tree.press_key(Key::Tab, Modifiers::NONE);
1133 assert_eq!(tree.focused(), Some(c));
1134
1135 tree.press_key(Key::Tab, Modifiers::NONE);
1136 assert_eq!(tree.focused(), Some(a));
1137 }
1138
1139 #[test]
1140 fn tab_stop_on_composite_excludes_focusable_descendant() {
1141 let mut tree = WidgetTree::new();
1146 let leaf = tree.add(FillWidget::new().focusable());
1147 let composite = tree.add(crate::test_widgets::StackWidget::new().add_child(leaf));
1148 let other = tree.add(FillWidget::new().focusable());
1149 tree.layout(SizeProposal::exact(100.0, 50.0));
1150
1151 tree.set_tab_stop(composite, false);
1152
1153 tree.press_key(Key::Tab, Modifiers::NONE);
1154 assert_eq!(
1155 tree.focused(),
1156 Some(other),
1157 "Tab must skip the suppressed composite's inner leaf"
1158 );
1159 tree.press_key(Key::Tab, Modifiers::NONE);
1160 assert_eq!(
1161 tree.focused(),
1162 Some(other),
1163 "only the un-suppressed control participates in Tab"
1164 );
1165
1166 tree.set_tab_stop(composite, true);
1168 tree.set_focused(None);
1169 tree.press_key(Key::Tab, Modifiers::NONE);
1170 let first = tree.focused();
1171 tree.press_key(Key::Tab, Modifiers::NONE);
1172 let second = tree.focused();
1173 assert_ne!(
1174 first, second,
1175 "with the composite re-enabled, Tab visits both controls"
1176 );
1177 }
1178
1179 #[test]
1180 fn shift_tab_cycles_backwards() {
1181 let mut tree = WidgetTree::new();
1182 let a = tree.add(FillWidget::new().focusable());
1183 let b = tree.add(FillWidget::new().focusable());
1184 let c = tree.add(FillWidget::new().focusable());
1185 tree.layout(SizeProposal::exact(100.0, 50.0));
1186
1187 tree.press_key(Key::Tab, Modifiers::NONE);
1188 assert_eq!(tree.focused(), Some(a));
1189
1190 tree.press_key(Key::Tab, Modifiers::SHIFT);
1191 assert_eq!(tree.focused(), Some(c));
1192
1193 tree.press_key(Key::Tab, Modifiers::SHIFT);
1194 assert_eq!(tree.focused(), Some(b));
1195 }
1196
1197 #[test]
1198 fn tab_skips_non_focusable_widgets() {
1199 let mut tree = WidgetTree::new();
1200 let _not_focusable = tree.add(FillWidget::new());
1201 let a = tree.add(FillWidget::new().focusable());
1202 let _also_not = tree.add(FillWidget::new());
1203 let b = tree.add(FillWidget::new().focusable());
1204 tree.layout(SizeProposal::exact(100.0, 50.0));
1205
1206 tree.press_key(Key::Tab, Modifiers::NONE);
1207 assert_eq!(tree.focused(), Some(a));
1208
1209 tree.press_key(Key::Tab, Modifiers::NONE);
1210 assert_eq!(tree.focused(), Some(b));
1211 }
1212
1213 #[test]
1214 fn tab_focus_has_keyboard_origin() {
1215 let mut tree = WidgetTree::new();
1216 tree.add(FillWidget::new().focusable());
1217 tree.layout(SizeProposal::exact(100.0, 50.0));
1218
1219 tree.press_key(Key::Tab, Modifiers::NONE);
1220 assert_eq!(
1221 tree.focus_origin(),
1222 Some(crate::focus::FocusOrigin::Keyboard)
1223 );
1224 }
1225
1226 #[test]
1227 fn tab_skips_focusable_inside_disabled_ancestor() {
1228 use crate::signal::Signal;
1229 use crate::test_widgets::StackWidget;
1230
1231 let mut tree = WidgetTree::new();
1232 let a = tree.add(FillWidget::new().focusable());
1233 let inner = tree.add(FillWidget::new().focusable());
1234 let disabled_container = tree.add(StackWidget::new().add_child(inner));
1235 let c = tree.add(FillWidget::new().focusable());
1236 tree.enabled_when(disabled_container, Signal::new(false));
1237 tree.layout(SizeProposal::exact(200.0, 100.0));
1238
1239 tree.press_key(Key::Tab, Modifiers::NONE);
1240 assert_eq!(tree.focused(), Some(a));
1241
1242 tree.press_key(Key::Tab, Modifiers::NONE);
1243 assert_eq!(
1244 tree.focused(),
1245 Some(c),
1246 "tab should skip the focusable widget nested inside the disabled container"
1247 );
1248 }
1249
1250 #[test]
1251 fn dormant_widget_not_in_focus_cycle() {
1252 let mut tree = WidgetTree::new();
1253 let a = tree.add(FillWidget::new().focusable());
1254 let b = tree.add(FillWidget::new().focusable());
1255 let c = tree.add(FillWidget::new().focusable());
1256 tree.layout(SizeProposal::exact(200.0, 100.0));
1257
1258 tree.focus(a);
1259 tree.set_dormant(b);
1260
1261 tree.press_key(Key::Tab, Modifiers::NONE);
1262 assert_eq!(tree.focused(), Some(c));
1263 }
1264
1265 #[test]
1270 fn revalidate_delivers_focus_lost_when_focused_widget_goes_dormant() {
1271 use std::cell::Cell;
1272 use std::rc::Rc;
1273
1274 let lost = Rc::new(Cell::new(0_u32));
1275 let gained = Rc::new(Cell::new(0_u32));
1276 let lost_c = lost.clone();
1277 let gained_c = gained.clone();
1278
1279 let mut tree = WidgetTree::new();
1280 let editor = tree.add(
1281 FillWidget::new()
1282 .focusable()
1283 .on_focus(move |is_gained, _ctx| {
1284 if is_gained {
1285 gained_c.set(gained_c.get() + 1);
1286 } else {
1287 lost_c.set(lost_c.get() + 1);
1288 }
1289 }),
1290 );
1291 let _other = tree.add(FillWidget::new().focusable());
1292 tree.layout(SizeProposal::exact(200.0, 100.0));
1293
1294 tree.focus(editor);
1295 assert_eq!(tree.focused(), Some(editor));
1296 assert_eq!(gained.get(), 1, "focus() delivers FocusGained");
1297 assert_eq!(lost.get(), 0);
1298
1299 tree.set_dormant(editor);
1301 let mut noop = crate::window::NoopWindowOps;
1303 tree.revalidate_interaction_state(&mut noop);
1304
1305 assert_eq!(
1306 tree.focused(),
1307 None,
1308 "tree focus must clear when the target is dormant"
1309 );
1310 assert_eq!(
1311 lost.get(),
1312 1,
1313 "FocusLost must reach the dormant widget so it can clear has_focus / caret blink"
1314 );
1315 }
1316
1317 #[test]
1318 fn tab_cycles_focus_in_tree_order() {
1319 let mut tree = WidgetTree::new();
1320 let a = tree.add(FillWidget::new().focusable());
1321 let b = tree.add(FillWidget::new().focusable());
1322 tree.layout(SizeProposal::exact(200.0, 80.0));
1323
1324 tree.focus(a);
1325 assert_eq!(tree.focused(), Some(a));
1326
1327 tree.press_key(Key::Tab, Modifiers::NONE);
1328 assert_eq!(tree.focused(), Some(b));
1329
1330 tree.press_key(Key::Tab, Modifiers::SHIFT);
1331 assert_eq!(tree.focused(), Some(a));
1332 }
1333
1334 #[test]
1335 fn focus_survives_theme_switch() {
1336 let mut tree = WidgetTree::new();
1337 let a = tree.add(FillWidget::new().focusable());
1338 let _b = tree.add(FillWidget::new().focusable());
1339 tree.layout(SizeProposal::exact(200.0, 80.0));
1340
1341 tree.focus(a);
1342 assert_eq!(tree.focused(), Some(a));
1343
1344 tree.set_theme(crate::presets::intui::dark());
1345 tree.layout(SizeProposal::exact(200.0, 80.0));
1346
1347 assert_eq!(
1348 tree.focused(),
1349 Some(a),
1350 "theme switch must not clobber focus"
1351 );
1352 }
1353
1354 #[test]
1355 fn focus_survives_locale_switch() {
1356 let mut tree = WidgetTree::new();
1357 let a = tree.add(FillWidget::new().focusable());
1358 tree.layout(SizeProposal::exact(200.0, 80.0));
1359
1360 tree.focus(a);
1361 tree.set_locale("fr-FR".to_string());
1362 tree.layout(SizeProposal::exact(200.0, 80.0));
1363
1364 assert_eq!(
1365 tree.focused(),
1366 Some(a),
1367 "locale switch must not clobber focus"
1368 );
1369 }
1370
1371 #[test]
1374 fn focus_within_flips_when_descendant_takes_focus() {
1375 use crate::signal::Signal;
1376 use crate::test_widgets::StackWidget;
1377 use crate::widget_builder::WidgetBuilder;
1378
1379 let halo = Signal::new(false);
1380
1381 let mut tree = WidgetTree::new();
1382 let leaf = tree.add(FillWidget::new().focusable());
1383 let mid = tree.add(StackWidget::new().add_child(leaf));
1384 let _outer = tree.add(StackWidget::new().add_child(mid).focus_within(halo.clone()));
1385
1386 tree.layout(SizeProposal::exact(100.0, 50.0));
1387 assert!(!halo.get(), "no focus yet → signal is false");
1388
1389 tree.focus(leaf);
1390 assert!(halo.get(), "leaf now focused, outer is its strict ancestor");
1391 }
1392
1393 #[test]
1394 fn focus_within_strict_ancestors_only() {
1395 use crate::signal::Signal;
1398 use crate::widget_builder::WidgetBuilder;
1399
1400 let halo = Signal::new(false);
1401
1402 let mut tree = WidgetTree::new();
1403 let widget = tree.add(FillWidget::new().focusable().focus_within(halo.clone()));
1404 tree.layout(SizeProposal::exact(100.0, 50.0));
1405 tree.focus(widget);
1406
1407 assert!(
1408 !halo.get(),
1409 "focusing the widget itself must not set its own focus_within"
1410 );
1411 }
1412
1413 #[test]
1414 fn view_focus_active_is_inclusive_for_the_view_itself() {
1415 use crate::test_widgets::StackWidget;
1421 use crate::widget_builder::WidgetBuilder;
1422
1423 let mut tree = WidgetTree::new();
1424 let item = tree.add(FillWidget::new()); let view = tree.add(StackWidget::new().add_child(item).focusable(true));
1426 let outside = tree.add(FillWidget::new().focusable());
1427 tree.layout(SizeProposal::exact(100.0, 50.0));
1428
1429 let active = tree.view_focus_active_for(item);
1430 assert!(!active.get(), "no focus yet → scope inactive");
1431
1432 tree.focus(view);
1433 assert!(
1434 active.get(),
1435 "view focused directly → scope active (focus_within would be false here)"
1436 );
1437
1438 tree.focus(outside);
1439 assert!(
1440 !active.get(),
1441 "focus moved outside the view → scope inactive"
1442 );
1443 }
1444
1445 #[test]
1446 fn begin_view_focus_for_keys_on_the_view_root_not_the_building_pane() {
1447 use crate::test_widgets::StackWidget;
1455 use crate::widget_builder::WidgetBuilder;
1456
1457 let mut tree = WidgetTree::new();
1458 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));
1461 tree.layout(SizeProposal::exact(100.0, 50.0));
1462
1463 let keyed_on_root = tree.begin_view_focus(root);
1465 tree.end_view_focus();
1466 let keyed_on_pane = tree.view_focus_signal_for(pane);
1468
1469 assert!(!keyed_on_root.get(), "no focus yet → inactive");
1470 tree.focus(root);
1471 assert!(keyed_on_root.get(), "view root focused → row scope active");
1472 assert!(
1473 !keyed_on_pane.get(),
1474 "pane-keyed scope stays false: the focused root is the pane's ancestor, not its descendant",
1475 );
1476 }
1477
1478 #[test]
1479 fn focus_visible_tracks_input_modality() {
1480 use crate::event::{Key, Modifiers};
1483 let mut tree = WidgetTree::new();
1484 let w = tree.add(FillWidget::new().focusable());
1485 tree.layout(SizeProposal::exact(100.0, 50.0));
1486 let vis = tree.focus_visible_signal();
1487 assert!(!vis.get(), "starts not focus-visible");
1488
1489 tree.press_key(Key::Tab, Modifiers::NONE);
1490 assert!(vis.get(), "keyboard input turns focus-visible ON");
1491
1492 tree.click(w);
1493 assert!(!vis.get(), "pointer input turns focus-visible OFF");
1494
1495 tree.press_key(Key::ArrowDown, Modifiers::NONE);
1496 assert!(vis.get(), "keyboard input turns it back ON");
1497 }
1498
1499 #[test]
1500 fn view_focus_active_without_focusable_ancestor_is_constant_true() {
1501 let mut tree = WidgetTree::new();
1504 let item = tree.add(FillWidget::new());
1505 tree.layout(SizeProposal::exact(100.0, 50.0));
1506 assert!(tree.view_focus_active_for(item).get());
1507 }
1508
1509 #[test]
1510 fn focus_within_diff_across_siblings() {
1511 use crate::signal::Signal;
1514 use crate::test_widgets::StackWidget;
1515 use crate::widget_builder::WidgetBuilder;
1516
1517 let sig_a = Signal::new(false);
1518 let sig_b = Signal::new(false);
1519
1520 let mut tree = WidgetTree::new();
1521 let leaf_a = tree.add(FillWidget::new().focusable());
1522 let leaf_b = tree.add(FillWidget::new().focusable());
1523 let mid_a = tree.add(
1524 StackWidget::new()
1525 .add_child(leaf_a)
1526 .focus_within(sig_a.clone()),
1527 );
1528 let mid_b = tree.add(
1529 StackWidget::new()
1530 .add_child(leaf_b)
1531 .focus_within(sig_b.clone()),
1532 );
1533 let _root = tree.add(StackWidget::new().add_child(mid_a).add_child(mid_b));
1534
1535 tree.layout(SizeProposal::exact(100.0, 50.0));
1536 tree.focus(leaf_a);
1537 assert!(sig_a.get(), "sig_a true after focusing leaf_a");
1538 assert!(!sig_b.get(), "sig_b false: leaf_a is not its descendant");
1539
1540 tree.focus(leaf_b);
1541 assert!(!sig_a.get(), "sig_a flipped to false on focus move");
1542 assert!(sig_b.get(), "sig_b flipped to true on focus move");
1543 }
1544
1545 #[test]
1546 fn focus_within_clears_when_focused_widget_destroyed() {
1547 use crate::signal::Signal;
1548 use crate::test_widgets::StackWidget;
1549 use crate::widget_builder::WidgetBuilder;
1550
1551 let halo = Signal::new(false);
1552
1553 let mut tree = WidgetTree::new();
1554 let leaf = tree.add(FillWidget::new().focusable());
1555 let _outer = tree.add(
1556 StackWidget::new()
1557 .add_child(leaf)
1558 .focus_within(halo.clone()),
1559 );
1560
1561 tree.layout(SizeProposal::exact(100.0, 50.0));
1562 tree.focus(leaf);
1563 assert!(halo.get());
1564
1565 tree.destroy_subtree(leaf);
1566 assert!(
1567 !halo.get(),
1568 "destroying the focused widget must clear focus_within on its ancestors"
1569 );
1570 }
1571
1572 #[test]
1573 fn hover_within_flips_via_pointer_move() {
1574 use crate::signal::Signal;
1575 use crate::test_widgets::StackWidget;
1576 use crate::widget_builder::WidgetBuilder;
1577 use teksilo_canvas::Point;
1578
1579 let glow = Signal::new(false);
1580
1581 let mut tree = WidgetTree::new();
1582 let leaf = tree.add(FillWidget::new());
1583 let _outer = tree.add(
1584 StackWidget::new()
1585 .add_child(leaf)
1586 .hover_within(glow.clone()),
1587 );
1588
1589 tree.layout(SizeProposal::exact(100.0, 50.0));
1590 assert!(!glow.get());
1591
1592 tree.pointer_move(Point::new(50.0, 25.0));
1593 assert!(glow.get(), "pointer over leaf → outer.hover_within = true");
1594
1595 tree.pointer_move(Point::new(500.0, 500.0));
1596 assert!(
1597 !glow.get(),
1598 "pointer moves outside the tree → hover_within clears"
1599 );
1600 }
1601
1602 #[test]
1603 fn hover_within_strict_ancestors_only() {
1604 use crate::signal::Signal;
1605 use crate::widget_builder::WidgetBuilder;
1606 use teksilo_canvas::Point;
1607
1608 let glow = Signal::new(false);
1609
1610 let mut tree = WidgetTree::new();
1611 let _w = tree.add(FillWidget::new().hover_within(glow.clone()));
1612 tree.layout(SizeProposal::exact(100.0, 50.0));
1613 tree.pointer_move(Point::new(50.0, 25.0));
1614
1615 assert!(
1616 !glow.get(),
1617 "hovering the widget itself must not set its own hover_within"
1618 );
1619 }
1620}
1621
1622#[cfg(test)]
1623mod tests_scope {
1624 use super::*;
1629 use crate::focus::TraversalScopePolicy::{Continue, Cycle};
1630 use crate::test_widgets::{FillWidget, StackWidget};
1631 use crate::widget_builder::WidgetBuilder;
1632
1633 fn indexed(tree: &mut WidgetTree, idx: i32) -> WidgetId {
1635 tree.add(FillWidget::new().focusable().tab_index(idx))
1636 }
1637
1638 fn tab(tree: &mut WidgetTree) {
1639 tree.press_key(Key::Tab, Modifiers::NONE);
1640 }
1641 fn shift_tab(tree: &mut WidgetTree) {
1642 tree.press_key(Key::Tab, Modifiers::SHIFT);
1643 }
1644
1645 #[test]
1646 fn overlapping_tab_index_in_sibling_scopes_does_not_interleave() {
1647 let mut tree = WidgetTree::new();
1648 let a1 = indexed(&mut tree, 1);
1649 let a2 = indexed(&mut tree, 2);
1650 let scope_a = tree.add(StackWidget::new().add_child(a1).add_child(a2));
1651 let b1 = indexed(&mut tree, 1);
1652 let b2 = indexed(&mut tree, 2);
1653 let scope_b = tree.add(StackWidget::new().add_child(b1).add_child(b2));
1654 let _root = tree.add(StackWidget::new().add_child(scope_a).add_child(scope_b));
1655 tree.set_traversal_scope(scope_a, Continue);
1656 tree.set_traversal_scope(scope_b, Continue);
1657 tree.layout(SizeProposal::exact(100.0, 100.0));
1658
1659 for expected in [a1, a2, b1, b2, a1] {
1661 tab(&mut tree);
1662 assert_eq!(tree.focused(), Some(expected));
1663 }
1664 }
1665
1666 #[test]
1667 fn continue_scope_flows_out_at_the_ends() {
1668 let mut tree = WidgetTree::new();
1670 let a = tree.add(FillWidget::new().focusable());
1671 let c1 = tree.add(FillWidget::new().focusable());
1672 let c2 = tree.add(FillWidget::new().focusable());
1673 let scope_c = tree.add(StackWidget::new().add_child(c1).add_child(c2));
1674 let b = tree.add(FillWidget::new().focusable());
1675 let _root = tree.add(
1676 StackWidget::new()
1677 .add_child(a)
1678 .add_child(scope_c)
1679 .add_child(b),
1680 );
1681 tree.set_traversal_scope(scope_c, Continue);
1682 tree.layout(SizeProposal::exact(100.0, 100.0));
1683
1684 for expected in [a, c1, c2, b, a] {
1685 tab(&mut tree);
1686 assert_eq!(tree.focused(), Some(expected));
1687 }
1688 tree.focus(c1);
1690 shift_tab(&mut tree);
1691 assert_eq!(tree.focused(), Some(a));
1692 }
1693
1694 #[test]
1695 fn cycle_scope_wraps_and_never_escapes() {
1696 let mut tree = WidgetTree::new();
1698 let a = tree.add(FillWidget::new().focusable());
1699 let d1 = tree.add(FillWidget::new().focusable());
1700 let d2 = tree.add(FillWidget::new().focusable());
1701 let scope_d = tree.add(StackWidget::new().add_child(d1).add_child(d2));
1702 let _root = tree.add(StackWidget::new().add_child(a).add_child(scope_d));
1703 tree.set_traversal_scope(scope_d, Cycle);
1704 tree.layout(SizeProposal::exact(100.0, 100.0));
1705
1706 tree.focus(d1);
1707 for _ in 0..10 {
1708 tab(&mut tree);
1709 let f = tree.focused();
1710 assert!(
1711 f == Some(d1) || f == Some(d2),
1712 "Cycle scope must trap Tab inside {{d1,d2}}, got {f:?}"
1713 );
1714 }
1715 tree.focus(d1);
1717 tab(&mut tree);
1718 assert_eq!(tree.focused(), Some(d2));
1719 shift_tab(&mut tree);
1720 assert_eq!(tree.focused(), Some(d1));
1721 shift_tab(&mut tree);
1722 assert_eq!(tree.focused(), Some(d2), "Shift+Tab at start wraps to last");
1723 }
1724
1725 #[test]
1726 fn empty_scope_is_skipped() {
1727 let mut tree = WidgetTree::new();
1729 let a = tree.add(FillWidget::new().focusable());
1730 let empty = tree.add(StackWidget::new());
1731 let b = tree.add(FillWidget::new().focusable());
1732 let _root = tree.add(
1733 StackWidget::new()
1734 .add_child(a)
1735 .add_child(empty)
1736 .add_child(b),
1737 );
1738 tree.set_traversal_scope(empty, Continue);
1739 tree.layout(SizeProposal::exact(100.0, 100.0));
1740
1741 for expected in [a, b, a] {
1742 tab(&mut tree);
1743 assert_eq!(tree.focused(), Some(expected));
1744 }
1745 }
1746
1747 #[test]
1748 fn single_member_cycle_scope_stays_put() {
1749 let mut tree = WidgetTree::new();
1750 let e = tree.add(FillWidget::new().focusable());
1751 let scope_e = tree.add(StackWidget::new().add_child(e));
1752 tree.set_traversal_scope(scope_e, Cycle);
1753 tree.layout(SizeProposal::exact(100.0, 50.0));
1754
1755 tree.focus(e);
1756 tab(&mut tree);
1757 assert_eq!(tree.focused(), Some(e));
1758 shift_tab(&mut tree);
1759 assert_eq!(tree.focused(), Some(e));
1760 }
1761
1762 #[test]
1763 fn nested_continue_in_cycle_flows_out_to_outer() {
1764 let mut tree = WidgetTree::new();
1766 let x = tree.add(FillWidget::new().focusable());
1767 let i1 = tree.add(FillWidget::new().focusable());
1768 let i2 = tree.add(FillWidget::new().focusable());
1769 let inner = tree.add(StackWidget::new().add_child(i1).add_child(i2));
1770 let y = tree.add(FillWidget::new().focusable());
1771 let outer = tree.add(
1772 StackWidget::new()
1773 .add_child(x)
1774 .add_child(inner)
1775 .add_child(y),
1776 );
1777 tree.set_traversal_scope(inner, Continue);
1778 tree.set_traversal_scope(outer, Cycle);
1779 tree.layout(SizeProposal::exact(100.0, 100.0));
1780
1781 for expected in [x, i1, i2, y, x] {
1782 tab(&mut tree);
1783 assert_eq!(tree.focused(), Some(expected));
1784 }
1785 tree.focus(i1);
1787 shift_tab(&mut tree);
1788 assert_eq!(tree.focused(), Some(x));
1789 }
1790
1791 #[test]
1792 fn nested_cycle_in_cycle_traps_in_the_inner_scope() {
1793 let mut tree = WidgetTree::new();
1796 let x = tree.add(FillWidget::new().focusable());
1797 let i1 = tree.add(FillWidget::new().focusable());
1798 let i2 = tree.add(FillWidget::new().focusable());
1799 let inner = tree.add(StackWidget::new().add_child(i1).add_child(i2));
1800 let y = tree.add(FillWidget::new().focusable());
1801 let outer = tree.add(
1802 StackWidget::new()
1803 .add_child(x)
1804 .add_child(inner)
1805 .add_child(y),
1806 );
1807 tree.set_traversal_scope(inner, Cycle);
1808 tree.set_traversal_scope(outer, Cycle);
1809 tree.layout(SizeProposal::exact(100.0, 100.0));
1810
1811 tree.focus(i1);
1812 for _ in 0..6 {
1813 tab(&mut tree);
1814 let f = tree.focused();
1815 assert!(
1816 f == Some(i1) || f == Some(i2),
1817 "inner Cycle must trap Tab; reached {f:?}"
1818 );
1819 }
1820 }
1821
1822 #[test]
1823 fn scoped_tab_index_orders_within_a_scope() {
1824 let mut tree = WidgetTree::new();
1826 let f3 = indexed(&mut tree, 3);
1827 let f1 = indexed(&mut tree, 1);
1828 let f2 = indexed(&mut tree, 2);
1829 let scope_f = tree.add(StackWidget::new().add_child(f3).add_child(f1).add_child(f2));
1830 tree.set_traversal_scope(scope_f, Cycle);
1831 tree.layout(SizeProposal::exact(100.0, 100.0));
1832
1833 for expected in [f1, f2, f3, f1] {
1834 tab(&mut tree);
1835 assert_eq!(tree.focused(), Some(expected));
1836 }
1837 }
1838
1839 #[test]
1840 fn destroyed_focused_widget_re_enters_at_first() {
1841 let mut tree = WidgetTree::new();
1842 let a = tree.add(FillWidget::new().focusable());
1843 let b = tree.add(FillWidget::new().focusable());
1844 tree.layout(SizeProposal::exact(100.0, 50.0));
1845
1846 tree.focus(b);
1847 tree.destroy_subtree(b);
1848 tab(&mut tree);
1849 assert_eq!(tree.focused(), Some(a), "Tab after destroy enters at first");
1850 }
1851
1852 #[test]
1853 fn set_traversal_scope_forces_node_non_focusable() {
1854 let mut tree = WidgetTree::new();
1857 let inner = tree.add(FillWidget::new().focusable());
1858 let scope = tree.add(StackWidget::new().add_child(inner).focusable(true));
1859 tree.set_traversal_scope(scope, Continue);
1860 tree.layout(SizeProposal::exact(100.0, 50.0));
1861
1862 tab(&mut tree);
1864 assert_eq!(tree.focused(), Some(inner));
1865 tab(&mut tree);
1866 assert_eq!(tree.focused(), Some(inner));
1867 }
1868
1869 #[test]
1870 fn no_scopes_behaves_like_a_flat_cycle() {
1871 let mut tree = WidgetTree::new();
1874 let a = indexed(&mut tree, 2);
1875 let b = indexed(&mut tree, 1);
1876 let c = tree.add(FillWidget::new().focusable());
1877 tree.layout(SizeProposal::exact(100.0, 50.0));
1878
1879 for expected in [b, a, c, b] {
1881 tab(&mut tree);
1882 assert_eq!(tree.focused(), Some(expected));
1883 }
1884 }
1885
1886 #[test]
1887 fn centered_modal_confines_tab_to_its_content() {
1888 let mut tree = WidgetTree::new();
1891 let outside1 = tree.add(FillWidget::new().focusable());
1892 let outside2 = tree.add(FillWidget::new().focusable());
1893 let m1 = tree.add(FillWidget::new().focusable());
1894 let m2 = tree.add(FillWidget::new().focusable());
1895 let content = tree.add(StackWidget::new().add_child(m1).add_child(m2));
1896 tree.layout(SizeProposal::exact(200.0, 100.0));
1897
1898 tree.show_overlay(crate::overlay::OverlayRequest {
1899 content_id: content,
1900 anchor: outside1,
1901 placement: crate::overlay::OverlayPlacement::Centered,
1902 dismiss: crate::overlay::DismissBehavior::Manual,
1903 layer: crate::overlay::OverlayLayer::InTree,
1904 parent_overlay: None,
1905 on_dismiss: None,
1906 fade_duration: None,
1907 });
1908
1909 for _ in 0..6 {
1910 tab(&mut tree);
1911 let f = tree.focused();
1912 assert!(
1913 f == Some(m1) || f == Some(m2),
1914 "modal must trap Tab inside its content, reached {f:?}"
1915 );
1916 }
1917 assert_ne!(tree.focused(), Some(outside1));
1918 assert_ne!(tree.focused(), Some(outside2));
1919 }
1920}
1921
1922#[cfg(test)]
1923mod tests_focus_out_dismissal {
1924 use super::*;
1931 use crate::overlay::{DismissBehavior, OverlayLayer, OverlayPlacement, OverlayRequest};
1932 use crate::test_widgets::{FillWidget, StackWidget};
1933
1934 fn tab(tree: &mut WidgetTree) {
1935 tree.press_key(Key::Tab, Modifiers::NONE);
1936 }
1937
1938 fn show_anchored(
1940 tree: &mut WidgetTree,
1941 content: WidgetId,
1942 anchor: WidgetId,
1943 parent: Option<crate::overlay::OverlayId>,
1944 ) -> crate::overlay::OverlayId {
1945 tree.show_overlay(OverlayRequest {
1946 content_id: content,
1947 anchor,
1948 placement: OverlayPlacement::Below,
1949 dismiss: DismissBehavior::EscapeOrClickOutside,
1950 layer: OverlayLayer::InTree,
1951 parent_overlay: parent,
1952 on_dismiss: None,
1953 fade_duration: None,
1954 })
1955 }
1956
1957 #[test]
1958 fn tab_out_of_a_non_modal_overlay_dismisses_it() {
1959 let mut tree = WidgetTree::new();
1960 let anchor = tree.add(FillWidget::new().focusable());
1961 let after = tree.add(FillWidget::new().focusable());
1962 let inner = tree.add(FillWidget::new().focusable());
1963 let content = tree.add(StackWidget::new().add_child(inner));
1964 tree.layout(SizeProposal::exact(200.0, 100.0));
1965 show_anchored(&mut tree, content, anchor, None);
1966
1967 tree.focus(inner);
1968 tab(&mut tree);
1969
1970 assert_ne!(tree.focused(), Some(inner), "focus genuinely left");
1971 assert!(
1972 tree.active_overlays().is_empty(),
1973 "an overlay must not stay open over the focus ring that left it"
1974 );
1975 assert!(tree.focused() == Some(after) || tree.focused() == Some(anchor));
1976 }
1977
1978 #[test]
1980 fn a_centered_modal_is_never_dismissed_by_focus_moving() {
1981 let mut tree = WidgetTree::new();
1982 let outside = tree.add(FillWidget::new().focusable());
1983 let m1 = tree.add(FillWidget::new().focusable());
1984 let content = tree.add(StackWidget::new().add_child(m1));
1985 tree.layout(SizeProposal::exact(200.0, 100.0));
1986 tree.show_overlay(OverlayRequest {
1987 content_id: content,
1988 anchor: outside,
1989 placement: OverlayPlacement::Centered,
1990 dismiss: DismissBehavior::Manual,
1991 layer: OverlayLayer::InTree,
1992 parent_overlay: None,
1993 on_dismiss: None,
1994 fade_duration: None,
1995 });
1996
1997 tree.focus(m1);
1998 tree.focus(outside);
2001
2002 assert_eq!(
2003 tree.active_overlays().len(),
2004 1,
2005 "a modal is the one overlay that legitimately contains focus"
2006 );
2007 }
2008
2009 #[test]
2013 fn the_modal_scrim_survives_focus_moving_inside_the_modal() {
2014 let mut tree = WidgetTree::new();
2015 let opener = tree.add(FillWidget::new().focusable());
2016 let m1 = tree.add(FillWidget::new().focusable());
2017 let m2 = tree.add(FillWidget::new().focusable());
2018 let scrim = tree.add(FillWidget::new());
2019 let content = tree.add(StackWidget::new().add_child(m1).add_child(m2));
2020 tree.layout(SizeProposal::exact(200.0, 100.0));
2021
2022 tree.show_overlay(OverlayRequest {
2023 content_id: scrim,
2024 anchor: opener,
2025 placement: OverlayPlacement::FullViewport,
2026 dismiss: DismissBehavior::Manual,
2027 layer: OverlayLayer::InTree,
2028 parent_overlay: None,
2029 on_dismiss: None,
2030 fade_duration: None,
2031 });
2032 tree.show_overlay(OverlayRequest {
2033 content_id: content,
2034 anchor: opener,
2035 placement: OverlayPlacement::Centered,
2036 dismiss: DismissBehavior::Manual,
2037 layer: OverlayLayer::InTree,
2038 parent_overlay: None,
2039 on_dismiss: None,
2040 fade_duration: None,
2041 });
2042
2043 tree.focus(opener);
2044 tree.focus(m1);
2045 tab(&mut tree);
2046
2047 assert_eq!(
2048 tree.active_overlays().len(),
2049 2,
2050 "scrim and modal both stand while focus moves within the modal"
2051 );
2052 }
2053
2054 #[test]
2057 fn focus_moving_into_a_child_overlay_keeps_the_parent_open() {
2058 let mut tree = WidgetTree::new();
2059 let anchor = tree.add(FillWidget::new().focusable());
2060 let parent_item = tree.add(FillWidget::new().focusable());
2061 let parent_content = tree.add(StackWidget::new().add_child(parent_item));
2062 let child_item = tree.add(FillWidget::new().focusable());
2063 let child_content = tree.add(StackWidget::new().add_child(child_item));
2064 tree.layout(SizeProposal::exact(200.0, 100.0));
2065
2066 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2067 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2068
2069 tree.focus(parent_item);
2070 tree.focus(child_item);
2071
2072 assert_eq!(
2073 tree.active_overlays().len(),
2074 2,
2075 "opening a submenu is not leaving the menu that owns it"
2076 );
2077 }
2078
2079 #[test]
2082 fn focus_back_to_the_parent_overlay_closes_only_the_child() {
2083 let mut tree = WidgetTree::new();
2084 let anchor = tree.add(FillWidget::new().focusable());
2085 let parent_item = tree.add(FillWidget::new().focusable());
2086 let parent_content = tree.add(StackWidget::new().add_child(parent_item));
2087 let child_item = tree.add(FillWidget::new().focusable());
2088 let child_content = tree.add(StackWidget::new().add_child(child_item));
2089 tree.layout(SizeProposal::exact(200.0, 100.0));
2090
2091 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2092 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2093
2094 tree.focus(child_item);
2095 tree.focus(parent_item);
2096
2097 assert_eq!(
2098 tree.active_overlays(),
2099 vec![parent],
2100 "the submenu goes, the menu that owns it stays"
2101 );
2102 }
2103
2104 #[test]
2107 fn leaving_a_nested_cascade_closes_every_level() {
2108 let mut tree = WidgetTree::new();
2109 let anchor = tree.add(FillWidget::new().focusable());
2110 let away = tree.add(FillWidget::new().focusable());
2111 let parent_item = tree.add(FillWidget::new().focusable());
2112 let parent_content = tree.add(StackWidget::new().add_child(parent_item));
2113 let child_item = tree.add(FillWidget::new().focusable());
2114 let child_content = tree.add(StackWidget::new().add_child(child_item));
2115 tree.layout(SizeProposal::exact(200.0, 100.0));
2116
2117 let parent = show_anchored(&mut tree, parent_content, anchor, None);
2118 show_anchored(&mut tree, child_content, parent_item, Some(parent));
2119
2120 tree.focus(child_item);
2121 tree.focus(away);
2122
2123 assert!(
2124 tree.active_overlays().is_empty(),
2125 "one move out of the cascade must leave nothing behind"
2126 );
2127 }
2128
2129 #[test]
2139 fn a_dropdown_inside_a_modal_still_follows_focus_out() {
2140 let mut tree = WidgetTree::new();
2141 let opener = tree.add(FillWidget::new().focusable());
2142 let trigger = tree.add(FillWidget::new().focusable());
2143 let next = tree.add(FillWidget::new().focusable());
2144 let modal_content = tree.add(StackWidget::new().add_child(trigger).add_child(next));
2145 let panel = tree.add(StackWidget::new());
2146 tree.layout(SizeProposal::exact(200.0, 100.0));
2147
2148 tree.show_overlay(OverlayRequest {
2149 content_id: modal_content,
2150 anchor: opener,
2151 placement: OverlayPlacement::Centered,
2152 dismiss: DismissBehavior::Manual,
2153 layer: OverlayLayer::InTree,
2154 parent_overlay: None,
2155 on_dismiss: None,
2156 fade_duration: None,
2157 });
2158 show_anchored(&mut tree, panel, trigger, None);
2160
2161 tree.focus(trigger);
2162 assert_eq!(
2163 tree.active_overlays().len(),
2164 2,
2165 "precondition: modal + panel"
2166 );
2167
2168 tab(&mut tree);
2169
2170 assert_eq!(
2171 tree.focused(),
2172 Some(next),
2173 "Tab moves on within the modal, as it should"
2174 );
2175 assert_eq!(
2176 tree.active_overlays().len(),
2177 1,
2178 "the dropdown must go — only the modal hosting it stays"
2179 );
2180 }
2181
2182 #[test]
2190 fn a_viewport_placed_notification_ignores_focus_moving() {
2191 let mut tree = WidgetTree::new();
2192 let trigger = tree.add(FillWidget::new().focusable());
2193 let elsewhere = tree.add(FillWidget::new().focusable());
2194 let snack = tree.add(StackWidget::new());
2195 tree.layout(SizeProposal::exact(200.0, 100.0));
2196 tree.show_overlay(OverlayRequest {
2197 content_id: snack,
2198 anchor: trigger,
2199 placement: OverlayPlacement::BottomCenter,
2200 dismiss: DismissBehavior::Manual,
2201 layer: OverlayLayer::InTree,
2202 parent_overlay: None,
2203 on_dismiss: None,
2204 fade_duration: None,
2205 });
2206
2207 tree.focus(trigger);
2208 tab(&mut tree);
2209
2210 assert_eq!(
2211 tree.active_overlays().len(),
2212 1,
2213 "a snackbar outlives the keystroke that moved focus off its trigger"
2214 );
2215 assert_ne!(tree.focused(), Some(trigger), "and focus did move");
2216 assert_eq!(tree.focused(), Some(elsewhere));
2217 }
2218
2219 #[test]
2223 fn leaving_a_hosted_menu_spares_the_host() {
2224 let mut tree = WidgetTree::new();
2225 let opener = tree.add(FillWidget::new().focusable());
2226 let away = tree.add(FillWidget::new().focusable());
2227 let modal_item = tree.add(FillWidget::new().focusable());
2228 let modal_content = tree.add(StackWidget::new().add_child(modal_item));
2229 let menu_item = tree.add(FillWidget::new().focusable());
2230 let menu_content = tree.add(StackWidget::new().add_child(menu_item));
2231 tree.layout(SizeProposal::exact(200.0, 100.0));
2232
2233 let host = tree.show_overlay(OverlayRequest {
2235 content_id: modal_content,
2236 anchor: opener,
2237 placement: OverlayPlacement::Centered,
2238 dismiss: DismissBehavior::Manual,
2239 layer: OverlayLayer::InTree,
2240 parent_overlay: None,
2241 on_dismiss: None,
2242 fade_duration: None,
2243 });
2244 show_anchored(&mut tree, menu_content, modal_item, Some(host));
2245
2246 tree.focus(menu_item);
2247 tree.focus(away);
2248
2249 assert_eq!(
2250 tree.active_overlays(),
2251 vec![host],
2252 "the menu goes; the modal hosting it stays"
2253 );
2254 }
2255
2256 #[test]
2260 fn leaving_the_anchor_dismisses_a_panel_focus_never_entered() {
2261 let mut tree = WidgetTree::new();
2262 let trigger = tree.add(FillWidget::new().focusable());
2263 let after = tree.add(FillWidget::new().focusable());
2264 let content = tree.add(StackWidget::new());
2265 tree.layout(SizeProposal::exact(200.0, 100.0));
2266 show_anchored(&mut tree, content, trigger, None);
2267
2268 tree.focus(trigger);
2269 assert_eq!(tree.active_overlays().len(), 1, "precondition: panel is up");
2270
2271 tree.focus(after);
2272 assert!(
2273 tree.active_overlays().is_empty(),
2274 "leaving the trigger is leaving the dropdown it owns"
2275 );
2276 }
2277}