docs/guide/ui-tree
Concepts clés · Nodes

Arbre d'UI et mise en page

Construisez le modèle spatial de zenit avec les constructeurs de nœuds et une mise en page de style Flex.

10 min de lecture

Modèle mental

Votre fonction de montage construit un vrai arbre persistant de *ui.Node — une seule fois. Les conteneurs décident de la disposition des enfants ; les feuilles portent du texte, des images ou des icônes. À chaque frame suivante, le framework ne réentre dans le pipeline qu’à partir des nœuds sales : layout, peinture en display items, transmission à Metal.

FRAME PIPELINE
Chaque champ de style a un niveau de saleté. Un changement de fond ne régénère que la peinture ; un changement de largeur refait aussi le layout du parent. Avec un arbre propre et aucune animation en cours, toute la soumission GPU est ignorée.

Constructeurs

ConstructeurUsage
ui.boxConteneur générique avec contrôle complet de la direction, de la taille, de l’espacement et du fond. Par défaut : column, largeur et hauteur fit
ui.hstack / ui.vstackUn box dont la direction est fixée à row / column
ui.text / ui.textFmtTexte statique / texte formaté abonné à des Signals : textFmt(cx, scope, fmt, .{signals}, props)
ui.icon / ui.iconTint / ui.svg / ui.imageIcônes vectorielles, icônes teintées, SVG et textures bitmap
ui.gridUn conteneur de grille disposé sur des pistes de colonnes / lignes
ui.spacerEspace vide flexible, grow sur les deux axes
ui.clickableAttache un handler on_click à n’importe quel nœud et le renvoie
ui.boxStyled / hstackStyled / vstackStyled / textStyledPrennent une fonction de style nommée et la rejouent au changement de thème — voir Styles et thèmes

Un exemple de mise en page

Créez le conteneur, puis attachez les enfants avec appendChild. Les valeurs de style viennent d’abord de cx.tokens : changer de thème ou d’espacement ne demande pas de traquer les littéraux.

card.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);

Les enfants peuvent aussi être passés sous forme de tuple à la construction ; ui.Padding.symmetric(v, h) définit séparément le padding vertical et horizontal.

row.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));
La vraie story Stack : conteneurs horizontaux, verticaux et imbriqués se résolvent tous via les mêmes règles de rect, gap, align et sizing.

Dimensions et coordonnées

La largeur et la hauteur sont une union ui.Sizing ; chacun de ses quatre membres a un raccourci :

RaccourciMembreSignification
.fixed(120).{ .px = 120 }Pixels fixes
.fill().{ .grow = .{} }Prend l’espace restant ; grow porte une charge min / max, p. ex. .{ .grow = .{ .min = 200 } }
.pct(50).{ .percent = 50 }Pourcentage (0–100) de la content box du parent, padding exclu
—.{ .fit = .{} }Dimensionné par le contenu ; valeur par défaut de box, aussi avec min / max
SIZING
Quand le parent est redimensionné, .fixed ne bouge pas, .pct suit proportionnellement la content box, et .fill prend tout ce qui reste après les tailles fixes, les pourcentages et les gaps.

Les résultats de layout ne sont pas stockés dans un champ du nœud. node.rectFromWorldOrFallback() renvoie le rect de la dernière passe de layout, relatif au coin supérieur gauche du parent ; pour des coordonnées de fenêtre, utilisez node.globalRect(), qui cumule positions, translate et offsets sticky le long de la chaîne d’ancêtres.

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;
La story LayoutBox : modèle de boîte, pourcentages, positionnement absolu, Flex / Grid et découpage, chacun avec des repères de padding box et de content box.

Mettre à jour les nœuds

Quand vous modifiez un nœud directement, le framework doit savoir quelle partie du pipeline réexécuter. Préférez setStyle : il choisit le niveau de saleté à partir du champ à la compilation, sans markLayoutDirty / markRenderDirty manuel.

NiveauChamps typiquesFrame suivante
.sizingwidth, heightRelayout du nœud et du parent
.layoutpadding, margin, gap, direction, justify, align_items, min/max_*Relayout de ce nœud
.interactionopacity, translate_*, corner_radius, border, z_index, cursorMise à jour de l’index de hit et repeinture
.renderbackground, shadow, gradient, outline, text_colorRégénération de la peinture seule
.nonetab_index, layout_isolationAucun travail de frame
update.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 compare les signatures de l’ancien et du nouveau texte : sizing-dirty si la mesure change, render-dirty si seule l’apparence change, rien du tout s’ils sont identiques. Pas besoin de markRenderDirty après une mise à jour du texte.

Pour masquer un sous-arbre un moment, appelez node.setDisplay(.none) (ou définissez .display = .none dans un BoxStyle) : le nœud et son sous-arbre n’occupent plus d’espace, ne comptent plus de gap et ne sont plus peints, et ils sortent du hit-testing, du parcours Tab et de l’arbre d’accessibilité. Les nœuds et l’état restent vivants, et .flex les fait revenir — sans démontage ni reconstruction. setDisplay marque lui-même comme sale le layout du nœud et de son parent. Une opacity de 0 seule arrête aussi le hit-testing, mais le nœud occupe toujours de l’espace et reste dans l’ordre de Tab.

zenit · Double licenceGratuit pour les projets open source sous GPL-3.0-only ; les produits propriétaires ou commerciaux nécessitent une licence commerciale.Contacter l’auteur : zongyi.xzy#gmail.com (remplacez # par @)zenit 5f9add5+wip 2026-09-30