# Windows and surfaces

> Multi-window apps, native surface composition, OS window chrome, GPU surfaces, and host presentation on macOS and desktop peers.

- Repository: vercel-labs/native
- GitHub: https://github.com/vercel-labs/native
- Human docs: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a
- Complete Markdown: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a/llms-full.txt

## Source Files

- `docs/src/app/windows/layout.tsx`
- `docs/src/app/native-surfaces/layout.tsx`
- `src/platform/macos/appkit_host.m`
- `examples/deck/README.md`
- `src/runtime/gpu_surface_events.zig`

---

---
title: "Windows and surfaces"
description: "Multi-window apps, native surface composition, OS window chrome, GPU surfaces, and host presentation on macOS and desktop peers."
---

Native SDK presents desktop apps as **platform windows** that own a tree of **surfaces** — GPU canvases, native chrome and controls, and optional WebViews — composed through `ShellConfig` / `ShellWindow` / `ShellView`, reconciled by the runtime, and driven on the host by macOS AppKit (Metal-backed `gpu_surface`), Linux, and Windows system peers.

## Architecture

```mermaid
flowchart TB
  subgraph app [App layer]
    Scene["ShellConfig / scene"]
    WindowsFn["UiApp.windows_fn"]
    WindowView["UiApp.window_view"]
    ChromePass["UiApp.chrome display-list"]
    Effects["Effects.closeWindow / minimizeWindow"]
  end

  subgraph runtime [Runtime]
    ShellLayout["shell_layout + createShellViews"]
    WinViews["window_views create/focus/close"]
    GpuEvents["gpu_surface_events"]
    CanvasFrame["canvas_frame present"]
  end

  subgraph host [Platform host]
    AppKit["macOS AppKit / Metal surface view"]
    Linux["Linux system host"]
    Win32["Windows system host"]
  end

  Scene --> ShellLayout
  WindowsFn --> WinViews
  WindowView --> GpuEvents
  ChromePass --> CanvasFrame
  Effects --> WinViews
  ShellLayout --> AppKit
  ShellLayout --> Linux
  ShellLayout --> Win32
  CanvasFrame --> AppKit
  CanvasFrame --> Linux
  CanvasFrame --> Win32
  GpuEvents --> CanvasFrame
```

| Layer | Owns | Key types |
| --- | --- | --- |
| Manifest / scene | Startup window shape, titlebar, view tree | `ShellWindow`, `ShellView`, `app.zon` `.windows` / `.shell` |
| `UiApp` | Model-declared secondary windows, per-window views, chrome overlays | `WindowDescriptor`, `windows_fn`, `window_view`, `on_chrome` |
| Runtime | Create/focus/close, surface present, input → widgets | `createWindow`, `createView`, `GpuSurfaceFrameEvent` |
| Host | OS window chrome, Metal/software present, drag hit-test | AppKit `NativeSdkMetalSurfaceView`, platform `PlatformServices` |

## Default shape: one window, one canvas

Generated and most `UiApp` apps declare a single shell window whose fill view is a `gpu_surface`. The widget tree from `Options.view` renders into that canvas.

```zig
const shell_views = [_]native_sdk.ShellView{
    .{
        .label = "main-canvas",
        .kind = .gpu_surface,
        .fill = true,
        .gpu_backend = .metal,
        .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 = "My App",
    .width = 720,
    .height = 480,
    .views = &shell_views,
}};
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
```

Wire the scene through `UiApp.Options.scene` (and keep the first window’s `titlebar` / size aligned with `app.zon` so the host startup create and the scene never disagree).

## Declaring windows

### Shell scene (`ShellWindow`)

