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/src/desktop_bridge_text.zigmodified+1-1
Changes to packages/zig/src/desktop_bridge_text.zig
@@ -6,7 +6,7 @@ const desktop_bridge_envelope = ;
66
77pub fn fromUtf16(allocator: std.mem.Allocator, wide: []const u16) ![:0]u8 {
88 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);
1010 errdefer allocator.free(utf8);
1111 if (utf8.len > desktop_bridge_envelope.max_message_bytes) return error.MessageTooLarge;
1212 return utf8;
packages/zig/src/linux.zigmodified+77-0
Changes to packages/zig/src/linux.zig
@@ -2,6 +2,7 @@ const std = ;
22const bridge_error = ;
33const desktop_bridge_envelope = ;
44const desktop_window_registry = ;
5const desktop_window_events = ;
56const json_utils = ;
67const request_context = ;
78const window_context = ;
@@ -30,6 +31,10 @@ pub extern "c" fn gtk_window_iconify(window: *anyopaque) void;
3031pub extern "c" fn gtk_widget_hide(widget: *anyopaque) void;
3132pub extern "c" fn gtk_widget_show(widget: *anyopaque) void;
3233pub extern "c" fn gtk_window_set_position(window: *anyopaque, x: c_int, y: c_int) void;
34pub extern "c" fn gtk_window_get_position(window: *anyopaque, x: *c_int, y: *c_int) void;
35pub extern "c" fn gtk_window_get_size(window: *anyopaque, width: *c_int, height: *c_int) void;
36pub extern "c" fn gtk_widget_get_window(widget: *anyopaque) ?*anyopaque;
37pub extern "c" fn gdk_window_get_state(window: *anyopaque) c_uint;
3338
3439pub extern "c" fn webkit_web_view_new() *anyopaque;
3540pub 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 {
229234/// Forget the handle at actual teardown, so neither path leaves a stale
230235/// webview eligible for bridge replies.
231236fn onWindowDestroyed(widget: *anyopaque, _: ?*anyopaque) callconv(.c) void {
237 if (desktop_windows.byWindow(@intFromPtr(widget))) |entry| {
238 deliverWindowEvent(entry, "close", "");
239 }
232240 const entry = desktop_windows.forgetWindow(@intFromPtr(widget)) orelse return;
233241 window_registry.forgetOwner(entry.webview);
234242 window_registry.forget(entry.window);
235243}
236244
245fn 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
253fn 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
261fn 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
266fn 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
271fn 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
298fn 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
237308/// WebKit supplies the WebView via signal user data; a page-supplied window id
238309/// is only a name to resolve after the native sender has been authenticated.
239310fn onScriptMessage(_: *anyopaque, result: *anyopaque, user_data: ?*anyopaque) callconv(.c) void {
@@ -520,6 +591,12 @@ pub const Window = struct {
520591 // Add WebView to window
521592 gtk_container_add(window, webview);
522593
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
523600 // Register window in the multi-window registry
524601 const window_id = registerWindow(window, webview) orelse {
525602 return error.TooManyWindows;
packages/zig/src/windows.zigmodified+66-1
Changes to packages/zig/src/windows.zig
@@ -3,6 +3,7 @@ const bridge_error = ;
33const desktop_bridge_envelope = ;
44const desktop_bridge_text = ;
55const desktop_script_encoding = ;
6const desktop_window_events = ;
67const desktop_window_registry = ;
78const json_utils = ;
89const request_context = ;
@@ -88,7 +89,9 @@ pub const SW_HIDE: c_int = 0;
8889pub const SW_MAXIMIZE: c_int = 3;
8990pub const SW_MINIMIZE: c_int = 6;
9091pub const WM_DESTROY: UINT = 0x0002;
92pub const WM_MOVE: UINT = 0x0003;
9193pub const WM_SIZE: UINT = 0x0005;
94pub const WM_ACTIVATE: UINT = 0x0006;
9295pub const WM_CLOSE: UINT = 0x0010;
9396pub const WM_QUIT: UINT = 0x0012;
9497pub const PM_REMOVE: UINT = 0x0001;
@@ -123,6 +126,7 @@ pub extern "user32" fn SetWindowTextW(hWnd: HWND, lpString: LPCWSTR) callconv(.c
123126pub 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;
124127pub extern "user32" fn LoadCursorW(hInstance: ?HINSTANCE, lpCursorName: LPCWSTR) callconv(.c) ?*anyopaque;
125128pub extern "user32" fn GetClientRect(hWnd: HWND, lpRect: *RECT) callconv(.c) BOOL;
129pub extern "user32" fn GetWindowRect(hWnd: HWND, lpRect: *RECT) callconv(.c) BOOL;
126130pub extern "user32" fn SetWindowLongPtrW(hWnd: HWND, nIndex: c_int, dwNewLong: isize) callconv(.c) isize;
127131pub extern "user32" fn GetWindowLongPtrW(hWnd: HWND, nIndex: c_int) callconv(.c) isize;
128132pub 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',
846850// Window.create. Keep stable handles for every live window here.
847851var desktop_windows: desktop_window_registry.Registry = .{};
848852
853fn 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
865fn 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
873fn 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
849896fn namedWindowResult(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
850897 var json: std.ArrayListUnmanaged(u8) = .empty;
851898 errdefer json.deinit(allocator);
@@ -1151,6 +1198,7 @@ pub const Window = struct {
11511198 _ = webview.lpVtbl.remove_WebMessageReceived(webview, message_token);
11521199 return error.WebMessageRegistrationFailed;
11531200 }
1201 observeWindowGeometry(desktop_windows.byId(window_id).?);
11541202
11551203 var window = Window{
11561204 .id = window_id,
@@ -1359,14 +1407,31 @@ fn WindowProc(hwnd: HWND, msg: UINT, wParam: WPARAM, lParam: LPARAM) callconv(.c
13591407 var bounds: RECT = undefined;
13601408 _ = GetClientRect(hwnd, &bounds);
13611409 _ = 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);
13621422 }
13631423 return 0;
13641424 },
1425 WM_ACTIVATE => {
1426 if (desktop_windows.byWindow(@intFromPtr(hwnd))) |entry|
1427 deliverWindowEvent(entry, if ((wParam & 0xffff) == 0) "blur" else "focus", "");
1428 },
13651429 WM_CLOSE => {
13661430 _ = DestroyWindow(hwnd);
13671431 return 0;
13681432 },
13691433 WM_DESTROY => {
1434 if (desktop_windows.byWindow(@intFromPtr(hwnd))) |entry| deliverWindowEvent(entry, "close", "");
13701435 if (desktop_windows.forgetWindow(@intFromPtr(hwnd))) |entry| {
13711436 const controller: *ICoreWebView2Controller = @ptrFromInt(entry.context);
13721437 const webview: *ICoreWebView2 = @ptrFromInt(entry.webview);
@@ -1520,6 +1585,6 @@ pub fn getClipboard(allocator: std.mem.Allocator) ![]u8 {
15201585 const text_len = std.mem.indexOfSentinel(u16, 0, text_wide);
15211586
15221587 // 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 "";
15241589 return utf8_len;
15251590}