also looking at this
feat: add Linux and Windows named-window routing
#325
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.
| @@ -33,6 +33,11 @@ that window before entering `g_application_run()`. On Windows, the controller | ||
| 33 | 33 | passed to the asynchronous WebView2 completion callback is borrowed. Each live |
| 34 | 34 | window retains its own controller reference until native destruction; otherwise |
| 35 | 35 | WebView2 can close before the page bridge registers or navigation begins. |
| 36 | WebView2 also forbids entering a nested message loop from its message callback. | |
| 37 | Windows therefore queues page-requested child creation onto the UI message | |
| 38 | queue, preserving the authenticated owner and reply ID until that callback | |
| 39 | returns. Concurrent opens are drained serially and abandoned if their creator | |
| 40 | window is destroyed before dispatch. | |
| 36 | 41 | |
| 37 | 42 | ## Per-window state |
| 38 | 43 | |
| @@ -95,6 +95,7 @@ pub const WM_SIZE: UINT = 0x0005; | ||
| 95 | 95 | pub const WM_ACTIVATE: UINT = 0x0006; |
| 96 | 96 | pub const WM_CLOSE: UINT = 0x0010; |
| 97 | 97 | pub const WM_QUIT: UINT = 0x0012; |
| 98 | const WM_CRAFT_OPEN_WINDOW: UINT = 0x8001; // WM_APP + 1 | |
| 98 | 99 | pub const PM_REMOVE: UINT = 0x0001; |
| 99 | 100 | pub const GWLP_USERDATA: c_int = -21; |
| 100 | 101 | |
| @@ -122,6 +123,7 @@ pub extern "user32" fn TranslateMessage(lpMsg: *const MSG) callconv(.c) BOOL; | ||
| 122 | 123 | pub extern "user32" fn DispatchMessageW(lpMsg: *const MSG) callconv(.c) LRESULT; |
| 123 | 124 | pub extern "user32" fn DefWindowProcW(hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c) LRESULT; |
| 124 | 125 | pub extern "user32" fn PostQuitMessage(nExitCode: c_int) callconv(.c) void; |
| 126 | pub extern "user32" fn PostMessageW(hWnd: ?HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c) BOOL; | |
| 125 | 127 | pub extern "user32" fn DestroyWindow(hWnd: HWND) callconv(.c) BOOL; |
| 126 | 128 | pub extern "user32" fn SetWindowTextW(hWnd: HWND, lpString: LPCWSTR) callconv(.c) BOOL; |
| 127 | 129 | pub extern "user32" fn GetWindowTextLengthW(hWnd: HWND) callconv(.c) c_int; |
| @@ -700,6 +702,15 @@ const WebMessageReceivedHandler = extern struct { | ||
| 700 | 702 | bridge_error.sendErrorToJS(std.heap.c_allocator, envelope.action, error.PlatformNotSupported); |
| 701 | 703 | return S_OK; |
| 702 | 704 | } |
| 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 | } | |
| 703 | 714 | handleWindowAction(envelope.action, envelope.data) catch |err| { |
| 704 | 715 | bridge_error.sendErrorToJS(std.heap.c_allocator, envelope.action, bridge_error.fromHandlerError(err)); |
| 705 | 716 | }; |
| @@ -862,6 +873,85 @@ const CLASS_NAME: [:0]const u16 = &[_:0]u16{ 'Z', 'y', 't', 'e', 'W', 'i', 'n', | ||
| 862 | 873 | // Window.create. Keep stable handles for every live window here. |
| 863 | 874 | var desktop_windows: desktop_window_registry.Registry = .{}; |
| 864 | 875 | |
| 876 | const PendingWindowOpen = struct { | |
| 877 | next: ?*PendingWindowOpen = null, | |
| 878 | owner_id: u32, | |
| 879 | request_id: ?u64, | |
| 880 | action: []const u8, | |
| 881 | data: ?[]u8, | |
| 882 | }; | |
| 883 | ||
| 884 | var pending_open_head: ?*PendingWindowOpen = null; | |
| 885 | var pending_open_tail: ?*PendingWindowOpen = null; | |
| 886 | var pending_open_count: usize = 0; | |
| 887 | var processing_open = false; | |
| 888 | ||
| 889 | fn freePendingOpen(task: *PendingWindowOpen) void { | |
| 890 | if (task.data) |data| std.heap.c_allocator.free(data); | |
| 891 | std.heap.c_allocator.destroy(task); | |
| 892 | } | |
| 893 | ||
| 894 | fn 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 | ||
| 914 | fn 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 | ||
| 931 | fn 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 | ||
| 865 | 955 | fn deliverToWebview(webview_handle: usize, name: []const u8, detail_json: []const u8, window_name: ?[]const u8) void { |
| 866 | 956 | const script = desktop_window_events.format(std.heap.c_allocator, name, detail_json, window_name) catch return; |
| 867 | 957 | defer std.heap.c_allocator.free(script); |
| @@ -971,6 +1061,7 @@ fn openNamedWindow(action: []const u8, data: ?[]const u8) !void { | ||
| 971 | 1061 | }); |
| 972 | 1062 | errdefer created.close(); |
| 973 | 1063 | 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; | |
| 974 | 1065 | if (!window_registry.rememberNamedOwned(@intFromPtr(created.hwnd), name, owner)) |
| 975 | 1066 | return error.TooManyWindows; |
| 976 | 1067 | created.show(); |
| @@ -1462,6 +1553,10 @@ pub const Window = struct { | ||
| 1462 | 1553 | |
| 1463 | 1554 | fn WindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c) LRESULT { |
| 1464 | 1555 | switch (msg) { |
| 1556 | WM_CRAFT_OPEN_WINDOW => { | |
| 1557 | processPendingOpens(); | |
| 1558 | return 0; | |
| 1559 | }, | |
| 1465 | 1560 | WM_SIZE => { |
| 1466 | 1561 | // A resize belongs to this HWND, not whichever window was created last. |
| 1467 | 1562 | if (desktop_windows.byWindow(@intFromPtr(hwnd))) |entry| { |
| @@ -1495,6 +1590,7 @@ fn WindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c | ||
| 1495 | 1590 | WM_DESTROY => { |
| 1496 | 1591 | if (desktop_windows.byWindow(@intFromPtr(hwnd))) |entry| deliverWindowEvent(entry, "close", ""); |
| 1497 | 1592 | if (desktop_windows.forgetWindow(@intFromPtr(hwnd))) |entry| { |
| 1593 | discardPendingOpens(entry.id); | |
| 1498 | 1594 | const controller: *ICoreWebView2Controller = @ptrFromInt(entry.context); |
| 1499 | 1595 | const webview: *ICoreWebView2 = @ptrFromInt(entry.webview); |
| 1500 | 1596 | window_registry.forgetOwner(entry.webview); |
| @@ -47,6 +47,17 @@ const mainPage = `<!doctype html><script> | ||
| 47 | 47 | const title = await window.craft.window._call('getTitle', {}, 'settings') |
| 48 | 48 | if (title !== 'Child 1') throw new Error('child title not routed to creator: ' + title) |
| 49 | 49 | |
| 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 | ||
| 50 | 61 | const closed = new Promise((resolve, reject) => { |
| 51 | 62 | const timeout = setTimeout(() => reject(new Error('child close event missing')), 20000) |
| 52 | 63 | window.addEventListener('craft:window:close', (event) => { |