| Field | Type / default | Role |
| --- | --- | --- |
| `label` | `[]const u8`, default `"main"` | Stable identity for restore merge and automation |
| `title` | optional | Window title at create |
| `width` / `height` | `f32`, default `720` / `480` | Default frame size |
| `x` / `y` | optional | Default origin when not restored |
| `resizable` | `bool`, default `true` | Host resize affordance |
| `restore_state` | `bool`, default `true` | Load frame from `windows.zon` by label |
| `restore_policy` | `clamp_to_visible_screen` \| `center_on_primary` | Placement when restoring |
| `titlebar` | `WindowTitlebarStyle`, default `.standard` | OS chrome style at create |
| `min_width` / `min_height` | `f32`, default `0` | Host content min-size floor (`contentMinSize` on macOS) |
| `views` | `[]const ShellView` | Surface tree for this window |

### `app.zon` windows

Top-level `.windows` (and `.shell.windows`) feed the host **startup** window create — including `titlebar` and min size — before the Zig scene loads:

```zon
.windows = .{
    .{ .label = "main", .title = "native-sdk", .width = 720, .height = 480, .restore_state = true },
},
```

Supported `titlebar` string values: `"standard"`, `"hidden_inset"`, `"hidden_inset_tall"`, `"chromeless"`. Unknown styles fail manifest parse.

### Imperative runtime windows

```zig
const info = try runtime.createWindow(.{
    .label = "tools",
    .title = "Tools",
    .default_frame = native_sdk.geometry.RectF.init(80, 80, 420, 320),
    .titlebar = .standard,
});
try runtime.focusWindow(info.id);
```

### JavaScript (`js_window_api`)

When bridge permissions allow `window` and an allowed origin is configured:

```javascript
const win = await window.zero.windows.create({
  label: "tools",
  title: "Tools",
  width: 420,
  height: 320,
});
const all = await window.zero.windows.list();
await window.zero.windows.focus(win.id);
await window.zero.windows.close(win.id);
```

Zig side typically sets `.js_window_api = true` and includes `native_sdk.security.permission_window` in `.security.permissions`.

## Multi-window apps (`windows_fn`)

Secondary windows are **model-declared**. Presence in the slice returned by `Options.windows_fn` **is** visibility: the runtime reconciles declared labels against live windows after every rebuild — creates missing, closes undeclared. There is no separate `visible` flag.

```zig
fn deckWindows(model: *const Model, scratch: *DeckApp.WindowsScratch) []const DeckApp.WindowDescriptor {
    var count: usize = 0;
    if (model.playlist_open) {
        scratch.windows[count] = .{
            .label = "playlist",
            .canvas_label = "playlist-canvas",
            .title = "Deck Playlist",
            .width = 512,
            .height = 440,
            .resizable = false,
            .titlebar = .chromeless,
            .on_close = .playlist_closed,
        };
        count += 1;
    }
    return scratch.windows[0..count];
}
```

| `WindowDescriptor` field | Constraint |
| --- | --- |
| `label` | Stable window identity (automation, close/minimize by label) |
| `canvas_label` | `gpu_surface` view label; must be unique app-wide (≠ main `canvas_label`) |
| `title` | Applied at creation only (no retitle channel) |
| `width` / `height` / `x` / `y` | Frame at create |
| `resizable`, `min_width`, `min_height` | Host resize policy |
| `titlebar` | Same enum as shell windows |
| `on_close` | `?Msg` when the **user** closes the window — never for a reconcile close the model initiated |

`Options.window_view` builds each secondary window’s widget tree by `window_label`. `windows_fn` requires `window_view` (asserted at init).

Reference implementations:

- `examples/deck` — fixed chromeless player + playlist unit toggled by model flag / `primary+L`
- `examples/system-monitor` — settings window while `settings_open`, standard chrome, `on_close` clears the flag

<Note>
Budget: `canvas_limits.max_ui_app_windows` = **4** model-declared secondary windows per `UiApp` (on top of the scene main window). Excess descriptors warn and are ignored. Platform total is `max_windows` = **16**.
</Note>

## Titlebar styles and OS chrome

`WindowTitlebarStyle` controls host chrome at window create:

