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
@@ -1255,6 +1255,14 @@ pub fn build(b: *std.Build) void {
12551255 }),
12561256 });
12571257
1258 const desktop_window_registry_tests = b.addTest(.{
1259 .root_module = b.createModule(.{
1260 .root_source_file = b.path("src/desktop_window_registry.zig"),
1261 .target = target,
1262 .optimize = optimize,
1263 }),
1264 });
1265
12581266 // The shell bridge's argv construction: what a `spawn` actually runs.
12591267 // Untested until now, which is how it shipped parsing an `args` array and
12601268 // throwing it away.
@@ -1709,6 +1717,7 @@ pub fn build(b: *std.Build) void {
17091717 const run_window_registry_tests = b.addRunArtifact(window_registry_tests);
17101718 const run_window_context_tests = b.addRunArtifact(window_context_tests);
17111719 const run_window_reply_target_tests = b.addRunArtifact(window_reply_target_tests);
1720 const run_desktop_window_registry_tests = b.addRunArtifact(desktop_window_registry_tests);
17121721 const run_external_link_tests = b.addRunArtifact(external_link_tests);
17131722 const run_webview_recovery_tests = b.addRunArtifact(webview_recovery_tests);
17141723 const run_request_context_tests = b.addRunArtifact(request_context_tests);
@@ -1858,6 +1867,7 @@ pub fn build(b: *std.Build) void {
18581867 test_step.dependOn(&run_window_registry_tests.step);
18591868 test_step.dependOn(&run_window_context_tests.step);
18601869 test_step.dependOn(&run_window_reply_target_tests.step);
1870 test_step.dependOn(&run_desktop_window_registry_tests.step);
18611871 test_step.dependOn(&run_external_link_tests.step);
18621872 test_step.dependOn(&run_webview_recovery_tests.step);
18631873 test_step.dependOn(&run_request_context_tests.step);
packages/zig/src/desktop_window_registry.zigadded+142-0
Changes to packages/zig/src/desktop_window_registry.zig
@@ -0,0 +1,142 @@
1//! Platform-neutral live desktop window handles.
2//!
3//! A native close can arrive without going through `Window.close` (the titlebar
4//! close button, for example). The platform callback must forget that window
5//! before any later bridge reply or resize selects its destroyed webview.
6
7const std = ;
8
9pub const capacity = 32;
10pub const Entry = struct {
11 id: u32,
12 window: usize,
13 webview: usize,
14};
15
16pub const Registry = struct {
17 entries: [capacity]?Entry = @splat(null),
18 next_id: u32 = 1,
19
20 pub fn remember(self: *Registry, window: usize, webview: usize) ?u32 {
21 if (window == 0 or webview == 0 or self.byWindow(window) != null or self.next_id == 0) return null;
22 for (&self.entries) |*slot| {
23 if (slot.* == null) {
24 const id = self.next_id;
25 // Exhaust rather than reuse an id that a caller may still hold.
26 self.next_id = if (id == std.math.maxInt(u32)) 0 else id + 1;
27 slot.* = .{ .id = id, .window = window, .webview = webview };
28 return id;
29 }
30 }
31 return null;
32 }
33
34 pub fn byId(self: *const Registry, id: u32) ?Entry {
35 for (self.entries) |slot| {
36 if (slot) |entry| {
37 if (entry.id == id) return entry;
38 }
39 }
40 return null;
41 }
42
43 pub fn byWindow(self: *const Registry, window: usize) ?Entry {
44 if (window == 0) return null;
45 for (self.entries) |slot| {
46 if (slot) |entry| {
47 if (entry.window == window) return entry;
48 }
49 }
50 return null;
51 }
52
53 pub fn latest(self: *const Registry) ?Entry {
54 var result: ?Entry = null;
55 for (self.entries) |slot| {
56 if (slot) |entry| {
57 if (result == null or entry.id > result.?.id) result = entry;
58 }
59 }
60 return result;
61 }
62
63 pub fn forgetWindow(self: *Registry, window: usize) ?Entry {
64 if (window == 0) return null;
65 for (&self.entries) |*slot| {
66 if (slot.*) |entry| {
67 if (entry.window == window) {
68 slot.* = null;
69 return entry;
70 }
71 }
72 }
73 return null;
74 }
75
76 pub fn count(self: *const Registry) usize {
77 var total: usize = 0;
78 for (self.entries) |slot| {
79 if (slot != null) total += 1;
80 }
81 return total;
82 }
83
84 pub fn liveWebviews(self: *const Registry, out: *[capacity]usize) []const usize {
85 var total: usize = 0;
86 for (self.entries) |slot| {
87 if (slot) |entry| {
88 out[total] = entry.webview;
89 total += 1;
90 }
91 }
92 return out[0..total];
93 }
94};
95
96test "closing one window retains the other and reopen gets a new identity" {
97 var registry: Registry = .{};
98 const first = registry.remember(0x1000, 0x1001).?;
99 const second = registry.remember(0x2000, 0x2001).?;
100 try std.testing.expectEqual(@as(usize, 2), registry.count());
101 try std.testing.expectEqual(first, registry.forgetWindow(0x1000).?.id);
102 try std.testing.expectEqual(@as(usize, 1), registry.count());
103 try std.testing.expectEqual(second, registry.latest().?.id);
104 try std.testing.expect(registry.byId(first) == null);
105
106 const reopened = registry.remember(0x3000, 0x3001).?;
107 try std.testing.expect(reopened != first and reopened != second);
108 try std.testing.expectEqual(reopened, registry.latest().?.id);
109 try std.testing.expectEqual(@as(usize, 2), registry.count());
110}
111
112test "invalid and duplicate native handles never occupy a slot" {
113 var registry: Registry = .{};
114 try std.testing.expect(registry.remember(0, 0x1001) == null);
115 try std.testing.expect(registry.remember(0x1000, 0) == null);
116 try std.testing.expect(registry.remember(0x1000, 0x1001) != null);
117 try std.testing.expect(registry.remember(0x1000, 0x2001) == null);
118 try std.testing.expectEqual(@as(usize, 1), registry.count());
119 try std.testing.expect(registry.forgetWindow(0x2000) == null);
120 try std.testing.expectEqual(@as(usize, 1), registry.count());
121}
122
123test "a full registry rejects creation without displacing a live window" {
124 var registry: Registry = .{};
125 for (0..capacity) |i| {
126 try std.testing.expect(registry.remember(i + 1, i + 100) != null);
127 }
128 try std.testing.expect(registry.remember(0x9000, 0x9001) == null);
129 try std.testing.expectEqual(@as(usize, capacity), registry.count());
130 try std.testing.expect(registry.byWindow(1) != null);
131}
132
133test "reply candidates contain only live webviews" {
134 var registry: Registry = .{};
135 _ = registry.remember(0x1000, 0x1001);
136 _ = registry.remember(0x2000, 0x2001);
137 _ = registry.forgetWindow(0x1000);
138 var handles: [capacity]usize = undefined;
139 try std.testing.expectEqualSlices(usize, &.{0x2001}, registry.liveWebviews(&handles));
140 _ = registry.forgetWindow(0x2000);
141 try std.testing.expectEqual(@as(usize, 0), registry.liveWebviews(&handles).len);
142}
packages/zig/src/linux.zigmodified+27-71
Changes to packages/zig/src/linux.zig
@@ -1,4 +1,5 @@
11const std = ;
2const desktop_window_registry = ;
23const window_context = ;
34const window_reply_target = ;
45
@@ -200,55 +201,30 @@ pub const WindowEntry = struct {
200201 webview: *anyopaque,
201202};
202203
203var window_registry: [32]?WindowEntry = std.mem.zeroes([32]?WindowEntry);
204var window_count: u32 = 0;
205var next_window_id: u32 = 1;
204var desktop_windows: desktop_window_registry.Registry = .{};
206205
207206pub fn getWindowById(id: u32) ?WindowEntry {
208 for (window_registry) |entry| {
209 if (entry) |e| {
210 if (e.id == id) return e;
211 }
212 }
213 return null;
207 const entry = desktop_windows.byId(id) orelse return null;
208 return .{ .id = entry.id, .gtk_window = @ptrFromInt(entry.window), .webview = @ptrFromInt(entry.webview) };
214209}
215210
216211pub fn getWindowCount() u32 {
217 return window_count;
212 return @intCast(desktop_windows.count());
218213}
219214
220215fn registerWindow(gtk_window: *anyopaque, webview: *anyopaque) ?u32 {
221 for (&window_registry) |*slot| {
222 if (slot.* == null) {
223 const id = next_window_id;
224 next_window_id += 1;
225 slot.* = WindowEntry{
226 .id = id,
227 .gtk_window = gtk_window,
228 .webview = webview,
229 };
230 window_count += 1;
231 return id;
232 }
233 }
234 return null; // registry full
216 return desktop_windows.remember(@intFromPtr(gtk_window), @intFromPtr(webview));
235217}
236218
237fn unregisterWindow(id: u32) void {
238 for (&window_registry) |*slot| {
239 if (slot.*) |e| {
240 if (e.id == id) {
241 slot.* = null;
242 window_count -= 1;
243 break;
244 }
245 }
246 }
219/// GTK emits destroy for titlebar closes as well as programmatic closes.
220/// Forget the handle at actual teardown, so neither path leaves a stale
221/// webview eligible for bridge replies.
222fn onWindowDestroyed(widget: *anyopaque, _: ?*anyopaque) callconv(.c) void {
223 _ = desktop_windows.forgetWindow(@intFromPtr(widget));
247224}
248225
249226// Application state
250227var app_instance: ?*anyopaque = null;
251var current_window: ?*anyopaque = null;
252228
253229pub const WindowStyle = struct {
254230 frameless: bool = false,
@@ -289,7 +265,6 @@ pub const Window = struct {
289265 }
290266
291267 const window = gtk_application_window_new(app_instance.?);
292 current_window = window;
293268
294269 // Create WebView
295270 const webview = webkit_web_view_new();
@@ -362,7 +337,15 @@ pub const Window = struct {
362337 gtk_container_add(window, webview);
363338
364339 // Register window in the multi-window registry
365 const window_id = registerWindow(window, webview) orelse return error.TooManyWindows;
340 const window_id = registerWindow(window, webview) orelse {
341 gtk_window_close(window);
342 return error.TooManyWindows;
343 };
344 if (g_signal_connect_data(window, "destroy", @ptrCast(&onWindowDestroyed), null, null, 0) == 0) {
345 _ = desktop_windows.forgetWindow(@intFromPtr(window));
346 gtk_window_close(window);
347 return error.SignalConnectionFailed;
348 }
366349
367350 return Window{
368351 .id = window_id,
@@ -386,24 +369,9 @@ pub const Window = struct {
386369 }
387370
388371 pub fn close(self: *Window) void {
389 // Remove from window registry
390 unregisterWindow(self.id);
391
392 // Update current_window if this was the active one
393 if (current_window == self.gtk_window) {
394 current_window = null;
395 // Set current_window to the most recently registered window, if any
396 var latest_id: u32 = 0;
397 for (window_registry) |entry| {
398 if (entry) |e| {
399 if (e.id > latest_id) {
400 latest_id = e.id;
401 current_window = e.gtk_window;
402 }
403 }
404 }
405 }
406
372 // The destroy signal unregisters only after GTK actually closes it.
373 const live = desktop_windows.byId(self.id) orelse return;
374 if (live.window != @intFromPtr(self.gtk_window)) return;
407375 gtk_window_close(self.gtk_window);
408376 }
409377
@@ -494,22 +462,10 @@ pub const App = struct {
494462/// Evaluate JavaScript in the page that sent the current bridge message.
495463/// Native callers without a sender continue to use the latest live window.
496464pub fn evalJS(script: []const u8) !void {
497 var live: [window_registry.len]usize = undefined;
498 var live_len: usize = 0;
499 var latest_id: u32 = 0;
500 var fallback: ?usize = null;
501 for (window_registry) |entry| {
502 if (entry) |e| {
503 live[live_len] = @intFromPtr(e.webview);
504 live_len += 1;
505 if (e.id > latest_id) {
506 latest_id = e.id;
507 fallback = @intFromPtr(e.webview);
508 }
509 }
510 }
511
512 const target = window_reply_target.select(live[0..live_len], window_context.currentWebView(), fallback) orelse
465 var handles: [desktop_window_registry.capacity]usize = undefined;
466 const live = desktop_windows.liveWebviews(&handles);
467 const latest = desktop_windows.latest();
468 const target = window_reply_target.select(live, window_context.currentWebView(), if (latest) |entry| entry.webview else null) orelse
513469 return error.NoWebView;
514470 const script_z = try .dupeZ(std.heap.c_allocator, u8, script);
515471 defer std.heap.c_allocator.free(script_z);