Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -987,10 +987,10 @@ pub fn build(b: *std.Build) void {
// this step until the encoder comment, the host decoder comment, and
// the patterns below move with it.
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-wire-format-version-prose", "Verify wire-format version prose matches the packet version constant", &.{
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "pub const binary_packet_version: u8 = 5;" },
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "Compact binary gpu-surface packet encoding (wire format v5)." },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "Compact binary gpu-surface packet decoding (wire format v5)." },
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "Compact binary gpu-surface packet decoding (wire format v5)." },
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "pub const binary_packet_version: u8 = 7;" },
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "Compact binary gpu-surface packet encoding (wire format v7)." },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "Compact binary gpu-surface packet decoding (wire format v7)." },
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "Compact binary gpu-surface packet decoding (wire format v7)." },
});
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-windows-gpu-packet-presenter", "Verify Windows uses retained Direct2D packets with recovery, bounded resources, and dirty-region pixel fallback", &.{
.{ .path = "src/platform/windows/root.zig", .pattern = ".present_gpu_surface_packet_binary_fn = presentGpuSurfacePacketBinary" },
Expand Down
2 changes: 1 addition & 1 deletion docs/src/app/docs/capabilities/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Web content itself is declare-to-use: an app ships the embedded web layer only w
<tbody>
<tr>
<td>Open URL in system browser</td>
<td><code>runtime.openExternalUrl(url)</code></td>
<td><code>fx.openUrl(url)</code> / <code>runtime.openExternalUrl(url)</code></td>
<td><code>native-sdk.os.openUrl</code></td>
<td><code>network</code></td>
<td>macOS, Linux, and Windows system WebView; macOS Chromium</td>
Expand Down
6 changes: 5 additions & 1 deletion docs/src/app/docs/native-surfaces/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ fn view(ui: *Ui, model: *const Model) Ui.Node {
});
}

