@zogien/native-sdk 0.11.0-fork.2 → 0.11.0-fork.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/app.zig CHANGED
@@ -2017,8 +2017,14 @@ pub fn addAppArtifacts(b: *std.Build, dep: *std.Build.Dependency, app_options: A
2017
2017
  // on GUI exes (handles inherit; only console AUTO-allocation is
2018
2018
  // gated by the subsystem), so automation harnesses that pipe
2019
2019
  // `app.exe > log 2>&1` keep their logs either way.
2020
+ //
2021
+ // MSVC's CRT enters a GUI-subsystem image through WinMainCRTStartup,
2022
+ // which calls WinMain, but Zig apps export only `main`; MinGW's startup
2023
+ // calls `main` under either subsystem. MSVC targets therefore keep the
2024
+ // GUI subsystem and enter through the console CRT entry instead.
2020
2025
  if (target.result.os.tag == .windows and app_optimize != .Debug) {
2021
2026
  exe.subsystem = .windows;
2027
+ if (target.result.abi == .msvc) exe.entry = .{ .symbol_name = "mainCRTStartup" };
2022
2028
  }
2023
2029
  linkPlatform(b, dep, target, app_mod, exe, selected_platform, web_engine, web_layer, cef_dir, cef_auto_install);
2024
2030
  const install = b.addInstallArtifact(exe, .{});
@@ -2096,32 +2102,25 @@ pub fn addAppArtifacts(b: *std.Build, dep: *std.Build.Dependency, app_options: A
2096
2102
  // not just its test failures. Test builds never analyze `main` (the
2097
2103
  // test runner replaces the entry point), so rules that fire inside it
2098
2104
  // — UiApp.create's Model-defaults rule above all — used to ambush at
2099
- // `native build`, the LAST step in the loop. Compiling this object
2100
- // forces full semantic analysis of the app module, entry point
2101
- // included; nothing links or runs.
2102
- const analysis_root = b.addWriteFiles().add("app_analysis.zig",
2103
- \\//! Generated by the app build: force semantic analysis of the
2104
- \\//! app's entry point at test time. Exactly `main`, transitively —
2105
- \\//! the same surface `native build` analyzes — so test-time can
2106
- \\//! never be stricter than the build it fronts.
2107
- \\comptime {
2108
- \\ const app = @import("app");
2109
- \\ if (@hasDecl(app, "main")) _ = &app.main;
2110
- \\}
2111
- \\
2112
- );
2113
- const analysis_mod = b.createModule(.{
2114
- .root_source_file = analysis_root,
2115
- .target = target,
2116
- .optimize = optimize,
2117
- });
2118
- analysis_mod.addImport("app", test_app_mod);
2119
- const analysis_obj = b.addObject(.{
2105
+ // `native build`, the LAST step in the loop. This executable roots at
2106
+ // the test app module itself, so std.start analyzes `main` exactly as
2107
+ // the app's own entry build does: root-level decls, the body, and the
2108
+ // entry signature. Test time is therefore never stricter, nor laxer,
2109
+ // than the build it fronts. Nothing requests its binary, so Zig
2110
+ // compiles it with -fno-emit-bin: nothing links or runs, and nothing
2111
+ // may ever install it or ask for its emitted bin.
2112
+ //
2113
+ // It must stay an executable. Zig refuses any COFF object compile with
2114
+ // more than one object, archive, or C input, -fno-emit-bin included,
2115
+ // and Windows app modules routinely carry several: the compiled core
2116
+ // archive beside the Debug markup data object, the platform hosts'
2117
+ // C++ sources, and MSVC system import libraries.
2118
+ const entry_analysis = b.addExecutable(.{
2120
2119
  .name = b.fmt("{s}-analysis", .{app_options.name}),
2121
- .root_module = analysis_mod,
2120
+ .root_module = test_app_mod,
2122
2121
  .use_llvm = useLlvmWorkaround(target),
2123
2122
  });
2124
- test_step.dependOn(&analysis_obj.step);
2123
+ test_step.dependOn(&entry_analysis.step);
2125
2124
 
2126
2125
  // `zig build model-contract`: reflect the app's Model/Msg into
2127
2126
  // zig-out/model-contract.zon so `native check` can verify markup
@@ -2314,6 +2313,9 @@ fn appModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Resolv
2314
2313
  if (link_ts_archives) {
2315
2314
  if (stage.service_archive) |service_archive| app_mod.addObjectFile(service_archive);
2316
2315
  }
2316
+ if (link_ts_archives and target.result.os.tag == .windows and target.result.abi == .msvc) {
2317
+ linkScriptcMsvcRuntime(dep, app_mod);
2318
+ }
2317
2319
  }
2318
2320
  if (app_config.sqlite_capability) {
2319
2321
  // `store` and the relational `sqlite` tier share this exact object.
@@ -2331,6 +2333,25 @@ fn appModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Resolv
2331
2333
  return app_mod;
2332
2334
  }
2333
2335
 
2336
+ /// The Windows libraries scriptc's runtime pack declares (runtime-pack.json
2337
+ /// system_libraries; bcrypt and crypt32 back its TLS). An unused import
2338
+ /// library adds no DLL dependency, so they are linked unconditionally.
2339
+ const scriptc_windows_system_libraries = [_][]const u8{ "advapi32", "iphlpapi", "ws2_32", "bcrypt", "crypt32" };
2340
+
2341
+ /// The pinned scriptc builds its Windows runtime against Zig's MinGW sysroot,
2342
+ /// and an MSVC image links the static UCRT without MinGW's default libraries.
2343
+ /// Restate what MinGW would have supplied on the module that carries the core
2344
+ /// and service archives, so the app exe and every test-side artifact that
2345
+ /// imports it (unit tests, model contract, test host) link alike: the pack's
2346
+ /// system libraries, MSVC's oldnames.lib for the POSIX names (open, read,
2347
+ /// close, ...), and scriptc_mingw_compat.c for the rest of MinGW's startup
2348
+ /// and CRT layer (running the runtime's .ctors constructors above all).
2349
+ fn linkScriptcMsvcRuntime(dep: *std.Build.Dependency, mod: *std.Build.Module) void {
2350
+ for (scriptc_windows_system_libraries) |name| mod.linkSystemLibrary(name, .{});
2351
+ mod.linkSystemLibrary("oldnames", .{});
2352
+ mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/scriptc_mingw_compat.c"), .flags = &.{} });
2353
+ }
2354
+
2334
2355
  /// Compile primary markup bytes independently from the Zig app graph. The
2335
2356
  /// final artifact sees this as an object-file input, so a markup edit dirties
2336
2357
  /// only this tiny C compile and the final link.
package/native-sdk.d.ts CHANGED
@@ -32,6 +32,8 @@ export interface NativeSdkWindowInfo {
32
32
  width: number;
33
33
  height: number;
34
34
  scale: number;
35
+ /** Maximized, whoever changed it; `window.zero.on("window:state", ...)` reports each change. */
36
+ maximized: boolean;
35
37
  }
36
38
 
37
39
  export type NativeSdkWindowTitlebarStyle =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zogien/native-sdk",
3
- "version": "0.11.0-fork.2",
3
+ "version": "0.11.0-fork.3",
4
4
  "description": "Community-maintained fork of Native SDK: TypeScript, Native markup, and native desktop tooling",
5
5
  "type": "module",
6
6
  "engines": {
@@ -41,14 +41,14 @@
41
41
  "tar": "7.5.22"
42
42
  },
43
43
  "optionalDependencies": {
44
- "@zogien/native-sdk-darwin-arm64": "0.11.0-fork.2",
45
- "@zogien/native-sdk-darwin-x64": "0.11.0-fork.2",
46
- "@zogien/native-sdk-linux-arm64-gnu": "0.11.0-fork.2",
47
- "@zogien/native-sdk-linux-arm64-musl": "0.11.0-fork.2",
48
- "@zogien/native-sdk-linux-x64-gnu": "0.11.0-fork.2",
49
- "@zogien/native-sdk-linux-x64-musl": "0.11.0-fork.2",
50
- "@zogien/native-sdk-win32-arm64": "0.11.0-fork.2",
51
- "@zogien/native-sdk-win32-x64": "0.11.0-fork.2"
44
+ "@zogien/native-sdk-darwin-arm64": "0.11.0-fork.3",
45
+ "@zogien/native-sdk-darwin-x64": "0.11.0-fork.3",
46
+ "@zogien/native-sdk-linux-arm64-gnu": "0.11.0-fork.3",
47
+ "@zogien/native-sdk-linux-arm64-musl": "0.11.0-fork.3",
48
+ "@zogien/native-sdk-linux-x64-gnu": "0.11.0-fork.3",
49
+ "@zogien/native-sdk-linux-x64-musl": "0.11.0-fork.3",
50
+ "@zogien/native-sdk-win32-arm64": "0.11.0-fork.3",
51
+ "@zogien/native-sdk-win32-x64": "0.11.0-fork.3"
52
52
  },
53
53
  "repository": {
54
54
  "type": "git",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@native-sdk/core",
3
- "version": "0.11.0-fork.2",
3
+ "version": "0.11.0-fork.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@native-sdk/core",
9
- "version": "0.11.0-fork.2",
9
+ "version": "0.11.0-fork.3",
10
10
  "hasInstallScript": true,
11
11
  "dependencies": {
12
12
  "@scriptc/types": "0.3.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@native-sdk/core",
3
- "version": "0.11.0-fork.2",
3
+ "version": "0.11.0-fork.3",
4
4
  "description": "The TypeScript authoring tier: the app-core subset, its checker and contract frontend, the exact-pinned core compiler dependency, and the SDK module cores import",
5
5
  "repository": {
6
6
  "type": "git",
@@ -448,6 +448,11 @@ pub const NullPlatform = struct {
448
448
  /// real hosts' close delegates, hiding instead of closing under
449
449
  /// `.hide`.
450
450
  window_close_policy: [max_windows]types.WindowClosePolicy = [_]types.WindowClosePolicy{.quit} ** max_windows,
451
+ /// Close requests (`request_close_window_fn`) not yet handled, oldest
452
+ /// first. The Windows host posts WM_CLOSE, so a requested close
453
+ /// resolves on a later loop turn; `takeQueuedCloseRequest` models it.
454
+ queued_close_requests: [max_windows]WindowId = [_]WindowId{0} ** max_windows,
455
+ queued_close_request_count: usize = 0,
451
456
  /// Test seam: refuse one OS create without retaining a window identity.
452
457
  fail_next_create_window: bool = false,
453
458
  /// Test seam: make the NEXT `close_window_fn` call fail with
@@ -918,6 +923,9 @@ pub const NullPlatform = struct {
918
923
  .focus_window_fn = focusWindow,
919
924
  .close_window_fn = closeWindow,
920
925
  .minimize_window_fn = minimizeWindow,
926
+ .maximize_window_fn = maximizeWindow,
927
+ .unmaximize_window_fn = unmaximizeWindow,
928
+ .request_close_window_fn = requestCloseWindow,
921
929
  .hide_window_fn = hideWindow,
922
930
  .show_window_fn = showWindow,
923
931
  .set_dock_presence_fn = setDockPresence,
@@ -1319,6 +1327,36 @@ pub const NullPlatform = struct {
1319
1327
  self.window_occluded[index] = true;
1320
1328
  }
1321
1329
 
1330
+ /// The modeled maximize: the refusals the Windows host makes, then the
1331
+ /// state flip its WM_SIZE handler reports. `windowFrameEvent` hands a
1332
+ /// test the frame event that report carries.
1333
+ fn maximizeWindow(context: ?*anyopaque, window_id: WindowId) anyerror!void {
1334
+ const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1335
+ const index = self.findWindowIndex(window_id) orelse return error.WindowNotFound;
1336
+ if (!self.windows[index].open) return error.WindowNotFound;
1337
+ if (!self.window_resizable[index]) return error.WindowNotResizable;
1338
+ if (self.windows[index].hidden or !self.window_visible[index]) return error.WindowHidden;
1339
+ self.windows[index].maximized = true;
1340
+ }
1341
+
1342
+ fn unmaximizeWindow(context: ?*anyopaque, window_id: WindowId) anyerror!void {
1343
+ const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1344
+ const index = self.findWindowIndex(window_id) orelse return error.WindowNotFound;
1345
+ if (!self.windows[index].open) return error.WindowNotFound;
1346
+ if (!self.windows[index].maximized) return;
1347
+ if (self.windows[index].hidden or !self.window_visible[index]) return error.WindowHidden;
1348
+ self.windows[index].maximized = false;
1349
+ }
1350
+
1351
+ fn requestCloseWindow(context: ?*anyopaque, window_id: WindowId) anyerror!void {
1352
+ const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1353
+ const index = self.findWindowIndex(window_id) orelse return error.WindowNotFound;
1354
+ if (!self.windows[index].open) return error.WindowNotFound;
1355
+ if (self.queued_close_request_count >= self.queued_close_requests.len) return error.WindowLimitReached;
1356
+ self.queued_close_requests[self.queued_close_request_count] = window_id;
1357
+ self.queued_close_request_count += 1;
1358
+ }
1359
+
1322
1360
  fn showWindow(context: ?*anyopaque, window_id: WindowId) anyerror!void {
1323
1361
  const self: *NullPlatform = @ptrCast(@alignCast(context.?));
1324
1362
  if (self.fail_next_show_window) {
@@ -2928,6 +2966,7 @@ pub const NullPlatform = struct {
2928
2966
  .open = true,
2929
2967
  .focused = false,
2930
2968
  .hidden = true,
2969
+ .maximized = info.maximized,
2931
2970
  } };
2932
2971
  }
2933
2972
  const info = self.windows[index];
@@ -2945,6 +2984,33 @@ pub const NullPlatform = struct {
2945
2984
  } };
2946
2985
  }
2947
2986
 
2987
+ /// Test seam: drain the oldest queued close request, the modeled loop
2988
+ /// turn on which the host handles its posted WM_CLOSE. It resolves
2989
+ /// exactly like `userCloseWindow` (close_policy decides); dispatch the
2990
+ /// returned event through the runtime. Null while nothing is queued,
2991
+ /// or when the window closed before its turn (a WM_CLOSE posted to a
2992
+ /// destroyed window is never delivered).
2993
+ pub fn takeQueuedCloseRequest(self: *NullPlatform) ?Event {
2994
+ if (self.queued_close_request_count == 0) return null;
2995
+ const window_id = self.queued_close_requests[0];
2996
+ var cursor: usize = 0;
2997
+ while (cursor + 1 < self.queued_close_request_count) : (cursor += 1) {
2998
+ self.queued_close_requests[cursor] = self.queued_close_requests[cursor + 1];
2999
+ }
3000
+ self.queued_close_request_count -= 1;
3001
+ const index = self.findWindowIndex(window_id) orelse return null;
3002
+ if (!self.windows[index].open) return null;
3003
+ return self.userCloseWindow(window_id);
3004
+ }
3005
+
3006
+ /// Test seam: the frame event for a window's current modeled state,
3007
+ /// `maximized` included: what the Windows host emits when a maximize
3008
+ /// or restore lands. Null for an unknown window.
3009
+ pub fn windowFrameEvent(self: *const NullPlatform, window_id: WindowId) ?Event {
3010
+ const index = self.findWindowIndex(window_id) orelse return null;
3011
+ return .{ .window_frame_changed = self.windows[index].state() };
3012
+ }
3013
+
2948
3014
  /// Test seam: the user asked the app to come back with no window on
2949
3015
  /// the glass (the macOS Dock-icon reopen). Models the host's
2950
3016
  /// `applicationShouldHandleReopen:` — every window hidden by its
@@ -718,6 +718,10 @@ pub const WindowInfo = struct {
718
718
  focused: bool = false,
719
719
  /// Alive but policy-hidden — see `WindowState.hidden`.
720
720
  hidden: bool = false,
721
+ /// Maximized as last reported by the host's frame event, whichever
722
+ /// path changed it (app call, caption double-click, system menu,
723
+ /// keyboard shortcut). Hosts that do not report it leave it false.
724
+ maximized: bool = false,
721
725
 
722
726
  pub fn state(self: WindowInfo) WindowState {
723
727
  return .{
@@ -729,6 +733,7 @@ pub const WindowInfo = struct {
729
733
  .open = self.open,
730
734
  .focused = self.focused,
731
735
  .hidden = self.hidden,
736
+ .maximized = self.maximized,
732
737
  };
733
738
  }
734
739
  };
@@ -2708,6 +2713,23 @@ pub const PlatformServices = struct {
2708
2713
  /// controls — chromeless windows have no system button to click.
2709
2714
  /// Platforms without the concept leave this null.
2710
2715
  minimize_window_fn: ?*const fn (context: ?*anyopaque, window_id: WindowId) anyerror!void = null,
2716
+ /// The OS maximize and restore-from-maximized verbs for app-drawn
2717
+ /// window controls (Windows `SW_MAXIMIZE`/`SW_RESTORE`). Hosts refuse
2718
+ /// a non-resizable window (`error.WindowNotResizable`, the system has
2719
+ /// no maximize affordance for it) and a window that is not on the
2720
+ /// glass (`error.WindowHidden`). Unmaximizing a window that is not
2721
+ /// maximized succeeds without effect. The resulting state arrives on
2722
+ /// the window's frame event (`WindowState.maximized`). Platforms
2723
+ /// without the verbs leave these null.
2724
+ maximize_window_fn: ?*const fn (context: ?*anyopaque, window_id: WindowId) anyerror!void = null,
2725
+ unmaximize_window_fn: ?*const fn (context: ?*anyopaque, window_id: WindowId) anyerror!void = null,
2726
+ /// Ask the window to close the way the user's close button does
2727
+ /// (Windows posts `WM_CLOSE`), so `close_policy` decides between
2728
+ /// hiding and closing. Unlike `close_window_fn` this only queues the
2729
+ /// request: the outcome arrives later as the host's ordinary frame
2730
+ /// events, exactly like a user close. Platforms without the verb
2731
+ /// leave this null.
2732
+ request_close_window_fn: ?*const fn (context: ?*anyopaque, window_id: WindowId) anyerror!void = null,
2711
2733
  /// Keep a live window and all of its views, but order it off the
2712
2734
  /// glass. The runtime marks `WindowInfo.hidden` while it is out;
2713
2735
  /// `show_window_fn` is the inverse. Platforms without a reliable
@@ -3167,6 +3189,21 @@ pub const PlatformServices = struct {
3167
3189
  return minimize_fn(self.context, window_id);
3168
3190
  }
3169
3191
 
3192
+ pub fn maximizeWindow(self: PlatformServices, window_id: WindowId) anyerror!void {
3193
+ const maximize_fn = self.maximize_window_fn orelse return error.UnsupportedService;
3194
+ return maximize_fn(self.context, window_id);
3195
+ }
3196
+
3197
+ pub fn unmaximizeWindow(self: PlatformServices, window_id: WindowId) anyerror!void {
3198
+ const unmaximize_fn = self.unmaximize_window_fn orelse return error.UnsupportedService;
3199
+ return unmaximize_fn(self.context, window_id);
3200
+ }
3201
+
3202
+ pub fn requestCloseWindow(self: PlatformServices, window_id: WindowId) anyerror!void {
3203
+ const request_fn = self.request_close_window_fn orelse return error.UnsupportedService;
3204
+ return request_fn(self.context, window_id);
3205
+ }
3206
+
3170
3207
  pub fn hideWindow(self: PlatformServices, window_id: WindowId) anyerror!void {
3171
3208
  const hide_fn = self.hide_window_fn orelse return error.UnsupportedService;
3172
3209
  return hide_fn(self.context, window_id);
@@ -125,6 +125,9 @@ const WindowsEvent = extern struct {
125
125
  /// selection).
126
126
  widget_id: u64,
127
127
  menu_item_id: u32,
128
+ /// Window events: nonzero while the window is maximized. Every emit
129
+ /// carries the live value, so any frame event reports it.
130
+ maximized: c_int,
128
131
  };
129
132
 
130
133
  const WindowsCallback = *const fn (context: ?*anyopaque, event: *const WindowsEvent) callconv(.c) void;
@@ -166,6 +169,9 @@ extern fn native_sdk_windows_minimize_window(host: *WindowsHost, window_id: u64)
166
169
  extern fn native_sdk_windows_hide_window(host: *WindowsHost, window_id: u64) c_int;
167
170
  extern fn native_sdk_windows_show_window(host: *WindowsHost, window_id: u64) c_int;
168
171
  extern fn native_sdk_windows_set_window_close_policy(host: *WindowsHost, window_id: u64, close_policy: c_int) c_int;
172
+ extern fn native_sdk_windows_maximize_window(host: *WindowsHost, window_id: u64) c_int;
173
+ extern fn native_sdk_windows_unmaximize_window(host: *WindowsHost, window_id: u64) c_int;
174
+ extern fn native_sdk_windows_request_close_window(host: *WindowsHost, window_id: u64) c_int;
169
175
  extern fn native_sdk_windows_create_view(host: *WindowsHost, window_id: u64, label: [*]const u8, label_len: usize, kind: c_int, gpu_backend_request: c_int, parent: [*]const u8, parent_len: usize, x: f64, y: f64, width: f64, height: f64, layer: c_int, visible: c_int, enabled: c_int, role: [*]const u8, role_len: usize, accessibility_label: [*]const u8, accessibility_label_len: usize, text: [*]const u8, text_len: usize, command: [*]const u8, command_len: usize) c_int;
170
176
  extern fn native_sdk_windows_update_view(host: *WindowsHost, window_id: u64, label: [*]const u8, label_len: usize, has_frame: c_int, x: f64, y: f64, width: f64, height: f64, has_layer: c_int, layer: c_int, has_visible: c_int, visible: c_int, has_enabled: c_int, enabled: c_int, has_role: c_int, role: [*]const u8, role_len: usize, has_accessibility_label: c_int, accessibility_label: [*]const u8, accessibility_label_len: usize, has_text: c_int, text: [*]const u8, text_len: usize, has_command: c_int, command: [*]const u8, command_len: usize) c_int;
171
177
  extern fn native_sdk_windows_set_view_frame(host: *WindowsHost, window_id: u64, label: [*]const u8, label_len: usize, x: f64, y: f64, width: f64, height: f64) c_int;
@@ -438,6 +444,9 @@ pub const WindowsPlatform = struct {
438
444
  .focus_window_fn = focusWindow,
439
445
  .close_window_fn = closeWindow,
440
446
  .minimize_window_fn = minimizeWindow,
447
+ .maximize_window_fn = maximizeWindow,
448
+ .unmaximize_window_fn = unmaximizeWindow,
449
+ .request_close_window_fn = requestCloseWindow,
441
450
  .hide_window_fn = hideWindow,
442
451
  .show_window_fn = showWindow,
443
452
  .quit_app_fn = quitApp,
@@ -658,6 +667,7 @@ fn windowsCallback(context: ?*anyopaque, event: *const WindowsEvent) callconv(.c
658
667
  .open = event.open != 0,
659
668
  .focused = event.focused != 0,
660
669
  .hidden = event.hidden != 0,
670
+ .maximized = event.maximized != 0,
661
671
  } });
662
672
  },
663
673
  .view_focused => state.emit(.{ .view_focused = .{
@@ -1036,6 +1046,36 @@ fn minimizeWindow(context: ?*anyopaque, window_id: platform_mod.WindowId) anyerr
1036
1046
  if (native_sdk_windows_minimize_window(self.host, window_id) == 0) return error.WindowNotFound;
1037
1047
  }
1038
1048
 
1049
+ fn maximizeWindow(context: ?*anyopaque, window_id: platform_mod.WindowId) anyerror!void {
1050
+ const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1051
+ return windowControlResult(native_sdk_windows_maximize_window(self.host, window_id));
1052
+ }
1053
+
1054
+ fn unmaximizeWindow(context: ?*anyopaque, window_id: platform_mod.WindowId) anyerror!void {
1055
+ const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1056
+ return windowControlResult(native_sdk_windows_unmaximize_window(self.host, window_id));
1057
+ }
1058
+
1059
+ fn requestCloseWindow(context: ?*anyopaque, window_id: platform_mod.WindowId) anyerror!void {
1060
+ const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1061
+ return switch (native_sdk_windows_request_close_window(self.host, window_id)) {
1062
+ 1 => {},
1063
+ 0 => error.WindowNotFound,
1064
+ else => error.CloseFailed,
1065
+ };
1066
+ }
1067
+
1068
+ /// The maximize/unmaximize host status codes (see webview2_host.cpp).
1069
+ fn windowControlResult(status: c_int) anyerror!void {
1070
+ return switch (status) {
1071
+ 1 => {},
1072
+ 0 => error.WindowNotFound,
1073
+ -1 => error.WindowNotResizable,
1074
+ -2 => error.WindowHidden,
1075
+ else => error.Unexpected,
1076
+ };
1077
+ }
1078
+
1039
1079
  fn hideWindow(context: ?*anyopaque, window_id: platform_mod.WindowId) anyerror!void {
1040
1080
  const self: *WindowsPlatform = @ptrCast(@alignCast(context.?));
1041
1081
  if (native_sdk_windows_hide_window(self.host, window_id) == 0) return error.WindowNotFound;
@@ -2760,3 +2800,100 @@ fn flattenFilters(filters: []const platform_mod.FileFilter, buffer: []u8) []cons
2760
2800
  test "windows platform module exports type" {
2761
2801
  _ = WindowsPlatform;
2762
2802
  }
2803
+
2804
+ test "windows event declares the same fields in the same order as the C host" {
2805
+ // WindowsEvent is written twice, here and in webview2_host.cpp, and
2806
+ // crosses the C ABI by layout. A field added on one side only, or at
2807
+ // a different position, shifts every later field without a compile
2808
+ // error, so the two declarations are compared by name and order.
2809
+ const field_names = comptime blk: {
2810
+ const fields = @typeInfo(WindowsEvent).@"struct".fields;
2811
+ var names: [fields.len][]const u8 = undefined;
2812
+ for (fields, 0..) |field, index| names[index] = field.name;
2813
+ break :blk names;
2814
+ };
2815
+ const host_source = @embedFile("webview2_host.cpp");
2816
+ const header = "struct WindowsEvent {";
2817
+ const start = (std.mem.indexOf(u8, host_source, header) orelse return error.TestExpectedEqual) + header.len;
2818
+ const end = std.mem.indexOfPos(u8, host_source, start, "};") orelse return error.TestExpectedEqual;
2819
+ var lines = std.mem.splitScalar(u8, host_source[start..end], '\n');
2820
+ var count: usize = 0;
2821
+ var in_comment = false;
2822
+ while (lines.next()) |raw| {
2823
+ const line = std.mem.trim(u8, raw, " \t\r");
2824
+ if (in_comment) {
2825
+ if (std.mem.indexOf(u8, line, "*/") != null) in_comment = false;
2826
+ continue;
2827
+ }
2828
+ if (std.mem.startsWith(u8, line, "/*")) {
2829
+ if (std.mem.indexOf(u8, line, "*/") == null) in_comment = true;
2830
+ continue;
2831
+ }
2832
+ if (!std.mem.endsWith(u8, line, ";")) continue;
2833
+ var declaration = line[0 .. line.len - 1];
2834
+ if (std.mem.indexOfScalar(u8, declaration, '[')) |bracket| declaration = declaration[0..bracket];
2835
+ const name_start = (std.mem.lastIndexOfAny(u8, declaration, " *") orelse return error.TestExpectedEqual) + 1;
2836
+ try std.testing.expect(count < field_names.len);
2837
+ try std.testing.expectEqualStrings(field_names[count], declaration[name_start..]);
2838
+ count += 1;
2839
+ }
2840
+ try std.testing.expectEqual(field_names.len, count);
2841
+ }
2842
+
2843
+ /// One exported host function's source: from `start` up to the next
2844
+ /// exported `native_sdk_windows_` definition.
2845
+ fn hostFunction(host_source: []const u8, start: usize) ?[]const u8 {
2846
+ const end = std.mem.indexOfPos(u8, host_source, start + 1, "int native_sdk_windows_") orelse return null;
2847
+ return host_source[start..end];
2848
+ }
2849
+
2850
+ test "windows maximize status codes map to explicit errors" {
2851
+ try windowControlResult(1);
2852
+ try std.testing.expectError(error.WindowNotFound, windowControlResult(0));
2853
+ try std.testing.expectError(error.WindowNotResizable, windowControlResult(-1));
2854
+ try std.testing.expectError(error.WindowHidden, windowControlResult(-2));
2855
+ }
2856
+
2857
+ test "windows maximize refuses what the system would not maximize and reports every transition" {
2858
+ // The host verbs are C++ this suite cannot execute; pin their shape
2859
+ // textually, like the WM_CLOSE hook test above.
2860
+ const host_source = @embedFile("webview2_host.cpp");
2861
+ const maximize_at = std.mem.indexOf(u8, host_source, "int native_sdk_windows_maximize_window(") orelse return error.TestExpectedEqual;
2862
+ const maximize_fn = hostFunction(host_source, maximize_at) orelse return error.TestExpectedEqual;
2863
+ const resizable_at = std.mem.indexOf(u8, maximize_fn, "if (!found->second.resizable) return -1;") orelse return error.TestExpectedEqual;
2864
+ const visible_at = std.mem.indexOf(u8, maximize_fn, "if (!IsWindowVisible(found->second.hwnd)) return -2;") orelse return error.TestExpectedEqual;
2865
+ const show_at = std.mem.indexOf(u8, maximize_fn, "ShowWindow(found->second.hwnd, SW_MAXIMIZE);") orelse return error.TestExpectedEqual;
2866
+ try std.testing.expect(resizable_at < show_at and visible_at < show_at);
2867
+
2868
+ // Every frame event carries the live state, and the top-level window
2869
+ // procedure's WM_SIZE reports each transition, whoever caused it.
2870
+ try std.testing.expect(std.mem.indexOf(u8, host_source, "event.maximized = window.hwnd && IsZoomed(window.hwnd) ? 1 : 0;") != null);
2871
+ const proc_at = std.mem.indexOf(u8, host_source, "static LRESULT CALLBACK windowProc(") orelse return error.TestExpectedEqual;
2872
+ const size_at = std.mem.indexOfPos(u8, host_source, proc_at, "case WM_SIZE:") orelse return error.TestExpectedEqual;
2873
+ const next_case_at = std.mem.indexOfPos(u8, host_source, size_at, "case WM_SETFOCUS:") orelse return error.TestExpectedEqual;
2874
+ try std.testing.expect(std.mem.indexOf(u8, host_source[size_at..next_case_at], "emitMaximizedTransition(host, hwnd);") != null);
2875
+
2876
+ // A maximized chromeless window keeps the taskbar visible: without
2877
+ // a caption the system would size it to the whole monitor. The
2878
+ // min/max answer sets the size, and the position change re-derives
2879
+ // it from the monitor the window lands on, which the min/max answer
2880
+ // alone can get wrong on a secondary monitor larger than the primary.
2881
+ const minmax_at = std.mem.indexOfPos(u8, host_source, proc_at, "case WM_GETMINMAXINFO:") orelse return error.TestExpectedEqual;
2882
+ try std.testing.expect(std.mem.startsWith(u8, std.mem.trimStart(u8, host_source[minmax_at + "case WM_GETMINMAXINFO:".len ..], " \t\r\n"), "applyChromelessMaximizedBounds(host, hwnd, reinterpret_cast<MINMAXINFO *>(lparam));"));
2883
+ const changing_at = std.mem.indexOfPos(u8, host_source, proc_at, "case WM_WINDOWPOSCHANGING:") orelse return error.TestExpectedEqual;
2884
+ try std.testing.expect(std.mem.startsWith(u8, std.mem.trimStart(u8, host_source[changing_at + "case WM_WINDOWPOSCHANGING:".len ..], " \t\r\n"), "clampChromelessMaximizedFrame(host, hwnd, reinterpret_cast<WINDOWPOS *>(lparam));"));
2885
+ }
2886
+
2887
+ test "windows close request posts the user's WM_CLOSE while the app's close stays direct" {
2888
+ const host_source = @embedFile("webview2_host.cpp");
2889
+ const request_at = std.mem.indexOf(u8, host_source, "int native_sdk_windows_request_close_window(") orelse return error.TestExpectedEqual;
2890
+ const request_fn = hostFunction(host_source, request_at) orelse return error.TestExpectedEqual;
2891
+ try std.testing.expect(std.mem.indexOf(u8, request_fn, "PostMessageW(found->second.hwnd, WM_CLOSE, 0, 0)") != null);
2892
+ try std.testing.expect(std.mem.indexOf(u8, request_fn, "DestroyWindow") == null);
2893
+
2894
+ // The app's own close keeps bypassing the WM_CLOSE close-policy hook.
2895
+ const close_at = std.mem.indexOf(u8, host_source, "int native_sdk_windows_close_window(") orelse return error.TestExpectedEqual;
2896
+ const close_fn = hostFunction(host_source, close_at) orelse return error.TestExpectedEqual;
2897
+ try std.testing.expect(std.mem.indexOf(u8, close_fn, "DestroyWindow(found->second.hwnd);") != null);
2898
+ try std.testing.expect(std.mem.indexOf(u8, close_fn, "WM_CLOSE") == null);
2899
+ }
@@ -0,0 +1,248 @@
1
+ /* MinGW startup and CRT pieces for scriptc's Windows runtime on MSVC targets.
2
+ *
3
+ * The pinned scriptc builds its Windows runtime pack against Zig's MinGW
4
+ * sysroot (packages/runtime-win32-x64-msvc/scripts/build.mjs passes
5
+ * -target x86_64-windows-gnu). The runtime is already Win32-native for
6
+ * threads, child processes and its event loop; what remains is what MinGW
7
+ * layers over the UCRT. The MSVC build links the same UCRT statically, so
8
+ * this file restates exactly that, with the MinGW behaviour the runtime was
9
+ * compiled against: running its .ctors constructors, the dllimport slots for
10
+ * UCRT functions, stat64i32, the ___chkstk_ms stack probe, and <dirent.h>.
11
+ * POSIX names such as open/read/close come from MSVC's oldnames.lib instead.
12
+ *
13
+ * Delete this file once scriptc ships a runtime pack built for the MSVC CRT.
14
+ */
15
+
16
+ /* The import slots below only take the address of the UCRT's "insecure"
17
+ * functions (_wfopen, _wopen); nothing here calls them. */
18
+ #define _CRT_SECURE_NO_WARNINGS
19
+
20
+ #include <ctype.h>
21
+ #include <errno.h>
22
+ #include <io.h>
23
+ #include <process.h>
24
+ #include <stdint.h>
25
+ #include <stdio.h>
26
+ #include <stdlib.h>
27
+ #include <string.h>
28
+ #include <sys/stat.h>
29
+ #include <sys/utime.h>
30
+
31
+ #define WIN32_LEAN_AND_MEAN
32
+ #include <windows.h>
33
+
34
+ /* clang places a MinGW object's __attribute__((constructor)) functions in
35
+ * .ctors, which only MinGW's startup runs; the MSVC CRT runs .CRT$XCU. The
36
+ * runtime initializes state that way (scr_json.c's small-int table, for one),
37
+ * so walk this image's .ctors from a .CRT$XCU initializer, back to front as
38
+ * MinGW's __do_global_ctors does, skipping its 0/-1 list markers. */
39
+ typedef void (*scriptc_ctor_fn)(void);
40
+ extern IMAGE_DOS_HEADER __ImageBase;
41
+
42
+ static void __cdecl scriptc_run_mingw_ctors(void) {
43
+ const char *base = (const char *)&__ImageBase;
44
+ const IMAGE_NT_HEADERS *nt = (const IMAGE_NT_HEADERS *)(base + __ImageBase.e_lfanew);
45
+ const IMAGE_SECTION_HEADER *section = IMAGE_FIRST_SECTION(nt);
46
+ for (WORD index = 0; index < nt->FileHeader.NumberOfSections; index++, section++) {
47
+ if (memcmp(section->Name, ".ctors\0\0", IMAGE_SIZEOF_SHORT_NAME) != 0) continue;
48
+ scriptc_ctor_fn *begin = (scriptc_ctor_fn *)(base + section->VirtualAddress);
49
+ scriptc_ctor_fn *end = begin + section->Misc.VirtualSize / sizeof(scriptc_ctor_fn);
50
+ while (end > begin) {
51
+ scriptc_ctor_fn ctor = *--end;
52
+ if (ctor && ctor != (scriptc_ctor_fn)(intptr_t)-1) ctor();
53
+ }
54
+ }
55
+ }
56
+
57
+ __attribute__((section(".CRT$XCU"), used)) static void(__cdecl *const scriptc_mingw_ctors)(void) =
58
+ scriptc_run_mingw_ctors;
59
+
60
+ /* MinGW's headers declare UCRT functions dllimport, so the runtime calls them
61
+ * through __imp_ slots that the static UCRT does not define. lld would bind
62
+ * most of them to the static definitions anyway, but with one LNK4217 warning
63
+ * each (and tolower not at all), so define every slot the runtime pack's
64
+ * library objects reference. A slot added by a future pack shows up as an
65
+ * undefined symbol or a new LNK4217 warning in the MSVC link. */
66
+ #define SCRIPTC_UCRT_IMPORT(name) __typeof__(&name) const __imp_##name = &name;
67
+ /* The UCRT exports _assert, but its <assert.h> declares only _wassert. */
68
+ void __cdecl _assert(const char *message, const char *file, unsigned line);
69
+ SCRIPTC_UCRT_IMPORT(__acrt_iob_func)
70
+ SCRIPTC_UCRT_IMPORT(_assert)
71
+ SCRIPTC_UCRT_IMPORT(_chsize_s)
72
+ SCRIPTC_UCRT_IMPORT(_commit)
73
+ SCRIPTC_UCRT_IMPORT(_errno)
74
+ SCRIPTC_UCRT_IMPORT(_fstat64)
75
+ SCRIPTC_UCRT_IMPORT(_fullpath)
76
+ SCRIPTC_UCRT_IMPORT(_get_osfhandle)
77
+ SCRIPTC_UCRT_IMPORT(_getpid)
78
+ SCRIPTC_UCRT_IMPORT(_stricmp)
79
+ SCRIPTC_UCRT_IMPORT(_umask_s)
80
+ SCRIPTC_UCRT_IMPORT(_utime64)
81
+ SCRIPTC_UCRT_IMPORT(_wfopen)
82
+ SCRIPTC_UCRT_IMPORT(_wopen)
83
+ SCRIPTC_UCRT_IMPORT(tolower)
84
+
85
+ /* MinGW-UCRT aliases stat64i32 to the UCRT's _stat64i32. */
86
+ int __cdecl stat64i32(const char *path, struct _stat64i32 *buf) {
87
+ return _stat64i32(path, buf);
88
+ }
89
+
90
+ /* MinGW's stack probe: rax holds the frame size; touch every page below the
91
+ * caller's stack pointer without moving it, preserving all registers. */
92
+ __attribute__((naked)) void ___chkstk_ms(void) {
93
+ __asm__ volatile(
94
+ "pushq %rcx\n\t"
95
+ "pushq %rax\n\t"
96
+ "cmpq $0x1000, %rax\n\t"
97
+ "leaq 24(%rsp), %rcx\n\t"
98
+ "jb 1f\n"
99
+ "2:\n\t"
100
+ "subq $0x1000, %rcx\n\t"
101
+ "testq %rcx, (%rcx)\n\t"
102
+ "subq $0x1000, %rax\n\t"
103
+ "cmpq $0x1000, %rax\n\t"
104
+ "ja 2b\n"
105
+ "1:\n\t"
106
+ "subq %rax, %rcx\n\t"
107
+ "testq %rcx, (%rcx)\n\t"
108
+ "popq %rax\n\t"
109
+ "popq %rcx\n\t"
110
+ "retq");
111
+ }
112
+
113
+ /* <dirent.h>, ported from mingw-w64's public-domain misc/dirent.c (char
114
+ * variant). struct dirent and DIR keep mingw-w64's layout because the
115
+ * runtime was compiled against its header. */
116
+
117
+ struct dirent {
118
+ long d_ino;
119
+ unsigned short d_reclen;
120
+ unsigned short d_namlen;
121
+ char d_name[260];
122
+ };
123
+
124
+ typedef struct {
125
+ struct _finddata_t dd_dta;
126
+ struct dirent dd_dir;
127
+ intptr_t dd_handle;
128
+ int dd_stat;
129
+ char dd_name[1];
130
+ } DIR;
131
+
132
+ DIR *__cdecl opendir(const char *path) {
133
+ errno = 0;
134
+ if (!path) {
135
+ errno = EFAULT;
136
+ return NULL;
137
+ }
138
+ if (path[0] == '\0') {
139
+ errno = ENOTDIR;
140
+ return NULL;
141
+ }
142
+ DWORD attributes = GetFileAttributesA(path);
143
+ if (attributes == INVALID_FILE_ATTRIBUTES) {
144
+ errno = ENOENT;
145
+ return NULL;
146
+ }
147
+ if (!(attributes & FILE_ATTRIBUTE_DIRECTORY)) {
148
+ errno = ENOTDIR;
149
+ return NULL;
150
+ }
151
+ char full[MAX_PATH];
152
+ if (!_fullpath(full, path, MAX_PATH)) return NULL;
153
+ size_t len = strlen(full);
154
+ /* The search pattern: the absolute path, a separator, "*", and NUL. */
155
+ DIR *dir = malloc(sizeof(DIR) + len + 3);
156
+ if (!dir) {
157
+ errno = ENOMEM;
158
+ return NULL;
159
+ }
160
+ memcpy(dir->dd_name, full, len);
161
+ if (len > 0 && full[len - 1] != '/' && full[len - 1] != '\\') dir->dd_name[len++] = '\\';
162
+ dir->dd_name[len++] = '*';
163
+ dir->dd_name[len] = '\0';
164
+ dir->dd_handle = -1;
165
+ dir->dd_stat = 0;
166
+ dir->dd_dir.d_ino = 0;
167
+ dir->dd_dir.d_reclen = 0;
168
+ dir->dd_dir.d_namlen = 0;
169
+ memset(dir->dd_dir.d_name, 0, sizeof(dir->dd_dir.d_name));
170
+ return dir;
171
+ }
172
+
173
+ struct dirent *__cdecl readdir(DIR *dir) {
174
+ errno = 0;
175
+ if (!dir) {
176
+ errno = EFAULT;
177
+ return NULL;
178
+ }
179
+ if (dir->dd_stat < 0) return NULL;
180
+ if (dir->dd_stat == 0) {
181
+ dir->dd_handle = _findfirst(dir->dd_name, &dir->dd_dta);
182
+ dir->dd_stat = dir->dd_handle == -1 ? -1 : 1;
183
+ } else if (_findnext(dir->dd_handle, &dir->dd_dta)) {
184
+ /* _findnext reports the end as ENOENT; the end is not an error. */
185
+ if (GetLastError() == ERROR_NO_MORE_FILES) errno = 0;
186
+ _findclose(dir->dd_handle);
187
+ dir->dd_handle = -1;
188
+ dir->dd_stat = -1;
189
+ } else {
190
+ dir->dd_stat++;
191
+ }
192
+ if (dir->dd_stat <= 0) return NULL;
193
+ /* Both buffers are MAX_PATH chars, so the name and its NUL always fit. */
194
+ size_t len = strlen(dir->dd_dta.name);
195
+ dir->dd_dir.d_namlen = (unsigned short)len;
196
+ memcpy(dir->dd_dir.d_name, dir->dd_dta.name, len + 1);
197
+ return &dir->dd_dir;
198
+ }
199
+
200
+ int __cdecl closedir(DIR *dir) {
201
+ errno = 0;
202
+ if (!dir) {
203
+ errno = EFAULT;
204
+ return -1;
205
+ }
206
+ int rc = dir->dd_handle != -1 ? _findclose(dir->dd_handle) : 0;
207
+ free(dir);
208
+ return rc;
209
+ }
210
+
211
+ void __cdecl rewinddir(DIR *dir) {
212
+ errno = 0;
213
+ if (!dir) {
214
+ errno = EFAULT;
215
+ return;
216
+ }
217
+ if (dir->dd_handle != -1) _findclose(dir->dd_handle);
218
+ dir->dd_handle = -1;
219
+ dir->dd_stat = 0;
220
+ }
221
+
222
+ long __cdecl telldir(DIR *dir) {
223
+ errno = 0;
224
+ if (!dir) {
225
+ errno = EFAULT;
226
+ return -1;
227
+ }
228
+ return dir->dd_stat;
229
+ }
230
+
231
+ void __cdecl seekdir(DIR *dir, long position) {
232
+ errno = 0;
233
+ if (!dir) {
234
+ errno = EFAULT;
235
+ return;
236
+ }
237
+ if (position < -1) {
238
+ errno = EINVAL;
239
+ } else if (position == -1) {
240
+ if (dir->dd_handle != -1) _findclose(dir->dd_handle);
241
+ dir->dd_handle = -1;
242
+ dir->dd_stat = -1;
243
+ } else {
244
+ rewinddir(dir);
245
+ while (dir->dd_stat < position && readdir(dir)) {
246
+ }
247
+ }
248
+ }
@@ -312,6 +312,9 @@ struct WindowsEvent {
312
312
  * the selected item's id (0 = dismissed without a selection). */
313
313
  uint64_t widget_id;
314
314
  uint32_t menu_item_id;
315
+ /* Window events: nonzero while the window is maximized (IsZoomed).
316
+ * Every emit carries the live value, so any frame event reports it. */
317
+ int maximized;
315
318
  };
316
319
 
317
320
  /* One context-menu entry crossing the C ABI (the same shape the macOS
@@ -415,6 +418,9 @@ struct Window {
415
418
  /* True while the .hide policy has this window off the glass; the
416
419
  * hwnd stays live and the map entry stays owned. */
417
420
  bool policy_hidden = false;
421
+ /* The maximized state last reported to the runtime, so WM_SIZE emits
422
+ * a frame event only when it changes. */
423
+ bool maximized = false;
418
424
  };
419
425
 
420
426
  /* One rectangle of a canvas view's window-drag mirror (runtime push,
@@ -1382,6 +1388,7 @@ static void emit(Host *host, const Window &window, EventKind kind) {
1382
1388
  event.open = window.hwnd != nullptr;
1383
1389
  event.focused = windowOwnsKeyboardFocus(window);
1384
1390
  event.hidden = window.policy_hidden ? 1 : 0;
1391
+ event.maximized = window.hwnd && IsZoomed(window.hwnd) ? 1 : 0;
1385
1392
  event.label = window.label.c_str();
1386
1393
  event.label_len = window.label.size();
1387
1394
  event.title = window.title.c_str();
@@ -1389,6 +1396,23 @@ static void emit(Host *host, const Window &window, EventKind kind) {
1389
1396
  host->callback(host->callback_context, &event);
1390
1397
  }
1391
1398
 
1399
+ /* WM_SIZE runs for every maximize and restore, whoever started it: the
1400
+ * caption double-click, the system menu, Win+Up, a drag to the top edge,
1401
+ * or the app's own maximize call. A frame event on each transition keeps the runtime's
1402
+ * maximized state current; ordinary resizes emit nothing new. */
1403
+ static void emitMaximizedTransition(Host *host, HWND hwnd) {
1404
+ if (!host || !hwnd) return;
1405
+ for (auto &entry : host->windows) {
1406
+ Window &window = entry.second;
1407
+ if (window.hwnd != hwnd) continue;
1408
+ const bool maximized = IsZoomed(hwnd) != FALSE;
1409
+ if (maximized == window.maximized) return;
1410
+ window.maximized = maximized;
1411
+ emit(host, window, kWindowFrame);
1412
+ return;
1413
+ }
1414
+ }
1415
+
1392
1416
  static void emitFileDrop(Host *host, const Window &window, const std::string &paths) {
1393
1417
  if (!host || !host->callback || paths.empty()) return;
1394
1418
  WindowsEvent event = {};
@@ -5673,6 +5697,67 @@ static bool handleAppTimerMessage(Host *host, WPARAM wparam) {
5673
5697
  return true;
5674
5698
  }
5675
5699
 
5700
+ /* Windows sizes only captioned windows to the work area when they
5701
+ * maximize; a caption-less popup covers the whole monitor, taskbar
5702
+ * included. Chromeless windows get the captioned behavior back: the
5703
+ * maximized frame is the work area of the window's monitor, grown by the
5704
+ * resize frame so the borders park offscreen and the client fills the
5705
+ * work area exactly, whether an app control, a drag-region double-click,
5706
+ * or the system menu maximized it. Writes that frame in screen
5707
+ * coordinates, plus the monitor rect it was taken from. */
5708
+ static bool chromelessMaximizedFrame(HWND hwnd, HMONITOR monitor_handle, RECT *maximized, RECT *monitor_rect) {
5709
+ MONITORINFO monitor = {};
5710
+ monitor.cbSize = sizeof(monitor);
5711
+ if (!GetMonitorInfoW(monitor_handle, &monitor)) return false;
5712
+ RECT frame = {};
5713
+ const DWORD style = (DWORD)GetWindowLongPtrW(hwnd, GWL_STYLE);
5714
+ const DWORD ex_style = (DWORD)GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
5715
+ if (!adjustWindowRectForDpi(&frame, style, GetMenu(hwnd) != nullptr, ex_style, dpiForWindow(hwnd))) return false;
5716
+ maximized->left = monitor.rcWork.left + frame.left;
5717
+ maximized->top = monitor.rcWork.top + frame.top;
5718
+ maximized->right = monitor.rcWork.right + frame.right;
5719
+ maximized->bottom = monitor.rcWork.bottom + frame.bottom;
5720
+ if (monitor_rect) *monitor_rect = monitor.rcMonitor;
5721
+ return true;
5722
+ }
5723
+
5724
+ static void applyChromelessMaximizedBounds(Host *host, HWND hwnd, MINMAXINFO *info) {
5725
+ if (!info || !chromelessWindowForHwnd(host, hwnd)) return;
5726
+ RECT maximized = {};
5727
+ RECT monitor = {};
5728
+ if (!chromelessMaximizedFrame(hwnd, MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &maximized, &monitor)) return;
5729
+ info->ptMaxPosition.x = maximized.left - monitor.left;
5730
+ info->ptMaxPosition.y = maximized.top - monitor.top;
5731
+ info->ptMaxSize.x = maximized.right - maximized.left;
5732
+ info->ptMaxSize.y = maximized.bottom - maximized.top;
5733
+ }
5734
+
5735
+ /* WM_GETMINMAXINFO speaks in primary-monitor terms: when a window
5736
+ * maximizes onto another monitor, the system rescales those values by
5737
+ * the difference between the two monitors, which can overshoot a
5738
+ * monitor larger than the primary one. So the final frame of a maximized
5739
+ * chromeless window is set here, from the monitor it actually lands on,
5740
+ * whatever moved it: a maximize, or a maximized window moving to another
5741
+ * monitor. Pure z-order changes pass through. */
5742
+ static void clampChromelessMaximizedFrame(Host *host, HWND hwnd, WINDOWPOS *pos) {
5743
+ if (!pos || ((pos->flags & SWP_NOMOVE) && (pos->flags & SWP_NOSIZE))) return;
5744
+ if (!IsZoomed(hwnd) || !chromelessWindowForHwnd(host, hwnd)) return;
5745
+ RECT proposed = {};
5746
+ GetWindowRect(hwnd, &proposed);
5747
+ if (!(pos->flags & SWP_NOMOVE)) OffsetRect(&proposed, pos->x - proposed.left, pos->y - proposed.top);
5748
+ if (!(pos->flags & SWP_NOSIZE)) {
5749
+ proposed.right = proposed.left + pos->cx;
5750
+ proposed.bottom = proposed.top + pos->cy;
5751
+ }
5752
+ RECT maximized = {};
5753
+ if (!chromelessMaximizedFrame(hwnd, MonitorFromRect(&proposed, MONITOR_DEFAULTTONEAREST), &maximized, nullptr)) return;
5754
+ pos->x = maximized.left;
5755
+ pos->y = maximized.top;
5756
+ pos->cx = maximized.right - maximized.left;
5757
+ pos->cy = maximized.bottom - maximized.top;
5758
+ pos->flags &= ~(SWP_NOMOVE | SWP_NOSIZE);
5759
+ }
5760
+
5676
5761
  static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
5677
5762
  if (message == WM_NCCREATE) {
5678
5763
  auto *create = reinterpret_cast<CREATESTRUCTW *>(lparam);
@@ -5925,6 +6010,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
5925
6010
  }
5926
6011
  }
5927
6012
  }
6013
+ emitMaximizedTransition(host, hwnd);
5928
6014
  return 0;
5929
6015
  case WM_SETFOCUS:
5930
6016
  case WM_KILLFOCUS:
@@ -6010,7 +6096,11 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
6010
6096
  for (auto &entry : host->windows) emit(host, entry.second, kFrame);
6011
6097
  }
6012
6098
  return 0;
6099
+ case WM_WINDOWPOSCHANGING:
6100
+ clampChromelessMaximizedFrame(host, hwnd, reinterpret_cast<WINDOWPOS *>(lparam));
6101
+ break;
6013
6102
  case WM_GETMINMAXINFO:
6103
+ applyChromelessMaximizedBounds(host, hwnd, reinterpret_cast<MINMAXINFO *>(lparam));
6014
6104
  if (host) {
6015
6105
  for (auto &entry : host->windows) {
6016
6106
  Window &window = entry.second;
@@ -6903,6 +6993,45 @@ int native_sdk_windows_minimize_window(Host *host, uint64_t window_id) {
6903
6993
  return 1;
6904
6994
  }
6905
6995
 
6996
+ /* Maximize and restore-from-maximized for app-drawn window controls.
6997
+ * Returns 1 on success, 0 for an unknown or destroyed window, -1 when the
6998
+ * window is not resizable (the system offers it no maximize either), and
6999
+ * -2 when it is not on the glass: SW_MAXIMIZE and SW_RESTORE both show a
7000
+ * hidden window, which would bypass the policy-hidden and deferred-show
7001
+ * bookkeeping. The state change reaches the runtime through WM_SIZE. */
7002
+ int native_sdk_windows_maximize_window(Host *host, uint64_t window_id) {
7003
+ if (!host) return 0;
7004
+ auto found = host->windows.find(window_id);
7005
+ if (found == host->windows.end() || !found->second.hwnd) return 0;
7006
+ if (!found->second.resizable) return -1;
7007
+ if (!IsWindowVisible(found->second.hwnd)) return -2;
7008
+ ShowWindow(found->second.hwnd, SW_MAXIMIZE);
7009
+ return 1;
7010
+ }
7011
+
7012
+ int native_sdk_windows_unmaximize_window(Host *host, uint64_t window_id) {
7013
+ if (!host) return 0;
7014
+ auto found = host->windows.find(window_id);
7015
+ if (found == host->windows.end() || !found->second.hwnd) return 0;
7016
+ if (!IsZoomed(found->second.hwnd)) return 1;
7017
+ if (!IsWindowVisible(found->second.hwnd)) return -2;
7018
+ ShowWindow(found->second.hwnd, SW_RESTORE);
7019
+ return 1;
7020
+ }
7021
+
7022
+ /* The user-close request: post the same WM_CLOSE the caption close button
7023
+ * and Alt+F4 produce, so the WM_CLOSE handler applies close_policy.
7024
+ * native_sdk_windows_close_window stays the app's unconditional close.
7025
+ * Posting keeps the close out of the runtime call that requested it.
7026
+ * Returns 1 when posted, 0 for an unknown or destroyed window, -1 when
7027
+ * the post fails. */
7028
+ int native_sdk_windows_request_close_window(Host *host, uint64_t window_id) {
7029
+ if (!host) return 0;
7030
+ auto found = host->windows.find(window_id);
7031
+ if (found == host->windows.end() || !found->second.hwnd) return 0;
7032
+ return PostMessageW(found->second.hwnd, WM_CLOSE, 0, 0) ? 1 : -1;
7033
+ }
7034
+
6906
7035
  int native_sdk_windows_hide_window(Host *host, uint64_t window_id) {
6907
7036
  if (!host) return 0;
6908
7037
  auto found = host->windows.find(window_id);
@@ -289,6 +289,8 @@ pub fn writeWindowJsonToWriter(window: platform.WindowInfo, writer: anytype) !vo
289
289
  window.frame.height,
290
290
  window.scale_factor,
291
291
  });
292
+ try writer.writeAll(",\"maximized\":");
293
+ try writer.writeAll(if (window.maximized) "true" else "false");
292
294
  try writer.writeByte('}');
293
295
  }
294
296
 
@@ -815,6 +815,11 @@ pub const Runtime = struct {
815
815
  const viewTreeHasFocused = WindowViewMethods.viewTreeHasFocused;
816
816
  const viewTreeHasFocusedDepth = WindowViewMethods.viewTreeHasFocusedDepth;
817
817
 
818
+ const WindowControlMethods = @import("window_controls.zig").RuntimeWindowControls(Runtime);
819
+ pub const maximizeWindow = WindowControlMethods.maximizeWindow;
820
+ pub const unmaximizeWindow = WindowControlMethods.unmaximizeWindow;
821
+ pub const requestCloseWindow = WindowControlMethods.requestCloseWindow;
822
+
818
823
  const SystemServiceMethods = runtime_system_services.RuntimeSystemServices(Runtime);
819
824
  pub const readClipboard = SystemServiceMethods.readClipboard;
820
825
  pub const writeClipboard = SystemServiceMethods.writeClipboard;
@@ -45,4 +45,5 @@ test {
45
45
  _ = @import("persist_store.zig");
46
46
  _ = @import("session_record.zig");
47
47
  _ = @import("session_tests.zig");
48
+ _ = @import("window_controls_tests.zig");
48
49
  }
@@ -0,0 +1,73 @@
1
+ //! Window controls for app-drawn title bars, beyond minimize: maximize,
2
+ //! restore from maximized, and the user-close request. Each is the real
3
+ //! OS verb for a tracked, open window. Like `minimizeWindow`, no runtime
4
+ //! bookkeeping moves here: the host's own frame events report what the
5
+ //! OS did, the same way they report the caption buttons, the system menu,
6
+ //! and keyboard shortcuts.
7
+ const lifecycle = @import("window_lifecycle_policy.zig");
8
+ const std = @import("std");
9
+ const runtime_window_views = @import("window_views.zig");
10
+ const platform = @import("../platform/root.zig");
11
+
12
+ /// The page event a window's WebViews receive when its maximized state
13
+ /// changes, whoever changed it, so a page-drawn maximize/restore button
14
+ /// can follow the caption double-click, the system menu, and keyboard
15
+ /// shortcuts: `window.zero.on("window:state", ({ maximized }) => ...)`.
16
+ pub const window_state_event = "window:state";
17
+
18
+ /// Called by the runtime's window-state update when `maximized` flips.
19
+ /// Best effort, like the other page notifications: the detail is always
20
+ /// valid JSON, so the only failure is a host without page events
21
+ /// (`error.UnsupportedService`), where no page exists to tell.
22
+ pub fn announceMaximized(runtime: anytype, window_id: platform.WindowId, maximized: bool) void {
23
+ var detail_buffer: [64]u8 = undefined;
24
+ const detail = std.fmt.bufPrint(&detail_buffer, "{{\"windowId\":{d},\"maximized\":{s}}}", .{ window_id, if (maximized) "true" else "false" }) catch return;
25
+ runtime.options.platform.services.emitWindowEvent(window_id, window_state_event, detail) catch {};
26
+ }
27
+
28
+ pub fn RuntimeWindowControls(comptime Runtime: type) type {
29
+ const WindowViewMethods = runtime_window_views.RuntimeWindowViews(Runtime);
30
+
31
+ return struct {
32
+ /// Maximize a window. Refused for a non-resizable window
33
+ /// (`error.WindowNotResizable`) and for one that is not on the
34
+ /// glass (`error.WindowHidden`). `WindowInfo.maximized` follows
35
+ /// the host's frame event.
36
+ pub fn maximizeWindow(self: *Runtime, window_id: platform.WindowId) anyerror!void {
37
+ try requireOpenWindow(self, window_id);
38
+ try self.options.platform.services.maximizeWindow(window_id);
39
+ }
40
+
41
+ /// Restore a maximized window to its normal frame. A window that
42
+ /// is not maximized is left as it is.
43
+ pub fn unmaximizeWindow(self: *Runtime, window_id: platform.WindowId) anyerror!void {
44
+ try requireOpenWindow(self, window_id);
45
+ try self.options.platform.services.unmaximizeWindow(window_id);
46
+ }
47
+
48
+ /// Ask a window to close as if the user pressed its close button,
49
+ /// so its `close_policy` applies: `.hide` hides it and keeps its
50
+ /// views, `.quit` closes it and dispatches `window_closed`.
51
+ /// `closeWindow` remains the app's own unconditional close. The
52
+ /// request is queued; its outcome arrives as the host's frame
53
+ /// events, so success here means the request was delivered.
54
+ pub fn requestCloseWindow(self: *Runtime, window_id: platform.WindowId) anyerror!void {
55
+ try requireOpenWindow(self, window_id);
56
+ try self.options.platform.services.requestCloseWindow(window_id);
57
+ }
58
+
59
+ /// The liveness gate every window verb shares: a closed window
60
+ /// keeps its table slot, so a resolvable id is not enough.
61
+ fn requireOpenWindow(self: *Runtime, window_id: platform.WindowId) !void {
62
+ if (self.window_lifecycle_policy) |policy| {
63
+ // The compiled policy knows close, hide, show, and focus,
64
+ // which move runtime flags, and minimize, which moves
65
+ // none. These controls move none either, so they take
66
+ // minimize's admission: the window exists and is open.
67
+ return lifecycle.plan(policy, self, .begin, .{ .id = window_id, .variant = @intFromEnum(lifecycle.Verb.minimize) }).admitted();
68
+ }
69
+ const index = WindowViewMethods.findWindowIndexById(self, window_id) orelse return error.WindowNotFound;
70
+ if (!self.windows[index].info.open) return error.WindowNotFound;
71
+ }
72
+ };
73
+ }
@@ -0,0 +1,192 @@
1
+ const support = @import("test_support.zig");
2
+ const std = support.std;
3
+ const platform = support.platform;
4
+ const App = support.App;
5
+ const Runtime = support.Runtime;
6
+ const Event = support.Event;
7
+ const TestHarness = support.TestHarness;
8
+ const window_controls = @import("window_controls.zig");
9
+ const bridge_responses = @import("bridge_responses.zig");
10
+
11
+ const ControlsApp = struct {
12
+ window_closed_count: u32 = 0,
13
+
14
+ fn app(self: *@This()) App {
15
+ return .{ .context = self, .name = "window-controls", .source = platform.WebViewSource.html("<p>Controls</p>"), .event_fn = event };
16
+ }
17
+
18
+ fn event(context: *anyopaque, runtime: *Runtime, incoming: Event) !void {
19
+ _ = runtime;
20
+ const self: *@This() = @ptrCast(@alignCast(context));
21
+ if (incoming == .window_closed) self.window_closed_count += 1;
22
+ }
23
+ };
24
+
25
+ fn windowInfo(runtime: *const Runtime, window_id: platform.WindowId) ?platform.WindowInfo {
26
+ for (runtime.windows[0..runtime.window_count]) |window| {
27
+ if (window.info.id == window_id) return window.info;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ fn expectEveryControlRefused(runtime: *Runtime, window_id: platform.WindowId, expected: anyerror) !void {
33
+ try std.testing.expectError(expected, runtime.maximizeWindow(window_id));
34
+ try std.testing.expectError(expected, runtime.unmaximizeWindow(window_id));
35
+ try std.testing.expectError(expected, runtime.requestCloseWindow(window_id));
36
+ }
37
+
38
+ test "maximize and unmaximize reach the host and maximized follows its frame events" {
39
+ const harness = try TestHarness().create(std.testing.allocator, .{});
40
+ defer harness.destroy(std.testing.allocator);
41
+ var app_state: ControlsApp = .{};
42
+ try harness.start(app_state.app());
43
+
44
+ const tools = try harness.runtime.createWindow(.{ .label = "tools", .title = "Tools" });
45
+ try harness.runtime.maximizeWindow(tools.id);
46
+ // The verb moves no runtime state; the host's frame event reports it.
47
+ try std.testing.expect(!windowInfo(&harness.runtime, tools.id).?.maximized);
48
+ try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.windowFrameEvent(tools.id).?);
49
+ try std.testing.expect(windowInfo(&harness.runtime, tools.id).?.maximized);
50
+ try std.testing.expect(windowInfo(&harness.runtime, tools.id).?.state().maximized);
51
+ var output: [platform.max_windows]platform.WindowInfo = undefined;
52
+ for (harness.runtime.listWindows(&output)) |listed| {
53
+ if (listed.id == tools.id) try std.testing.expect(listed.maximized);
54
+ }
55
+
56
+ try harness.runtime.unmaximizeWindow(tools.id);
57
+ try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.windowFrameEvent(tools.id).?);
58
+ try std.testing.expect(!windowInfo(&harness.runtime, tools.id).?.maximized);
59
+ // Restoring a window that is not maximized leaves it as it is.
60
+ try harness.runtime.unmaximizeWindow(tools.id);
61
+ try std.testing.expect(!harness.null_platform.windows[1].maximized);
62
+ }
63
+
64
+ test "a maximized change reaches the window's pages and its window JSON" {
65
+ const harness = try TestHarness().create(std.testing.allocator, .{});
66
+ defer harness.destroy(std.testing.allocator);
67
+ var app_state: ControlsApp = .{};
68
+ try harness.start(app_state.app());
69
+
70
+ const tools = try harness.runtime.createWindow(.{ .label = "tools" });
71
+ try harness.runtime.maximizeWindow(tools.id);
72
+ const before = harness.null_platform.windowEventCount();
73
+ try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.windowFrameEvent(tools.id).?);
74
+ try std.testing.expectEqual(before + 1, harness.null_platform.windowEventCount());
75
+ try std.testing.expectEqual(tools.id, harness.null_platform.lastWindowEventWindowId());
76
+ try std.testing.expectEqualStrings(window_controls.window_state_event, harness.null_platform.lastWindowEventName());
77
+ try std.testing.expectEqualStrings("{\"windowId\":2,\"maximized\":true}", harness.null_platform.lastWindowEventDetail());
78
+
79
+ // A page loading now reads the same state from the window list.
80
+ var json_buffer: [512]u8 = undefined;
81
+ var writer = std.Io.Writer.fixed(&json_buffer);
82
+ try bridge_responses.writeWindowJsonToWriter(windowInfo(&harness.runtime, tools.id).?, &writer);
83
+ try std.testing.expect(std.mem.endsWith(u8, writer.buffered(), ",\"maximized\":true}"));
84
+
85
+ // A frame event that leaves the state alone (a move) tells nobody.
86
+ try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.windowFrameEvent(tools.id).?);
87
+ try std.testing.expectEqual(before + 1, harness.null_platform.windowEventCount());
88
+
89
+ try harness.runtime.unmaximizeWindow(tools.id);
90
+ try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.windowFrameEvent(tools.id).?);
91
+ try std.testing.expectEqual(before + 2, harness.null_platform.windowEventCount());
92
+ try std.testing.expectEqualStrings("{\"windowId\":2,\"maximized\":false}", harness.null_platform.lastWindowEventDetail());
93
+ }
94
+
95
+ test "a maximize the app did not request still updates maximized" {
96
+ const harness = try TestHarness().create(std.testing.allocator, .{});
97
+ defer harness.destroy(std.testing.allocator);
98
+ var app_state: ControlsApp = .{};
99
+ try harness.start(app_state.app());
100
+
101
+ // A caption double-click, the system menu, or Win+Up: the host's
102
+ // frame event is the only signal, and it is enough.
103
+ var state = windowInfo(&harness.runtime, 1).?.state();
104
+ state.maximized = true;
105
+ try harness.runtime.dispatchPlatformEvent(app_state.app(), .{ .window_frame_changed = state });
106
+ try std.testing.expect(windowInfo(&harness.runtime, 1).?.maximized);
107
+ state.maximized = false;
108
+ try harness.runtime.dispatchPlatformEvent(app_state.app(), .{ .window_frame_changed = state });
109
+ try std.testing.expect(!windowInfo(&harness.runtime, 1).?.maximized);
110
+ }
111
+
112
+ test "window controls refuse unknown, closed, hidden, and non-resizable windows" {
113
+ const harness = try TestHarness().create(std.testing.allocator, .{});
114
+ defer harness.destroy(std.testing.allocator);
115
+ var app_state: ControlsApp = .{};
116
+ try harness.start(app_state.app());
117
+
118
+ try expectEveryControlRefused(&harness.runtime, 99, error.WindowNotFound);
119
+
120
+ const fixed = try harness.runtime.createWindow(.{ .label = "fixed", .resizable = false });
121
+ try std.testing.expectError(error.WindowNotResizable, harness.runtime.maximizeWindow(fixed.id));
122
+
123
+ const tools = try harness.runtime.createWindow(.{ .label = "tools" });
124
+ try harness.runtime.hideWindow(tools.id);
125
+ try std.testing.expectError(error.WindowHidden, harness.runtime.maximizeWindow(tools.id));
126
+
127
+ // A closed window keeps its runtime slot; resolving its id is not
128
+ // liveness, and no verb reaches the host for it.
129
+ try harness.runtime.closeWindow(tools.id);
130
+ try expectEveryControlRefused(&harness.runtime, tools.id, error.WindowNotFound);
131
+ try std.testing.expect(harness.null_platform.takeQueuedCloseRequest() == null);
132
+ }
133
+
134
+ test "requestCloseWindow closes a quit-policy window the way the user's close button does" {
135
+ const harness = try TestHarness().create(std.testing.allocator, .{});
136
+ defer harness.destroy(std.testing.allocator);
137
+ var app_state: ControlsApp = .{};
138
+ try harness.start(app_state.app());
139
+
140
+ const tools = try harness.runtime.createWindow(.{ .label = "tools" });
141
+ try harness.runtime.requestCloseWindow(tools.id);
142
+ // Only a request: the window is untouched until the host handles it.
143
+ try std.testing.expect(windowInfo(&harness.runtime, tools.id).?.open);
144
+ try std.testing.expectEqual(@as(u32, 0), app_state.window_closed_count);
145
+
146
+ try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.takeQueuedCloseRequest().?);
147
+ try std.testing.expect(!windowInfo(&harness.runtime, tools.id).?.open);
148
+ try std.testing.expectEqual(@as(u32, 1), app_state.window_closed_count);
149
+ try std.testing.expect(harness.null_platform.takeQueuedCloseRequest() == null);
150
+
151
+ // closeWindow keeps its meaning: the app's own close, which never
152
+ // reports back as a user close.
153
+ const other = try harness.runtime.createWindow(.{ .label = "other" });
154
+ try harness.runtime.closeWindow(other.id);
155
+ try std.testing.expectEqual(@as(u32, 1), app_state.window_closed_count);
156
+ }
157
+
158
+ test "requestCloseWindow hides a hide-policy window and keeps it alive" {
159
+ const harness = try TestHarness().create(std.testing.allocator, .{});
160
+ defer harness.destroy(std.testing.allocator);
161
+ var app_state: ControlsApp = .{};
162
+ try harness.start(app_state.app());
163
+
164
+ const palette = try harness.runtime.createWindow(.{ .label = "palette", .close_policy = .hide });
165
+ _ = try harness.runtime.createView(.{ .window_id = palette.id, .label = "action", .kind = .button, .frame = .init(10, 10, 80, 30) });
166
+ const view_count = harness.runtime.view_count;
167
+
168
+ try harness.runtime.requestCloseWindow(palette.id);
169
+ try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.takeQueuedCloseRequest().?);
170
+ const info = windowInfo(&harness.runtime, palette.id).?;
171
+ try std.testing.expect(info.open);
172
+ try std.testing.expect(info.hidden);
173
+ try std.testing.expectEqual(view_count, harness.runtime.view_count);
174
+ try std.testing.expectEqual(@as(u32, 0), app_state.window_closed_count);
175
+
176
+ // The hidden window comes back with its views, as after a user close.
177
+ try harness.runtime.showWindow(palette.id);
178
+ try std.testing.expect(!windowInfo(&harness.runtime, palette.id).?.hidden);
179
+ }
180
+
181
+ test "a close request for a window the app closed first is dropped" {
182
+ const harness = try TestHarness().create(std.testing.allocator, .{});
183
+ defer harness.destroy(std.testing.allocator);
184
+ var app_state: ControlsApp = .{};
185
+ try harness.start(app_state.app());
186
+
187
+ const tools = try harness.runtime.createWindow(.{ .label = "tools" });
188
+ try harness.runtime.requestCloseWindow(tools.id);
189
+ try harness.runtime.closeWindow(tools.id);
190
+ try std.testing.expect(harness.null_platform.takeQueuedCloseRequest() == null);
191
+ try std.testing.expectEqual(@as(u32, 0), app_state.window_closed_count);
192
+ }
@@ -220,6 +220,9 @@ pub fn RuntimeWindowStorage(comptime Runtime: type) type {
220
220
  const index = existing_index orelse try Self.reserveWindow(self, state.id, state.label, state.title, null, true, .allow_source_less);
221
221
  self.windows[index].info.frame = state.frame;
222
222
  self.windows[index].info.scale_factor = state.scale_factor;
223
+ const was_maximized = self.windows[index].info.maximized;
224
+ self.windows[index].info.maximized = state.maximized;
225
+ if (was_maximized != state.maximized) @import("window_controls.zig").announceMaximized(self, state.id, state.maximized);
223
226
  if (self.window_lifecycle_policy == null) {
224
227
  self.windows[index].info.open = state.open;
225
228
  self.windows[index].info.hidden = state.hidden;
@@ -1493,9 +1493,12 @@ fn buildZig(allocator: std.mem.Allocator, names: TemplateNames, framework_path:
1493
1493
  \\ // release-shaped exes are GUI-subsystem so the app never flashes a
1494
1494
  \\ // console behind its window; Debug keeps the console for dev logs.
1495
1495
  \\ // Redirected logging still works on GUI exes - only console
1496
- \\ // AUTO-allocation is subsystem-gated.
1496
+ \\ // AUTO-allocation is subsystem-gated. MSVC's CRT enters a GUI exe
1497
+ \\ // through WinMain, which Zig apps do not export, so MSVC targets
1498
+ \\ // keep the GUI subsystem but enter through mainCRTStartup.
1497
1499
  \\ if (target.result.os.tag == .windows and optimize != .Debug) {
1498
1500
  \\ exe.subsystem = .windows;
1501
+ \\ if (target.result.abi == .msvc) exe.entry = .{ .symbol_name = "mainCRTStartup" };
1499
1502
  \\ }
1500
1503
  \\ linkPlatform(b, target, app_mod, exe, selected_platform, web_engine, web_layer, native_sdk_path, cef_dir, cef_auto_install);
1501
1504
  \\ b.installArtifact(exe);
@@ -1554,6 +1557,7 @@ fn buildZig(allocator: std.mem.Allocator, names: TemplateNames, framework_path:
1554
1557
  \\ // exe's own mode: release-shaped Windows exes are GUI-subsystem.
1555
1558
  \\ if (target.result.os.tag == .windows and package_optimize != .Debug) {
1556
1559
  \\ built.subsystem = .windows;
1560
+ \\ if (target.result.abi == .msvc) built.entry = .{ .symbol_name = "mainCRTStartup" };
1557
1561
  \\ }
1558
1562
  \\ linkPlatform(b, target, package_app_mod, built, selected_platform, web_engine, web_layer, native_sdk_path, cef_dir, cef_auto_install);
1559
1563
  \\ break :pkg built;
@@ -4284,6 +4288,9 @@ test "writeDefaultApp emits Vite project files" {
4284
4288
  // the SDK build graph) so packaged scaffold apps never flash a console.
4285
4289
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "if (target.result.os.tag == .windows and optimize != .Debug) {") != null);
4286
4290
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "exe.subsystem = .windows;") != null);
4291
+ // MSVC's CRT would enter that GUI exe through WinMain, which Zig apps
4292
+ // do not export; MSVC targets enter through mainCRTStartup instead.
4293
+ try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "if (target.result.abi == .msvc) exe.entry = .{ .symbol_name = \"mainCRTStartup\" };") != null);
4287
4294
  // The package step wraps its own release-shaped exe: Debug stays the
4288
4295
  // run/dev default, but `zig build package` must never ship a Debug
4289
4296
  // (console-subsystem) binary unless the user pinned the mode
@@ -4296,6 +4303,7 @@ test "writeDefaultApp emits Vite project files" {
4296
4303
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "const package_exe = if (package_optimize == optimize) exe else pkg: {") != null);
4297
4304
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "if (target.result.os.tag == .windows and package_optimize != .Debug) {") != null);
4298
4305
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "built.subsystem = .windows;") != null);
4306
+ try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "if (target.result.abi == .msvc) built.entry = .{ .symbol_name = \"mainCRTStartup\" };") != null);
4299
4307
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package.addFileArg(package_exe.getEmittedBin());") != null);
4300
4308
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package.step.dependOn(&package_exe.step);") != null);
4301
4309
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package_optimize_name,") != null);
@@ -6,7 +6,7 @@ const tooling = @import("tooling");
6
6
  const automation_protocol = @import("automation_protocol");
7
7
  const cli_build_info = @import("cli_build_info");
8
8
 
9
- const version = "0.11.0-fork.2";
9
+ const version = "0.11.0-fork.3";
10
10
 
11
11
  pub fn main(init: std.process.Init) !void {
12
12
  const allocator = init.arena.allocator();