5.7k

Native Surfaces

Native surfaces are the app-owned regions that the Native SDK composes inside a platform window. A surface can be a GPU-rendered canvas, native chrome, a native utility panel, or an embedded WebView. The key model is that a window is a declared tree of surfaces — the default app is one gpu_surface canvas filling the window, and everything else composes around it.

Surface Types

SurfaceUse it forBacked by
GPU surfaceThe canvas your native UI renders into — plus custom drawing regions for editors, timelines, dashboards, and gamesMetal-backed on macOS; software (CPU reference renderer) presentation on Linux and Windows system hosts
Native chromeToolbars, titlebar accessories, sidebars, status barsNative platform views declared through ShellView or runtime.createView(...)
Native controlsButtons, search fields, toggles, segmented controls, labels, progressPlatform controls with command routing and accessibility metadata
OS capability surfacesDialogs, tray, clipboard, notifications, credentials, file dropsGuarded PlatformServices, runtime methods, and built-in bridge commands
WebViewEmbedded web content: web frontends, documents, previewsViewKind.webview, WebViewSource, and the selected web engine

The default shape: one canvas

The generated app declares one window with one GPU surface, and the UiApp loop renders the whole UI into it:

const shell_views = [_]native_sdk.ShellView{
    .{ .label = "main-canvas", .kind = .gpu_surface, .fill = true, .gpu_backend = .metal },
};
const shell_windows = [_]native_sdk.ShellWindow{.{
    .label = "main",
    .title = "My App",
    .width = 480,
    .height = 320,
    .views = &shell_views,
}};

Everything below this section is composition around that canvas: platform chrome, split panes, and (when needed) web content.

Window Shape

Use App.scene_fn (or UiApp's scene option) when a window should start with native structure, such as chrome around a WebView workspace:

const shell_views = [_]native_sdk.ShellView{
    .{ .label = "toolbar", .kind = .toolbar, .edge = .top, .height = 52 },
    .{ .label = "refresh", .kind = .button, .parent = "toolbar", .accessibility_label = "Refresh workspace", .text = "Refresh", .command = "app.refresh" },
    .{ .label = "body", .kind = .split, .fill = true, .axis = .row },
    .{ .label = "sidebar", .kind = .sidebar, .parent = "body", .width = 240 },
    .{ .label = "main", .kind = .webview, .parent = "body", .url = "zero://inline", .fill = true },
    .{ .label = "status", .kind = .statusbar, .edge = .bottom, .height = 32, .text = "Ready" },
};

const shell_windows = [_]native_sdk.ShellWindow{.{
    .label = "main",
    .title = "Acme",
    .width = 1100,
    .height = 760,
    .views = &shell_views,
}};

This creates native chrome and controls in the platform host while the main WebView owns the workspace content.

Canvas + WebView In One Window

A canvas-first UiApp can host a live webview next to its native-rendered widgets — the "native, web, or both per window" composition. The scene declares the webview shell view (parent it to the canvas view so frames share the canvas coordinate space), and UiApp.Options.web_panes keeps it snapped to a canvas widget's layout frame and drives navigation from the model:

const shell_views = [_]native_sdk.ShellView{
    .{ .label = "preview-canvas", .kind = .gpu_surface, .fill = true, .gpu_backend = .metal },
    .{ .label = "preview", .kind = .webview, .parent = "preview-canvas", .url = "https://example.com/", .x = 240, .y = 76, .width = 704, .height = 548 },
};

fn view(ui: *Ui, model: *const Model) Ui.Node {
    return ui.row(.{ .grow = 1 }, .{
        sidebar(ui, model),
        // An empty panel reserves the webview region; its semantics label
        // is the pane anchor.
        ui.panel(.{ .grow = 1, .semantics = .{ .label = "preview-pane" } }, .{}),
    });
}

fn panes(model: *const Model, out: []PreviewApp.WebViewPane) usize {
    out[0] = .{
        .label = "preview",
        .anchor = "preview-pane",
        .url = model.url(),          // changing it navigates
        .reload_token = model.reload_token, // bumping it reloads
    };
    return 1;
}

Panes re-apply after every rebuild and presented frame, reconciling against the runtime's actual webview state, so shell relayouts (window restores, resizes) cannot leave the webview detached from its anchor. Pane URLs are subject to security.navigation.allowed_origins. A scene whose only webviews are children like this never grows an implicit main webview — the window stays canvas-first. The seam is engine-agnostic (it rides the same PlatformServices webview surface both the system and Chromium hosts implement), though gpu_surface canvases currently require the system engine on macOS. examples/canvas-preview is the live proof, with a smoke test at zig build test-canvas-preview-smoke.

Imperative View API

For dynamic surfaces, create and update views at runtime:

const info = try runtime.createView(.{
    .window_id = 1,
    .label = "sync-status",
    .kind = .label,
    .parent = "status",
    .accessibility_label = "Sync status",
    .text = "Syncing",
});

_ = try runtime.updateView(info.window_id, info.label, .{
    .text = "Synced",
});

Trusted WebView code can use the guarded bridge helpers when js_window_api or an explicit builtin_bridge policy allows view commands:

const views = await window.zero.views.list();
await window.zero.views.update("status", { text: "Synced" });
await window.zero.commands.invoke("app.refresh");

Command Routing

Surfaces should share command IDs instead of each UI entry point owning separate behavior. A native menu item, shortcut, toolbar button, tray item, native button, and WebView bridge call can all dispatch app.refresh into the same Event.command handler.

CommandEvent.source records whether the action came from .menu, .shortcut, .toolbar, .tray, .native_view, .bridge, or .runtime. ViewInfo.id is a stable runtime handle for the view lifetime, while label remains the selector used by runtime and bridge APIs.

Compatibility

Existing App.source, runtime.createWebView(...), and window.zero.webviews.* apps remain valid. The generic view model adds a wider vocabulary:

  • ViewKind.webview wraps WebView-backed surfaces.
  • runtime.createView(.{ .kind = .webview, ... }) routes through the WebView backend.
  • runtime.listViews(...) returns WebView-backed and native views through one list.
  • Automation snapshots include native views and WebView-backed views.

Platform Support

macOS, Linux, and Windows system hosts support native views and controls today. The macOS host presents gpu_surface through a Metal-backed child view; Linux and Windows present it through the software reference renderer. See examples/gpu-surface for a minimal native controls plus WebView plus GPU surface composition. Chromium hosts and mobile hosts expose the same public vocabulary where useful, but unsupported operations reject explicitly instead of silently falling back.

Use support checks before showing optional native affordances:

if (runtime.supports(.native_views)) {
    try runtime.createView(.{ .label = "toolbar", .kind = .toolbar });
}
if (runtime.supports(.gpu_surfaces)) {
    try runtime.createView(.{
        .label = "canvas",
        .kind = .gpu_surface,
        .frame = native_sdk.geometry.RectF.init(0, 0, 480, 320),
        .gpu_surface = .{
            .backend = .metal,
            .pixel_format = .bgra8_unorm,
            .present_mode = .timer,
            .alpha_mode = .@"opaque",
            .color_space = .srgb,
            .vsync = true,
        },
    });
}
const hasNativeViews = await window.zero.platform.supports("native_views");

gpu_surface views report GPU presentation state through ViewInfo and GpuSurfaceFrameEvent, including the selected backend, pixel format, present mode, alpha mode, color space, vsync flag, status, frame size, frame index, timestamp, nonblank sample status, input latency budget, first-frame latency budget, retained canvas frame counters, widget revision, widget node count, and current cursor intent.

See Platform Support for the current host matrix.