native-rendering

native-rendering

all docs

Native rendering — the engine emits scenes, the viewer draws itself

A seed + roadmap document, written for the next agent (human or model). It captures *why* zero stopped being an HTML-over-webview app and became a thing that hands every viewer a scene and lets it draw with real native widgets — and *how* that ships without breaking a single page that exists today. Status: built and proven. The engine substrate (functions/ui/scene.cpp), the reference HTML renderer, the component class library (ui-kit/), and the macOS native renderer (desktop/scene_mac.h) are done and verified end to end. The Android / iOS / Windows / Linux renderers live in their shells and build in CI. See *Status* below for exactly what is verified where.

The one idea

For its whole life zero rendered exactly one way: an action emit-ed HTML bytes and a webview painted them. That couples every screen to HTML + WebKit. But zero is meant to *become the device*, and a real native button / navbar / list is more native than any webview can be.

So we decoupled what is on screen from how it is drawn:

• An action no longer says *how a thing looks*. It declares what is on screen

as a tree of semantic components — screen, navbar, tabbar, row, column, card, list, item, heading, text, button, field, image, form, divider, spacer. That tree is a scene.

• The engine hands the scene to whoever is looking and lets the *viewer* draw

itself:

• a native shell (Android, iOS, macOS, Windows, Linux, TV) asks for the

scene wire format and renders real native widgets;

• a browser / webview gets the SAME scene rendered to a self-contained HTML

document by the engine's reference renderer — so nothing that ships today breaks.

*"Website will get this, Android will get that, whoever the viewer is — draw yourself."* The engine is the invisible floor; the viewer is the framework. We are the framework.

Why this is in the grain (not new machinery)

Three facts that already shipped made this small, not large:

1. A view is already a computed projection, assembled from components. The

view class composes a page from named component actions (_v_hero, _v_menu, _v_actions, …). We did not invent "components" — we gave the component layer a viewer-agnostic output instead of hard-coded HTML.