| Style | Behavior |
| --- | --- |
| `standard` | Normal OS titlebar and system buttons |
| `hidden_inset` | Content under transparent titlebar; title hidden; macOS keeps traffic lights (~28pt band) |
| `hidden_inset_tall` | Same as hidden inset with unified-toolbar height (~52pt); lights vertically centered |
| `chromeless` | **No** OS titlebar and **no** system buttons (macOS borderless, Windows caption-less, Linux undecorated). Explicit opt-in for fully skinned UIs that draw **working** close/minimize controls |

<Warning>
Use `chromeless` only when the app draws real window actions (`fx.closeWindow` / `fx.minimizeWindow`). Ordinary apps should use `hidden_inset` / `hidden_inset_tall`, which keep OS controls.
</Warning>

### Chrome geometry (`WindowChrome` / `on_chrome`)

Hosts report overlay geometry so headers can pad for traffic lights / caption buttons without hardcoding:

| Field | Meaning |
| --- | --- |
| `insets` | Edge bands where OS chrome overlays content (macOS: top band + left lights; Windows: top band + right caption cluster) |
| `buttons` | Control cluster frame in content coordinates (centerline = `buttons.y + buttons.height / 2`) |
| `form_factor` | `unknown` \| `compact` \| `regular` (mobile size class; desktop often `unknown`) |
| `tabs_projected` | Host projects `ShellConfig.chrome.tabs` as real native tabs |

Subscribe with `UiApp.Options.on_chrome`:

```zig
pub fn onChrome(chrome: native_sdk.WindowChrome) ?Msg {
    return Msg{ .chrome_changed = chrome };
}
```

Reports arrive before the first view build and again when chrome changes (for example fullscreen zeroes the band). All-zero on standard chrome, fullscreen, or platforms without the concept.

### Window drag

Mark header chrome as a drag surface with markup `window-drag="true"` or the widget `.window_drag` flag. Press on background text/icons moves the window; press-claiming children (buttons) stay clickable. On window-drag down, the runtime skips the widget press pipeline so no control is left pressed (same as a native titlebar click).

Platform notes:

- macOS starts drag from the live pointer gesture (`performWindowDragWithEvent:`)
- Windows can mirror drag regions for `WM_NCHITTEST` / `HTCAPTION` when `set_window_drag_regions_fn` is wired
- Hosts without a drag service no-op the mirror

### App-drawn chrome pass

Fully skinned apps can draw hardware chrome via `UiApp.Options.chrome`: a fixed-count display-list pass with prefix (behind widgets) and suffix (in front). `examples/deck` machines absolute geometry from a layout table and pins command counts in tests — the runtime rejects a build that misses its declared count.

## Surfaces

A window is a declared tree of surfaces. Parent/edge/fill fields drive shell layout (`shell_layout`).

### Surface kinds (`ViewKind`)

| Kind | Role |
| --- | --- |
| `gpu_surface` | Canvas for native UI / custom drawing |
| `webview` | Embedded web content (`url`, engine via platform) |
| `toolbar`, `titlebar_accessory`, `sidebar`, `statusbar` | Docked native chrome regions |
| `split`, `stack` | Layout containers |
| `button`, `icon_button`, `list_item`, `checkbox`, `toggle`, `segmented_control`, `text_field`, `search_field`, `label`, `spacer`, `progress_indicator` | Native controls with command / a11y metadata |

### Composition patterns

**Native shell around WebView** (toolbar + split + webview + status):

```zig
const shell_views = [_]native_sdk.ShellView{
    .{ .label = "toolbar", .kind = .toolbar, .edge = .top, .height = 52 },
    .{ .label = "refresh", .kind = .button, .parent = "toolbar", .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" },
};
```

**Canvas + WebView siblings** — `examples/gpu-surface`: Metal `gpu_surface` and WebView in one split, plus native toolbar/status.

**Canvas-first panes** — parent a WebView to the canvas view and keep it snapped with `UiApp.Options.web_panes` (label, anchor semantics label, url, reload token). Pane URLs obey `security.navigation.allowed_origins`. A scene whose only webviews are children does not grow an implicit full-window `main` webview (`sceneNeedsMainWebView`).

### Imperative views

