@@ -1,7 +1,13 @@ 1 1 const std = ; 2 + const bridge_error = ; 3 + const desktop_bridge_envelope = ; 4 + const desktop_bridge_text = ; 2 5 const desktop_script_encoding = ; 3 6 const desktop_window_registry = ; 7 + const json_utils = ; 8 + const request_context = ; 4 9 const window_context = ; 10 + const window_registry = ; 5 11 const window_reply_target = ; 6 12 7 13 // Windows implementation using Win32 API and WebView2 ⋯ 113 lines Show the 113 lines above this hunk @@ -121,6 +127,7 @@ pub extern "user32" fn SetWindowLongPtrW(hWnd: HWND, nIndex: c_int, dwNewLong: i 121 127 pub extern "user32" fn GetWindowLongPtrW(hWnd: HWND, nIndex: c_int) callconv(.c) isize; 122 128 pub extern "kernel32" fn GetModuleHandleW(lpModuleName: ?LPCWSTR) callconv(.c) ?HINSTANCE; 123 129 pub extern "kernel32" fn Sleep(dwMilliseconds: DWORD) callconv(.c) void; 130 + pub extern "ole32" fn CoTaskMemFree(pv: ?*anyopaque) callconv(.c) void; 124 131 125 132 // ============================================================================ 126 133 // WebView2 COM vtable interfaces ⋯ 143 lines Show the 143 lines above this hunk @@ -270,6 +277,19 @@ pub const ICoreWebView2 = extern struct { 270 277 lpVtbl: *ICoreWebView2Vtbl, 271 278 }; 272 279 280 + pub 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 + 289 + pub const ICoreWebView2WebMessageReceivedEventArgs = extern struct { 290 + lpVtbl: *ICoreWebView2WebMessageReceivedEventArgsVtbl, 291 + }; 292 + 273 293 // -- ICoreWebView2Settings --------------------------------------------------- 274 294 275 295 pub const ICoreWebView2SettingsVtbl = extern struct { ⋯ 328 lines Show the 328 lines above this hunk @@ -604,6 +624,73 @@ const PermissionRequestedHandler = extern struct { 604 624 605 625 pub const ICoreWebView2PermissionRequestedEventHandler = PermissionRequestedHandler; 606 626 627 + // -- Page-to-host message handler -------------------------------------------- 628 + 629 + const 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 + 607 694 // -- ExecuteScript completed handler (fire-and-forget) ----------------------- 608 695 609 696 const ExecuteScriptCompletedHandler = extern struct { ⋯ 149 lines Show the 149 lines above this hunk @@ -759,6 +846,146 @@ const CLASS_NAME: [:0]const u16 = &[_:0]u16{ 'Z', 'y', 't', 'e', 'W', 'i', 'n', 759 846 // Window.create. Keep stable handles for every live window here. 760 847 var desktop_windows: desktop_window_registry.Registry = .{}; 761 848 849 + fn 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 + 858 + fn 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 + 921 + fn 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 + 935 + fn 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 + 762 989 pub const WindowStyle = struct { 763 990 frameless: bool = false, 764 991 transparent: bool = false, ⋯ 56 lines Show the 56 lines above this hunk @@ -821,10 +1048,8 @@ pub const Window = struct { 821 1048 const ex_style: DWORD = if (options.always_on_top) WS_EX_TOPMOST else 0; 822 1049 823 1050 // 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); 828 1053 829 1054 // Calculate window position 830 1055 const x = options.x orelse CW_USEDEFAULT; ⋯ 85 lines Show the 85 lines above this hunk @@ -916,6 +1141,17 @@ pub const Window = struct { 916 1141 return error.TooManyWindows; 917 1142 }; 918 1143 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 + 919 1155 var window = Window{ 920 1156 .id = window_id, 921 1157 .hwnd = hwnd, ⋯ 57 lines Show the 57 lines above this hunk @@ -979,9 +1215,7 @@ pub const Window = struct { 979 1215 980 1216 pub fn setTitle(self: *Window, title: []const u8) void { 981 1217 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 ; 985 1219 _ = SetWindowTextW(self.hwnd, &title_wide_buf); 986 1220 self.title = title; 987 1221 } ⋯ 148 lines Show the 148 lines above this hunk @@ -1136,6 +1370,11 @@ fn WindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c 1136 1370 if (desktop_windows.forgetWindow(@intFromPtr(hwnd))) |entry| { 1137 1371 const controller: *ICoreWebView2Controller = @ptrFromInt(entry.context); 1138 1372 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 + } 1139 1378 _ = controller.lpVtbl.Close(controller); 1140 1379 _ = webview.lpVtbl.Release(webview); 1141 1380 _ = controller.lpVtbl.Release(controller);