use crate::app::context::FocusPolicy;
use crate::callback::ScopeId;
use crate::core::element::Key;
use crate::core::node::{NodeId, NodeKind, NodeTree};
use crate::layout::tag::{Tag, tag_of_node};
use crate::widgets::FocusScope;
pub(crate) fn scope_for_node(tree: &NodeTree, id: NodeId) -> Option<ScopeId> {
let mut current = Some(id);
while let Some(id) = current {
if !tree.is_valid(id) {
break;
}
let node = tree.node(id);
if let NodeKind::Group(group) = &node.kind {
return Some(group.scope);
}
current = node.parent;
}
None
}
pub(crate) fn in_excluded_scope(tree: &NodeTree, id: NodeId) -> bool {
let mut current = Some(id);
while let Some(id) = current {
if !tree.is_valid(id) {
return false;
}
let node = tree.node(id);
if node.focus_scope() == FocusScope::Exclude {
return true;
}
current = node.parent;
}
false
}
fn containing_scope(tree: &NodeTree, id: NodeId) -> Option<NodeId> {
if in_excluded_scope(tree, id) {
return None;
}
if !tree.is_valid(id) {
return None;
}
let mut current = tree.node(id).parent;
while let Some(id) = current {
if !tree.is_valid(id) {
return None;
}
let node = tree.node(id);
if node.focus_scope() == FocusScope::Contain {
return Some(id);
}
current = node.parent;
}
None
}
pub(crate) fn traversal_focusables(tree: &NodeTree, focused: Option<NodeId>) -> Vec<NodeId> {
if let Some(scope) = focused.and_then(|id| containing_scope(tree, id)) {
return tree.focusables_in_subtree(scope);
}
let ring = tree.focusables();
if !ring.is_empty() {
return ring;
}
tree.focusables_unrestricted()
}
pub(crate) fn restore_focus(
tree: &NodeTree,
focused: &mut Option<NodeId>,
focused_key: &mut Option<Key>,
focused_tag: &mut Option<Tag>,
policy: FocusPolicy,
) {
if let Some(id) = *focused
&& tree.is_valid(id)
&& tree.node(id).is_focusable()
{
*focused_key = tree.node(id).key.clone();
*focused_tag = Some(tag_of_node(tree.node(id)));
return;
}
if let Some(key) = focused_key {
if let Some(id) = tree
.iter_with_overlays()
.find(|n| n.key.as_ref() == Some(key))
.map(|n| n.id)
{
if tree.node(id).is_focusable() {
*focused = Some(id);
*focused_tag = Some(tag_of_node(tree.node(id)));
return;
}
let descendant = if in_excluded_scope(tree, id) {
find_first_focusable_descendant_unscoped(tree, id)
} else {
find_first_focusable_descendant(tree, id)
};
if let Some(focusable_id) = descendant {
*focused = Some(focusable_id);
*focused_tag = Some(tag_of_node(tree.node(focusable_id)));
return;
}
}
}
if policy != FocusPolicy::Manual
&& let Some(tag) = *focused_tag
&& let Some(id) = tree
.iter_with_overlays()
.find(|n| n.is_focusable() && !in_excluded_scope(tree, n.id) && tag_of_node(n) == tag)
.map(|n| n.id)
{
*focused = Some(id);
*focused_key = tree.node(id).key.clone();
return;
}
if policy == FocusPolicy::Auto {
*focused = first_focusable(tree);
if let Some(id) = *focused {
*focused_key = tree.node(id).key.clone();
*focused_tag = Some(tag_of_node(tree.node(id)));
return;
}
*focused_key = None;
}
*focused = None;
*focused_tag = None;
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum FocusDirection {
Next,
Prev,
}
pub(crate) fn ring_step(
focusables: &[NodeId],
focused: Option<NodeId>,
direction: FocusDirection,
) -> Option<NodeId> {
let (&first, &last) = (focusables.first()?, focusables.last()?);
Some(match (focused, direction) {
(Some(curr), FocusDirection::Next) => {
let at = focusables.partition_point(|id| id.index() <= curr.index());
focusables.get(at).copied().unwrap_or(first)
}
(Some(curr), FocusDirection::Prev) => {
let at = focusables.partition_point(|id| id.index() < curr.index());
at.checked_sub(1).map_or(last, |i| focusables[i])
}
(None, FocusDirection::Next) => first,
(None, FocusDirection::Prev) => last,
})
}
pub(crate) fn step(
tree: &NodeTree,
focused: &mut Option<NodeId>,
focused_key: &mut Option<Key>,
focused_tag: &mut Option<Tag>,
direction: FocusDirection,
) {
let focusables = traversal_focusables(tree, *focused);
let Some(target) = ring_step(&focusables, *focused, direction) else {
return;
};
*focused = Some(target);
*focused_key = tree.node(target).key.clone();
*focused_tag = Some(tag_of_node(tree.node(target)));
}
pub(crate) fn focus_next(
tree: &NodeTree,
focused: &mut Option<NodeId>,
focused_key: &mut Option<Key>,
focused_tag: &mut Option<Tag>,
) {
step(
tree,
focused,
focused_key,
focused_tag,
FocusDirection::Next,
);
}
pub(crate) fn focus_prev(
tree: &NodeTree,
focused: &mut Option<NodeId>,
focused_key: &mut Option<Key>,
focused_tag: &mut Option<Tag>,
) {
step(
tree,
focused,
focused_key,
focused_tag,
FocusDirection::Prev,
);
}
pub(crate) fn step_for_policy(
tree: &NodeTree,
focused: &mut Option<NodeId>,
focused_key: &mut Option<Key>,
focused_tag: &mut Option<Tag>,
policy: FocusPolicy,
direction: FocusDirection,
) -> bool {
if policy == FocusPolicy::Manual {
return false;
}
step(tree, focused, focused_key, focused_tag, direction);
true
}
pub(crate) fn find_first_focusable_descendant(tree: &NodeTree, root: NodeId) -> Option<NodeId> {
find_first_focusable_descendant_impl(tree, root, true)
}
fn find_first_focusable_descendant_unscoped(tree: &NodeTree, root: NodeId) -> Option<NodeId> {
find_first_focusable_descendant_impl(tree, root, false)
}
fn find_first_focusable_descendant_impl(
tree: &NodeTree,
root: NodeId,
respect_exclude: bool,
) -> Option<NodeId> {
let mut queue = std::collections::VecDeque::new();
queue.push_back(root);
while let Some(current_id) = queue.pop_front() {
let node = tree.node(current_id);
for &child in &node.children {
if !tree.is_valid(child) {
continue;
}
let child_node = tree.node(child);
if respect_exclude && child_node.focus_scope() == FocusScope::Exclude {
continue;
}
if child_node.is_focusable() {
return Some(child);
}
queue.push_back(child);
}
}
None
}
fn first_focusable(tree: &NodeTree) -> Option<NodeId> {
if !tree.is_valid(tree.root) {
return None;
}
if let Some(first) = tree.focusables_unrestricted().first().copied() {
return Some(first);
}
let mut candidates: Vec<NodeId> = Vec::new();
let mut stack = vec![tree.root];
while let Some(id) = stack.pop() {
let node = tree.node(id);
if node.focus_scope() == FocusScope::Exclude {
continue;
}
if node.is_focusable() {
candidates.push(id);
}
stack.extend(
node.children
.iter()
.copied()
.filter(|child| tree.is_valid(*child)),
);
}
candidates.into_iter().min_by_key(|id| id.index())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widgets::Button;
use crate::widgets::internal::FrameNode;
fn alloc_node(tree: &mut NodeTree, parent: Option<NodeId>, focusable: bool) -> NodeId {
let id = tree.alloc();
let epoch = tree.node(tree.root).epoch; let node = tree.node_mut(id);
node.parent = parent;
node.epoch = epoch;
if focusable {
node.kind = NodeKind::from(Button::new("btn"));
}
id
}
fn alloc_focusable_not_tab_stop(tree: &mut NodeTree, parent: Option<NodeId>) -> NodeId {
let id = alloc_node(tree, parent, false);
tree.node_mut(id).kind = NodeKind::from(Button::new("btn").tab_stop(false));
id
}
fn alloc_scope(tree: &mut NodeTree, parent: Option<NodeId>, scope: FocusScope) -> NodeId {
let id = alloc_node(tree, parent, false);
tree.node_mut(id).kind = NodeKind::Frame(FrameNode {
focus_scope: scope,
..FrameNode::default()
});
id
}
fn alloc_focusable_scope(
tree: &mut NodeTree,
parent: Option<NodeId>,
scope: FocusScope,
) -> NodeId {
let id = alloc_node(tree, parent, false);
tree.node_mut(id).kind = NodeKind::Frame(FrameNode {
focus_scope: scope,
focusable: true,
..FrameNode::default()
});
id
}
fn build_tree_with_focusable_children(n: usize) -> (NodeTree, NodeId, Vec<NodeId>) {
let mut tree = NodeTree::new();
let epoch = tree.begin_epoch();
let root = tree.alloc();
tree.root = root;
{
let r = tree.node_mut(root);
r.epoch = epoch;
}
let mut children = Vec::new();
for _ in 0..n {
let child = alloc_node(&mut tree, Some(root), true);
children.push(child);
}
tree.node_mut(root).children = children.clone();
(tree, root, children)
}
#[test]
fn focus_next_wraps_around_at_end() {
let (tree, _root, children) = build_tree_with_focusable_children(3);
let mut focused = Some(children[2]); let mut key = None;
let mut tag = None;
focus_next(&tree, &mut focused, &mut key, &mut tag);
assert_eq!(focused, Some(children[0]), "should wrap to first focusable");
}
#[test]
fn focus_prev_wraps_around_at_start() {
let (tree, _root, children) = build_tree_with_focusable_children(3);
let mut focused = Some(children[0]); let mut key = None;
let mut tag = None;
focus_prev(&tree, &mut focused, &mut key, &mut tag);
assert_eq!(focused, Some(children[2]), "should wrap to last focusable");
}
#[test]
fn focus_next_selects_first_when_none_focused() {
let (tree, _root, children) = build_tree_with_focusable_children(3);
let mut focused: Option<NodeId> = None;
let mut key = None;
let mut tag = None;
focus_next(&tree, &mut focused, &mut key, &mut tag);
assert_eq!(focused, Some(children[0]));
}
#[test]
fn focus_prev_selects_last_when_none_focused() {
let (tree, _root, children) = build_tree_with_focusable_children(3);
let mut focused: Option<NodeId> = None;
let mut key = None;
let mut tag = None;
focus_prev(&tree, &mut focused, &mut key, &mut tag);
assert_eq!(focused, Some(children[2]));
}
#[test]
fn focus_next_noop_on_empty_tree() {
let mut tree = NodeTree::new();
tree.begin_epoch();
let mut focused: Option<NodeId> = None;
let mut key = None;
let mut tag = None;
focus_next(&tree, &mut focused, &mut key, &mut tag);
assert_eq!(focused, None);
}
#[test]
fn excluded_scope_is_skipped_by_ring_fallback_and_descendant_search() {
let (mut tree, root, _) = build_tree_with_focusable_children(0);
let excluded = alloc_scope(&mut tree, Some(root), FocusScope::Exclude);
let hidden = alloc_node(&mut tree, Some(excluded), true);
tree.node_mut(hidden).key = Some(Key::from("hidden"));
tree.node_mut(excluded).children = vec![hidden];
let visible = alloc_node(&mut tree, Some(root), true);
tree.node_mut(visible).key = Some(Key::from("visible"));
tree.node_mut(root).children = vec![excluded, visible];
assert_eq!(tree.focusables(), vec![visible]);
assert_eq!(find_first_focusable_descendant(&tree, root), Some(visible));
let mut focused = None;
let mut key = None;
let mut tag = None;
restore_focus(&tree, &mut focused, &mut key, &mut tag, FocusPolicy::Auto);
assert_eq!(focused, Some(visible));
focused = None;
key = Some(Key::from("hidden"));
tag = None;
restore_focus(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusPolicy::OnDemand,
);
assert_eq!(focused, Some(hidden), "keyed requests bypass exclusion");
tree.node_mut(root).key = Some(Key::from("root"));
focused = None;
key = Some(Key::from("root"));
tag = None;
restore_focus(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusPolicy::OnDemand,
);
assert_eq!(
focused,
Some(visible),
"ancestor requests must still respect nested exclusion"
);
}
#[test]
fn nearest_containing_scope_cycles_and_wraps() {
let (mut tree, root, _) = build_tree_with_focusable_children(0);
let outside = alloc_node(&mut tree, Some(root), true);
let outer = alloc_scope(&mut tree, Some(root), FocusScope::Contain);
let outer_button = alloc_node(&mut tree, Some(outer), true);
let inner = alloc_scope(&mut tree, Some(outer), FocusScope::Contain);
let inner_first = alloc_node(&mut tree, Some(inner), true);
let inner_second = alloc_node(&mut tree, Some(inner), true);
tree.node_mut(inner).children = vec![inner_first, inner_second];
tree.node_mut(outer).children = vec![outer_button, inner];
tree.node_mut(root).children = vec![outside, outer];
let mut focused = Some(inner_second);
let mut key = None;
let mut tag = None;
focus_next(&tree, &mut focused, &mut key, &mut tag);
assert_eq!(focused, Some(inner_first));
focus_prev(&tree, &mut focused, &mut key, &mut tag);
assert_eq!(focused, Some(inner_second));
}
#[test]
fn containing_scope_uses_global_node_order_after_child_reorder() {
let (mut tree, root, _) = build_tree_with_focusable_children(0);
let scope = alloc_scope(&mut tree, Some(root), FocusScope::Contain);
let first_allocated = alloc_node(&mut tree, Some(scope), true);
let second_allocated = alloc_node(&mut tree, Some(scope), true);
let third_allocated = alloc_node(&mut tree, Some(scope), true);
tree.node_mut(scope).children = vec![third_allocated, second_allocated, first_allocated];
tree.node_mut(root).children = vec![scope];
let mut focused = Some(third_allocated);
let mut key = None;
let mut tag = None;
focus_next(&tree, &mut focused, &mut key, &mut tag);
assert_eq!(focused, Some(first_allocated));
}
#[test]
fn find_first_focusable_descendant_breadth_first() {
let mut tree = NodeTree::new();
let epoch = tree.begin_epoch();
let root = tree.alloc();
tree.root = root;
tree.node_mut(root).epoch = epoch;
let child_a = alloc_node(&mut tree, Some(root), false);
let grandchild = alloc_node(&mut tree, Some(child_a), true);
tree.node_mut(child_a).children = vec![grandchild];
let child_b = alloc_node(&mut tree, Some(root), true);
tree.node_mut(root).children = vec![child_a, child_b];
let result = find_first_focusable_descendant(&tree, root);
assert_eq!(
result,
Some(child_b),
"BFS should find shallower child_b first"
);
}
#[test]
fn find_first_focusable_descendant_returns_none_when_no_focusable() {
let mut tree = NodeTree::new();
let epoch = tree.begin_epoch();
let root = tree.alloc();
tree.root = root;
tree.node_mut(root).epoch = epoch;
let child = alloc_node(&mut tree, Some(root), false);
tree.node_mut(root).children = vec![child];
assert_eq!(find_first_focusable_descendant(&tree, root), None);
}
#[test]
fn restore_focus_keeps_valid_focused_node() {
let (tree, _root, children) = build_tree_with_focusable_children(2);
let mut focused = Some(children[1]);
let mut key = None;
let mut tag = None;
restore_focus(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusPolicy::OnDemand,
);
assert_eq!(
focused,
Some(children[1]),
"valid focusable node should be kept"
);
assert_eq!(tag, Some(Tag::Button));
}
#[test]
fn restore_focus_falls_back_to_key_match() {
let (mut tree, _root, children) = build_tree_with_focusable_children(2);
let the_key: Key = "my-btn".into();
tree.node_mut(children[0]).key = Some(the_key.clone());
let mut focused = Some(NodeId::INVALID);
let mut key = Some(the_key);
let mut tag = None;
restore_focus(&tree, &mut focused, &mut key, &mut tag, FocusPolicy::Manual);
assert_eq!(focused, Some(children[0]), "should restore by key");
}
#[test]
fn restore_focus_falls_back_to_tag_match() {
let (tree, _root, children) = build_tree_with_focusable_children(2);
let mut focused = Some(NodeId::INVALID);
let mut key = None;
let mut tag = Some(Tag::Button);
restore_focus(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusPolicy::OnDemand,
);
assert_eq!(focused, Some(children[0]));
}
#[test]
fn restore_focus_falls_back_to_first_focusable() {
let (tree, _root, children) = build_tree_with_focusable_children(2);
let mut focused = Some(NodeId::INVALID);
let mut key = None;
let mut tag = None;
restore_focus(&tree, &mut focused, &mut key, &mut tag, FocusPolicy::Auto);
assert_eq!(focused, Some(children[0]));
}
#[test]
fn restore_focus_on_demand_keeps_key_without_fallback() {
let (tree, _root, _children) = build_tree_with_focusable_children(2);
let remembered_key: Key = "missing".into();
let mut focused = Some(NodeId::INVALID);
let mut key = Some(remembered_key.clone());
let mut tag = None;
restore_focus(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusPolicy::OnDemand,
);
assert_eq!(focused, None);
assert_eq!(key, Some(remembered_key));
assert_eq!(tag, None);
}
#[test]
fn tab_from_outside_does_not_tunnel_into_a_contain_pane() {
let (mut tree, root, _) = build_tree_with_focusable_children(0);
let outside = alloc_node(&mut tree, Some(root), true);
let pane = alloc_scope(&mut tree, Some(root), FocusScope::Contain);
let inside = alloc_node(&mut tree, Some(pane), true);
tree.node_mut(pane).children = vec![inside];
tree.node_mut(root).children = vec![outside, pane];
assert_eq!(
tree.focusables(),
vec![outside],
"pane is opaque to the ring"
);
let mut focused = Some(outside);
let (mut key, mut tag) = (None, None);
step(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusDirection::Next,
);
assert_eq!(focused, Some(outside), "Tab must not enter the pane");
focused = Some(inside);
step(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusDirection::Next,
);
assert_eq!(focused, Some(inside));
}
#[test]
fn nested_contain_pane_is_opaque_to_the_enclosing_pane() {
let (mut tree, root, _) = build_tree_with_focusable_children(0);
let outer = alloc_scope(&mut tree, Some(root), FocusScope::Contain);
let outer_button = alloc_node(&mut tree, Some(outer), true);
let inner = alloc_scope(&mut tree, Some(outer), FocusScope::Contain);
let inner_button = alloc_node(&mut tree, Some(inner), true);
tree.node_mut(inner).children = vec![inner_button];
tree.node_mut(outer).children = vec![outer_button, inner];
tree.node_mut(root).children = vec![outer];
assert_eq!(
tree.focusables_in_subtree(outer),
vec![outer_button],
"the inner pane is not part of the outer pane's ring"
);
let mut focused = Some(outer_button);
let (mut key, mut tag) = (None, None);
step(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusDirection::Next,
);
assert_eq!(
focused,
Some(outer_button),
"Tab must not fall into the inner pane and get stuck"
);
}
#[test]
fn tab_works_when_every_stop_lives_inside_a_pane() {
let (mut tree, root, _) = build_tree_with_focusable_children(0);
let pane = alloc_scope(&mut tree, Some(root), FocusScope::Contain);
let first = alloc_node(&mut tree, Some(pane), true);
let second = alloc_node(&mut tree, Some(pane), true);
tree.node_mut(pane).children = vec![first, second];
tree.node_mut(root).children = vec![pane];
assert!(tree.focusables().is_empty());
let mut focused = None;
let (mut key, mut tag) = (None, None);
step(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusDirection::Next,
);
assert_eq!(focused, Some(first), "Tab must not be dead");
step(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusDirection::Next,
);
assert_eq!(focused, Some(second), "then the pane's own ring applies");
}
#[test]
fn tab_steps_past_a_focused_node_that_is_not_a_tab_stop() {
let (mut tree, root, _) = build_tree_with_focusable_children(0);
let first = alloc_node(&mut tree, Some(root), true);
let skipped = alloc_focusable_not_tab_stop(&mut tree, Some(root));
let last = alloc_node(&mut tree, Some(root), true);
tree.node_mut(root).children = vec![first, skipped, last];
assert_eq!(tree.focusables(), vec![first, last]);
let mut focused = Some(skipped);
let (mut key, mut tag) = (None, None);
step(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusDirection::Next,
);
assert_eq!(
focused,
Some(last),
"advance past it, not back to the start"
);
focused = Some(skipped);
step(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusDirection::Prev,
);
assert_eq!(focused, Some(first), "and back to the true predecessor");
}
#[test]
fn focusable_contain_pane_is_a_stop_in_the_enclosing_ring() {
let (mut tree, root, _) = build_tree_with_focusable_children(0);
let outside = alloc_node(&mut tree, Some(root), true);
let pane = alloc_focusable_scope(&mut tree, Some(root), FocusScope::Contain);
let inside = alloc_node(&mut tree, Some(pane), true);
tree.node_mut(pane).children = vec![inside];
tree.node_mut(root).children = vec![outside, pane];
assert_eq!(
tree.focusables(),
vec![outside, pane],
"the boundary is a stop in the outer ring"
);
assert_eq!(
tree.focusables_in_subtree(pane),
vec![inside],
"the pane is not a member of its own ring"
);
let mut focused = Some(outside);
let (mut key, mut tag) = (None, None);
step(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusDirection::Next,
);
assert_eq!(focused, Some(pane));
step(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusDirection::Next,
);
assert_eq!(
focused,
Some(outside),
"Tab passes over the pane, not into it"
);
focused = Some(inside);
step(
&tree,
&mut focused,
&mut key,
&mut tag,
FocusDirection::Next,
);
assert_eq!(focused, Some(inside), "the trap stays airtight from inside");
}
#[test]
fn auto_fallback_agrees_with_the_first_tab_target() {
let (mut tree, root, _) = build_tree_with_focusable_children(0);
let first_allocated = alloc_node(&mut tree, Some(root), true);
let second_allocated = alloc_node(&mut tree, Some(root), true);
tree.node_mut(root).children = vec![second_allocated, first_allocated];
assert_eq!(first_focusable(&tree), Some(first_allocated));
assert_eq!(first_focusable(&tree), tree.focusables().first().copied());
let mut focused = Some(NodeId::INVALID);
let (mut key, mut tag) = (None, None);
restore_focus(&tree, &mut focused, &mut key, &mut tag, FocusPolicy::Auto);
assert_eq!(focused, Some(first_allocated));
}
#[test]
fn restore_focus_manual_skips_tag_restore() {
let (tree, _root, _children) = build_tree_with_focusable_children(2);
let mut focused = Some(NodeId::INVALID);
let mut key = None;
let mut tag = Some(Tag::Button);
restore_focus(&tree, &mut focused, &mut key, &mut tag, FocusPolicy::Manual);
assert_eq!(focused, None);
assert_eq!(key, None);
assert_eq!(tag, None);
}
}