docs/guide/project-structure
Démarrer · Organiser le code

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.

6 min de lecture

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/
myapp/
├── build.zig
├── build.zig.zon
└── src/
    ├── main.zig
    ├── app_state.zig
    ├── assets.zig
    └── features/
        └── dashboard/
            ├── view.zig
            ├── state.zig
            └── styles.zig

Rôle des fichiers

FichierContientÀ éviter
main.zigAllocator, initialisation de l’App, montage racineAccumuler l’UI des pages
view.zigL’arbre de nœuds et le câblage des événementsCouleurs brutes, transformations de données lourdes
state.zigL’état métier et ses méthodesDes pointeurs durables vers des nœuds susceptibles d’être supprimés ou reconstruits
styles.zigFonctions de style nommées pilotées par Token, fn (*const ui.ThemeTokens) ui.BoxStyleFaire des IO ou modifier l’état

Les 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.

styles.zig + view.zig
// 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.

zig
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.

OWNERSHIP
Disposer un Scope de page ne libère pas les structs que vous avez créées avec 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.

state.zig
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.

features/dashboard/view.zig
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.init et runWith — 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 deinit pub, au second un onCleanup — jamais les deux.

  • ✓

    Écrivez les styles comme des fonctions nommées. Placez-les dans styles.zig et utilisez-les via les constructeurs *Styled pour que les changements de thème fonctionnent d’office.

zenit · Double licenceGratuit pour les projets open source sous GPL-3.0-only ; les produits propriétaires ou commerciaux nécessitent une licence commerciale.Contacter l’auteur : zongyi.xzy#gmail.com (remplacez # par @)zenit 5f9add5+wip 2026-09-30