2. The viewer already reaches the DSL. __door (desktop / mobile / tv / web /

cli) told the engine *who is looking*; we added __accept (the request's Accept header) so the same action can answer two ways from one body.

3. Design tokens already live on thing. accent / ink / bg / navbg /

navink / radius / font are inherited by every object. A scene carries those same tokens, so native widgets and HTML share one source of design truth.

The substrate (engine, C++)

functions/ui/scene.cpp is one translation unit (the arena + open-container stack are per-request thread_local state every verb shares). It registers five primitives and one host hook:

DSL verb

effect

functions/ui_screen [k v]…

start a fresh scene; root is a screen node

functions/ui_begin <type> [k v]…

open a CONTAINER node, make it current

functions/ui_node <type> [k v]…

add a LEAF node to the current container

functions/ui_end

close the current container (back to its parent)

functions/ui_render

finalize: native viewer → scene JSON, web viewer → HTML; reset the arena

ui_reset() (host)

called by dispatch() per request so a pooled worker never leaks a half-built scene

Props are alternating key value tokens (each already resolved by the interpreter, so a value is a "literal" or a member's value). Unknown props pass straight through to JSON, so a new native capability never needs an engine change to be expressed.

Negotiation (ui_render): native is OPT-IN, never forced. A request is drawn as a scene when Accept contains application/vnd.zero.scene+json or the query carries view=native; otherwise it is HTML. So adding native rendering can never regress a webview/browser screen.

Wire format (application/vnd.zero.scene+json):

{ "zero_scene": 1, "door": "mobile",

"theme": { "accent": "#0a84ff", "ink": "#1a1a1a", "navbg": "#111", … },

"root": { "type": "screen", "kids": [ {"type":"navbar","props":{"title":"Orders"}}, … ] } }

Every node is { "type": string, "props"?: {string:string}, "kids"?: [node] }.

The components are an OOP class hierarchy (store)

The store side is plain zero OOP — folders + parent = inheritance:

thing (root: theme tokens, access, drives)

└── component (ui-kit/component) — a thing whose job is to be DRAWN

└── screen (ui-kit/screen) — a full surface; inherits the chrome

└── native (ui-kit/native) — a demo screen authored as components

screen carries the inherited chrome — _topbar (a navbar from the screen's title) and _tabbar (the bottom tabs). A subclass writes an index action that calls ui_screen, composes components, and ui_render; it gets the chrome for free and overrides _topbar / _tabbar to change it. The node *types* are the canonical taxonomy the engine + every renderer agree on; the store classes are the ergonomic, inheritable building blocks composed from them.

The ui-kit/ folder in this repo is the reference copy of these classes. The canonical home is the store; promote with store.push <class>. They are loaded from ~/zero.9.stores at runtime like any class.

The viewers (renderers)

Viewer

Where

Draws with

Web / any WebView

functions/ui/scene.cpp (server-side)

a self-contained HTML document (theme tokens → CSS variables)

macOS

desktop/scene_mac.h

AppKit (NSStackView, NSButton, NSTextField, …)

Linux

desktop/main_gtk.c

GTK3 (GtkBox, GtkButton, GtkEntry, …)

Windows

desktop/main_win.cpp

Win32 controls (BUTTON, EDIT, STATIC)

Android / Android-TV / Fire-TV

mobile/android/.../SceneRenderer.kt

android.widget views

iOS / iPadOS

mobile/ios/Sources/SceneRenderer.swift

UIKit

A shell renders natively when its manifest says render = native; otherwise it keeps the WebView door, unchanged. desktop/native.manifest + make desktop APP=native builds the macOS native demo (ZeroNative.app, serving native/index).

The interaction contract every renderer honors: a button/item/card with an action navigates (GET, Accept: …scene+json); a button with method=post (or a submit button inside a form) collects the on-screen fields, url-encodes them, and POSTs; navbar.back == "1" pops a small back stack. Same semantics, different draw — that is the whole point.

Explicit website open (the native-first rule): a native shell renders ONLY scenes inline — there is NO HTML fallback. A node opens a WEBSITE only when it asks: its action is a full http(s):// URL, or it carries external = "1". The renderer then hands that URL to the platform's website opener (Android: a dismissible in-app WebView overlay; macOS/Linux/Windows: the system browser; iOS: SFSafariViewController) instead of scene-fetching it. A relative action without external is always a scene navigation. So "the website is barred unless we call it" — from a menu, navbar, or drawer. A non-scene response to a native viewer is an error surface, never a webview.

Status

• [x] Engine substrate functions/ui/scene.cpp (5 primitives + ui_reset),

__accept threaded through every driver (http / cgi / cli / stream).

• [x] Reference HTML renderer (server-side, self-contained doc, theme tokens).

• [x] Component class library ui-kit/ (component → screen → native) + a

demo screen exercising every node type.

• [x] Verified end to end on the engine: one native.index action serves

HTML to a browser and a scene to a native viewer; a form POST round-trips in scene form (verify-scene.sh).

• [x] macOS AppKit renderer (desktop/scene_mac.h) — compiles, bundles with

render = native.

• [x] Android / iOS / Windows / Linux renderers in their shells — built in CI.

• [x] The default face is a SCENE (thing.content = _scene_face,

scene_view = 1 on the root): every thing renders its structure (top bar, hero, its data as a list, its actions as buttons) as a scene — native widgets on a shell, HTML derived by ui_render in a browser. There is NO HTML-specific default view; a class never emits HTML as its face.

• [x] chat is native (thing.chat): a conversation (message cards + a composer)

built as a scene; the browser renders the same scene. home, node, console, projects, the twin (person) and every plain thing are native scenes.

• [x] web_only = the one exception: a deliberate WEBSITE (marketing app/

landing/site, the WebAuthn auth pages, visual/admin tools like world/gl/ control/infer, the apis/openapi catalogs). HTML on the web; on a native shell reached only by an EXPLICIT external menu item (the platform web door). No HTML fallback ever; a non-scene to a native viewer is an error surface.

• [x] Contextual menus on every scene: a slide-out _drawer (Home + the thing's

role-aware nav + owner tools) AND a bottom-nav _tabbar (Home / Chat / Node), both per the viewer's lens. The drawer also populates the desktop OS menu bar (a native "Go" menu) on macOS / GTK / Windows.

• [ ] Apple TV (tvOS, no WebView): the native renderer is the *only* path there —

this protocol is exactly what unblocks it.

• [ ] Richer components (tabs, sheets, pull-to-refresh, lists with sections) as

new node types — additive, no engine change for props.

Where to go next

• functions/ui/scene.cpp — the substrate + the HTML reference renderer.

• desktop/scene_mac.h — the canonical native renderer to mirror.

• local://scene-protocol.md (build-time) / this doc — the wire contract.

• ui-kit/ — the component class hierarchy + the demo screen.

• [VISION.md](../VISION.md), [lenses.md](./lenses.md) — the same spine: thin

engine, meaning in the store, the viewer wears its own face.

The view-layer split (the architecture, not a per-action hack)

The load-bearing rule: an action never decides HOW it is shown. A thing's content declares WHAT is on screen *once* — semantic components, built with functions/ui_*. The view layer (thing.render → ui_render) is the only place that knows the viewer, and presents that one scene per how it is seen: native widgets to a shell, a self-contained HTML document to a browser/webview, canvas data to a canvas. The same action result, many faces.

• A class's content builds a SCENE; the DEFAULT (thing.content) is the universal

_scene_face (the thing's structure), so a thing is native with no per-class work. No if native … else html branch ever lives in an action — and there is no bespoke HTML "view": the structure is the only definition, parsed by each viewer.

• thing.render reads the posture (both inherited): a SCENE (scene_view = 1,

web_only empty — the default) → _render_scene (content builds it, _drawer + _tabbar wrap it, ui_render presents); a WEBSITE (web_only = 1) → _render_web_only (a browser gets the HTML page; a native viewer a tiny "open website" scene).

• Scene by default: home, node, console, chat, projects, the view FACE and

every plain thing (person, clinic, portfolio, model, …). Websites (web_only): the website/site/landing chain + app marketing, the auth pages (login/register/identity/trust, WebAuthn — the platform web door), visual/ admin tools (world, gl, control, infer, network, skill/deck), and the apis/openapi catalogs — each reached only by an explicit external item.

• Making a thing a website is the rare opt-in (web_only = 1); making one native is

the default — nothing to do.