Estructura del proyecto
Coloca el punto de entrada de la compilación, la UI de cada funcionalidad, los estilos y los recursos donde puedan crecer, y ten claro quién es dueño de cada bloque de memoria.
Estructura sugerida
zenit no impone una estructura de directorios. Mantén estables los archivos de compilación y coloca juntos el árbol, el estado y los estilos de cada funcionalidad, para que cambiar una no implique saltar por todo el repositorio.
myapp/
├── build.zig
├── build.zig.zon
└── src/
├── main.zig
├── app_state.zig
├── assets.zig
└── features/
└── dashboard/
├── view.zig
├── state.zig
└── styles.zigResponsabilidades de cada archivo
main.zigAllocator, inicialización de App, montaje raízAcumular UI de páginas concretasview.zigEl árbol de nodos y la conexión de eventosColores literales, transformaciones de datos pesadasstate.zigEl estado de negocio y sus métodosPunteros de larga duración a nodos que pueden eliminarse o reconstruirsestyles.zigFunciones de estilo con nombre basadas en Token, fn (*const ui.ThemeTokens) ui.BoxStyleHacer IO o mutar el estadoLas funciones de estilo son puras; constructores como ui.boxStyled / ui.vstackStyled las evalúan al montar y las vuelven a aplicar con los nuevos tokens cuando cambia el tema; los estilos literales en línea no obtienen eso.
// 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, .{});Límite de importación
La superficie pública tiene dos niveles: los tipos y constructores comunes están en ui.X, y las funciones avanzadas se agrupan en ui.<group>.X (ui.widgets, ui.fx, ui.events, ui.theme, …). Todo lo que queda fuera de esos dos niveles es interno.
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;El sistema de compilación lo garantiza: zenit.attach solo da a la app los módulos ui y zenit_app, así que los módulos internos como render y gpu no se pueden importar en absoluto (consulta el diagrama en la Introducción).
Propiedad y ciclos de vida
Dos dueños, dos ciclos de vida. Cx es de nivel de ventana: es dueño del árbol de nodos y de cada struct asignado por cx.bindState hasta Cx.deinit. Scope es dueño de los recursos reactivos — Signals, Memos, Effects, callbacks de onCleanup y recursos registrados — y los libera en dispose(); los scopes hijos se liberan con su padre.
bindState en esa página: viven hasta que se cierra la ventana. Cx.deinit ejecuta: dispose del Scope raíz → liberar nodos → liberar el estado.Estado vinculado: declara un pub deinit
Si el struct declara un pub deinit(*T), el framework lo llama automáticamente antes de liberar el struct en Cx.deinit. Si la limpieza necesita un allocator, guárdalo como campo.
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.Recursos con ciclo de vida de página: onCleanup
Recurre a scope.onCleanup solo en dos casos: la limpieza no es pub, o el recurso debe seguir el ciclo de vida de un Scope (por ejemplo, una página) en lugar del de la ventana. Asigna esos objetos tú mismo en lugar de usar 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 }, .{});
}Lista de verificación
- ✓
Mantén main.zig corto. Allocator,
App.inityrunWith; nada más. - ✓
Importa solo ui y zenit_app. Si algo que necesitas no está en los dos niveles públicos, abre un issue en lugar de meterte en el código interno.
- ✓
Ciclo de vida de ventana → bindState, de página → Scope. Da al primero un pub
deinity al segundo unonCleanup; nunca ambos. - ✓
Escribe los estilos como funciones con nombre. Ponlos en styles.zig y úsalos con los constructores
*Styledpara que los cambios de tema funcionen sin más.