docs/guide/state-events
Kernkonzepte · Interaction

Zustand und Events

Überlassen Sie dem Cx den Zustand, der über Frames hinweg bestehen muss, und beantworten Sie Benutzereingaben mit typsicheren Handlern.

9 Min. Lesezeit

Gebundener Zustand

cx.bindState(T, init) legt ein T im State-Store des Cx an und gibt einen *T zurück, der über Frames hinweg gültig bleibt. cx.on macht dann aus einer seiner Methoden eine HandlerRef, die jede Komponente akzeptiert — ohne von Hand vergebene State-IDs.

counter.zig
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);

Tauschen Sie Text in einem Callback mit node.setTextContent(allocator, text) aus: Es kopiert den Inhalt und markiert ihn als owned, gibt die vorherige owned-Kopie frei und markiert den Node nur dann als dirty, wenn sich der Text tatsächlich geändert hat — kein manuelles markRenderDirty. Deshalb hält das Struct ein allocator-Feld.

Komponenten verdrahten

Die Callback-Felder von Komponenten sind alle ?HandlerRef. Übergeben Sie das on_click von oben an einen Button und speichern Sie den zu aktualisierenden Text-Node im State-Struct.

view.zig
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);

Callbacks mit Wert — der Häkchen-Zustand einer Checkbox oder eines Switch, Text oder id aus Input oder Tabs — verwenden einen wertübergebenden Konstruktor wie ui.Cx.boolHandlerFrom.

prefs.zig
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);
Der harness klickt auf den echten Trefferbereich der Checkbox: Häkchen-Optik, on_change-Callback und Statustext aktualisieren sich in derselben Frame-Pipeline.

Der Weg eines Klicks

Die Plattform liefert Drücken und Loslassen, keine „Klicks“. Das Hit-Testing durchläuft die Kinder von vorne nach hinten (Z-Reihenfolge), um den obersten Node zu finden; landen Drücken und Loslassen auf demselben Element, synthetisiert der EventDispatcher einen click, verteilt ihn über capture → target → bubble, ruft die on_click-HandlerRef des Nodes auf und landet schließlich in der Methode, die Sie gebunden haben.

EVENT ROUTING
Eine HandlerRef ist nur „Callback + Kontextzeiger“. Der von cx.on erzeugte Callback castet den Kontext zurück zu *Counter und ruft die Methode auf; der Zustand selbst bleibt im State-Store des Cx.
EventResultBedeutung
.ignoredNicht behandelt; Weiterleitung läuft, Standardverhalten erlaubt
.handledBehandelt; Weiterleitung läuft, Standardverhalten wird verhindert
.stopBehandelt; Weiterleitung stoppt

Handler erstellen

KonstruktorMethodensignaturEinsatz
cx.on(State, s, State.m)fn (*State) voidButton-Klicks und andere „es ist passiert“-Events
ui.Cx.boolHandlerFrom(…)fn (*State, bool) voidCheckbox, Switch, Accordion
ui.Cx.strHandlerFrom(…)fn (*State, []const u8) voidInput, Textarea, Tabs, Radio; der Slice ist nur während des Aufrufs gültig
ui.clickable(node, handler)—on_click an beliebigen Node hängen

Low-Level-Events

Buttons, Eingabefelder und Co. kapseln die üblichen Interaktionen bereits. Greifen Sie nur für eigene Interaktionen zu ui.events.Event: Es deckt die Maus (down, up, click, double_click, Bewegung, Betreten/Verlassen, Scrollen, Magnify, Datei-Drag), die Tastatur, text_input, ime_preedit / ime_commit und den Fokus ab. Für Tastaturbefehle nutzen Sie bevorzugt ui.actions, zum Verschieben des Fokus ui.focus.

BedarfBevorzugte API
Klicken, tippen, auswählenui.widgets.* callbacks
Hover, Fokusringui.hooks.useHover / useFocusRing
Shortcuts und Befehleui.actions
Drag, Bereichs-Hoverui.interaction.drag / range_hover
Rohe Tasten/Maus, Scrollen, IMEui.events.Event

Lebensdauer und Aufräumen

Gebundener Zustand gehört dem Cx und wird beim deinit des Cx freigegeben. Deklariert T ein pub deinit(*T), ruft das Framework es direkt vor dem Freigeben auf — dort gehört das Aufräumen Ihrer ArrayList, HashMap und Puffer hin. Signals, Memos, Effects und onCleanup-Callbacks gehören zu einem Scope und verschwinden mit scope.dispose().

OWNERSHIP
Zwei Lebensdauern: Cx-weit (das ganze Fenster / die App) und Scope-weit (eine Seite oder ein Panel).
editor_state.zig
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.

Verwenden Sie scope.onCleanup nur, wenn das Aufräumen kein pub deinit ist oder eine Ressource einem Scope (einer Seite) statt dem ganzen Cx folgen muss.

page_cache.zig
// 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);
  • ✓

    Anfangswerte besitzen nichts. Übergeben Sie bindState einen Nullwert oder nur geliehene Felder (Zeiger, einen Allocator); allozieren Sie erst, wenn Sie den *T haben.

  • ✓

    Halten Sie den Allocator als Feld. deinit(*T) nimmt nur ein Argument, daher muss der Allocator zum Freigeben im Struct liegen.

  • ✓

    Halten Sie keine toten Nodes. Ein Node-Zeiger ist nur gültig, solange sein Baum und sein Scope leben; nachdem eine Seite unmountet oder neu aufgebaut wurde, greifen Sie nicht über den Zustand auf alte Nodes zu.

zenit · DoppellizenzKostenlos für Open-Source-Projekte unter GPL-3.0-only; Closed-Source- oder kommerzielle Produkte benötigen eine kommerzielle Lizenz.Kontakt zum Autor: zongyi.xzy#gmail.com (# durch @ ersetzen)zenit 5f9add5+wip 2026-09-30