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.

.github/workflows/ci.ymlmodified+11-0
Changes to .github/workflows/ci.yml
@@ -79,6 +79,17 @@ jobs:
7979 ls -lh packages/zig/zig-out/bin/
8080 size packages/zig/zig-out/bin/craft
8181
82 # The host matrix never analyzes windows.zig. Exercise the same target
83 # as release.yml here so multi-window WebView2 changes cannot wait until
84 # a tag to reveal a cross-compilation failure.
85 - name: Cross-build Windows CLI
86 if: runner.os == 'Linux'
87 working-directory: packages/zig
88 run: |
89 VERSION="$(bun -p "require('../../package.json').version")"
90 zig build -Doptimize=ReleaseSafe -Dversion="$VERSION" -Dtarget=x86_64-windows
91 test -f zig-out/bin/craft.exe
92
8293 typescript-sdk:
8394 runs-on: ubuntu-latest
8495
packages/zig/src/desktop_window_registry.zigmodified+17-1
Changes to packages/zig/src/desktop_window_registry.zig
@@ -11,6 +11,8 @@ pub const Entry = struct {
1111 id: u32,
1212 window: usize,
1313 webview: usize,
14 /// Platform-specific control object (WebView2 controller on Windows).
15 context: usize = 0,
1416};
1517
1618pub const Registry = struct {
@@ -18,13 +20,17 @@ pub const Registry = struct {
1820 next_id: u32 = 1,
1921
2022 pub fn remember(self: *Registry, window: usize, webview: usize) ?u32 {
23 return self.rememberWithContext(window, webview, 0);
24 }
25
26 pub fn rememberWithContext(self: *Registry, window: usize, webview: usize, context: usize) ?u32 {
2127 if (window == 0 or webview == 0 or self.byWindow(window) != null or self.next_id == 0) return null;
2228 for (&self.entries) |*slot| {
2329 if (slot.* == null) {
2430 const id = self.next_id;
2531 // Exhaust rather than reuse an id that a caller may still hold.
2632 self.next_id = if (id == std.math.maxInt(u32)) 0 else id + 1;
27 slot.* = .{ .id = id, .window = window, .webview = webview };
33 slot.* = .{ .id = id, .window = window, .webview = webview, .context = context };
2834 return id;
2935 }
3036 }
@@ -140,3 +146,13 @@ test "reply candidates contain only live webviews" {
140146 _ = registry.forgetWindow(0x2000);
141147 try std.testing.expectEqual(@as(usize, 0), registry.liveWebviews(&handles).len);
142148}
149
150test "a window keeps its own platform control object" {
151 var registry: Registry = .{};
152 const first = registry.rememberWithContext(0x1000, 0x1001, 0x1002).?;
153 const second = registry.rememberWithContext(0x2000, 0x2001, 0x2002).?;
154 try std.testing.expectEqual(@as(usize, 0x1002), registry.byId(first).?.context);
155 try std.testing.expectEqual(@as(usize, 0x2002), registry.byId(second).?.context);
156 _ = registry.forgetWindow(0x1000);
157 try std.testing.expectEqual(@as(usize, 0x2002), registry.byWindow(0x2000).?.context);
158}
packages/zig/src/windows.zigmodified+81-37
Changes to packages/zig/src/windows.zig
@@ -1,4 +1,7 @@
11const std = ;
2const desktop_window_registry = ;
3const window_context = ;
4const window_reply_target = ;
25
36// Windows implementation using Win32 API and WebView2
47// Requires: Microsoft.Web.WebView2 NuGet package
@@ -699,11 +702,9 @@ var app_running = false;
699702var window_class_registered = false;
700703const CLASS_NAME: [:0]const u16 = &[_:0]u16{ 'Z', 'y', 't', 'e', 'W', 'i', 'n', 'd', 'o', 'w' };
701704
702// WebView2 owns these COM interfaces independently of the stack value returned
703// from Window.create. Keep only stable COM pointers in global callback state.
704var g_active_hwnd: ?HWND = null;
705var g_active_controller: ?*ICoreWebView2Controller = null;
706var g_active_webview: ?*ICoreWebView2 = null;
705// Native callbacks arrive with an HWND, not the stack value returned by
706// Window.create. Keep stable handles for every live window here.
707var desktop_windows: desktop_window_registry.Registry = .{};
707708
708709pub const WindowStyle = struct {
709710 frameless: bool = false,
@@ -721,6 +722,7 @@ pub const WindowStyle = struct {
721722};
722723
723724pub const Window = struct {
725 id: u32,
724726 hwnd: HWND,
725727 controller: ?*ICoreWebView2Controller,
726728 webview: ?*ICoreWebView2,
@@ -790,6 +792,7 @@ pub const Window = struct {
790792 hInstance,
791793 null,
792794 ) orelse return error.WindowCreationFailed;
795 errdefer _ = DestroyWindow(hwnd);
793796
794797 // ----------------------------------------------------------------
795798 // Async WebView2 initialization
@@ -851,10 +854,20 @@ pub const Window = struct {
851854
852855 std.debug.print("[Media] Windows WebView2 configured for camera/microphone access\n", .{});
853856
857 const controller = init_ctx.controller orelse return error.WebView2InitFailed;
858 const webview = init_ctx.webview orelse return error.WebView2InitFailed;
859 const window_id = desktop_windows.rememberWithContext(@intFromPtr(hwnd), @intFromPtr(webview), @intFromPtr(controller)) orelse {
860 _ = controller.lpVtbl.Close(controller);
861 _ = webview.lpVtbl.Release(webview);
862 _ = controller.lpVtbl.Release(controller);
863 return error.TooManyWindows;
864 };
865
854866 const window = Window{
867 .id = window_id,
855868 .hwnd = hwnd,
856 .controller = init_ctx.controller,
857 .webview = init_ctx.webview,
869 .controller = controller,
870 .webview = webview,
858871 .title = options.title,
859872 .width = options.width,
860873 .height = options.height,
@@ -862,35 +875,39 @@ pub const Window = struct {
862875 .y = y,
863876 };
864877
865 g_active_hwnd = hwnd;
866 g_active_controller = window.controller;
867 g_active_webview = window.webview;
868
869878 return window;
870879 }
871880
881 fn liveEntry(self: *const Window) ?desktop_window_registry.Entry {
882 const entry = desktop_windows.byId(self.id) orelse return null;
883 return if (entry.window == @intFromPtr(self.hwnd)) entry else null;
884 }
885
872886 pub fn show(self: *Window) void {
887 if (self.liveEntry() == null) return;
873888 _ = ShowWindow(self.hwnd, SW_SHOW);
874889 _ = UpdateWindow(self.hwnd);
875890 }
876891
877892 pub fn hide(self: *Window) void {
893 if (self.liveEntry() == null) return;
878894 _ = ShowWindow(self.hwnd, SW_HIDE);
879895 }
880896
881897 pub fn close(self: *Window) void {
882 if (self.controller) |ctrl| {
883 _ = ctrl.lpVtbl.Close(ctrl);
898 if (self.liveEntry() == null) {
899 self.controller = null;
900 self.webview = null;
901 return;
902 }
903 if (DestroyWindow(self.hwnd) != 0) {
904 self.controller = null;
905 self.webview = null;
884906 }
885 self.controller = null;
886 self.webview = null;
887 g_active_hwnd = null;
888 g_active_controller = null;
889 g_active_webview = null;
890 _ = DestroyWindow(self.hwnd);
891907 }
892908
893909 pub fn setSize(self: *Window, width: u32, height: u32) void {
910 if (self.liveEntry() == null) return;
894911 _ = SetWindowPos(self.hwnd, null, 0, 0, @intCast(width), @intCast(height), 0x0002); // SWP_NOMOVE
895912 self.width = width;
896913 self.height = height;
@@ -898,12 +915,14 @@ pub const Window = struct {
898915 }
899916
900917 pub fn setPosition(self: *Window, x_pos: i32, y_pos: i32) void {
918 if (self.liveEntry() == null) return;
901919 _ = SetWindowPos(self.hwnd, null, @intCast(x_pos), @intCast(y_pos), 0, 0, 0x0001); // SWP_NOSIZE
902920 self.x = x_pos;
903921 self.y = y_pos;
904922 }
905923
906924 pub fn setTitle(self: *Window, title: []const u8) void {
925 if (self.liveEntry() == null) return;
907926 var title_wide_buf: [256]u16 = undefined;
908927 const len = std.unicode.utf8ToUtf16Le(&title_wide_buf, title) catch return;
909928 title_wide_buf[len] = 0;
@@ -912,7 +931,8 @@ pub const Window = struct {
912931 }
913932
914933 pub fn loadURL(self: *Window, url: []const u8) !void {
915 const webview = self.webview orelse return error.WebView2NotInitialized;
934 const entry = self.liveEntry() orelse return error.NoWebView;
935 const webview: *ICoreWebView2 = @ptrFromInt(entry.webview);
916936 var url_wide = try utf8ToUtf16Z(4096, url);
917937 const url_ptr: LPCWSTR = @ptrCast(&url_wide);
918938 const hr = webview.lpVtbl.Navigate(webview, url_ptr);
@@ -923,7 +943,8 @@ pub const Window = struct {
923943 }
924944
925945 pub fn loadHTML(self: *Window, html: []const u8) !void {
926 const webview = self.webview orelse return error.WebView2NotInitialized;
946 const entry = self.liveEntry() orelse return error.NoWebView;
947 const webview: *ICoreWebView2 = @ptrFromInt(entry.webview);
927948 // NavigateToString needs null-terminated UTF-16.
928949 // For large HTML we allocate on the heap.
929950 const wide_len = html.len + 1; // rough upper bound for ASCII-heavy content
@@ -942,10 +963,12 @@ pub const Window = struct {
942963 }
943964
944965 pub fn maximize(self: *Window) void {
966 if (self.liveEntry() == null) return;
945967 _ = ShowWindow(self.hwnd, SW_MAXIMIZE);
946968 }
947969
948970 pub fn minimize(self: *Window) void {
971 if (self.liveEntry() == null) return;
949972 _ = ShowWindow(self.hwnd, SW_MINIMIZE);
950973 }
951974
@@ -958,7 +981,8 @@ pub const Window = struct {
958981 }
959982
960983 pub fn executeJavaScript(self: *Window, script: []const u8) !void {
961 const webview = self.webview orelse return error.WebView2NotInitialized;
984 const entry = self.liveEntry() orelse return error.NoWebView;
985 const webview: *ICoreWebView2 = @ptrFromInt(entry.webview);
962986 var script_wide = try utf8ToUtf16Z(16384, script);
963987 const script_ptr: LPCWSTR = @ptrCast(&script_wide);
964988
@@ -978,7 +1002,8 @@ pub const Window = struct {
9781002 }
9791003
9801004 pub fn injectScript(self: *Window, script: []const u8) !void {
981 const webview = self.webview orelse return error.WebView2NotInitialized;
1005 const entry = self.liveEntry() orelse return error.NoWebView;
1006 const webview: *ICoreWebView2 = @ptrFromInt(entry.webview);
9821007 var script_wide = try utf8ToUtf16Z(16384, script);
9831008 const script_ptr: LPCWSTR = @ptrCast(&script_wide);
9841009
@@ -997,13 +1022,15 @@ pub const Window = struct {
9971022 }
9981023
9991024 pub fn openDevTools(self: *Window) void {
1000 const webview = self.webview orelse return;
1025 const entry = self.liveEntry() orelse return;
1026 const webview: *ICoreWebView2 = @ptrFromInt(entry.webview);
10011027 _ = webview.lpVtbl.OpenDevToolsWindow(webview);
10021028 }
10031029
10041030 // Resize the WebView2 control to match the current client area
10051031 fn resizeWebView(self: *Window) void {
1006 const controller = self.controller orelse return;
1032 const entry = self.liveEntry() orelse return;
1033 const controller: *ICoreWebView2Controller = @ptrFromInt(entry.context);
10071034 var bounds: RECT = undefined;
10081035 _ = GetClientRect(self.hwnd, &bounds);
10091036 _ = controller.lpVtbl.put_Bounds(controller, bounds);
@@ -1013,18 +1040,29 @@ pub const Window = struct {
10131040fn WindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c) LRESULT {
10141041 switch (msg) {
10151042 WM_SIZE => {
1016 // Resize the WebView2 control to fill the window
1017 if (g_active_hwnd == hwnd) {
1018 if (g_active_controller) |controller| {
1019 var bounds: RECT = undefined;
1020 _ = GetClientRect(hwnd, &bounds);
1021 _ = controller.lpVtbl.put_Bounds(controller, bounds);
1022 }
1043 // A resize belongs to this HWND, not whichever window was created last.
1044 if (desktop_windows.byWindow(@intFromPtr(hwnd))) |entry| {
1045 const controller: *ICoreWebView2Controller = @ptrFromInt(entry.context);
1046 var bounds: RECT = undefined;
1047 _ = GetClientRect(hwnd, &bounds);
1048 _ = controller.lpVtbl.put_Bounds(controller, bounds);
10231049 }
10241050 return 0;
10251051 },
1026 WM_DESTROY, WM_CLOSE => {
1027 PostQuitMessage(0);
1052 WM_CLOSE => {
1053 _ = DestroyWindow(hwnd);
1054 return 0;
1055 },
1056 WM_DESTROY => {
1057 if (desktop_windows.forgetWindow(@intFromPtr(hwnd))) |entry| {
1058 const controller: *ICoreWebView2Controller = @ptrFromInt(entry.context);
1059 const webview: *ICoreWebView2 = @ptrFromInt(entry.webview);
1060 _ = controller.lpVtbl.Close(controller);
1061 _ = webview.lpVtbl.Release(webview);
1062 _ = controller.lpVtbl.Release(controller);
1063 // Closing a child window must not end every window's event loop.
1064 if (app_running and desktop_windows.count() == 0) PostQuitMessage(0);
1065 }
10281066 return 0;
10291067 },
10301068 else => {},
@@ -1035,6 +1073,7 @@ fn WindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c
10351073pub const App = struct {
10361074 pub fn run() !void {
10371075 app_running = true;
1076 defer app_running = false;
10381077 var msg: MSG = undefined;
10391078
10401079 while (GetMessageW(&msg, null, 0, 0) != 0) {
@@ -1049,10 +1088,15 @@ pub const App = struct {
10491088 }
10501089};
10511090
1052/// Evaluate JavaScript in the current webview (cross-platform bridge helper).
1053/// Uses the active window's WebView2 instance.
1091/// Evaluate a reply in the authenticated sender, if that webview is still live.
1092/// Native callers without a sender use the most recently created live window.
10541093pub fn evalJS(script: []const u8) !void {
1055 const webview = g_active_webview orelse return error.NoWebView;
1094 var handles: [desktop_window_registry.capacity]usize = undefined;
1095 const live = desktop_windows.liveWebviews(&handles);
1096 const latest = desktop_windows.latest();
1097 const target = window_reply_target.select(live, window_context.currentWebView(), if (latest) |entry| entry.webview else null) orelse
1098 return error.NoWebView;
1099 const webview: *ICoreWebView2 = @ptrFromInt(target);
10561100 var script_wide = try utf8ToUtf16Z(16384, script);
10571101 const handler = try std.heap.c_allocator.create(ExecuteScriptCompletedHandler);
10581102 handler.* = .{ .lpVtbl = &ExecuteScriptCompletedHandler.vtbl_instance, .ref_count = 1 };