tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
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}