docs/advanced/multi-window
Capacités · Fenêtres natives

Applications multifenêtres

Créez des fenêtres natives isolées pour des éditeurs, des aperçus ou des panneaux d’outils, toutes pilotées par une seule boucle d’événements qui distribue par window_id.

6 min de lecture

Quand l’utiliser

Les applications à fenêtre unique continuent d’utiliser App ; son API ne change pas. Passez à MultiWindowApp (aussi exporté sous zenit_app.Application) uniquement lorsque les fenêtres ont besoin de leur propre cycle de vie, routage des entrées, surface GPU ou arbre d’accessibilité.

Créer des fenêtres

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()
}

Chaque appel à createWindow* renvoie un *App et accepte la même configuration qu’un App à fenêtre unique (window, console, frame_pacing, …). createWindow crée sans monter : vous appelez vous-même app.mount(mountFn) ; createWindowWith fait les deux en une seule transaction. L’exemple zig build multi-window du dépôt illustre les deux.

Garanties d’isolation

PER-WINDOW STACK
Un événement entre dans exactement une fenêtre, choisie par window_id ; le modèle partagé reste à l’extérieur et l’application notifie chaque fenêtre explicitement.
01
Propre à chaque fenêtre

Fenêtre native, Cx (avec son propre graphe réactif), contexte de polices, surface Metal / renderer / file du device, IME et route d’accessibilité.

02
Événements isolés

Les événements de pointeur, clavier, IME et glisser sont routés par window_id natif ; les commandes de menu vont à la key window capturée au déclenchement, pas à la fenêtre qui les a dépilées par hasard.

03
Destruction indépendante

Fermer une fenêtre ne détruit que ses propres ressources, les autres continuent de s’afficher ; run() rend la main quand la dernière se ferme.

Une vraie application à deux fenêtres : la cible observée et DevTools ont chacune leur propre Cx et leur fenêtre. Le harness passe la fenêtre d’outils sur Console et la filtre pendant que la cible reste active.

Cycle de vie

APIRôle
run()Fait tourner la boucle jusqu’à la fermeture de la dernière fenêtre ou l’appel de quit()
tick()Un tour de pump + frame, pour piloter la boucle vous-même
closeWindow(id)Ferme une fenêtre ; depuis un callback, c’est mis en file jusqu’à une frontière sûre
quit()Quitte toute l’application ; deinit() détruit les fenêtres restantes dans l’ordre
window(id)Trouve une fenêtre vivante par id ; null une fois fermée
activateWindow(id)Rend une fenêtre active
setMenuModel / bindMenuCommandLe modèle de menu est global au processus ; les callbacks de commande se lient par fenêtre
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
}

Partager l’état

Le modèle partagé entre fenêtres appartient à l’application ; chaque fenêtre ne garde que son propre état de vue. Chaque Cx a son propre graphe réactif : ne laissez donc pas l’Effect d’une fenêtre lire un Signal du Scope d’une autre, et ne partagez jamais de pointeurs Node ni de ressources de Scope entre Cx.

  • ✓

    Le modèle vit en dehors des fenêtres. Conservez les données dans une struct simple ou un store au niveau de l’application qui survit à chaque fenêtre.

  • ✓

    Chaque fenêtre reflète ce dont elle a besoin. Une fenêtre crée des Signals dans son propre Scope ; quand le modèle change, l’application écrit explicitement dans le miroir de chaque fenêtre.

  • ✓

    Désinscrivez à la fermeture. Une fois la fenêtre fermée, le modèle ne doit plus garder de pointeurs vers ses Signals.

zenit · Double licenceGratuit pour les projets open source sous GPL-3.0-only ; les produits propriétaires ou commerciaux nécessitent une licence commerciale.Contacter l’auteur : zongyi.xzy#gmail.com (remplacez # par @)zenit 5f9add5+wip 2026-09-30