멀티 윈도우 앱
에디터, 미리보기, 도구 패널용으로 서로 격리된 네이티브 창을 만들고, window_id로 분배하는 하나의 이벤트 루프로 구동합니다.
언제 사용하나
단일 창 앱은 계속 App을 사용하며, 그 API는 바뀌지 않습니다. 창마다 독립된 생명주기, 입력 라우팅, GPU surface, 접근성 트리가 필요할 때만 MultiWindowApp(zenit_app.Application으로도 export됨)으로 옮깁니다.
창 생성
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을 읽게 해서는 안 되며, Node 포인터나 Scope 리소스를 Cx 간에 공유해서도 안 됩니다.
- ✓
모델은 창 바깥에 둡니다. 사실 데이터는 일반 struct나 앱 수준 store에 두어 어떤 창보다도 오래 살게 합니다.
- ✓
각 창은 필요한 부분만 미러링합니다. 창은 자신의 Scope에 Signal을 만들고, 모델이 바뀌면 앱이 각 창의 미러에 명시적으로 씁니다.
- ✓
닫을 때 등록을 해제합니다. 창이 닫힌 뒤에는 모델이 그 창의 Signal을 가리키는 포인터를 유지해서는 안 됩니다.