---
title: "Árbol de UI y layout — Docs de zenit Zig UI"
description: "Construye el modelo espacial de zenit con constructores de nodos y layout de estilo Flex."
url: https://zenit.z.express/es/docs/guide/ui-tree
language: es
alternate_en: https://zenit.z.express/docs/guide/ui-tree.md
alternate_zh: https://zenit.z.express/zh/docs/guide/ui-tree.md
alternate_ja: https://zenit.z.express/ja/docs/guide/ui-tree.md
alternate_ko: https://zenit.z.express/ko/docs/guide/ui-tree.md
alternate_fr: https://zenit.z.express/fr/docs/guide/ui-tree.md
alternate_de: https://zenit.z.express/de/docs/guide/ui-tree.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Árbol de UI y layout

Construye el modelo espacial de zenit con constructores de nodos y layout de estilo Flex.

## Modelo mental

Tu función de montaje construye un árbol real y persistente de `*ui.Node`, una sola vez. Los contenedores deciden cómo se disponen los hijos; las hojas llevan texto, imágenes o iconos. En cada frame posterior, el framework vuelve a entrar en la pipeline solo desde los nodos sucios: layout, pintado en display items y entrega a Metal.

FRAME PIPELINE

Cada campo de estilo tiene un nivel de suciedad. Un cambio de fondo solo regenera el pintado; un cambio de ancho vuelve a maquetar también al padre. Con el árbol limpio y sin animaciones en curso, se omite todo el envío a la GPU.

## Constructores

