Zustand und Events
Überlassen Sie dem Cx den Zustand, der über Frames hinweg bestehen muss, und beantworten Sie Benutzereingaben mit typsicheren Handlern.
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.
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.
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.
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 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.
.ignoredNicht behandelt; Weiterleitung läuft, Standardverhalten erlaubt.handledBehandelt; Weiterleitung läuft, Standardverhalten wird verhindert.stopBehandelt; Weiterleitung stopptHandler erstellen
cx.on(State, s, State.m)fn (*State) voidButton-Klicks und andere „es ist passiert“-Eventsui.Cx.boolHandlerFrom(…)fn (*State, bool) voidCheckbox, Switch, Accordionui.Cx.strHandlerFrom(…)fn (*State, []const u8) voidInput, Textarea, Tabs, Radio; der Slice ist nur während des Aufrufs gültigui.clickable(node, handler)—on_click an beliebigen Node hängenLow-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.
ui.widgets.* callbacksui.hooks.useHover / useFocusRingui.actionsui.interaction.drag / range_hoverui.events.EventLebensdauer 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().
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.
// 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
bindStateeinen Nullwert oder nur geliehene Felder (Zeiger, einen Allocator); allozieren Sie erst, wenn Sie den*Thaben. - ✓
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.