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.
| @@ -1279,6 +1279,14 @@ pub fn build(b: *std.Build) void { | ||
| 1279 | 1279 | }), |
| 1280 | 1280 | }); |
| 1281 | 1281 | |
| 1282 | const desktop_window_events_tests = b.addTest(.{ | |
| 1283 | .root_module = b.createModule(.{ | |
| 1284 | .root_source_file = b.path("src/desktop_window_events.zig"), | |
| 1285 | .target = target, | |
| 1286 | .optimize = optimize, | |
| 1287 | }), | |
| 1288 | }); | |
| 1289 | ||
| 1282 | 1290 | const desktop_script_encoding_tests = b.addTest(.{ |
| 1283 | 1291 | .root_module = b.createModule(.{ |
| 1284 | 1292 | .root_source_file = b.path("src/desktop_script_encoding.zig"), |
| @@ -1744,6 +1752,7 @@ pub fn build(b: *std.Build) void { | ||
| 1744 | 1752 | const run_desktop_window_registry_tests = b.addRunArtifact(desktop_window_registry_tests); |
| 1745 | 1753 | const run_desktop_bridge_envelope_tests = b.addRunArtifact(desktop_bridge_envelope_tests); |
| 1746 | 1754 | const run_desktop_bridge_text_tests = b.addRunArtifact(desktop_bridge_text_tests); |
| 1755 | const run_desktop_window_events_tests = b.addRunArtifact(desktop_window_events_tests); | |
| 1747 | 1756 | const run_desktop_script_encoding_tests = b.addRunArtifact(desktop_script_encoding_tests); |
| 1748 | 1757 | const run_external_link_tests = b.addRunArtifact(external_link_tests); |
| 1749 | 1758 | const run_webview_recovery_tests = b.addRunArtifact(webview_recovery_tests); |
| @@ -1897,6 +1906,7 @@ pub fn build(b: *std.Build) void { | ||
| 1897 | 1906 | test_step.dependOn(&run_desktop_window_registry_tests.step); |
| 1898 | 1907 | test_step.dependOn(&run_desktop_bridge_envelope_tests.step); |
| 1899 | 1908 | test_step.dependOn(&run_desktop_bridge_text_tests.step); |
| 1909 | test_step.dependOn(&run_desktop_window_events_tests.step); | |
| 1900 | 1910 | test_step.dependOn(&run_desktop_script_encoding_tests.step); |
| 1901 | 1911 | test_step.dependOn(&run_external_link_tests.step); |
| 1902 | 1912 | test_step.dependOn(&run_webview_recovery_tests.step); |
| @@ -0,0 +1,38 @@ | ||
| 1 | //! JS delivery for a native desktop window transition. Platform code chooses | |
| 2 | //! the actual live WebView; this module only formats the event payload. | |
| 3 | ||
| 4 | const std = ; | |
| 5 | const bridge_error = ; | |
| 6 | ||
| 7 | pub fn format( | |
| 8 | allocator: std.mem.Allocator, | |
| 9 | event_name: []const u8, | |
| 10 | detail_json: []const u8, | |
| 11 | window_name: ?[]const u8, | |
| 12 | ) ![]u8 { | |
| 13 | var script: std.ArrayListUnmanaged(u8) = .empty; | |
| 14 | errdefer script.deinit(allocator); | |
| 15 | try script.appendSlice(allocator, "if(window.__craftDeliverWindowEvent)window.__craftDeliverWindowEvent(\""); | |
| 16 | try bridge_error.appendJsonEscaped(allocator, &script, event_name); | |
| 17 | try script.appendSlice(allocator, "\","); | |
| 18 | try script.appendSlice(allocator, if (detail_json.len == 0) "{}" else detail_json); | |
| 19 | if (window_name) |name| { | |
| 20 | try script.appendSlice(allocator, ",\""); | |
| 21 | try bridge_error.appendJsonEscaped(allocator, &script, name); | |
| 22 | try script.append(allocator, '"'); | |
| 23 | } | |
| 24 | try script.appendSlice(allocator, ");"); | |
| 25 | return script.toOwnedSlice(allocator); | |
| 26 | } | |
| 27 | ||
| 28 | test "a local transition addresses only the page's main handle" { | |
| 29 | const script = try format(std.testing.allocator, "focus", "", null); | |
| 30 | defer std.testing.allocator.free(script); | |
| 31 | try std.testing.expectEqualStrings("if(window.__craftDeliverWindowEvent)window.__craftDeliverWindowEvent(\"focus\",{});", script); | |
| 32 | } | |
| 33 | ||
| 34 | test "a creator receives the named child event with escaped id" { | |
| 35 | const script = try format(std.testing.allocator, "resize", "{\"width\":640}", "set\"tings"); | |
| 36 | defer std.testing.allocator.free(script); | |
| 37 | try std.testing.expectEqualStrings("if(window.__craftDeliverWindowEvent)window.__craftDeliverWindowEvent(\"resize\",{\"width\":640},\"set\\\"tings\");", script); | |
| 38 | } | |
| @@ -7,6 +7,23 @@ | ||
| 7 | 7 | const std = ; |
| 8 | 8 | |
| 9 | 9 | pub const capacity = 32; |
| 10 | pub const Geometry = struct { | |
| 11 | x: i32, | |
| 12 | y: i32, | |
| 13 | width: u32, | |
| 14 | height: u32, | |
| 15 | }; | |
| 16 | ||
| 17 | pub const GeometryChange = struct { | |
| 18 | moved: bool, | |
| 19 | resized: bool, | |
| 20 | }; | |
| 21 | ||
| 22 | pub const StateChange = struct { | |
| 23 | minimized: ?bool, | |
| 24 | fullscreen: ?bool, | |
| 25 | }; | |
| 26 | ||
| 10 | 27 | pub const Entry = struct { |
| 11 | 28 | id: u32, |
| 12 | 29 | window: usize, |
| @@ -15,6 +32,9 @@ pub const Entry = struct { | ||
| 15 | 32 | context: usize = 0, |
| 16 | 33 | /// WebView2 subscription to remove before releasing a closed webview. |
| 17 | 34 | message_token: ?i64 = null, |
| 35 | geometry: ?Geometry = null, | |
| 36 | minimized: bool = false, | |
| 37 | fullscreen: bool = false, | |
| 18 | 38 | }; |
| 19 | 39 | |
| 20 | 40 | pub const Registry = struct { |
| @@ -81,6 +101,42 @@ pub const Registry = struct { | ||
| 81 | 101 | return false; |
| 82 | 102 | } |
| 83 | 103 | |
| 104 | pub fn observeGeometry(self: *Registry, window: usize, next: Geometry) ?GeometryChange { | |
| 105 | if (window == 0) return null; | |
| 106 | for (&self.entries) |*slot| { | |
| 107 | if (slot.*) |*entry| { | |
| 108 | if (entry.window == window) { | |
| 109 | const previous = entry.geometry; | |
| 110 | entry.geometry = next; | |
| 111 | if (previous) |old| return .{ | |
| 112 | .moved = old.x != next.x or old.y != next.y, | |
| 113 | .resized = old.width != next.width or old.height != next.height, | |
| 114 | }; | |
| 115 | return .{ .moved = false, .resized = false }; | |
| 116 | } | |
| 117 | } | |
| 118 | } | |
| 119 | return null; | |
| 120 | } | |
| 121 | ||
| 122 | pub fn observeState(self: *Registry, window: usize, minimized: bool, fullscreen: bool) ?StateChange { | |
| 123 | if (window == 0) return null; | |
| 124 | for (&self.entries) |*slot| { | |
| 125 | if (slot.*) |*entry| { | |
| 126 | if (entry.window == window) { | |
| 127 | const change: StateChange = .{ | |
| 128 | .minimized = if (entry.minimized != minimized) minimized else null, | |
| 129 | .fullscreen = if (entry.fullscreen != fullscreen) fullscreen else null, | |
| 130 | }; | |
| 131 | entry.minimized = minimized; | |
| 132 | entry.fullscreen = fullscreen; | |
| 133 | return change; | |
| 134 | } | |
| 135 | } | |
| 136 | } | |
| 137 | return null; | |
| 138 | } | |
| 139 | ||
| 84 | 140 | pub fn latest(self: *const Registry) ?Entry { |
| 85 | 141 | var result: ?Entry = null; |
| 86 | 142 | for (self.entries) |slot| { |
| @@ -185,6 +241,38 @@ test "a web message subscription belongs only to its live window" { | ||
| 185 | 241 | try std.testing.expect(registry.byWebview(0x1001) == null); |
| 186 | 242 | } |
| 187 | 243 | |
| 244 | test "geometry changes belong to one live window and reset on reopen" { | |
| 245 | var registry: Registry = .{}; | |
| 246 | _ = registry.remember(0x1000, 0x1001); | |
| 247 | _ = registry.remember(0x2000, 0x2001); | |
| 248 | const initial: Geometry = .{ .x = 10, .y = 20, .width = 800, .height = 600 }; | |
| 249 | const first = registry.observeGeometry(0x1000, initial).?; | |
| 250 | try std.testing.expect(!first.moved and !first.resized); | |
| 251 | const changed = registry.observeGeometry(0x1000, .{ .x = 30, .y = 20, .width = 900, .height = 600 }).?; | |
| 252 | try std.testing.expect(changed.moved and changed.resized); | |
| 253 | try std.testing.expect(registry.byWindow(0x2000).?.geometry == null); | |
| 254 | _ = registry.forgetWindow(0x1000); | |
| 255 | try std.testing.expect(registry.observeGeometry(0x1000, initial) == null); | |
| 256 | _ = registry.remember(0x1000, 0x3001); | |
| 257 | try std.testing.expect(registry.byWindow(0x1000).?.geometry == null); | |
| 258 | } | |
| 259 | ||
| 260 | test "minimize and fullscreen transitions do not leak across windows" { | |
| 261 | var registry: Registry = .{}; | |
| 262 | _ = registry.remember(0x1000, 0x1001); | |
| 263 | _ = registry.remember(0x2000, 0x2001); | |
| 264 | const minimized = registry.observeState(0x1000, true, false).?; | |
| 265 | try std.testing.expectEqual(@as(?bool, true), minimized.minimized); | |
| 266 | try std.testing.expect(minimized.fullscreen == null); | |
| 267 | const same = registry.observeState(0x1000, true, false).?; | |
| 268 | try std.testing.expect(same.minimized == null and same.fullscreen == null); | |
| 269 | const restored = registry.observeState(0x1000, false, true).?; | |
| 270 | try std.testing.expectEqual(@as(?bool, false), restored.minimized); | |
| 271 | try std.testing.expectEqual(@as(?bool, true), restored.fullscreen); | |
| 272 | try std.testing.expect(!registry.byWindow(0x2000).?.minimized); | |
| 273 | try std.testing.expect(!registry.byWindow(0x2000).?.fullscreen); | |
| 274 | } | |
| 275 | ||
| 188 | 276 | test "a window keeps its own platform control object" { |
| 189 | 277 | var registry: Registry = .{}; |
| 190 | 278 | const first = registry.rememberWithContext(0x1000, 0x1001, 0x1002).?; |