状態とイベント
フレームをまたいで保持すべき状態は Cx に管理させ、ユーザー入力には型安全な handler で応答します。
バインドされた状態
cx.bindState(T, init) は Cx の状態ストアに T を作成し、フレームをまたいで有効な *T を返します。続いて cx.on がそのメソッドを、どのコンポーネントでも受け取れる HandlerRef に変換します。状態 ID を手作業で割り当てる必要はありません。
const Counter = struct {
allocator: std.mem.Allocator,
value: u32 = 0,
label: ?*ui.Node = null,
buffer: [32]u8 = undefined,
fn increment(self: *Counter) void {
self.value += 1;
const label = self.label orelse return;
const text = std.fmt.bufPrint(
&self.buffer,
"Clicked {d} times",
.{self.value},
) catch return;
// Copies the text, marks it owned, frees the previous owned copy,
// and marks the node dirty only if the text actually changed.
label.setTextContent(self.allocator, text) catch return;
}
};
const counter = try cx.bindState(Counter, .{ .allocator = cx.allocator });
const on_click = cx.on(Counter, counter, Counter.increment);コールバック内でテキストを差し替えるには node.setTextContent(allocator, text) を使います。内容をコピーして owned としてマークし、以前の owned な内容を解放します。テキストが実際に変わったときだけ dirty にするので、手動の markRenderDirty は不要です。そのため struct は allocator フィールドを持っています。
コンポーネントへの接続
コンポーネントのコールバックフィールドはすべて ?HandlerRef です。前の手順で得た on_click を Button に渡し、更新したいテキストノードを状態 struct に記録します。
const label = try ui.text(cx, "Clicked 0 times", .{
.color = cx.tokens.color.fg_primary,
});
counter.label = label;
const button = try ui.widgets.Button(.{
.label = "Increment",
.variant = .primary,
.on_click = on_click,
}).mount(scope, cx);
try root.appendChild(cx.allocator, button);
try root.appendChild(cx.allocator, label);値を受け取るコールバック(Checkbox や Switch のチェック状態、Input や Tabs のテキストや id)には、ui.Cx.boolHandlerFrom のような値付きのコンストラクタを使います。
const Prefs = struct {
notify: bool = false,
fn setNotify(self: *Prefs, checked: bool) void {
self.notify = checked;
}
};
const prefs = try cx.bindState(Prefs, .{});
const notify = try ui.widgets.Checkbox(.{
.label_text = "Notify me",
.on_change = ui.Cx.boolHandlerFrom(Prefs, prefs, Prefs.setNotify),
}).mount(scope, cx);クリックの経路
プラットフォームから届くのは押下と解放であって「クリック」ではありません。ヒットテストは Z 順に手前から子を走査して最前面のノードを見つけます。押下と解放が同じ要素上で起きると、EventDispatcher が click を合成し、capture → target → bubble の 3 段階で配送してノードの on_click という HandlerRef を呼び出し、最終的にバインドしたメソッドに到達します。
.ignored未処理。伝播を続け、デフォルト動作を許可.handled処理済み。伝播は続けるが、デフォルト動作を抑止.stop処理済み。伝播を停止Handler の構築方法
cx.on(State, s, State.m)fn (*State) voidボタンのクリックなど「起きた」ことの通知ui.Cx.boolHandlerFrom(…)fn (*State, bool) voidCheckbox、Switch、Accordionui.Cx.strHandlerFrom(…)fn (*State, []const u8) voidInput、Textarea、Tabs、Radio。スライスはコールバック中のみ有効ui.clickable(node, handler)—任意のノードに on_click を付ける低レベルイベント
ボタンや入力欄などのコンポーネントは、一般的なインタラクションをすでにカプセル化しています。ui.events.Event が必要なのは独自のインタラクションを実装するときだけです。マウス(押下、解放、click、double_click、移動、出入り、スクロール、拡大縮小、ファイルのドラッグ)、キーボード、text_input、ime_preedit / ime_commit、フォーカスを扱います。キーボードコマンドには ui.actions、フォーカス移動には ui.focus を優先してください。
ui.widgets.* callbacksui.hooks.useHover / useFocusRingui.actionsui.interaction.drag / range_hoverui.events.Eventライフサイクルとクリーンアップ
バインドされた状態は Cx が所有し、Cx の破棄時に解放されます。T が pub な deinit(*T) を宣言していれば、フレームワークが解放直前に自動で呼び出します。ArrayList、HashMap、確保したバッファのクリーンアップはそこに書けば十分です。Signal、Memo、Effect、onCleanup コールバックは Scope に属し、scope.dispose() と一緒に解放されます。
const EditorState = struct {
allocator: std.mem.Allocator,
lines: std.ArrayList([]u8) = .{},
// Must be pub: the state store detects it and calls it when the Cx deinits.
pub fn deinit(self: *EditorState) void {
for (self.lines.items) |line| self.allocator.free(line);
self.lines.deinit(self.allocator);
}
};
// The initial value must not own resources yet; allocate after binding.
const editor = try cx.bindState(EditorState, .{ .allocator = cx.allocator });
// Do NOT also call scope.onCleanup(EditorState, editor, EditorState.deinit):
// the framework already calls deinit, so that would free everything twice.scope.onCleanup を使うのは 2 つの場合だけです。クリーンアップ関数が pub な deinit でないとき、またはリソースを Cx 全体ではなく特定の Scope(ページ)に合わせて解放する必要があるときです。
// A resource that must follow the page (Scope), not the whole Cx.
const PageCache = struct {
allocator: std.mem.Allocator,
hits: std.StringHashMapUnmanaged(u32) = .{},
// Not named deinit, so the state store will not call it again.
fn release(self: *PageCache) void {
self.hits.deinit(self.allocator);
self.hits = .{};
}
};
const cache = try cx.bindState(PageCache, .{ .allocator = cx.allocator });
try scope.onCleanup(PageCache, cache, PageCache.release);- ✓
初期値はリソースを持たない。
bindStateに渡す初期値はゼロ値か、ポインタや allocator のような借用フィールドだけにします。確保が必要なものは*Tを得てから作ります。 - ✓
allocator はフィールドとして保持する。
deinit(*T)の引数は 1 つだけなので、解放に使う allocator は struct 内に保存しておく必要があります。 - ✓
無効になったノードをキャッシュしない。Node ポインタは所属するツリーと Scope が生きている間だけ有効です。ページのアンマウントや再構築の後は、状態経由で古いノードにアクセスしないでください。