tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main llimphi-ui/src/a11y.rs Rust · 524 lines · 22.1 KB History Who touched what raw

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · Sergio ·

1//! Traducción del árbol Llimphi a un árbol [AccessKit](https://accesskit.dev)
2//! para alimentar lectores de pantalla y otras tecnologías de asistencia.
3//!
4//! Cada frame el runtime llama a [`build_tree`] con el árbol montado +
5//! `ComputedLayout` + el id de foco actual. La función produce un
6//! `accesskit::TreeUpdate` que el adapter (`accesskit_winit::Adapter`) empuja
7//! al sistema operativo.
8//!
9//! ## Mapeo de identidades
10//!
11//! Cada `MountedNode` recibe un `NodeId(idx + ROOT_OFFSET)` derivado de su
12//! índice en `Mounted::nodes` — estable dentro de un frame, no necesariamente
13//! entre frames (si la app re-renderiza un árbol distinto, los ids cambian).
14//! `ROOT_NODE_ID` queda reservado para el nodo raíz sintético que envuelve
15//! todo el árbol. Los nodos sin semántica declarada igual aparecen en el árbol
16//! si contienen texto, son `focusable` o tienen `on_click` — los lectores los
17//! anuncian aunque el caller no haya marcado un rol explícito.
18//!
19//! ## Acciones soportadas
20//!
21//! - `Action::Focus`: mueve el foco de Llimphi a ese nodo (vía
22//!   [`crate::App::on_focus`]).
23//! - `Action::Click` / `Default`: activa el nodo que resuelve [`click_target`]
24//!   — el propio si tiene handler, o su **único** descendiente clickeable si el
25//!   nodo declara un rol (el caso de las filas de árbol/tabla, donde el rol vive
26//!   en la fila y el `on_click` en el nodo del label).
27//!
28//!   Los handlers `on_click_at` **sí** se atienden: la posición sintética es el
29//!   centro del rect del nodo en el layout del frame — lo que produce un click
30//!   "en el medio del control", misma convención que
31//!   `llimphi_test::query::NodeRef::click`.
32
33use accesskit::{Action, Node, NodeId, Rect as AkRect, Role as AkRole, Tree, TreeId, TreeUpdate};
34use llimphi_compositor::{Mounted, Role as LRole, SemanticsSpec};
35use llimphi_layout::ComputedLayout;
36
37/// NodeId reservado para la raíz sintética del árbol. El nodo App es siempre
38/// el padre lógico de todos los `MountedNode` que producimos.
39pub const ROOT_NODE_ID: NodeId = NodeId(1);
40
41/// Offset desde el cual numeramos los `MountedNode`. Deja el rango [0, OFFSET)
42/// para ids reservados (root y futuros nodos sintéticos como overlays).
43const MOUNTED_OFFSET: u64 = 1000;
44
45/// `NodeId` AccessKit asignado al `MountedNode` con índice `idx`. La función
46/// es inversa de [`mounted_idx_for`].
47pub fn node_id_for(idx: usize) -> NodeId {
48    NodeId(MOUNTED_OFFSET + idx as u64)
49}
50
51/// Recupera el índice del `MountedNode` que corresponde a un `NodeId`
52/// AccessKit, o `None` si el id está fuera del rango de nodos montados.
53pub fn mounted_idx_for(id: NodeId) -> Option<usize> {
54    let v = id.0;
55    if v >= MOUNTED_OFFSET {
56        Some((v - MOUNTED_OFFSET) as usize)
57    } else {
58        None
59    }
60}
61
62/// Construye el árbol AccessKit completo para el frame actual.
63///
64/// `focused_idx` es el índice del `MountedNode` enfocado (si lo hay) —
65/// resolvelo desde `state.focused: Option<u64>` mapeando contra el campo
66/// `focusable` de cada MountedNode.
67pub fn build_tree<Msg>(
68    mounted: &Mounted<Msg>,
69    computed: &ComputedLayout,
70    focused_idx: Option<usize>,
71    app_name: &str,
72    tree_id: TreeId,
73) -> TreeUpdate {
74    let mut nodes: Vec<(NodeId, Node)> = Vec::with_capacity(mounted.nodes.len() + 1);
75
76    // 1) Raíz sintética: lista los hijos top-level (los nodos cuya posición en
77    // el array es 0 o queda fuera de cualquier subárbol previo). En la práctica
78    // el `MountedNode` con índice 0 es la raíz del View — la usamos directo.
79    let mut root = Node::new(AkRole::Window);
80    root.set_label(app_name.to_string());
81    if !mounted.nodes.is_empty() {
82        root.set_children(vec![node_id_for(0)]);
83    }
84    nodes.push((ROOT_NODE_ID, root));
85
86    // 2) Un Node AccessKit por cada MountedNode. Hijos = los hijos directos en
87    // el árbol pre-orden (subtree_end nos da el rango).
88    for (idx, mn) in mounted.nodes.iter().enumerate() {
89        let mut node = Node::new(map_role(&mn.semantics, mn));
90
91        // Bounds del nodo (rect absoluto del layout). Sin esto el lector no
92        // sabe dónde está visualmente y la navegación por reading order
93        // degrada a "como vinieron".
94        if let Some(r) = computed.get(mn.id) {
95            node.set_bounds(AkRect {
96                x0: r.x as f64,
97                y0: r.y as f64,
98                x1: (r.x + r.w) as f64,
99                y1: (r.y + r.h) as f64,
100            });
101        }
102
103        // Label / value / description. Si la app declaró semantics, mandamos
104        // esos. Si no, intentamos derivar un label del texto visible — los
105        // lectores leen igualmente texto sin rol, pero un label explícito es
106        // más claro.
107        if let Some(spec) = &mn.semantics {
108            apply_semantics(&mut node, spec);
109            // Si declaró rol pero no label y hay texto plano en el nodo,
110            // caemos al texto: cubre widgets como `app-header` que setean
111            // `.role(Heading)` sobre un nodo con `text_aligned("Título", …)`
112            // sin duplicar el string en `.aria_label(...)`.
113            if spec.label.is_none() {
114                if let Some(t) = &mn.text {
115                    node.set_label(t.content.clone());
116                } else if let Some(tip) = &mn.tooltip {
117                    // **El tooltip de un botón de sólo ícono ES su nombre.**
118                    // Un «✕» o un «⋯» no tiene texto del que sacar un rótulo, y
119                    // el único que dice qué hace es el tooltip — que además ya
120                    // está escrito, traducido y al día. Sin esta caída había que
121                    // repetir el mismo string en `.aria_label(...)` al lado del
122                    // `.tooltip(...)`, y dos copias de un rótulo se desincronizan.
123                    // Es la misma regla que el `title` de HTML en accname.
124                    //
125                    // Sólo cae acá el nodo que **declaró rol** y no tiene texto
126                    // propio: nunca pisa un `aria_label` explícito.
127                    node.set_label(tip.clone());
128                }
129            }
130        } else if let Some(t) = &mn.text {
131            node.set_label(t.content.clone());
132        }
133
134        // Acciones: declaramos las que el adapter va a recibir y ejecutar.
135        // `Focus` para cualquier nodo enfocable; `Click` para cualquier nodo
136        // con `on_click`. El handler del runtime las despacha en `act` (ver
137        // eventloop.rs).
138        if mn.focusable.is_some() {
139            node.add_action(Action::Focus);
140        }
141        if click_target(mounted, idx).is_some() {
142            node.add_action(Action::Click);
143        }
144
145        // Hijos: rango [idx+1, subtree_end) — pero aquí necesitamos sólo los
146        // hijos DIRECTOS, no descendientes. Los hijos directos son los nodos
147        // cuyo padre es este: en el orden pre-orden con `subtree_end`, los
148        // hijos directos del nodo idx son los nodos h tales que h.parent == idx.
149        // Lo computamos: empezamos desde idx+1 y saltamos por subtree_end de
150        // cada hijo, hasta salir del rango.
151        let children = direct_children(mounted, idx);
152        if !children.is_empty() {
153            node.set_children(children.into_iter().map(node_id_for).collect::<Vec<_>>());
154        }
155
156        nodes.push((node_id_for(idx), node));
157    }
158
159    let focus = focused_idx
160        .map(node_id_for)
161        .unwrap_or(ROOT_NODE_ID);
162
163    TreeUpdate {
164        nodes,
165        tree: Some(Tree::new(ROOT_NODE_ID)),
166        focus,
167        tree_id,
168    }
169}
170
171/// Nodo que efectivamente ejecuta la activación (`Action::Click`) pedida sobre
172/// el nodo `idx`, o `None` si no hay ninguno.
173///
174/// Resuelve el desajuste estructural entre semántica e interacción: los widgets
175/// ponen el rol en la **fila** (`TreeItem`, `Row`, `ListItem`) porque es la
176/// unidad que el lector navega, pero el `on_click` suele vivir en un **hijo**
177/// (el nodo del label). Sin esto, un usuario de lector podía leer la fila y no
178/// activarla — la limitación que `a11y` documentaba desde v1.
179///
180/// Reglas, en orden:
181///
182/// 1. Si el nodo mismo tiene handler, es él.
183/// 2. Si no, y **declara un rol**, se busca en su subárbol: si hay **exactamente
184///    un** descendiente clickeable *sin rol propio*, la activación se delega
185///    ahí.
186/// 3. Si hay varios, `None` — la elección sería arbitraria, y esos hijos ya
187///    están en el árbol AccessKit como nodos propios para que el usuario los
188///    active directamente.
189///
190/// El requisito de rol en (2) es lo que evita que cada caja de layout se vuelva
191/// clickeable para el lector: sólo los nodos que alguien declaró como algo
192/// heredan la acción de su hijo.
193///
194/// **Por qué un descendiente CON rol no compite** (2026-08-29): una fila de
195/// árbol con hijos lleva dos clickeables —el chevron y el label— y con la regla
196/// vieja eso la volvía ambigua, así que la fila se anunciaba y no se podía
197/// activar. Pero el chevron declara su propio rol: ya es un nodo aparte que el
198/// lector alcanza y activa por su cuenta, así que no hay ninguna elección
199/// arbitraria que hacer — el único candidato a heredar la acción de la fila es
200/// el que no tiene identidad propia. Lo cazó el auditor de nodos mudos
201/// (`02_ruway/llimphi/auditor_mudos.rs`): con la regla vieja, las 16 carpetas
202/// del árbol de `nada` salían inalcanzables.
203pub fn click_target<Msg>(mounted: &Mounted<Msg>, idx: usize) -> Option<usize> {
204    let n = mounted.nodes.get(idx)?;
205    let tiene_handler =
206        |m: &llimphi_compositor::MountedNode<Msg>| m.on_click.is_some() || m.on_click_at.is_some();
207    if tiene_handler(n) {
208        return Some(idx);
209    }
210    n.semantics.as_ref().and_then(|s| s.role)?;
211    let mut encontrado = None;
212    for j in (idx + 1)..n.subtree_end.min(mounted.nodes.len()) {
213        let h = &mounted.nodes[j];
214        // Un descendiente que declara su propio rol ya es un nodo activable del
215        // árbol: no compite por la activación de su ancestro (ver la nota de
216        // arriba sobre el chevron del árbol).
217        if h.semantics.as_ref().and_then(|s| s.role).is_some() {
218            continue;
219        }
220        if tiene_handler(h) {
221            if encontrado.is_some() {
222                return None; // ambiguo: que el lector active el hijo concreto
223            }
224            encontrado = Some(j);
225        }
226    }
227    encontrado
228}
229
230/// Índices de los hijos directos del MountedNode `parent_idx`. Asume el
231/// recorrido pre-orden estándar del `mount`: el primer hijo está en
232/// `parent_idx + 1`; los siguientes se obtienen saltando por `subtree_end`.
233fn direct_children<Msg>(mounted: &Mounted<Msg>, parent_idx: usize) -> Vec<usize> {
234    let parent = &mounted.nodes[parent_idx];
235    let mut out = Vec::new();
236    let mut cursor = parent_idx + 1;
237    while cursor < parent.subtree_end {
238        out.push(cursor);
239        cursor = mounted.nodes[cursor].subtree_end;
240    }
241    out
242}
243
244/// Aplica los campos de un `SemanticsSpec` sobre un `accesskit::Node` recién
245/// creado (rol ya fijado). Mapea flags ARIA → setters AccessKit.
246fn apply_semantics(node: &mut Node, spec: &SemanticsSpec) {
247    if let Some(label) = &spec.label {
248        node.set_label(label.to_string());
249    }
250    if let Some(desc) = &spec.description {
251        node.set_description(desc.to_string());
252    }
253    if let Some(value) = &spec.value {
254        node.set_value(value.to_string());
255    }
256    // Flags. AccessKit usa `toggled` para checked/pressed (mismo enum); para
257    // expanded hay `set_expanded(bool)`; disabled = is_disabled flag.
258    if let Some(checked) = spec.flags.checked.or(spec.flags.pressed) {
259        node.set_toggled(if checked {
260            accesskit::Toggled::True
261        } else {
262            accesskit::Toggled::False
263        });
264    }
265    if let Some(expanded) = spec.flags.expanded {
266        node.set_expanded(expanded);
267    }
268    if spec.flags.disabled == Some(true) {
269        node.set_disabled();
270    }
271    if spec.flags.readonly == Some(true) {
272        node.set_read_only();
273    }
274    if spec.flags.required == Some(true) {
275        node.set_required();
276    }
277}
278
279/// Mapea un `Role` de Llimphi a un `accesskit::Role`. Para nodos sin
280/// `semantics` declarado, fallback a `Role::GenericContainer` (un grupo
281/// transparente que no aporta semántica propia pero permite que la jerarquía
282/// se navegue).
283fn map_role<Msg>(spec: &Option<SemanticsSpec>, _mn: &llimphi_compositor::MountedNode<Msg>) -> AkRole {
284    let Some(role) = spec.as_ref().and_then(|s| s.role) else {
285        return AkRole::GenericContainer;
286    };
287    match role {
288        LRole::Button => AkRole::Button,
289        LRole::TextInput => AkRole::TextInput,
290        LRole::Heading => AkRole::Heading,
291        LRole::Checkbox => AkRole::CheckBox,
292        LRole::Label => AkRole::Label,
293        LRole::Link => AkRole::Link,
294        LRole::MenuItem => AkRole::MenuItem,
295        LRole::Tab => AkRole::Tab,
296        LRole::Image => AkRole::Image,
297        LRole::Slider => AkRole::Slider,
298        LRole::Group => AkRole::Group,
299        LRole::TabList => AkRole::TabList,
300        LRole::Toolbar => AkRole::Toolbar,
301        LRole::MenuBar => AkRole::MenuBar,
302        LRole::Menu => AkRole::Menu,
303        LRole::List => AkRole::List,
304        LRole::ListItem => AkRole::ListItem,
305        LRole::Tree => AkRole::Tree,
306        LRole::TreeItem => AkRole::TreeItem,
307        LRole::Table => AkRole::Table,
308        LRole::Row => AkRole::Row,
309        LRole::Cell => AkRole::Cell,
310        LRole::ColumnHeader => AkRole::ColumnHeader,
311        LRole::ProgressBar => AkRole::ProgressIndicator,
312        LRole::Alert => AkRole::Alert,
313        LRole::Status => AkRole::Status,
314        LRole::Tooltip => AkRole::Tooltip,
315        LRole::Switch => AkRole::Switch,
316        LRole::Dialog => AkRole::Dialog,
317        LRole::MultilineTextInput => AkRole::MultilineTextInput,
318        LRole::Terminal => AkRole::Terminal,
319        LRole::Splitter => AkRole::Splitter,
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use llimphi_compositor::{mount, Role as LRole, View};
327    use llimphi_layout::taffy::prelude::length;
328    use llimphi_layout::taffy::Size;
329    use llimphi_layout::{LayoutTree, Style};
330
331    /// Monta un árbol con un nodo botón + un texto plano. Devuelve mounted +
332    /// computed contra un viewport razonable.
333    fn arbol_simple() -> (llimphi_compositor::Mounted<()>, ComputedLayout) {
334        let boton = View::<()>::new(Style {
335            size: Size { width: length(80.0_f32), height: length(40.0_f32) },
336            ..Default::default()
337        })
338        .role(LRole::Button)
339        .aria_label("Guardar");
340        let texto = View::<()>::new(Style::default()).text(
341            "Hola",
342            14.0,
343            llimphi_raster::peniko::Color::WHITE,
344        );
345        let raiz = View::<()>::new(Style::default()).children(vec![boton, texto]);
346        let mut layout = LayoutTree::new();
347        let mounted = mount(&mut layout, raiz);
348        let computed = layout
349            .compute(mounted.root, (1000.0_f32, 1000.0_f32))
350            .expect("layout");
351        (mounted, computed)
352    }
353
354    #[test]
355    fn build_tree_arma_raiz_y_un_node_por_mounted() {
356        let (m, c) = arbol_simple();
357        let tree = build_tree(&m, &c, None, "tawasuyu-test", TreeId::ROOT);
358        // root + 3 nodos (raíz View, boton, texto).
359        assert_eq!(tree.nodes.len(), 1 + m.nodes.len());
360        assert_eq!(tree.nodes[0].0, ROOT_NODE_ID);
361        // El segundo Node es el primer MountedNode (raíz del View).
362        assert_eq!(tree.nodes[1].0, node_id_for(0));
363        // Foco fallback = root sintético.
364        assert_eq!(tree.focus, ROOT_NODE_ID);
365    }
366
367    #[test]
368    fn boton_con_label_se_traduce_a_role_button() {
369        let (m, c) = arbol_simple();
370        let tree = build_tree(&m, &c, None, "test", TreeId::ROOT);
371        // El nodo con role=Button debería tener rol Button en accesskit.
372        let boton_node = tree
373            .nodes
374            .iter()
375            .find(|(_, n)| n.role() == AkRole::Button)
376            .expect("hay un Button");
377        assert_eq!(boton_node.1.label().as_deref(), Some("Guardar"));
378    }
379
380    #[test]
381    fn texto_sin_semantica_se_lee_como_label_del_node_generico() {
382        let (m, c) = arbol_simple();
383        let tree = build_tree(&m, &c, None, "test", TreeId::ROOT);
384        // Algún nodo con label "Hola" (el texto plano).
385        assert!(
386            tree.nodes
387                .iter()
388                .any(|(_, n)| n.label().as_deref() == Some("Hola")),
389            "el texto plano debería aparecer como label"
390        );
391    }
392
393    #[test]
394    fn foco_explicito_se_refleja_en_treeupdate_focus() {
395        let (m, c) = arbol_simple();
396        let tree = build_tree(&m, &c, Some(1), "test", TreeId::ROOT);
397        assert_eq!(tree.focus, node_id_for(1));
398    }
399
400    #[test]
401    fn mounted_idx_for_invierte_node_id_for() {
402        for i in 0..16 {
403            let nid = node_id_for(i);
404            assert_eq!(mounted_idx_for(nid), Some(i));
405        }
406        assert_eq!(mounted_idx_for(ROOT_NODE_ID), None);
407    }
408
409    // ── click_target: activación cuando el rol y el handler no coinciden ──
410
411    /// Fila con rol (como un `TreeItem`) cuyo `on_click` vive en el hijo del
412    /// label. Es la forma real que producen tree/list/detail-table.
413    fn fila_con_click_en_el_hijo() -> llimphi_compositor::Mounted<()> {
414        let label = View::<()>::new(Style::default())
415            .text("main.rs", 12.0, llimphi_raster::peniko::Color::WHITE)
416            .on_click(());
417        let fila = View::<()>::new(Style::default())
418            .role(LRole::TreeItem)
419            .aria_label("main.rs")
420            .children(vec![label]);
421        let raiz = View::<()>::new(Style::default()).children(vec![fila]);
422        let mut layout = LayoutTree::new();
423        mount(&mut layout, raiz)
424    }
425
426    #[test]
427    fn click_target_delega_al_unico_hijo_clickeable() {
428        let m = fila_con_click_en_el_hijo();
429        // 0 = raíz, 1 = fila (TreeItem), 2 = label (el que tiene on_click).
430        assert_eq!(click_target(&m, 1), Some(2), "la fila debe delegar en su label");
431        assert_eq!(click_target(&m, 2), Some(2), "el que tiene handler es él mismo");
432    }
433
434    #[test]
435    fn click_target_no_delega_sin_rol_declarado() {
436        let m = fila_con_click_en_el_hijo();
437        // La raíz no declara rol: no hereda el click de su descendiente, si no
438        // toda caja de layout sería activable desde el lector.
439        assert_eq!(click_target(&m, 0), None);
440    }
441
442    #[test]
443    fn click_target_es_none_si_hay_varios_candidatos() {
444        let a = View::<()>::new(Style::default()).on_click(());
445        let b = View::<()>::new(Style::default()).on_click(());
446        let grupo = View::<()>::new(Style::default())
447            .role(LRole::Group)
448            .children(vec![a, b]);
449        let mut layout = LayoutTree::new();
450        let m = mount(&mut layout, grupo);
451        // Ambiguo → que el usuario active el hijo concreto, que ya está en el
452        // árbol como nodo propio.
453        assert_eq!(click_target(&m, 0), None);
454    }
455
456    #[test]
457    fn un_hijo_con_rol_propio_no_vuelve_ambigua_la_fila() {
458        // La forma exacta de una fila de árbol con hijos: chevron (botón con su
459        // rol) + label (el que lleva la acción de la fila). El chevron ya es
460        // activable por su cuenta, así que no compite: la fila delega en el
461        // label. Sin esto, TODA carpeta de un árbol se anuncia y no se activa.
462        let chevron = View::<()>::new(Style::default())
463            .role(LRole::Button)
464            .aria_label("Desplegar")
465            .on_click(());
466        let label = View::<()>::new(Style::default()).on_click(());
467        let fila = View::<()>::new(Style::default())
468            .role(LRole::TreeItem)
469            .aria_label("00_unanchay/")
470            .children(vec![chevron, label]);
471        let mut layout = LayoutTree::new();
472        let m = mount(&mut layout, fila);
473        assert_eq!(click_target(&m, 0), Some(2), "la fila delega en el label, no en el chevron");
474        assert_eq!(click_target(&m, 1), Some(1), "y el chevron sigue siendo él mismo");
475    }
476
477    #[test]
478    fn el_treeitem_gana_accion_click_en_el_arbol_accesskit() {
479        let m = fila_con_click_en_el_hijo();
480        let mut layout = LayoutTree::new();
481        let _ = &mut layout;
482        let computed = ComputedLayout::default();
483        let tree = build_tree(&m, &computed, None, "test", TreeId::ROOT);
484        let fila = tree
485            .nodes
486            .iter()
487            .find(|(_, n)| n.role() == AkRole::TreeItem)
488            .expect("hay un TreeItem");
489        assert!(
490            fila.1.supports_action(Action::Click),
491            "la fila debe ofrecer Click aunque el handler esté en su hijo"
492        );
493    }
494
495    /// **El oráculo que faltaba.** Los tests de arriba miran el `TreeUpdate`
496    /// que armamos; éste se lo da a `accesskit_consumer::Tree`, que es
497    /// literalmente lo que corre del otro lado del adapter cuando un lector de
498    /// pantalla se activa. Sin esto, el árbol podía estar perfecto por dentro y
499    /// matar la app al publicarse: el runtime pasaba un `TreeId` v4 y el
500    /// consumer paniquea con cualquiera que no sea `ROOT`. Los tests no lo
501    /// veían porque acá el id siempre se escribió a mano, y a mano salía nil.
502    #[test]
503    fn el_arbol_publicado_lo_acepta_el_consumidor_de_verdad() {
504        let (m, c) = arbol_simple();
505        let tree = build_tree(&m, &c, None, "tawasuyu-test", TreeId::ROOT);
506        // `Tree::new` paniquea si el update inicial no sirve como raíz. Que no
507        // paniquee ES la aserción — la misma que se rompía en metal.
508        let consumidor = accesskit_consumer::Tree::new(tree, true);
509        assert_eq!(consumidor.state().root().role(), AkRole::Window);
510    }
511
512    /// Y el oráculo MUERDE: con el id que ponía el runtime hasta hoy (un uuid
513    /// por proceso), `Tree::new` paniquea. Sin esta mitad, el test de arriba
514    /// pasaría igual aunque el consumidor no mirara nada.
515    #[test]
516    #[should_panic(expected = "Cannot initialize with a subtree")]
517    fn un_tree_id_que_no_es_root_lo_rechaza_el_consumidor() {
518        let (m, c) = arbol_simple();
519        let ajeno = TreeId(accesskit::Uuid::from_u128(0x7a5a_5555_0000_0000_0000_0000_0000_0001));
520        assert_ne!(ajeno, TreeId::ROOT);
521        let tree = build_tree(&m, &c, None, "tawasuyu-test", ajeno);
522        let _ = accesskit_consumer::Tree::new(tree, true);
523    }
524}