| Constructor | Uso |
| --- | --- |
| `ui.box` | Contenedor genérico con control total de dirección, tamaño, espaciado y fondo. Por defecto: column, ancho y alto fit |
| `ui.hstack / ui.vstack` | Un box con la dirección fijada en row / column |
| `ui.text / ui.textFmt` | Texto estático / texto formateado suscrito a Signals: `textFmt(cx, scope, fmt, .{signals}, props)` |
| `ui.icon / ui.iconTint / ui.svg / ui.image` | Iconos vectoriales, iconos tintados, SVG y texturas de mapa de bits |
| `ui.grid` | Un contenedor de cuadrícula dispuesto en pistas de columnas / filas |
| `ui.spacer` | Espacio en blanco flexible, grow en ambos ejes |
| `ui.clickable` | Asocia un handler `on_click` a cualquier nodo y lo devuelve |
| `ui.boxStyled / hstackStyled / vstackStyled / textStyled` | Reciben una función de estilo con nombre y la reaplican al cambiar de tema; consulta [Estilos y temas](https://zenit.z.express/es/docs/guide/styling) |

> TIP
> 
> **Busca primero un widget.** Los botones, inputs, listas y demás controles de `ui.widgets` ya incluyen estado, foco y semántica de teclado; los constructores sirven para la estructura entre ellos. Explora el [sitio de componentes](https://zenit.z.express/es/components).

## Un ejemplo de layout

Crea el contenedor y luego añade los hijos con `appendChild`. Los valores de estilo salen primero de `cx.tokens`, así que cambiar de tema o ajustar el espaciado no obliga a buscar literales uno por uno.

`card.zig`

```zig
const card = try ui.vstack(cx, .{
    .width = .fixed(360),
    .gap = cx.tokens.space._4,
    .padding = ui.Padding.all(cx.tokens.space._6),
    .background = cx.tokens.color.bg_secondary,
    .corner_radius = cx.tokens.radius.xl,
    .align_items = .stretch,
}, .{});

const header = try ui.hstack(cx, .{
    .gap = cx.tokens.space._2,
    .align_items = .center,
}, .{});

try header.appendChild(cx.allocator, try ui.iconTint(
    cx,
    ui.icons.star,
    cx.tokens.color.accent,
    .{ .width = .fixed(18), .height = .fixed(18) },
));
try header.appendChild(cx.allocator, try ui.text(cx, "Overview", .{}));
try header.appendChild(cx.allocator, try ui.spacer(cx));
try card.appendChild(cx.allocator, header);
```

Los hijos también pueden pasarse como tupla al construir; `ui.Padding.symmetric(v, h)` define el padding vertical y horizontal por separado.

`row.zig`

```zig
// Children can also be passed as a tuple at construction time.
const row = try ui.hstack(cx, .{
    .width = .fill(),
    .padding = ui.Padding.symmetric(cx.tokens.space._2, cx.tokens.space._4),
    .gap = cx.tokens.space._3,
}, .{
    try ui.text(cx, "Name", .{}),
    try ui.spacer(cx),
    try ui.text(cx, "42", .{ .color = cx.tokens.color.fg_secondary }),
});

// Any node can take a click handler.
// (model: *Model from cx.bindState; Model.select is fn (*Model) void)
_ = ui.clickable(row, cx.on(Model, model, Model.select));
```

[Video](https://zenit.z.express/media/stories/stack.mp4?v=f9618b3adf)

La story real de [Stack](https://zenit.z.express/es/components/stack): los contenedores horizontales, verticales y anidados se resuelven con las mismas reglas de rect, gap, align y sizing.

## Tamaño y coordenadas

El ancho y el alto son una unión `ui.Sizing`; cada uno de sus cuatro miembros tiene una forma abreviada:

| Abreviatura | Miembro | Significado |
| --- | --- | --- |
| `.fixed(120)` | `.{ .px = 120 }` | Píxeles fijos |
| `.fill()` | `.{ .grow = .{} }` | Ocupa el espacio restante; `grow` lleva un payload `min` / `max`, p. ej. `.{ .grow = .{ .min = 200 } }` |
| `.pct(50)` | `.{ .percent = 50 }` | Porcentaje (0–100) del content box del padre, sin padding |
| — | `.{ .fit = .{} }` | Dimensionado por el contenido; valor por defecto de box, también con min / max |

SIZING

Al redimensionar el padre, .fixed no se mueve, .pct sigue proporcionalmente al content box y .fill toma todo lo que queda tras los tamaños fijos, los porcentajes y los gaps.

Los resultados del layout no se guardan como campo del nodo. `node.rectFromWorldOrFallback()` devuelve el rect de la última pasada de layout, relativo a la esquina superior izquierda del padre; para coordenadas de ventana usa `node.globalRect()`, que acumula posiciones, translate y offsets sticky a lo largo de la cadena de ancestros.

```zig
// Parent-relative rect from the last layout pass.
const local = node.rectFromWorldOrFallback();

// Window coordinates: accumulates ancestors, translate and sticky offsets.
const screen = node.globalRect();
std.log.info("{d}x{d} at ({d}, {d})", .{ screen.w, screen.h, screen.x, screen.y });
_ = local;
```

[Video](https://zenit.z.express/media/stories/layoutbox.mp4?v=4d3c4ba31b)

La story LayoutBox: modelo de caja, porcentajes, posicionamiento absoluto, Flex / Grid y recorte, cada uno con guías de padding box y content box.

> TIP
> 
> **Revisa primero el padre.** Cuando la alineación o las áreas de clic se ven mal, la causa suele estar en el padding, gap, direction o sizing del padre, no en la hoja. Durante el desarrollo, `ui.devtools.overlay.attach(cx, scope, root, .{})` añade un inspector al pasar el ratón que dibuja el rect de cada nodo; consulta [DevTools](https://zenit.z.express/es/docs/advanced/devtools).

## Actualizar nodos

Cuando cambias un nodo directamente, el framework tiene que saber qué parte de la pipeline volver a ejecutar. Prefiere `setStyle`: elige el nivel de suciedad a partir del campo en tiempo de compilación, así que no hace falta `markLayoutDirty` / `markRenderDirty` manual.

| Nivel | Campos típicos | Siguiente frame |
| --- | --- | --- |
| `.sizing` | `width, height` | Re-layout propio y del padre |
| `.layout` | `padding, margin, gap, direction, justify, align_items, min/max_*` | Re-layout de este nodo |
| `.interaction` | `opacity, translate_*, corner_radius, border, z_index, cursor` | Actualizar el índice de hit y repintar |
| `.render` | `background, shadow, gradient, outline, text_color` | Solo regenerar el pintado |
| `.none` | `tab_index, layout_isolation` | Sin trabajo de frame |

`update.zig`

```zig
// setStyle picks the dirty level from the field at compile time.
node.setStyle(cx.allocator, .width, .fixed(240)); // sizing
node.setStyle(cx.allocator, .gap, 12);             // layout
node.setStyle(cx.allocator, .background, next);    // render

// Low-frequency fields live in StyleExt and need a real allocator.
node.setStyle(cx.allocator, .z_index, 10);

// Replace text: dupes the content, frees the previous owned copy,
// and re-measures only if the content actually changed.
try node.setTextContent(cx.allocator, "Updated");

// Hide without unmounting: display:none leaves layout, paint,
// hit-testing, Tab order and the a11y tree; state stays alive.
panel.setDisplay(.none);
panel.setDisplay(.flex); // back, same nodes

// Recolor an icon node whether it is icon-table or image (svgTint) backed.
_ = icon.setTint(cx.tokens.color.accent); // false: node has neither
```

`setText` compara las firmas del texto antiguo y el nuevo: sizing-dirty si cambia la medición, render-dirty si solo cambia la apariencia, y nada en absoluto si son idénticas. No hace falta `markRenderDirty` tras actualizar el texto.

Para ocultar un subárbol durante un tiempo, llama a `node.setDisplay(.none)` (o pon `.display = .none` en un BoxStyle): el nodo y su subárbol no ocupan espacio, no cuentan gap ni se pintan, y salen del hit-testing, del recorrido con Tab y del árbol de accesibilidad. Los nodos y el estado siguen vivos, y `.flex` los recupera, sin desmontar ni reconstruir. `setDisplay` marca por sí mismo como sucio el layout del nodo y de su padre. Una opacity de 0 por sí sola también detiene el hit-testing, pero el nodo sigue ocupando espacio y permanece en el orden de Tab.

> WARNING
> 
> **Reemplaza el texto con setTextContent.** No hagas `getText()`, apuntes `content` a una cadena temporal y luego llames a `setText`: `setText` no copia el contenido y, si el contenido anterior era owned, el flag `owned` se copia también y se confunde con el nuevo slice. `setTextContent` duplica el nuevo contenido, lo marca como owned y deja que el framework libere la copia anterior. Si construyes `TextProps` tú mismo, usa `props.setContent(cx.allocator, src)`: hasta 16 bytes van al buffer inline sin asignación, y el contenido más largo se duplica como owned. `setInlineContent` devuelve `error.InlineContentTooLong` por encima de 16 bytes en lugar de truncar en silencio.

> NOTE
> 
> **Los campos de baja frecuencia necesitan un allocator.** Campos como `shadow`, `z_index`, `corner_radius` y `min_width` viven en un StyleExt asignado bajo demanda. Pasar un allocator `null` literal para ellos es un error de compilación; si dudas, pasa `cx.allocator`.
