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
@@ -1271,6 +1271,14 @@ pub fn build(b: *std.Build) void {
12711271 }),
12721272 });
12731273
1274 const desktop_bridge_text_tests = b.addTest(.{
1275 .root_module = b.createModule(.{
1276 .root_source_file = b.path("src/desktop_bridge_text.zig"),
1277 .target = target,
1278 .optimize = optimize,
1279 }),
1280 });
1281
12741282 const desktop_script_encoding_tests = b.addTest(.{
12751283 .root_module = b.createModule(.{
12761284 .root_source_file = b.path("src/desktop_script_encoding.zig"),
@@ -1735,6 +1743,7 @@ pub fn build(b: *std.Build) void {
17351743 const run_window_reply_target_tests = b.addRunArtifact(window_reply_target_tests);
17361744 const run_desktop_window_registry_tests = b.addRunArtifact(desktop_window_registry_tests);
17371745 const run_desktop_bridge_envelope_tests = b.addRunArtifact(desktop_bridge_envelope_tests);
1746 const run_desktop_bridge_text_tests = b.addRunArtifact(desktop_bridge_text_tests);
17381747 const run_desktop_script_encoding_tests = b.addRunArtifact(desktop_script_encoding_tests);
17391748 const run_external_link_tests = b.addRunArtifact(external_link_tests);
17401749 const run_webview_recovery_tests = b.addRunArtifact(webview_recovery_tests);
@@ -1887,6 +1896,7 @@ pub fn build(b: *std.Build) void {
18871896 test_step.dependOn(&run_window_reply_target_tests.step);
18881897 test_step.dependOn(&run_desktop_window_registry_tests.step);
18891898 test_step.dependOn(&run_desktop_bridge_envelope_tests.step);
1899 test_step.dependOn(&run_desktop_bridge_text_tests.step);
18901900 test_step.dependOn(&run_desktop_script_encoding_tests.step);
18911901 test_step.dependOn(&run_external_link_tests.step);
18921902 test_step.dependOn(&run_webview_recovery_tests.step);
packages/zig/src/desktop_bridge_text.zigadded+25-0
Changes to packages/zig/src/desktop_bridge_text.zig
@@ -0,0 +1,25 @@
1//! Decode WebView2's UTF-16 message with the same byte limit as the JSON
2//! envelope parser. This stays platform-neutral so CI can test the boundary.
3
4const std = ;
5const desktop_bridge_envelope = ;
6
7pub fn fromUtf16(allocator: std.mem.Allocator, wide: []const u16) ![:0]u8 {
8 if (wide.len > desktop_bridge_envelope.max_message_bytes) return error.MessageTooLarge;
9 const utf8 = try std.unicode.utf16leToUtf8AllocZ(allocator, wide);
10 errdefer allocator.free(utf8);
11 if (utf8.len > desktop_bridge_envelope.max_message_bytes) return error.MessageTooLarge;
12 return utf8;
13}
14
15test "converts WebView2 JSON with non-ASCII window names" {
16 const message = try fromUtf16(std.testing.allocator, &.{ '{', '"', 'a', '"', ':', '"', 0x00E9, '"', '}' });
17 defer std.testing.allocator.free(message);
18 try std.testing.expectEqualStrings("{\"a\":\"é\"}", message);
19}
20
21test "rejects an oversized UTF-16 message before decoding" {
22 const wide = try std.testing.allocator.alloc(u16, desktop_bridge_envelope.max_message_bytes + 1);
23 defer std.testing.allocator.free(wide);
24 try std.testing.expectError(error.MessageTooLarge, fromUtf16(std.testing.allocator, wide));
25}
packages/zig/src/desktop_window_registry.zigmodified+27-0
Changes to packages/zig/src/desktop_window_registry.zig
@@ -13,6 +13,8 @@ pub const Entry = struct {
1313 webview: usize,
1414 /// Platform-specific control object (WebView2 controller on Windows).
1515 context: usize = 0,
16 /// WebView2 subscription to remove before releasing a closed webview.
17 message_token: ?i64 = null,
1618};
1719
1820pub const Registry = struct {
@@ -66,6 +68,19 @@ pub const Registry = struct {
6668 return null;
6769 }
6870
71 pub fn setMessageToken(self: *Registry, window: usize, token: i64) bool {
72 if (window == 0) return false;
73 for (&self.entries) |*slot| {
74 if (slot.*) |*entry| {
75 if (entry.window == window) {
76 entry.message_token = token;
77 return true;
78 }
79 }
80 }
81 return false;
82 }
83
6984 pub fn latest(self: *const Registry) ?Entry {
7085 var result: ?Entry = null;
7186 for (self.entries) |slot| {
@@ -158,6 +173,18 @@ test "reply candidates contain only live webviews" {
158173 try std.testing.expectEqual(@as(usize, 0), registry.liveWebviews(&handles).len);
159174}
160175
176test "a web message subscription belongs only to its live window" {
177 var registry: Registry = .{};
178 _ = registry.remember(0x1000, 0x1001);
179 _ = registry.remember(0x2000, 0x2001);
180 try std.testing.expect(registry.setMessageToken(0x1000, 42));
181 try std.testing.expect(!registry.setMessageToken(0x3000, 99));
182 try std.testing.expectEqual(@as(?i64, 42), registry.byWebview(0x1001).?.message_token);
183 try std.testing.expect(registry.byWebview(0x2001).?.message_token == null);
184 try std.testing.expectEqual(@as(?i64, 42), registry.forgetWindow(0x1000).?.message_token);
185 try std.testing.expect(registry.byWebview(0x1001) == null);
186}
187
161188test "a window keeps its own platform control object" {
162189 var registry: Registry = .{};
163190 const first = registry.rememberWithContext(0x1000, 0x1001, 0x1002).?;
packages/zig/src/linux.zigmodified+3-3
Changes to packages/zig/src/linux.zig
@@ -26,7 +26,7 @@ pub extern "c" fn gtk_window_fullscreen(window: *anyopaque) void;
2626pub extern "c" fn gtk_window_unfullscreen(window: *anyopaque) void;
2727pub extern "c" fn gtk_window_maximize(window: *anyopaque) void;
2828pub extern "c" fn gtk_window_unmaximize(window: *anyopaque) void;
29pub extern "c" fn gtk_window_minimize(window: *anyopaque) void;
29pub extern "c" fn gtk_window_iconify(window: *anyopaque) void;
3030pub extern "c" fn gtk_widget_hide(widget: *anyopaque) void;
3131pub extern "c" fn gtk_widget_show(widget: *anyopaque) void;
3232pub extern "c" fn gtk_window_set_position(window: *anyopaque, x: c_int, y: c_int) void;
@@ -359,7 +359,7 @@ fn handleWindowAction(action: []const u8, data: ?[]const u8) !void {
359359 } else if (std.mem.eql(u8, action, "close") or std.mem.eql(u8, action, "destroy")) {
360360 gtk_window_close(window);
361361 } else if (std.mem.eql(u8, action, "minimize")) {
362 gtk_window_minimize(window);
362 gtk_window_iconify(window);
363363 } else if (std.mem.eql(u8, action, "maximize")) {
364364 gtk_window_maximize(window);
365365 } else if (std.mem.eql(u8, action, "unmaximize") or std.mem.eql(u8, action, "restore")) {
@@ -592,7 +592,7 @@ pub const Window = struct {
592592 }
593593
594594 pub fn minimize(self: *Window) void {
595 gtk_window_minimize(self.gtk_window);
595 gtk_window_iconify(self.gtk_window);
596596 }
597597
598598 pub fn setFullscreen(self: *Window, fullscreen: bool) void {
packages/zig/src/windows.zigmodified+246-7
Changes to packages/zig/src/windows.zig
@@ -1,7 +1,13 @@
11const std = ;
2const bridge_error = ;
3const desktop_bridge_envelope = ;
4const desktop_bridge_text = ;
25const desktop_script_encoding = ;
36const desktop_window_registry = ;
7const json_utils = ;
8const request_context = ;
49const window_context = ;
10const window_registry = ;
511const window_reply_target = ;
612
713// Windows implementation using Win32 API and WebView2
@@ -121,6 +127,7 @@ pub extern "user32" fn SetWindowLongPtrW(hWnd: HWND, nIndex: c_int, dwNewLong: i
121127pub extern "user32" fn GetWindowLongPtrW(hWnd: HWND, nIndex: c_int) callconv(.c) isize;
122128pub extern "kernel32" fn GetModuleHandleW(lpModuleName: ?LPCWSTR) callconv(.c) ?HINSTANCE;
123129pub extern "kernel32" fn Sleep(dwMilliseconds: DWORD) callconv(.c) void;
130pub extern "ole32" fn CoTaskMemFree(pv: ?*anyopaque) callconv(.c) void;
124131
125132// ============================================================================
126133// WebView2 COM vtable interfaces
@@ -270,6 +277,19 @@ pub const ICoreWebView2 = extern struct {
270277 lpVtbl: *ICoreWebView2Vtbl,
271278};
272279
280pub const ICoreWebView2WebMessageReceivedEventArgsVtbl = extern struct {
281 QueryInterface: *const fn (*ICoreWebView2WebMessageReceivedEventArgs, *const GUID, *?*anyopaque) callconv(.c) HRESULT,
282 AddRef: *const fn (*ICoreWebView2WebMessageReceivedEventArgs) callconv(.c) c_ulong,
283 Release: *const fn (*ICoreWebView2WebMessageReceivedEventArgs) callconv(.c) c_ulong,
284 get_Source: *const fn (*ICoreWebView2WebMessageReceivedEventArgs, *?LPWSTR) callconv(.c) HRESULT,
285 get_WebMessageAsJson: *const fn (*ICoreWebView2WebMessageReceivedEventArgs, *?LPWSTR) callconv(.c) HRESULT,
286 TryGetWebMessageAsString: *const fn (*ICoreWebView2WebMessageReceivedEventArgs, *?LPWSTR) callconv(.c) HRESULT,
287};
288
289pub const ICoreWebView2WebMessageReceivedEventArgs = extern struct {
290 lpVtbl: *ICoreWebView2WebMessageReceivedEventArgsVtbl,
291};
292
273293// -- ICoreWebView2Settings ---------------------------------------------------
274294
275295pub const ICoreWebView2SettingsVtbl = extern struct {
@@ -604,6 +624,73 @@ const PermissionRequestedHandler = extern struct {
604624
605625pub const ICoreWebView2PermissionRequestedEventHandler = PermissionRequestedHandler;
606626
627// -- Page-to-host message handler --------------------------------------------
628
629const WebMessageReceivedHandler = extern struct {
630 lpVtbl: *const WebMessageReceivedHandlerVtbl,
631 ref_count: c_ulong,
632
633 const WebMessageReceivedHandlerVtbl = extern struct {
634 QueryInterface: *const fn (*WebMessageReceivedHandler, *const GUID, *?*anyopaque) callconv(.c) HRESULT,
635 AddRef: *const fn (*WebMessageReceivedHandler) callconv(.c) c_ulong,
636 Release: *const fn (*WebMessageReceivedHandler) callconv(.c) c_ulong,
637 Invoke: *const fn (*WebMessageReceivedHandler, *ICoreWebView2, *ICoreWebView2WebMessageReceivedEventArgs) callconv(.c) HRESULT,
638 };
639
640 const vtbl_instance = WebMessageReceivedHandlerVtbl{
641 .QueryInterface = &queryInterface,
642 .AddRef = &addRef,
643 .Release = &release,
644 .Invoke = &invoke,
645 };
646
647 fn queryInterface(self: *WebMessageReceivedHandler, _: *const GUID, ppv: *?*anyopaque) callconv(.c) HRESULT {
648 ppv.* = @ptrCast(self);
649 _ = addRef(self);
650 return S_OK;
651 }
652
653 fn addRef(self: *WebMessageReceivedHandler) callconv(.c) c_ulong {
654 self.ref_count += 1;
655 return self.ref_count;
656 }
657
658 fn release(self: *WebMessageReceivedHandler) callconv(.c) c_ulong {
659 if (self.ref_count > 0) self.ref_count -= 1;
660 const remaining = self.ref_count;
661 if (remaining == 0) std.heap.c_allocator.destroy(self);
662 return remaining;
663 }
664
665 fn invoke(_: *WebMessageReceivedHandler, sender: *ICoreWebView2, args: *ICoreWebView2WebMessageReceivedEventArgs) callconv(.c) HRESULT {
666 // Authenticate with the COM sender, never an id supplied by the page.
667 const entry = desktop_windows.byWebview(@intFromPtr(sender)) orelse return S_OK;
668 var message_wide: ?LPWSTR = null;
669 if (!succeeded(args.lpVtbl.get_WebMessageAsJson(args, &message_wide))) return S_OK;
670 const wide = message_wide orelse return S_OK;
671 defer CoTaskMemFree(@ptrCast(wide));
672 const wide_text = std.mem.span(wide);
673 const message = desktop_bridge_text.fromUtf16(std.heap.c_allocator, wide_text) catch return S_OK;
674 defer std.heap.c_allocator.free(message);
675
676 var envelope = desktop_bridge_envelope.parse(std.heap.c_allocator, message) catch return S_OK;
677 defer envelope.deinit();
678 window_context.push(entry.window, entry.webview);
679 defer window_context.pop();
680 request_context.push(envelope.request_id);
681 defer request_context.pop();
682
683 if (!std.mem.eql(u8, envelope.kind, "window")) {
684 bridge_error.sendErrorToJS(std.heap.c_allocator, envelope.action, error.PlatformNotSupported);
685 return S_OK;
686 }
687 handleWindowAction(envelope.action, envelope.data) catch |err| {
688 bridge_error.sendErrorToJS(std.heap.c_allocator, envelope.action, bridge_error.fromHandlerError(err));
689 };
690 return S_OK;
691 }
692};
693
607694// -- ExecuteScript completed handler (fire-and-forget) -----------------------
608695
609696const ExecuteScriptCompletedHandler = extern struct {
@@ -759,6 +846,146 @@ const CLASS_NAME: [:0]const u16 = &[_:0]u16{ 'Z', 'y', 't', 'e', 'W', 'i', 'n',
759846// Window.create. Keep stable handles for every live window here.
760847var desktop_windows: desktop_window_registry.Registry = .{};
761848
849fn namedWindowResult(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
850 var json: std.ArrayListUnmanaged(u8) = .empty;
851 errdefer json.deinit(allocator);
852 try json.appendSlice(allocator, "{\"name\":\"");
853 try bridge_error.appendJsonEscaped(allocator, &json, name);
854 try json.appendSlice(allocator, "\"}");
855 return json.toOwnedSlice(allocator);
856}
857
858fn openNamedWindow(action: []const u8, data: ?[]const u8) !void {
859 const allocator = std.heap.c_allocator;
860 const json = data orelse return error.MissingData;
861 const explicit_name = try json_utils.getStringDecoded(allocator, json, "name");
862 defer if (explicit_name) |name| allocator.free(name);
863 const fallback_id = if (explicit_name == null)
864 try json_utils.getStringDecoded(allocator, json, "id")
865 else
866 null;
867 defer if (fallback_id) |name| allocator.free(name);
868 const name = explicit_name orelse fallback_id orelse return error.InvalidParameter;
869 if (name.len == 0 or name.len > window_registry.max_name or
870 std.mem.eql(u8, name, "main") or std.mem.indexOfScalar(u8, name, 0) != null)
871 return error.InvalidParameter;
872
873 const url = try json_utils.getStringDecoded(allocator, json, "url");
874 defer if (url) |text| allocator.free(text);
875 const html = try json_utils.getStringDecoded(allocator, json, "html");
876 defer if (html) |text| allocator.free(text);
877 if (url == null and html == null) return error.InvalidParameter;
878 const title = try json_utils.getStringDecoded(allocator, json, "title");
879 defer if (title) |text| allocator.free(text);
880 for ([_]?[]const u8{ url, html, title }) |value| {
881 if (value) |text| if (std.mem.indexOfScalar(u8, text, 0) != null)
882 return error.InvalidParameter;
883 }
884
885 const result = try namedWindowResult(allocator, name);
886 defer allocator.free(result);
887 const owner = window_context.currentWebView() orelse return error.WebViewHandleNotSet;
888 if (window_registry.byName(name)) |existing| {
889 if (desktop_windows.byWindow(existing) == null) return error.WindowHandleNotSet;
890 if (!window_registry.rememberNamedOwned(existing, name, owner)) return error.InvalidParameter;
891 _ = ShowWindow(@ptrFromInt(existing), SW_SHOW);
892 _ = UpdateWindow(@ptrFromInt(existing));
893 bridge_error.sendResultToJS(allocator, action, result);
894 return;
895 }
896
897 const width = json_utils.getInt(u32, json, "width") orelse 800;
898 const height = json_utils.getInt(u32, json, "height") orelse 600;
899 if (width == 0 or height == 0 or width > @as(u32, std.math.maxInt(c_int)) or height > @as(u32, std.math.maxInt(c_int)))
900 return error.InvalidParameter;
901 var created = try Window.create(.{
902 .title = title orelse name,
903 .width = width,
904 .height = height,
905 .x = json_utils.getInt(i32, json, "x"),
906 .y = json_utils.getInt(i32, json, "y"),
907 .resizable = json_utils.getBool(json, "resizable") orelse true,
908 .frameless = json_utils.getBool(json, "frameless") orelse false,
909 .always_on_top = json_utils.getBool(json, "alwaysOnTop") orelse false,
910 .fullscreen = json_utils.getBool(json, "fullscreen") orelse false,
911 .dev_tools = json_utils.getBool(json, "devTools") orelse false,
912 });
913 errdefer created.close();
914 if (url) |text| try created.loadURL(text) else if (html) |text| try created.loadHTML(text);
915 if (!window_registry.rememberNamedOwned(@intFromPtr(created.hwnd), name, owner))
916 return error.TooManyWindows;
917 created.show();
918 bridge_error.sendResultToJS(allocator, action, result);
919}
920
921fn targetWindow(data: ?[]const u8) !desktop_window_registry.Entry {
922 if (data) |json| {
923 const name = try json_utils.getStringDecoded(std.heap.c_allocator, json, "windowId");
924 defer if (name) |text| std.heap.c_allocator.free(text);
925 if (name) |text| {
926 if (!std.mem.eql(u8, text, "main")) {
927 const handle = window_registry.byName(text) orelse return error.NotFound;
928 return desktop_windows.byWindow(handle) orelse error.NotFound;
929 }
930 }
931 }
932 return desktop_windows.byWindow(window_context.current() orelse return error.WindowHandleNotSet) orelse error.WindowHandleNotSet;
933}
934
935fn handleWindowAction(action: []const u8, data: ?[]const u8) !void {
936 if (std.mem.eql(u8, action, "open") or std.mem.eql(u8, action, "create"))
937 return openNamedWindow(action, data);
938
939 const entry = try targetWindow(data);
940 const hwnd: HWND = @ptrFromInt(entry.window);
941 const webview: *ICoreWebView2 = @ptrFromInt(entry.webview);
942 if (std.mem.eql(u8, action, "show") or std.mem.eql(u8, action, "focus")) {
943 _ = ShowWindow(hwnd, SW_SHOW);
944 _ = UpdateWindow(hwnd);
945 } else if (std.mem.eql(u8, action, "hide")) {
946 _ = ShowWindow(hwnd, SW_HIDE);
947 } else if (std.mem.eql(u8, action, "close") or std.mem.eql(u8, action, "destroy")) {
948 if (DestroyWindow(hwnd) == 0) return error.NativeCallFailed;
949 } else if (std.mem.eql(u8, action, "minimize")) {
950 _ = ShowWindow(hwnd, SW_MINIMIZE);
951 } else if (std.mem.eql(u8, action, "maximize")) {
952 _ = ShowWindow(hwnd, SW_MAXIMIZE);
953 } else if (std.mem.eql(u8, action, "restore") or std.mem.eql(u8, action, "unmaximize")) {
954 _ = ShowWindow(hwnd, 9); // SW_RESTORE
955 } else if (std.mem.eql(u8, action, "reload")) {
956 if (!succeeded(webview.lpVtbl.Reload(webview))) return error.NativeCallFailed;
957 } else if (std.mem.eql(u8, action, "setTitle")) {
958 const json = data orelse return error.MissingData;
959 const title = (try json_utils.getStringDecoded(std.heap.c_allocator, json, "title")) orelse return error.InvalidParameter;
960 defer std.heap.c_allocator.free(title);
961 if (std.mem.indexOfScalar(u8, title, 0) != null) return error.InvalidParameter;
962 const title_wide = utf8ToUtf16Z(256, title) catch return error.InvalidParameter;
963 if (SetWindowTextW(hwnd, &title_wide) == 0) return error.NativeCallFailed;
964 } else if (std.mem.eql(u8, action, "setSize")) {
965 const json = data orelse return error.MissingData;
966 const width = json_utils.getInt(u32, json, "width") orelse return error.InvalidParameter;
967 const height = json_utils.getInt(u32, json, "height") orelse return error.InvalidParameter;
968 if (width == 0 or height == 0 or width > @as(u32, std.math.maxInt(c_int)) or height > @as(u32, std.math.maxInt(c_int)))
969 return error.InvalidParameter;
970 if (SetWindowPos(hwnd, null, 0, 0, @intCast(width), @intCast(height), 0x0002) == 0) return error.NativeCallFailed;
971 } else if (std.mem.eql(u8, action, "setPosition")) {
972 const json = data orelse return error.MissingData;
973 const x = json_utils.getInt(i32, json, "x") orelse return error.InvalidParameter;
974 const y = json_utils.getInt(i32, json, "y") orelse return error.InvalidParameter;
975 if (SetWindowPos(hwnd, null, x, y, 0, 0, 0x0001) == 0) return error.NativeCallFailed;
976 } else if (std.mem.eql(u8, action, "loadURL") or std.mem.eql(u8, action, "loadHTML")) {
977 const json = data orelse return error.MissingData;
978 const key: []const u8 = if (std.mem.eql(u8, action, "loadURL")) "url" else "html";
979 const value = (try json_utils.getStringDecoded(std.heap.c_allocator, json, key)) orelse return error.InvalidParameter;
980 defer std.heap.c_allocator.free(value);
981 if (std.mem.indexOfScalar(u8, value, 0) != null) return error.InvalidParameter;
982 var native = Window{ .id = entry.id, .hwnd = hwnd, .controller = @ptrFromInt(entry.context), .webview = webview, .title = "", .width = 0, .height = 0, .x = 0, .y = 0 };
983 if (std.mem.eql(u8, action, "loadURL")) try native.loadURL(value) else try native.loadHTML(value);
984 } else {
985 return error.PlatformNotSupported;
986 }
987}
988
762989pub const WindowStyle = struct {
763990 frameless: bool = false,
764991 transparent: bool = false,
@@ -821,10 +1048,8 @@ pub const Window = struct {
8211048 const ex_style: DWORD = if (options.always_on_top) WS_EX_TOPMOST else 0;
8221049
8231050 // Convert title to wide string
824 var title_wide: [256]u16 = undefined;
825 const title_len = std.unicode.utf8ToUtf16Le(&title_wide, options.title) catch return error.WindowCreationFailed;
826 title_wide[title_len] = 0;
827 const title_ptr: [*:0]const u16 = title_wide[0..title_len :0];
1051 const title_wide = utf8ToUtf16Z(256, options.title) catch return error.WindowCreationFailed;
1052 const title_ptr: LPCWSTR = @ptrCast(&title_wide);
8281053
8291054 // Calculate window position
8301055 const x = options.x orelse CW_USEDEFAULT;
@@ -916,6 +1141,17 @@ pub const Window = struct {
9161141 return error.TooManyWindows;
9171142 };
9181143
1144 const message_handler = try std.heap.c_allocator.create(WebMessageReceivedHandler);
1145 message_handler.* = .{ .lpVtbl = &WebMessageReceivedHandler.vtbl_instance, .ref_count = 1 };
1146 var message_token: EventRegistrationToken = .{ .value = 0 };
1147 const message_hr = webview.lpVtbl.add_WebMessageReceived(webview, @ptrCast(message_handler), &message_token);
1148 _ = WebMessageReceivedHandler.release(message_handler);
1149 if (!succeeded(message_hr)) return error.WebMessageRegistrationFailed;
1150 if (!desktop_windows.setMessageToken(@intFromPtr(hwnd), message_token.value)) {
1151 _ = webview.lpVtbl.remove_WebMessageReceived(webview, message_token);
1152 return error.WebMessageRegistrationFailed;
1153 }
1154
9191155 var window = Window{
9201156 .id = window_id,
9211157 .hwnd = hwnd,
@@ -979,9 +1215,7 @@ pub const Window = struct {
9791215
9801216 pub fn setTitle(self: *Window, title: []const u8) void {
9811217 if (self.liveEntry() == null) return;
982 var title_wide_buf: [256]u16 = undefined;
983 const len = std.unicode.utf8ToUtf16Le(&title_wide_buf, title) catch return;
984 title_wide_buf[len] = 0;
1218 const title_wide_buf = utf8ToUtf16Z(256, title) catch return;
9851219 _ = SetWindowTextW(self.hwnd, &title_wide_buf);
9861220 self.title = title;
9871221 }
@@ -1136,6 +1370,11 @@ fn WindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c
11361370 if (desktop_windows.forgetWindow(@intFromPtr(hwnd))) |entry| {
11371371 const controller: *ICoreWebView2Controller = @ptrFromInt(entry.context);
11381372 const webview: *ICoreWebView2 = @ptrFromInt(entry.webview);
1373 window_registry.forgetOwner(entry.webview);
1374 window_registry.forget(entry.window);
1375 if (entry.message_token) |token| {
1376 _ = webview.lpVtbl.remove_WebMessageReceived(webview, .{ .value = token });
1377 }
11391378 _ = controller.lpVtbl.Close(controller);
11401379 _ = webview.lpVtbl.Release(webview);
11411380 _ = controller.lpVtbl.Release(controller);