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.