docs/advanced/multi-window
Capacidades · Ventanas nativas

Aplicaciones multiventana

Crea ventanas nativas aisladas para editores, vistas previas o paneles de herramientas, todas movidas por un único bucle de eventos que despacha por window_id.

6 min de lectura

Cuándo usarlo

Las aplicaciones de una sola ventana siguen usando App; su API no cambia. Pasa a MultiWindowApp (también exportado como zenit_app.Application) solo cuando las ventanas necesiten su propio ciclo de vida, enrutamiento de entrada, superficie GPU o árbol de accesibilidad.

Crear ventanas

main.zig
const std = @import("std");
const ui = @import("ui");
const zenit_app = @import("zenit_app");

fn mountEditor(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    _ = scope;
    return ui.box(cx, .{ .width = .fill(), .height = .fill() }, .{});
}

fn mountPreview(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    _ = scope;
    return ui.box(cx, .{ .width = .fill(), .height = .fill() }, .{});
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var application = zenit_app.MultiWindowApp.init(gpa.allocator(), .{});
    defer application.deinit();

    // createWindowWith = create + mount in one transaction: if mount fails,
    // every native / GPU resource of that window is torn down again.
    const editor = try application.createWindowWith(.{
        .window = .{ .width = 900, .height = 700, .title = "Editor" },
    }, mountEditor);

    _ = try application.createWindowWith(.{
        .window = .{ .width = 480, .height = 700, .title = "Preview" },
    }, mountPreview);

    _ = application.activateWindow(editor.windowId());
    try application.run(); // returns after the last window closes or quit()
}

Cada llamada a createWindow* devuelve un *App y acepta la misma configuración que un App de una sola ventana (window, console, frame_pacing, …). createWindow crea sin montar, así que llamas tú a app.mount(mountFn); createWindowWith hace ambas cosas en una sola transacción. El ejemplo zig build multi-window del repositorio muestra las dos.

Garantías de aislamiento

PER-WINDOW STACK
Un evento entra exactamente en una ventana, elegida por window_id; el modelo compartido está fuera y la aplicación notifica a cada ventana de forma explícita.
01
Independiente por ventana

Ventana nativa, Cx (con su propio grafo reactivo), contexto de fuentes, superficie Metal / renderer / cola del dispositivo, IME y ruta de accesibilidad.

02
Eventos aislados

Los eventos de puntero, teclado, IME y arrastre se enrutan por el window_id nativo; los comandos de menú van a la key window capturada al dispararse, no a la ventana que casualmente los sacó de la cola.

03
Cierre independiente

Cerrar una ventana libera solo sus propios recursos mientras las demás siguen renderizando; run() retorna cuando se cierra la última.

Una aplicación real de dos ventanas: el target observado y DevTools tienen cada uno su propio Cx y su ventana. El harness cambia la ventana de herramientas a Console y la filtra mientras el target sigue vivo.

Ciclo de vida

APIQué hace
run()Ejecuta el bucle hasta que se cierra la última ventana o se llama a quit()
tick()Una ronda de pump + frame, para controlar el bucle tú mismo
closeWindow(id)Cierra una ventana; dentro de un callback se encola hasta un límite seguro
quit()Sale de toda la aplicación; deinit() cierra en orden las ventanas restantes
window(id)Busca una ventana viva por id; null si ya se cerró
activateWindow(id)Convierte una ventana en la activa
setMenuModel / bindMenuCommandEl modelo de menú es global al proceso; los callbacks de comandos se vinculan por ventana
close.zig
const preview_id = preview.windowId(); // preview: *App from createWindowWith

// Safe from inside an input / menu / render callback: the close is queued
// and committed at the loop's iteration boundary.
_ = application.closeWindow(preview_id);

// Look windows up by id instead of holding *App across a close.
if (application.window(preview_id)) |app| {
    _ = app; // still alive
}

Compartir estado

El modelo compartido entre ventanas pertenece a la aplicación; cada ventana guarda solo su propio estado de vista. Cada Cx tiene su propio grafo reactivo, así que no dejes que el Effect de una ventana lea un Signal del Scope de otra, y nunca compartas punteros a Node ni recursos de Scope entre Cx.

  • ✓

    El modelo vive fuera de las ventanas. Guarda los datos en un struct simple o en un store de nivel de aplicación que sobreviva a cualquier ventana.

  • ✓

    Cada ventana refleja lo que necesita. Una ventana crea Signals en su propio Scope; cuando el modelo cambia, la aplicación escribe explícitamente el reflejo de cada ventana.

  • ✓

    Da de baja al cerrar. Tras cerrarse una ventana, el modelo no debe conservar punteros a sus Signals.

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