fn panes(model: *const Model, out: []PreviewApp.WebViewPane) usize {
fn panes(model: *const Model, context: PreviewApp.ChromeContext, out: []PreviewApp.WebViewPane) usize {
// Panes reconcile per window; this webview lives in the main one.
if (!context.is_main) return 0;
out[0] = .{
.label = "preview",
.anchor = "preview-pane",
Expand All @@ -115,6 +117,8 @@ fn panes(model: *const Model, out: []PreviewApp.WebViewPane) usize {
}
```

The hook takes the same `ChromeContext` as `chrome.build_window`, naming the window being reconciled: switch on `context.canvas_label` (or `context.is_main`) and return only that window's panes. A webview belongs to one window, so answering with the whole app's pane set makes every other window resolve an anchor its widget tree does not contain — correct behaviour, but a `no canvas widget carries semantics label ...` warning on every rebuild of every other window.

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
Expand Down
8 changes: 8 additions & 0 deletions docs/src/app/docs/native-ui/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,12 @@ case "build_finished":

</CodeToggle>

Handing a URL to the user's default handler is the same fire-and-forget shape: `fx.openUrl(url)` from a Zig `update_fx` arm asks the OS to open it — the browser for `http`/`https`, the mail client for `mailto` — through the platform verb the [bridge](/docs/bridge/builtin-commands) exposes to web content as `native-sdk.os.openUrl`. The URL is treated as hostile, because cores build them from terminal output, fetch bodies, and pastes: schemes are an allowlist (`http`, `https`, `mailto`, matched case-insensitively), and an empty URL, one past the 4 KiB bound, one carrying a NUL or any other control byte, or one naming an unvetted scheme — `file:` and `javascript:` included — is refused whole rather than trimmed into something openable. Unlike `runtime.openExternalUrl`, it is not gated on the webview external-link policy: that policy governs links *web content* follows, while this call comes from the app's own `update`. Fake execution and session replay never open anything.

```zig
.open_docs => fx.openUrl("https://example.com/docs/start"),
```

Failure and overflow are always visible: a spawn that cannot run delivers an exit Msg with reason `rejected`, a fetch that cannot run delivers a response Msg with outcome `rejected`, and a file effect that cannot run delivers a result Msg with outcome `rejected`; dropped or truncated lines carry counts and flags; `cancel` kills and reaps the process and always ends in exactly one `cancelled` exit Msg, with no further line Msgs after it. Tests use the fake executor (`effects.executor = .fake`) to assert on spawn, fetch, and file requests and feed synthetic lines, stderr (`feedStderr`, collect spawns), exits, responses, and file results back deterministically — set it before the first frame and `init_fx` boot spawns are recorded too. See `examples/effects-probe`.

For timestamps, the facade owns the clocks (Zig 0.16 puts `std.time` behind `std.Io`, which `update` never sees): `native_sdk.nowMs()` / `nowNanoseconds()` read the wall clock and `monotonicMs()` / `monotonicNanoseconds()` the duration clock. Time-dependent logic stores the `native_sdk.Clock` seam in the model (`.system` by default) so tests substitute a deterministic `native_sdk.TestClock` and advance it by hand.
Expand Down Expand Up @@ -729,6 +735,8 @@ This is a machine check, not a review item. An unnamed control, an icon-only con

Zig-built views get the same discipline at tree level: `canvas.expectA11yAuditSweepClean` lays out the real tree and reports interactive widgets announced with no name (including dynamic labels that resolve empty at runtime), focusable widgets keyboard traversal can never reach, and identically labeled sibling controls — adopt it in your test suite next to `canvas.expectLayoutAuditSweepClean`. Contrast checking and focus-visible styling checks are not part of the audit yet.

Zig views also carry two flags that are easy to confuse, and the engine keeps them strictly apart. `semantics.decorative` is the accessibility one: the widget and its subtree leave the semantic tree and can never take focus, while painting, layout, hit-testing, and event routing are untouched. It is the `aria-hidden` counterpart, and the right flag for chrome drawn for the eye alone — a search field's magnifier glyph, a rendered caret, a leading rule beside a labeled control. `semantics.hidden` is the VISIBILITY one: the widget keeps its layout box — the space stays reserved and siblings do not reflow — but drops out of painting, hit-testing, focus, and the semantic tree. That is the flag for an empty reserved slot (a fixed-width space held for an affordance that is not currently shown) and for `ui.nav`'s retained-but-inactive pages. A `hidden` icon paints nothing, so reaching for it to quiet a screen reader leaves a blank gap where the glyph should be; `decorative` is what that case wants.

## Tooling

- `native markup check src/app.native` — instant validation with `file:line:column` errors, including the font-coverage tofu guard: literal text with a codepoint outside the bundled face is a teaching error naming the character (it renders as a tofu box on the reference/screenshot and mobile paths — [register a font](/docs/fonts) that covers it and bind the text from the model, or use a vector icon or plain words; the static check knows only the bundled face's coverage). Dynamic strings get the same lesson as a Debug-build diagnostic when the view builds. Accessibility findings ride the same pass: unnamed interactive controls and role misuse are errors, unnamed images and redundant labels are warnings (see [Accessibility](#accessibility)).
Expand Down
5 changes: 4 additions & 1 deletion examples/canvas-preview/src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@ pub fn view(ui: *PreviewUi, model: *const Model) PreviewUi.Node {

// ------------------------------------------------------ webview pane seam

pub fn panes(model: *const Model, out: []PreviewApp.WebViewPane) usize {
pub fn panes(model: *const Model, context: PreviewApp.ChromeContext, out: []PreviewApp.WebViewPane) usize {
// The preview webview is declared in the main window's scene, so
// only the main window's rebuild has an anchor widget for it.
if (!context.is_main) return 0;
out[0] = .{
.label = webview_label,
.anchor = pane_anchor,
Expand Down
5 changes: 4 additions & 1 deletion examples/split-collapse/src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,11 @@ pub const web_pane_anchor = "content-web-pane";
/// pane, reflowing through the whole tween (the heavy field shape).
var web_pane_enabled = false;

fn webPanes(model: *const Model, out: []SplitCollapseApp.WebViewPane) usize {
fn webPanes(model: *const Model, context: SplitCollapseApp.ChromeContext, out: []SplitCollapseApp.WebViewPane) usize {
_ = model;
// One window, one pane — the discriminator is still honoured so the
// example teaches the per-window shape.
if (!context.is_main) return 0;
out[0] = .{
.label = web_pane_label,
.anchor = web_pane_anchor,
Expand Down
5 changes: 4 additions & 1 deletion examples/workbench/src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,10 @@ pub const CompiledWorkbenchView = canvas.CompiledMarkupView(Model, Msg, workbenc
/// The web pane: snapped to the markup's anchor column every presented
/// frame — the split divider reflows live web content. Setting `url`
/// navigates; bumping `reload_token` reloads the same URL.
pub fn webPanes(model: *const Model, out: []WorkbenchApp.WebViewPane) usize {
pub fn webPanes(model: *const Model, context: WorkbenchApp.ChromeContext, out: []WorkbenchApp.WebViewPane) usize {
// The browser pane belongs to the main window's scene; any other
// window owns no pane.
if (!context.is_main) return 0;
out[0] = .{
.label = web_view_label,
.anchor = web_pane_anchor,
Expand Down
23 changes: 18 additions & 5 deletions examples/workbench/src/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ fn fakeEffects() app.Effects {
return fx;
}

/// The main window's chrome context, as the runtime hands it to
/// `webPanes` on every rebuild — the pane hook is per-WINDOW, so the
/// unit tests must ask it the same question the runtime does.
fn mainPaneContext() WorkbenchApp.ChromeContext {
return .{
.canvas_label = app.canvas_label,
.window_id = 1,
.size = geometry.SizeF.init(1280, 800),
.tokens = .{},
.is_main = true,
};
}

fn expectTerminalCursorPaint(harness: *native_sdk.TestHarness(), terminal_id: canvas.ObjectId, expected: CursorPaintKind) !void {
const cursor_id = canvas.terminal_grid.paintIdBase(terminal_id) + 0x61_0002;
const command = (try harness.runtime.canvasDisplayList(1, app.canvas_label)).findCommandById(cursor_id) orelse return error.TestExpectedCursor;
Expand Down Expand Up @@ -186,7 +199,7 @@ test "the address bar commits a navigation the web pane picks up" {
var panes: [1]WorkbenchApp.WebViewPane = undefined;

// Boot: the home page is the pane's URL, and back/forward are dead.
try testing.expectEqual(@as(usize, 1), app.webPanes(&model, &panes));
try testing.expectEqual(@as(usize, 1), app.webPanes(&model, mainPaneContext(), &panes));
try testing.expectEqualStrings(app.web_view_label, panes[0].label);
try testing.expectEqualStrings(app.web_pane_anchor, panes[0].anchor orelse "");
try testing.expectEqualStrings(app.home_url, panes[0].url);
Expand All @@ -196,12 +209,12 @@ test "the address bar commits a navigation the web pane picks up" {
// Typing alone navigates NOTHING: the pane follows committed
// history, never the in-progress edit.
app.update(&model, .{ .address_edit = .{ .insert_text = "!" } }, &fx);
_ = app.webPanes(&model, &panes);
_ = app.webPanes(&model, mainPaneContext(), &panes);
try testing.expectEqualStrings(app.home_url, panes[0].url);

// Submitting commits it.
app.update(&model, .navigate, &fx);
_ = app.webPanes(&model, &panes);
_ = app.webPanes(&model, mainPaneContext(), &panes);
try testing.expectEqualStrings("https://ziglang.org!", panes[0].url);
try testing.expect(!model.back_disabled());
try testing.expect(model.forward_disabled());
Expand Down Expand Up @@ -297,10 +310,10 @@ test "reload bumps the pane token without changing the URL" {
var model = bootedModel(&fx);
var panes: [1]WorkbenchApp.WebViewPane = undefined;

_ = app.webPanes(&model, &panes);
_ = app.webPanes(&model, mainPaneContext(), &panes);
const before = panes[0].reload_token;
app.update(&model, .reload, &fx);
_ = app.webPanes(&model, &panes);
_ = app.webPanes(&model, mainPaneContext(), &panes);
try testing.expect(panes[0].reload_token != before);
try testing.expectEqualStrings(app.home_url, panes[0].url);
}
Expand Down
2 changes: 1 addition & 1 deletion skill-data/automation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Semantics:
7. Use `native automate widget-click <view-label> <widget-id>` to exercise pointer-style retained widget routing. `widget-hold <view-label> <widget-id>` drives a press-and-hold through the same path — pointer down, the reserved hold timer fired, then the suppressed release — so `on_hold` Msgs are live-drivable (a target without `on_hold` degrades to the click a real long press is). `widget-context-press <view-label> <widget-id>` is the secondary click: it presents the widget's context menu, or dispatches `on_hold` immediately when the route declares none.
8. Use `native automate widget-drag <view-label> <widget-id> <start-x-ratio> <end-x-ratio> [start-y-ratio end-y-ratio]` for continuous pointer controls.
9. Use `native automate widget-wheel <view-label> <widget-id> <delta-y>` for retained widget scroll input. Wheel targets must be interactive/scrollable widgets — a plain layout column or text node is not a wheel target; aim at the scroll/list widget id from the snapshot. Failures land in the snapshot as named reasons: `error event=automation.widget_wheel name=WheelTargetUnknown|WheelTargetNotInteractive|WheelTargetHasEmptyBounds detail="<command args>"`.
10. Use `native automate widget-key <view-label> <key> [text]` for focused retained widget keyboard input. The key accepts modifier chords — `cmd+a`, `cmd+c`, `cmd+v`, `cmd+x`, `ctrl+shift+arrowleft` (`cmd` sets the primary shortcut modifier on every platform) — so select-all/copy/cut/paste and shift-extended selection are drivable; after a copy, widget lines in the snapshot show the live selection as `selection=a..b`, and the copied text lands on the real system clipboard (`pbpaste` on macOS).
10. Use `native automate widget-key <view-label> <key> [text]` for focused retained widget keyboard input. The key accepts modifier chords — `cmd+a`, `cmd+c`, `cmd+v`, `cmd+x`, `ctrl+shift+arrowleft` (`cmd` sets the primary shortcut modifier on every platform) — so select-all/copy/cut/paste and shift-extended selection are drivable; after a copy, widget lines in the snapshot show the live selection as `selection=a..b`, and the copied text lands on the real system clipboard (`pbpaste` on macOS). One `widget-key` is one KEYSTROKE: the runtime dispatches the real `key_down` AND its paired `key_up` (the release carries the chord's modifiers but no text), so key-lifetime latches retire the way they do under real hardware and driving the same chord twice in a row runs it twice.
11. Use `native automate widget-pinch <view-label> <scale> [x y]` for trackpad pinch gestures against a gpu-surface view: the runtime dispatches the real `pinch_begin`/`pinch_change`/`pinch_end` platform events, with one change carrying `scale - 1`. `<scale>` is the FINAL multiplicative zoom for the gesture — the cumulative gesture scale (the product of `1 + delta`) lands exactly on it — `1.5` zooms in 50%, `0.5` zooms out to half. The optional anchor point is view-local points, defaulting to the view center. Apps hear it through the pinch channel (`Options.on_pinch` / the TS core's `pinchMsg`).
12. Use `native automate screenshot <view-label> [scale]` to capture the named `gpu_surface` view's canvas as `screenshot-<view-label>.png` (the CLI prints the artifact path and waits for the file).
13. Use `native automate tray-action <item-id>` to select a status-item dropdown row through the same platform event a real menu-bar click emits (command dispatch with source `.tray`). The live tray is visible in `snapshot.txt` as a `tray title="..." items=N` line followed by ` tray-item #id label="..." command="..." enabled=...` rows — the macOS menu bar is outside every window capture, so the snapshot is the only automation evidence the model-driven tray exists, and the `#id` there is what `tray-action` takes. Unknown ids degrade into the dispatch-error ring as `automation.tray_action`.
Expand Down
Loading