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.
| @@ -10,7 +10,7 @@ whether it stores a window-specific target. | ||
| 10 | 10 | |
| 11 | 11 | | Area | macOS | Linux | Windows | |
| 12 | 12 | | --- | --- | --- | --- | |
| 13 | | Runtime creation from `createWindow()` | Implemented | Implemented; GUI smoke in CI | Implemented; WebView2 GUI smoke under validation in CI | | |
| 13 | | Runtime creation from `createWindow()` | Implemented | Implemented; GUI smoke in CI | Implemented; WebView2 GUI smoke in CI | | |
| 14 | 14 | | Stable named handles | Implemented | Implemented | Implemented | |
| 15 | 15 | | Sender-authenticated local actions | Implemented | Implemented through WebKitGTK | Implemented through WebView2 | |
| 16 | 16 | | Named cross-window actions | Implemented | Implemented for core controls | Implemented for core controls | |
| @@ -18,7 +18,7 @@ whether it stores a window-specific target. | ||
| 18 | 18 | | Permanent destroy and cleanup | Implemented for named windows | Implemented on close/destroy | Implemented on close/destroy | |
| 19 | 19 | | Modal and parent relationships | Unspecified | Unspecified | Unspecified | |
| 20 | 20 | |
| 21 | Issue #67 remains open while the Windows runtime smoke and the still-unspecified | |
| 21 | Issue #67 remains open while the full CI matrix and the still-unspecified | |
| 22 | 22 | modal/parent semantics are reviewed. The Windows release archive now stages an |
| 23 | 23 | app-local WebView2 loader; users still need the Microsoft WebView2 Runtime. |
| 24 | 24 | |
| @@ -682,7 +682,6 @@ const WebMessageReceivedHandler = extern struct { | ||
| 682 | 682 | // Bind its live registry id rather than comparing raw COM interface |
| 683 | 683 | // pointers, which need not have the same address for one object. |
| 684 | 684 | const entry = desktop_windows.byId(self.window_id) orelse return S_OK; |
| 685 | std.debug.print("[WebView2] Page message received for window {d}\n", .{self.window_id}); | |
| 686 | 685 | var message_wide: ?LPWSTR = null; |
| 687 | 686 | if (!succeeded(args.lpVtbl.get_WebMessageAsJson(args, &message_wide))) return S_OK; |
| 688 | 687 | const wide = message_wide orelse return S_OK; |
| @@ -1334,21 +1333,20 @@ pub const Window = struct { | ||
| 1334 | 1333 | _ = controller.lpVtbl.Release(controller); |
| 1335 | 1334 | return error.TooManyWindows; |
| 1336 | 1335 | }; |
| 1337 | std.debug.print("[WebView2] Native window registered\n", .{}); | |
| 1338 | 1336 | |
| 1339 | 1337 | const message_handler = try std.heap.c_allocator.create(WebMessageReceivedHandler); |
| 1340 | 1338 | message_handler.* = .{ .lpVtbl = &WebMessageReceivedHandler.vtbl_instance, .ref_count = 1, .window_id = window_id }; |
| 1341 | 1339 | var message_token: EventRegistrationToken = .{ .value = 0 }; |
| 1342 | std.debug.print("[WebView2] Registering page message handler\n", .{}); | |
| 1343 | 1340 | const message_hr = webview.lpVtbl.add_WebMessageReceived(webview, @ptrCast(message_handler), &message_token); |
| 1344 | std.debug.print("[WebView2] Page message handler returned 0x{x}\n", .{@as(u32, @bitCast(message_hr))}); | |
| 1345 | 1341 | _ = WebMessageReceivedHandler.release(message_handler); |
| 1346 | if (!succeeded(message_hr)) return error.WebMessageRegistrationFailed; | |
| 1342 | if (!succeeded(message_hr)) { | |
| 1343 | std.debug.print("[WebView2] Page message registration failed: 0x{x}\n", .{@as(u32, @bitCast(message_hr))}); | |
| 1344 | return error.WebMessageRegistrationFailed; | |
| 1345 | } | |
| 1347 | 1346 | if (!desktop_windows.setMessageToken(@intFromPtr(hwnd), message_token.value)) { |
| 1348 | 1347 | _ = webview.lpVtbl.remove_WebMessageReceived(webview, message_token); |
| 1349 | 1348 | return error.WebMessageRegistrationFailed; |
| 1350 | 1349 | } |
| 1351 | std.debug.print("[WebView2] Capturing initial window geometry\n", .{}); | |
| 1352 | 1350 | observeWindowGeometry(desktop_windows.byId(window_id).?); |
| 1353 | 1351 | |
| 1354 | 1352 | var window = Window{ |
| @@ -1365,9 +1363,7 @@ pub const Window = struct { | ||
| 1365 | 1363 | |
| 1366 | 1364 | // Install before the first Navigate/NavigateToString so every page |
| 1367 | 1365 | // starts with the same bridge surface as a macOS Craft window. |
| 1368 | std.debug.print("[WebView2] Installing document-start bridge\n", .{}); | |
| 1369 | 1366 | try window.injectScript(@embedFile("js/craft-bridge.js")); |
| 1370 | std.debug.print("[WebView2] Document-start bridge installed\n", .{}); | |
| 1371 | 1367 | return window; |
| 1372 | 1368 | } |
| 1373 | 1369 | |
| @@ -55,8 +55,27 @@ const mainPage = `<!doctype html><script> | ||
| 55 | 55 | throw new Error('concurrent child opens returned the wrong handles') |
| 56 | 56 | await waitFor('child-queue-a') |
| 57 | 57 | await waitFor('child-queue-b') |
| 58 | const queueClosed = new Set() | |
| 59 | const queueClosedDone = new Promise((resolve, reject) => { | |
| 60 | const timeout = setTimeout(() => reject(new Error('queued child close events missing')), 20000) | |
| 61 | window.addEventListener('craft:window:close', (event) => { | |
| 62 | if (event.detail.windowId !== 'queue-a' && event.detail.windowId !== 'queue-b') return | |
| 63 | queueClosed.add(event.detail.windowId) | |
| 64 | if (queueClosed.size !== 2) return | |
| 65 | clearTimeout(timeout) | |
| 66 | resolve() | |
| 67 | }) | |
| 68 | }) | |
| 58 | 69 | await window.craft.window._call('close', {}, 'queue-a') |
| 59 | 70 | await window.craft.window._call('close', {}, 'queue-b') |
| 71 | await queueClosedDone | |
| 72 | for (const name of ['queue-a', 'queue-b']) { | |
| 73 | let rejected = false | |
| 74 | try { await window.craft.window._call('getBounds', {}, name) } | |
| 75 | catch (_) { rejected = true } | |
| 76 | if (!rejected) throw new Error('closed queued child still answered a read: ' + name) | |
| 77 | } | |
| 78 | await report('queued-closed') | |
| 60 | 79 | |
| 61 | 80 | const closed = new Promise((resolve, reject) => { |
| 62 | 81 | const timeout = setTimeout(() => reject(new Error('child close event missing')), 20000) |
| @@ -132,7 +151,7 @@ const server = createServer((request, response) => { | ||
| 132 | 151 | if (step) steps.add(step) |
| 133 | 152 | response.end('ok') |
| 134 | 153 | if (step === 'done') { |
| 135 | const required = ['main', 'child-1', 'closed', 'child-2', 'done'] | |
| 154 | const required = ['main', 'child-1', 'child-queue-a', 'child-queue-b', 'queued-closed', 'closed', 'child-2', 'done'] | |
| 136 | 155 | if (required.every(name => steps.has(name))) resolveDone() |
| 137 | 156 | else rejectDone(new Error(`incomplete smoke steps: ${[...steps].join(', ')}`)) |
| 138 | 157 | } |