docs/guide/ui-tree
Kernkonzepte · Nodes

UI-Baum und Layout

Bauen Sie zenits räumliches Modell mit Node-Konstruktoren und Layout im Flex-Stil auf.

10 Min. Lesezeit

Mentales Modell

Ihre Mount-Funktion baut einen echten, dauerhaften Baum aus *ui.Nodes – genau einmal. Container bestimmen, wie Kinder angeordnet werden; Blätter tragen Text, Bilder oder Icons. In jedem späteren Frame steigt das Framework nur ab dirty Nodes wieder in die Pipeline ein: Layout, Paint in Display Items, Übergabe an Metal.

FRAME PIPELINE
Jedes Style-Feld hat eine Dirty-Stufe. Eine Hintergrundänderung erzeugt nur den Paint neu; eine Breitenänderung layoutet auch den Parent neu. Bei sauberem Baum und ohne laufende Animation wird die gesamte GPU-Übermittlung übersprungen.

Konstruktoren

KonstruktorVerwendung
ui.boxAllgemeiner Container mit voller Kontrolle über Richtung, Größe, Abstände und Hintergrund. Standard: column, Breite und Höhe fit
ui.hstack / ui.vstackEine Box mit fest auf row / column gesetzter Richtung
ui.text / ui.textFmtStatischer Text / formatierter Text, der Signals abonniert: textFmt(cx, scope, fmt, .{signals}, props)
ui.icon / ui.iconTint / ui.svg / ui.imageVektor-Icons, eingefärbte Icons, SVG und Bitmap-Texturen
ui.gridEin Grid-Container, der auf Spalten- / Zeilen-Tracks auslegt
ui.spacerFlexibler Leerraum, grow in beiden Achsen
ui.clickableHängt einen on_click-Handler an einen beliebigen Node und gibt ihn zurück
ui.boxStyled / hstackStyled / vstackStyled / textStyledNehmen eine benannte Style-Funktion entgegen und spielen sie bei Theme-Wechsel erneut ab – siehe Styling und Themes

Ein Layout-Beispiel

Erstellen Sie den Container und hängen Sie dann Kinder mit appendChild an. Style-Werte kommen zuerst aus cx.tokens, sodass Theme-Wechsel und Abstandsänderungen keine Jagd nach Literalen bedeuten.

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

Kinder lassen sich bei der Konstruktion auch als Tupel übergeben; ui.Padding.symmetric(v, h) setzt vertikales und horizontales Padding getrennt.

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));
Die echte Stack-Story: horizontale, vertikale und verschachtelte Container werden alle über dieselben Regeln für rect, gap, align und sizing aufgelöst.

Größen und Koordinaten

Breite und Höhe sind eine ui.Sizing-Union; jedes ihrer vier Mitglieder hat eine Kurzform:

KurzformMitgliedBedeutung
.fixed(120).{ .px = 120 }Feste Pixel
.fill().{ .grow = .{} }Nimmt den verbleibenden Platz; grow trägt eine min / max-Nutzlast, z. B. .{ .grow = .{ .min = 200 } }
.pct(50).{ .percent = 50 }Prozent (0–100) der Content-Box des Parents, ohne Padding
—.{ .fit = .{} }Durch den Inhalt bestimmt; Standard von box, ebenfalls mit min / max
SIZING
Ändert der Parent seine Größe, bleibt .fixed unverändert, .pct folgt der Content-Box proportional, und .fill nimmt alles, was nach festen Größen, Prozentwerten und Gaps übrig bleibt.

Layout-Ergebnisse werden nicht als Node-Feld gespeichert. node.rectFromWorldOrFallback() liefert das Rect aus dem letzten Layout-Durchlauf, relativ zur linken oberen Ecke des Parents; für Fensterkoordinaten nutzen Sie node.globalRect(), das Positionen, Translate- und Sticky-Offsets entlang der Vorfahrenkette aufsummiert.

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;
Die LayoutBox-Story: Box-Modell, Prozentwerte, absolute Positionierung, Flex / Grid und Clipping, jeweils mit Hilfslinien für Padding-Box und Content-Box.

Nodes aktualisieren

Wenn Sie einen Node direkt ändern, muss das Framework wissen, welcher Teil der Pipeline erneut laufen soll. Bevorzugen Sie setStyle: Es wählt die Dirty-Stufe zur Compile-Zeit anhand des Felds, ein manuelles markLayoutDirty / markRenderDirty entfällt.

StufeTypische FelderNächster Frame
.sizingwidth, heightRelayout von Node und Parent
.layoutpadding, margin, gap, direction, justify, align_items, min/max_*Relayout dieses Nodes
.interactionopacity, translate_*, corner_radius, border, z_index, cursorHit-Index aktualisieren und neu zeichnen
.renderbackground, shadow, gradient, outline, text_colorNur Paint neu erzeugen
.nonetab_index, layout_isolationKeine Frame-Arbeit
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 vergleicht die Signaturen von altem und neuem Text: sizing-dirty, wenn sich die Messung ändert, render-dirty, wenn sich nur das Aussehen ändert, gar nichts bei Gleichheit. Nach einer Textänderung ist kein markRenderDirty nötig.

Um einen Teilbaum vorübergehend auszublenden, rufen Sie node.setDisplay(.none) auf (oder setzen Sie .display = .none in einem BoxStyle): Der Node und sein Teilbaum belegen keinen Platz, zählen keinen Gap und werden nicht gezeichnet; außerdem verlassen sie Hit-Testing, Tab-Reihenfolge und Accessibility-Baum. Nodes und Zustand bleiben erhalten, und .flex holt sie zurück – ohne Unmount und Neuaufbau. setDisplay markiert das Layout des Nodes und seines Parents selbst als dirty. Opacity 0 allein stoppt zwar ebenfalls das Hit-Testing, doch der Node belegt weiterhin Platz und bleibt in der Tab-Reihenfolge.

zenit · DoppellizenzKostenlos für Open-Source-Projekte unter GPL-3.0-only; Closed-Source- oder kommerzielle Produkte benötigen eine kommerzielle Lizenz.Kontakt zum Autor: zongyi.xzy#gmail.com (# durch @ ersetzen)zenit 5f9add5+wip 2026-09-30