プロジェクト構成
ビルドのエントリ、機能ごとの UI、スタイル、アセットを拡張しやすい場所に置き、各メモリの所有者を把握します。
推奨構成
zenit はディレクトリ構成を強制しません。ビルドファイルは安定させたまま、機能ごとのツリー、状態、スタイルを同じディレクトリに並べると、1 つの機能を変更するときにリポジトリ内を行き来せずに済みます。
myapp/
├── build.zig
├── build.zig.zon
└── src/
├── main.zig
├── app_state.zig
├── assets.zig
└── features/
└── dashboard/
├── view.zig
├── state.zig
└── styles.zigファイルの役割
main.zigAllocator、App の初期化、ルートのマウント個別ページの UI を詰め込むことview.zigノードツリーとイベントの接続生の色値、重いデータ変換state.zigビジネス状態とそのメソッド削除・再構築されうる Node へのポインターを長く保持することstyles.zigToken 駆動の名前付きスタイル関数 fn (*const ui.ThemeTokens) ui.BoxStyleIO を行うことや状態を変更することスタイル関数は純粋関数で、ui.boxStyled / ui.vstackStyled などのビルダーがマウント時に評価します。テーマが切り替わるとフレームワークが新しいトークンで再適用しますが、インラインのリテラルスタイルは対象外です。
// 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, .{});インポートの境界
公開 API は 2 層です。よく使う型とビルダーは ui.X に、高度な機能は関心ごとに ui.<group>.X(ui.widgets、ui.fx、ui.events、ui.theme など)にまとめられています。この 2 層以外はすべて内部実装です。
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;この境界はビルドシステムが保証します。zenit.attach はアプリに ui と zenit_app の 2 モジュールだけを追加するため、render や gpu などの内部モジュールはそもそもインポートできません(構成図は概要を参照)。
所有権とライフタイム
所有者は 2 つ、寿命も 2 種類です。Cx はウィンドウ単位で、ノードツリーと cx.bindState が確保したすべての struct を Cx.deinit まで所有します。Scope はリアクティブなリソース(Signal、Memo、Effect、onCleanup コールバック、登録されたリソース)を所有し、dispose() で解放します。子 Scope は親と一緒に解放されます。
bindState した struct は解放されません。ウィンドウが閉じるまで生き続けます。Cx.deinit の順序は、ルート Scope の dispose → ノードの解放 → state の解放です。バインドした状態:pub deinit を宣言するだけ
struct が pub な deinit(*T) を宣言していれば、フレームワークは Cx.deinit で struct を解放する前に自動で呼び出します。クリーンアップに allocator が必要なら、フィールドとして保持してください。
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.ページ単位のリソース:onCleanup
scope.onCleanup が必要なのは 2 つの場合だけです。クリーンアップ関数が pub でない場合と、リソースをウィンドウではなく特定の Scope(たとえばページ)の寿命に合わせて解放する必要がある場合です。こうしたオブジェクトは 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 }, .{});
}チェックリスト
- ✓
main.zig は短く保つ。allocator、
App.init、runWithだけにします。 - ✓
インポートは ui と zenit_app だけ。必要な機能が 2 層の公開 API にない場合は、内部に手を伸ばさず issue を立ててください。
- ✓
ウィンドウの寿命には bindState、ページの寿命には Scope。前者には pub な
deinit、後者にはonCleanupを使い、両方を重ねないでください。 - ✓
スタイルは名前付き関数で書く。styles.zig に置き、
*Styledビルダーで使えば、テーマ切り替えがそのまま反映されます。