マルチウィンドウアプリ
エディタ、プレビュー、ツールパネル向けに互いに分離されたネイティブウィンドウを作成し、window_id で振り分ける 1 つのイベントループで駆動します。
使うべき場面
単一ウィンドウのアプリは引き続き App を使えます。その API は変わりません。ウィンドウごとに独立したライフサイクル、入力ルーティング、GPU surface、アクセシビリティツリーが必要な場合にだけ MultiWindowApp(zenit_app.Application としてもエクスポート)に移行します。
ウィンドウの作成
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()
}createWindow* は呼び出しごとに *App を返し、単一ウィンドウの App と同じ設定(window、console、frame_pacing など)を受け取ります。createWindow は作成のみでマウントしないため、app.mount(mountFn) を自分で呼びます。createWindowWith は作成とマウントを 1 つのトランザクションで行います。リポジトリの zig build multi-window サンプルで両方を確認できます。
分離の保証
ネイティブ window、Cx(独自のリアクティブグラフを含む)、フォントコンテキスト、Metal surface / renderer / device queue、IME とアクセシビリティのルート。
ポインタ、キーボード、IME、ドラッグのイベントはネイティブの window_id でルーティングされます。メニューコマンドは、たまたまイベントを取り出したウィンドウではなく、発火時に捕捉した key window に送られます。
ウィンドウを閉じると自身のリソースだけが破棄され、他のウィンドウは描画を続けます。最後の 1 つが閉じると run() が戻ります。
ライフサイクル
run()最後のウィンドウが閉じるか quit() が呼ばれるまでループを回すtick()pump + フレームを 1 回実行。ループを自分で回す場合にcloseWindow(id)ウィンドウを 1 つ閉じる。コールバック内では安全な境界までキューに積まれるquit()アプリ全体を終了。残りのウィンドウは deinit() が順に破棄するwindow(id)id で生存中のウィンドウを検索。閉じた後は nullactivateWindow(id)ウィンドウをアクティブにするsetMenuModel / bindMenuCommandメニューモデルはプロセス全体で共有。コマンドのコールバックはウィンドウごとにバインド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
}状態の共有
ウィンドウをまたぐモデルはアプリが所有し、各ウィンドウは自分のビュー状態だけを持ちます。Cx ごとにリアクティブグラフが独立しているため、あるウィンドウの Effect に別のウィンドウの Scope にある Signal を読ませてはいけません。Node ポインタや Scope のリソースを Cx をまたいで共有することも避けてください。
- ✓
モデルはウィンドウの外に置く。事実は素の struct かアプリレベルの store に保持し、どのウィンドウよりも長く生存させます。
- ✓
各ウィンドウは必要な部分だけをミラーする。ウィンドウは自分の Scope に Signal を作り、モデルが変わったらアプリが各ウィンドウのミラーへ明示的に書き込みます。
- ✓
閉じるときに登録を解除する。ウィンドウが閉じた後、モデルはそのウィンドウの Signal へのポインタを保持してはいけません。