use accesskit::NodeId;
use crate::tree::{A11yNode, WidgetRole};
fn is_focusable_role(role: WidgetRole) -> bool {
matches!(
role,
WidgetRole::Button
| WidgetRole::TextInput
| WidgetRole::Checkbox
| WidgetRole::Slider
| WidgetRole::Tab
| WidgetRole::Link
| WidgetRole::MenuItem
)
}
pub struct TabOrder {
pub order: Vec<NodeId>,
}
impl TabOrder {
pub fn compute(root: &A11yNode) -> Self {
let mut explicit: Vec<(u32, NodeId)> = Vec::new();
let mut natural: Vec<NodeId> = Vec::new();
collect_focusable(root, &mut explicit, &mut natural);
explicit.sort_by_key(|(idx, _)| *idx);
let mut order: Vec<NodeId> = explicit.into_iter().map(|(_, id)| id).collect();
order.extend(natural);
Self { order }
}
pub fn next_focus(&self, current: Option<NodeId>) -> Option<NodeId> {
if self.order.is_empty() {
return None;
}
match current {
None => self.order.first().copied(),
Some(id) => {
let pos = self.order.iter().position(|&n| n == id);
match pos {
None => self.order.first().copied(),
Some(i) => {
let next = (i + 1) % self.order.len();
self.order.get(next).copied()
}
}
}
}
}
pub fn prev_focus(&self, current: Option<NodeId>) -> Option<NodeId> {
if self.order.is_empty() {
return None;
}
match current {
None => self.order.last().copied(),
Some(id) => {
let pos = self.order.iter().position(|&n| n == id);
match pos {
None => self.order.last().copied(),
Some(0) => self.order.last().copied(),
Some(i) => self.order.get(i - 1).copied(),
}
}
}
}
}
pub fn tab_next(tab_order: &TabOrder, current: Option<NodeId>) -> Option<NodeId> {
tab_order.next_focus(current)
}
pub fn tab_prev(tab_order: &TabOrder, current: Option<NodeId>) -> Option<NodeId> {
tab_order.prev_focus(current)
}
fn collect_focusable(
node: &A11yNode,
explicit: &mut Vec<(u32, NodeId)>,
natural: &mut Vec<NodeId>,
) {
let focusable = is_focusable_role(node.role) && !node.props.disabled;
if focusable {
match node.props.tab_index {
Some(idx) if idx > 0 => explicit.push((idx, node.id)),
_ => natural.push(node.id),
}
}
for child in &node.children {
collect_focusable(child, explicit, natural);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tree::{A11yNode, WidgetRole};
use accesskit::NodeId;
fn nid(n: u64) -> NodeId {
NodeId(n)
}
fn btn(id: u64) -> A11yNode {
A11yNode::simple(nid(id), WidgetRole::Button, Some(format!("Btn{id}")))
}
fn btn_with_tab_index(id: u64, idx: u32) -> A11yNode {
let mut n = btn(id);
n.props.tab_index = Some(idx);
n
}
#[test]
fn test_tab_order_natural() {
let mut root = A11yNode::simple(nid(0), WidgetRole::Window, None);
root.children.push(btn(1));
root.children.push(btn(2));
root.children.push(btn(3));
let order = TabOrder::compute(&root);
assert_eq!(order.order, vec![nid(1), nid(2), nid(3)]);
}
#[test]
fn test_tab_order_explicit_tab_index() {
let mut root = A11yNode::simple(nid(0), WidgetRole::Window, None);
root.children.push(btn(1));
root.children.push(btn(2));
root.children.push(btn_with_tab_index(3, 1));
let order = TabOrder::compute(&root);
assert_eq!(order.order[0], nid(3), "tab_index=1 node must be first");
assert_eq!(order.order[1], nid(1));
assert_eq!(order.order[2], nid(2));
}
#[test]
fn test_next_focus_wraps() {
let mut root = A11yNode::simple(nid(0), WidgetRole::Window, None);
root.children.push(btn(1));
root.children.push(btn(2));
root.children.push(btn(3));
let order = TabOrder::compute(&root);
assert_eq!(order.next_focus(Some(nid(3))), Some(nid(1)));
}
#[test]
fn test_prev_focus_wraps() {
let mut root = A11yNode::simple(nid(0), WidgetRole::Window, None);
root.children.push(btn(1));
root.children.push(btn(2));
root.children.push(btn(3));
let order = TabOrder::compute(&root);
assert_eq!(order.prev_focus(Some(nid(1))), Some(nid(3)));
}
#[test]
fn test_tab_order_disabled_excluded() {
let mut root = A11yNode::simple(nid(0), WidgetRole::Window, None);
let mut disabled_btn = btn(1);
disabled_btn.props.disabled = true;
root.children.push(disabled_btn);
root.children.push(btn(2));
let order = TabOrder::compute(&root);
assert_eq!(order.order, vec![nid(2)]);
}
#[test]
fn test_tab_order_non_focusable_excluded() {
let mut root = A11yNode::simple(nid(0), WidgetRole::Window, None);
root.children
.push(A11yNode::simple(nid(1), WidgetRole::Label, None));
root.children.push(btn(2));
let order = TabOrder::compute(&root);
assert_eq!(order.order, vec![nid(2)]);
}
}