use rdom_core::NodeId;
use crate::TuiDom;
pub fn tab_index(dom: &TuiDom, id: NodeId) -> Option<i32> {
let node = dom.node(id);
if node.has_attribute("disabled") {
return None;
}
if let Some(t) = node
.get_attribute("tabindex")
.and_then(|s| s.parse::<i32>().ok())
{
return Some(t);
}
if is_implicit_focusable(dom, id) {
return Some(0);
}
None
}
fn is_implicit_focusable(dom: &TuiDom, id: NodeId) -> bool {
let node = dom.node(id);
let Some(tag) = node.tag_name() else {
return false;
};
match tag {
"input" => !matches!(node.get_attribute("type"), Some("hidden")),
"button" | "textarea" | "select" => true,
"summary" => true,
"a" | "area" => node.has_attribute("href"),
_ if node.get_attribute("role") == Some("tree") => true,
_ => false,
}
}
pub fn is_focusable(dom: &TuiDom, id: NodeId) -> bool {
tab_index(dom, id).is_some()
}
pub fn is_tab_focusable(dom: &TuiDom, id: NodeId) -> bool {
tab_index(dom, id).is_some_and(|t| t >= 0)
}
pub fn focusable_elements(dom: &TuiDom) -> Vec<NodeId> {
let mut positive: Vec<(i32, usize, NodeId)> = Vec::new();
let mut zero: Vec<(usize, NodeId)> = Vec::new();
let mut order: usize = 0;
collect(dom, dom.root(), &mut positive, &mut zero, &mut order);
positive.sort_by_key(|(ti, ord, _)| (*ti, *ord));
zero.sort_by_key(|(ord, _)| *ord);
let raw: Vec<NodeId> = positive
.into_iter()
.map(|(_, _, id)| id)
.chain(zero.into_iter().map(|(_, id)| id))
.collect();
dedupe_radio_groups(dom, raw)
}
fn dedupe_radio_groups(dom: &TuiDom, list: Vec<NodeId>) -> Vec<NodeId> {
use std::collections::HashMap;
let mut rep_for: HashMap<String, NodeId> = HashMap::new();
for &id in &list {
let Some(name) = named_radio_name(dom, id) else {
continue;
};
let is_checked = dom.node(id).has_attribute("checked");
match rep_for.get(&name) {
None => {
rep_for.insert(name, id);
}
Some(&existing) if is_checked && !dom.node(existing).has_attribute("checked") => {
rep_for.insert(name, id);
}
_ => {}
}
}
list.into_iter()
.filter(|&id| match named_radio_name(dom, id) {
Some(name) => rep_for.get(&name) == Some(&id),
None => true,
})
.collect()
}
fn named_radio_name(dom: &TuiDom, id: NodeId) -> Option<String> {
let node = dom.node(id);
if node.tag_name() != Some("input") {
return None;
}
if node.get_attribute("type") != Some("radio") {
return None;
}
let name = node.get_attribute("name")?;
if name.is_empty() {
return None;
}
Some(name.to_string())
}
fn collect(
dom: &TuiDom,
id: NodeId,
positive: &mut Vec<(i32, usize, NodeId)>,
zero: &mut Vec<(usize, NodeId)>,
order: &mut usize,
) {
*order += 1;
let current_order = *order;
if let Some(t) = tab_index(dom, id) {
if t > 0 {
positive.push((t, current_order, id));
} else if t == 0 {
zero.push((current_order, id));
}
}
for child in dom.node(id).child_nodes() {
collect(dom, child.id(), positive, zero, order);
}
}
pub fn focus_next(dom: &mut TuiDom) {
step_focus(dom, 1);
}
pub fn focus_prev(dom: &mut TuiDom) {
step_focus(dom, -1);
}
fn step_focus(dom: &mut TuiDom, direction: i32) {
let list = focusable_elements(dom);
if list.is_empty() {
return;
}
let target = match dom
.focused()
.and_then(|cur| list.iter().position(|&e| e == cur))
{
Some(i) => {
let len = list.len() as i32;
let next = (i as i32 + direction).rem_euclid(len);
list[next as usize]
}
None => {
if direction > 0 {
list[0]
} else {
list[list.len() - 1]
}
}
};
super::focus_node(dom, Some(target));
}