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.
| @@ -6,7 +6,7 @@ const desktop_bridge_envelope = ; | ||
| 6 | 6 | |
| 7 | 7 | pub fn fromUtf16(allocator: std.mem.Allocator, wide: []const u16) ![:0]u8 { |
| 8 | 8 | if (wide.len > desktop_bridge_envelope.max_message_bytes) return error.MessageTooLarge; |
| 9 | const utf8 = try std.unicode.utf16leToUtf8AllocZ(allocator, wide); | |
| 9 | const utf8 = try std.unicode.utf16LeToUtf8AllocZ(allocator, wide); | |
| 10 | 10 | errdefer allocator.free(utf8); |
| 11 | 11 | if (utf8.len > desktop_bridge_envelope.max_message_bytes) return error.MessageTooLarge; |
| 12 | 12 | return utf8; |
| @@ -2,6 +2,7 @@ const std = ; | ||
| 2 | 2 | const bridge_error = ; |
| 3 | 3 | const desktop_bridge_envelope = ; |
| 4 | 4 | const desktop_window_registry = ; |
| 5 | const desktop_window_events = ; | |
| 5 | 6 | const json_utils = ; |
| 6 | 7 | const request_context = ; |
| 7 | 8 | const window_context = ; |
| @@ -30,6 +31,10 @@ pub extern "c" fn gtk_window_iconify(window: *anyopaque) void; | ||
| 30 | 31 | pub extern "c" fn gtk_widget_hide(widget: *anyopaque) void; |
| 31 | 32 | pub extern "c" fn gtk_widget_show(widget: *anyopaque) void; |
| 32 | 33 | pub extern "c" fn gtk_window_set_position(window: *anyopaque, x: c_int, y: c_int) void; |
| 34 | pub extern "c" fn gtk_window_get_position(window: *anyopaque, x: *c_int, y: *c_int) void; | |
| 35 | pub extern "c" fn gtk_window_get_size(window: *anyopaque, width: *c_int, height: *c_int) void; | |
| 36 | pub extern "c" fn gtk_widget_get_window(widget: *anyopaque) ?*anyopaque; | |
| 37 | pub extern "c" fn gdk_window_get_state(window: *anyopaque) c_uint; | |
| 33 | 38 | |
| 34 | 39 | pub extern "c" fn webkit_web_view_new() *anyopaque; |
| 35 | 40 | pub extern "c" fn webkit_web_view_load_uri(webview: *anyopaque, uri: [*:0]const u8) void; |
| @@ -229,11 +234,77 @@ fn registerWindow(gtk_window: *anyopaque, webview: *anyopaque) ?u32 { | ||
| 229 | 234 | /// Forget the handle at actual teardown, so neither path leaves a stale |
| 230 | 235 | /// webview eligible for bridge replies. |
| 231 | 236 | fn onWindowDestroyed(widget: *anyopaque, _: ?*anyopaque) callconv(.c) void { |
| 237 | if (desktop_windows.byWindow(@intFromPtr(widget))) |entry| { | |
| 238 | deliverWindowEvent(entry, "close", ""); | |
| 239 | } | |
| 232 | 240 | const entry = desktop_windows.forgetWindow(@intFromPtr(widget)) orelse return; |
| 233 | 241 | window_registry.forgetOwner(entry.webview); |
| 234 | 242 | window_registry.forget(entry.window); |
| 235 | 243 | } |
| 236 | 244 | |
| 245 | fn deliverToWebview(webview: usize, name: []const u8, detail_json: []const u8, window_name: ?[]const u8) void { | |
| 246 | const script = desktop_window_events.format(std.heap.c_allocator, name, detail_json, window_name) catch return; | |
| 247 | defer std.heap.c_allocator.free(script); | |
| 248 | const script_z = .dupeZ(std.heap.c_allocator, u8, script) catch return; | |
| 249 | defer std.heap.c_allocator.free(script_z); | |
| 250 | webkit_web_view_run_javascript(@ptrFromInt(webview), script_z, null, null, null); | |
| 251 | } | |
| 252 | ||
| 253 | fn deliverWindowEvent(entry: desktop_window_registry.Entry, name: []const u8, detail_json: []const u8) void { | |
| 254 | deliverToWebview(entry.webview, name, detail_json, null); | |
| 255 | const owner = window_registry.ownerWebViewOf(entry.window) orelse return; | |
| 256 | if (owner == entry.webview or desktop_windows.byWebview(owner) == null) return; | |
| 257 | const window_name = window_registry.nameOf(entry.window) orelse return; | |
| 258 | deliverToWebview(owner, name, detail_json, window_name); | |
| 259 | } | |
| 260 | ||
| 261 | fn onWindowFocusIn(widget: *anyopaque, _: *anyopaque, _: ?*anyopaque) callconv(.c) c_int { | |
| 262 | if (desktop_windows.byWindow(@intFromPtr(widget))) |entry| deliverWindowEvent(entry, "focus", ""); | |
| 263 | return 0; | |
| 264 | } | |
| 265 | ||
| 266 | fn onWindowFocusOut(widget: *anyopaque, _: *anyopaque, _: ?*anyopaque) callconv(.c) c_int { | |
| 267 | if (desktop_windows.byWindow(@intFromPtr(widget))) |entry| deliverWindowEvent(entry, "blur", ""); | |
| 268 | return 0; | |
| 269 | } | |
| 270 | ||
| 271 | fn onWindowConfigure(widget: *anyopaque, _: *anyopaque, _: ?*anyopaque) callconv(.c) c_int { | |
| 272 | const entry = desktop_windows.byWindow(@intFromPtr(widget)) orelse return 0; | |
| 273 | var x: c_int = 0; | |
| 274 | var y: c_int = 0; | |
| 275 | var width: c_int = 0; | |
| 276 | var height: c_int = 0; | |
| 277 | gtk_window_get_position(widget, &x, &y); | |
| 278 | gtk_window_get_size(widget, &width, &height); | |
| 279 | if (width < 0 or height < 0) return 0; | |
| 280 | const change = desktop_windows.observeGeometry(entry.window, .{ | |
| 281 | .x = x, | |
| 282 | .y = y, | |
| 283 | .width = @intCast(width), | |
| 284 | .height = @intCast(height), | |
| 285 | }) orelse return 0; | |
| 286 | var detail_buf: [96]u8 = undefined; | |
| 287 | if (change.moved) { | |
| 288 | const detail = std.fmt.bufPrint(&detail_buf, "", .{ x, y }) catch return 0; | |
| 289 | deliverWindowEvent(entry, "move", detail); | |
| 290 | } | |
| 291 | if (change.resized) { | |
| 292 | const detail = std.fmt.bufPrint(&detail_buf, "", .{ width, height }) catch return 0; | |
| 293 | deliverWindowEvent(entry, "resize", detail); | |
| 294 | } | |
| 295 | return 0; | |
| 296 | } | |
| 297 | ||
| 298 | fn onWindowState(widget: *anyopaque, _: *anyopaque, _: ?*anyopaque) callconv(.c) c_int { | |
| 299 | const entry = desktop_windows.byWindow(@intFromPtr(widget)) orelse return 0; | |
| 300 | const gdk_window = gtk_widget_get_window(widget) orelse return 0; | |
| 301 | const state = gdk_window_get_state(gdk_window); | |
| 302 | const change = desktop_windows.observeState(entry.window, (state & 2) != 0, (state & 16) != 0) orelse return 0; | |
| 303 | if (change.minimized) |minimized| deliverWindowEvent(entry, if (minimized) "minimize" else "restore", ""); | |
| 304 | if (change.fullscreen) |fullscreen| deliverWindowEvent(entry, if (fullscreen) "enter-fullscreen" else "leave-fullscreen", ""); | |
| 305 | return 0; | |
| 306 | } | |
| 307 | ||
| 237 | 308 | /// WebKit supplies the WebView via signal user data; a page-supplied window id |
| 238 | 309 | /// is only a name to resolve after the native sender has been authenticated. |
| 239 | 310 | fn onScriptMessage(_: *anyopaque, result: *anyopaque, user_data: ?*anyopaque) callconv(.c) void { |
| @@ -520,6 +591,12 @@ pub const Window = struct { | ||
| 520 | 591 | // Add WebView to window |
| 521 | 592 | gtk_container_add(window, webview); |
| 522 | 593 | |
| 594 | if (g_signal_connect_data(window, "focus-in-event", @ptrCast(&onWindowFocusIn), null, null, 0) == 0 or | |
| 595 | g_signal_connect_data(window, "focus-out-event", @ptrCast(&onWindowFocusOut), null, null, 0) == 0 or | |
| 596 | g_signal_connect_data(window, "configure-event", @ptrCast(&onWindowConfigure), null, null, 0) == 0 or | |
| 597 | g_signal_connect_data(window, "window-state-event", @ptrCast(&onWindowState), null, null, 0) == 0) | |
| 598 | return error.SignalConnectionFailed; | |
| 599 | ||
| 523 | 600 | // Register window in the multi-window registry |
| 524 | 601 | const window_id = registerWindow(window, webview) orelse { |
| 525 | 602 | return error.TooManyWindows; |
| @@ -3,6 +3,7 @@ const bridge_error = ; | ||
| 3 | 3 | const desktop_bridge_envelope = ; |
| 4 | 4 | const desktop_bridge_text = ; |
| 5 | 5 | const desktop_script_encoding = ; |
| 6 | const desktop_window_events = ; | |
| 6 | 7 | const desktop_window_registry = ; |
| 7 | 8 | const json_utils = ; |
| 8 | 9 | const request_context = ; |
| @@ -88,7 +89,9 @@ pub const SW_HIDE: c_int = 0; | ||
| 88 | 89 | pub const SW_MAXIMIZE: c_int = 3; |
| 89 | 90 | pub const SW_MINIMIZE: c_int = 6; |
| 90 | 91 | pub const WM_DESTROY: UINT = 0x0002; |
| 92 | pub const WM_MOVE: UINT = 0x0003; | |
| 91 | 93 | pub const WM_SIZE: UINT = 0x0005; |
| 94 | pub const WM_ACTIVATE: UINT = 0x0006; | |
| 92 | 95 | pub const WM_CLOSE: UINT = 0x0010; |
| 93 | 96 | pub const WM_QUIT: UINT = 0x0012; |
| 94 | 97 | pub const PM_REMOVE: UINT = 0x0001; |
| @@ -123,6 +126,7 @@ pub extern "user32" fn SetWindowTextW(hWnd: HWND, lpString: LPCWSTR) callconv(.c | ||
| 123 | 126 | pub extern "user32" fn SetWindowPos(hWnd: HWND, hWndInsertAfter: ?HWND, X: c_int, Y: c_int, cx: c_int, cy: c_int, uFlags: UINT) callconv(.c) BOOL; |
| 124 | 127 | pub extern "user32" fn LoadCursorW(hInstance: ?HINSTANCE, lpCursorName: LPCWSTR) callconv(.c) ?*anyopaque; |
| 125 | 128 | pub extern "user32" fn GetClientRect(hWnd: HWND, lpRect: *RECT) callconv(.c) BOOL; |
| 129 | pub extern "user32" fn GetWindowRect(hWnd: HWND, lpRect: *RECT) callconv(.c) BOOL; | |
| 126 | 130 | pub extern "user32" fn SetWindowLongPtrW(hWnd: HWND, nIndex: c_int, dwNewLong: isize) callconv(.c) isize; |
| 127 | 131 | pub extern "user32" fn GetWindowLongPtrW(hWnd: HWND, nIndex: c_int) callconv(.c) isize; |
| 128 | 132 | pub extern "kernel32" fn GetModuleHandleW(lpModuleName: ?LPCWSTR) callconv(.c) ?HINSTANCE; |
| @@ -846,6 +850,49 @@ const CLASS_NAME: [:0]const u16 = &[_:0]u16{ 'Z', 'y', 't', 'e', 'W', 'i', 'n', | ||
| 846 | 850 | // Window.create. Keep stable handles for every live window here. |
| 847 | 851 | var desktop_windows: desktop_window_registry.Registry = .{}; |
| 848 | 852 | |
| 853 | fn deliverToWebview(webview_handle: usize, name: []const u8, detail_json: []const u8, window_name: ?[]const u8) void { | |
| 854 | const script = desktop_window_events.format(std.heap.c_allocator, name, detail_json, window_name) catch return; | |
| 855 | defer std.heap.c_allocator.free(script); | |
| 856 | var wide = desktop_script_encoding.encode(std.heap.c_allocator, script) catch return; | |
| 857 | defer wide.deinit(std.heap.c_allocator); | |
| 858 | const webview: *ICoreWebView2 = @ptrFromInt(webview_handle); | |
| 859 | const handler = std.heap.c_allocator.create(ExecuteScriptCompletedHandler) catch return; | |
| 860 | handler.* = .{ .lpVtbl = &ExecuteScriptCompletedHandler.vtbl_instance, .ref_count = 1 }; | |
| 861 | _ = webview.lpVtbl.ExecuteScript(webview, wide.ptr(), handler); | |
| 862 | _ = ExecuteScriptCompletedHandler.esRelease(handler); | |
| 863 | } | |
| 864 | ||
| 865 | fn deliverWindowEvent(entry: desktop_window_registry.Entry, name: []const u8, detail_json: []const u8) void { | |
| 866 | deliverToWebview(entry.webview, name, detail_json, null); | |
| 867 | const owner = window_registry.ownerWebViewOf(entry.window) orelse return; | |
| 868 | if (owner == entry.webview or desktop_windows.byWebview(owner) == null) return; | |
| 869 | const window_name = window_registry.nameOf(entry.window) orelse return; | |
| 870 | deliverToWebview(owner, name, detail_json, window_name); | |
| 871 | } | |
| 872 | ||
| 873 | fn observeWindowGeometry(entry: desktop_window_registry.Entry) void { | |
| 874 | var rect: RECT = undefined; | |
| 875 | if (GetWindowRect(@ptrFromInt(entry.window), &rect) == 0) return; | |
| 876 | const width = @as(i64, rect.right) - @as(i64, rect.left); | |
| 877 | const height = @as(i64, rect.bottom) - @as(i64, rect.top); | |
| 878 | if (width < 0 or height < 0 or width > std.math.maxInt(u32) or height > std.math.maxInt(u32)) return; | |
| 879 | const change = desktop_windows.observeGeometry(entry.window, .{ | |
| 880 | .x = @intCast(rect.left), | |
| 881 | .y = @intCast(rect.top), | |
| 882 | .width = @intCast(width), | |
| 883 | .height = @intCast(height), | |
| 884 | }) orelse return; | |
| 885 | var detail_buf: [96]u8 = undefined; | |
| 886 | if (change.moved) { | |
| 887 | const detail = std.fmt.bufPrint(&detail_buf, "", .{ rect.left, rect.top }) catch return; | |
| 888 | deliverWindowEvent(entry, "move", detail); | |
| 889 | } | |
| 890 | if (change.resized) { | |
| 891 | const detail = std.fmt.bufPrint(&detail_buf, "", .{ width, height }) catch return; | |
| 892 | deliverWindowEvent(entry, "resize", detail); | |
| 893 | } | |
| 894 | } | |
| 895 | ||
| 849 | 896 | fn namedWindowResult(allocator: std.mem.Allocator, name: []const u8) ![]u8 { |
| 850 | 897 | var json: std.ArrayListUnmanaged(u8) = .empty; |
| 851 | 898 | errdefer json.deinit(allocator); |
| @@ -1151,6 +1198,7 @@ pub const Window = struct { | ||
| 1151 | 1198 | _ = webview.lpVtbl.remove_WebMessageReceived(webview, message_token); |
| 1152 | 1199 | return error.WebMessageRegistrationFailed; |
| 1153 | 1200 | } |
| 1201 | observeWindowGeometry(desktop_windows.byId(window_id).?); | |
| 1154 | 1202 | |
| 1155 | 1203 | var window = Window{ |
| 1156 | 1204 | .id = window_id, |
| @@ -1359,14 +1407,31 @@ fn WindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c | ||
| 1359 | 1407 | var bounds: RECT = undefined; |
| 1360 | 1408 | _ = GetClientRect(hwnd, &bounds); |
| 1361 | 1409 | _ = controller.lpVtbl.put_Bounds(controller, bounds); |
| 1410 | const minimized = wParam == 1; // SIZE_MINIMIZED | |
| 1411 | if (desktop_windows.observeState(entry.window, minimized, false)) |change| { | |
| 1412 | if (change.minimized) |now_minimized| | |
| 1413 | deliverWindowEvent(entry, if (now_minimized) "minimize" else "restore", ""); | |
| 1414 | } | |
| 1415 | if (!minimized) observeWindowGeometry(entry); | |
| 1416 | } | |
| 1417 | return 0; | |
| 1418 | }, | |
| 1419 | WM_MOVE => { | |
| 1420 | if (desktop_windows.byWindow(@intFromPtr(hwnd))) |entry| { | |
| 1421 | if (!entry.minimized) observeWindowGeometry(entry); | |
| 1362 | 1422 | } |
| 1363 | 1423 | return 0; |
| 1364 | 1424 | }, |
| 1425 | WM_ACTIVATE => { | |
| 1426 | if (desktop_windows.byWindow(@intFromPtr(hwnd))) |entry| | |
| 1427 | deliverWindowEvent(entry, if ((wParam & 0xffff) == 0) "blur" else "focus", ""); | |
| 1428 | }, | |
| 1365 | 1429 | WM_CLOSE => { |
| 1366 | 1430 | _ = DestroyWindow(hwnd); |
| 1367 | 1431 | return 0; |
| 1368 | 1432 | }, |
| 1369 | 1433 | WM_DESTROY => { |
| 1434 | if (desktop_windows.byWindow(@intFromPtr(hwnd))) |entry| deliverWindowEvent(entry, "close", ""); | |
| 1370 | 1435 | if (desktop_windows.forgetWindow(@intFromPtr(hwnd))) |entry| { |
| 1371 | 1436 | const controller: *ICoreWebView2Controller = @ptrFromInt(entry.context); |
| 1372 | 1437 | const webview: *ICoreWebView2 = @ptrFromInt(entry.webview); |
| @@ -1520,6 +1585,6 @@ pub fn getClipboard(allocator: std.mem.Allocator) ![]u8 { | ||
| 1520 | 1585 | const text_len = std.mem.indexOfSentinel(u16, 0, text_wide); |
| 1521 | 1586 | |
| 1522 | 1587 | // Convert UTF-16 to UTF-8 |
| 1523 | const utf8_len = std.unicode.utf16leToUtf8AllocZ(allocator, text_wide[0..text_len]) catch return ""; | |
| 1588 | const utf8_len = std.unicode.utf16LeToUtf8AllocZ(allocator, text_wide[0..text_len]) catch return ""; | |
| 1524 | 1589 | return utf8_len; |
| 1525 | 1590 | } |