use accesskit::{Action, Node, NodeId, Rect as AkRect, Role as AkRole, Tree, TreeId, TreeUpdate};
use llimphi_compositor::{Mounted, Role as LRole, SemanticsSpec};
use llimphi_layout::ComputedLayout;
pub const ROOT_NODE_ID: NodeId = NodeId(1);
const MOUNTED_OFFSET: u64 = 1000;
pub fn node_id_for(idx: usize) -> NodeId {
NodeId(MOUNTED_OFFSET + idx as u64)
}
pub fn mounted_idx_for(id: NodeId) -> Option<usize> {
let v = id.0;
if v >= MOUNTED_OFFSET {
Some((v - MOUNTED_OFFSET) as usize)
} else {
None
}
}
pub fn build_tree<Msg>(
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
focused_idx: Option<usize>,
app_name: &str,
tree_id: TreeId,
) -> TreeUpdate {
let mut nodes: Vec<(NodeId, Node)> = Vec::with_capacity(mounted.nodes.len() + 1);
let mut root = Node::new(AkRole::Window);
root.set_label(app_name.to_string());
if !mounted.nodes.is_empty() {
root.set_children(vec![node_id_for(0)]);
}
nodes.push((ROOT_NODE_ID, root));
for (idx, mn) in mounted.nodes.iter().enumerate() {
let mut node = Node::new(map_role(&mn.semantics, mn));
if let Some(r) = computed.get(mn.id) {
node.set_bounds(AkRect {
x0: r.x as f64,
y0: r.y as f64,
x1: (r.x + r.w) as f64,
y1: (r.y + r.h) as f64,
});
}
if let Some(spec) = &mn.semantics {
apply_semantics(&mut node, spec);
if spec.label.is_none() {
if let Some(t) = &mn.text {
node.set_label(t.content.clone());
}
}
} else if let Some(t) = &mn.text {
node.set_label(t.content.clone());
}
if mn.focusable.is_some() {
node.add_action(Action::Focus);
}
if mn.on_click.is_some() || mn.on_click_at.is_some() {
node.add_action(Action::Click);
}
let children = direct_children(mounted, idx);
if !children.is_empty() {
node.set_children(children.into_iter().map(node_id_for).collect::<Vec<_>>());
}
nodes.push((node_id_for(idx), node));
}
let focus = focused_idx
.map(node_id_for)
.unwrap_or(ROOT_NODE_ID);
TreeUpdate {
nodes,
tree: Some(Tree::new(ROOT_NODE_ID)),
focus,
tree_id,
}
}
fn direct_children<Msg>(mounted: &Mounted<Msg>, parent_idx: usize) -> Vec<usize> {
let parent = &mounted.nodes[parent_idx];
let mut out = Vec::new();
let mut cursor = parent_idx + 1;
while cursor < parent.subtree_end {
out.push(cursor);
cursor = mounted.nodes[cursor].subtree_end;
}
out
}
fn apply_semantics(node: &mut Node, spec: &SemanticsSpec) {
if let Some(label) = &spec.label {
node.set_label(label.to_string());
}
if let Some(desc) = &spec.description {
node.set_description(desc.to_string());
}
if let Some(value) = &spec.value {
node.set_value(value.to_string());
}
if let Some(checked) = spec.flags.checked.or(spec.flags.pressed) {
node.set_toggled(if checked {
accesskit::Toggled::True
} else {
accesskit::Toggled::False
});
}
if let Some(expanded) = spec.flags.expanded {
node.set_expanded(expanded);
}
if spec.flags.disabled == Some(true) {
node.set_disabled();
}
if spec.flags.readonly == Some(true) {
node.set_read_only();
}
if spec.flags.required == Some(true) {
node.set_required();
}
}
fn map_role<Msg>(spec: &Option<SemanticsSpec>, _mn: &llimphi_compositor::MountedNode<Msg>) -> AkRole {
let Some(role) = spec.as_ref().and_then(|s| s.role) else {
return AkRole::GenericContainer;
};
match role {
LRole::Button => AkRole::Button,
LRole::TextInput => AkRole::TextInput,
LRole::Heading => AkRole::Heading,
LRole::Checkbox => AkRole::CheckBox,
LRole::Label => AkRole::Label,
LRole::Link => AkRole::Link,
LRole::MenuItem => AkRole::MenuItem,
LRole::Tab => AkRole::Tab,
LRole::Image => AkRole::Image,
LRole::Slider => AkRole::Slider,
LRole::Group => AkRole::Group,
}
}
#[cfg(test)]
mod tests {
use super::*;
use llimphi_compositor::{mount, Role as LRole, View};
use llimphi_layout::taffy::prelude::length;
use llimphi_layout::taffy::Size;
use llimphi_layout::{LayoutTree, Style};
fn arbol_simple() -> (llimphi_compositor::Mounted<()>, ComputedLayout) {
let boton = View::<()>::new(Style {
size: Size { width: length(80.0_f32), height: length(40.0_f32) },
..Default::default()
})
.role(LRole::Button)
.aria_label("Guardar");
let texto = View::<()>::new(Style::default()).text(
"Hola",
14.0,
llimphi_raster::peniko::Color::WHITE,
);
let raiz = View::<()>::new(Style::default()).children(vec![boton, texto]);
let mut layout = LayoutTree::new();
let mounted = mount(&mut layout, raiz);
let computed = layout
.compute(mounted.root, (1000.0_f32, 1000.0_f32))
.expect("layout");
(mounted, computed)
}
#[test]
fn build_tree_arma_raiz_y_un_node_por_mounted() {
let (m, c) = arbol_simple();
let tree = build_tree(&m, &c, None, "tawasuyu-test", TreeId(uuid::Uuid::nil()));
assert_eq!(tree.nodes.len(), 1 + m.nodes.len());
assert_eq!(tree.nodes[0].0, ROOT_NODE_ID);
assert_eq!(tree.nodes[1].0, node_id_for(0));
assert_eq!(tree.focus, ROOT_NODE_ID);
}
#[test]
fn boton_con_label_se_traduce_a_role_button() {
let (m, c) = arbol_simple();
let tree = build_tree(&m, &c, None, "test", TreeId(uuid::Uuid::nil()));
let boton_node = tree
.nodes
.iter()
.find(|(_, n)| n.role() == AkRole::Button)
.expect("hay un Button");
assert_eq!(boton_node.1.label().as_deref(), Some("Guardar"));
}
#[test]
fn texto_sin_semantica_se_lee_como_label_del_node_generico() {
let (m, c) = arbol_simple();
let tree = build_tree(&m, &c, None, "test", TreeId(uuid::Uuid::nil()));
assert!(
tree.nodes
.iter()
.any(|(_, n)| n.label().as_deref() == Some("Hola")),
"el texto plano debería aparecer como label"
);
}
#[test]
fn foco_explicito_se_refleja_en_treeupdate_focus() {
let (m, c) = arbol_simple();
let tree = build_tree(&m, &c, Some(1), "test", TreeId(uuid::Uuid::nil()));
assert_eq!(tree.focus, node_id_for(1));
}
#[test]
fn mounted_idx_for_invierte_node_id_for() {
for i in 0..16 {
let nid = node_id_for(i);
assert_eq!(mounted_idx_for(nid), Some(i));
}
assert_eq!(mounted_idx_for(ROOT_NODE_ID), None);
}
}