ReviewOS

also looking at this

craft-native/craft

feat: add Linux and Windows named-window routing

#325
Merged glennmichael123 wants to merge codex/multi-window-linux-windows into main
29 files +2250 -200

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

docs/architecture/multi-window-ownership.mdmodified+5-0
Changes to docs/architecture/multi-window-ownership.md
@@ -33,6 +33,11 @@ that window before entering `g_application_run()`. On Windows, the controller
3333passed to the asynchronous WebView2 completion callback is borrowed. Each live
3434window retains its own controller reference until native destruction; otherwise
3535WebView2 can close before the page bridge registers or navigation begins.
36WebView2 also forbids entering a nested message loop from its message callback.
37Windows therefore queues page-requested child creation onto the UI message
38queue, preserving the authenticated owner and reply ID until that callback
39returns. Concurrent opens are drained serially and abandoned if their creator
40window is destroyed before dispatch.
3641
3742## Per-window state
3843
packages/zig/src/windows.zigmodified+96-0
Changes to packages/zig/src/windows.zig
@@ -95,6 +95,7 @@ pub const WM_SIZE: UINT = 0x0005;
9595pub const WM_ACTIVATE: UINT = 0x0006;
9696pub const WM_CLOSE: UINT = 0x0010;
9797pub const WM_QUIT: UINT = 0x0012;
98const WM_CRAFT_OPEN_WINDOW: UINT = 0x8001; // WM_APP + 1
9899pub const PM_REMOVE: UINT = 0x0001;
99100pub const GWLP_USERDATA: c_int = -21;
100101
@@ -122,6 +123,7 @@ pub extern "user32" fn TranslateMessage(lpMsg: *const MSG) callconv(.c) BOOL;
122123pub extern "user32" fn DispatchMessageW(lpMsg: *const MSG) callconv(.c) LRESULT;
123124pub extern "user32" fn DefWindowProcW(hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c) LRESULT;
124125pub extern "user32" fn PostQuitMessage(nExitCode: c_int) callconv(.c) void;
126pub extern "user32" fn PostMessageW(hWnd: ?HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c) BOOL;
125127pub extern "user32" fn DestroyWindow(hWnd: HWND) callconv(.c) BOOL;
126128pub extern "user32" fn SetWindowTextW(hWnd: HWND, lpString: LPCWSTR) callconv(.c) BOOL;
127129pub extern "user32" fn GetWindowTextLengthW(hWnd: HWND) callconv(.c) c_int;
@@ -700,6 +702,15 @@ const WebMessageReceivedHandler = extern struct {
700702 bridge_error.sendErrorToJS(std.heap.c_allocator, envelope.action, error.PlatformNotSupported);
701703 return S_OK;
702704 }
705 if (std.mem.eql(u8, envelope.action, "open") or std.mem.eql(u8, envelope.action, "create")) {
706 // WebView2 does not deliver async completion callbacks inside its
707 // own event callback. Window.create pumps until such a callback,
708 // so run it from a posted Win32 message after Invoke returns.
709 queueWindowOpen(entry, envelope.action, envelope.data, envelope.request_id) catch |err| {
710 bridge_error.sendErrorToJS(std.heap.c_allocator, envelope.action, bridge_error.fromHandlerError(err));
711 };
712 return S_OK;
713 }
703714 handleWindowAction(envelope.action, envelope.data) catch |err| {
704715 bridge_error.sendErrorToJS(std.heap.c_allocator, envelope.action, bridge_error.fromHandlerError(err));
705716 };
@@ -862,6 +873,85 @@ const CLASS_NAME: [:0]const u16 = &[_:0]u16{ 'Z', 'y', 't', 'e', 'W', 'i', 'n',
862873// Window.create. Keep stable handles for every live window here.
863874var desktop_windows: desktop_window_registry.Registry = .{};
864875
876const PendingWindowOpen = struct {
877 next: ?*PendingWindowOpen = null,
878 owner_id: u32,
879 request_id: ?u64,
880 action: []const u8,
881 data: ?[]u8,
882};
883
884var pending_open_head: ?*PendingWindowOpen = null;
885var pending_open_tail: ?*PendingWindowOpen = null;
886var pending_open_count: usize = 0;
887var processing_open = false;
888
889fn freePendingOpen(task: *PendingWindowOpen) void {
890 if (task.data) |data| std.heap.c_allocator.free(data);
891 std.heap.c_allocator.destroy(task);
892}
893
894fn queueWindowOpen(owner: desktop_window_registry.Entry, action: []const u8, data: ?[]const u8, request_id: ?u64) !void {
895 if (pending_open_count >= desktop_window_registry.capacity) return error.TooManyWindows;
896 const copied_data: ?[]u8 = if (data) |value| try std.heap.c_allocator.dupe(u8, value) else null;
897 errdefer if (copied_data) |value| std.heap.c_allocator.free(value);
898 const task = try std.heap.c_allocator.create(PendingWindowOpen);
899 errdefer std.heap.c_allocator.destroy(task);
900 task.* = .{
901 .owner_id = owner.id,
902 .request_id = request_id,
903 .action = if (std.mem.eql(u8, action, "open")) "open" else "create",
904 .data = copied_data,
905 };
906 const owner_hwnd: HWND = @ptrFromInt(owner.window);
907 if (PostMessageW(owner_hwnd, WM_CRAFT_OPEN_WINDOW, 0, 0) == 0)
908 return error.NativeCallFailed;
909 if (pending_open_tail) |tail| tail.next = task else pending_open_head = task;
910 pending_open_tail = task;
911 pending_open_count += 1;
912}
913
914fn discardPendingOpens(owner_id: u32) void {
915 var previous: ?*PendingWindowOpen = null;
916 var current = pending_open_head;
917 while (current) |task| {
918 const next = task.next;
919 if (task.owner_id == owner_id) {
920 if (previous) |prior| prior.next = next else pending_open_head = next;
921 if (pending_open_tail != null and pending_open_tail.? == task) pending_open_tail = previous;
922 pending_open_count -= 1;
923 freePendingOpen(task);
924 } else {
925 previous = task;
926 }
927 current = next;
928 }
929}
930
931fn processPendingOpens() void {
932 if (processing_open) return;
933 processing_open = true;
934 defer processing_open = false;
935
936 while (pending_open_head) |task| {
937 pending_open_head = task.next;
938 if (pending_open_head == null) pending_open_tail = null;
939 pending_open_count -= 1;
940 if (desktop_windows.byId(task.owner_id)) |owner| {
941 window_context.push(owner.window, owner.webview);
942 request_context.push(task.request_id);
943 const data: ?[]const u8 = if (task.data) |value| value else null;
944 openNamedWindow(task.action, data) catch |err| {
945 if (desktop_windows.byId(task.owner_id) != null)
946 bridge_error.sendErrorToJS(std.heap.c_allocator, task.action, bridge_error.fromHandlerError(err));
947 };
948 request_context.pop();
949 window_context.pop();
950 }
951 freePendingOpen(task);
952 }
953}
954
865955fn deliverToWebview(webview_handle: usize, name: []const u8, detail_json: []const u8, window_name: ?[]const u8) void {
866956 const script = desktop_window_events.format(std.heap.c_allocator, name, detail_json, window_name) catch return;
867957 defer std.heap.c_allocator.free(script);
@@ -971,6 +1061,7 @@ fn openNamedWindow(action: []const u8, data: ?[]const u8) !void {
9711061 });
9721062 errdefer created.close();
9731063 if (url) |text| try created.loadURL(text) else if (html) |text| try created.loadHTML(text);
1064 if (desktop_windows.byWebview(owner) == null) return error.WindowHandleNotSet;
9741065 if (!window_registry.rememberNamedOwned(@intFromPtr(created.hwnd), name, owner))
9751066 return error.TooManyWindows;
9761067 created.show();
@@ -1462,6 +1553,10 @@ pub const Window = struct {
14621553
14631554fn WindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c) LRESULT {
14641555 switch (msg) {
1556 WM_CRAFT_OPEN_WINDOW => {
1557 processPendingOpens();
1558 return 0;
1559 },
14651560 WM_SIZE => {
14661561 // A resize belongs to this HWND, not whichever window was created last.
14671562 if (desktop_windows.byWindow(@intFromPtr(hwnd))) |entry| {
@@ -1495,6 +1590,7 @@ fn WindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c
14951590 WM_DESTROY => {
14961591 if (desktop_windows.byWindow(@intFromPtr(hwnd))) |entry| deliverWindowEvent(entry, "close", "");
14971592 if (desktop_windows.forgetWindow(@intFromPtr(hwnd))) |entry| {
1593 discardPendingOpens(entry.id);
14981594 const controller: *ICoreWebView2Controller = @ptrFromInt(entry.context);
14991595 const webview: *ICoreWebView2 = @ptrFromInt(entry.webview);
15001596 window_registry.forgetOwner(entry.webview);
scripts/verify-desktop-multi-window.tsmodified+11-0
Changes to scripts/verify-desktop-multi-window.ts
@@ -47,6 +47,17 @@ const mainPage = `<!doctype html><script>
4747 const title = await window.craft.window._call('getTitle', {}, 'settings')
4848 if (title !== 'Child 1') throw new Error('child title not routed to creator: ' + title)
4949
50 const [queuedA, queuedB] = await Promise.all([
51 window.craft.window.open({ name: 'queue-a', title: 'Child queue-a', url: location.origin + '/child?cycle=queue-a' }),
52 window.craft.window.open({ name: 'queue-b', title: 'Child queue-b', url: location.origin + '/child?cycle=queue-b' }),
53 ])
54 if (queuedA.name !== 'queue-a' || queuedB.name !== 'queue-b')
55 throw new Error('concurrent child opens returned the wrong handles')
56 await waitFor('child-queue-a')
57 await waitFor('child-queue-b')
58 await window.craft.window._call('close', {}, 'queue-a')
59 await window.craft.window._call('close', {}, 'queue-b')
60
5061 const closed = new Promise((resolve, reject) => {
5162 const timeout = setTimeout(() => reject(new Error('child close event missing')), 20000)
5263 window.addEventListener('craft:window:close', (event) => {