Estado y eventos
Deja que el Cx sea dueño del estado que debe sobrevivir entre frames, y responde a la entrada del usuario con handlers con tipos seguros.
Estado vinculado
cx.bindState(T, init) crea un T en el almacén de estado del Cx y devuelve un *T que sigue siendo válido entre frames. Luego cx.on convierte uno de sus métodos en un HandlerRef que acepta cualquier componente — sin IDs de estado asignados a mano.
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);Cambia el texto desde un callback con node.setTextContent(allocator, text): copia el contenido y lo marca como owned, libera la copia owned anterior y solo marca el nodo como sucio si el texto cambió de verdad — sin markRenderDirty manual. Por eso el struct guarda un campo allocator.
Conectar componentes
Todos los campos de callback de los componentes son ?HandlerRef. Pasa el on_click de arriba a un Button y guarda en el struct de estado el nodo de texto que quieras actualizar.
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);Los callbacks que llevan un valor — el estado marcado de un Checkbox o Switch, el texto o el id de Input o Tabs — usan un constructor que transporta valor, como 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);El recorrido de un clic
La plataforma entrega pulsaciones y liberaciones, no “clics”. El hit-testing recorre los hijos de delante hacia atrás (orden Z) para encontrar el nodo superior; cuando la pulsación y la liberación caen sobre el mismo elemento, el EventDispatcher sintetiza un click, lo despacha por capture → target → bubble, llama al HandlerRef on_click del nodo y termina en el método que vinculaste.
.ignoredNo gestionado; sigue propagándose y permite el comportamiento por defecto.handledGestionado; sigue propagándose pero impide el comportamiento por defecto.stopGestionado; detiene la propagaciónConstruir handlers
cx.on(State, s, State.m)fn (*State) voidClics de botón y otros eventos de “ha ocurrido”ui.Cx.boolHandlerFrom(…)fn (*State, bool) voidCheckbox, Switch, Accordionui.Cx.strHandlerFrom(…)fn (*State, []const u8) voidInput, Textarea, Tabs, Radio; el slice solo es válido durante la llamadaui.clickable(node, handler)—Añadir on_click a cualquier nodoEventos de bajo nivel
Los botones, los campos de entrada y compañía ya encapsulan las interacciones habituales. Recurre a ui.events.Event solo para interacciones personalizadas: cubre el ratón (down, up, click, double_click, movimiento, entrada/salida, scroll, magnify, arrastre de archivos), el teclado, text_input, ime_preedit / ime_commit y el foco. Prefiere ui.actions para los comandos de teclado y ui.focus para mover el foco.
ui.widgets.* callbacksui.hooks.useHover / useFocusRingui.actionsui.interaction.drag / range_hoverui.events.EventCiclo de vida y limpieza
El estado vinculado pertenece al Cx y se libera cuando el Cx hace deinit. Si T declara un deinit(*T) pub, el framework lo llama justo antes de liberarlo — pon ahí la limpieza de tus ArrayList, HashMap y buffers. Los Signals, Memos, Effects y callbacks de onCleanup pertenecen a un Scope y desaparecen con 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.Usa scope.onCleanup solo cuando la limpieza no sea un deinit pub, o cuando un recurso deba seguir a un Scope (una página) en lugar de a todo el 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);- ✓
Los valores iniciales no poseen nada. Pasa a
bindStateun valor cero o solo campos prestados (punteros, un allocator); asigna memoria cuando ya tengas el*T. - ✓
Guarda el allocator como campo.
deinit(*T)recibe un solo argumento, así que el allocator usado para liberar tiene que vivir en el struct. - ✓
No retengas nodos muertos. Un puntero a Node solo es válido mientras vivan su árbol y su Scope; después de que una página se desmonte o se reconstruya, no accedas a nodos antiguos a través del estado.