UI-Baum und Layout
Bauen Sie zenits räumliches Modell mit Node-Konstruktoren und Layout im Flex-Stil auf.
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.
Konstruktoren
ui.boxAllgemeiner Container mit voller Kontrolle über Richtung, Größe, Abstände und Hintergrund. Standard: column, Breite und Höhe fitui.hstack / ui.vstackEine Box mit fest auf row / column gesetzter Richtungui.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-Texturenui.gridEin Grid-Container, der auf Spalten- / Zeilen-Tracks auslegtui.spacerFlexibler Leerraum, grow in beiden Achsenui.clickableHängt einen on_click-Handler an einen beliebigen Node und gibt ihn zurückui.boxStyled / hstackStyled / vstackStyled / textStyledNehmen eine benannte Style-Funktion entgegen und spielen sie bei Theme-Wechsel erneut ab – siehe Styling und ThemesEin 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.
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.
// 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));Größen und Koordinaten
Breite und Höhe sind eine ui.Sizing-Union; jedes ihrer vier Mitglieder hat eine Kurzform:
.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 / maxLayout-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.
// 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;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.
.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// 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 neithersetText 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.