@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 +44 -23
- package/native-sdk.d.ts +2 -0
- package/package.json +9 -9
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/src/platform/null_platform.zig +66 -0
- package/src/platform/types.zig +37 -0
- package/src/platform/windows/root.zig +137 -0
- package/src/platform/windows/scriptc_mingw_compat.c +248 -0
- package/src/platform/windows/webview2_host.cpp +129 -0
- package/src/runtime/bridge_responses.zig +2 -0
- package/src/runtime/core.zig +5 -0
- package/src/runtime/tests.zig +1 -0
- package/src/runtime/window_controls.zig +73 -0
- package/src/runtime/window_controls_tests.zig +192 -0
- package/src/runtime/window_storage.zig +3 -0
- package/src/tooling/templates.zig +9 -1
- package/tools/native-sdk/main.zig +1 -1
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.
|
|
2100
|
-
//
|
|
2101
|
-
//
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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 =
|
|
2120
|
+
.root_module = test_app_mod,
|
|
2122
2121
|
.use_llvm = useLlvmWorkaround(target),
|
|
2123
2122
|
});
|
|
2124
|
-
test_step.dependOn(&
|
|
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.
|
|
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.
|
|
45
|
-
"@zogien/native-sdk-darwin-x64": "0.11.0-fork.
|
|
46
|
-
"@zogien/native-sdk-linux-arm64-gnu": "0.11.0-fork.
|
|
47
|
-
"@zogien/native-sdk-linux-arm64-musl": "0.11.0-fork.
|
|
48
|
-
"@zogien/native-sdk-linux-x64-gnu": "0.11.0-fork.
|
|
49
|
-
"@zogien/native-sdk-linux-x64-musl": "0.11.0-fork.
|
|
50
|
-
"@zogien/native-sdk-win32-arm64": "0.11.0-fork.
|
|
51
|
-
"@zogien/native-sdk-win32-x64": "0.11.0-fork.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
package/src/platform/types.zig
CHANGED
|
@@ -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
|
|
package/src/runtime/core.zig
CHANGED
|
@@ -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;
|
package/src/runtime/tests.zig
CHANGED
|
@@ -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.
|
|
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();
|