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.

packages/zig/build.zigmodified+10-0
Changes to packages/zig/build.zig
@@ -1263,6 +1263,14 @@ pub fn build(b: *std.Build) void {
12631263 }),
12641264 });
12651265
1266 const desktop_script_encoding_tests = b.addTest(.{
1267 .root_module = b.createModule(.{
1268 .root_source_file = b.path("src/desktop_script_encoding.zig"),
1269 .target = target,
1270 .optimize = optimize,
1271 }),
1272 });
1273
12661274 // The shell bridge's argv construction: what a `spawn` actually runs.
12671275 // Untested until now, which is how it shipped parsing an `args` array and
12681276 // throwing it away.
@@ -1718,6 +1726,7 @@ pub fn build(b: *std.Build) void {
17181726 const run_window_context_tests = b.addRunArtifact(window_context_tests);
17191727 const run_window_reply_target_tests = b.addRunArtifact(window_reply_target_tests);
17201728 const run_desktop_window_registry_tests = b.addRunArtifact(desktop_window_registry_tests);
1729 const run_desktop_script_encoding_tests = b.addRunArtifact(desktop_script_encoding_tests);
17211730 const run_external_link_tests = b.addRunArtifact(external_link_tests);
17221731 const run_webview_recovery_tests = b.addRunArtifact(webview_recovery_tests);
17231732 const run_request_context_tests = b.addRunArtifact(request_context_tests);
@@ -1868,6 +1877,7 @@ pub fn build(b: *std.Build) void {
18681877 test_step.dependOn(&run_window_context_tests.step);
18691878 test_step.dependOn(&run_window_reply_target_tests.step);
18701879 test_step.dependOn(&run_desktop_window_registry_tests.step);
1880 test_step.dependOn(&run_desktop_script_encoding_tests.step);
18711881 test_step.dependOn(&run_external_link_tests.step);
18721882 test_step.dependOn(&run_webview_recovery_tests.step);
18731883 test_step.dependOn(&run_request_context_tests.step);
packages/zig/src/desktop_script_encoding.zigadded+55-0
Changes to packages/zig/src/desktop_script_encoding.zig
@@ -0,0 +1,55 @@
1//! UTF-16 storage for WebView2 document-start scripts.
2//!
3//! The embedded Craft bridge is larger than the old 16 KiB stack buffer.
4//! Keep the backing allocation alive until WebView2 confirms installation,
5//! and preserve its original length so the allocator can free it correctly.
6
7const std = ;
8
9pub const WideScript = struct {
10 storage: []u16,
11 length: usize,
12
13 pub fn ptr(self: *const WideScript) [*:0]const u16 {
14 return @ptrCast(self.storage.ptr);
15 }
16
17 pub fn deinit(self: *WideScript, allocator: std.mem.Allocator) void {
18 allocator.free(self.storage);
19 }
20};
21
22pub fn encode(allocator: std.mem.Allocator, source: []const u8) !WideScript {
23 if (std.mem.indexOfScalar(u8, source, 0) != null) return error.InvalidScript;
24 // Each UTF-16 code unit needs at least one UTF-8 byte, so source.len is
25 // a safe upper bound for code units, plus the terminating zero.
26 const storage = try allocator.alloc(u16, source.len + 1);
27 errdefer allocator.free(storage);
28 const length = try std.unicode.utf8ToUtf16Le(storage[0..source.len], source);
29 storage[length] = 0;
30 return .{ .storage = storage, .length = length };
31}
32
33test "a document-start script larger than the former stack limit is retained" {
34 const allocator = std.testing.allocator;
35 const script = try allocator.alloc(u8, 20_000);
36 defer allocator.free(script);
37 @memset(script, 'a');
38
39 var wide = try encode(allocator, script);
40 defer wide.deinit(allocator);
41 try std.testing.expectEqual(@as(usize, 20_000), wide.length);
42 try std.testing.expectEqual(@as(u16, 0), wide.ptr()[wide.length]);
43 try std.testing.expectEqual(@as(u16, 'a'), wide.ptr()[19_999]);
44}
45
46test "UTF-8 characters count UTF-16 code units rather than bytes" {
47 var wide = try encode(std.testing.allocator, "Aé😀");
48 defer wide.deinit(std.testing.allocator);
49 try std.testing.expectEqual(@as(usize, 4), wide.length);
50 try std.testing.expectEqual(@as(u16, 0), wide.ptr()[4]);
51}
52
53test "an embedded NUL cannot silently truncate the installed script" {
54 try std.testing.expectError(error.InvalidScript, encode(std.testing.allocator, "one\x00two"));
55}
packages/zig/src/linux.zigmodified+8-1
Changes to packages/zig/src/linux.zig
@@ -347,7 +347,7 @@ pub const Window = struct {
347347 return error.SignalConnectionFailed;
348348 }
349349
350 return Window{
350 var created = Window{
351351 .id = window_id,
352352 .gtk_window = window,
353353 .webview = webview,
@@ -357,6 +357,12 @@ pub const Window = struct {
357357 .x = x,
358358 .y = y,
359359 };
360 errdefer created.close();
361
362 // Register the page API before any load. The user-content manager
363 // reapplies this document-start script after navigation too.
364 try created.injectScript(@embedFile("js/craft-bridge.js"));
365 return created;
360366 }
361367
362368 pub fn show(self: *Window) void {
@@ -424,6 +430,7 @@ pub const Window = struct {
424430 }
425431
426432 pub fn injectScript(self: *Window, script: []const u8) !void {
433 if (std.mem.indexOfScalar(u8, script, 0) != null) return error.InvalidScript;
427434 const script_z = try .dupeZ(std.heap.c_allocator, u8, script);
428435 defer std.heap.c_allocator.free(script_z);
429436
packages/zig/src/windows.zigmodified+84-5
Changes to packages/zig/src/windows.zig
@@ -1,4 +1,5 @@
11const std = ;
2const desktop_script_encoding = ;
23const desktop_window_registry = ;
34const window_context = ;
45const window_reply_target = ;
@@ -83,6 +84,7 @@ pub const SW_MINIMIZE: c_int = 6;
8384pub const WM_DESTROY: UINT = 0x0002;
8485pub const WM_SIZE: UINT = 0x0005;
8586pub const WM_CLOSE: UINT = 0x0010;
87pub const WM_QUIT: UINT = 0x0012;
8688pub const PM_REMOVE: UINT = 0x0001;
8789pub const GWLP_USERDATA: c_int = -21;
8890
@@ -650,6 +652,57 @@ const ExecuteScriptCompletedHandler = extern struct {
650652
651653pub const ICoreWebView2ExecuteScriptCompletedHandler = ExecuteScriptCompletedHandler;
652654
655// -- Document-start script completed handler ---------------------------------
656
657const ScriptInstallContext = struct {
658 done: bool = false,
659 result: HRESULT = -1,
660};
661
662const ScriptInstallCompletedHandler = extern struct {
663 lpVtbl: *const ScriptInstallCompletedHandlerVtbl,
664 ref_count: c_ulong,
665 ctx: *ScriptInstallContext,
666
667 const ScriptInstallCompletedHandlerVtbl = extern struct {
668 QueryInterface: *const fn (*ScriptInstallCompletedHandler, *const GUID, *?*anyopaque) callconv(.c) HRESULT,
669 AddRef: *const fn (*ScriptInstallCompletedHandler) callconv(.c) c_ulong,
670 Release: *const fn (*ScriptInstallCompletedHandler) callconv(.c) c_ulong,
671 Invoke: *const fn (*ScriptInstallCompletedHandler, HRESULT, LPCWSTR) callconv(.c) HRESULT,
672 };
673
674 const vtbl_instance = ScriptInstallCompletedHandlerVtbl{
675 .QueryInterface = &queryInterface,
676 .AddRef = &addRef,
677 .Release = &release,
678 .Invoke = &invoke,
679 };
680
681 fn queryInterface(self: *ScriptInstallCompletedHandler, _: *const GUID, ppv: *?*anyopaque) callconv(.c) HRESULT {
682 ppv.* = @ptrCast(self);
683 _ = addRef(self);
684 return S_OK;
685 }
686
687 fn addRef(self: *ScriptInstallCompletedHandler) callconv(.c) c_ulong {
688 self.ref_count += 1;
689 return self.ref_count;
690 }
691
692 fn release(self: *ScriptInstallCompletedHandler) callconv(.c) c_ulong {
693 if (self.ref_count > 0) self.ref_count -= 1;
694 const remaining = self.ref_count;
695 if (remaining == 0) std.heap.c_allocator.destroy(self);
696 return remaining;
697 }
698
699 fn invoke(self: *ScriptInstallCompletedHandler, hr: HRESULT, _: LPCWSTR) callconv(.c) HRESULT {
700 self.ctx.result = hr;
701 self.ctx.done = true;
702 return S_OK;
703 }
704};
705
653706// ============================================================================
654707// WebView2Loader — loaded dynamically at runtime to avoid link-time dependency
655708// ============================================================================
@@ -863,7 +916,7 @@ pub const Window = struct {
863916 return error.TooManyWindows;
864917 };
865918
866 const window = Window{
919 var window = Window{
867920 .id = window_id,
868921 .hwnd = hwnd,
869922 .controller = controller,
@@ -875,6 +928,9 @@ pub const Window = struct {
875928 .y = y,
876929 };
877930
931 // Install before the first Navigate/NavigateToString so every page
932 // starts with the same bridge surface as a macOS Craft window.
933 try window.injectScript(@embedFile("js/craft-bridge.js"));
878934 return window;
879935 }
880936
@@ -1004,14 +1060,37 @@ pub const Window = struct {
10041060 pub fn injectScript(self: *Window, script: []const u8) !void {
10051061 const entry = self.liveEntry() orelse return error.NoWebView;
10061062 const webview: *ICoreWebView2 = @ptrFromInt(entry.webview);
1007 var script_wide = try utf8ToUtf16Z(16384, script);
1008 const script_ptr: LPCWSTR = @ptrCast(&script_wide);
1009
1010 const hr = webview.lpVtbl.AddScriptToExecuteOnDocumentCreated(webview, script_ptr, null);
1063 var script_wide = try desktop_script_encoding.encode(std.heap.c_allocator, script);
1064 defer script_wide.deinit(std.heap.c_allocator);
1065
1066 var install: ScriptInstallContext = .{};
1067 const handler = try std.heap.c_allocator.create(ScriptInstallCompletedHandler);
1068 handler.* = .{ .lpVtbl = &ScriptInstallCompletedHandler.vtbl_instance, .ref_count = 1, .ctx = &install };
1069 const hr = webview.lpVtbl.AddScriptToExecuteOnDocumentCreated(webview, script_wide.ptr(), handler);
1070 _ = ScriptInstallCompletedHandler.release(handler);
10111071 if (!succeeded(hr)) {
10121072 std.debug.print("[WebView2] AddScriptToExecuteOnDocumentCreated failed: 0x{x}\n", .{@as(u32, @bitCast(hr))});
10131073 return error.ScriptInjectionFailed;
10141074 }
1075
1076 // WebView2 installs this asynchronously. Navigate immediately after
1077 // the API call and the first page can miss the bridge entirely.
1078 var msg: MSG = undefined;
1079 var quit_requested = false;
1080 while (!install.done) {
1081 if (PeekMessageW(&msg, null, 0, 0, PM_REMOVE) != 0) {
1082 if (msg.message == WM_QUIT) {
1083 quit_requested = true;
1084 } else {
1085 _ = TranslateMessage(&msg);
1086 _ = DispatchMessageW(&msg);
1087 }
1088 } else {
1089 Sleep(1);
1090 }
1091 }
1092 if (quit_requested) PostQuitMessage(0);
1093 if (!succeeded(install.result)) return error.ScriptInjectionFailed;
10151094 }
10161095
10171096 pub fn enableGPUAcceleration(self: *Window, enable: bool) !void {