Structure du projet
Placez le point d’entrée du build, l’UI des fonctionnalités, les styles et les ressources là où ils pourront grandir — et sachez à qui appartient chaque bloc de mémoire.
Structure suggérée
zenit n’impose pas d’arborescence. Gardez les fichiers de build stables et placez côte à côte l’arbre, l’état et les styles de chaque fonctionnalité, pour que modifier une fonctionnalité ne vous oblige pas à parcourir tout le dépôt.
myapp/
├── build.zig
├── build.zig.zon
└── src/
├── main.zig
├── app_state.zig
├── assets.zig
└── features/
└── dashboard/
├── view.zig
├── state.zig
└── styles.zigRôle des fichiers
main.zigAllocator, initialisation de l’App, montage racineAccumuler l’UI des pagesview.zigL’arbre de nœuds et le câblage des événementsCouleurs brutes, transformations de données lourdesstate.zigL’état métier et ses méthodesDes pointeurs durables vers des nœuds susceptibles d’être supprimés ou reconstruitsstyles.zigFonctions de style nommées pilotées par Token, fn (*const ui.ThemeTokens) ui.BoxStyleFaire des IO ou modifier l’étatLes fonctions de style sont pures ; des constructeurs comme ui.boxStyled / ui.vstackStyled les évaluent au montage et les rejouent avec les nouveaux tokens quand le thème change — les styles littéraux en ligne n’en bénéficient pas.
// 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, .{});Frontière d’import
La surface publique comporte deux niveaux : les types et constructeurs courants sont dans ui.X, les fonctionnalités avancées sont regroupées sous ui.<group>.X (ui.widgets, ui.fx, ui.events, ui.theme, …). Tout ce qui sort de ces deux niveaux est interne.
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;Le système de build l’impose : zenit.attach ne fournit à l’application que les modules ui et zenit_app, si bien que les modules internes comme render et gpu ne peuvent tout simplement pas être importés (voir le schéma de la Vue d’ensemble).
Propriété et durées de vie
Deux propriétaires, deux durées de vie. Cx est au niveau de la fenêtre : il possède l’arbre de nœuds et chaque struct allouée par cx.bindState jusqu’à Cx.deinit. Scope possède les ressources réactives — Signals, Memos, Effects, callbacks onCleanup et ressources enregistrées — et les libère lors de dispose() ; les scopes enfants partent avec leur parent.
bindState sur cette page — elles vivent jusqu’à la fermeture de la fenêtre. Cx.deinit exécute : dispose du Scope racine → libération des nœuds → libération de l’état.État lié : déclarez un pub deinit
Si la struct déclare un deinit(*T) pub, le framework l’appelle automatiquement avant de libérer la struct lors de Cx.deinit. Si le nettoyage a besoin d’un allocator, stockez-le dans un champ.
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.Ressources à durée de vie de page : onCleanup
N’utilisez scope.onCleanup que dans deux cas : le nettoyage n’est pas pub, ou la ressource doit suivre la durée de vie d’un Scope (une page, par exemple) plutôt que celle de la fenêtre. Allouez alors ces objets vous-même au lieu de passer par 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 }, .{});
}Liste de contrôle
- ✓
Gardez main.zig court. Allocator,
App.initetrunWith— rien d’autre. - ✓
N’importez que ui et zenit_app. Si ce dont vous avez besoin ne figure pas dans les deux niveaux publics, ouvrez une issue plutôt que d’aller fouiller dans les internes.
- ✓
Durée de vie de fenêtre → bindState, de page → Scope. Donnez au premier un
deinitpub, au second unonCleanup— jamais les deux. - ✓
Écrivez les styles comme des fonctions nommées. Placez-les dans styles.zig et utilisez-les via les constructeurs
*Styledpour que les changements de thème fonctionnent d’office.