docs/reference/troubleshooting
Referencia · Debugging

Solución de problemas

De los errores de build al ciclo de vida del estado y los fallos de renderizado: el camino más corto hasta la causa.

8 min de lectura

Errores de build

invalid fingerprint

La fingerprint de una plantilla no se puede reutilizar. Borra la línea .fingerprint de build.zig.zon, ejecuta zig build una vez y vuelve a pegar el valor único que imprime Zig.

import of file outside module path

No ejecutes zig test directamente sobre archivos internos del framework. Usa los steps de prueba del repo, como zig build test-ui (consulta Comandos).

@import("ui") / @import("zenit_app") not found

Comprueba que build.zig.zon declara la dependencia .zenit y que llamas a zenit.attach(zenit_dep, exe) después de crear el ejecutable: añade los módulos ui / zenit_app, compila los puentes de macOS y enlaza los frameworks del sistema.

build API field mismatch

Ejecuta primero zig version. zenit requiere Zig 0.15.2 (minimum_zig_version en build.zig.zon); las build API de otras versiones no están soportadas.

-Dtest-mode=true has no effect

Zig aísla las opciones de las dependencias. Reenvía .@"test-mode" y .@"e2e-port" explícitamente en b.dependency("zenit", ...), como hace templates/minimal-app/build.zig.

Ejecución y estado

error.StateNotFound

Un cx.handler(State, id, method) con id explícito se ejecutó antes de que existiera ese estado. Mejor usa cx.bindState + cx.on: pasan el puntero al estado directamente, así que no hay ids que gestionar.

leaked ArrayList / HashMap at exit

El estado de bindState pertenece al Cx y se libera junto con él; si T declara un deinit(self: *T) pub, el framework lo llama automáticamente. Una fuga suele indicar que deinit no es pub o que el recurso debía seguir el ciclo de vida de una página. Consulta la siguiente sección.

crash after switching pages

Busca un *Node o *Scope de la página anterior guardado en estado global. Cuando el Scope de la página se libera, esos punteros dejan de ser válidos y deben limpiarse junto con él.

Invalid free after updating text

El texto actual del nodo puede ser una copia que el propio nodo posee. No edites content en las props obtenidas con getText() para volver a pasarlas a setText; usa try node.setTextContent(cx.allocator, "Updated"), que copia la cadena y marca la propiedad correctamente.

Limpiar el estado correctamente

Guarda un campo allocator en el estado y deja que un deinit pub haga la limpieza. El valor inicial todavía no debe poseer recursos: asigna después de bindState.

state_deinit.zig
const Editor = struct {
    allocator: std.mem.Allocator,
    lines: std.ArrayList([]const u8) = .empty,

    // pub: the Cx calls this automatically when it frees the state.
    pub fn deinit(self: *Editor) void {
        self.lines.deinit(self.allocator);
    }
};

// The initial value must not own resources yet; allocate after binding.
const editor = try cx.bindState(Editor, .{ .allocator = cx.allocator });

Recurre a scope.onCleanup solo cuando la limpieza no es un deinit pub, o cuando el recurso debe seguir a un Scope (una página) y no a todo el Cx.

scope_cleanup.zig
// Resources that must follow a page (Scope), not the whole window (Cx).
const PageCache = struct {
    allocator: std.mem.Allocator,
    map: std.StringHashMapUnmanaged(u32) = .empty,

    // Not named `pub fn deinit`: the Cx would call it again and double-free.
    fn release(self: *PageCache) void {
        self.map.deinit(self.allocator);
    }
};

const cache = try cx.bindState(PageCache, .{ .allocator = cx.allocator });
try scope.onCleanup(PageCache, cache, PageCache.release);

Layout y dibujo

state changed but the text did not

setText ya compara las firmas antigua y nueva y marca sizing / render dirty cuando hace falta: no necesitas markRenderDirty manual. Un texto desactualizado suele indicar que se editó una copia de TextProps sin llamar a setText, o que el valor mostrado nunca se suscribió a su Signal. Mejor usa ui.textFmt(cx, scope, fmt, .{ signals }, props) para suscribirte directamente a Signals / Memos.

some colors ignore a theme switch

boxStyled / textStyled y compañía adjuntan un hook on_theme, y cx.setTheme solo los vuelve a ejecutar en el subárbol de cx.root; los nodos simples y los estilos de componentes son instantáneas tomadas en el mount. Reconstruye esos árboles al cambiar de tema, o suscríbete a cx.themeSignal(scope) en un Effect y reaplica los estilos; los nodos que no cuelgan de cx.root tampoco se actualizan.

clicks fall through an overlay’s empty area

Los contenedores puramente visuales sin interacción son pass_through por defecto. Define hit_behavior = .@"opaque" en la extensión de estilo de la raíz del overlay, o usa Modal / Sheet, que traen su propia barrera.

a huge invisible hit area in the window

Adjunta ui.devtools.overlay para inspeccionar el nodo alcanzado e imprime su rect en pantalla con node.globalRect(). La causa habitual es un sizing fill / grow del padre o un posicionamiento absoluto demasiado grande; en los overlays ocultos, comprueba que estén fuera del hit-testing: un subárbol oculto con node.setDisplay(.none) sale por completo del layout, el pintado, el hit-testing y el recorrido con Tab.

opaque_overlay.zig
// Plain visual containers are pass-through by default.
// Make an overlay root swallow clicks on its empty area:
(try panel.style.ensureExtFallible(cx.allocator)).hit_behavior = .@"opaque";

¿Sigues atascado?

Cuando abras un issue, incluye como mínimo:

  • ✓

    Versiones: la revisión de zenit, zig version y tu versión de macOS.

  • ✓

    Reproducción: un ejemplo mínimo reproducible y el build step que ejecutaste.

  • ✓

    Salida: la salida de error completa, no solo la última línea.

  • ✓

    Problemas de renderizado: una captura de pantalla y los tamaños de los nodos implicados según DevTools.

zenit · Doble licenciaGratis para proyectos de código abierto bajo GPL-3.0-only; los productos cerrados o comerciales necesitan una licencia comercial.Contacta con el autor: zongyi.xzy#gmail.com (cambia # por @)zenit 5f9add5+wip 2026-09-30