État et événements
Confiez au Cx l'état qui doit survivre d'une frame à l'autre, et répondez aux entrées utilisateur avec des handlers typés.
État lié
cx.bindState(T, init) crée un T dans le magasin d'état du Cx et renvoie un *T qui reste valide d'une frame à l'autre. cx.on transforme ensuite l'une de ses méthodes en un HandlerRef accepté par n'importe quel composant — sans ID d'état attribué à la main.
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);Remplacez le texte depuis un callback avec node.setTextContent(allocator, text) : il copie le contenu et le marque owned, libère la copie owned précédente, et ne marque le nœud comme sale que si le texte a réellement changé — pas de markRenderDirty manuel. C'est pourquoi le struct conserve un champ allocator.
Brancher les composants
Les champs de callback des composants sont tous des ?HandlerRef. Passez le on_click obtenu plus haut à un Button, et stockez dans le struct d'état le nœud texte à mettre à jour.
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);Les callbacks porteurs d'une valeur — l'état coché d'un Checkbox ou d'un Switch, le texte ou l'id d'un Input ou de Tabs — utilisent un constructeur qui transmet la valeur, comme 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);Le trajet d'un clic
La plateforme transmet des appuis et des relâchements, pas des « clics ». Le hit-testing parcourt les enfants de l'avant vers l'arrière (ordre Z) pour trouver le nœud le plus haut ; quand l'appui et le relâchement tombent sur le même élément, l'EventDispatcher synthétise un click, le distribue via capture → target → bubble, appelle le HandlerRef on_click du nœud et aboutit dans la méthode que vous avez liée.
.ignoredNon traité ; la propagation continue et le comportement par défaut est permis.handledTraité ; la propagation continue mais le comportement par défaut est empêché.stopTraité ; la propagation s'arrêteConstruire des handlers
cx.on(State, s, State.m)fn (*State) voidClics de bouton et autres événements « c'est arrivé »ui.Cx.boolHandlerFrom(…)fn (*State, bool) voidCheckbox, Switch, Accordionui.Cx.strHandlerFrom(…)fn (*State, []const u8) voidInput, Textarea, Tabs, Radio ; le slice n'est valide que pendant l'appelui.clickable(node, handler)—Attacher on_click à n'importe quel nœudÉvénements de bas niveau
Les boutons, champs de saisie et consorts encapsulent déjà les interactions courantes. N'utilisez ui.events.Event que pour une interaction personnalisée : il couvre la souris (down, up, click, double_click, déplacement, entrée/sortie, défilement, magnify, glisser de fichiers), le clavier, text_input, ime_preedit / ime_commit et le focus. Préférez ui.actions pour les commandes clavier et ui.focus pour déplacer le focus.
ui.widgets.* callbacksui.hooks.useHover / useFocusRingui.actionsui.interaction.drag / range_hoverui.events.EventDurée de vie et nettoyage
L'état lié appartient au Cx et est libéré lors du deinit du Cx. Si T déclare un deinit(*T) pub, le framework l'appelle juste avant la libération — placez-y le nettoyage de vos ArrayList, HashMap et buffers. Les Signals, Memos, Effects et callbacks onCleanup appartiennent à un Scope et disparaissent avec 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.N'utilisez scope.onCleanup que lorsque le nettoyage n'est pas un deinit pub, ou lorsqu'une ressource doit suivre un Scope (une page) plutôt que tout le Cx.
// 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);- ✓
Les valeurs initiales ne possèdent rien. Passez à
bindStateune valeur zéro ou uniquement des champs empruntés (pointeurs, un allocator) ; allouez une fois que vous avez le*T. - ✓
Gardez l'allocator dans un champ.
deinit(*T)ne prend qu'un argument, donc l'allocator servant à libérer doit vivre dans le struct. - ✓
Ne gardez pas de nœuds morts. Un pointeur de Node n'est valide que tant que son arbre et son Scope vivent ; après le démontage ou la reconstruction d'une page, n'accédez plus aux anciens nœuds via l'état.