Projektstruktur
Legen Sie Build-Einstieg, Feature-UI, Styles und Assets dort ab, wo sie wachsen können — und wissen Sie, wem welcher Speicher gehört.
Empfohlene Struktur
zenit schreibt keine Verzeichnisstruktur vor. Halten Sie die Build-Dateien stabil und legen Sie Baum, State und Styles jedes Features nebeneinander, damit Sie für eine Änderung nicht quer durchs Repo springen müssen.
myapp/
├── build.zig
├── build.zig.zon
└── src/
├── main.zig
├── app_state.zig
├── assets.zig
└── features/
└── dashboard/
├── view.zig
├── state.zig
└── styles.zigAufgaben der Dateien
main.zigAllocator, App-Init, Root-MountSeiten-UI anhäufenview.zigNode-Baum und Event-VerdrahtungRohe Farbwerte, aufwendige Datentransformationenstate.zigFachlicher State und seine MethodenLanglebige Zeiger auf Nodes, die entfernt oder neu gebaut werden könnenstyles.zigToken-basierte benannte Style-Funktionen, fn (*const ui.ThemeTokens) ui.BoxStyleIO ausführen oder State ändernStyle-Funktionen sind pure; Builder wie ui.boxStyled / ui.vstackStyled werten sie beim Mounten aus und spielen sie bei einem Theme-Wechsel mit den neuen Tokens erneut ab — Inline-Literal-Styles bekommen das nicht.
// features/dashboard/styles.zig
const ui = @import("ui");
pub fn card(t: *const ui.ThemeTokens) ui.BoxStyle {
return .{
.padding = ui.Padding.all(t.space._6),
.gap = t.space._4,
.background = t.color.bg_secondary,
.corner_radius = t.radius.xl,
};
}
// features/dashboard/view.zig
const S = @import("styles.zig");
const panel = try ui.vstackStyled(cx, S.card, .{});Import-Grenze
Die öffentliche Oberfläche hat zwei Ebenen: Gängige Typen und Builder liegen unter ui.X, fortgeschrittene Funktionen sind nach Thema unter ui.<group>.X gruppiert (ui.widgets, ui.fx, ui.events, ui.theme, …). Alles außerhalb dieser beiden Ebenen ist intern.
const ui = @import("ui");
// Tier 1: what most UI code needs
const Node = ui.Node;
const Signal = ui.Signal;
// Tier 2: grouped by concern
const Router = ui.fx.Router;
const Button = ui.widgets.Button;
const Event = ui.events.Event;Das Build-System erzwingt dies: zenit.attach gibt der App nur die Module ui und zenit_app, interne Module wie render und gpu lassen sich also gar nicht importieren (siehe das Diagramm in der Übersicht).
Besitz und Lebensdauer
Zwei Besitzer, zwei Lebensdauern. Cx lebt auf Fensterebene: Es besitzt den Node-Baum und jedes von cx.bindState allozierte Struct bis Cx.deinit. Scope besitzt reaktive Ressourcen — Signals, Memos, Effects, onCleanup-Callbacks und registrierte Ressourcen — und gibt sie bei dispose() frei; Kind-Scopes gehen mit ihrem Parent.
bindState angelegt haben, nicht frei — sie leben, bis das Fenster schließt. Cx.deinit läuft so ab: Root-Scope disposen → Nodes freigeben → State freigeben.Gebundener State: ein pub deinit deklarieren
Deklariert das Struct ein pub deinit(*T), ruft das Framework es automatisch auf, bevor es das Struct bei Cx.deinit freigibt. Braucht das Aufräumen einen Allocator, speichern Sie ihn als Feld.
const Item = struct { title: []const u8 };
const Model = struct {
allocator: std.mem.Allocator,
items: std.ArrayList(Item) = .empty,
// Must be pub: Cx.deinit detects it and calls it before freeing the struct.
pub fn deinit(self: *Model) void {
self.items.deinit(self.allocator);
}
};
const model = try cx.bindState(Model, .{ .allocator = cx.allocator });
// Do NOT also call scope.onCleanup(Model, model, Model.deinit):
// that would run deinit twice.Ressourcen mit Seiten-Lebensdauer: onCleanup
Greifen Sie nur in zwei Fällen zu scope.onCleanup: Die Aufräumfunktion ist nicht pub, oder die Ressource muss der Lebensdauer eines Scopes (etwa einer Seite) folgen statt der des Fensters. Allozieren Sie solche Objekte selbst statt über bindState.
const PageCache = struct {
allocator: std.mem.Allocator,
rows: std.ArrayList(Item) = .empty,
fn release(self: *PageCache) void {
self.rows.deinit(self.allocator);
self.allocator.destroy(self);
}
};
fn mountDashboard(cx: *ui.Cx, parent: *ui.Scope) !*ui.Node {
const scope = try parent.childScope();
// Page-lifetime resource: freed when this scope is disposed,
// not when the window closes.
const cache = try cx.allocator.create(PageCache);
cache.* = .{ .allocator = cx.allocator };
scope.onCleanup(PageCache, cache, PageCache.release) catch |err| {
cx.allocator.destroy(cache);
return err;
};
return ui.vstack(cx, .{ .gap = cx.tokens.space._4 }, .{});
}Checkliste
- ✓
Halten Sie main.zig kurz. Allocator,
App.initundrunWith— sonst nichts. - ✓
Nur ui und zenit_app importieren. Fehlt etwas auf den beiden öffentlichen Ebenen, eröffnen Sie ein Issue, statt in Interna zu greifen.
- ✓
Fenster-Lebensdauer → bindState, Seiten-Lebensdauer → Scope. Ersteres bekommt ein pub
deinit, Letzteres einonCleanup— nie beides. - ✓
Styles als benannte Funktionen schreiben. Legen Sie sie in styles.zig ab und nutzen Sie sie über die
*Styled-Builder, dann funktionieren Theme-Wechsel einfach.