From 6ade9a62ab2075bb661fe3c6b46d1f9f9e6adf50 Mon Sep 17 00:00:00 2001 From: WhiteHades <44260523+WhiteHades@users.noreply.github.com> Date: Fri, 10 Jul 2026 06:10:07 +0600 Subject: [PATCH] feat: add native-only desktop host --- build.zig | 86 +++++++++++ build/app.zig | 125 ++++++++++------ build_app_tests.zig | 5 + changelog.d/native-desktop-host.md | 1 + docs/src/app/app-zon/page.mdx | 19 ++- examples/native-only/README.md | 9 ++ examples/native-only/app.zon | 24 +++ examples/native-only/assets/native-only.txt | 1 + examples/native-only/src/app.native | 5 + examples/native-only/src/main.zig | 68 +++++++++ src/app_runner/root.zig | 67 +++++++-- src/platform/linux/gtk_host.c | 23 +++ src/platform/linux/root.zig | 64 ++++++-- src/platform/macos/appkit_host.m | 140 +++++++++++++++--- src/platform/macos/root.zig | 65 ++++++-- src/platform/null_platform.zig | 53 +++++-- src/platform/null_platform_tests.zig | 23 +++ src/platform/windows/root.zig | 64 ++++++-- src/platform/windows/webview2_host.cpp | 4 +- src/runtime/automation_widget_dispatch.zig | 38 ++--- src/runtime/core.zig | 2 + src/runtime/flow.zig | 79 +++++++--- src/runtime/platform_config_tests.zig | 38 +++++ src/runtime/root.zig | 1 + src/runtime/session_replay.zig | 25 +++- src/tooling/manifest.zig | 136 +++++++++++++++++ src/tooling/package.zig | 47 +++++- src/tooling/raw_manifest.zig | 1 + src/tooling/templates.zig | 25 ++-- .../native-only-browser-headers/WebView2.h | 1 + tools/native-sdk/main.zig | 26 +++- 31 files changed, 1056 insertions(+), 209 deletions(-) create mode 100644 build_app_tests.zig create mode 100644 changelog.d/native-desktop-host.md create mode 100644 examples/native-only/README.md create mode 100644 examples/native-only/app.zon create mode 100644 examples/native-only/assets/native-only.txt create mode 100644 examples/native-only/src/app.native create mode 100644 examples/native-only/src/main.zig create mode 100644 tests/fixtures/native-only-browser-headers/WebView2.h diff --git a/build.zig b/build.zig index 005fb8691..b3c7c4abd 100644 --- a/build.zig +++ b/build.zig @@ -140,6 +140,7 @@ pub fn build(b: *std.Build) void { desktop_mod.addImport("canvas", canvas_mod); const desktop_tests = testArtifact(b, desktop_mod); const desktop_test_shards = desktopTestShardArtifacts(b, desktop_mod); + const app_build_tests = testArtifact(b, module(b, host_target, optimize, "build_app_tests.zig")); // The embeddable static library's root module carries only the C ABI // exports (fixed WebView shell host); user-app canvas libraries are @@ -402,6 +403,7 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&b.addRunArtifact(eject_components_tests).step); test_step.dependOn(&b.addRunArtifact(markup_lsp_tests).step); test_step.dependOn(&b.addRunArtifact(automation_cli_tests).step); + test_step.dependOn(&b.addRunArtifact(app_build_tests).step); addFileContainsCheckStep(b, file_contains_checker, test_step, "test-package-types", "Verify package TypeScript platform feature names", &.{ .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "NativeSdkCommandInfo" }, .{ .path = "packages/native-sdk/native-sdk.d.ts", .pattern = "list(): Promise" }, @@ -723,6 +725,7 @@ pub fn build(b: *std.Build) void { addTestStep(b, "test-automation-protocol", "Run automation protocol tests", automation_protocol_tests); addTestStep(b, "test-automation-cli", "Run native automate CLI tests", automation_cli_tests); addTestStep(b, "test-tooling", "Run Native SDK tooling tests", tooling_tests); + addTestStep(b, "test-app-build-config", "Run app build manifest parsing tests", app_build_tests); addTestStep(b, "test-eject-components", "Run ejected-component widget-identity tests", eject_components_tests); const run_hello = b.addSystemCommand(&.{ "zig", "build", "run", b.fmt("-Dplatform={s}", .{platform_arg}), b.fmt("-Dtrace={s}", .{@tagName(trace_option)}) }); @@ -765,6 +768,7 @@ pub fn build(b: *std.Build) void { const native_examples_step = b.step("test-examples-native", "Run native-first example tests"); addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-command-app", "Run command app example tests", "examples/command-app", .owned); + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-native-only", "Run native-only host example tests", "examples/native-only", .managed); addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-native-shell", "Run native shell example tests", "examples/native-shell", .owned); addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-native-panels", "Run native panels example tests", "examples/native-panels", .owned); addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-gpu-surface", "Run GPU surface example tests", "examples/gpu-surface", .managed); @@ -788,6 +792,88 @@ pub fn build(b: *std.Build) void { .{ .path = "examples/capabilities/src/main.zig", .pattern = "native-sdk:drop:files" }, }); + addFileContainsCheckStep(b, file_contains_checker, native_examples_step, "test-native-only-host-source-guards", "Verify native-only browser source guards", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "#if !defined(NATIVE_SDK_NATIVE_ONLY)\n#import " }, + .{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "#if !defined(NATIVE_SDK_NATIVE_ONLY) && __has_include()" }, + .{ .path = "build/app.zig", .pattern = "-DNATIVE_SDK_NATIVE_ONLY=1" }, + }); + const native_only_source_step = b.step("test-native-only-host-sources", "Verify native-only macOS and Windows browser source exclusion"); + if (host_target.result.os.tag == .linux or host_target.result.os.tag == .macos) { + const check_native_only_macos_source = b.addSystemCommand(&.{ "sh", "-c", "set -eu; active=$(mktemp); trap 'rm -f \"$active\"' EXIT; awk '!/^#(import|include)/' src/platform/macos/appkit_host.m | zig cc -E -P -x objective-c -DNATIVE_SDK_NATIVE_ONLY=1 - > \"$active\"; if grep -E -n 'WebKit|WK[A-Z]|WebView2|ICoreWebView2|libcef|CEF' \"$active\"; then echo 'native-only AppKit source retains browser code' >&2; exit 1; fi" }); + const check_native_only_windows_source = b.addSystemCommand(&.{ "zig", "c++", "-target", "x86_64-windows-gnu", "-std=c++17", "-DNATIVE_SDK_NATIVE_ONLY=1", "-Itests/fixtures/native-only-browser-headers", "-c" }); + check_native_only_windows_source.addFileArg(b.path("src/platform/windows/webview2_host.cpp")); + check_native_only_windows_source.addArg("-o"); + _ = check_native_only_windows_source.addOutputFileArg("native-only-webview2-host.obj"); + check_native_only_windows_source.step.dependOn(&check_native_only_macos_source.step); + native_only_source_step.dependOn(&check_native_only_windows_source.step); + } else if (host_target.result.os.tag == .windows) { + const check_native_only_windows_source = b.addSystemCommand(&.{ "zig", "c++", "-target", "x86_64-windows-gnu", "-std=c++17", "-DNATIVE_SDK_NATIVE_ONLY=1", "-Itests/fixtures/native-only-browser-headers", "-c" }); + check_native_only_windows_source.addFileArg(b.path("src/platform/windows/webview2_host.cpp")); + check_native_only_windows_source.addArg("-o"); + _ = check_native_only_windows_source.addOutputFileArg("native-only-webview2-host.obj"); + native_only_source_step.dependOn(&check_native_only_windows_source.step); + } + + const native_only_link_step = b.step("test-native-only-host-link", "Build the native-only host and verify it has no browser dependencies or bridge dispatcher"); + const native_only_package_step = b.step("test-native-only-host-package", "Package the native-only host and audit the staged artifact"); + const native_only_windows_step = b.step("test-native-only-host-windows", "Cross-compile the native-only Windows host and verify it has no browser imports"); + + if (host_target.result.os.tag == .linux) { + const build_native_only = managedExampleRun(b, host_cli_exe, &.{ "build", "-Dplatform=linux" }); + build_native_only.setCwd(b.path("examples/native-only")); + build_native_only.has_side_effects = true; + build_native_only.step.dependOn(native_only_source_step); + const check_native_only_link = b.addSystemCommand(&.{ "sh", "-c", "set -eu; binary=zig-out/bin/native-only; if readelf -d \"$binary\" | grep -E -i 'NEEDED.*(webkit|webview|libcef|cef\\.so)'; then echo 'native-only Linux binary imports a browser library' >&2; exit 1; fi; if strings \"$binary\" | grep -E -i 'WebKitWebProcess|WebView2Loader|libcef|cef\\.so|native-sdk\\.(command|window|view|webview|platform|dialog|os|clipboard|credentials)\\.'; then echo 'native-only Linux binary contains a browser or JavaScript bridge dispatcher reference' >&2; exit 1; fi; if nm -a \"$binary\" 2>/dev/null | grep -E 'dispatchWebViewBridgeCommand|handleBuiltinBridgeMessage'; then echo 'native-only Linux binary retains bridge dispatcher symbols' >&2; exit 1; fi" }); + check_native_only_link.setCwd(b.path("examples/native-only")); + check_native_only_link.step.dependOn(&build_native_only.step); + native_only_link_step.dependOn(&check_native_only_link.step); + + const clean_native_only_package = b.addSystemCommand(&.{ "sh", "-c", "rm -rf zig-out/package/native-only-linux" }); + clean_native_only_package.setCwd(b.path("examples/native-only")); + const package_native_only = managedExampleRun(b, host_cli_exe, &.{ "package", "--target", "linux", "--output", "zig-out/package/native-only-linux", "--binary", "zig-out/bin/native-only", "--assets", "assets" }); + package_native_only.setCwd(b.path("examples/native-only")); + package_native_only.has_side_effects = true; + package_native_only.step.dependOn(&clean_native_only_package.step); + package_native_only.step.dependOn(&check_native_only_link.step); + const check_native_only_package = b.addSystemCommand(&.{ "sh", "-c", "set -eu; package=zig-out/package/native-only-linux; binary=$package/bin/native-only; test -f \"$binary\"; test -f \"$package/resources/native-only.txt\"; test ! -e \"$package/Frameworks\"; if grep -E -i 'frontend|chromium|cef' \"$package/package-manifest.zon\"; then echo 'native-only package report retains frontend or CEF staging' >&2; exit 1; fi; if readelf -d \"$binary\" | grep -E -i 'NEEDED.*(webkit|webview|libcef|cef\\.so)'; then echo 'packaged native-only Linux binary imports a browser library' >&2; exit 1; fi; if strings \"$binary\" | grep -E -i 'WebKitWebProcess|WebView2Loader|libcef|cef\\.so|native-sdk\\.(command|window|view|webview|platform|dialog|os|clipboard|credentials)\\.'; then echo 'packaged native-only Linux binary retains browser or bridge dispatcher references' >&2; exit 1; fi" }); + check_native_only_package.setCwd(b.path("examples/native-only")); + check_native_only_package.step.dependOn(&package_native_only.step); + native_only_package_step.dependOn(&check_native_only_package.step); + native_examples_step.dependOn(&check_native_only_package.step); + + const build_native_only_windows = managedExampleRun(b, host_cli_exe, &.{ "build", "-Dplatform=windows", "-Dtarget=x86_64-windows-gnu" }); + build_native_only_windows.setCwd(b.path("examples/native-only")); + build_native_only_windows.has_side_effects = true; + build_native_only_windows.step.dependOn(&check_native_only_package.step); + const check_native_only_windows_imports = b.addSystemCommand(&.{ "sh", "-c", "set -eu; binary=zig-out/bin/native-only.exe; if objdump -p \"$binary\" | grep -E -i 'DLL Name:.*(WebView2|libcef|cef\\.dll|WebKit)'; then echo 'native-only Windows binary imports a browser library' >&2; exit 1; fi; if strings \"$binary\" | grep -E -i 'WebView2Loader|libcef|cef\\.dll|WebKitWebProcess|native-sdk\\.(command|window|view|webview|platform|dialog|os|clipboard|credentials)\\.'; then echo 'native-only Windows binary contains a browser or JavaScript bridge dispatcher reference' >&2; exit 1; fi; if objdump -t \"$binary\" | grep -E 'dispatchWebViewBridgeCommand|handleBuiltinBridgeMessage'; then echo 'native-only Windows binary retains bridge dispatcher symbols' >&2; exit 1; fi" }); + check_native_only_windows_imports.setCwd(b.path("examples/native-only")); + check_native_only_windows_imports.step.dependOn(&build_native_only_windows.step); + native_only_windows_step.dependOn(&check_native_only_windows_imports.step); + native_examples_step.dependOn(&check_native_only_windows_imports.step); + } else if (host_target.result.os.tag == .macos) { + const build_native_only = managedExampleRun(b, host_cli_exe, &.{ "build", "-Dplatform=macos" }); + build_native_only.setCwd(b.path("examples/native-only")); + build_native_only.has_side_effects = true; + build_native_only.step.dependOn(native_only_source_step); + const check_native_only_link = b.addSystemCommand(&.{ "sh", "-c", "set -eu; binary=zig-out/bin/native-only; if otool -L \"$binary\" | grep -E -i 'WebKit|WebView2|libcef|Chromium Embedded Framework'; then echo 'native-only macOS binary imports a browser framework' >&2; exit 1; fi; if strings \"$binary\" | grep -E -i 'WebKitWebProcess|WebView2Loader|libcef|native-sdk\\.(command|window|view|webview|platform|dialog|os|clipboard|credentials)\\.'; then echo 'native-only macOS binary contains a browser or JavaScript bridge dispatcher reference' >&2; exit 1; fi; if nm -a \"$binary\" 2>/dev/null | grep -E 'dispatchWebViewBridgeCommand|handleBuiltinBridgeMessage'; then echo 'native-only macOS binary retains bridge dispatcher symbols' >&2; exit 1; fi" }); + check_native_only_link.setCwd(b.path("examples/native-only")); + check_native_only_link.step.dependOn(&build_native_only.step); + native_only_link_step.dependOn(&check_native_only_link.step); + + const clean_native_only_package = b.addSystemCommand(&.{ "sh", "-c", "rm -rf zig-out/package/native-only.app" }); + clean_native_only_package.setCwd(b.path("examples/native-only")); + const package_native_only = managedExampleRun(b, host_cli_exe, &.{ "package", "--target", "macos", "--output", "zig-out/package/native-only.app", "--binary", "zig-out/bin/native-only", "--assets", "assets" }); + package_native_only.setCwd(b.path("examples/native-only")); + package_native_only.has_side_effects = true; + package_native_only.step.dependOn(&clean_native_only_package.step); + package_native_only.step.dependOn(&check_native_only_link.step); + const check_native_only_package = b.addSystemCommand(&.{ "sh", "-c", "set -eu; package=zig-out/package/native-only.app; binary=$package/Contents/MacOS/native-only; test -f \"$binary\"; test -f \"$package/Contents/Resources/assets/native-only.txt\"; test ! -e \"$package/Contents/Frameworks\"; if grep -E -i 'frontend|chromium|cef' \"$package/Contents/Resources/package-manifest.zon\"; then echo 'native-only package report retains frontend or CEF staging' >&2; exit 1; fi; if otool -L \"$binary\" | grep -E -i 'WebKit|WebView2|libcef|Chromium Embedded Framework'; then echo 'packaged native-only macOS binary imports a browser framework' >&2; exit 1; fi; if strings \"$binary\" | grep -E -i 'WebKitWebProcess|WebView2Loader|libcef|native-sdk\\.(command|window|view|webview|platform|dialog|os|clipboard|credentials)\\.'; then echo 'packaged native-only macOS binary retains browser or bridge dispatcher references' >&2; exit 1; fi" }); + check_native_only_package.setCwd(b.path("examples/native-only")); + check_native_only_package.step.dependOn(&package_native_only.step); + native_only_package_step.dependOn(&check_native_only_package.step); + native_examples_step.dependOn(&check_native_only_package.step); + } + const mobile_examples_step = b.step("test-examples-mobile", "Verify mobile example project layouts"); addLayoutCheckStep(b, mobile_examples_step, "test-example-ios-layout", "Verify iOS example layout", &.{ "examples/ios/README.md", diff --git a/build/app.zig b/build/app.zig index ca3c806c6..cd0b0ba30 100644 --- a/build/app.zig +++ b/build/app.zig @@ -4,6 +4,7 @@ //! come from the native-sdk dependency. const std = @import("std"); +const RawManifest = @import("../src/tooling/raw_manifest.zig").RawManifest; const PlatformOption = enum { auto, @@ -25,6 +26,11 @@ const WebEngineOption = enum { chromium, }; +const HostOption = enum { + webview, + native, +}; + pub const AppOptions = struct { name: []const u8, /// App entry point; defaults to src/main.zig (relative to `app_root`). @@ -218,7 +224,11 @@ pub fn addAppArtifacts(b: *std.Build, dep: *std.Build.Dependency, app_options: A @panic("-Dplatform=windows requires a Windows target"); } const app_web_engine = appWebEngineConfig(b, app_options.app_root); - const web_engine = web_engine_override orelse app_web_engine.web_engine; + const host = app_web_engine.host; + if (host == .native and (web_engine_override == .chromium or app_web_engine.web_engine == .chromium or cef_dir_override != null or cef_auto_install_override != null or app_web_engine.cef_auto_install or !std.mem.eql(u8, app_web_engine.cef_dir, "third_party/cef/macos"))) { + @panic("app.zon host = \"native\" cannot select Chromium or configure CEF"); + } + const web_engine = if (host == .native) .system else web_engine_override orelse app_web_engine.web_engine; const cef_dir = cef_dir_override orelse defaultCefDir(selected_platform, app_web_engine.cef_dir); const cef_auto_install = cef_auto_install_override orelse app_web_engine.cef_auto_install; if (web_engine == .chromium and selected_platform != .macos) { @@ -235,6 +245,7 @@ pub fn addAppArtifacts(b: *std.Build, dep: *std.Build.Dependency, app_options: A }); options.addOption([]const u8, "trace", @tagName(trace_option)); options.addOption([]const u8, "web_engine", @tagName(web_engine)); + options.addOption([]const u8, "host", @tagName(host)); options.addOption(bool, "debug_overlay", debug_overlay); options.addOption(bool, "automation", automation_enabled); options.addOption(bool, "js_bridge", js_bridge_enabled); @@ -245,7 +256,7 @@ pub fn addAppArtifacts(b: *std.Build, dep: *std.Build.Dependency, app_options: A .name = app_options.name, .root_module = app_mod, }); - linkPlatform(b, dep, target, app_mod, exe, selected_platform, web_engine, cef_dir, cef_auto_install); + linkPlatform(b, dep, target, app_mod, exe, selected_platform, host, web_engine, cef_dir, cef_auto_install); const install = b.addInstallArtifact(exe, .{}); b.getInstallStep().dependOn(&install.step); @@ -465,9 +476,13 @@ fn externalModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.R // Reproduced with a 10-line `zig cc` program against both the 14.5 and // 26.0 SDKs. Release builds never hit it (no UBSan), which is why only // Debug-built examples (standardOptimizeOption default) crashed. -fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, app_mod: *std.Build.Module, exe: *std.Build.Step.Compile, platform: PlatformOption, web_engine: WebEngineOption, cef_dir: []const u8, cef_auto_install: bool) void { +fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, app_mod: *std.Build.Module, exe: *std.Build.Step.Compile, platform: PlatformOption, host: HostOption, web_engine: WebEngineOption, cef_dir: []const u8, cef_auto_install: bool) void { if (platform == .macos) { - switch (web_engine) { + if (host == .native) { + const sdk_include = if (b.sysroot) |sysroot| b.fmt("-I{s}/usr/include", .{sysroot}) else ""; + const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-DNATIVE_SDK_NATIVE_ONLY=1", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-DNATIVE_SDK_NATIVE_ONLY=1", "-mmacosx-version-min=11.0" }; + app_mod.addCSourceFile(.{ .file = dep.path("src/platform/macos/appkit_host.m"), .flags = flags }); + } else switch (web_engine) { .system => { const sdk_include = if (b.sysroot) |sysroot| b.fmt("-I{s}/usr/include", .{sysroot}) else ""; const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0" }; @@ -515,7 +530,11 @@ fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Res app_mod.linkSystemLibrary("c", .{}); if (web_engine == .chromium) app_mod.linkSystemLibrary("c++", .{}); } else if (platform == .linux) { - switch (web_engine) { + if (host == .native) { + app_mod.addCSourceFile(.{ .file = dep.path("src/platform/linux/gtk_host.c"), .flags = &.{"-DNATIVE_SDK_NATIVE_ONLY=1"} }); + app_mod.linkSystemLibrary("gtk4", .{}); + app_mod.linkSystemLibrary("dl", .{}); + } else switch (web_engine) { .system => { app_mod.addCSourceFile(.{ .file = dep.path("src/platform/linux/gtk_host.c"), .flags = &.{} }); app_mod.linkSystemLibrary("gtk4", .{}); @@ -545,7 +564,9 @@ fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.Res // manifest the loader binds the system-default v5 assembly, which // renders classic-styled controls and lacks the v6-only exports. exe.win32_manifest = dep.path("assets/native-sdk.manifest"); - switch (web_engine) { + if (host == .native) { + app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{ "-std=c++17", "-DNATIVE_SDK_NATIVE_ONLY=1" } }); + } else switch (web_engine) { .system => app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{"-std=c++17"} }), .chromium => { const cef_check = addCefCheck(b, target, cef_dir); @@ -638,6 +659,7 @@ fn addCefCheck(b: *std.Build, target: std.Build.ResolvedTarget, cef_dir: []const } const AppWebEngineConfig = struct { + host: HostOption = .webview, web_engine: WebEngineOption = .system, cef_dir: []const u8 = "third_party/cef/macos", cef_auto_install: bool = false, @@ -661,16 +683,34 @@ fn appPath(b: *std.Build, app_root: []const u8, sub_path: []const u8) []const u8 } fn appWebEngineConfig(b: *std.Build, app_root: []const u8) AppWebEngineConfig { - const source = b.build_root.handle.readFileAlloc(b.graph.io, appPath(b, app_root, "app.zon"), b.allocator, .limited(1024 * 1024)) catch return .{}; - var config: AppWebEngineConfig = .{}; - if (stringField(source, ".web_engine")) |value| { - config.web_engine = parseWebEngine(value) orelse .system; - } - if (objectSection(source, ".cef")) |cef| { - if (stringField(cef, ".dir")) |value| config.cef_dir = value; - if (boolField(cef, ".auto_install")) |value| config.cef_auto_install = value; - } - return config; + const manifest_path = appPath(b, app_root, "app.zon"); + const source = b.build_root.handle.readFileAlloc(b.graph.io, manifest_path, b.allocator, .limited(1024 * 1024)) catch |err| { + std.debug.panic("failed to read {s}: {s}", .{ manifest_path, @errorName(err) }); + }; + return parseAppWebEngineConfig(b.allocator, source) catch |err| { + std.debug.panic("invalid {s}: {s}", .{ manifest_path, @errorName(err) }); + }; +} + +fn parseAppWebEngineConfig(allocator: std.mem.Allocator, source: []const u8) !AppWebEngineConfig { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const scratch = arena.allocator(); + const source_z = try scratch.dupeZ(u8, source); + @setEvalBranchQuota(5000); + const raw = try std.zon.parse.fromSliceAlloc(RawManifest, scratch, source_z, null, .{}); + return .{ + .host = parseHost(raw.host) orelse return error.InvalidHost, + .web_engine = parseWebEngine(raw.web_engine) orelse return error.InvalidWebEngine, + .cef_dir = try allocator.dupe(u8, raw.cef.dir), + .cef_auto_install = raw.cef.auto_install, + }; +} + +fn parseHost(value: []const u8) ?HostOption { + if (std.mem.eql(u8, value, "webview")) return .webview; + if (std.mem.eql(u8, value, "native")) return .native; + return null; } fn parseWebEngine(value: []const u8) ?WebEngineOption { @@ -679,38 +719,27 @@ fn parseWebEngine(value: []const u8) ?WebEngineOption { return null; } -fn stringField(source: []const u8, field: []const u8) ?[]const u8 { - const field_index = std.mem.indexOf(u8, source, field) orelse return null; - const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null; - const start_quote = std.mem.indexOfScalarPos(u8, source, equals, '"') orelse return null; - const end_quote = std.mem.indexOfScalarPos(u8, source, start_quote + 1, '"') orelse return null; - return source[start_quote + 1 .. end_quote]; -} - -fn objectSection(source: []const u8, field: []const u8) ?[]const u8 { - const field_index = std.mem.indexOf(u8, source, field) orelse return null; - const open = std.mem.indexOfScalarPos(u8, source, field_index, '{') orelse return null; - var depth: usize = 0; - var index = open; - while (index < source.len) : (index += 1) { - switch (source[index]) { - '{' => depth += 1, - '}' => { - depth -= 1; - if (depth == 0) return source[open + 1 .. index]; - }, - else => {}, - } - } - return null; +test "app build host parser ignores comment-shadowed fields" { + const config = try parseAppWebEngineConfig(std.testing.allocator, + \\.{ + \\ .id = "dev.native_sdk.comment_host", + \\ .name = "comment-host", + \\ .version = "1.0.0", + \\ // .host = "webview", + \\ .host = "native", + \\} + ); + defer std.testing.allocator.free(config.cef_dir); + try std.testing.expectEqual(HostOption.native, config.host); } -fn boolField(source: []const u8, field: []const u8) ?bool { - const field_index = std.mem.indexOf(u8, source, field) orelse return null; - const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null; - var index = equals + 1; - while (index < source.len and std.ascii.isWhitespace(source[index])) : (index += 1) {} - if (std.mem.startsWith(u8, source[index..], "true")) return true; - if (std.mem.startsWith(u8, source[index..], "false")) return false; - return null; +test "app build host parser rejects invalid values" { + try std.testing.expectError(error.InvalidHost, parseAppWebEngineConfig(std.testing.allocator, + \\.{ + \\ .id = "dev.native_sdk.invalid_host", + \\ .name = "invalid-host", + \\ .version = "1.0.0", + \\ .host = "browser", + \\} + )); } diff --git a/build_app_tests.zig b/build_app_tests.zig new file mode 100644 index 000000000..d37a69d4d --- /dev/null +++ b/build_app_tests.zig @@ -0,0 +1,5 @@ +const std = @import("std"); + +test { + std.testing.refAllDecls(@import("build/app.zig")); +} diff --git a/changelog.d/native-desktop-host.md b/changelog.d/native-desktop-host.md new file mode 100644 index 000000000..844f4b96d --- /dev/null +++ b/changelog.d/native-desktop-host.md @@ -0,0 +1 @@ +feature: **Native-only desktop hosts**: canvas apps can set `host = "native"` to exclude browser backends, the JavaScript bridge dispatcher, frontend staging, and Chromium packaging while retaining native platform services. diff --git a/docs/src/app/app-zon/page.mdx b/docs/src/app/app-zon/page.mdx index fb6fdb9b1..31f82ec89 100644 --- a/docs/src/app/app-zon/page.mdx +++ b/docs/src/app/app-zon/page.mdx @@ -17,6 +17,7 @@ The manifest `native init` generates — identity, one shell window with a GPU s .platforms = .{"macos"}, .permissions = .{ "view", "command" }, .capabilities = .{ "native_views", "gpu_surfaces" }, + .host = "native", .shell = .{ .windows = .{ .{ @@ -32,14 +33,6 @@ The manifest `native init` generates — identity, one shell window with a GPU s }, }, }, - .security = .{ - .navigation = .{ - .allowed_origins = .{ "zero://app", "zero://inline" }, - .external_links = .{ .action = "deny" }, - }, - }, - .web_engine = "system", - .cef = .{ .dir = "third_party/cef/macos", .auto_install = false }, } ``` @@ -156,6 +149,10 @@ A fuller manifest for an app that also [embeds web content](/frontend) and decla capabilities Feature declarations (see Security) + + host + webview (default) or native; native hosts exclude browser SDKs and services + bridge Bridge command policies (see Bridge) @@ -211,6 +208,12 @@ A fuller manifest for an app that also [embeds web content](/frontend) and decla +## `host` + +`host = "webview"` is the default and preserves the browser-capable desktop host. Set `host = "native"` for a canvas/native-view app that must not compile or link WebKit, WebKitGTK, WebView2, or CEF, stage frontend content, or include the JavaScript command dispatcher. Native manifests reject webview, bridge, frontend, Chromium, and CEF declarations. Native hosts retain non-browser platform services and package ordinary app assets such as images, fonts, audio, and icons. + +A native host cannot declare `frontend`, the `webview` or `js_bridge` capabilities, bridge commands, WebView shell views, or Chromium. `native validate app.zon` reports the conflicting declaration instead of silently adding a browser layer. + ## `shell` The optional `shell` block declares native-first windows with explicit view trees. Existing `windows` entries remain the simple compatibility path; `shell.windows` is the richer contract for native chrome, native controls, WebViews, and GPU surfaces. diff --git a/examples/native-only/README.md b/examples/native-only/README.md new file mode 100644 index 000000000..6915fd5b2 --- /dev/null +++ b/examples/native-only/README.md @@ -0,0 +1,9 @@ +# Native-only host fixture + +This canvas-only app sets `host = "native"`. Its desktop build must omit WebKit, WebKitGTK, WebView2, CEF, frontend output, and JavaScript bridge services while preserving the native window and GPU-surface runtime. + +```sh +native validate app.zon +native test +native build -Dplatform=linux +``` diff --git a/examples/native-only/app.zon b/examples/native-only/app.zon new file mode 100644 index 000000000..df29f0c96 --- /dev/null +++ b/examples/native-only/app.zon @@ -0,0 +1,24 @@ +.{ + .id = "dev.native_sdk.native_only", + .name = "native-only", + .display_name = "Native Only", + .description = "A canvas-only desktop host fixture.", + .version = "0.1.0", + .host = "native", + .platforms = .{ "macos", "linux", "windows" }, + .capabilities = .{ "native_views", "gpu_surfaces" }, + .shell = .{ + .windows = .{ + .{ + .label = "main", + .title = "Native Only", + .width = 420, + .height = 260, + .restore_state = false, + .views = .{ + .{ .label = "main-canvas", .kind = "gpu_surface", .fill = true, .role = "Native-only canvas", .accessibility_label = "Native-only canvas", .gpu_backend = "software", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true }, + }, + }, + }, + }, +} diff --git a/examples/native-only/assets/native-only.txt b/examples/native-only/assets/native-only.txt new file mode 100644 index 000000000..155f7262b --- /dev/null +++ b/examples/native-only/assets/native-only.txt @@ -0,0 +1 @@ +native-only packaged asset diff --git a/examples/native-only/src/app.native b/examples/native-only/src/app.native new file mode 100644 index 000000000..fb0778842 --- /dev/null +++ b/examples/native-only/src/app.native @@ -0,0 +1,5 @@ + + Native-only host + No browser engine is compiled or linked. + + diff --git a/examples/native-only/src/main.zig b/examples/native-only/src/main.zig new file mode 100644 index 000000000..02a9cf9ee --- /dev/null +++ b/examples/native-only/src/main.zig @@ -0,0 +1,68 @@ +const std = @import("std"); +const runner = @import("runner"); +const native_sdk = @import("native_sdk"); + +pub const panic = std.debug.FullPanic(native_sdk.debug.capturePanic); + +const canvas = native_sdk.canvas; +const geometry = native_sdk.geometry; +const canvas_label = "main-canvas"; +const window_width: f32 = 420; +const window_height: f32 = 260; + +const shell_views = [_]native_sdk.ShellView{ + .{ .label = canvas_label, .kind = .gpu_surface, .fill = true, .role = "Native-only canvas", .accessibility_label = "Native-only canvas", .gpu_backend = .software, .gpu_pixel_format = .bgra8_unorm, .gpu_present_mode = .timer, .gpu_alpha_mode = .@"opaque", .gpu_color_space = .srgb, .gpu_vsync = true }, +}; +const shell_windows = [_]native_sdk.ShellWindow{.{ + .label = "main", + .title = "Native Only", + .width = window_width, + .height = window_height, + .restore_state = false, + .views = &shell_views, +}}; +const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows }; + +pub const Msg = union(enum) { toggle }; + +pub const Model = struct { + active: bool = false, + + pub fn status(self: *const Model, arena: std.mem.Allocator) []const u8 { + _ = arena; + return if (self.active) "Active" else "Ready"; + } +}; + +pub fn update(model: *Model, msg: Msg) void { + switch (msg) { + .toggle => model.active = !model.active, + } +} + +const NativeOnlyApp = native_sdk.UiApp(Model, Msg); + +pub fn main(init: std.process.Init) !void { + const app_state = try NativeOnlyApp.create(std.heap.page_allocator, .{ + .name = "native-only", + .scene = shell_scene, + .canvas_label = canvas_label, + .update = update, + .markup = .{ .source = @embedFile("app.native"), .watch_path = "src/app.native", .io = init.io }, + }); + defer app_state.destroy(); + + try runner.runWithOptions(app_state.app(), .{ + .app_name = "native-only", + .window_title = "Native Only", + .bundle_id = "dev.native_sdk.native_only", + .default_frame = geometry.RectF.init(0, 0, window_width, window_height), + .restore_state = false, + }, init); +} + +test "native-only fixture updates without web content" { + var model: Model = .{}; + update(&model, .toggle); + try std.testing.expect(model.active); +} diff --git a/src/app_runner/root.zig b/src/app_runner/root.zig index dca40f9a5..9ba2f3f77 100644 --- a/src/app_runner/root.zig +++ b/src/app_runner/root.zig @@ -320,8 +320,9 @@ fn shortcutModifiers(comptime shortcut: anytype) native_sdk.ShortcutModifiers { } pub fn runWithOptions(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void { + comptime validateNativeHostManifest(); if (build_options.debug_overlay) { - std.debug.print("debug-overlay=true backend={s} web-engine={s} trace={s}\n", .{ build_options.platform, build_options.web_engine, build_options.trace }); + std.debug.print("debug-overlay=true backend={s} host={s} web-engine={s} trace={s}\n", .{ build_options.platform, build_options.host, build_options.web_engine, build_options.trace }); } // Session replay never opens a real platform: the journal is the // world, and it drives a headless runtime over the null platform. @@ -345,6 +346,7 @@ fn runNull(app: native_sdk.App, options: RunOptions, init: std.process.Init) !vo const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers); const session_recorder = setupSessionRecorder(init, app_info); var null_platform = native_sdk.NullPlatform.initWithOptions(.{}, webEngine(), app_info); + const platform = if (comptime nativeHost()) null_platform.nativePlatform() else null_platform.platform(); var trace_sink = StdoutTraceSink{}; var log_buffers: native_sdk.debug.LogPathBuffers = .{}; const log_setup = native_sdk.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null; @@ -368,7 +370,7 @@ fn runNull(app: native_sdk.App, options: RunOptions, init: std.process.Init) !vo const runtime = try std.heap.page_allocator.create(native_sdk.Runtime); defer std.heap.page_allocator.destroy(runtime); native_sdk.Runtime.initAt(runtime, .{ - .platform = null_platform.platform(), + .platform = platform, .trace_sink = runtime_trace_sink, .log_path = if (log_setup) |setup| setup.paths.log_file else null, .bridge = options.bridge, @@ -384,7 +386,7 @@ fn runNull(app: native_sdk.App, options: RunOptions, init: std.process.Init) !vo .session_recorder = session_recorder, }); - try runtime.run(app); + try runRuntime(runtime, app); finishSessionRecorder(session_recorder); } @@ -422,7 +424,7 @@ fn runMacos(app: native_sdk.App, options: RunOptions, init: std.process.Init) !v const runtime = try std.heap.page_allocator.create(native_sdk.Runtime); defer std.heap.page_allocator.destroy(runtime); native_sdk.Runtime.initAt(runtime, .{ - .platform = mac_platform.platform(), + .platform = if (comptime nativeHost()) mac_platform.nativePlatform() else mac_platform.platform(), .trace_sink = runtime_trace_sink, .log_path = if (log_setup) |setup| setup.paths.log_file else null, .bridge = options.bridge, @@ -439,7 +441,7 @@ fn runMacos(app: native_sdk.App, options: RunOptions, init: std.process.Init) !v }); native_sdk.runtime.launch_timing.lap("runtime_ready"); - try runtime.run(app); + try runRuntime(runtime, app); finishSessionRecorder(session_recorder); } @@ -473,7 +475,7 @@ fn runLinux(app: native_sdk.App, options: RunOptions, init: std.process.Init) !v const runtime = try std.heap.page_allocator.create(native_sdk.Runtime); defer std.heap.page_allocator.destroy(runtime); native_sdk.Runtime.initAt(runtime, .{ - .platform = linux_platform.platform(), + .platform = if (comptime nativeHost()) linux_platform.nativePlatform() else linux_platform.platform(), .trace_sink = runtime_trace_sink, .log_path = if (log_setup) |setup| setup.paths.log_file else null, .bridge = options.bridge, @@ -489,7 +491,7 @@ fn runLinux(app: native_sdk.App, options: RunOptions, init: std.process.Init) !v .session_recorder = session_recorder, }); - try runtime.run(app); + try runRuntime(runtime, app); finishSessionRecorder(session_recorder); } @@ -523,7 +525,7 @@ fn runWindows(app: native_sdk.App, options: RunOptions, init: std.process.Init) const runtime = try std.heap.page_allocator.create(native_sdk.Runtime); defer std.heap.page_allocator.destroy(runtime); native_sdk.Runtime.initAt(runtime, .{ - .platform = windows_platform.platform(), + .platform = if (comptime nativeHost()) windows_platform.nativePlatform() else windows_platform.platform(), .trace_sink = runtime_trace_sink, .log_path = if (log_setup) |setup| setup.paths.log_file else null, .bridge = options.bridge, @@ -539,10 +541,18 @@ fn runWindows(app: native_sdk.App, options: RunOptions, init: std.process.Init) .session_recorder = session_recorder, }); - try runtime.run(app); + try runRuntime(runtime, app); finishSessionRecorder(session_recorder); } +fn runRuntime(runtime: *native_sdk.Runtime, app: native_sdk.App) !void { + if (comptime nativeHost()) { + try runtime.runNative(app); + } else { + try runtime.run(app); + } +} + // ------------------------------------------------- session record/replay /// Positional file sink behind the session recorder. Process-lifetime: @@ -620,7 +630,7 @@ fn runSessionReplay(app: native_sdk.App, options: RunOptions, init: std.process. const app_info = options.appInfo(&buffers); var null_platform = native_sdk.NullPlatform.initWithOptions(.{}, webEngine(), app_info); null_platform.gpu_surfaces = true; - var replay_platform = null_platform.platform(); + var replay_platform = if (comptime nativeHost()) null_platform.nativePlatform() else null_platform.platform(); // Same-platform replay must mirror the RECORDING host's rendering // capabilities, or pixel checkpoints catch the honest difference: // - text measures through the SAME host seam (macOS: CoreText + the @@ -651,7 +661,7 @@ fn runSessionReplay(app: native_sdk.App, options: RunOptions, init: std.process. !std.mem.eql(u8, value, "0") else true; - const report = native_sdk.runtime.replaySession(runtime, app, journal_bytes, .{ .verify = verify }) catch |err| { + const report = (if (comptime nativeHost()) native_sdk.runtime.replayNativeSession else native_sdk.runtime.replaySession)(runtime, app, journal_bytes, .{ .verify = verify }) catch |err| { switch (err) { error.JournalBadMagic, error.JournalUnsupportedVersion, @@ -712,6 +722,41 @@ fn webEngine() native_sdk.WebEngine { return .system; } +fn nativeHost() bool { + return std.mem.eql(u8, build_options.host, "native"); +} + +fn validateNativeHostManifest() void { + if (!nativeHost()) return; + if (@hasField(@TypeOf(app_manifest), "frontend")) { + @compileError("app.zon host = \"native\" cannot declare frontend content"); + } + if (@hasField(@TypeOf(app_manifest), "capabilities")) { + for (app_manifest.capabilities) |capability| { + if (std.mem.eql(u8, capability, "webview")) { + @compileError("app.zon host = \"native\" cannot declare the webview capability"); + } + if (std.mem.eql(u8, capability, "js_bridge")) { + @compileError("app.zon host = \"native\" cannot declare the JavaScript bridge capability"); + } + } + } + if (@hasField(@TypeOf(app_manifest), "bridge") and @hasField(@TypeOf(app_manifest.bridge), "commands") and app_manifest.bridge.commands.len > 0) { + @compileError("app.zon host = \"native\" cannot declare JavaScript bridge commands"); + } + if (!@hasField(@TypeOf(app_manifest), "shell")) return; + const shell = app_manifest.shell; + if (!@hasField(@TypeOf(shell), "windows")) return; + for (shell.windows) |window| { + if (!@hasField(@TypeOf(window), "views")) continue; + for (window.views) |view| { + if (@hasField(@TypeOf(view), "kind") and std.mem.eql(u8, view.kind, "webview")) { + @compileError("app.zon host = \"native\" cannot declare webview shell views"); + } + } + } +} + const StateBuffers = struct { state_dir: [1024]u8 = undefined, file_path: [1200]u8 = undefined, diff --git a/src/platform/linux/gtk_host.c b/src/platform/linux/gtk_host.c index c0895892a..c2437209b 100644 --- a/src/platform/linux/gtk_host.c +++ b/src/platform/linux/gtk_host.c @@ -1,7 +1,12 @@ #include "gtk_host.h" #include +#if !defined(NATIVE_SDK_NATIVE_ONLY) #include +#else +typedef struct _WebKitWebView WebKitWebView; +typedef struct _WebKitUserContentManager WebKitUserContentManager; +#endif #include #include #include @@ -430,6 +435,7 @@ static int native_sdk_window_allows_asset_origin(native_sdk_gtk_window_t *win, c return native_sdk_strings_equal(win->asset_origin, origin) || native_sdk_strings_equal(win->bridge_origin, origin); } +#if !defined(NATIVE_SDK_NATIVE_ONLY) typedef WebKitWebView *(*native_sdk_request_get_web_view_fn)(WebKitURISchemeRequest *request); static native_sdk_request_get_web_view_fn native_sdk_request_get_web_view(void) { @@ -484,6 +490,7 @@ static void native_sdk_apply_webview_frame(native_sdk_gtk_webview_t *webview) { gtk_widget_set_margin_top(widget, native_sdk_webview_coord(webview->y)); gtk_widget_set_size_request(widget, native_sdk_webview_extent(webview->width), native_sdk_webview_extent(webview->height)); } +#endif static int native_sdk_valid_native_view_frame(double x, double y, double width, double height) { return x >= 0 && y >= 0 && width >= 0 && height >= 0; @@ -1778,6 +1785,7 @@ static const char *native_sdk_mime_for_ext(const char *path) { return "application/octet-stream"; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) static native_sdk_gtk_window_t *native_sdk_window_for_asset_uri(native_sdk_gtk_host_t *host, const char *uri, WebKitWebView *request_web_view, int request_web_view_supported) { char *origin = native_sdk_origin_for_uri(uri); if (!origin) return NULL; @@ -1882,6 +1890,7 @@ static void native_sdk_asset_scheme_request(WebKitURISchemeRequest *request, gpo g_free(path); g_free(relative); } +#endif static native_sdk_gtk_window_t *native_sdk_find_window(native_sdk_gtk_host_t *host, uint64_t id) { for (int i = 0; i < host->window_count; i++) { @@ -2385,6 +2394,7 @@ static gboolean on_close_request(GtkWindow *window, gpointer data) { return FALSE; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) static const char *native_sdk_decision_uri(WebKitPolicyDecision *decision, WebKitPolicyDecisionType type) { if (type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) return NULL; WebKitNavigationPolicyDecision *navigation = WEBKIT_NAVIGATION_POLICY_DECISION(decision); @@ -2392,6 +2402,7 @@ static const char *native_sdk_decision_uri(WebKitPolicyDecision *decision, WebKi WebKitURIRequest *request = action ? webkit_navigation_action_get_request(action) : NULL; return request ? webkit_uri_request_get_uri(request) : NULL; } +#endif #if GTK_CHECK_VERSION(4, 10, 0) static void native_sdk_uri_launch_done(GObject *source_object, GAsyncResult *result, gpointer data) { @@ -2417,6 +2428,7 @@ static void native_sdk_open_external_uri(GtkWindow *parent, const char *uri) { #endif } +#if !defined(NATIVE_SDK_NATIVE_ONLY) static gboolean on_decide_policy(WebKitWebView *web_view, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type, gpointer data) { (void)web_view; native_sdk_gtk_window_t *win = data; @@ -2507,6 +2519,7 @@ static void native_sdk_setup_bridge(native_sdk_gtk_window_t *win) { webkit_user_content_manager_add_script(manager, script); webkit_user_script_unref(script); } +#endif static native_sdk_gtk_window_t *native_sdk_create_window_internal(native_sdk_gtk_host_t *host, uint64_t window_id, const char *title, const char *label, double x, double y, double width, double height, int restore_frame, int resizable, int titlebar_style, double min_width, double min_height) { if (native_sdk_find_window(host, window_id)) return NULL; @@ -2591,6 +2604,7 @@ static native_sdk_gtk_window_t *native_sdk_create_window_internal(native_sdk_gtk g_signal_connect(bar, "map", G_CALLBACK(native_sdk_on_header_bar_mapped), win->host); } +#if !defined(NATIVE_SDK_NATIVE_ONLY) win->content_manager = webkit_user_content_manager_new(); WebKitWebView *wv = WEBKIT_WEB_VIEW( g_object_new(WEBKIT_TYPE_WEB_VIEW, @@ -2602,6 +2616,7 @@ static native_sdk_gtk_window_t *native_sdk_create_window_internal(native_sdk_gtk host->scheme_registered = 1; } native_sdk_setup_bridge(win); +#endif win->root_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); /* Declared content min-size floor: the size request on the content @@ -2620,7 +2635,9 @@ static native_sdk_gtk_window_t *native_sdk_create_window_internal(native_sdk_gtk win->stack_root = gtk_overlay_new(); gtk_widget_set_hexpand(win->stack_root, TRUE); gtk_widget_set_vexpand(win->stack_root, TRUE); +#if !defined(NATIVE_SDK_NATIVE_ONLY) gtk_overlay_set_child(GTK_OVERLAY(win->stack_root), GTK_WIDGET(wv)); +#endif gtk_box_append(GTK_BOX(win->root_box), win->stack_root); gtk_window_set_child(win->gtk_window, win->root_box); native_sdk_install_file_drop_target(win); @@ -2629,7 +2646,9 @@ static native_sdk_gtk_window_t *native_sdk_create_window_internal(native_sdk_gtk g_signal_connect(win->gtk_window, "notify::default-height", G_CALLBACK(on_resize), win); g_signal_connect(win->gtk_window, "notify::is-active", G_CALLBACK(on_focus), win); g_signal_connect(win->gtk_window, "close-request", G_CALLBACK(on_close_request), win); +#if !defined(NATIVE_SDK_NATIVE_ONLY) g_signal_connect(win->web_view, "decide-policy", G_CALLBACK(on_decide_policy), win); +#endif GtkEventController *shortcut_controller = gtk_event_controller_key_new(); gtk_event_controller_set_propagation_phase(shortcut_controller, GTK_PHASE_CAPTURE); g_signal_connect(shortcut_controller, "key-pressed", G_CALLBACK(on_shortcut_key_pressed), win); @@ -2842,6 +2861,7 @@ int native_sdk_gtk_decode_image(const uint8_t *bytes, size_t bytes_len, uint8_t return 1; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) void native_sdk_gtk_load_webview(native_sdk_gtk_host_t *host, const char *source, size_t source_len, int source_kind, const char *asset_root, size_t asset_root_len, const char *asset_entry, size_t asset_entry_len, const char *asset_origin, size_t asset_origin_len, int spa_fallback) { native_sdk_gtk_load_window_webview(host, 1, source, source_len, source_kind, asset_root, asset_root_len, asset_entry, asset_entry_len, asset_origin, asset_origin_len, spa_fallback); } @@ -2971,6 +2991,7 @@ void native_sdk_gtk_emit_window_event(native_sdk_gtk_host_t *host, uint64_t wind free(event_name); free(detail); } +#endif void native_sdk_gtk_set_security_policy(native_sdk_gtk_host_t *host, const char *allowed_origins, size_t allowed_origins_len, const char *external_urls, size_t external_urls_len, int external_action) { native_sdk_free_string_list(host->allowed_origins, host->allowed_origins_count); @@ -3567,6 +3588,7 @@ int native_sdk_gtk_close_view(native_sdk_gtk_host_t *host, uint64_t window_id, c return 1; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) int native_sdk_gtk_create_webview(native_sdk_gtk_host_t *host, uint64_t window_id, const char *label, size_t label_len, const char *url, size_t url_len, double x, double y, double width, double height, int layer, int transparent, int bridge_enabled) { native_sdk_gtk_window_t *win = native_sdk_find_window(host, window_id); if (!win || !win->stack_root || label_len == 0 || url_len == 0 || !native_sdk_valid_webview_frame(x, y, width, height)) return 0; @@ -3722,6 +3744,7 @@ int native_sdk_gtk_close_webview(native_sdk_gtk_host_t *host, uint64_t window_id free(label_copy); return 0; } +#endif int native_sdk_gtk_open_external_url(native_sdk_gtk_host_t *host, const char *url, size_t url_len) { if (!host || !url || url_len == 0) return 0; diff --git a/src/platform/linux/root.zig b/src/platform/linux/root.zig index d4f87ebd4..c28f9f8ec 100644 --- a/src/platform/linux/root.zig +++ b/src/platform/linux/root.zig @@ -249,23 +249,31 @@ pub const LinuxPlatform = struct { } pub fn platform(self: *LinuxPlatform) platform_mod.Platform { + return self.platformWithWebViews(true); + } + + pub fn nativePlatform(self: *LinuxPlatform) platform_mod.Platform { + return self.platformWithWebViews(false); + } + + fn platformWithWebViews(self: *LinuxPlatform, comptime webviews_enabled: bool) platform_mod.Platform { return .{ .context = self, .name = "linux", .surface_value = self.surface_value, - .run_fn = run, - .supports_fn = supportsFeature, + .run_fn = if (webviews_enabled) run else runNative, + .supports_fn = if (webviews_enabled) supportsFeature else supportsNativeFeature, .services = .{ .context = self, .read_clipboard_fn = readClipboard, .write_clipboard_fn = writeClipboard, .read_clipboard_data_fn = readClipboardData, .write_clipboard_data_fn = writeClipboardData, - .load_webview_fn = loadWebView, - .load_window_webview_fn = loadWindowWebView, - .complete_bridge_fn = completeBridge, - .complete_window_bridge_fn = completeWindowBridge, - .complete_webview_bridge_fn = completeWebViewBridge, + .load_webview_fn = if (webviews_enabled) loadWebView else null, + .load_window_webview_fn = if (webviews_enabled) loadWindowWebView else null, + .complete_bridge_fn = if (webviews_enabled) completeBridge else null, + .complete_window_bridge_fn = if (webviews_enabled) completeWindowBridge else null, + .complete_webview_bridge_fn = if (webviews_enabled) completeWebViewBridge else null, .create_window_fn = createWindow, .focus_window_fn = focusWindow, .close_window_fn = closeWindow, @@ -273,7 +281,7 @@ pub const LinuxPlatform = struct { .start_window_drag_fn = startWindowDrag, .set_window_drag_regions_fn = setWindowDragRegions, .window_chrome_fn = windowChrome, - .create_view_fn = createView, + .create_view_fn = if (webviews_enabled) createView else createNativeView, .update_view_fn = updateView, .set_view_frame_fn = setViewFrame, .set_view_visible_fn = setViewVisible, @@ -281,12 +289,12 @@ pub const LinuxPlatform = struct { .close_view_fn = closeView, .request_gpu_surface_frame_fn = requestGpuSurfaceFrame, .present_gpu_surface_pixels_fn = presentGpuSurfacePixels, - .create_webview_fn = createWebView, - .set_webview_frame_fn = setWebViewFrame, - .navigate_webview_fn = navigateWebView, - .set_webview_zoom_fn = setWebViewZoom, - .set_webview_layer_fn = setWebViewLayer, - .close_webview_fn = closeWebView, + .create_webview_fn = if (webviews_enabled) createWebView else null, + .set_webview_frame_fn = if (webviews_enabled) setWebViewFrame else null, + .navigate_webview_fn = if (webviews_enabled) navigateWebView else null, + .set_webview_zoom_fn = if (webviews_enabled) setWebViewZoom else null, + .set_webview_layer_fn = if (webviews_enabled) setWebViewLayer else null, + .close_webview_fn = if (webviews_enabled) closeWebView else null, .show_open_dialog_fn = showOpenDialog, .show_save_dialog_fn = showSaveDialog, .show_message_dialog_fn = showMessageDialog, @@ -308,10 +316,10 @@ pub const LinuxPlatform = struct { .create_tray_fn = createTray, .update_tray_menu_fn = updateTrayMenu, .remove_tray_fn = removeTray, - .configure_security_policy_fn = configureSecurityPolicy, + .configure_security_policy_fn = if (webviews_enabled) configureSecurityPolicy else null, .configure_menus_fn = configureMenus, .configure_shortcuts_fn = configureShortcuts, - .emit_window_event_fn = emitWindowEvent, + .emit_window_event_fn = if (webviews_enabled) emitWindowEvent else null, .start_timer_fn = startTimer, .cancel_timer_fn = cancelTimer, .wake_fn = wake, @@ -327,6 +335,7 @@ pub const LinuxPlatform = struct { return switch (feature) { .main_webview, .child_webviews, + => self.web_engine == .system, .native_views, .native_control_commands, .menus, @@ -363,6 +372,13 @@ pub const LinuxPlatform = struct { }; } + fn supportsNativeFeature(context: *anyopaque, feature: platform_mod.PlatformFeature) bool { + return switch (feature) { + .main_webview, .child_webviews => false, + else => supportsFeature(context, feature), + }; + } + fn credentialsAvailable(host: *GtkHost) bool { if (comptime @import("builtin").is_test) return false; if (@import("builtin").target.os.tag != .linux) return false; @@ -396,6 +412,17 @@ pub const LinuxPlatform = struct { if (self.state.failed) return error.CallbackFailed; } + fn runNative(context: *anyopaque, handler: platform_mod.EventHandler, handler_context: *anyopaque) anyerror!void { + const self: *LinuxPlatform = @ptrCast(@alignCast(context)); + self.state = .{ + .self = self, + .handler = handler, + .handler_context = handler_context, + }; + native_sdk_gtk_run(self.host, gtkCallback, &self.state); + if (self.state.failed) return error.CallbackFailed; + } + fn windowById(self: *const LinuxPlatform, window_id: platform_mod.WindowId) platform_mod.WindowOptions { var index: usize = 0; while (index < self.app_info.startupWindowCount()) : (index += 1) { @@ -803,6 +830,11 @@ fn cancelTimer(context: ?*anyopaque, id: u64) anyerror!void { fn createView(context: ?*anyopaque, options: platform_mod.ViewOptions) anyerror!void { if (options.kind == .webview) return createWebView(context, options.webViewOptions()); + return createNativeView(context, options); +} + +fn createNativeView(context: ?*anyopaque, options: platform_mod.ViewOptions) anyerror!void { + if (options.kind == .webview) return error.UnsupportedViewKind; if (!isSupportedNativeViewKind(options.kind)) return error.UnsupportedViewKind; const self: *LinuxPlatform = @ptrCast(@alignCast(context.?)); if (self.web_engine != .system) return error.UnsupportedViewKind; diff --git a/src/platform/macos/appkit_host.m b/src/platform/macos/appkit_host.m index b3f138dce..0a9420ad7 100644 --- a/src/platform/macos/appkit_host.m +++ b/src/platform/macos/appkit_host.m @@ -10,7 +10,9 @@ #import #import #import +#if !defined(NATIVE_SDK_NATIVE_ONLY) #import +#endif #import #import #import @@ -26,9 +28,11 @@ @class NativeSdkAppKitHost; -static const NSUInteger NativeSdkMaxChildWebViews = 16; static const NSUInteger NativeSdkMaxNativeViews = 32; +#if !defined(NATIVE_SDK_NATIVE_ONLY) +static const NSUInteger NativeSdkMaxChildWebViews = 16; static const NSInteger NativeSdkBridgeFrameKeepaliveFrames = 600; +#endif static const uint64_t NativeSdkNanosecondsPerSecond = 1000000000ull; static const uint32_t NativeSdkShortcutModifierPrimary = 1u << 0; static const uint32_t NativeSdkShortcutModifierCommand = 1u << 1; @@ -47,15 +51,17 @@ * of the spectrum machinery in the audio section below. */ typedef struct native_sdk_spectrum_tap_state native_sdk_spectrum_tap_state_t; static NSRect constrainFrame(NSRect frame); +#if !defined(NATIVE_SDK_NATIVE_ONLY) static NSString *NativeSdkAppKitBridgeScript(void); static NSString *NativeSdkMimeTypeForPath(NSString *path); static NSString *NativeSdkResolvedAssetRoot(NSString *rootPath); -static void NativeSdkRegisterBundledFonts(void); static NSString *NativeSdkSafeAssetPath(NSURL *url, NSString *entryPath); static NSURL *NativeSdkAssetEntryURL(NSString *origin, NSString *entryPath); static NSArray *NativeSdkPolicyListFromBytes(const char *bytes, size_t len, NSArray *fallback); static NSString *NativeSdkOriginForURL(NSURL *url); static BOOL NativeSdkPolicyListMatches(NSArray *values, NSURL *url); +#endif +static void NativeSdkRegisterBundledFonts(void); static NSString *NativeSdkShortcutKeyForEvent(NSEvent *event); static BOOL NativeSdkShortcutUsesImplicitShift(NSString *key, NSEvent *event); static BOOL NativeSdkShortcutModifiersMatch(uint32_t shortcutModifiers, NSEventModifierFlags eventModifiers, BOOL allowImplicitShift); @@ -301,6 +307,7 @@ @interface NativeSdkWindowDelegate : NSObject @property(nonatomic, assign) BOOL observesContentLayout; @end +#if !defined(NATIVE_SDK_NATIVE_ONLY) @interface NativeSdkWebView : WKWebView @property(nonatomic, strong) NSArray *coveredMouseRects; @property(nonatomic, assign) NativeSdkAppKitHost *host; @@ -312,6 +319,7 @@ @interface NativeSdkBridgeScriptHandler : NSObject @property(nonatomic, assign) uint64_t windowId; @property(nonatomic, strong) NSString *webViewLabel; @end +#endif @class NativeSdkMetalSurfaceView; @@ -625,12 +633,14 @@ - (void)setSurfaceCursor:(NSCursor *)cursor; - (void)setScrollDrivers:(const native_sdk_appkit_scroll_driver_t *)drivers count:(NSUInteger)count; @end +#if !defined(NATIVE_SDK_NATIVE_ONLY) @interface NativeSdkAssetSchemeHandler : NSObject @property(nonatomic, strong) NSString *rootPath; @property(nonatomic, strong) NSString *entryPath; @property(nonatomic, assign) BOOL spaFallback; - (void)configureWithRootPath:(NSString *)rootPath entryPath:(NSString *)entryPath spaFallback:(BOOL)spaFallback; @end +#endif @interface NativeSdkShortcut : NSObject @property(nonatomic, strong) NSString *identifier; @@ -638,17 +648,29 @@ @interface NativeSdkShortcut : NSObject @property(nonatomic, assign) uint32_t modifiers; @end +#if !defined(NATIVE_SDK_NATIVE_ONLY) @interface NativeSdkAppKitHost : NSObject +#else +@interface NativeSdkAppKitHost : NSObject +#endif @property(nonatomic, strong) NSWindow *window; +#if !defined(NATIVE_SDK_NATIVE_ONLY) @property(nonatomic, strong) WKWebView *webView; +#endif @property(nonatomic, strong) NativeSdkWindowDelegate *delegate; +#if !defined(NATIVE_SDK_NATIVE_ONLY) @property(nonatomic, strong) NativeSdkBridgeScriptHandler *bridgeScriptHandler; @property(nonatomic, strong) NativeSdkAssetSchemeHandler *assetSchemeHandler; +#endif @property(nonatomic, strong) NSMutableDictionary *windows; +#if !defined(NATIVE_SDK_NATIVE_ONLY) @property(nonatomic, strong) NSMutableDictionary *webViews; +#endif @property(nonatomic, strong) NSMutableDictionary *delegates; +#if !defined(NATIVE_SDK_NATIVE_ONLY) @property(nonatomic, strong) NSMutableDictionary *bridgeScriptHandlers; @property(nonatomic, strong) NSMutableDictionary *assetSchemeHandlers; +#endif @property(nonatomic, strong) NSMutableDictionary *windowLabels; /// Present-before-show bookkeeping: windows created with the deferred /// show policy stay ordered OUT until their first gpu-surface present @@ -659,7 +681,9 @@ @interface NativeSdkAppKitHost : NSObject /// color, packed RGBA8 per window — so residual gaps (resize slack, /// titlebar bands) show the app's background, never a blank default. @property(nonatomic, strong) NSMutableDictionary *windowClearColors; +#if !defined(NATIVE_SDK_NATIVE_ONLY) @property(nonatomic, strong) NSMutableDictionary *childWebViews; +#endif @property(nonatomic, strong) NSMutableDictionary *nativeViews; /// App-owned NSViews adopted into native view containers (native-surface /// adoption): container key → adopted view. Kept alongside `nativeViews` @@ -674,7 +698,9 @@ @interface NativeSdkAppKitHost : NSObject @property(nonatomic, strong) NSMutableDictionary *canvasImageStore; @property(nonatomic, strong) NSMutableDictionary *nativeViewCommands; @property(nonatomic, strong) NSMutableSet *nativeViewExplicitTextKeys; +#if !defined(NATIVE_SDK_NATIVE_ONLY) @property(nonatomic, strong) NSMutableSet *bridgeEnabledChildWebViewKeys; +#endif @property(nonatomic, strong) NSTimer *timer; /* Coalescing flag for cross-thread frame requests: set (atomically) by * requestFrameFromAnyThread before it posts to the main queue, cleared @@ -757,13 +783,17 @@ @interface NativeSdkAppKitHost : NSObject @property(nonatomic, strong) NSString *iconPath; @property(nonatomic, strong) NSString *windowLabel; @property(nonatomic, assign) native_sdk_appkit_event_callback_t callback; -@property(nonatomic, assign) native_sdk_appkit_bridge_callback_t bridgeCallback; @property(nonatomic, assign) void *context; +#if !defined(NATIVE_SDK_NATIVE_ONLY) +@property(nonatomic, assign) native_sdk_appkit_bridge_callback_t bridgeCallback; @property(nonatomic, assign) void *bridgeContext; +#endif @property(nonatomic, assign) BOOL didShutdown; @property(nonatomic, assign) BOOL observesApplicationActivation; @property(nonatomic, assign) BOOL observesAppearanceChanges; +#if !defined(NATIVE_SDK_NATIVE_ONLY) @property(nonatomic, assign) NSInteger bridgeFrameKeepalive; +#endif @property(nonatomic, strong) id shortcutEventMonitor; @property(nonatomic, strong) id willTerminateObserver; @property(nonatomic, strong) dispatch_source_t sigtermSource; @@ -771,9 +801,11 @@ @interface NativeSdkAppKitHost : NSObject @property(nonatomic, strong) NSStatusItem *statusItem; @property(nonatomic, assign) native_sdk_appkit_tray_callback_t trayCallback; @property(nonatomic, assign) void *trayContext; +#if !defined(NATIVE_SDK_NATIVE_ONLY) @property(nonatomic, strong) NSArray *allowedNavigationOrigins; @property(nonatomic, strong) NSArray *allowedExternalURLs; @property(nonatomic, assign) NSInteger externalLinkAction; +#endif - (instancetype)initWithAppName:(NSString *)appName displayName:(NSString *)displayName version:(NSString *)version aboutDescription:(NSString *)aboutDescription hasWebContent:(BOOL)hasWebContent windowTitle:(NSString *)windowTitle bundleIdentifier:(NSString *)bundleIdentifier iconPath:(NSString *)iconPath windowLabel:(NSString *)windowLabel x:(double)x y:(double)y width:(double)width height:(double)height restoreFrame:(BOOL)restoreFrame resizable:(BOOL)resizable titlebarStyle:(int)titlebarStyle showPolicy:(int)showPolicy; - (BOOL)createWindowWithId:(uint64_t)windowId title:(NSString *)title label:(NSString *)label x:(double)x y:(double)y width:(double)width height:(double)height restoreFrame:(BOOL)restoreFrame resizable:(BOOL)resizable titlebarStyle:(int)titlebarStyle showPolicy:(int)showPolicy makeMain:(BOOL)makeMain; - (void)showDeferredWindowIfPending:(uint64_t)windowId reason:(const char *)reason; @@ -782,10 +814,12 @@ - (void)focusWindowWithId:(uint64_t)windowId; - (void)closeWindowWithId:(uint64_t)windowId; - (BOOL)startWindowDragWithId:(uint64_t)windowId; - (BOOL)chromeInsetsForWindowId:(uint64_t)windowId top:(double *)top left:(double *)left bottom:(double *)bottom right:(double *)right buttonsX:(double *)buttonsX buttonsY:(double *)buttonsY buttonsWidth:(double *)buttonsWidth buttonsHeight:(double *)buttonsHeight; +#if !defined(NATIVE_SDK_NATIVE_ONLY) - (WKWebView *)ensureMainWebViewForWindowId:(uint64_t)windowId; - (WKWebView *)webViewForWindowId:(uint64_t)windowId; - (WKWebView *)mainWebViewForWindow:(NSWindow *)window; - (NativeSdkAssetSchemeHandler *)assetHandlerForWindowId:(uint64_t)windowId; +#endif - (NSString *)nativeViewKeyForWindow:(uint64_t)windowId label:(NSString *)label; - (NSRect)viewFrameForContainer:(NSView *)container x:(double)x y:(double)y width:(double)width height:(double)height; - (NSView *)nativeParentViewForWindow:(uint64_t)windowId parent:(NSString *)parent; @@ -817,19 +851,21 @@ - (BOOL)adoptViewSurfaceInWindow:(uint64_t)windowId label:(NSString *)label surf - (BOOL)viewIsAdoptedSurfaceDescendant:(NSView *)view; - (void)installAdoptedSurfaceClickMonitor; - (BOOL)releaseViewSurfaceInWindow:(uint64_t)windowId label:(NSString *)label; -- (BOOL)createWebViewInWindow:(uint64_t)windowId label:(NSString *)label url:(NSString *)url x:(double)x y:(double)y width:(double)width height:(double)height layer:(NSInteger)layer transparent:(BOOL)transparent bridgeEnabled:(BOOL)bridgeEnabled; - (BOOL)setNativeViewCursorInWindow:(uint64_t)windowId label:(NSString *)label cursor:(NSInteger)cursor; +- (void)reorderViewsInWindow:(uint64_t)windowId; +#if !defined(NATIVE_SDK_NATIVE_ONLY) +- (BOOL)createWebViewInWindow:(uint64_t)windowId label:(NSString *)label url:(NSString *)url x:(double)x y:(double)y width:(double)width height:(double)height layer:(NSInteger)layer transparent:(BOOL)transparent bridgeEnabled:(BOOL)bridgeEnabled; - (BOOL)setWebViewFrameInWindow:(uint64_t)windowId label:(NSString *)label x:(double)x y:(double)y width:(double)width height:(double)height; - (BOOL)navigateWebViewInWindow:(uint64_t)windowId label:(NSString *)label url:(NSString *)url; - (BOOL)setWebViewZoomInWindow:(uint64_t)windowId label:(NSString *)label zoom:(double)zoom; - (BOOL)setWebViewLayerInWindow:(uint64_t)windowId label:(NSString *)label layer:(NSInteger)layer; - (BOOL)closeWebViewInWindow:(uint64_t)windowId label:(NSString *)label; - (void)closeWebViewsInWindow:(uint64_t)windowId; -- (void)reorderWebViewsInWindow:(uint64_t)windowId; - (void)updateCoveredMouseRectsInWindow:(uint64_t)windowId; - (void)applyCoveredMouseRects:(NSArray *)rects toWebView:(WKWebView *)webView; - (void)removeBridgeHandlerForChildWebView:(WKWebView *)webView key:(NSString *)key; - (void)removeAllChildBridgeHandlers; +#endif - (void)configureApplication; - (void)buildMenuBar; - (void)addApplicationMenuToMenu:(NSMenu *)mainMenu; @@ -882,9 +918,12 @@ - (void)stopAudioSpectrumTimer; - (BOOL)anyHostWindowVisibleOnGlass; - (void)audioSpectrumTimerFired:(NSTimer *)timer; - (void)wakeFromAnyThread; +#if !defined(NATIVE_SDK_NATIVE_ONLY) - (void)scheduleBridgeFrames; +#endif - (void)emitFrame; - (void)emitShutdown; +#if !defined(NATIVE_SDK_NATIVE_ONLY) - (void)loadSource:(NSString *)source kind:(NSInteger)kind assetRoot:(NSString *)assetRoot entry:(NSString *)entry origin:(NSString *)origin spaFallback:(BOOL)spaFallback; - (void)loadSource:(NSString *)source kind:(NSInteger)kind assetRoot:(NSString *)assetRoot entry:(NSString *)entry origin:(NSString *)origin spaFallback:(BOOL)spaFallback windowId:(uint64_t)windowId; - (void)setAllowedNavigationOrigins:(NSArray *)origins externalURLs:(NSArray *)externalURLs externalAction:(NSInteger)externalAction; @@ -896,6 +935,7 @@ - (void)completeBridgeWithResponse:(NSString *)response; - (void)completeBridgeWithResponse:(NSString *)response windowId:(uint64_t)windowId; - (void)completeBridgeWithResponse:(NSString *)response windowId:(uint64_t)windowId webViewLabel:(NSString *)webViewLabel; - (void)emitEventNamed:(NSString *)name detailJSON:(NSString *)detailJSON windowId:(uint64_t)windowId; +#endif - (void)setShortcutsWithIds:(const char *const *)ids idLengths:(const size_t *)idLengths keys:(const char *const *)keys keyLengths:(const size_t *)keyLengths modifiers:(const uint32_t *)modifiers count:(size_t)count; - (BOOL)handleShortcutEvent:(NSEvent *)event; - (void)emitShortcutWithId:(NSString *)identifier key:(NSString *)key modifiers:(uint32_t)modifiers event:(NSEvent *)event; @@ -1027,14 +1067,20 @@ - (void)windowWillClose:(NSNotification *)notification { self.observesContentLayout = NO; } [self.host emitWindowFrameForWindowId:self.windowId open:NO]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) [self.host closeWebViewsInWindow:self.windowId]; +#endif [self.host closeNativeViewsInWindow:self.windowId]; NSNumber *key = @(self.windowId); [self.host.windows removeObjectForKey:key]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) [self.host.webViews removeObjectForKey:key]; +#endif [self.host.delegates removeObjectForKey:key]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) [self.host.bridgeScriptHandlers removeObjectForKey:key]; [self.host.assetSchemeHandlers removeObjectForKey:key]; +#endif [self.host.windowLabels removeObjectForKey:key]; [self.host.deferredShowWindows removeObjectForKey:key]; [self.host.windowClearColors removeObjectForKey:key]; @@ -1046,6 +1092,7 @@ - (void)windowWillClose:(NSNotification *)notification { @end +#if !defined(NATIVE_SDK_NATIVE_ONLY) @implementation NativeSdkWebView - (BOOL)pointIsCovered:(NSPoint)point { @@ -1088,7 +1135,9 @@ - (BOOL)performDragOperation:(id)sender { } @end +#endif +#if !defined(NATIVE_SDK_NATIVE_ONLY) @implementation NativeSdkBridgeScriptHandler - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { @@ -1097,6 +1146,7 @@ - (void)userContentController:(WKUserContentController *)userContentController d } @end +#endif @implementation NativeSdkWidgetAccessibilityElement @@ -6172,6 +6222,7 @@ - (BOOL)emitWidgetAccessibilityActionWithId:(uint64_t)widgetId action:(NSInteger @end +#if !defined(NATIVE_SDK_NATIVE_ONLY) @implementation NativeSdkAssetSchemeHandler - (instancetype)init { @@ -6228,6 +6279,7 @@ - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id)urlS } @end +#endif @implementation NativeSdkShortcut @end @@ -6255,24 +6307,34 @@ - (instancetype)initWithAppName:(NSString *)appName displayName:(NSString *)disp self.iconPath = iconPath ?: @""; self.windowLabel = windowLabel.length > 0 ? windowLabel : @"main"; self.windows = [[NSMutableDictionary alloc] init]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) self.webViews = [[NSMutableDictionary alloc] init]; +#endif self.delegates = [[NSMutableDictionary alloc] init]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) self.bridgeScriptHandlers = [[NSMutableDictionary alloc] init]; self.assetSchemeHandlers = [[NSMutableDictionary alloc] init]; +#endif self.windowLabels = [[NSMutableDictionary alloc] init]; self.deferredShowWindows = [[NSMutableDictionary alloc] init]; self.windowClearColors = [[NSMutableDictionary alloc] init]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) self.childWebViews = [[NSMutableDictionary alloc] init]; +#endif self.nativeViews = [[NSMutableDictionary alloc] init]; self.adoptedViewSurfaces = [[NSMutableDictionary alloc] init]; self.canvasImageStore = [[NSMutableDictionary alloc] init]; self.nativeViewCommands = [[NSMutableDictionary alloc] init]; self.nativeViewExplicitTextKeys = [[NSMutableSet alloc] init]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) self.bridgeEnabledChildWebViewKeys = [[NSMutableSet alloc] init]; +#endif self.appTimers = [[NSMutableDictionary alloc] init]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) self.allowedNavigationOrigins = @[ @"zero://app", @"zero://inline" ]; self.allowedExternalURLs = @[]; self.externalLinkAction = 0; +#endif self.shortcuts = @[]; [self configureApplication]; NativeSdkLaunchLap("app_configured"); @@ -6467,10 +6529,12 @@ - (void)dealloc { [NSEvent removeMonitor:self.shortcutEventMonitor]; self.shortcutEventMonitor = nil; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) [self removeAllChildBridgeHandlers]; for (WKWebView *webView in self.webViews.allValues) { [webView.configuration.userContentController removeScriptMessageHandlerForName:@"nativeSdkBridge"]; } +#endif } - (void)focusWindowWithId:(uint64_t)windowId { @@ -6598,6 +6662,7 @@ - (BOOL)chromeInsetsForWindowId:(uint64_t)windowId top:(double *)top left:(doubl return YES; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) // Create-on-first-use for a window's main WebView. Pure peek reads // (event emission, bridge completion echoes, reorder passes) keep going // through `webViewForWindowId:` and skip absent WebViews — a page that @@ -6656,7 +6721,7 @@ - (WKWebView *)ensureMainWebViewForWindowId:(uint64_t)windowId { self.bridgeScriptHandler = bridgeScriptHandler; self.assetSchemeHandler = assetSchemeHandler; } - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; NativeSdkLaunchLap("main_webview_ready"); return webView; } @@ -6686,6 +6751,7 @@ - (NSRect)webViewFrameForWindow:(NSWindow *)window x:(double)x y:(double)y width CGFloat nativeY = contentView.isFlipped ? y : contentView.bounds.size.height - y - height; return NSMakeRect(x, nativeY, width, height); } +#endif - (NSString *)nativeViewKeyForWindow:(uint64_t)windowId label:(NSString *)label { return [NSString stringWithFormat:@"%llu:%@", windowId, label ?: @""]; @@ -6931,7 +6997,7 @@ - (BOOL)createNativeViewInWindow:(uint64_t)windowId label:(NSString *)label kind } else { [self.nativeViewExplicitTextKeys removeObject:key]; } - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; [self scheduleFrame]; return YES; } @@ -6972,7 +7038,7 @@ - (BOOL)updateNativeViewInWindow:(uint64_t)windowId label:(NSString *)label hasF [self applyNativeViewState:view enabled:currentEnabled role:(hasRole ? role : nil) accessibilityLabel:(hasAccessibilityLabel ? accessibilityLabel : nil) text:displayText]; } if (hasCommand) [self configureNativeView:view command:command key:key]; - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; [self scheduleFrame]; return YES; } @@ -6988,6 +7054,7 @@ - (BOOL)setNativeViewVisibleInWindow:(uint64_t)windowId label:(NSString *)label - (BOOL)focusNativeViewInWindow:(uint64_t)windowId label:(NSString *)label { NSWindow *window = self.windows[@(windowId)] ?: (windowId == 1 ? self.window : nil); if (!window) return NO; +#if !defined(NATIVE_SDK_NATIVE_ONLY) if ([label isEqualToString:@"main"]) { WKWebView *webView = [self webViewForWindowId:windowId]; if (!webView || webView.hidden) return NO; @@ -6999,6 +7066,7 @@ - (BOOL)focusNativeViewInWindow:(uint64_t)windowId label:(NSString *)label { [window makeKeyAndOrderFront:nil]; return [window makeFirstResponder:webView]; } +#endif NSView *view = self.nativeViews[[self nativeViewKeyForWindow:windowId label:label]]; if (!view || view.hidden) return NO; window = view.window ?: window; @@ -7200,7 +7268,7 @@ - (BOOL)closeNativeViewInWindow:(uint64_t)windowId label:(NSString *)label { [self.nativeViewCommands removeObjectForKey:viewKey]; [self.nativeViewExplicitTextKeys removeObject:viewKey]; } - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; [self scheduleFrame]; return YES; } @@ -7217,7 +7285,7 @@ - (void)closeNativeViewsInWindow:(uint64_t)windowId { [self.nativeViewCommands removeObjectForKey:key]; [self.nativeViewExplicitTextKeys removeObject:key]; } - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; } - (void)dropAdoptedViewSurfaceForKey:(NSString *)key { @@ -7280,6 +7348,7 @@ - (BOOL)releaseViewSurfaceInWindow:(uint64_t)windowId label:(NSString *)label { return YES; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) - (BOOL)createWebViewInWindow:(uint64_t)windowId label:(NSString *)label url:(NSString *)url x:(double)x y:(double)y width:(double)width height:(double)height layer:(NSInteger)layer transparent:(BOOL)transparent bridgeEnabled:(BOOL)bridgeEnabled { if (label.length == 0 || url.length == 0 || width <= 0 || height <= 0 || x < 0 || y < 0) return NO; NSWindow *window = self.windows[@(windowId)] ?: (windowId == 1 ? self.window : nil); @@ -7331,7 +7400,7 @@ - (BOOL)createWebViewInWindow:(uint64_t)windowId label:(NSString *)label url:(NS [webview loadRequest:[NSURLRequest requestWithURL:targetURL]]; self.childWebViews[key] = webview; if (bridgeEnabled) [self.bridgeEnabledChildWebViewKeys addObject:key]; - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; [self scheduleBridgeFrames]; return YES; } @@ -7344,14 +7413,14 @@ - (BOOL)setWebViewFrameInWindow:(uint64_t)windowId label:(NSString *)label x:(do if (!window || !webView) return NO; webView.autoresizingMask = NSViewNotSizable; webView.frame = [self webViewFrameForWindow:window x:x y:y width:width height:height]; - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; [self scheduleBridgeFrames]; return YES; } WKWebView *webview = self.childWebViews[[self webViewKeyForWindow:windowId label:label]]; if (!window || !webview) return NO; webview.frame = [self webViewFrameForWindow:window x:x y:y width:width height:height]; - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; [self scheduleBridgeFrames]; return YES; } @@ -7396,14 +7465,14 @@ - (BOOL)setWebViewLayerInWindow:(uint64_t)windowId label:(NSString *)label layer if (!webView) return NO; webView.wantsLayer = YES; webView.layer.zPosition = layer; - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; return YES; } WKWebView *webview = self.childWebViews[[self webViewKeyForWindow:windowId label:label]]; if (!webview) return NO; webview.wantsLayer = YES; webview.layer.zPosition = layer; - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; return YES; } @@ -7414,7 +7483,7 @@ - (BOOL)closeWebViewInWindow:(uint64_t)windowId label:(NSString *)label { [self removeBridgeHandlerForChildWebView:webview key:key]; [webview removeFromSuperview]; [self.childWebViews removeObjectForKey:key]; - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; [self scheduleBridgeFrames]; return YES; } @@ -7429,21 +7498,23 @@ - (void)closeWebViewsInWindow:(uint64_t)windowId { [webview removeFromSuperview]; [self.childWebViews removeObjectForKey:key]; } - [self reorderWebViewsInWindow:windowId]; + [self reorderViewsInWindow:windowId]; } +#endif -- (void)reorderWebViewsInWindow:(uint64_t)windowId { +- (void)reorderViewsInWindow:(uint64_t)windowId { NSWindow *window = self.windows[@(windowId)] ?: (windowId == 1 ? self.window : nil); NSView *contentView = window.contentView; if (!contentView) return; NSMutableArray *views = [[NSMutableArray alloc] init]; + NSString *prefix = [NSString stringWithFormat:@"%llu:", windowId]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) WKWebView *mainWebView = self.webViews[@(windowId)]; if (mainWebView && mainWebView.superview == contentView) { [views addObject:mainWebView]; } - NSString *prefix = [NSString stringWithFormat:@"%llu:", windowId]; for (NSString *key in self.childWebViews) { if (![key hasPrefix:prefix]) continue; WKWebView *view = self.childWebViews[key]; @@ -7451,6 +7522,7 @@ - (void)reorderWebViewsInWindow:(uint64_t)windowId { [views addObject:view]; } } +#endif for (NSString *key in self.nativeViews) { if (![key hasPrefix:prefix]) continue; NSView *view = self.nativeViews[key]; @@ -7476,9 +7548,12 @@ - (void)reorderWebViewsInWindow:(uint64_t)windowId { [contentView addSubview:view positioned:NSWindowAbove relativeTo:previous]; previous = view; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) [self updateCoveredMouseRectsInWindow:windowId]; +#endif } +#if !defined(NATIVE_SDK_NATIVE_ONLY) - (void)updateCoveredMouseRectsInWindow:(uint64_t)windowId { NSWindow *window = self.windows[@(windowId)] ?: (windowId == 1 ? self.window : nil); NSView *contentView = window.contentView; @@ -7586,6 +7661,7 @@ - (void)removeAllChildBridgeHandlers { [self removeBridgeHandlerForChildWebView:self.childWebViews[key] key:key]; } } +#endif static NSRect constrainFrame(NSRect frame) { NSScreen *screen = [NSScreen mainScreen]; @@ -7600,6 +7676,7 @@ static NSRect constrainFrame(NSRect frame) { return frame; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) static NSString *NativeSdkAppKitBridgeScript(void) { return @"(function(){" "if(window.zero&&window.zero.invoke){return;}" @@ -7738,6 +7815,7 @@ static NSRect constrainFrame(NSRect frame) { if ([ext isEqualToString:@"wasm"]) return @"application/wasm"; return @"application/octet-stream"; } +#endif static BOOL NativeSdkDirectoryExists(NSString *path) { BOOL isDirectory = NO; @@ -7765,6 +7843,7 @@ static BOOL NativeSdkDirectoryExists(NSString *path) { return path; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) static NSString *NativeSdkResolvedAssetRoot(NSString *rootPath) { NSString *resourcePath = [NSBundle mainBundle].resourcePath; BOOL isAppBundle = [[NSBundle mainBundle].bundlePath.pathExtension.lowercaseString isEqualToString:@"app"]; @@ -7780,6 +7859,7 @@ static BOOL NativeSdkDirectoryExists(NSString *path) { } return cwdPath; } +#endif static BOOL NativeSdkFontAssetExtension(NSString *path) { NSString *extension = path.pathExtension.lowercaseString; @@ -7832,6 +7912,7 @@ static void NativeSdkRegisterBundledFonts(void) { }); } +#if !defined(NATIVE_SDK_NATIVE_ONLY) static BOOL NativeSdkPathHasUnsafeSegment(NSString *path) { for (NSString *segment in [path componentsSeparatedByString:@"/"]) { if (segment.length == 0) continue; @@ -7864,6 +7945,7 @@ static BOOL NativeSdkPathHasUnsafeSegment(NSString *path) { } return [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", base, entry]]; } +#endif /* Ask the process services layer to show the display name for this * process in the Dock tile and the app switcher. Unbundled dev binaries @@ -7945,6 +8027,7 @@ - (void)buildMenuBar { [mainMenu addItem:editMenuItem]; NSMenu *editMenu = [[NSMenu alloc] initWithTitle:@"Edit"]; [editMenuItem setSubmenu:editMenu]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) if (self.hasWebContent) { // Undo/Redo answer only inside web content (the webview's own // editing stack); the canvas text editor has no undo stack, so @@ -7953,6 +8036,7 @@ - (void)buildMenuBar { [editMenu addItem:[self menuItem:@"Redo" action:@selector(redo:) key:@"Z" modifiers:NSEventModifierFlagCommand]]; [editMenu addItem:[NSMenuItem separatorItem]]; } +#endif [editMenu addItem:[self menuItem:@"Cut" action:@selector(cut:) key:@"x" modifiers:NSEventModifierFlagCommand]]; [editMenu addItem:[self menuItem:@"Copy" action:@selector(copy:) key:@"c" modifiers:NSEventModifierFlagCommand]]; [editMenu addItem:[self menuItem:@"Paste" action:@selector(paste:) key:@"v" modifiers:NSEventModifierFlagCommand]]; @@ -7966,11 +8050,13 @@ - (void)buildMenuBar { [mainMenu addItem:viewMenuItem]; NSMenu *viewMenu = [[NSMenu alloc] initWithTitle:@"View"]; [viewMenuItem setSubmenu:viewMenu]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) if (self.hasWebContent) { [viewMenu addItem:[self menuItem:@"Reload" action:@selector(reload:) key:@"r" modifiers:NSEventModifierFlagCommand]]; [viewMenu addItem:[self menuItem:@"Toggle Web Inspector" action:@selector(toggleWebInspector:) key:@"i" modifiers:(NSEventModifierFlagCommand | NSEventModifierFlagOption)]]; [viewMenu addItem:[NSMenuItem separatorItem]]; } +#endif [viewMenu addItem:[self menuItem:@"Enter Full Screen" action:@selector(toggleFullScreen:) key:@"f" modifiers:(NSEventModifierFlagCommand | NSEventModifierFlagControl)]]; // A real Window menu: Minimize/Zoom act through the responder @@ -9154,18 +9240,22 @@ - (int)audioSetVolume:(double)volume { return 1; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) - (void)scheduleBridgeFrames { self.bridgeFrameKeepalive = NativeSdkBridgeFrameKeepaliveFrames; [self scheduleFrame]; } +#endif - (void)emitFrame { self.timer = nil; [self emitEvent:(native_sdk_appkit_event_t){ .kind = NATIVE_SDK_APPKIT_EVENT_FRAME }]; +#if !defined(NATIVE_SDK_NATIVE_ONLY) if (self.bridgeFrameKeepalive > 0) { self.bridgeFrameKeepalive -= 1; [self scheduleFrame]; } +#endif } - (void)emitShutdown { @@ -9176,6 +9266,7 @@ - (void)emitShutdown { [self emitEvent:(native_sdk_appkit_event_t){ .kind = NATIVE_SDK_APPKIT_EVENT_SHUTDOWN }]; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) - (void)loadSource:(NSString *)source kind:(NSInteger)kind assetRoot:(NSString *)assetRoot entry:(NSString *)entry origin:(NSString *)origin spaFallback:(BOOL)spaFallback { [self loadSource:source kind:kind assetRoot:assetRoot entry:entry origin:origin spaFallback:spaFallback windowId:1]; } @@ -9355,6 +9446,7 @@ - (void)emitEventNamed:(NSString *)name detailJSON:(NSString *)detailJSON window [webView evaluateJavaScript:script completionHandler:nil]; [self scheduleBridgeFrames]; } +#endif - (BOOL)handleShortcutEvent:(NSEvent *)event { if (event.type != NSEventTypeKeyDown) return NO; @@ -9466,6 +9558,7 @@ - (void)showAboutPanel:(id)sender { [NSApp orderFrontStandardAboutPanelWithOptions:options]; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) - (void)reload:(id)sender { (void)sender; WKWebView *webView = [self mainWebViewForWindow:NSApp.keyWindow]; @@ -9483,6 +9576,7 @@ - (void)toggleWebInspector:(id)sender { ((void (*)(id, SEL))[webView methodForSelector:selector])(webView, selector); } } +#endif - (void)trayMenuItemClicked:(NSMenuItem *)menuItem { if (self.trayCallback) { @@ -9492,6 +9586,7 @@ - (void)trayMenuItemClicked:(NSMenuItem *)menuItem { @end +#if !defined(NATIVE_SDK_NATIVE_ONLY) static NSArray *NativeSdkPolicyListFromBytes(const char *bytes, size_t len, NSArray *fallback) { if (!bytes || len == 0) return fallback ?: @[]; NSString *joined = [[NSString alloc] initWithBytes:bytes length:len encoding:NSUTF8StringEncoding]; @@ -9515,6 +9610,7 @@ - (void)trayMenuItemClicked:(NSMenuItem *)menuItem { if (port) return [NSString stringWithFormat:@"%@://%@:%@", scheme, host, port]; return [NSString stringWithFormat:@"%@://%@", scheme, host]; } +#endif static NSString *NativeSdkShortcutKeyForEvent(NSEvent *event) { NSString *characters = event.charactersIgnoringModifiers ?: @""; @@ -9592,6 +9688,7 @@ static NSEventModifierFlags NativeSdkMenuModifierFlags(uint32_t modifiers) { return flags; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) static BOOL NativeSdkWildcardPrefixHasPath(NSString *prefix) { NSURLComponents *components = [NSURLComponents componentsWithString:prefix ?: @""]; return components.scheme.length > 0 && components.host.length > 0 && components.percentEncodedPath.length > 0; @@ -9610,6 +9707,7 @@ static BOOL NativeSdkPolicyListMatches(NSArray *values, NSURL *url) } return NO; } +#endif native_sdk_appkit_host_t *native_sdk_appkit_create(const char *app_name, size_t app_name_len, const char *display_name, size_t display_name_len, const char *version, size_t version_len, const char *about_description, size_t about_description_len, int has_web_content, const char *window_title, size_t window_title_len, const char *bundle_id, size_t bundle_id_len, const char *icon_path, size_t icon_path_len, const char *window_label, size_t window_label_len, double x, double y, double width, double height, int restore_frame, int resizable, int titlebar_style, int show_policy) { @autoreleasepool { @@ -9754,6 +9852,7 @@ void native_sdk_appkit_stop(native_sdk_appkit_host_t *host) { [object stop]; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) void native_sdk_appkit_load_webview(native_sdk_appkit_host_t *host, const char *source, size_t source_len, int source_kind, const char *asset_root, size_t asset_root_len, const char *asset_entry, size_t asset_entry_len, const char *asset_origin, size_t asset_origin_len, int spa_fallback) { native_sdk_appkit_load_window_webview(host, 1, source, source_len, source_kind, asset_root, asset_root_len, asset_entry, asset_entry_len, asset_origin, asset_origin_len, spa_fallback); } @@ -9809,6 +9908,7 @@ void native_sdk_appkit_set_security_policy(native_sdk_appkit_host_t *host, const NSArray *externalURLs = NativeSdkPolicyListFromBytes(external_urls, external_urls_len, @[]); [object setAllowedNavigationOrigins:origins externalURLs:externalURLs externalAction:external_action]; } +#endif void native_sdk_appkit_set_menus(native_sdk_appkit_host_t *host, const char *const *menu_titles, const size_t *menu_title_lens, size_t menu_count, const uint32_t *item_menu_indices, const char *const *item_labels, const size_t *item_label_lens, const char *const *item_commands, const size_t *item_command_lens, const char *const *item_keys, const size_t *item_key_lens, const uint32_t *item_modifiers, const int *item_separators, const int *item_enabled, const int *item_checked, size_t item_count) { NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host; @@ -9994,6 +10094,7 @@ int native_sdk_appkit_update_widget_accessibility(native_sdk_appkit_host_t *host return [object updateWidgetAccessibilityInWindow:window_id label:labelString ?: @"" nodes:nodes count:node_count] ? 1 : 0; } +#if !defined(NATIVE_SDK_NATIVE_ONLY) int native_sdk_appkit_create_webview(native_sdk_appkit_host_t *host, uint64_t window_id, const char *label, size_t label_len, const char *url, size_t url_len, double x, double y, double width, double height, int layer, int transparent, int bridge_enabled) { NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host; NSString *labelString = label ? [[NSString alloc] initWithBytes:label length:label_len encoding:NSUTF8StringEncoding] : @""; @@ -10031,6 +10132,7 @@ int native_sdk_appkit_close_webview(native_sdk_appkit_host_t *host, uint64_t win NSString *labelString = label ? [[NSString alloc] initWithBytes:label length:label_len encoding:NSUTF8StringEncoding] : @""; return [object closeWebViewInWindow:window_id label:labelString ?: @""] ? 1 : 0; } +#endif size_t native_sdk_appkit_clipboard_read(native_sdk_appkit_host_t *host, char *buffer, size_t buffer_len) { return native_sdk_appkit_clipboard_read_data(host, "text/plain", strlen("text/plain"), buffer, buffer_len); diff --git a/src/platform/macos/root.zig b/src/platform/macos/root.zig index 83b579bb0..4ba5ed651 100644 --- a/src/platform/macos/root.zig +++ b/src/platform/macos/root.zig @@ -562,30 +562,38 @@ pub const MacPlatform = struct { } pub fn platform(self: *MacPlatform) platform_mod.Platform { + return self.platformWithWebViews(true); + } + + pub fn nativePlatform(self: *MacPlatform) platform_mod.Platform { + return self.platformWithWebViews(false); + } + + fn platformWithWebViews(self: *MacPlatform, comptime webviews_enabled: bool) platform_mod.Platform { return .{ .context = self, .name = "macos", .surface_value = self.surface_value, - .run_fn = run, - .supports_fn = supportsFeature, + .run_fn = if (webviews_enabled) run else runNative, + .supports_fn = if (webviews_enabled) supportsFeature else supportsNativeFeature, .services = .{ .context = self, .read_clipboard_fn = readClipboard, .write_clipboard_fn = writeClipboard, .read_clipboard_data_fn = readClipboardData, .write_clipboard_data_fn = writeClipboardData, - .load_webview_fn = loadWebView, - .load_window_webview_fn = loadWindowWebView, - .complete_bridge_fn = completeBridge, - .complete_window_bridge_fn = completeWindowBridge, - .complete_webview_bridge_fn = completeWebViewBridge, + .load_webview_fn = if (webviews_enabled) loadWebView else null, + .load_window_webview_fn = if (webviews_enabled) loadWindowWebView else null, + .complete_bridge_fn = if (webviews_enabled) completeBridge else null, + .complete_window_bridge_fn = if (webviews_enabled) completeWindowBridge else null, + .complete_webview_bridge_fn = if (webviews_enabled) completeWebViewBridge else null, .create_window_fn = createWindow, .focus_window_fn = focusWindow, .close_window_fn = closeWindow, .minimize_window_fn = minimizeWindow, .start_window_drag_fn = startWindowDrag, .window_chrome_fn = windowChrome, - .create_view_fn = createView, + .create_view_fn = if (webviews_enabled) createView else createNativeView, .update_view_fn = updateView, .set_view_frame_fn = setViewFrame, .set_view_visible_fn = setViewVisible, @@ -594,12 +602,12 @@ pub const MacPlatform = struct { .close_view_fn = closeView, .adopt_view_surface_fn = adoptViewSurface, .release_view_surface_fn = releaseViewSurface, - .create_webview_fn = createWebView, - .set_webview_frame_fn = setWebViewFrame, - .navigate_webview_fn = navigateWebView, - .set_webview_zoom_fn = setWebViewZoom, - .set_webview_layer_fn = setWebViewLayer, - .close_webview_fn = closeWebView, + .create_webview_fn = if (webviews_enabled) createWebView else null, + .set_webview_frame_fn = if (webviews_enabled) setWebViewFrame else null, + .navigate_webview_fn = if (webviews_enabled) navigateWebView else null, + .set_webview_zoom_fn = if (webviews_enabled) setWebViewZoom else null, + .set_webview_layer_fn = if (webviews_enabled) setWebViewLayer else null, + .close_webview_fn = if (webviews_enabled) closeWebView else null, .show_open_dialog_fn = showOpenDialog, .show_save_dialog_fn = showSaveDialog, .show_message_dialog_fn = showMessageDialog, @@ -615,10 +623,10 @@ pub const MacPlatform = struct { .update_tray_menu_fn = updateTrayMenu, .update_tray_title_fn = updateTrayTitle, .remove_tray_fn = removeTray, - .configure_security_policy_fn = configureSecurityPolicy, + .configure_security_policy_fn = if (webviews_enabled) configureSecurityPolicy else null, .configure_menus_fn = configureMenus, .configure_shortcuts_fn = configureShortcuts, - .emit_window_event_fn = emitWindowEvent, + .emit_window_event_fn = if (webviews_enabled) emitWindowEvent else null, .start_timer_fn = startTimer, .cancel_timer_fn = cancelTimer, .audio_load_fn = audioLoad, @@ -654,6 +662,7 @@ pub const MacPlatform = struct { return switch (feature) { .main_webview, .child_webviews, + => true, .tray, .shortcuts, .dialogs, @@ -688,6 +697,13 @@ pub const MacPlatform = struct { }; } + fn supportsNativeFeature(context: *anyopaque, feature: platform_mod.PlatformFeature) bool { + return switch (feature) { + .main_webview, .child_webviews => false, + else => supportsFeature(context, feature), + }; + } + fn run(context: *anyopaque, handler: platform_mod.EventHandler, handler_context: *anyopaque) anyerror!void { const self: *MacPlatform = @ptrCast(@alignCast(context)); self.state = .{ @@ -701,6 +717,18 @@ pub const MacPlatform = struct { if (self.state.failed) return error.CallbackFailed; } + fn runNative(context: *anyopaque, handler: platform_mod.EventHandler, handler_context: *anyopaque) anyerror!void { + const self: *MacPlatform = @ptrCast(@alignCast(context)); + self.state = .{ + .self = self, + .handler = handler, + .handler_context = handler_context, + }; + native_sdk_appkit_set_tray_callback(self.host, appkitTrayCallback, &self.state); + native_sdk_appkit_run(self.host, appkitCallback, &self.state); + if (self.state.failed) return error.CallbackFailed; + } + fn windowById(self: *const MacPlatform, window_id: platform_mod.WindowId) platform_mod.WindowOptions { var index: usize = 0; while (index < self.app_info.startupWindowCount()) : (index += 1) { @@ -1117,6 +1145,11 @@ fn windowChrome(context: ?*anyopaque, window_id: platform_mod.WindowId) platform fn createView(context: ?*anyopaque, options: platform_mod.ViewOptions) anyerror!void { if (options.kind == .webview) return createWebView(context, options.webViewOptions()); + return createNativeView(context, options); +} + +fn createNativeView(context: ?*anyopaque, options: platform_mod.ViewOptions) anyerror!void { + if (options.kind == .webview) return error.UnsupportedViewKind; if (!isSupportedNativeViewKind(options.kind)) return error.UnsupportedViewKind; const self: *MacPlatform = @ptrCast(@alignCast(context.?)); if (self.web_engine != .system) return error.UnsupportedViewKind; diff --git a/src/platform/null_platform.zig b/src/platform/null_platform.zig index 7491a3fe6..48107d547 100644 --- a/src/platform/null_platform.zig +++ b/src/platform/null_platform.zig @@ -546,23 +546,31 @@ pub const NullPlatform = struct { } pub fn platform(self: *NullPlatform) Platform { + return self.platformWithWebViews(true); + } + + pub fn nativePlatform(self: *NullPlatform) Platform { + return self.platformWithWebViews(false); + } + + fn platformWithWebViews(self: *NullPlatform, comptime webviews_enabled: bool) Platform { return .{ .context = self, .name = "null", .surface_value = self.surface_value, .run_fn = run, - .supports_fn = supportsFeature, + .supports_fn = if (webviews_enabled) supportsFeature else supportsNativeFeature, .services = .{ .context = self, .read_clipboard_fn = readClipboard, .write_clipboard_fn = writeClipboard, .read_clipboard_data_fn = readClipboardData, .write_clipboard_data_fn = writeClipboardData, - .load_webview_fn = loadWebView, - .load_window_webview_fn = loadWindowWebView, - .complete_bridge_fn = completeBridge, - .complete_window_bridge_fn = completeWindowBridge, - .complete_webview_bridge_fn = completeWebViewBridge, + .load_webview_fn = if (webviews_enabled) loadWebView else null, + .load_window_webview_fn = if (webviews_enabled) loadWindowWebView else null, + .complete_bridge_fn = if (webviews_enabled) completeBridge else null, + .complete_window_bridge_fn = if (webviews_enabled) completeWindowBridge else null, + .complete_webview_bridge_fn = if (webviews_enabled) completeWebViewBridge else null, .create_window_fn = createWindow, .focus_window_fn = focusWindow, .close_window_fn = closeWindow, @@ -570,19 +578,19 @@ pub const NullPlatform = struct { .start_window_drag_fn = startWindowDrag, .window_chrome_fn = windowChrome, .set_window_drag_regions_fn = setWindowDragRegions, - .create_view_fn = createView, + .create_view_fn = if (webviews_enabled) createView else createNativeView, .update_view_fn = updateView, .set_view_frame_fn = setViewFrame, .set_view_visible_fn = setViewVisible, .set_view_cursor_fn = setViewCursor, .focus_view_fn = focusView, .close_view_fn = closeView, - .create_webview_fn = createWebView, - .set_webview_frame_fn = setWebViewFrame, - .navigate_webview_fn = navigateWebView, - .set_webview_zoom_fn = setWebViewZoom, - .set_webview_layer_fn = setWebViewLayer, - .close_webview_fn = closeWebView, + .create_webview_fn = if (webviews_enabled) createWebView else null, + .set_webview_frame_fn = if (webviews_enabled) setWebViewFrame else null, + .navigate_webview_fn = if (webviews_enabled) navigateWebView else null, + .set_webview_zoom_fn = if (webviews_enabled) setWebViewZoom else null, + .set_webview_layer_fn = if (webviews_enabled) setWebViewLayer else null, + .close_webview_fn = if (webviews_enabled) closeWebView else null, .show_open_dialog_fn = showOpenDialog, .show_save_dialog_fn = showSaveDialog, .show_message_dialog_fn = showMessageDialog, @@ -598,10 +606,10 @@ pub const NullPlatform = struct { .reveal_path_fn = revealPath, .add_recent_document_fn = addRecentDocument, .clear_recent_documents_fn = clearRecentDocuments, - .configure_security_policy_fn = configureSecurityPolicy, + .configure_security_policy_fn = if (webviews_enabled) configureSecurityPolicy else null, .configure_menus_fn = configureMenus, .configure_shortcuts_fn = configureShortcuts, - .emit_window_event_fn = emitWindowEvent, + .emit_window_event_fn = if (webviews_enabled) emitWindowEvent else null, .start_timer_fn = startTimer, .cancel_timer_fn = cancelTimer, .audio_load_fn = if (self.audio_playback) audioLoad else null, @@ -632,6 +640,7 @@ pub const NullPlatform = struct { return switch (feature) { .main_webview, .child_webviews, + => true, .native_views, .native_control_commands, .menus, @@ -661,6 +670,13 @@ pub const NullPlatform = struct { }; } + fn supportsNativeFeature(context: *anyopaque, feature: PlatformFeature) bool { + return switch (feature) { + .main_webview, .child_webviews => false, + else => supportsFeature(context, feature), + }; + } + pub fn hostInfo(self: NullPlatform) platform_info.HostInfo { _ = self; const target = platform_info.Target.current(); @@ -898,8 +914,13 @@ pub const NullPlatform = struct { } fn createView(context: ?*anyopaque, options: ViewOptions) anyerror!void { - const self: *NullPlatform = @ptrCast(@alignCast(context.?)); if (options.kind == .webview) return createWebView(context, options.webViewOptions()); + return createNativeView(context, options); + } + + fn createNativeView(context: ?*anyopaque, options: ViewOptions) anyerror!void { + if (options.kind == .webview) return error.UnsupportedViewKind; + const self: *NullPlatform = @ptrCast(@alignCast(context.?)); if (options.kind == .gpu_surface and !self.gpu_surfaces) return error.UnsupportedViewKind; try self.validateViewOptions(options); if (self.findViewIndex(options.window_id, options.label) != null) return error.DuplicateViewLabel; diff --git a/src/platform/null_platform_tests.zig b/src/platform/null_platform_tests.zig index e8ac86e0b..39a609e91 100644 --- a/src/platform/null_platform_tests.zig +++ b/src/platform/null_platform_tests.zig @@ -329,6 +329,29 @@ test "webview bridge fallback only routes main responses" { try std.testing.expectError(error.UnsupportedService, services.completeWebViewBridge(3, "preview", "{\"ok\":true}")); } +test "native-only null platform omits WebView services" { + var null_platform = NullPlatform.init(.{}); + const web_platform = null_platform.platform(); + const platform = null_platform.nativePlatform(); + + try std.testing.expect(web_platform.supports(.main_webview)); + try std.testing.expect(!platform.supports(.main_webview)); + try std.testing.expect(!platform.supports(.child_webviews)); + try std.testing.expect(platform.supports(.native_views)); + try std.testing.expect(platform.services.load_webview_fn == null); + try std.testing.expect(platform.services.load_window_webview_fn == null); + try std.testing.expect(platform.services.complete_bridge_fn == null); + try std.testing.expect(platform.services.complete_window_bridge_fn == null); + try std.testing.expect(platform.services.complete_webview_bridge_fn == null); + try std.testing.expect(platform.services.create_webview_fn == null); + try std.testing.expect(platform.services.configure_security_policy_fn == null); + try std.testing.expect(platform.services.emit_window_event_fn == null); + try std.testing.expectError(error.UnsupportedService, platform.services.loadWebView(.html(""))); + try std.testing.expectError(error.UnsupportedViewKind, platform.services.createView(.{ .label = "browser", .kind = .webview, .url = "https://example.com" })); + try std.testing.expectError(error.UnsupportedService, platform.services.configureSecurityPolicy(.{})); + try std.testing.expect(web_platform.supports(.main_webview)); +} + test "shortcut configuration requires backend support for non-empty lists" { const services = PlatformServices{}; try services.configureShortcuts(&.{}); diff --git a/src/platform/windows/root.zig b/src/platform/windows/root.zig index 734ee044e..756fea358 100644 --- a/src/platform/windows/root.zig +++ b/src/platform/windows/root.zig @@ -256,23 +256,31 @@ pub const WindowsPlatform = struct { } pub fn platform(self: *WindowsPlatform) platform_mod.Platform { + return self.platformWithWebViews(true); + } + + pub fn nativePlatform(self: *WindowsPlatform) platform_mod.Platform { + return self.platformWithWebViews(false); + } + + fn platformWithWebViews(self: *WindowsPlatform, comptime webviews_enabled: bool) platform_mod.Platform { return .{ .context = self, .name = "windows", .surface_value = self.surface_value, - .run_fn = run, - .supports_fn = supportsFeature, + .run_fn = if (webviews_enabled) run else runNative, + .supports_fn = if (webviews_enabled) supportsFeature else supportsNativeFeature, .services = .{ .context = self, .read_clipboard_fn = readClipboard, .write_clipboard_fn = writeClipboard, .read_clipboard_data_fn = readClipboardData, .write_clipboard_data_fn = writeClipboardData, - .load_webview_fn = loadWebView, - .load_window_webview_fn = loadWindowWebView, - .complete_bridge_fn = completeBridge, - .complete_window_bridge_fn = completeWindowBridge, - .complete_webview_bridge_fn = completeWebViewBridge, + .load_webview_fn = if (webviews_enabled) loadWebView else null, + .load_window_webview_fn = if (webviews_enabled) loadWindowWebView else null, + .complete_bridge_fn = if (webviews_enabled) completeBridge else null, + .complete_window_bridge_fn = if (webviews_enabled) completeWindowBridge else null, + .complete_webview_bridge_fn = if (webviews_enabled) completeWebViewBridge else null, .create_window_fn = createWindow, .focus_window_fn = focusWindow, .close_window_fn = closeWindow, @@ -280,7 +288,7 @@ pub const WindowsPlatform = struct { .start_window_drag_fn = startWindowDrag, .set_window_drag_regions_fn = setWindowDragRegions, .window_chrome_fn = windowChrome, - .create_view_fn = createView, + .create_view_fn = if (webviews_enabled) createView else createNativeView, .update_view_fn = updateView, .set_view_frame_fn = setViewFrame, .set_view_visible_fn = setViewVisible, @@ -289,12 +297,12 @@ pub const WindowsPlatform = struct { .request_gpu_surface_frame_fn = requestGpuSurfaceFrame, .note_gpu_surface_input_fn = noteGpuSurfaceInput, .present_gpu_surface_pixels_fn = presentGpuSurfacePixels, - .create_webview_fn = createWebView, - .set_webview_frame_fn = setWebViewFrame, - .navigate_webview_fn = navigateWebView, - .set_webview_zoom_fn = setWebViewZoom, - .set_webview_layer_fn = setWebViewLayer, - .close_webview_fn = closeWebView, + .create_webview_fn = if (webviews_enabled) createWebView else null, + .set_webview_frame_fn = if (webviews_enabled) setWebViewFrame else null, + .navigate_webview_fn = if (webviews_enabled) navigateWebView else null, + .set_webview_zoom_fn = if (webviews_enabled) setWebViewZoom else null, + .set_webview_layer_fn = if (webviews_enabled) setWebViewLayer else null, + .close_webview_fn = if (webviews_enabled) closeWebView else null, .show_open_dialog_fn = showOpenDialog, .show_save_dialog_fn = showSaveDialog, .show_message_dialog_fn = showMessageDialog, @@ -316,10 +324,10 @@ pub const WindowsPlatform = struct { .audio_stop_fn = audioStop, .audio_seek_fn = audioSeek, .audio_set_volume_fn = audioSetVolume, - .configure_security_policy_fn = configureSecurityPolicy, + .configure_security_policy_fn = if (webviews_enabled) configureSecurityPolicy else null, .configure_menus_fn = configureMenus, .configure_shortcuts_fn = configureShortcuts, - .emit_window_event_fn = emitWindowEvent, + .emit_window_event_fn = if (webviews_enabled) emitWindowEvent else null, .start_timer_fn = startTimer, .cancel_timer_fn = cancelTimer, .wake_fn = wake, @@ -335,6 +343,7 @@ pub const WindowsPlatform = struct { return switch (feature) { .main_webview, .child_webviews, + => self.web_engine == .system, .native_views, .native_control_commands, .menus, @@ -367,6 +376,13 @@ pub const WindowsPlatform = struct { }; } + fn supportsNativeFeature(context: *anyopaque, feature: platform_mod.PlatformFeature) bool { + return switch (feature) { + .main_webview, .child_webviews => false, + else => supportsFeature(context, feature), + }; + } + /// Live probe for process-scoped loopback capture (the host asks /// the audio activation API rather than trusting a version check). /// Test builds link no Win32 host, so they answer the honest floor. @@ -388,6 +404,17 @@ pub const WindowsPlatform = struct { if (self.state.failed) return error.CallbackFailed; } + fn runNative(context: *anyopaque, handler: platform_mod.EventHandler, handler_context: *anyopaque) anyerror!void { + const self: *WindowsPlatform = @ptrCast(@alignCast(context)); + self.state = .{ + .self = self, + .handler = handler, + .handler_context = handler_context, + }; + native_sdk_windows_run(self.host, windowsCallback, &self.state); + if (self.state.failed) return error.CallbackFailed; + } + fn windowById(self: *const WindowsPlatform, window_id: platform_mod.WindowId) platform_mod.WindowOptions { var index: usize = 0; while (index < self.app_info.startupWindowCount()) : (index += 1) { @@ -794,6 +821,11 @@ fn cancelTimer(context: ?*anyopaque, id: u64) anyerror!void { fn createView(context: ?*anyopaque, options: platform_mod.ViewOptions) anyerror!void { if (options.kind == .webview) return createWebView(context, options.webViewOptions()); + return createNativeView(context, options); +} + +fn createNativeView(context: ?*anyopaque, options: platform_mod.ViewOptions) anyerror!void { + if (options.kind == .webview) return error.UnsupportedViewKind; if (!isSupportedNativeViewKind(options.kind)) return error.UnsupportedViewKind; const self: *WindowsPlatform = @ptrCast(@alignCast(context.?)); if (self.web_engine != .system) return error.UnsupportedViewKind; diff --git a/src/platform/windows/webview2_host.cpp b/src/platform/windows/webview2_host.cpp index 7ff27d2a9..fe4e50b40 100644 --- a/src/platform/windows/webview2_host.cpp +++ b/src/platform/windows/webview2_host.cpp @@ -27,7 +27,7 @@ #include #include -#if __has_include() && __has_include() +#if !defined(NATIVE_SDK_NATIVE_ONLY) && __has_include() && __has_include() #include #include #define NATIVE_SDK_HAS_WEBVIEW2 1 @@ -39,8 +39,10 @@ using Microsoft::WRL::ComPtr; * the host builds with the embedded WebView layer stubbed out — canvas * apps are unaffected, but apps that load a WebView report * WebViewNotFound at start. */ +#if !defined(NATIVE_SDK_NATIVE_ONLY) #pragma message("WebView2.h not found: building the Windows host without the embedded WebView layer (canvas apps unaffected; WebView loads will report WebViewNotFound)") #endif +#endif /* Media Foundation (the audio backend below) + WinHTTP (the audio cache * fill). initguid.h makes the DEFINE_GUID declarations in the MF headers diff --git a/src/runtime/automation_widget_dispatch.zig b/src/runtime/automation_widget_dispatch.zig index 28733bcd8..e46d57575 100644 --- a/src/runtime/automation_widget_dispatch.zig +++ b/src/runtime/automation_widget_dispatch.zig @@ -70,7 +70,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { }; }; - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = window_id, .label = label, .kind = .pointer_down, @@ -79,7 +79,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { .y = point.y, .button = 0, } }); - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = window_id, .label = label, .kind = .pointer_up, @@ -109,7 +109,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { const label = self.views[view_index].label; const timestamp_ns = automationInputTimestampNs(); - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = window_id, .label = label, .kind = .pointer_down, @@ -118,11 +118,11 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { .y = point.y, .button = 0, } }); - try self.dispatchPlatformEvent(app, .{ .timer = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .timer = .{ .id = platform.press_hold_timer_id, .timestamp_ns = timestamp_ns, } }); - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = window_id, .label = label, .kind = .pointer_up, @@ -152,7 +152,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { const label = self.views[view_index].label; const timestamp_ns = automationInputTimestampNs(); - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = window_id, .label = label, .kind = .pointer_down, @@ -161,7 +161,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { .y = point.y, .button = 1, } }); - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = window_id, .label = label, .kind = .pointer_up, @@ -199,7 +199,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { .token = widget.id, .kind = .app, }; - try self.dispatchPlatformEvent(app, .{ .context_menu_action = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .context_menu_action = .{ .window_id = self.views[view_index].window_id, .view_label = self.views[view_index].label, .token = widget.id, @@ -238,7 +238,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { if (bounds.isEmpty()) return error.WheelTargetHasEmptyBounds; const point = bounds.center(); const timestamp_ns = automationInputTimestampNs(); - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = self.views[view_index].window_id, .label = self.views[view_index].label, .kind = .scroll, @@ -252,7 +252,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { pub fn dispatchAutomationWidgetKeyInput(self: *Runtime, app: runtime_api.App(Runtime), key: AutomationWidgetKey) anyerror!void { const view_index = try automationGpuSurfaceViewIndexByLabel(self, key.view_label); try self.focusView(self.views[view_index].window_id, self.views[view_index].label); - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = self.views[view_index].window_id, .label = self.views[view_index].label, .kind = .key_down, @@ -286,7 +286,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { ); const timestamp_ns = automationInputTimestampNs(); - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = self.views[view_index].window_id, .label = self.views[view_index].label, .kind = .pointer_down, @@ -295,7 +295,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { .y = start.y, .button = 0, } }); - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = self.views[view_index].window_id, .label = self.views[view_index].label, .kind = .pointer_drag, @@ -306,7 +306,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { .delta_y = end.y - start.y, .button = 0, } }); - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = self.views[view_index].window_id, .label = self.views[view_index].label, .kind = .pointer_up, @@ -376,7 +376,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { try CanvasWidgetEventMethods().invalidateForCanvasWidgetRenderStateChange(self, view_index, previous_state, self.views[view_index].canvasWidgetRenderState()); } } - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = self.views[view_index].window_id, .label = self.views[view_index].label, .kind = .key_down, @@ -422,7 +422,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { // Select all (the platform primary shortcut), exactly like a // user pressing cmd/ctrl+a in the focused field. - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = window_id, .label = label, .kind = .key_down, @@ -432,7 +432,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { } }); if (text.len == 0) { // Empty replacement: delete the selection. - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = window_id, .label = label, .kind = .key_down, @@ -441,7 +441,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { } }); return; } - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = window_id, .label = label, .kind = .text_input, @@ -483,7 +483,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { } } - try self.dispatchPlatformEvent(app, .{ .gpu_surface_input = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .gpu_surface_input = .{ .window_id = window_id, .label = label, .kind = .pointer_drag, @@ -512,7 +512,7 @@ pub fn RuntimeAutomationWidgetDispatch(comptime Runtime: type) type { const bounds = node.frame.normalized(); if (bounds.isEmpty()) return error.InvalidCommand; - try self.dispatchPlatformEvent(app, .{ .files_dropped = .{ + try self.dispatchNonBridgePlatformEvent(app, .{ .files_dropped = .{ .window_id = self.views[view_index].window_id, .view_label = self.views[view_index].label, .point = bounds.center(), diff --git a/src/runtime/core.zig b/src/runtime/core.zig index cb610acd0..c86bd2c32 100644 --- a/src/runtime/core.zig +++ b/src/runtime/core.zig @@ -446,9 +446,11 @@ pub const Runtime = struct { const FlowMethods = runtime_flow.RuntimeFlow(Runtime); pub const run = FlowMethods.run; + pub const runNative = FlowMethods.runNative; pub const emitWindowEvent = FlowMethods.emitWindowEvent; pub const respondToBridge = FlowMethods.respondToBridge; pub const dispatchPlatformEvent = FlowMethods.dispatchPlatformEvent; + pub const dispatchNonBridgePlatformEvent = FlowMethods.dispatchNonBridgePlatformEvent; pub const dispatchEvent = FlowMethods.dispatchEvent; pub const dispatchCommand = FlowMethods.dispatchCommand; pub const frame = FlowMethods.frame; diff --git a/src/runtime/flow.zig b/src/runtime/flow.zig index 00309c4e6..335d7fa6a 100644 --- a/src/runtime/flow.zig +++ b/src/runtime/flow.zig @@ -81,6 +81,14 @@ pub fn RuntimeFlow(comptime Runtime: type) type { } pub fn run(self: *Runtime, app: App) anyerror!void { + return runWithBridges(self, app, true); + } + + pub fn runNative(self: *Runtime, app: App) anyerror!void { + return runWithBridges(self, app, false); + } + + fn runWithBridges(self: *Runtime, app: App, comptime web_bridges_enabled: bool) anyerror!void { var init_fields: [3]trace.Field = undefined; init_fields[0] = trace.string("app", app.name); init_fields[1] = trace.string("platform", self.options.platform.name); @@ -91,7 +99,11 @@ pub fn RuntimeFlow(comptime Runtime: type) type { } log(self, "runtime.init", "runtime initialized", init_fields[0..init_field_count]); try app_manifest.validateCommands(self.options.commands); - try self.options.platform.services.configureSecurityPolicy(self.options.security); + if (self.options.platform.services.configure_security_policy_fn) |configure_fn| { + try configure_fn(self.options.platform.services.context, self.options.security); + } else if (self.options.platform.supports(.main_webview) or self.options.platform.supports(.child_webviews)) { + return error.UnsupportedService; + } try self.options.platform.services.configureMenus(self.options.menus); try self.options.platform.services.configureShortcuts(self.options.shortcuts); // Automation liveness: the drain (`consumeAutomationCommand`, @@ -152,7 +164,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type { }; var context: RunContext = .{ .runtime = self, .app = app }; - try self.options.platform.run(handlePlatformEvent, &context); + try self.options.platform.run(if (web_bridges_enabled) handlePlatformEvent else handleNativePlatformEvent, &context); log(self, "runtime.done", "runtime finished", &.{}); } @@ -179,6 +191,14 @@ pub fn RuntimeFlow(comptime Runtime: type) type { } pub fn dispatchPlatformEvent(self: *Runtime, app: App, event_value: platform.Event) anyerror!void { + return dispatchPlatformEventWithBridges(self, app, event_value, true); + } + + pub fn dispatchNonBridgePlatformEvent(self: *Runtime, app: App, event_value: platform.Event) anyerror!void { + return dispatchPlatformEventWithBridges(self, app, event_value, false); + } + + fn dispatchPlatformEventWithBridges(self: *Runtime, app: App, event_value: platform.Event, comptime web_bridges_enabled: bool) anyerror!void { // Session recording: stage the event on entry, commit it on // exit — so effect results drained DURING dispatch precede // the event record in the journal (replay feeds them before @@ -308,8 +328,14 @@ pub fn RuntimeFlow(comptime Runtime: type) type { if (WindowViewMethods().findWindowIndexById(self, window_id)) |index| WindowViewMethods().setFocusedIndex(self, index); self.invalidated = true; }, - .frame_requested => try frame(self, app), - .bridge_message => |message| try handleBridgeMessage(self, app, message), + .frame_requested => try frameWithBridges(self, app, web_bridges_enabled), + .bridge_message => |message| { + if (comptime web_bridges_enabled) { + try handleBridgeMessage(self, app, message); + } else { + return error.UnsupportedService; + } + }, .tray_action => |item_id| { log(self, "tray.action", "tray item selected", &.{trace.uint("item_id", item_id)}); try dispatchCommand(self, app, .{ @@ -499,8 +525,12 @@ pub fn RuntimeFlow(comptime Runtime: type) type { } pub fn frame(self: *Runtime, app: App) anyerror!void { + return frameWithBridges(self, app, true); + } + + fn frameWithBridges(self: *Runtime, app: App, comptime web_bridges_enabled: bool) anyerror!void { const start_ns = nowNanoseconds(); - try consumeAutomationCommand(self, app); + try consumeAutomationCommand(self, app, web_bridges_enabled); if (!self.invalidated) return; try publishAutomation(self); @@ -534,7 +564,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type { } pub fn dispatchAutomationCommand(self: *Runtime, app: App, line: []const u8) anyerror!void { - try dispatchAutomationProtocolCommand(self, app, try automation.protocol.Command.parse(line)); + try dispatchAutomationProtocolCommand(self, app, try automation.protocol.Command.parse(line), true); } pub fn frameDiagnostics(self: *Runtime) FrameDiagnostics { @@ -547,7 +577,12 @@ pub fn RuntimeFlow(comptime Runtime: type) type { fn handlePlatformEvent(context: *anyopaque, event_value: platform.Event) anyerror!void { const run_context: *RunContext = @ptrCast(@alignCast(context)); - try run_context.runtime.dispatchPlatformEvent(run_context.app, event_value); + try dispatchPlatformEventWithBridges(run_context.runtime, run_context.app, event_value, true); + } + + fn handleNativePlatformEvent(context: *anyopaque, event_value: platform.Event) anyerror!void { + const run_context: *RunContext = @ptrCast(@alignCast(context)); + try dispatchPlatformEventWithBridges(run_context.runtime, run_context.app, event_value, false); } fn loadStartupWindows(self: *Runtime, app: App) anyerror!void { @@ -752,7 +787,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type { try server.publish(automationSnapshot(self, server.title)); } - fn consumeAutomationCommand(self: *Runtime, app: App) anyerror!void { + fn consumeAutomationCommand(self: *Runtime, app: App, comptime web_bridges_enabled: bool) anyerror!void { const server = self.options.automation orelse return; var buffer: [automation.protocol.max_command_bytes]u8 = undefined; // Automation command errors ALWAYS degrade, regardless of @@ -767,7 +802,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type { recordDispatchError(self, "automation.command", err); return; }) orelse return; - dispatchAutomationProtocolCommand(self, app, command) catch |err| { + dispatchAutomationProtocolCommand(self, app, command, web_bridges_enabled) catch |err| { recordDispatchErrorDetail(self, automationCommandEventName(command.action), err, command.value); }; } @@ -791,7 +826,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type { }; } - fn dispatchAutomationProtocolCommand(self: *Runtime, app: App, command: automation.protocol.Command) anyerror!void { + fn dispatchAutomationProtocolCommand(self: *Runtime, app: App, command: automation.protocol.Command, comptime web_bridges_enabled: bool) anyerror!void { switch (command.action) { .reload => { self.command_count += 1; @@ -799,15 +834,19 @@ pub fn RuntimeFlow(comptime Runtime: type) type { self.invalidateFor(.command, null); }, .bridge => { - try handleBridgeMessage(self, app, .{ .bytes = command.value, .origin = "zero://inline", .window_id = 1, .webview_label = "main" }); + if (comptime web_bridges_enabled) { + try handleBridgeMessage(self, app, .{ .bytes = command.value, .origin = "zero://inline", .window_id = 1, .webview_label = "main" }); + } else { + return error.UnsupportedService; + } }, .resize => { const parsed = try parseAutomationResizeCommand(command.value); - try dispatchPlatformEvent(self, app, .{ .surface_resized = .{ + try dispatchPlatformEventWithBridges(self, app, .{ .surface_resized = .{ .id = 1, .size = geometry.SizeF.init(parsed.width, parsed.height), .scale_factor = parsed.scale_factor, - } }); + } }, web_bridges_enabled); }, .screenshot => { publishAutomationScreenshot(self, command.value) catch |err| { @@ -816,11 +855,11 @@ pub fn RuntimeFlow(comptime Runtime: type) type { }, .native_command => { const parsed = try parseAutomationNativeCommand(command.value); - try dispatchPlatformEvent(self, app, .{ .native_command = .{ + try dispatchPlatformEventWithBridges(self, app, .{ .native_command = .{ .name = parsed.name, .window_id = 1, .view_label = parsed.view_label, - } }); + } }, web_bridges_enabled); }, .widget_action => { try AutomationWidgetMethods().dispatchAutomationWidgetAction(self, app, try parseAutomationWidgetAction(command.value)); @@ -847,17 +886,17 @@ pub fn RuntimeFlow(comptime Runtime: type) type { try AutomationWidgetMethods().dispatchAutomationWidgetKeyInput(self, app, try parseAutomationWidgetKey(command.value)); }, .menu_command => { - try dispatchPlatformEvent(self, app, .{ .menu_command = .{ + try dispatchPlatformEventWithBridges(self, app, .{ .menu_command = .{ .name = try parseAutomationCommandName(command.value), .window_id = 1, - } }); + } }, web_bridges_enabled); }, .shortcut => { - try dispatchPlatformEvent(self, app, .{ .shortcut = .{ + try dispatchPlatformEventWithBridges(self, app, .{ .shortcut = .{ .id = try parseAutomationCommandName(command.value), .key = "", .window_id = 1, - } }); + } }, web_bridges_enabled); }, .tray_action => { // Drive a status-item dropdown row through the @@ -868,7 +907,7 @@ pub fn RuntimeFlow(comptime Runtime: type) type { // like widget-click on an unmounted widget. const item_id = try parseAutomationTrayItemId(command.value); if (!self.tray_created or !SystemServiceMethods().trayItemExists(self, item_id)) return error.InvalidCommand; - try dispatchPlatformEvent(self, app, .{ .tray_action = item_id }); + try dispatchPlatformEventWithBridges(self, app, .{ .tray_action = item_id }, web_bridges_enabled); }, .focus_view => { try WindowViewMethods().focusView(self, 1, try parseAutomationViewLabel(command.value)); diff --git a/src/runtime/platform_config_tests.zig b/src/runtime/platform_config_tests.zig index 67c74ff27..1d11af4cf 100644 --- a/src/runtime/platform_config_tests.zig +++ b/src/runtime/platform_config_tests.zig @@ -65,6 +65,44 @@ const builtinBridgeErrorMessage = support.builtinBridgeErrorMessage; const testViewByLabel = support.testViewByLabel; const testCanvasWidgetPartId = support.testCanvasWidgetPartId; +test "native-only null runtime completes startup without a browser security hook" { + const TestApp = struct { + const views = [_]app_manifest.ShellView{.{ + .label = "main-canvas", + .kind = .gpu_surface, + .fill = true, + }}; + const windows = [_]app_manifest.ShellWindow{.{ + .label = "main", + .views = &views, + }}; + + fn scene(context: *anyopaque) anyerror!app_manifest.ShellConfig { + _ = context; + return .{ .windows = &windows }; + } + }; + + var app_context: u8 = 0; + var null_platform = platform.NullPlatform.init(.{}); + null_platform.gpu_surfaces = true; + const runtime = try std.testing.allocator.create(Runtime); + defer std.testing.allocator.destroy(runtime); + Runtime.initAt(runtime, .{ + .platform = null_platform.nativePlatform(), + .security = .{ .permissions = &.{security.permission_view} }, + }); + + try runtime.run(.{ + .context = &app_context, + .name = "native-runtime-smoke", + .scene_fn = TestApp.scene, + }); + + try std.testing.expectEqual(@as(usize, 1), runtime.view_count); + try std.testing.expectEqual(@as(u64, 1), runtime.frame_index); +} + test "runtime dispatches shortcut command events" { const TestApp = struct { command_count: u32 = 0, diff --git a/src/runtime/root.zig b/src/runtime/root.zig index dc1c17200..83aab6b57 100644 --- a/src/runtime/root.zig +++ b/src/runtime/root.zig @@ -109,6 +109,7 @@ pub const SessionHeader = runtime_session_journal.Header; pub const sessionHeaderNow = runtime_session_record.headerNow; pub const sessionPlatformName = runtime_session_replay.currentPlatformName; pub const replaySession = runtime_session_replay.replaySession; +pub const replayNativeSession = runtime_session_replay.replayNativeSession; pub const ReplayOptions = runtime_session_replay.ReplayOptions; pub const ReplayReport = runtime_session_replay.ReplayReport; pub const ReplayMismatch = runtime_session_replay.ReplayMismatch; diff --git a/src/runtime/session_replay.zig b/src/runtime/session_replay.zig index 99cf3ca48..e2b20d57f 100644 --- a/src/runtime/session_replay.zig +++ b/src/runtime/session_replay.zig @@ -116,6 +116,25 @@ pub fn replaySession( app: core.App, journal_bytes: []const u8, options: ReplayOptions, +) anyerror!ReplayReport { + return replaySessionWithBridges(runtime, app, journal_bytes, options, true); +} + +pub fn replayNativeSession( + runtime: *core.Runtime, + app: core.App, + journal_bytes: []const u8, + options: ReplayOptions, +) anyerror!ReplayReport { + return replaySessionWithBridges(runtime, app, journal_bytes, options, false); +} + +fn replaySessionWithBridges( + runtime: *core.Runtime, + app: core.App, + journal_bytes: []const u8, + options: ReplayOptions, + comptime web_bridges_enabled: bool, ) anyerror!ReplayReport { var reader = try journal.Reader.init(journal_bytes); var report: ReplayReport = .{}; @@ -149,7 +168,11 @@ pub fn replaySession( // below fails loudly instead. }; } - try runtime.dispatchPlatformEvent(app, event); + if (comptime web_bridges_enabled) { + try runtime.dispatchPlatformEvent(app, event); + } else { + try runtime.dispatchNonBridgePlatformEvent(app, event); + } report.events_replayed += 1; }, .effect => |effect| { diff --git a/src/tooling/manifest.zig b/src/tooling/manifest.zig index ee10795ca..6a2897154 100644 --- a/src/tooling/manifest.zig +++ b/src/tooling/manifest.zig @@ -22,6 +22,7 @@ pub const Metadata = struct { platforms: []const []const u8 = &.{}, permissions: []const []const u8 = &.{}, capabilities: []const []const u8 = &.{}, + host: []const u8 = "webview", bridge_commands: []const BridgeCommandMetadata = &.{}, web_engine: []const u8 = "system", /// The built-in theme pack the app selects (`theme = "geist"`). @@ -51,6 +52,7 @@ pub const Metadata = struct { if (self.description) |value| allocator.free(value); if (self.theme) |value| allocator.free(value); allocator.free(self.version); + allocator.free(self.host); allocator.free(self.web_engine); allocator.free(self.cef.dir); for (self.icons) |value| allocator.free(value); @@ -388,6 +390,7 @@ pub fn validateFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8) defer allocator.free(windows); const shell = parseShell(allocator, metadata.shell) catch return .{ .ok = false, .message = "app.zon shell is invalid" }; defer deinitParsedShell(allocator, shell); + if (hostValidationError(metadata)) |message| return .{ .ok = false, .message = message }; const commands = parseCommands(allocator, metadata.commands) catch return .{ .ok = false, .message = "app.zon commands are invalid" }; defer allocator.free(commands); const menus = parseMenus(allocator, metadata.menus) catch return .{ .ok = false, .message = "app.zon menus are invalid" }; @@ -472,6 +475,7 @@ pub fn parseText(allocator: std.mem.Allocator, source: []const u8) !Metadata { .platforms = try duplicateStringList(allocator, raw.platforms), .permissions = try duplicateStringList(allocator, raw.permissions), .capabilities = try duplicateStringList(allocator, raw.capabilities), + .host = try allocator.dupe(u8, raw.host), .bridge_commands = try convertRawBridgeCommands(allocator, raw.bridge.commands), .web_engine = try allocator.dupe(u8, raw.web_engine), .cef = .{ @@ -490,6 +494,37 @@ pub fn parseText(allocator: std.mem.Allocator, source: []const u8) !Metadata { }; } +const Host = enum { webview, native }; + +fn parseHost(value: []const u8) ?Host { + if (std.mem.eql(u8, value, "webview")) return .webview; + if (std.mem.eql(u8, value, "native")) return .native; + return null; +} + +pub fn hostValidationError(metadata: Metadata) ?[]const u8 { + const host = parseHost(metadata.host) orelse return "app.zon host is invalid - expected one of: webview, native"; + if (host != .native) return null; + if (metadata.frontend != null) return "app.zon native host cannot declare frontend content"; + if (hasString(metadata.capabilities, "webview")) return "app.zon native host cannot declare the webview capability"; + for (metadata.shell.windows) |window| { + for (window.views) |view| { + if (std.mem.eql(u8, view.kind, "webview")) return "app.zon native host cannot declare webview shell views"; + } + } + if (hasString(metadata.capabilities, "js_bridge") or metadata.bridge_commands.len > 0) return "app.zon native host cannot declare JavaScript bridge content"; + if (!std.mem.eql(u8, metadata.web_engine, "system")) return "app.zon native host cannot select a web engine"; + if (metadata.cef.auto_install or !std.mem.eql(u8, metadata.cef.dir, web_engine_tool.default_cef_dir)) return "app.zon native host cannot configure CEF"; + return null; +} + +fn hasString(values: []const []const u8, expected: []const u8) bool { + for (values) |value| { + if (std.mem.eql(u8, value, expected)) return true; + } + return false; +} + fn duplicateOptionalString(allocator: std.mem.Allocator, value: ?[]const u8) !?[]const u8 { return if (value) |payload| try allocator.dupe(u8, payload) else null; } @@ -1813,6 +1848,107 @@ test "manifest metadata parser reads frontend config" { try std.testing.expectEqual(@as(u32, 12000), metadata.frontend.?.dev.?.timeout_ms); } +test "manifest metadata parser keeps webview hosting by default and reads native hosting" { + const default_metadata = try parseText(std.testing.allocator, + \\.{ + \\ .id = "com.example.default", + \\ .name = "default", + \\ .version = "1.0.0", + \\} + ); + defer default_metadata.deinit(std.testing.allocator); + try std.testing.expectEqualStrings("webview", default_metadata.host); + + const native_metadata = try parseText(std.testing.allocator, + \\.{ + \\ .id = "com.example.native", + \\ .name = "native", + \\ .version = "1.0.0", + \\ .host = "native", + \\} + ); + defer native_metadata.deinit(std.testing.allocator); + try std.testing.expectEqualStrings("native", native_metadata.host); +} + +test "native host validation rejects web content declarations" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + var cwd = std.Io.Dir.cwd(); + const root = ".zig-cache/test-native-host-manifest"; + try cwd.deleteTree(std.testing.io, root); + defer cwd.deleteTree(std.testing.io, root) catch {}; + try cwd.createDirPath(std.testing.io, root); + + const cases = [_]struct { + source: []const u8, + message: []const u8, + }{ + .{ + .source = + \\.{ .id = "dev.example.app", .name = "demo", .version = "1.0.0", .host = "browser" } + , + .message = "app.zon host is invalid - expected one of: webview, native", + }, + .{ + .source = + \\.{ .id = "dev.example.app", .name = "demo", .version = "1.0.0", .host = "native", .frontend = .{} } + , + .message = "app.zon native host cannot declare frontend content", + }, + .{ + .source = + \\.{ .id = "dev.example.app", .name = "demo", .version = "1.0.0", .host = "native", .capabilities = .{ "webview" } } + , + .message = "app.zon native host cannot declare the webview capability", + }, + .{ + .source = + \\.{ .id = "dev.example.app", .name = "demo", .version = "1.0.0", .host = "native", .shell = .{ .windows = .{ .{ .views = .{ .{ .label = "main", .kind = "webview" } } } } } } + , + .message = "app.zon native host cannot declare webview shell views", + }, + .{ + .source = + \\.{ .id = "dev.example.app", .name = "demo", .version = "1.0.0", .host = "native", .capabilities = .{ "js_bridge" } } + , + .message = "app.zon native host cannot declare JavaScript bridge content", + }, + .{ + .source = + \\.{ .id = "dev.example.app", .name = "demo", .version = "1.0.0", .host = "native", .bridge = .{ .commands = .{ .{ .name = "native.ping" } } } } + , + .message = "app.zon native host cannot declare JavaScript bridge content", + }, + .{ + .source = + \\.{ .id = "dev.example.app", .name = "demo", .version = "1.0.0", .host = "native", .web_engine = "chromium" } + , + .message = "app.zon native host cannot select a web engine", + }, + .{ + .source = + \\.{ .id = "dev.example.app", .name = "demo", .version = "1.0.0", .host = "native", .cef = .{ .auto_install = true } } + , + .message = "app.zon native host cannot configure CEF", + }, + .{ + .source = + \\.{ .id = "dev.example.app", .name = "demo", .version = "1.0.0", .host = "native", .cef = .{ .dir = "vendor/cef" } } + , + .message = "app.zon native host cannot configure CEF", + }, + }; + + for (cases) |case| { + try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data = case.source }); + const result = try validateFile(allocator, std.testing.io, root ++ "/app.zon"); + try std.testing.expect(!result.ok); + try std.testing.expectEqualStrings(case.message, result.message); + } +} + test "validate surfaces the non-square icon teaching error with dimensions" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); diff --git a/src/tooling/package.zig b/src/tooling/package.zig index a082d9a3e..72704a62c 100644 --- a/src/tooling/package.zig +++ b/src/tooling/package.zig @@ -66,6 +66,7 @@ pub const PackageOptions = struct { frontend: ?manifest_tool.FrontendMetadata = null, web_engine: WebEngine = .system, cef_dir: []const u8 = web_engine_tool.default_cef_dir, + cef_override: bool = false, signing: SigningConfig = .{}, archive: bool = false, /// The process environment, when the caller has one (the CLI). The @@ -97,6 +98,10 @@ pub fn artifactName(buffer: []u8, metadata: manifest_tool.Metadata, target: Pack } pub fn createPackage(allocator: std.mem.Allocator, io: std.Io, options: PackageOptions) !PackageStats { + if (manifest_tool.hostValidationError(options.metadata) != null) return error.InvalidManifest; + if (std.mem.eql(u8, options.metadata.host, "native") and (options.frontend != null or options.web_engine != .system or options.cef_override)) { + return error.InvalidNativeHostPackage; + } try validateWebEngineTarget(options.target, options.web_engine); var stats = switch (options.target) { .macos => try createMacosApp(allocator, io, options), @@ -149,6 +154,47 @@ pub fn createLocalPackage(io: std.Io, output_path: []const u8) !PackageStats { }); } +test "native host package options reject browser staging" { + const metadata: manifest_tool.Metadata = .{ + .id = "dev.native_sdk.native_package", + .name = "native-package", + .version = "1.0.0", + .host = "native", + }; + const frontend: manifest_tool.FrontendMetadata = .{}; + + try std.testing.expectError(error.InvalidNativeHostPackage, createPackage(std.testing.allocator, std.testing.io, .{ + .metadata = metadata, + .output_path = ".zig-cache/native-package-frontend", + .frontend = frontend, + })); + try std.testing.expectError(error.InvalidNativeHostPackage, createPackage(std.testing.allocator, std.testing.io, .{ + .metadata = metadata, + .output_path = ".zig-cache/native-package-chromium", + .web_engine = .chromium, + })); + try std.testing.expectError(error.InvalidNativeHostPackage, createPackage(std.testing.allocator, std.testing.io, .{ + .metadata = metadata, + .output_path = ".zig-cache/native-package-cef", + .cef_override = true, + })); +} + +test "native host package rejects invalid manifest metadata" { + const metadata: manifest_tool.Metadata = .{ + .id = "dev.native_sdk.invalid_native_package", + .name = "invalid-native-package", + .version = "1.0.0", + .host = "native", + .capabilities = &.{"js_bridge"}, + }; + + try std.testing.expectError(error.InvalidManifest, createPackage(std.testing.allocator, std.testing.io, .{ + .metadata = metadata, + .output_path = ".zig-cache/native-package-invalid-manifest", + })); +} + pub fn createMacosApp(allocator: std.mem.Allocator, io: std.Io, options: PackageOptions) !PackageStats { var cwd = std.Io.Dir.cwd(); try cwd.createDirPath(io, options.output_path); @@ -1670,7 +1716,6 @@ test "mobile package templates ship the toolkit hosts" { try std.testing.expect(std.mem.indexOf(u8, android_bridge, "WINDOW_FORMAT_RGBA_8888") != null); } - test "mobile package artifacts use manifest identity metadata" { var cwd = std.Io.Dir.cwd(); try cwd.deleteTree(std.testing.io, ".zig-cache/test-package-mobile-identity"); diff --git a/src/tooling/raw_manifest.zig b/src/tooling/raw_manifest.zig index e91fc7c3f..4d143fcfe 100644 --- a/src/tooling/raw_manifest.zig +++ b/src/tooling/raw_manifest.zig @@ -10,6 +10,7 @@ pub const RawManifest = struct { platforms: []const []const u8 = &.{}, permissions: []const []const u8 = &.{}, capabilities: []const []const u8 = &.{}, + host: []const u8 = "webview", bridge: RawBridge = .{}, web_engine: []const u8 = @tagName(web_engine.default_engine), theme: ?[]const u8 = null, diff --git a/src/tooling/templates.zig b/src/tooling/templates.zig index d4b4708a2..10a906da7 100644 --- a/src/tooling/templates.zig +++ b/src/tooling/templates.zig @@ -390,7 +390,6 @@ fn nativeMainZig(allocator: std.mem.Allocator, names: TemplateNames) ![]const u8 \\ .js_window_api = false, \\ .security = .{ \\ .permissions = &app_permissions, - \\ .navigation = .{ .allowed_origins = &.{ "zero://inline", "zero://app" } }, \\ }, \\ }, init); \\} @@ -561,6 +560,7 @@ fn nativeAppZon(allocator: std.mem.Allocator, names: TemplateNames) ![]const u8 \\, \\ .description = "A counter that lives in one native window.", \\ .version = "0.1.0", + \\ .host = "native", \\ .icons = .{"assets/icon.png"}, \\ .platforms = .{"macos"}, \\ .permissions = .{ "view", "command" }, @@ -585,14 +585,6 @@ fn nativeAppZon(allocator: std.mem.Allocator, names: TemplateNames) ![]const u8 \\ }, \\ }, \\ }, - \\ .security = .{ - \\ .navigation = .{ - \\ .allowed_origins = .{ "zero://app", "zero://inline" }, - \\ .external_links = .{ .action = "deny" }, - \\ }, - \\ }, - \\ .web_engine = "system", - \\ .cef = .{ .dir = "third_party/cef/macos", .auto_install = false }, \\} \\ ); @@ -716,7 +708,7 @@ fn nativeCiYaml(allocator: std.mem.Allocator, names: TemplateNames, framework_pa \\ with: \\ version: 0.16.0 \\ - name: Install GTK and Xvfb - \\ run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev xvfb + \\ run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev xvfb \\ - name: Fetch native-sdk \\ run: | \\ if [ ! -f "$NATIVE_SDK_PATH/build.zig" ]; then @@ -728,7 +720,7 @@ fn nativeCiYaml(allocator: std.mem.Allocator, names: TemplateNames, framework_pa \\ run: | \\ set -euo pipefail \\ cli="$NATIVE_SDK_PATH/zig-out/bin/native" - \\ zig build -Dplatform=linux -Dweb-engine=system -Dautomation=true + \\ zig build -Dplatform=linux -Dautomation=true \\ rm -rf .zig-cache/native-sdk-automation \\ xvfb-run -a ./zig-out/bin/ ); @@ -2916,6 +2908,9 @@ test "writeDefaultApp emits a slim native scaffold by default" { try std.testing.expectError(error.FileNotFound, readTestFile(std.testing.allocator, std.testing.io, destination, ".github/workflows/ci.yml")); try std.testing.expect(std.mem.indexOf(u8, app_zon_text, "gpu_surface") != null); + try std.testing.expect(std.mem.indexOf(u8, app_zon_text, ".host = \"native\"") != null); + try std.testing.expect(std.mem.indexOf(u8, app_zon_text, ".navigation") == null); + try std.testing.expect(std.mem.indexOf(u8, main_zig_text, ".navigation") == null); try std.testing.expect(std.mem.indexOf(u8, main_zig_text, "native_sdk.UiApp(Model, Msg)") != null); // The generated + derived state is ignored wholesale. try std.testing.expect(std.mem.indexOf(u8, gitignore_text, ".native/") != null); @@ -2957,9 +2952,12 @@ test "writeDefaultApp emits native project files" { try std.testing.expectError(error.FileNotFound, readTestFile(std.testing.allocator, std.testing.io, destination, "src/runner.zig")); try std.testing.expect(std.mem.indexOf(u8, app_zon_text, "gpu_surface") != null); + try std.testing.expect(std.mem.indexOf(u8, app_zon_text, ".host = \"native\"") != null); try std.testing.expect(std.mem.indexOf(u8, app_zon_text, "\"native_views\", \"gpu_surfaces\"") != null); try std.testing.expect(std.mem.indexOf(u8, app_zon_text, "dev.native_sdk.my-app") != null); try std.testing.expect(std.mem.indexOf(u8, app_zon_text, ".frontend") == null); + try std.testing.expect(std.mem.indexOf(u8, app_zon_text, ".navigation") == null); + try std.testing.expect(std.mem.indexOf(u8, main_zig_text, ".navigation") == null); try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "native_sdk.addApp(b, b.dependency(\"native_sdk\", .{}), .{ .name = \"my-app\" })") != null); try std.testing.expect(std.mem.indexOf(u8, build_zon_text, ".native_sdk = .{ .path = ") != null); try std.testing.expect(std.mem.indexOf(u8, build_zon_text, ".name = .my_app") != null); @@ -2992,8 +2990,9 @@ test "writeDefaultApp emits a CI workflow for native apps" { try std.testing.expect(std.mem.indexOf(u8, ci_yaml_text, "zig build test -Dplatform=null") != null); // The smoke job builds with automation, launches under Xvfb, and drives // the snapshot: the binary name comes from the template context. - try std.testing.expect(std.mem.indexOf(u8, ci_yaml_text, "libgtk-4-dev libwebkitgtk-6.0-dev xvfb") != null); - try std.testing.expect(std.mem.indexOf(u8, ci_yaml_text, "zig build -Dplatform=linux -Dweb-engine=system -Dautomation=true") != null); + try std.testing.expect(std.mem.indexOf(u8, ci_yaml_text, "libgtk-4-dev xvfb") != null); + try std.testing.expect(std.mem.indexOf(u8, ci_yaml_text, "libwebkitgtk") == null); + try std.testing.expect(std.mem.indexOf(u8, ci_yaml_text, "zig build -Dplatform=linux -Dautomation=true") != null); try std.testing.expect(std.mem.indexOf(u8, ci_yaml_text, "xvfb-run -a ./zig-out/bin/my-app &") != null); try std.testing.expect(std.mem.indexOf(u8, ci_yaml_text, "automate wait") != null); try std.testing.expect(std.mem.indexOf(u8, ci_yaml_text, "automate assert 'gpu_nonblank=true' 'role=button name=\"Reset\"' 'count: 0'") != null); diff --git a/tests/fixtures/native-only-browser-headers/WebView2.h b/tests/fixtures/native-only-browser-headers/WebView2.h new file mode 100644 index 000000000..9e0dc4870 --- /dev/null +++ b/tests/fixtures/native-only-browser-headers/WebView2.h @@ -0,0 +1 @@ +#error "native-only Windows builds must not include WebView2.h" diff --git a/tools/native-sdk/main.zig b/tools/native-sdk/main.zig index ee746b58d..b87dede49 100644 --- a/tools/native-sdk/main.zig +++ b/tools/native-sdk/main.zig @@ -152,6 +152,17 @@ pub fn main(init: std.process.Init) !void { .bool_flags = &.{ "--cef-auto-install", "--archive" }, }); const manifest_path = try flagValue(args, "--manifest") orelse "app.zon"; + const validation = tooling.manifest.validateFile(allocator, init.io, manifest_path) catch |err| switch (err) { + error.FileNotFound => { + std.debug.print("error: {s} not found - run this from your app's root (the folder containing app.zon), or pass --manifest \n", .{manifest_path}); + std.process.exit(1); + }, + else => return err, + }; + if (!validation.ok) { + tooling.manifest.printDiagnostic(validation); + std.process.exit(1); + } const metadata = tooling.manifest.readMetadata(allocator, init.io, manifest_path) catch |err| switch (err) { error.FileNotFound => { std.debug.print("error: {s} not found - run this from your app's root (the folder containing app.zon), or pass --manifest \n", .{manifest_path}); @@ -165,10 +176,16 @@ pub fn main(init: std.process.Init) !void { tooling.web_engine.Engine.parse(value) orelse fail("invalid web engine") else null; + const cef_dir_override = try flagValue(args, "--cef-dir"); + const cef_auto_install_override = flagBool(args, "--cef-auto-install"); + const native_host = std.mem.eql(u8, metadata.host, "native"); + if (native_host and (web_engine_override == .chromium or cef_dir_override != null or cef_auto_install_override)) { + fail("host=native cannot use Chromium or CEF package overrides"); + } const web_engine = try tooling.web_engine.resolve(.{ .web_engine = metadata.web_engine, .cef = metadata.cef }, .{ .web_engine = web_engine_override, - .cef_dir = try flagValue(args, "--cef-dir"), - .cef_auto_install = if (flagBool(args, "--cef-auto-install")) true else null, + .cef_dir = cef_dir_override, + .cef_auto_install = if (cef_auto_install_override) true else null, }); const signing_name = try flagValue(args, "--signing") orelse "none"; const signing = tooling.package.SigningMode.parse(signing_name) orelse fail("invalid signing mode"); @@ -205,10 +222,11 @@ pub fn main(init: std.process.Init) !void { .optimize = optimize_value, .output_path = output_dir, .binary_path = binary_path, - .assets_dir = try flagValue(args, "--assets") orelse if (metadata.frontend) |frontend| frontend.dist else "assets", - .frontend = metadata.frontend, + .assets_dir = try flagValue(args, "--assets") orelse if (!native_host and metadata.frontend != null) metadata.frontend.?.dist else "assets", + .frontend = if (native_host) null else metadata.frontend, .web_engine = web_engine.engine, .cef_dir = web_engine.cef_dir, + .cef_override = cef_dir_override != null or cef_auto_install_override, .signing = .{ .mode = signing, .identity = try flagValue(args, "--identity"), .entitlements = try flagValue(args, "--entitlements"), .team_id = try flagValue(args, "--team-id") }, .archive = archive, .env_map = init.environ_map,