```zig
const info = try runtime.createView(.{
    .window_id = window_id,
    .label = "sync-status",
    .kind = .label,
    .parent = "status",
    .text = "Syncing",
});
_ = try runtime.updateView(info.window_id, info.label, .{ .text = "Synced" });
```

Gate optional chrome with capability checks:

```zig
if (runtime.supports(.native_views)) { /* create toolbar */ }
if (runtime.supports(.gpu_surfaces)) { /* create gpu_surface */ }
```

## GPU surfaces and presentation

### Host backends

| Host | `gpu_surface` presentation |
| --- | --- |
| macOS system (AppKit) | Metal-backed child view (`NativeSdkMetalSurfaceView`) |
| Linux / Windows system | Software (CPU reference) renderer |
| Null platform (tests) | Fake presents for hermetic suites |
| Chromium / mobile hosts | Same public vocabulary where useful; unsupported ops fail explicitly |

### GPU options on `ShellView` / create

| Option | Typical values |
| --- | --- |
| `gpu_backend` | `.metal`, `.software`, `.none` |
| `gpu_pixel_format` | `.bgra8_unorm` |
| `gpu_present_mode` | `.timer` |
| `gpu_alpha_mode` | `.opaque`, `.premultiplied` |
| `gpu_color_space` | `.srgb`, `.display_p3` |
| `gpu_vsync` | `bool` |

### Present-before-show

Shell windows that contain any `gpu_surface` use `WindowShowMode.on_first_present`: the window is created ordered out and becomes visible only after the first canvas present completes, so users never see a blank frame. WebView-only windows stay `.immediate` (engine owns first paint). Hosts keep a short fallback deadline if first present never arrives. Explicit focus can override a still-deferred show.

### Frame path

```text
Host present complete
    → GpuSurfaceFrameEvent
    → RuntimeGpuSurfaceEvents.dispatchGpuSurfaceFrame
        · update gpu size / scale / nonblank / backend metadata
        · advance layout/disclosure tweens on recorded timestamps
        · sync native scroll drivers
        · dispatch Event.gpu_surface_frame → app
        · resolve input latency; flush deferred accessibility
```

Related events:

| Event | When |
| --- | --- |
| `gpu_surface_frame` | Each host frame completion (includes `occluded` logical heartbeats) |
| `gpu_surface_resized` | Surface frame / scale change |
| `gpu_surface_input` | Pointer, key, text, IME on the surface |

`UiApp.Options.on_frame` can map a presented `GpuFrame` into a Msg (for example deck’s playback frame clock while audio is playing). Occluded completions are not valid latency endpoints.

Diagnostics-rich `GpuSurfaceFrameEvent` fields (when enabled) include nonblank status, sample color, present mode, canvas frame budgets, widget revision/node counts, and first-frame / input latency budgets (defaults: frame interval ~16.67ms; first-frame budget 150ms).

Automation can assert presentation truth, for example `gpu_nonblank=true` after `native build -Dautomation=true`.

## Window actions

| API | Behavior |
| --- | --- |
| `fx.closeWindow(label)` | Real OS close by declared label; last-window follows host exit semantics. Fire-and-forget; unknown label no-ops. Prefer declarative undeclare for secondary windows |
| `fx.minimizeWindow(label)` | Real OS minimize (Dock / taskbar / GTK) |
| `runtime.closeWindow` / `minimizeWindow` | Runtime service path used by effects and bridge |
| User titlebar close on model window | Dispatches descriptor `on_close` Msg; model must clear its open flag or the next reconcile recreates the window |

Deck chromeless keys wire Msgs to these effects so skin controls are real, not decorative.

## State persistence

`window_state.Store` persists geometry to `windows.zon` in the app state directory. Merge key is window `label` (or `id`). Restored: frame, maximized, fullscreen, scale. Titles continue to come from `app.zon` / create options. Empty or malformed labels are ignored on load.

## Limits and errors

