多窗口应用
为编辑器、预览器或工具面板创建彼此隔离的原生窗口,由一个事件循环按 window_id 分发。
何时使用
单窗口应用继续使用 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 把创建与挂载合成一个事务。仓库的 zig build multi-window 示例同时演示两种写法。
隔离保证
原生 window、Cx(含自己的响应式图)、字体上下文、Metal surface / renderer / device queue、IME 与辅助功能路由。
指针、键盘、IME 与拖拽按原生 window_id 路由;菜单命令按触发时捕获的 key window 路由,而不是按哪个窗口恰好取到了事件。
关闭一扇窗口只销毁它自己的资源,其他窗口继续渲染;最后一扇关闭后 run() 返回。
生命周期
run()驱动循环,直到最后一扇窗口关闭或调用 quit()tick()执行一轮 pump + 按需渲染,适合自己控制循环closeWindow(id)关闭一扇窗口;在回调里调用时排队到安全边界再提交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,也不要跨 Cx 共享 Node 指针或 Scope 资源。
- ✓
模型在窗口之外。用普通结构体或应用级 Store 保存事实,生命周期长于任何一扇窗口。
- ✓
每个窗口镜像自己需要的部分。窗口在自己的 Scope 里创建 Signal,模型变化时由应用显式写入各窗口的镜像。
- ✓
关闭时解除登记。窗口关闭后,模型不能再持有指向该窗口 Signal 的指针。