| Constant | Value |
| --- | --- |
| `max_windows` | 16 |
| `max_ui_app_windows` | 4 secondary `UiApp` windows |
| `max_views` | 32 |
| `max_window_label_bytes` | 64 |
| `max_window_title_bytes` | 128 |
| `max_gpu_surface_scroll_drivers` | 16 per gpu-surface view |

Representative platform errors: `WindowLimitReached`, `DuplicateWindowLabel`, `WindowNotFound`, `ViewLimitReached`, `UnsupportedViewKind`, `InvalidGpuSurfacePacket`, `CrossWindowViewDenied`.

## Platform matrix (desktop)

| Capability | macOS AppKit | Linux | Windows |
| --- | --- | --- | --- |
| Multi-window create/focus/close | yes | yes | yes |
| `hidden_inset` / tall | yes | degrades toward standard | degrades toward standard |
| `chromeless` | borderless NSWindow | undecorated | caption-less |
| `WindowChrome` insets | traffic lights + band | best-effort / zero | caption buttons + band |
| `gpu_surface` | Metal | software | software |
| Present-before-show | yes | yes (null-tested) | yes |
| Native views / controls | yes | yes | yes |

Use `runtime.supports(.gpu_surfaces)` / `.native_views` before optional affordances. See [Platform support and security](/platform-security) for the full host matrix.

## Examples to run

| Example | What it proves |
| --- | --- |
| `examples/deck` | Two chromeless windows, skin window keys, chrome pass, `windows_fn` |
| `examples/system-monitor` | Tall hidden-inset titlebar, `on_chrome` padding, settings secondary window |
| `examples/gpu-surface` | Native toolbar + Metal surface + WebView split |
| `examples/canvas-preview` | Canvas-first WebView panes anchored to widgets |

```sh
native dev                          # from an example directory
native test -Dplatform=null         # hermetic multi-window / chrome suites
```

## Troubleshooting

| Symptom | Check |
| --- | --- |
| Blank window flash on launch | Canvas shell windows should use present-before-show; confirm a `gpu_surface` is in the scene so `shellWindowShowMode` returns `.on_first_present` |
| Secondary window never appears | Model flag must leave it in `windows_fn` output; `canvas_label` must be unique; stay within `max_ui_app_windows` |
| Secondary window reappears after close | Handle `on_close` and clear the model open flag (or accept re-create) |
| Header under traffic lights | Subscribe `on_chrome`; pad leading content by `insets.left`/`insets.right` and match band height |
| Chromeless window cannot close | Wire real `fx.closeWindow` / `fx.minimizeWindow` (or keep OS buttons via hidden styles) |
| Drag does nothing | Set `window-drag` on the header; ensure press is not on a button child; confirm host supports drag |
| GPU surface missing | `runtime.supports(.gpu_surfaces)`; macOS Metal vs software peers; `InvalidGpuSurfacePacket` in logs |
| Titlebar style ignored at startup | Align `app.zon` first window `titlebar` with scene `ShellWindow.titlebar` |

## Related pages

<CardGroup>
  <Card title="App model" href="/app-model">
    Model, Msg, update loop, and how UiApp owns rebuilds across windows.
  </Card>
  <Card title="Native UI markup" href="/native-ui">
    Elements, window-drag, flex layout, and bindings compiled into canvas trees.
  </Card>
  <Card title="Theming" href="/theming">
    Design tokens, live re-resolution, and chrome passes for skinned windows.
  </Card>
  <Card title="Menus, dialogs, and tray" href="/menus-dialogs-tray">
    OS-owned UI that coexists with engine-drawn surfaces.
  </Card>
  <Card title="Web engines" href="/web-engines">
    WebView composition beside gpu_surface canvases.
  </Card>
  <Card title="Runtime and effects" href="/runtime-effects">
    Effect channel including closeWindow and minimizeWindow.
  </Card>
  <Card title="Platform support and security" href="/platform-security">
    Host capability matrix and bridge permission boundaries.
  </Card>
  <Card title="Automation" href="/automation">
    Snapshots, gpu_nonblank asserts, and multi-window driving.
  </Card>
</CardGroup>
