What is Pax?

Pax is a language-first GUI framework for Rust. A Pax application combines declarative .pax templates, reactive properties and PAXEL expressions, Rust application logic, and a portable runtime. The same interface model currently targets web browsers, macOS, iOS, and iPadOS.

Pax is for builders who want the structure of an application framework without giving up direct control over layout, vector drawing, composition, and motion. It exists to widen the expressive range of software while keeping the source legible enough to reason about.

If you already know you want to try it, go directly to Getting Started. That chapter stands on its own; you do not need to read the rest of the documentation first.

A language built for interfaces

A Pax component usually has two source layers: a Rust type and a .pax template. The template declares the component's interface tree, layout, styling, bindings, control flow, event routing, responsive choices, and motion. The Rust type exposes application state and owns handlers, data access, platform integration, and other imperative work.

Inside a template, PAXEL expressions derive values from reactive properties. They can combine values, choose between alternatives, perform unit arithmetic, and feed the result into content, layout, styling, or animation. PAXEL is side-effect-free: it describes relationships between values rather than a sequence of commands.

Templates remain declarative and inspectable, while Rust remains available for everything that genuinely changes the world: responding to an event, loading data, writing a file, or updating application state. In practice, most components are understood by reading the template and Rust file together.

Try it: Living Quilt

Living Quilt is an expressive showcase, available through pax-cli create my-quilt --example=living-quilt. Move the pointer to shift its light, click or tap the quilt to send a wave of color through the tiles, and click the Pax card to replay its entrance. The scene combines responsive components, Rust-driven motion, lighting, and feathered alpha masks. Its color reveal requires the GPU renderer.

The source tabs show how those parts fit together. For a smaller first project, pax-cli create my-app starts with Pax Logo: a logo, slider, and buttons. Getting Started walks through editing that simpler scene.

The Pax authoring loop

The central loop has four beats:

 .pax template ──renders──> interface ──event──> Rust handler
       ▲                                           │
       │                                           │ sets
       └── PAXEL derives visible values ◀── reactive properties
  1. A .pax template declares the interface and binds an event.
  2. PAXEL expressions derive visible values from reactive properties.
  3. An interaction invokes a Rust handler.
  4. The handler updates a property, and Pax updates the dependent interface work.

When the property changes, Pax invalidates the values that depend on it and reevaluates them when needed. This is the sense in which Pax is spreadsheet-like: change an input, and the formulas downstream of it become the new interface. Rust can also construct computed properties and subscriptions directly when an application needs a relationship or effect beyond the template.

The later chapters separate these responsibilities in more detail: Template Language & Structure covers the declarative tree, Data Binding & Expressions covers PAXEL, State & Properties covers the reactive graph, and Event Handling & Rust Logic covers the imperative side of the loop.

One interface model, multiple native targets

Pax uses a shared compiler and runtime, then connects them to each platform through a target-specific chassis. At a high level, the compiler prepares the program, the runtime expands its reactive scene, and the chassis integrates rendering, input, native elements, and platform services.

How Pax Runs follows this process from source to the screen and explains how reactive updates and rendering work fit together.

The current application targets are:

  • web browsers, through WebAssembly;
  • macOS;
  • iOS; and
  • iPadOS.

Web development is supported from macOS, Debian/Ubuntu Linux, and Windows; Apple targets require a suitable macOS and Xcode environment. Getting Started has the current workstation setup.

A shared interface model also does not mean that every platform follows an identical rendering path. Web builds select WebGPU where browser policy and support permit it, with a Piet/CPU renderer where that path is required. Apple targets use their native chassis. Backend-specific capabilities must therefore state their target and fallback behavior rather than presenting one backend as universal.

Pax also integrates native elements where platform behavior matters. Native text, form controls, and scrolling can share a scene and coordinate space with rendered shapes, images, paths, masks, and motion. The runtime manages the boundary between those surfaces so application code can work with one component tree while still respecting target-specific behavior.

Why choose Pax?

The language is designed around interface work. Structure, reactive formulas, units, event bindings, conditional settings, and declarative motion live close to the elements they affect. Rust is not squeezed into the template language; it remains the application layer and the escape hatch for unrestricted logic.

Pax also puts ordinary application architecture and a higher creative ceiling in the same system. Components, state, routing, events, and responsive layout can inhabit the same scene as vector primitives, paths, gradients, masks, clipping, native-element composition, and animation. Builders do not have to switch to a separate presentation model as an interface becomes more visual.

Finally, the portable runtime is intended for shipping rather than previewing alone. It carries the reactive model across the current web and Apple targets while retaining native integration and target-aware rendering. Because the source and running scene are structured, the same model also supports tools for hot reload, inspection, screenshots, and repeatable event-driven checks for both human and agent-assisted workflows.

Current scope and maturity

Pax is ready for builders: it is coherent and runnable enough to evaluate, learn, and build real interfaces with today. It remains pre-1.0, so APIs can evolve, some areas are still limited, and feature or backend maturity can vary by target. “Ready for builders” is a transition in project maturity, not a claim that every application is production-ready without its own evaluation.

Rust is the current application language. Native text and controls, selection and editing, and image alternatives provide accessibility foundations, but broader work such as reading order, tab order, annotations, and comprehensive audits is still in progress.

The framework, language, compiler, runtime, CLI, hot reload, source mapping, inspection, screenshots, and event-driving infrastructure ship as open source in the Pax repository. No companion product is required to build, run, inspect, or ship a Pax application. Any future companion would add to that open-source workflow rather than unlock it.

Start building

Install the CLI, create a project, and run it.

If you prefer to explore before installing, browse the repository examples and return to the authoring loop above when you want to understand how their templates and Rust code fit together.

Getting Started

Pax is a UI framework for Rust, with targets for web, macOS, iOS, and iPadOS. Start with pax-cli and the web target on macOS, Linux, or Windows. This gives every workstation the same first project, development server, and template reload workflow. Native Apple targets are an optional next step and require a macOS workstation with full Xcode.

Quick start

Install Rust and your workstation's web prerequisites, then run:

cargo install pax-cli
pax-cli create my-first-project
cd my-first-project
pax-cli run --target web

Open the local URL printed by the last command. The CLI builds, packages, and serves the application. Installing the CLI compiles the development tools once; the first project build also compiles its dependencies. Your application needs no build.rs or Cargo runner configuration. .pax edits reload in the running session; Rust changes require rebuilding and restarting.

Choose a web port or see the available development options:

pax-cli run --target web --port 8080
pax-cli run --help

Web development does not build the Apple SDK. Once the browser app is running, you can run it natively on macOS or try an iOS simulator.

The CLI's telemetry policy is described in CLI Telemetry. Ordinary Cargo compilation and direct native execution do not invoke it.

Need the prerequisites? Start with Prepare your workstation. Already running? Continue to Project anatomy.

Prepare your workstation

You can develop Pax web applications on macOS, Debian/Ubuntu Linux, or Windows. Native macOS, iOS, and iPadOS development requires a macOS workstation with full Xcode. Native Linux and Windows applications are not supported yet; use the web target on those workstations.

Choose the instructions for your workstation below. If Rust is already installed, you can skip its installation commands. The WebAssembly target and wasm-pack are needed only for web builds.

macOS

For web development, Apple's Command Line Tools are sufficient. Install them if needed:

xcode-select --install

Install Rust:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
. "$HOME/.cargo/env"

Install the web tools:

rustup target add wasm32-unknown-unknown
cargo install wasm-pack --version 0.15.0

Native Apple development additionally requires full Xcode and its first-launch setup. See Apple preparation when you are ready to use a native target.

Continue to Install the Pax CLI.

Linux (Debian / Ubuntu)

Install the system packages, Rust, and the web build tools:

# Install native build dependencies.
sudo apt update
sudo apt install -y \
  ca-certificates curl git build-essential pkg-config libssl-dev \
  python3 unzip xvfb \
  libglib2.0-dev libcairo2-dev libpango1.0-dev

# Install Rust.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
. "$HOME/.cargo/env"

# Install the WebAssembly target and helper.
rustup target add wasm32-unknown-unknown
cargo install wasm-pack --version 0.15.0

Continue to Install the Pax CLI.

Windows

Install Visual Studio Build Tools with the C++ workload, Git, Rust, and the web build tools from PowerShell. The setup below also installs Clang for dependencies that need it on ARM64 Windows. It uses winget to install Git and may prompt for administrator approval.

Windows setup commands

Run these commands in order in the same PowerShell session:

# Install Visual Studio Build Tools.
$installer = "$env:TEMP\vs_BuildTools.exe"
Invoke-WebRequest https://aka.ms/vs/17/release/vs_BuildTools.exe -OutFile $installer
$vsArgs = @(
  "--quiet",
  "--wait",
  "--norestart",
  "--installPath", "C:\BuildTools",
  "--add", "Microsoft.VisualStudio.Workload.VCTools",
  "--add", "Microsoft.VisualStudio.Component.VC.Llvm.Clang",
  "--add", "Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset",
  "--includeRecommended"
)

if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
  $vsArgs += @("--add", "Microsoft.VisualStudio.Component.VC.Tools.ARM64")
}

Start-Process $installer -Wait -ArgumentList $vsArgs

# Load the MSVC environment in this shell.
$vcvars = "C:\BuildTools\VC\Auxiliary\Build\vcvarsall.bat"
$vcArch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "x64" }
cmd.exe /s /c "`"$vcvars`" $vcArch >nul && set" | ForEach-Object {
  $separator = $_.IndexOf("=")
  if ($separator -gt 0) {
    Set-Item -Path "Env:$($_.Substring(0, $separator))" `
      -Value $_.Substring($separator + 1)
  }
}

# Make Git and clang available to future shells.
winget install --id Git.Git --exact --source winget `
  --accept-package-agreements --accept-source-agreements

$llvmPath = "C:\BuildTools\VC\Tools\Llvm\bin"
$userPath = @(
  [Environment]::GetEnvironmentVariable("Path", "User") -split ";" |
    Where-Object { $_ }
)
foreach ($path in @("C:\Program Files\Git\cmd", $llvmPath)) {
  if ($userPath -notcontains $path) {
    $userPath = @($userPath) + $path
  }
}
[Environment]::SetEnvironmentVariable("Path", ($userPath -join ";"), "User")
$env:Path = "C:\Program Files\Git\cmd;$llvmPath;$env:Path"

# Install Rust.
$rustupArch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
  "aarch64-pc-windows-msvc"
} else {
  "x86_64-pc-windows-msvc"
}

$rustup = "$env:TEMP\rustup-init.exe"
Invoke-WebRequest "https://static.rust-lang.org/rustup/dist/$rustupArch/rustup-init.exe" -OutFile $rustup
& $rustup -y --profile default --default-toolchain stable
$env:Path = "$env:USERPROFILE\.cargo\bin;$env:Path"

# Install the WebAssembly target and helper.
rustup target add wasm32-unknown-unknown
cargo install wasm-pack --version 0.15.0

Continue in the same PowerShell session to Install the Pax CLI.

Install the Pax CLI

Cargo is Rust's package manager. Use it to install the published Pax CLI:

cargo install pax-cli
pax-cli --version

The version command confirms that your shell can find the installed CLI. Installing it compiles Pax and its dependencies, so expect compilation output before the command finishes.

If you completed the Quick Start, continue to Project anatomy.

Create a project

From the directory where you keep your projects, run:

pax-cli create my-first-project

Choose a destination that does not already exist. The CLI creates Pax Logo: a small scene with an animation-position slider, Replay, and a light/dark background toggle. Its three source files contain the scene, controls, and native entry point. The logo itself comes from pax-std, re-exported by pax_kit; its geometry and animation sources stay out of your project. The starter is bundled with your CLI version; no repository checkout is needed.

For a smaller starting point, use pax-cli create my-counter --example=increment. The curated alternatives and their purpose are covered in Developer Workflow.

Run it on the web

Enter the project directory and start the web target:

cd my-first-project
pax-cli run --target web

The first run builds your application and its dependencies. Once the server is ready, the terminal prints a local URL beginning with http://127.0.0.1:. Open that URL in your browser. Use the address from your current run, since the port can change between sessions.

Your first-run checkpoint is the animated Pax logo. Drag the slider to choose an animation position; click Replay or the logo to play it again. The button at the top switches between light and dark backgrounds. A blank page or a build error means there's still something to resolve; see When something goes wrong.

Leave the command running while you use the app. To stop the development session, press Ctrl-C in its terminal. You can start it again with pax-cli run --target web from the project directory.

Run it natively on macOS

Enter the project directory and start a native development session:

cd my-first-project
pax-cli run --target macos

The window displays the same logo scene and controls. Drag the slider to scrub the animation, replay it, and try the background toggle. Template edits reload during this development session; Rust changes require rebuilding and restarting. Alternatively, cargo run runs the native app without a development server. Use cargo build to compile without launching. Distribution bundles are a separate packaging step, even when Cargo builds optimized artifacts.

Use --target=ios or --target=ipados for a simulator session; see mobile development.

Project anatomy

Open the project in your editor. These are the main roles to recognize:

File or directoryWhat it contains
Cargo.tomlThe Rust package definition, dependencies, and optional Pax project metadata.
src/main.rsCalls pax_kit::init!()?.run() to initialize and run the UI in the native application process.
src/lib.rsThe scene's state and event handlers, plus pax_kit::runnable! { root: crate::Example }. An optional init callback customizes root construction.
src/lib.paxThe starter's layout, logo instance, slider, and buttons.
Rust source (.rs)Component definitions, application state, event handlers, and other application logic.
Pax templates (.pax)UI elements, layout, styles, expressions, and bindings to event handlers. Templates can also be embedded in Rust source.
assets/, when presentApplication media such as images and fonts.
.pax/Generated build and development files, created by Pax's packaging tools. Make application changes in the source files above.

Pax templates describe the interface. Rust holds the state and behavior behind it. Expressions inside templates connect property values to what you see on screen. You'll work with these parts together as you build an interface.

The library builds as both rlib and cdylib. Its macros compile the Pax program during ordinary Rust compilation; your application needs no build.rs. The native executable has its own [[bin]] target, gated by required-features = ["standalone"], and starters enable standalone and designtime by default. On macOS, cargo run runs the native application. pax-cli run adds packaging, a development server, and template reload on the selected target. Xcode is required for Apple targets.

init! and init_with! initialize in the current native process; they do not parse application arguments or launch development tools. The designtime feature enables runtime inspection and template changes but does not start a server. Depending on a UI library never auto-launches it. See application setup for typed root construction and entry and mounting for platform details. Components can live in a nested ui/ module or a separate workspace library; see declare a runnable UI.

Make a first edit

Open src/lib.pax and find the Text element with id=replay_label. Change:

text="Replay"

to:

text="Play again"

Save to see the label update in your running pax-cli run session. The label is part of your scene; the animation behind AnimatedPaxLogo is a standard-library component. Follow @click=self.replay on its enclosing Group into src/lib.rs to see how the button resets and animates logo_progress. The slider binds to that same property, so it also follows playback.

Bare cargo run requires rebuilding after source edits. Rust changes require rebuilding and restarting on either path. See Hot reloading.

When something goes wrong

SymptomWhat to check
cargo or pax-cli is not foundConfirm the installation finished successfully. Rust installs commands in $HOME/.cargo/bin on macOS/Linux or %USERPROFILE%\.cargo\bin on Windows. That directory must be on your shell's PATH; reopening the terminal after installation usually picks up the change.
A build cannot find the WebAssembly target or its standard libraryRun rustup target add wasm32-unknown-unknown, then retry.
wasm-pack is not foundRun cargo install wasm-pack --version 0.15.0, then check wasm-pack --version.
A compiler, linker, or system library is missingRevisit your workstation setup. On Windows, use the PowerShell session where you loaded the MSVC environment.
The project destination already existsChoose a new directory name. Keep any existing project files.
The browser cannot connectCheck that pax-cli run is still running and use the local URL from that session.

For more build detail, run pax-cli run --target=web --verbose. If compilation fails, start with the first reported error. When asking for help, include that error, your workstation OS, and the output of pax-cli --version.

Where to go next

Continue with Template Language to learn how to describe an interface. Data Binding and Expressions explains how values flow into that interface, and Event Handling with Rust covers responding to input.

For hot reload, inspection, screenshots, and local reference, continue with Developer Workflow and Tools.

To choose another target or ship a release, see Targets, Build, and Deployment.

Further reference

The links below preserve earlier reference locations. Detailed workflow and packaging guidance lives in the linked chapters.

CLI telemetry

The first public CLI command prints the telemetry privacy notice and sends no telemetry. Later public commands enable minimal telemetry by default. See CLI Telemetry for the fields and opt-out controls.

Formatting Pax source

Use pax-cli fmt for Pax source and pax-cli fmt --check to check without writing. See Format Pax source for file selection, inline templates, and CI usage.

Hot reloading

Debug pax-cli run sessions reload .pax templates by default. Bare Cargo runs require rebuilding and restarting after source edits. For Rust changes, stop and rerun the app on every target; refresh the browser after rebuilding. See Hot reloading for the mode matrix, target boundaries, and project configuration.

Web public files

Use a project-root public/ directory for web-only files such as robots.txt and well-known metadata. See Web public files for copying, live serving, reserved paths, and the distinction from app assets.

Project Metadata

Configure titles, icons, Apple identity, and packaging values in [package.metadata.pax] in Cargo.toml. See Project metadata for the supported keys, target inheritance, and icon requirements. Apple apps can also declare typed Info.plist values and signing entitlements; see Apple property lists and entitlements.

Local release runs on iOS and iPadOS

Use pax-cli run --release --target ios or --target ipados for an optimized simulator run. Connected-device runs also accept a device selector and Apple development team. See Local release runs for signing, device selection, and the boundary with App Store distribution.

CLI Telemetry

Pax uses a small amount of CLI telemetry to understand whether the tooling works and whether installations return to build applications. The first public CLI command on an installation prints a notice and sends no telemetry. Later public commands enable telemetry by default.

Telemetry contains only:

  • a random installation identifier stored by the CLI;
  • the CLI version;
  • the host operating-system family and CPU architecture;
  • the top-level command family and, when applicable, its Pax target;
  • whether a finished command succeeded or failed;
  • whether a pax-cli run session reached its target-specific ready point; and
  • approximate city, region, and country derived server-side from the connection IP.

The CLI does not add an IP address or location field to its telemetry payload. Like any HTTPS service, the telemetry service sees the connection IP while handling the request. It passes that address to Mixpanel only as reserved, transient geolocation input. Mixpanel derives the approximate city, region, and country and discards the IP before ingesting the event. Pax does not log or persist the raw address. The coarse location is used only in aggregate to guide product, content, outreach, and marketing investment.

A run that reaches its target-specific ready point emits only the ready event, never a second event when it exits. Without observed readiness, a finished run emits only its command outcome. In particular, iOS/iPadOS release runs have no readiness channel: a clean exit is a successful command outcome, not evidence of activation. Activation counts only ready events.

Pax does not collect source code, filenames, project paths, project names, project content, command arguments, error text, account information, locale, or hardware-derived identifiers.

Events are sent on a bounded best-effort basis by a short-lived background copy of the CLI. They are not persisted, durably queued, or retried. A telemetry failure never changes command behavior or exit status, and the foreground CLI does not wait for network delivery. The worker may finish after the command has returned to the prompt. Each request has a two-second timeout; once started, the worker has a five-second deadline for input handling, local setup and delivery. If worker launch or delivery fails, that event is silently dropped.

The worker rechecks consent immediately before sending. Ordinary commands and telemetry status do not wait for in-flight network requests. An explicit telemetry off disables future sends first, then waits up to 2.5 seconds for any already-authorized requests to finish before reporting success. If that wait times out, it reports an error but telemetry remains disabled.

Controls

Use these commands to inspect or change the persistent setting:

pax-cli telemetry status
pax-cli telemetry off
pax-cli telemetry on

pax-cli telemetry on creates a fresh random installation ID immediately but sends no event itself. The next public command may send telemetry. Turning telemetry off and back on therefore starts a new installation identity.

Set PAX_TELEMETRY=off or DO_NOT_TRACK=1 to disable telemetry for a process. Telemetry is also disabled by default when CI=1 or CI=true is present.

The local setting is stored under the operating system's Pax application-state directory:

  • macOS: ~/Library/Application Support/Pax/telemetry
  • Windows: %LOCALAPPDATA%\Pax\telemetry
  • Linux: $XDG_STATE_HOME/pax/telemetry, or ~/.local/state/pax/telemetry

The random installation identifier represents this local CLI installation and is not tied to a person, account, hardware fingerprint, or project. Turning telemetry off removes that identifier. Turning it on again creates a new one immediately without sending an event.

Update checks

CLI update checking is functional and separate from telemetry. It continues when telemetry is off and never includes the installation identifier. Like any HTTPS request, the update service sees the connection IP while handling the request. Under the server contract, Pax does not retain that raw IP, derive coarse location for the update check, or turn the update request into a telemetry event. Update checks run in a background thread and never delay CLI exit; an update notice is shown only if the result is already available.

Cargo workflows

Ordinary cargo build, cargo check, and direct native cargo run do not emit Pax CLI telemetry or perform its update check. Sessions started through pax-cli use the CLI preferences described above. CLI usage therefore covers only the CLI cohort; it cannot be treated as a count of all active Pax developers or all Cargo builds.

Templates and UI structure

A Pax template describes a component's interface: the elements it contains, their properties, and their connections to state and events. Reading the tree gives you its structure; reading the values on each element tells you how that structure becomes a visible interface.

This chapter starts with a small panel, then introduces the settings mechanisms you can use as an interface grows. If you haven't run a Pax project yet, Getting Started covers installation and the first run.

Reading a template

A component usually pairs a Rust type with a .pax file. For a main component called Notes, the Rust declaration can be as small as:

#![allow(unused)]
fn main() {
use pax_kit::*;

pax_kit::runnable! { root: crate::Notes }

#[pax]
#[file("lib.pax")]
pub struct Notes {}
}

#[pax] makes the type a Pax component, runnable! selects it as this application's root, and #[file("lib.pax")] names its template. Here is that template:

<Group x=24px y=24px width=320px height=160px>
    <Text x=24px y=24px width={100% - 48px} height=36px
        text="Field notes"
        style={font_size: 24px, fill: rgb(32, 40, 48)}
    />
    <Text x=24px y=72px width={100% - 48px} height=48px
        text="A place for your next idea."
        style={font_size: 16px, fill: rgb(72, 80, 88)}
    />
    // The background is behind the text above it.
    <Rectangle width=100% height=100%
        fill=rgb(246, 241, 230) corner_radius=12
    />
</Group>

Tags name components, using PascalCase: Group, Text, and Rectangle here. The Group has an opening and closing tag because it contains other elements. The two Text elements and the Rectangle use self-closing tags. Visible words are supplied through the Text component's text property.

The group is the parent of these three elements, and they are siblings of one another. Each child's position and percentage dimensions are relative to its container. The group supplies a shared 320-by-160-pixel area for this panel. A template can also contain several root elements; add a container when you need them to share a layout area.

Comments such as the // line above help explain the intent of a template. Keep them between elements or settings. Pax also accepts block comments and HTML-style comments in those positions.

Components can use templates embedded in Rust with #[inlined(...)] as well. Separate .pax files keep the interface easy to find and edit. See Components and Composition for defining and organizing reusable components.

Setting values

An attribute such as width=320px assigns a property on an element. Some properties are common across elements, including position and size; others, such as text or corner_radius, belong to the receiving component.

The shape of the value tells you how it is supplied:

FormExampleMeaning
Literaltext="Field notes"Use this value directly.
Value with a unitwidth=320px or width=100%Express a dimension in pixels or relative to the parent.
Expressionwidth={100% - 48px}Derive the value with PAXEL.
State bindingtext={self.title}Read the containing component's title property reactively.
Event binding@click=self.activateInvoke the component's activate handler when the event occurs.
Read/write bindingtext=bind:titleConnect a supporting control's value to the component's property in both directions.

The last three examples assume the Rust component exposes the named property or handler. In a template expression, self refers to that containing component. It gives a child element access to the state of the component whose template you are reading.

Braces mark PAXEL expressions. When title changes, a Text element using text={self.title} updates from that value. A control such as <Textbox text=bind:title /> can also write edits back to title. See Data Binding and Expressions for expression syntax and State and Properties for the Rust side.

Event bindings connect the tree to application behavior. The handler's body lives in Rust; Event Handling and Rust Logic covers its signature, event data, and state updates.

Element order

Earlier siblings appear in front of later siblings. In the panel above, both text elements are in front of the rectangle. Moving the rectangle before the text would put its opaque fill over the words.

This makes a template read like a stack of layers: foreground details first, background last. It is worth checking source order whenever an element seems to have disappeared behind another.

Nesting, draw order, and spatial layout each answer a different question. Nesting gives elements a shared container; sibling order determines which is in front; properties such as x, y, width, and height determine their geometry. A Group lets you place children in its area. Flow containers such as Stacker can arrange children for you. Continue with Layout and Responsiveness for those layout tools, or Compositing and Effects for clipping, masks, and native-element layering.

IDs and classes

Inline values keep a small element easy to read. As settings become reusable, give elements a class and collect those settings in an @settings block:

<Group id=panel x=24px y=24px>
    <Text class="heading" x=24px y=24px width={100% - 48px}
        height=36px text="Field notes" />
    <Text class="body" x=24px y=72px width={100% - 48px}
        height=48px text="A place for your next idea." />
    <Rectangle class="surface" width=100% height=100% />
</Group>

@settings {
    #panel {
        width: 320px
        height: 160px
    }
    .heading {
        style: {
            font_size: 24px
            fill: rgb(32, 40, 48)
        }
    }
    .body {
        style: {
            font_size: 16px
            fill: rgb(72, 80, 88)
        }
    }
    .surface {
        fill: rgb(246, 241, 230)
        corner_radius: 12
    }
}

This produces the same panel. Tags use = for assignments; the settings block uses :. The nested style object groups properties of the text style.

Use an id to address an individual node within the template, and a class for settings that several elements can share. #panel selects id=panel; .heading selects elements with class="heading". The supported selectors inside @settings are these ID and class forms.

Settings belong to the component's template. A class on a child component's tag can configure that child instance; the child's internal template has its own settings. Explicit sharing is covered in Imported settings.

Multiple and reactive classes

A single class is a string. Multiple classes use one ordered string list:

<Rectangle class=["surface", "selected"] />

Within the component's local settings, matching classes apply in the order listed on the element. Later classes can override earlier ones, matching ID settings apply after those classes, and inline values apply last. If several blocks use the same selector, their source order decides which comes later.

For example, an inline fill=WHITE wins over a fill supplied by .surface, .selected, or a local ID selector. Put a value inline when it should remain specific to that element; put it in a class when another class or a theme should be able to vary it.

Class bindings can be reactive too. If the containing component exposes a boolean selected property, it can choose the class list:

<Rectangle class={self.selected ? ["surface", "selected"] : "surface"} />

An expression may return a string or a list of strings. It can read a Rust component property of type Property<String> or Property<Vec<String>>.

There are a few precise rules worth keeping nearby:

  • An empty string or empty list means no classes. A class may have no matching selector.
  • A string names one class and is never split on whitespace. Write ["surface", "selected"] for two classes.
  • Duplicate names keep their final position in the list.
  • Class names start with an ASCII letter or underscore, followed by ASCII letters, digits, underscores, or hyphens. Invalid names and non-string values are ignored with a runtime warning.
  • An element has one class attribute. The formatter writes a one-item literal list as a single string.

class selects settings; it cannot itself be assigned inside a selector, settings, or timeline block. Give every inline property a single assignment.

Conditional settings

Settings can respond to a condition while keeping the same elements in the tree. For example, this panel has a fixed width in a wide viewport and leaves a 24-pixel margin on each side in a narrow one:

<Group class="panel" x=24px y=24px height=160px>
    <Rectangle class="surface" width=100% height=100% />
</Group>

@settings {
    .panel {
        width: 320px
    }
    .surface {
        fill: rgb(246, 241, 230)
        corner_radius: 12
    }
    if $viewport.width < 600 {
        .panel {
            width: {100% - 48px}
        }
    }
}

The condition is a PAXEL expression that evaluates to a boolean. Pax updates the affected settings when its inputs change. Here, the conditional width overrides the earlier .panel width while the viewport is under 600 pixels; the earlier width applies again when the condition becomes false.

Use else if and else for additional branches. Conditions can also use component state or built-in globals such as $ios, $macos, and $landscape. The viewport describes the application window; percentages in the assigned width still resolve against the element's parent. See Layout and Responsiveness for choosing responsive rules for nested components.

Place these conditions around selector blocks inside @settings. They may contain nested conditions. Inside an individual selector, use a PAXEL expression for a conditional property value. An if among the template's elements instead controls which elements exist; Conditional content explains that structural form.

Imported settings

ImportSettings lets a component use settings provided by other components. This is useful for sharing a palette, typography, or other coordinated choices. The provider subtree supplies settings without drawing its own interface.

For example, declare a provider alongside the main component in Rust:

#![allow(unused)]
fn main() {
#[pax]
#[file("paper_theme.pax")]
pub struct PaperTheme {}
}

Its paper_theme.pax file can supply the panel's surface settings:

<Group />

@settings {
    .surface {
        fill: rgb(232, 240, 226)
        corner_radius: 18
    }
}

Add the provider to the panel's template, keeping the visible elements as siblings of ImportSettings:

<ImportSettings>
    <PaperTheme />
</ImportSettings>

<Group x=24px y=24px width=320px height=160px>
    <Text x=24px y=24px width={100% - 48px} height=36px
        text="Field notes"
        style={font_size: 24px, fill: rgb(32, 40, 48)}
    />
    <Rectangle class="surface" width=100% height=100% />
</Group>

The rectangle receives .surface from PaperTheme. The import applies to elements authored in the containing component's template. A child component manages imports for its own internal template.

For ordinary settings, the layers are applied in this order:

  1. The containing component's local settings.
  2. Imported providers, in provider order.
  3. Inline values on the element.

Later layers override earlier ones for the same property. Within each provider, the same class-list and ID ordering rules apply. A class in an imported provider can therefore override a local ID setting; layer order is considered before selector order. When providers are direct siblings inside one ImportSettings, their order in the template is their layer order.

The provider can expose reactive properties of its own. Expressions in its settings use that provider's self and state, so a shared palette can change reactively. Stable imports reuse their provider layers on unchanged frames; provider-property updates continue through the existing reactive bindings. See Drawing and Styling for visual design choices and PAXEL's $base for building on an earlier property value.

Add a transition to ease changes to the effective imported settings:

<ImportSettings transition=SettingsTransition::Ease(400ms, TransitionCurve::InOutQuad)>
    <PaperTheme />
</ImportSettings>

All changed settings use that duration and curve. The initial appearance is immediate; subsequent provider changes animate from each receiver's current value. Omitting transition (or using SettingsTransition::None) preserves immediate updates. Inline values still win over imported settings. See Automatic settings transitions for interruption, supported types, and ownership rules. Imported providers do not currently export named timeline tracks.

Read more

Templates also support structural if and for blocks, child projection with slot(...), and motion with @timeline. Continue with Components and Composition and Animation and Motion as you encounter those needs.

For working with values, Data Binding and Expressions is the next step. To keep source formatting consistent as you edit, use pax-cli fmt.

PAXEL and reactive values

A label might show a name, a bar might show progress, and a color might signal that something is ready. PAXEL connects those visible values to the state they describe. It is Pax's expression language, written inside {...} in templates.

Think of each expression as a spreadsheet formula: it describes how to obtain a value from its inputs. Rust supplies application state and changes it in response to events; the formulas let the interface follow along.

This chapter builds on the panel from Templates. It covers the values a template can derive. State and Properties explains how to manage those values in Rust, including computed properties and subscriptions.

Reactive bindings

Start with a literal, then replace it with a property read:

<Text text="Field notes" />
<Text text={self.title} />

The first element always displays the same words. The second reads title from the component that owns the template. That component exposes the value through a Rust Property<T> field. Here is a complete declaration with a title and a progress value:

#![allow(unused)]
fn main() {
use pax_kit::*;

pax_kit::runnable! { root: crate::Notes }

#[pax]
#[file("lib.pax")]
pub struct Notes {
    pub title: Property<String>,
    pub progress: Property<f64>,
}

impl Notes {
    pub fn on_mount(&mut self, _ctx: &NodeContext) {
        self.title.set("Field notes".to_string());
        self.progress.set(0.25);
    }
}
}

Pax calls on_mount when the component mounts. These initial values give the template something to display. In lib.pax, the same progress value can drive several properties:

<Group x=24px y=24px width=320px height=220px>
    <Text x=24px y=24px width={100% - 48px} height=36px
        text={self.title}
        style={font_size: 24px, fill: rgb(32, 40, 48)}
    />
    <Slider x=24px y=76px width={100% - 48px} height=24px
        value=bind:progress min=0.0 max=1.0 step=0.25
    />
    <Text x=24px y=116px width={100% - 48px} height=28px
        text={"Progress: " + (self.progress * 100) + "%"}
        style={font_size: 16px, fill: rgb(72, 80, 88)}
    />
    <Group x=24px y=160px width={100% - 48px} height=12px>
        <Rectangle width={(self.progress * 100)%} height=100%
            fill={self.progress >= 0.5 ? rgb(55, 120, 90) : rgb(75, 100, 160)}
            corner_radius=6
        />
        <Rectangle width=100% height=100%
            fill=rgb(220, 216, 206) corner_radius=6
        />
    </Group>
    <Rectangle width=100% height=100%
        fill=rgb(246, 241, 230) corner_radius=12
    />
</Group>

Dragging the slider writes to progress. The label reads it to form a string; the foreground rectangle reads it to choose a percentage width and a color. At 0.25, the bar fills a quarter of its container. At 0.5, it fills half and turns green.

Each formula reading progress is a derived value with a dependency on that property. A write marks those derived values for reevaluation. Pax computes them when they are next needed and caches their results between changes. There is no separate redraw call in this example.

Reading versus writing

An expression such as text={self.title} reads state. A supporting control can also write back through bind::

<Textbox text=bind:title />

Typing updates the same title property that the heading reads. bind: names a property to share with the control; it does not accept a formula for a derived result. Use {...} for labels, dimensions, colors, and other calculated values. See Accessibility and Native Controls for control behavior, and Events and Rust for updates that need application logic.

In this panel, self.title and title read the same property. This chapter uses the explicit self form when referring to component state.

Literals

The receiving property supplies the expected type. A Text needs a string for text; a Rectangle needs a fill for fill and a size for width. Pax converts compatible values to that type. A value with the wrong shape or type is an error.

KindExamples
Number24, 0.25
Booleantrue, false
String"Field notes"
Size24px, 50%
Rotation15deg, 0.5rad
Time250ms, 1s, 5f
ColorBLUE, rgb(32, 40, 48), rgba(32, 40, 48, 128)
List[120px, None, 30%]
Contextual object{font_size: 16px, fill: BLUE}
Empty optionNone

Some literals have a property-specific meaning. For example, the entries in a Stacker's sizes list describe its cells, while entries in a rectangle's corner_radius list describe its corners. The receiving type defines those positions; see the shape reference below.

Units in formulas

Units remain useful inside expressions:

<Rectangle width={100% - 48px} />
<Rectangle width={(self.progress * 100)%} />

The first width leaves 48 pixels out of the available parent width. The second turns a numeric fraction into a percentage. Layout determines what a dimension is relative to; Layout and Responsiveness develops that relationship in more detail.

Write a unit immediately after its number or parenthesized expression: 24px and (self.progress * 100)%. A space between the value and the unit breaks that form. % means percent; the modulo operator is %%.

Time suffixes follow the same pattern: (100 + self.delay)ms produces a duration if delay is numeric. Animation and Motion covers durations, frame counts, and clocks.

Choosing values

A ternary selects a value using a boolean condition:

<Text text={self.progress >= 1.0 ? "Ready" : "In progress"} />

The form is condition ? when_true : when_false. Pax evaluates the chosen branch. Both alternatives should produce values compatible with the receiving property. A ternary changes a value on an existing element; use conditional content when the condition should determine which elements exist.

For optional data, ?? supplies a fallback. If the component declares maybe_title: Property<Option<String>>, its template can use:

<Text text={self.maybe_title ?? "Untitled"} />

An option contains either Some(value) or None. ?? unwraps a present value, and evaluates its right side only when the left side is None. A non-option value passes through unchanged: false, zero, and an empty string all remain valid values. The fallback does not catch an error in the expression on its left.

When assigning to an Option<T> property, an ordinary compatible value can be lifted into the option automatically. Write None for the empty case; there is no need to wrap a present template literal in Some(...).

Structured values

Use . to read a field and square brackets to read a zero-based list entry. For example, if items is a nonempty Property<Vec<String>>, self.items[0] reads its first string. The index must be within the list's bounds. Update application data through the owning Rust property so its dependent values can be invalidated.

A list can itself be derived. Given first_size: Property<Size>, this Stacker uses it for the first cell and leaves the second flexible:

<Stacker sizes={[self.first_size, None]}>
    <Text text="First cell" />
    <Text text="Flexible cell" />
</Stacker>

The outer braces make the whole list an expression. Use this form when list entries read state; a literal list such as [120px, None] is convenient for fixed values.

Contextual objects can hold individual bindings. With heading_size: Property<Size>, a text style can keep its color fixed while reading the font size:

<Text text="Field notes" style={
    font_size: {self.heading_size}
    fill: rgb(32, 40, 48)
} />

Here the outer braces describe the style's fields, and the braces around self.heading_size mark that field's expression. An expression can also produce a whole object; the receiving property still determines which fields and types are meaningful.

Loops

A range such as 0..3 includes 0, 1, and 2. Its upper bound is exclusive. Ranges often provide the source for a template for:

for i in 0..3 {
    <Text y={(i * 24)px} text={"Item " + i} />
}

The template repeats the element; PAXEL supplies its values. Continue with Data-driven components for iteration over data, keys, and item identity.

Function calls

Built-in functions can express a calculation more clearly than a longer formula. Math::min and Math::max choose between two values; Math::len returns a list's length. For example, a numeric desired_width can become a width with an 80-pixel minimum:

<Rectangle width={(Math::max(80, self.desired_width))px} />

Function calls use a registered Type::function(...) name. Enum constructors also use path syntax, such as StackerDirection::Horizontal. PAXEL's call syntax reaches the functions and constructors exposed to it; arbitrary Rust methods are not automatically available.

A small Rust helper

To give the panel a reusable progress label, add #[has_helpers] alongside the attributes on the existing Notes declaration. Then add a separate helper implementation:

#![allow(unused)]
fn main() {
#[helpers]
impl Notes {
    pub fn progress_label(progress: f64) -> String {
        format!("{:.0}% complete", progress * 100.0)
    }
}
}

Call it with the changing value as an explicit argument:

<Text text={Notes::progress_label(self.progress)} />

#[helpers] exposes public associated functions in that implementation; they have no self receiver. #[has_helpers] tells the Pax type declaration to use that helper implementation. Keep lifecycle methods such as on_mount in the ordinary impl block shown earlier.

The formula can track self.progress because it appears in the arguments. Hidden reads of external state inside a helper cannot supply that connection. Helpers should return a value without changing application state, reading files, or performing other side effects. Rust event handlers and subscriptions provide those capabilities. See State and Properties when a derived value needs to be shared beyond one template.

Built-in globals

Built-in values use a $ prefix. They describe the running app and its environment:

ValueWhat it provides
$viewportScene width and height in logical pixels; major, minor, aspect, landscape, portrait, and square.
$targetBoolean platform and operating-system facts, such as web, native, macos, ios, iphone, and ipad.
$frames, $millisRuntime frame count and elapsed time in milliseconds.
$gyro, $accelDevice orientation and acceleration readings, where supplied by the target and permitted by the user.

Viewport orientation and shape fields also have short aliases, including $landscape, $portrait, $square, $major, $minor, and $aspect:

<Group width={$landscape ? 50% : 100%} />
<Rectangle width={($viewport.width / 2)px} />

The viewport dimensions are numbers. Attach a unit when using them to produce a size. A parent-relative percentage is often more useful for nested content.

Each $target field likewise has an alias: $web means $target.web, for example. Platform and operating-system facts can overlap: an app in a browser on macOS can have both $web and $macos set. The remaining fields are android, windows, linux, mobile, and desktop. These describe runtime facts, including browser hosts; they do not extend the supported build targets beyond web, macOS, iOS, and iPadOS.

The sensor values expose x, y, and z. Orientation is in degrees and acceleration is in meters per second squared; web acceleration includes gravity when that reading is available. Mobile WebKit requires a user gesture before sensors can stream. A web app can request access by invoking window.paxRequestDeviceSensorPermissions() from an app-owned JavaScript button handler. This is a web integration call, separate from PAXEL. See Accessibility and Native Controls for input behavior and Animation and Motion for using clocks.

$base

$base refers to the earlier value of the same property while Pax resolves its settings. It lets a later assignment build on what an earlier one supplied:

<Rectangle class="card" width={$base + 24px} height=80px />

@settings {
    .card {
        width: 240px
    }
}

The class supplies a width of 240 pixels. The inline expression adds 24, so this rectangle is 264 pixels wide. If the class width changes, the inline formula continues to build on it.

$base is contextual to the property being resolved. It does not read a parent element's property or the previous animation frame. See Templates for ordinary settings layers and Animation and Motion for timeline-relative values.

Operators

This is a syntax lookup, grouped by purpose. Parentheses make the intended grouping explicit in formulas that mix different operations.

PurposeOperatorsExample
Fallback??maybe_title ?? "Untitled"
Choice? :selected ? BLUE : GRAY
Boolean!, &&, ||enabled && !disabled
Equality==, !=mode != "hidden"
Comparison<, <=, >, >=progress >= 0.5
Arithmetic+, -, *, /, %%, ^(index + 1) * 24
Range..0..count
Access., [...]user.name, items[0]
Grouping and units(...) with optional suffix(width + 12)px

+ also joins strings and displayable values, as in the panel's progress label. ^ is exponentiation. The unit suffixes are px, %, deg, rad, ms, s, and f.

Shape value reference

The following forms are useful when assigning structured values to drawing properties. Their syntax and visual use now live together in Drawing and Styling.

Corner radii

See Corner radii for the one-to-four-value list, its per-corner mapping, and the named-object longhand.

Gradients

See Gradients for @gradient stops, [x, y] points, alpha, linear defaults, and current radial/backend limits.

Timeline-relative values

See Relative values and property ownership for using $base in timeline keyframes, setting a base layout value, and understanding when an inline assignment overrides an ordinary timeline track.

Evaluating expressions from Rust

Tools using pax-language can evaluate an expression against a HashMap<String, PaxValue> without mounting an app or entering a PropertyGraph. With the crate's default parser feature enabled:

#![allow(unused)]
fn main() {
use pax_language::interpreter::compute_paxel;
use pax_runtime_api::{Functions, Numeric, PaxValue};
use std::{collections::HashMap, rc::Rc};

Functions::register_all_functions();
let values = HashMap::from([("count".to_string(), PaxValue::Numeric(Numeric::I64(3)))]);
let result = compute_paxel("count * 2", Rc::new(values)).unwrap();
assert_eq!(result, PaxValue::Numeric(Numeric::I64(6)));
}

The map resolver creates local snapshots of its entries. It does not subscribe to shared application state. Custom resolvers that bind shared Property<T> values still need an entered graph; see State and Properties for local computations and the shared-state boundary.

Read more

State and Properties is the next step for the Rust side of reactivity. Events and Rust connects user actions to state changes. For a larger template, continue with Components and Composition, including conditional content, lists, and keyed identity.

Appearance values

Vector fill and stroke properties normalize a Color or Paint to one default layer, and a single layer to one stack entry. This promotion is specific to appearance types; ordinary Vec<T> properties still require lists.

<Rectangle width=240px height=120px fill=[
    Fill {paint: {self.accent}, opacity: 50%},
    @gradient {0%: CYAN, 100%: FUCHSIA}
]/>

Each entry can be shorthand paint, a typed or untyped layer object, or a bound value. Fields stay reactive when nested in a list. Binding a Rust Property<Vec<Stroke>> also observes edits to the layers' nested Properties; you do not have to replace the outer vector to change paint or width. The same behavior applies in debug and baked release cartridges.

Template lists accept @gradient entries; this does not introduce @gradient inside arbitrary PAXEL expressions. Lists must be flat and cannot contain None. Wrong explicit layer types, unknown fields, and obsolete color fields are errors, with a zero-based entry index for a failing list entry. Statically known inline and matching settings values are validated during compilation; values selected by dynamic expressions are checked when evaluated. An invalid dynamic stack follows the existing property-error policy and falls back to an empty stack as a whole, never a partially accepted prefix. See Paint layers for defaults and ordering.

State and Properties

A progress value can feed a label, a bar, and a color. In PAXEL, we described those relationships with template formulas. Here we turn to the Rust side: where the value lives, how an action changes it, and how to build relationships between properties in Rust.

Pax represents reactive state with Property<T>, where T is the value's Rust type. Property<T> holds transferable application state. LocalProperty<T> holds values and computations owned by the UI thread. Shared values enter the same reactive graph as local computations and template expressions; nodes and rendering remain on the UI thread.

This chapter continues the small Field notes panel from Templates. It includes the component declaration and template needed to try the first example in a project from Getting Started.

State in a component

The panel has a title and a numeric progress value. Declare them as fields on the Rust component that owns its template:

#![allow(unused)]
fn main() {
use pax_kit::*;

pax_kit::runnable! { root: crate::Notes }

#[pax]
#[file("lib.pax")]
pub struct Notes {
    pub title: Property<String>,
    pub progress: Property<f64>,
}
}

Rust accesses these values through methods such as self.progress.get(). The template reads the value directly with {self.progress}. PAXEL connects that read to the property so subsequent writes can update the interface.

Use a property for state that other reactive values need to observe. Ordinary Rust data is useful for calculations and data structures, but changing a plain field alone does not notify the graph. Fields on a #[pax] type also participate in generated serialization and value-conversion code; they must meet that generated code's type requirements.

Initial values

By default, #[pax] derives Rust's Default trait for the component. Property<T>::default() creates a property containing T::default(): the title starts as an empty string and progress starts at zero.

To give this panel its initial content, add #[custom(Default)] alongside the existing attributes on Notes, then supply the implementation:

#![allow(unused)]
fn main() {
impl Default for Notes {
    fn default() -> Self {
        Self {
            title: Property::new("Field notes".to_string()),
            progress: Property::new(0.25),
        }
    }
}
}

#[custom(Default)] tells the macro to use your implementation in place of its generated one. Each call creates properties for that component instance. Property::new also works outside a component, including on a worker thread.

The preceding PAXEL example set these same values in on_mount. With the custom default above, remove those initialization writes. Mount remains a useful place for setup that needs NodeContext or the component's established property connections, as the computed-property example below will show. See Events and Rust for lifecycle behavior and Components and Composition for values supplied by a parent component.

Reading and updating state

Give the panel a button that advances progress by a quarter, up to one. Add this handler to Notes:

#![allow(unused)]
fn main() {
impl Notes {
    pub fn advance(&mut self, _ctx: &NodeContext, _event: Event<ButtonClick>) {
        let next = (self.progress.get() + 0.25).min(1.0);
        self.progress.set(next);
    }
}
}

In lib.pax, bind the button to the handler and read progress in the label and bar:

<Group x=24px y=24px width=320px height=220px>
    <Text x=24px y=24px width={100% - 48px} height=36px
        text={self.title}
        style={font_size: 24px, fill: rgb(32, 40, 48)}
    />
    <Button x=24px y=76px width=160px height=28px
        label="Advance" @button_click=self.advance
    />
    <Text x=24px y=116px width={100% - 48px} height=28px
        text={"Progress: " + (self.progress * 100) + "%"}
        style={font_size: 16px, fill: rgb(72, 80, 88)}
    />
    <Group x=24px y=160px width={100% - 48px} height=12px>
        <Rectangle width={(self.progress * 100)%} height=100%
            fill=rgb(55, 120, 90) corner_radius=6
        />
        <Rectangle width=100% height=100%
            fill=rgb(220, 216, 206) corner_radius=6
        />
    </Group>
    <Rectangle width=100% height=100%
        fill=rgb(246, 241, 230) corner_radius=12
    />
</Group>

The handler reads the current value, calculates the next one, and writes it with set. The label and width then follow the new progress. Event wiring is covered in Events and Rust; the state operation itself is the same whether an update begins with a button, a data-loading result, or another application action.

Collections and value snapshots

get() returns a clone of the stored value. For a Vec<String>, you can edit that copy and write it back, or use update to perform both steps:

#![allow(unused)]
fn main() {
let entries = Property::new(vec!["Sketch the header".to_string()]);

let mut draft = entries.get();
draft.push("Choose a palette".to_string());
assert_eq!(entries.get().len(), 1);

entries.set(draft);
assert_eq!(entries.get().len(), 2);

entries.update(|items| items.push("Add an interaction".to_string()));
assert_eq!(entries.get().len(), 3);
}

The first push changes the local vector. set publishes that vector through the property. update serializes mutation of this one property: it clones the current value, invokes your closure exactly once, and commits the result. Concurrent updates do not lose increments. A panic leaves the previous published value intact. Use a plain struct-valued property when several fields need one coherent publication; separate properties are not a transaction.

The access methods have these roles:

MethodBehavior
get()Clone the latest published snapshot; never evaluate UI code.
read(f)Lend an immutable snapshot to f, without holding a storage lock.
set(value)Publish immediately and notify attached UI graphs.
set_if_neq(value)Set only when the value differs from the current one; return whether a write occurred.
update(f)Atomically edit and publish a private draft, invoking f once.

set and update invalidate dependents even if the resulting value is equal to the previous one. Use set_if_neq when unchanged writes are common and equality is inexpensive; it requires T: PartialEq. The button above could use it to avoid another invalidation when progress is already at one.

For an inspection that does not need a copy, entries.read(|items| items.len()) returns the count without cloning the vector. Shared read callbacks may reenter reads or writes: their borrowed snapshot stays unchanged. Mutation callbacks, including cloning/equality invoked by update or set_if_neq, must not recursively mutate another shared property. Pax diagnoses that case with a panic before acquiring a second write lock. Keep mutations short; do not wait for another thread or perform I/O inside them.

Property handles

Cloning a Property<T> gives you another handle to the same shared allocation. A write through either handle changes the value both observe:

#![allow(unused)]
fn main() {
let progress = Property::new(0.25_f64);
let progress_handle = progress.clone();
let earlier_value = progress.get();

progress_handle.set(0.5);
assert_eq!(progress.get(), 0.5);
assert_eq!(earlier_value, 0.25);
}

This is useful when a closure needs access to a property after the method that created it returns. Move a cloned handle into the closure; later reads still obtain the property's current value.

The depth of a value copy follows T's own Clone implementation. If a value contains other property handles or shared pointers, cloning that value can still share the objects inside it. In particular, cloning an entire component does not produce independent copies of its property values.

To change an existing component field's value, call set or update. An assignment such as self.progress = Property::new(0.5) gives the field a new handle; any templates or closures holding the previous handle keep their old connection. The next section introduces replace_with for changing how an existing field is computed while keeping its graph identity.

Shared handles are also useful across components. Continue with Components and Composition for ownership, parent/child bindings, and stores shared within a subtree. A store locates shared handles by Rust type; the properties provide reactivity. Final unmount clears a component's local state and store registrations. Explicitly retained handles still refer to the old state, not a later mount.

Shared state and local views

Shared values meet Default + Clone + Interpolatable + Send + Sync + 'static. A cloned shared handle can be moved into an ordinary Rust worker or a native async task. set publishes immediately, and get reads the latest snapshot on any thread. Template expressions and local views observe that publication at a UI settlement boundary. Repeated writes may coalesce; a property represents latest state, not a lossless stream of events.

#![allow(unused)]
fn main() {
let count = Property::new(0_usize);
let worker_count = count.clone();
std::thread::spawn(move || worker_count.update(|count| *count += 1));
}

Starting an untracked thread does not give it automatic cancellation or a node lifetime. A retained shared value can outlive its view; unmount detaches the view's graph connections. Publishing that retained value cannot remount it.

Use LocalProperty<T> for Rc/node values and local computations. It and its untyped dependency handles cannot cross threads, even inside Arc<Mutex<_>>. A local get evaluates dirty dependencies before returning. Its read closure runs under a local table borrow and must not reenter that table.

Within a Pax handler or mount callback, shared.local() obtains the current application's local view. Capture that view in computed evaluators so every read uses the UI's imported snapshot. Pax selects the graph for handlers; low-level embedders must enter a PropertyGraph themselves. Easing also uses this view: self.progress.local().ease_to(...). Shared get never becomes a lazy computed read simply because the caller is on the UI thread.

Computed properties

A computed property derives its value from other properties. A PAXEL formula already does this for a template. Rust's LocalProperty::computed is useful when the derived value should be available to Rust code as well, or when you want to assemble the dependency relationship in Rust.

For example, the panel can expose its formatted progress label as a property. Add pub progress_label: Property<String> to Notes and progress_label: Property::default() to its custom default. Then add this mount method:

#![allow(unused)]
fn main() {
impl Notes {
    pub fn on_mount(&mut self, _ctx: &NodeContext) {
        let progress = self.progress.local();
        let dependencies = [progress.untyped()];

        self.progress_label.replace_with(LocalProperty::computed(
            move || format!("{:.0}% complete", progress.get() * 100.0),
            &dependencies,
        ));
    }
}
}

Replace the panel's progress text element with:

<Text x=24px y=116px width={100% - 48px} height=28px
    text={self.progress_label}
    style={font_size: 16px, fill: rgb(72, 80, 88)}
/>

The initial label reads “25% complete.” Pressing Advance changes the source property and the label becomes “50% complete.” The bar still reads progress through its PAXEL formula.

LocalProperty::computed takes an evaluator and an explicit dependency list. .untyped() provides a handle suitable for that list, allowing properties with different value types to be dependencies of one computation. Include every input whose changes should trigger reevaluation. A .get() inside a Rust evaluator does not automatically register a dependency.

replace_with installs the new evaluator and dependencies on the existing progress_label field. The field keeps the outgoing connections already established by its template and any other consumers. For a new local property, you can use the handle returned by LocalProperty::computed directly.

Change derived state by updating its source inputs. Writing to a computed property with set changes its stored value but leaves the evaluator in place; a later dependency change can compute the value again. Keep the dependency graph acyclic, and avoid an evaluator that reads the very field it is replacing, directly or through another computed property.

Computed evaluators should be deterministic and free of side effects. Their execution follows demand for a local value. Bindings installed on a shared field also have a local observer that publishes accepted computed results. Use a subscription for work that must respond to a change independently of a value being read.

Use property.published() when a consumer should only read computed results. Published<T> can cross threads and exposes get, read, and revision, but no mutation API. Its local view is an independent computed proxy; changing that proxy does not grant access to the producer. A regular component Property<T> remains writable, including expression-bound fields used by two-way bindings.

Nested drawing records also have an explicit boundary. Custom primitives should call stroke.resolve_in_graph() and material.resolve_in_graph() on the UI owner before passing values to RenderContext. These return plain ResolvedStroke and ResolvedMaterial records, so a backend cannot observe a worker's newer publication halfway through recording a draw. snapshot() reads the latest shared fields without entering a graph and is intended for non-UI consumers. Neither operation makes independent properties a transaction.

Subscriptions and effects

A subscription runs a callback in response to reactive dependencies. Use it for a bounded side effect, such as notifying an application service that a selection changed. A pure derived value is usually simpler as a computed property or PAXEL formula.

For a small observable example, log the panel's progress. Rename _ctx to ctx in the mount method above and add the following after its computed property setup:

#![allow(unused)]
fn main() {
let progress = self.progress.local();
let dependencies = [progress.untyped()];

ctx.subscribe(&dependencies, move || {
    log::info!("Progress observed: {}", progress.get());
});
}

On the web, append ?pax_log=info to the running app's URL and reload to see these messages in the browser console. Informational logs are hidden at the default logging level.

The callback is scheduled once after registration to observe the initial state. It is then scheduled when a dependency dirties. subscribe does not call it immediately inside the registration call, and several writes before the reactive work is drained can produce one callback observing the latest value. A subscription is therefore useful for observing current state; it does not provide a record of every intermediate write.

NodeContext retains subscriptions for its node and clears them when the node unmounts. ctx.clear_subscriptions() removes all subscriptions registered on that node. subscribe returns no individual cancellation handle.

Callbacks run synchronously as Pax drains reactive work during runtime updates. They may set another property and enqueue further work. Keep them short, move blocking I/O outside the callback, and avoid effects that continually dirty one another. For actions that should happen specifically because a user pressed a button, put the action in the event handler; see Events and Rust.

How updates travel

The property graph records which values depend on which inputs. The panel now contains relationships like these:

progress ----> bar width (PAXEL)
    |
    +--------> progress_label (Rust) ----> text (PAXEL)

Setting progress marks its ordinary computed dependents dirty: their cached values may be out of date. This invalidation travels through those dependents before their new values are calculated.

A later get or read, including a read needed to update the interface, recomputes the required dirty values. As each evaluator reads its inputs, those inputs are brought up to date too. Repeated reads of a clean computed property use its cached value. Multiple writes can coalesce before a read, and an ordinary computed value with no observers need not run just because an input changed.

This is eager invalidation with on-demand evaluation. Subscriptions are explicit observers: the runtime schedules them when they become dirty. Propagation cutoffs, described next, also schedule work so they can decide whether a change should travel farther through the graph.

Propagation cutoffs

Cutoffs are an advanced tool for a graph whose frequently changing inputs produce comparatively stable outputs. The ordinary properties above are enough for the panel; consider a cutoff when measurement points to expensive downstream work that often produces the same result.

Ordinary invalidation travels through a computed property before Pax knows whether its output has changed. LocalProperty::computed_with_cutoff creates a boundary: when an input changes, Pax evaluates this property before deciding whether to invalidate its downstream dependents.

For example, a pointer position might select one bucket for every 20 units:

#![allow(unused)]
fn main() {
let pointer_x = LocalProperty::new(0.0_f64);
let pointer_x_for_bucket = pointer_x.clone();

let bucket = LocalProperty::computed_with_cutoff(
    move || (pointer_x_for_bucket.get() / 20.0).floor() as i64,
    &[pointer_x.untyped()],
    |last_accepted, candidate| last_accepted == candidate,
);
}

Moving from 2 to 3 leaves the bucket at zero. Moving to 21 changes it to one. The predicate compares the last accepted output with the new candidate:

Return valueResult
trueDiscard the candidate, retain the last accepted value, and stop invalidation at the cutoff.
falseAccept the candidate and invalidate downstream dependents.

The first evaluation is always accepted because there is no prior accepted value to compare. After a candidate is suppressed, the next comparison still uses the last accepted value. Equality is common; approximate or domain-specific predicates are also possible, with that same retention behavior.

Cutoffs settle as part of the runtime's synchronous reactive work, before queued effects. A direct local get or read of a dirty cutoff can settle it earlier. The cutoff still evaluates its own function; the saved work is the invalidation and reevaluation beyond it when the output is equivalent.

Use a cutoff where inputs change frequently, the result often remains equivalent, and downstream work is materially more expensive than evaluating the cutoff and its predicate. Keep both callbacks inexpensive and free of side effects. computed_with_cutoff_and_name adds a diagnostic name; the other named property constructors can also help identify a large graph's values during diagnosis.

Choosing a pattern

NeedStart with
A value derived for a templateA PAXEL formula; the compiler supplies its dependency edges.
Application state changed by an actionA property written by the Rust handler.
Derived reactive state used from RustLocalProperty::computed, with every dependency listed explicitly.
A short side effect observing stateNodeContext::subscribe, with its node-scoped lifetime.
Fewer unchanged writesset_if_neq, when equality is inexpensive.
A stable output between a busy input and expensive consumersA measured use of computed_with_cutoff.

For the complete method surface and value-type requirements, see the properties API. Continue with Events and Rust to connect more kinds of actions to state, Components and Composition to organize shared state, or Animation and Motion to change values over time.

Events and Rust

A template describes how an interface responds to its state. An event handler gives the user a way to change that state: advance a task, edit a title, choose a destination, or move something across the screen.

This chapter continues the Field notes panel from Properties. We will look closely at its button handler, add a title editor and a keyboard shortcut, then explore lifecycle and custom interactions. The final sections cover application setup and ownership and asynchronous work with Tokio. The examples use the same Notes component and its title and progress properties; the complete starting declaration and template are in Properties. Keep use pax_kit::*; in the Rust file.

Space Game combines keyboard input with a running simulation. Use w, a, s, and d to move and Space to fire. In an embed, first click the game area so keyboard input reaches its document. After game-over, choose Play Again below the score to start a new round. Its source combines key state with tick-driven updates; the frame-based movement is specific to this example.

Connect an action to Rust

The panel's button names the method to call when it is activated:

<Button x=24px y=76px width=160px height=28px
    label="Advance" @button_click=self.advance
/>

The corresponding method lives in impl Notes:

#![allow(unused)]
fn main() {
impl Notes {
    pub fn advance(&mut self, _ctx: &NodeContext, _event: Event<ButtonClick>) {
        let next = (self.progress.get() + 0.25).min(1.0);
        self.progress.set(next);
    }
}
}

The binding @button_click=self.advance connects the Button's activation event to a public Rust method. The method receives three things:

  • &mut self gives it mutable access to the Notes instance that owns this template.
  • &NodeContext describes the node handling the event and provides runtime information and operations, such as its bounds and navigation.
  • Event<ButtonClick> carries the event. This particular payload has no fields; the activation itself is enough to advance progress.

The leading underscores in _ctx and _event are Rust's convention for arguments that this method does not use. Other handlers can name them ctx and event and read their contents.

The handler calculates a new value and writes it to the property. The label and progress bar already read that property, so their bindings pick up the change. See Properties for the state operations and PAXEL for the formulas that consume them.

Buttons and custom activation

Use @button_click with the native Button control. For custom content, such as a card or drawing, @click and @tap provide general activation handlers. Both receive Event<Click>, which includes position, button, and modifier information through event.mouse.

Either binding on its own receives mouse clicks and single-touch taps. If both are present on the same node, mouse input selects @click and touch input selects @tap. This lets a custom surface share an action across input types, while still allowing separate actions where needed.

A touch activation occurs at release, after TouchEnd, when the gesture qualifies as a tap. For immediate pressed feedback or dragging, use the lower-level mouse and touch events described under Events and coordinates.

Use event data

An event's type tells you what data the handler receives. These are common starting points:

BindingHandler argumentUseful data
@button_clickEvent<ButtonClick>Activation of a native Button
@click, @tapEvent<Click>event.mouse.x, .y, .button, .modifiers
@mouse_moveEvent<MouseMove>Pointer position through event.mouse
@touch_start, @touch_move, @touch_end, @touch_cancelEvent<TouchStart>, Event<TouchMove>, Event<TouchEnd>, Event<TouchCancel>event.touches, including identifiers and positions
@key_down, @key_upEvent<KeyDown>, Event<KeyUp>event.keyboard.key, .modifiers, .is_repeat
@textbox_input, @textbox_changeEvent<TextboxInput>, Event<TextboxChange>event.text
@checkbox_changeEvent<CheckboxChange>event.checked
@slider_changeEvent<SliderChange>event.value
@viewport_proximity_enterEvent<ViewportProximityEnter>Initial nearby current sample
@viewport_proximity_changeEvent<ViewportProximityChange>previous and current geometry
@viewport_proximity_exitEvent<ViewportProximityExit>Last inside and terminal outside samples

Event<T> exposes its payload fields directly, so event.text is a convenient way to read event.args.text. The event API reference lists the complete payloads, including wheel, drop, and other input events. Sensor availability and native control behavior also depend on the target.

To edit the panel's title, add this Textbox after the panel's outer Group in lib.pax:

<Textbox x=24px y=260px width=320px height=32px
    text={self.title} @textbox_input=self.rename
/>

Add its handler to the Rust file:

#![allow(unused)]
fn main() {
impl Notes {
    pub fn rename(&mut self, _ctx: &NodeContext, event: Event<TextboxInput>) {
        self.title.set(event.text.clone());
    }
}
}

As the user types, the handler copies the event's text into title, and the panel's heading follows. The template supplies the current title to the Textbox; the handler makes the application's response to edits explicit.

On web, @textbox_input follows the browser's input event, while @textbox_change follows its change event, used for committed edits. Choose input for live feedback; choose change when the action should wait for a commit. Exact commit timing belongs to the platform control, so test it on the targets you ship. Forms, two-way bindings, and focus are covered in Native Controls.

Choose the binding's scope

An inline binding belongs to the element where it is written. In the Advance example, the Button is the receiving node, and self is the containing Notes component. Consequently, ctx.bounds_self describes the Button's bounds, even though self.progress is a field on Notes.

A binding in the component's @settings block runs at component scope. For example, to handle button activations that reach Notes, move the binding off the Button and into the template's settings:

@settings {
    @button_click: advance
}

Here both self and the receiving context refer to Notes. Activations from its buttons can reach this handler through event propagation. If you try this alternative, remove @button_click=self.advance from the Button: keeping both bindings calls advance at both places and increments twice.

The same distinction applies when you use a child component. A handler in the child's own settings acts on that child; an inline binding on the child's invocation acts on the containing component. See Components and Composition for the component boundary and Event delivery for propagation.

Do application work

Handlers are ordinary Rust methods. They can validate input, call helpers, update several properties, and call ordinary Rust dependencies. Keep reusable work in a helper when more than one interaction needs it.

For example, let the right arrow key advance the panel as well as the button. Replace the original impl Notes containing advance with:

#![allow(unused)]
fn main() {
impl Notes {
    fn advance_progress(&mut self) {
        let next = (self.progress.get() + 0.25).min(1.0);
        self.progress.set(next);
    }

    pub fn advance(&mut self, _ctx: &NodeContext, _event: Event<ButtonClick>) {
        self.advance_progress();
    }

    pub fn key_down(&mut self, _ctx: &NodeContext, event: Event<KeyDown>) {
        if event.keyboard.key == "ArrowRight" && !event.keyboard.is_repeat {
            self.advance_progress();
            event.prevent_default();
        }
    }
}
}

Keep rename and any other methods in their existing impl blocks. Add the keyboard binding to the template's @settings block, or create that block if there is none:

@settings {
    @key_down: key_down
}

The is_repeat check limits this action to the initial key press. On web, try it while focus is outside the Textbox and other native controls. The keyboard dispatch and default-prevention boundaries are explained below.

Pax calls event handlers synchronously. Keep their work short enough for the interface to remain responsive. For network requests or lengthy work, start a scoped task through an explicit executor. Shared Property handles can receive worker publications; expressions and UI callbacks stay on the UI thread. Declaring a handler async fn produces a compile-time diagnostic: Pax does not implicitly await a handler's return value.

A loading interface can expose a loading flag, a result, and an error as properties. Its template can then describe those states while the application layer manages the request. See Application setup and Asynchronous work below. For other common actions, read Routing for navigation and Motion for easing and timeline control.

Work with component lifecycle

Some work follows the lifetime of a component rather than an input event. Pax recognizes public lifecycle methods on its Rust implementation:

MethodWhen it runsTypical use
on_mountWhen the component mountsInstall computations or subscriptions that need established bindings or context
on_tickOn runtime ticks while the component is activeAdvance an application simulation or other time-driven logic
on_pre_renderAfter tick handlers, in the pre-render phaseSynchronize work that must follow that tick's updates
on_unmountWhen the component is removed from the mounted treeRelease application-owned registrations or finish mounted work

Lifecycle methods take &mut self and &NodeContext, without an Event argument. For a small way to observe the panel's lifetime, add:

#![allow(unused)]
fn main() {
impl Notes {
    pub fn on_mount(&mut self, _ctx: &NodeContext) {
        log::info!("Notes mounted");
    }

    pub fn on_unmount(&mut self, _ctx: &NodeContext) {
        log::info!("Notes unmounted");
    }
}
}

On web, open the app with ?pax_log=info to see these messages in the browser console. The unmount message appears when Pax removes the component from the tree, for example through a template conditional. It is not a guarantee of cleanup on browser or process termination.

If Notes already has an on_mount, add the new work to that method. The computed-property example uses mount to establish a derived label. Ordinary initial values can live in Default; mount is useful for setup that needs the mounted context or property connections. Do not assume that all children have finished mounting or measuring when the parent's mount handler runs.

The short names mount, tick, pre_render, and unmount are also recognized. When both forms exist, the on_* name is selected. To use a different method name, bind it explicitly, for example @mount: initialize in @settings, with a public initialize(&mut self, ctx: &NodeContext) method. An explicit binding replaces automatic selection for that lifecycle event.

Lifecycle bindings also work inline on a child element:

<Group width=100% height=120px @mount=self.panel_mount>
    <Text text={self.title} width=100% height=100% />
</Group>
#![allow(unused)]
fn main() {
impl Notes {
    pub fn panel_mount(&mut self, ctx: &NodeContext) {
        log::info!("{}: panel mounted at {:?}", self.title.get(), ctx.bounds_self.get());
    }
}
}

For inline @mount, @tick, @pre_render, and @unmount, self is the specific component instance that authored the binding (Notes here). ctx describes the child receiving the call (Group here), including its bounds, tree identity, expression stack, and subscription lifetime. Moving that child to a different rendering parent does not change the handler's Rust state owner. Each instance keeps its own state. Reactive bindings created by the handler belong to that authored owner; subscriptions and async work started through ctx retain the child's lifetime.

A child's own lifecycle methods or @settings bindings still run on the child's state. An inline binding on its invocation can run alongside them; it does not replace them or bubble to ancestors. Suspended nodes skip tick and pre-render handlers, but still run mount and unmount handlers when their mounted lifetime changes. These ownership rules apply in debug and release. Component mask-source subtrees have a mounted logical lifetime and follow the same lifecycle ownership rules, even though their output is used as a mask rather than presented directly.

on_tick is driven by the runtime, with no fixed frame-rate guarantee. Use elapsed time for behavior that should progress at a stable real-time rate; ctx.elapsed_frames and ctx.elapsed_millis expose the runtime's clocks. For ordinary visual transitions, start with the facilities in Motion.

Register subtree stores in the provider component's own mount handler with ctx.provide_store(...). The context's node owns the registration, even when an inline handler's Rust self belongs to another component. Descendants can look up a parent's store once that parent has published it during mount.

Final unmount clears the node's store registrations after unmount handlers run, so those handlers can still access the departing scope. A later mount starts fresh state; a saved context cannot access that new mount's stores. Exit transitions can delay final unmount, as described in conditional content.

Subscriptions registered through NodeContext::subscribe are automatically cleared when their node unmounts. Other resources owned by your application still need the cleanup appropriate to that integration. See Subscriptions and effects for callback scheduling and lifetime details.

Viewport proximity is opt-in

Use explicit element bindings for @viewport_proximity_enter, @viewport_proximity_change, and @viewport_proximity_exit. These use the three-argument Event<T> handler form; naming a method on_viewport_proximity_enter does not automatically subscribe it. The observed element can be a Group, but the reference remains the global viewport.

Events are local, do not bubble, and have no cancellable default. Initially nearby content receives enter followed by change. Unmount does not synthesize exit, so resource-owning components also need their normal unmount cleanup. Observe viewport proximity explains snapshots, clipping, fixed margins, and a complete animation policy.

Dispatch custom events

A reusable control can report an application-level action such as “advance,” “save,” or “dismiss.” Its caller chooses how to respond. Use NodeContext::dispatch_event to connect that named action to a handler on the caller.

Consider an Advance control whose internal Button should ask Notes to update progress. Add this declaration to lib.rs, keeping the existing use pax_kit::*; import:

#![allow(unused)]
fn main() {
#[pax]
#[inlined(
    <Button width=100% height=100% label="Advance"
        @button_click=self.activate
    />
)]
pub struct AdvanceButton {}

impl AdvanceButton {
    pub fn activate(&mut self, ctx: &NodeContext, _event: Event<ButtonClick>) {
        ctx.dispatch_event("advance")
            .expect("AdvanceButton requires an @advance binding");
    }
}
}

#[inlined(...)] associates this short template with AdvanceButton. The native Button calls activate on that component. Its handler then dispatches the custom name "advance". The name is a string literal; the method accepts an &'static str. Choose a name for the action your component offers, keeping it distinct from built-in input event names.

Replace the panel's original Button in lib.pax with:

<AdvanceButton x=24px y=76px width=160px height=28px
    @advance=self.advance_from_control
/>

Add the receiver to Notes:

#![allow(unused)]
fn main() {
impl Notes {
    pub fn advance_from_control(&mut self, _ctx: &NodeContext) {
        self.progress.set((self.progress.get() + 0.25).min(1.0));
    }
}
}

The @advance binding on the component invocation registers that receiver. Pressing the Button calls AdvanceButton::activate, which queues advance; Pax then calls Notes::advance_from_control. The two handlers belong to different component instances. The control needs no access to Notes' fields, and the caller can reuse the control with another response elsewhere.

Context and receiver

The context passed to dispatch_event identifies the emitting component. Here ctx is the internal Button's context, whose containing component is AdvanceButton. Pax looks for advance on that component's invocation. In the receiver, self is Notes while the context describes the AdvanceButton invocation, following the inline-binding scope described above.

This mechanism connects a component to its caller. It does not search the tree for a matching name or broadcast the action to every ancestor. Use the context supplied to the emitting handler and connect the action explicitly at the call site. A root component has no outer caller to receive such an action.

Bind custom event names inline, as @advance=self.advance_from_control. Custom names are not currently supported in an @settings handler declaration. That restriction is separate from the built-in bindings, such as @button_click, that settings blocks do support.

Delivery and data

dispatch_event returns Result<(), String>. It checks that the named receiver is registered before adding the event to the runtime's queue. Ok(()) means the event was queued; the receiving handler has not run when the call returns. Delivery happens in the custom-event phase at the end of a runtime tick. Avoid reading state immediately after dispatch under the assumption that the receiver has already updated it.

The example uses expect because the control requires an @advance binding. If the action is optional, handle the returned error according to that component's contract. A missing receiver does not create a new event binding.

Custom callbacks have two arguments, &mut self and &NodeContext, with no Event<T> payload. This API sends a name only: it has no argument for custom data, no return value from the receiver, and no input event to cancel. Use properties or shared state for data the receiver needs, and remember that a queued receiver observes that state at delivery time. If several queued actions each need distinct data, the application must preserve those values explicitly rather than overwrite a single shared field.

The original Button event still follows its ordinary delivery rules. Dispatching advance neither cancels that input nor stops its propagation. If the same application action is also bound to a bubbling @button_click, both paths can run it. Use the custom action as the control's outward interface and keep the native activation handler inside the control.

For split-file components, value inputs, bind:, and scoped stores, continue with Components and Composition.

Event delivery

The remaining sections are useful when interactions span several elements, share shortcuts, or depend on pointer geometry.

Targeting and propagation

Pointer input begins with hit testing: Pax finds the topmost eligible node under the pointer. Remember that earlier siblings in a Pax template are drawn above later siblings; see Templates. Native controls can also send events directly to their corresponding node, as a Button does for @button_click.

Common pointer and control events then travel from the receiving node through its template parents. This is event bubbling. Each visited node runs its applicable handlers with its own context, which is why an inline handler and a component-level handler can both observe one action.

The route follows template ancestry. Projection and component composition can make that ancestry different from the visual container arrangement. Do not infer the entire event path from which rectangles overlap on screen.

Not every event family bubbles. For example, @mouse_over and @mouse_out dispatch locally, while keyboard events use the global delivery described below. The click/tap choice described earlier is made at each node visited during activation propagation.

Default actions

event.prevent_default() requests that the chassis suppress the platform's default response. The arrow-key handler uses it to request that the browser not perform its normal arrow-key action for that handled press.

Default prevention leaves Pax's handler delivery intact: other handlers and template parents still receive the event. The current Event API has no stop_propagation method.

Whether the request can suppress an action depends on the chassis and event. On web, it also depends on the browser listener's cancellation rules; the current touch-start, touch-end, and touch-cancel listeners are passive. Do not rely on default prevention as a portable way to take ownership of a gesture or prevent a native control from changing its value.

Keyboard delivery

The global @key_down, @key_up, and @key_press event bridge is implemented on web, macOS, and iOS/iPadOS with a hardware keyboard. The binding and Rust handler shown above work on each of these targets.

Keyboard events currently dispatch across the mounted Pax tree, so multiple components with keyboard handlers may receive the same key. If a shortcut belongs to one active view or mode, check that application state in the handler before acting on it.

On web, the chassis forwards these global keyboard events only while the document body is the active element. When a Textbox or another native DOM element has focus, its keyboard input stays with that control.

On Apple targets, the Pax canvas receives keys as the native first responder. A standalone app initially focuses its canvas. An embedded mount preserves the host's focus; click or tap Pax's rendered content to focus the canvas. Native text controls and the host's own controls retain their normal input paths. Losing focus, deactivating the app, or closing the mount releases held keys through @key_up. A component's @key_down binding does not by itself give that component keyboard focus.

event.keyboard.key carries the typed character, or names such as Enter, Backspace, Delete, Tab, Escape, and ArrowLeft. Modifiers use ModifierKey::{Shift, Control, Alt, Command}. macOS preserves the native repeat flag; on iOS, repeated key-down deliveries before a key-up are marked as repeats. Pax does not synthesize a repeat timer on iOS. Physical keyboard layout and system shortcuts still affect which events the OS delivers.

On Apple targets, calling event.prevent_default() suppresses forwarding that key to the next native responder. Unhandled keys continue through the native responder chain. Cancelling @key_down also suppresses the legacy @key_press event; otherwise it is emitted for printable characters and Enter without Control or Command. Prefer @key_down/@key_up for shortcuts and held-key state. These canvas events do not implement IME composition or summon an iOS software keyboard; use native text controls for platform text entry.

Read Native Controls for the control-specific focus and accessibility model.

Events and coordinates

Mouse and touch positions use Pax's window coordinate space. An interaction inside a translated, scaled, rotated, or scrolling node needs local coordinates. Use ctx.local_point to convert through the node handling the event.

For example, add a pad below the title editor:

<Rectangle x=24px y=320px width=320px height=100px
    fill=rgb(220, 216, 206) corner_radius=12
    @mouse_move=self.track_mouse
/>

Import Point2 alongside the existing pax_kit import and add this handler:

#![allow(unused)]
fn main() {
use pax_kit::math::Point2;

impl Notes {
    pub fn track_mouse(&mut self, ctx: &NodeContext, event: Event<MouseMove>) {
        let local = ctx.local_point(Point2::new(event.mouse.x, event.mouse.y));
        self.progress.set(local.x.clamp(0.0, 1.0));
    }
}
}

Moving across the pad maps its left edge to zero progress and its right edge to one. local_point returns normalized coordinates: (0, 0) is the node's local origin and (1, 1) is its opposite corner. To work in local pixel-like units, read (width, height) from ctx.bounds_self.get() and multiply local.x * width and local.y * height. These are layout units, not a promise of physical display pixels.

The conversion includes the node's transform and ancestor Scroller presentation offsets. Convert inside the handler using its current context; subtracting only the element's x and y misses rotation, scale, and moving scroll content. Read more about Layout and Scrolling.

Touch sequences and capture

For a custom touch interaction, use @touch_start, @touch_move, @touch_end, and @touch_cancel. Each payload contains touches with a position, movement deltas, and an identifier. Retain the identifier your interaction accepts so subsequent events can be matched to it.

The primary touch captures the topmost hit node at TouchStart. While that capture remains valid, subsequent move, end, and cancel events target it even when the finger leaves its bounds; the usual template-parent delivery still applies. This is useful for keeping a drag attached to its original control. It does not establish equivalent mouse capture or a general multi-touch gesture recognizer.

Clear pressed or dragging state in both end and cancel handlers. TouchEnd finishes a normally released sequence. TouchCancel handles an interrupted sequence, such as one aborted by the platform. A cancelled sequence does not synthesize a click or tap. Use the activation event for the final action and the lower-level events for transient feedback when those responsibilities need to stay separate.

Touches inside a Scroller

On iOS and iPadOS, the native Scroller's touch observation lets custom child content receive the start immediately and continue receiving moves while scrolling. Normal release produces TouchEnd; recognition of a scroll disqualifies tap activation without requiring cancellation of that lower-level stream. This allows a child to light up on contact, follow the touch, and clear its feedback when the finger lifts.

A qualifying release emits one tap. The innermost native Scroller owns the touch sequence; ancestor Scrollers do not forward it again. Native controls keep their own action delivery. Bind either @click or @tap for a custom key's activation, and use touch end/cancel only to clear pressed feedback.

Because local conversion includes the moving content transform, a contact that moves with the scroll stays approximately fixed on its original child in that direction. Native controls and platform gestures can impose further input behavior, so test the particular control and target together. On web, tap qualification also uses a movement tolerance; a moved touch sequence need not produce activation on release.

Application setup

Native applications own a normal Rust fn main on macOS, iOS and iPadOS. pax_kit::init!()?.run() prepares the separate UI cartridge and enters the platform host loop in that process. Initialization does not parse application arguments, start development tools, or create an async runtime, even with designtime enabled. Web pages explicitly call Pax.init_web; importing a component library does not launch an app. See entry and mounting for Cargo targets, supplied host views and browser elements.

Inside the library, select an ordinary typed initializer in runnable!:

#![allow(unused)]
fn main() {
use pax_kit::*;

pax_kit::runnable! { root: crate::Notes, init: init_notes }

#[pax]
#[file("lib.pax")]
pub struct Notes { pub title: Property<String> }

fn init_notes(cx: &InitContext) -> AppResult<Notes> {
    let _target = cx.target;
    Ok(Notes { title: Property::new("Field notes".into()) })
}
}

The initializer runs once on the UI owner thread, inside the application's property graph. Pax mounts the returned value itself: it does not serialize it or create another default root. Without init:, the selected root uses Default::default(). Using a component from a dependency does not initialize or launch that dependency's cartridge. Root selection is independent of component location; see runnable declarations.

Keep initialization prompt. Return a real Loading root, stage asynchronous work through an explicit executor, and update its properties when that work completes. Neither returning a Loading root nor awaiting host readiness waits for the data request. Synchronous blocking code in the initializer still blocks the host thread. Target facts are available during initialization; actual viewport bounds are established by mounting.

Dependencies and application lifetime

Dependencies are ordinary Rust fields, constructor inputs or lexical component stores. Mark a field #[pax(rust)] when it owns a Rust value that must stay out of the template value system, reflection and baked program data. Such fields still need the component's usual Clone, Debug and Default behavior; an Option<Rc<Resources>> is useful for resources supplied by the initializer. The field cannot be bound from a template or reconstructed from serialized Pax values. See the Async Workbench source for a complete portable example.

OwnerSuitable stateLifetime
Component propertiesPresentation and interaction stateComponent instance
Lexical local storeDependencies shared within a subtreeProviding node
Initialized root fieldsConfiguration, clients, shared state, executor clientsApplication instance, across child remounts

The root can provide a cloned dependency to descendants with a lexical Store. See component stores and the shared/local property boundary. ctx.application() exposes weak lifetime and cancellation access; it provides no dependency lookup. Cloning it does not keep a closed application active.

Migration: replace #[application(Type)] and its Application implementation with runnable! { root: Root, init: function }. Construct and return the root in that function; replace AppBuilder::provide and AppContext::service with ordinary fields, explicit arguments or lexical stores. Move UI-specific work from started into the root's mount handler. There is no replacement lifecycle-hook trait.

Executor and resource shutdown

Executor adapters retain their cleanup owners with the application. Closing first invalidates work and unmounts the tree, then starts asynchronous cleanup. The supplied root and its Rust dependencies remain allocated until cleanup acknowledges completion. The two-second diagnostic deadline reports unfinished cleanup; it never pretends a blocked task stopped. An AppContext::closed() observer does not drive cleanup, and dropping it does not stop host wakeups. Native cartridge code stays loaded while it can still be used.

Ordinary Rust destructors must return promptly. Pax does not turn arbitrary blocking Drop implementations into asynchronous cleanup. Task error reporting continues during retained shutdown; its sink must avoid detached UI once the application is Closing.

Asynchronous work

Event handlers start work and return so input and rendering can continue. Pax tracks work against an application, node or operation lifetime and wakes the UI when results are ready. Do not poll a channel from @tick or block the UI loop with Runtime::block_on.

Set up Tokio

Add the adapter under the application's native dependencies:

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
pax-tokio = "0.39.0"
tokio = { version = "1", features = ["rt-multi-thread", "time", "net"] }

You construct and configure the exact runtime. The adapter does not initialize another Tokio instance. In a native root initializer:

#![allow(unused)]
fn main() {
fn init_notes(cx: &InitContext) -> AppResult<Notes> {
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .enable_all()
        .build()?;
    let executor = pax_tokio::Executor::owned(runtime, cx.lifetime())?;
    Ok(Notes {
        title: Property::new("Loading".into()),
        executor: Some(executor),
    })
}
}

For this variant, add #[pax(rust)] pub executor: Option<pax_tokio::Executor> to Notes. A synchronous handler can then use that explicitly supplied client:

#![allow(unused)]
fn main() {
impl Notes {
    pub fn load_title(&mut self, ctx: &NodeContext, _: Event<ButtonClick>) {
        self.executor.as_ref().unwrap().for_node(ctx).unwrap()
            .spawn_into(&self.title, async {
                tokio::time::sleep(std::time::Duration::from_millis(600)).await;
                "Ready".to_owned()
            }).unwrap();
    }
}
}

Bind a Button's @button_click=self.load_title to try it. Construct Tokio I/O and timers inside the async block, where its executor is active. UI callbacks do not implicitly enter Tokio; free-standing tokio::spawn in such a callback is not the integration pattern. The portable Workbench selects the appropriate executor through a target-specific module.

Choose ownership and result delivery

spawn_into creates a cancellable operation and publishes its return value through a guarded writer. Cancellation or unmount revokes that writer before requesting task abortion. A late result cannot overwrite state written after revocation. A value already committed is not rolled back. Keep the returned TaskControl to cancel it or observe TaskStatus; dropping the control leaves the work owned by its lifetime. A task panic is reported separately from an application's ordinary Result<T, E>. Recoverable panic reporting requires an unwinding build; panic=abort, including the usual browser build, terminates execution instead.

for_node(ctx) uses the exact node in the context. An inline button handler usually receives that button's context. For component lifetime, retain a bound helper or scope from the component's on_mount, for example in a lexical local store. for_application(&app) deliberately selects application lifetime. Neither an Rc dependency handle, NodeContext, nor LocalProperty can be sent into a native task; native task futures must be Send + 'static.

For replaceable requests, retain an operation scope. Calling owner.replace(&mut previous, &result, loading_state) revokes the previous operation before publishing the new loading state and allocating its successor. Pass the resulting scope to executor.for_scope(scope). Explicit cancellation likewise calls scope.cancel() before setting a Cancelled result. The workbench contains a retained stale producer that ignores cancellation and still cannot publish into its replacement.

A task may also capture raw shared Property clones using spawn. Such writes are application data publications: cancelling its scope does not revoke those clones. Use this for intentionally retained shared state, or use a guarded publisher from an operation scope when cancellation must remove write authority. Intermediate property revisions may coalesce. Worker reads see published snapshots, while the UI imports them into its local graph at safe boundaries.

Use spawn_with_completion(future, callback) to manipulate local state after work completes. The callback receives TaskOutcome<T> on the UI thread and may capture local properties. An executor-independent scope.completion(callback) returns a transferable, single-use endpoint for ordinary threads or callback APIs. Dropping an unused endpoint releases its callback on the next UI turn.

For every accepted event to be delivered in order, use scope.channel(capacity, callback). Keep its UiSubscription; dropping it closes the channel. Producers use try_send for explicit Full/Closed errors, or await send for capacity without blocking. Accepted values may still be discarded if the lifetime closes before delivery. Callbacks run after structural settlement and settle their writes before the next callback; queued work cannot call back into a node that was just removed.

Pax bounds asynchronous resource counts. Defaults are 1,024 scopes, tasks, publishers and endpoints each, 4,096 total message slots, and capacity 32 for default_channel. Dispatch rotates ready channels and uses at most 64 callbacks or two milliseconds per turn. A slow callback cannot be preempted; keep callbacks short and bound the size of payloads separately. Hosts schedule work on publication/delivery wakes; applications need no channel polling in @tick.

On macOS, initial layout starts the application independently of the display clock. If CoreVideo cannot start while displays are unavailable, the host retries on display/session wake or application activation. Async delivery still uses its own wake route. Temporary canvas reattachment does not close the application; window close or canvas disposal does.

Runtime ownership and target limits

Executor::owned accepts a multithread runtime and moves its eventual destruction to an owner thread. A started spawn_blocking job cannot be forcibly aborted, so shutdown may remain pending until it finishes. Bound blocking work and use cooperative cancellation. Executor::borrowed(handle, lifetime) leaves the runtime alive; its external owner must continuously drive it and keep it alive through adapter shutdown. A dedicated service thread can drive a current-thread runtime and a LocalSet, keeping its non-Send state on that thread. This does not make UI nodes transferable. The external driver must outlive the adapter's cleanup acknowledgement. A Rust Runtime or Handle can be shared only within the same Rust linkage boundary; do not pass it from the native launcher through the cartridge C ABI.

The adapter's raw handle() is available for advanced integrations. Tasks spawned through it bypass Pax staging, tracking and scope cancellation. Scoped tasks created during initialization or mounting wait until host activation. Discarding preparation never starts them. Pax-only reload preserves the initialized root and its dependencies; actual node unmounts cancel their scopes. Rust changes require rebuilding and restarting.

Browser initializers can construct pax_web_async::Executor::new(cx.lifetime())? under Wasm-only dependencies and use the same bound helper methods. Browser futures need not be Send. They run on the browser thread and use browser timers and fetch, not desktop Tokio drivers. Dropping a future only cancels its Rust polling; the I/O implementation must release or abort external resources. Browser suspension, mobile backgrounding and abrupt process termination do not guarantee cleanup or continued execution. Native I/O remains subject to platform permissions. The fixture README at examples/fixtures/async-workbench/README.md records which targets and build modes have actually been qualified.

For networking in a sandboxed macOS release, explicitly configure the required network entitlements. The same section explains iOS local-network privacy and background-execution limits. Using Tokio does not grant operating-system permissions.

Try Async Workbench

The repository's examples/fixtures/async-workbench is a complete application using these APIs. From the repository root, with a built pax-cli, run one target:

pax-cli run --path examples/fixtures/async-workbench --target macos
pax-cli run --path examples/fixtures/async-workbench --target web
pax-cli run --path examples/fixtures/async-workbench --target ios --ios-device simulator

For the sandboxed native release, use pax-cli build --path examples/fixtures/async-workbench --target macos --release and open the resulting app. The fixture opts into both macOS network entitlements. Its Cargo dependencies select Tokio on native targets and browser futures on web.

Use the fixture to explore ownership and delivery:

  1. Choose Run · 600 ms, then Interact. Input remains usable while the executor's timer runs; the result updates without application @tick polling.
  2. Choose Replace A with B. B wins; the retained producer for A cannot publish after its authority has been revoked.
  3. Start work and Cancel, or Unmount the panel. Unmount discards the panel's local state and revokes pending results. Remount creates fresh panel state and scopes; the application-owned counter, history, and dependencies remain.
  4. Choose Stream 100 events. The bounded channel delivers ordered events; a full buffer is handled explicitly rather than dropping arbitrary messages.
  5. Choose Run local I/O. Native performs a loopback TCP exchange; web fetches its bundled sample. Local callback demonstrates ordinary-thread completion into UI-local state.

Start reading src/application.rs for the initializer and application data, src/work_service.rs for executor setup and platform I/O, and src/request_panel.rs for scoped requests, cancellation, and delivery. The fixture's README records debug/release qualification and remaining target limits; its existence is not a claim that every mobile lifecycle scenario has passed.

API details: application lifetime, managed shutdown, async scopes and delivery, Tokio adapter, and browser adapter.

Read further

Continue with Components and Composition for reusable component interfaces, shared state, and custom event contracts.

Components and Composition

A panel becomes more useful when it can appear in several places, with a different title or progress value each time. A component gives that panel a name and an interface: the values it accepts, the actions it offers, and the content its caller can supply.

This chapter extracts the Field notes panel from Properties and Events and Rust into a small reusable card. It then builds on the same boundary to explain conditional views, lists, slots, and shared state. If you are new to Pax source, start with Templates.

Make a reusable component

Create src/note_card.rs and src/note_card.pax beside your application's lib.rs and lib.pax:

src/
├── lib.rs          Notes: application state and actions
├── lib.pax         places cards and supplies their inputs
├── note_card.rs    NoteCard: its public properties and defaults
└── note_card.pax   the card's internal interface

In src/note_card.rs, declare the card's inputs:

#![allow(unused)]
fn main() {
use pax_kit::*;

#[pax]
#[file("note_card.pax")]
#[custom(Default)]
pub struct NoteCard {
    pub title: Property<String>,
    pub progress: Property<f64>,
}

impl Default for NoteCard {
    fn default() -> Self {
        Self {
            title: Property::new("Untitled note".to_string()),
            progress: Property::new(0.0),
        }
    }
}
}

The type has #[pax] and an associated template, like any root component. A separate runnable! declaration selects the application root. Public properties form the card's value interface; here progress uses a zero-to-one range. The custom default provides useful values when a caller omits an input. Each new card instance gets its own initial properties.

Give src/note_card.pax the display portion of the panel:

<Text x=24px y=24px width={100% - 48px} height=36px
    text={self.title}
    style={font_size: 24px, fill: rgb(32, 40, 48)}
/>
<Text x=24px y=72px width={100% - 48px} height=28px
    text={"Progress: " + (self.progress * 100) + "%"}
    style={font_size: 16px, fill: rgb(72, 80, 88)}
/>
<Group x=24px y=116px width={100% - 48px} height=12px>
    <Rectangle width={(self.progress * 100)%} height=100%
        fill=rgb(55, 120, 90) corner_radius=6
    />
    <Rectangle width=100% height=100%
        fill=rgb(220, 216, 206) corner_radius=6
    />
</Group>
<Rectangle width=100% height=100%
    fill=rgb(246, 241, 230) corner_radius=12
/>

The card uses the layout area supplied by its caller. The percentage widths follow that area; the fixed offsets give this example its padding. Its background remains last so the text and bar render in front. For sizing and container choices, read Layout and Responsiveness.

Use the card

Here is a complete src/lib.rs for the two-card example:

#![allow(unused)]
fn main() {
use pax_kit::*;

pub mod note_card;
pub use note_card::NoteCard;

pax_kit::runnable! { root: crate::Notes }

#[pax]
#[file("lib.pax")]
#[custom(Default)]
pub struct Notes {
    pub title: Property<String>,
    pub progress: Property<f64>,
}

impl Default for Notes {
    fn default() -> Self {
        Self {
            title: Property::new("Field notes".to_string()),
            progress: Property::new(0.25),
        }
    }
}

impl Notes {
    pub fn advance(&mut self, _ctx: &NodeContext, _event: Event<ButtonClick>) {
        self.progress.set((self.progress.get() + 0.25).min(1.0));
    }
}
}

pub mod includes the Rust module, and pub use makes NoteCard available at the crate root. This is a straightforward pattern for a project's reusable components. The .pax file is associated through #[file(...)]; it needs no separate Rust module declaration.

Template paths are looked up from the package root, with a src/ fallback. If you move the pair into a deeper directory, update that path explicitly, for example #[file("src/cards/note_card.pax")], along with the Rust module wiring. The path is not relative to the Rust file containing the attribute.

In src/lib.pax, place two cards and an application-level button:

<Button x=24px y=24px width=160px height=28px
    label="Advance" @button_click=self.advance
/>
<NoteCard x=24px y=72px width=320px height=160px
    title={self.title} progress={self.progress}
/>
<NoteCard x=24px y=248px width=320px height=160px
    title="Sketchbook" progress=0.75
/>

The first card starts at 25 percent and follows the application's progress. The second displays 75 percent independently. Pressing Advance changes only the first card. Remove the second card's title and progress attributes to try its defaults: “Untitled note” and zero progress.

Restart the app after adding the Rust files and declarations. The default hot-reload lane updates template edits; see Developer Workflow for the application-logic reload options.

Declare a runnable UI

runnable! declares a Pax UI that can be launched as an application or embedded in a host. It selects an ordinary component as the root, connects its optional initializer, and generates the host-facing entry functions. The component can also be used as a subtree in another application. The declaration itself runs no initialization and mounts no UI; a host invokes those entry functions later. The compiled UI remains a cartridge with a separate linkage boundary. src/main.rs remains the native process entry and does not need to contain UI.

For a nested layout, put this in src/lib.rs:

#![allow(unused)]
fn main() {
pub mod ui;
pax_kit::runnable! { root: crate::ui::App, init: crate::ui::init }
}

Put the component and initializer in src/ui/mod.rs:

#![allow(unused)]
fn main() {
use pax_kit::*;

#[pax]
#[file("src/ui/app.pax")]
pub struct App { pub title: Property<String> }

pub fn init(_: &InitContext) -> AppResult<App> {
    Ok(App { title: Property::new("My application".into()) })
}
}

src/ui/app.pax can contain <Text text={title} width=100% height=100%/>. Template paths remain relative to the package root, with a src/ fallback; they are not relative to the Rust module. Nested inline and file modules are discovered statically. Use an ordinary Rust path or explicit use/pub use for the selected type. A public re-export can expose a component from a private module. Generated descriptors use an accessible qualified path and preserve one type identity across imports in the cartridge.

When publishing a reusable component library, expose every component and value type that its templates need to the consuming cartridge. For a transitive library, an explicit pub use controls::Counter; in your library's public API lets code generation reach that type through your library. Alternatively, the consuming cartridge can depend directly on controls. Generated descriptors follow normal Rust visibility; they cannot reach private, unexported component internals.

init: is optional and defaults to App::default(). When supplied, Rust checks that the function accepts &InitContext and returns AppResult<App>. The compiler does not run main or init to discover UI. Multiple runnable! declarations share one catalog of component definitions. Repeated declarations of a root are deduplicated; a root must have one unambiguous initializer. With several distinct roots, choose a canonical path through InitOptions::entry, such as Some("crate::ui::App".into()), or entry in the browser mount options. Initializers run only for the selected mount, not when a runnable is used as a child component. One active native mount is supported at a time.

Rust selects cfg and cfg_attr in each owning crate, including dependency features, target conditions, tests, and custom build-script cfgs. Disabled modules and templates are not loaded. Definitions produced by arbitrary Rust macros are not source-discoverable; keep component and runnable declarations in ordinary modules. Compilation caches parsed source and shares the catalog across runnable declarations. Keep slow work asynchronous after returning the initial root; see application setup.

Migration: remove #[main] and #[init(path)] from the component, then add pax_kit::runnable! { root: YourRoot, init: path } beside the module declarations. The old #[main] plus optional #[init(path)] remains a compatibility shorthand when no explicit declaration exists. Mixing the legacy shorthand with explicit declarations produces an error. Library use does not initialize or mount the application; each cartridge keeps its own platform entrypoints and immutable program definitions.

Use a separate UI package

In a Cargo workspace, a host package can own src/main.rs while a sibling UI package owns the library, declaration, initializer, templates, and assets. Both packages must be workspace members. Add this to the host Cargo.toml:

[dependencies]
my-ui = { path = "../my-ui", default-features = false }

[package.metadata.pax.cartridge]
package = "my-ui"
lib = "my_ui"

package is the Cargo package name. Optional lib checks the selected library target name (including a custom [lib].name); Cargo permits one library target per package. Without the table, Pax selects the host package's own library. The UI library needs crate-type = ["rlib", "cdylib"] and its Pax dependencies. The host needs the native launcher target when building for Apple. The direct dependency lets Cargo schedule the UI artifacts; metadata alone does not add a Cargo dependency. init!() selects that library through its metadata helper, including renamed dependencies, while preserving the separate native cartridge boundary.

Run from the host, or use pax-cli run --path path/to/host --target web. Pax reads platform metadata and web public/ from the host and writes generated .pax/ output there. It compiles the selected UI library using Cargo's workspace target directory, including a configured CARGO_TARGET_DIR. UI template paths and assets/ resolve from the UI package. Assets from referenced component packages are included too; give differing files distinct paths under assets/ to avoid collisions. Identical shared files may use the same path.

Debug sessions watch the host, selected UI package, and referenced template and asset directories. Template reload and dev inspection use their real source paths. Rust changes still require rebuilding and restarting. Debug and release use the same root selection and qualified component identities. The repository fixture tests/src/cartridge-layouts exercises this layout, including a reused legacy root exposed through a public re-export.

Inputs and state ownership

In lib.pax, {self.title} reads Notes.title. Inside note_card.pax, the same expression reads that particular NoteCard.title. The attribute on the invocation connects the two scopes. A child template can use its own fields without knowing the containing application's type or variable names.

Values supplied by the caller take the place of the child's defaults for those inputs. Use defaults for fallback state, and avoid unconditionally writing caller-supplied inputs in a mount handler. Such a write can replace the displayed value even though the parent is meant to own it.

An ordinary binding such as progress={self.progress} derives the child's input from the parent. It does not provide write-through to the parent. When the parent owns progress, have application actions update that source. The card's label and bar then follow its input. Properties explains what happens if Rust writes directly to a computed field.

For a component designed to edit a shared value, bind: explicitly connects the same property handle at both ends:

<NoteCard x=24px y=72px width=320px height=160px
    title={self.title} progress=bind:progress
/>

The read-only card looks the same. If it gains an editing handler that calls self.progress.set(...), that handler can now update Notes.progress too. Both fields must have compatible property types; bind: takes a property identifier, not an arbitrary formula. Use it when editing that value is part of the component's intended interface. See PAXEL for the binding syntax and property handles for the Rust model.

For example, ExampleHost's source tabs bind to their own host's selected_source property. Clicking a tab changes that drawer only; sibling hosts keep independent selections. They do not need a local store for this direct parent/child connection. Callers can deliberately bind multiple hosts to the same selection property when synchronized selection is wanted.

State used only inside one card can stay on that card. State shared by several cards usually belongs in their common owner. For an action such as “advance this note,” a named event can let the owner decide how state should change; we will build that connection below.

Data-driven components

A component invocation can be part of a conditional branch or a repeated group. The template describes which instances should exist for the current state; Rust changes that state in response to actions.

Conditional content

For example, replace the first card with a completion message once progress reaches one:

<Group x=24px y=72px width=320px height=160px>
    if self.progress < 1.0 {
        <NoteCard width=100% height=100%
            title={self.title} progress={self.progress}
        />
    } else {
        <Text width=100% height=100% text="Ready for the next idea."
            style={font_size: 20px, fill: rgb(55, 120, 90)}
        />
    }
</Group>

Conditions use PAXEL directly after if; the braces enclose template content. An else if chain can choose among several states, and else is optional. Pax uses the first true branch, or the final else when present. Each branch can contain several elements.

Changing branches removes the old content and mounts the new content. Local state belongs to those component instances: if a card is removed and later created again, its local state starts again. Keep state that must survive that change in Notes or another longer-lived owner. Exit transitions can retain departing content temporarily, and rapid re-entry can reuse it; see Animation and Motion for that lifecycle.

A structural if controls which elements exist. A conditional inside @settings changes settings on existing elements. Choose according to whether the interface needs different content or different styling.

Ranges and collections

Use for to repeat template content. A range is useful for a fixed set of decorative elements or positions:

for i in 0..3 {
    <Text x=24px y={(24 + i * 32)px} width=240px height=28px
        text={"Position " + i}
    />
}

This produces positions 0, 1, and 2; the upper bound is excluded. A loop receives each value as its item. With for (item, i) in ..., the second binding is its current zero-based index. Bindings are available inside that loop's body; nested loops can give their own items and indices distinct names.

For application data, add a small record to lib.rs:

#![allow(unused)]
fn main() {
#[pax]
pub struct NoteEntry {
    pub id: usize,
    pub title: String,
}
}

Add pub entries: Property<Vec<NoteEntry>> to Notes. In its existing Default implementation, initialize that field with:

#![allow(unused)]
fn main() {
entries: Property::new(vec![
    NoteEntry { id: 10, title: "Field notes".to_string() },
    NoteEntry { id: 20, title: "Sketchbook".to_string() },
    NoteEntry { id: 30, title: "References".to_string() },
]),
}

The record's plain fields describe one value in the collection. The outer property publishes changes to the collection; use set or update as described in Properties.

Keyed lists

Replace the two individual cards with a list:

<Group x=24px y=72px width=320px height=528px>
    for (entry, i) in self.entries key entry.id {
        <NoteCard y={(i * 176)px} width=100% height=160px
            title={entry.title}
        />
    }
</Group>

Here i determines position, while entry.id determines identity. If the entries change from [10, 20, 30] to [30, 10, 20], Pax reuses the existing component groups in their new order. Local state, such as a card's expanded view or an in-progress edit, follows the entry with the same key. New keys mount new groups; removed keys leave the active list and can play exit transitions. A loop body may contain several elements, which share that iteration's identity.

Choose a string or integer key that is stable for the item's lifetime and unique within that loop. The current index is generally a poor key for a list that can be reordered. Duplicate or unsupported key values cause a runtime warning and positional fallback, so check the data instead of relying on that fallback to preserve state.

An unkeyed loop reuses positions. It is useful for fixed grids and repeated decoration; for mutable application lists, use an item identifier. Keys govern reuse within their own loop, not identity across unrelated loops or branches. Layout remains a separate choice: this example uses explicit row positions; Stacker and Scroller can arrange or reveal a larger collection.

Explore Transition Grid to see a keyed list being inserted into, removed from, and reversed. Its child components animate while Stacker owns their placement. The tile counters are stored in the parent collection; the child's animated color is local component state.

Slots

Sometimes the reusable part is a frame around content its caller supplies. A card frame might reserve a header area while accepting text, controls, or another component in its body. Slots specify where that caller-provided content appears inside the frame's template.

Add a component declaration to lib.rs:

#![allow(unused)]
fn main() {
#[pax]
#[file("note_frame.pax")]
pub struct NoteFrame {}
}

In src/note_frame.pax, use one explicit slot and a remainder slot:

<Group x=24px y=24px width={100% - 48px} height=32px>
    slot(0)
</Group>
<Group x=24px y=72px width={100% - 48px} height={100% - 96px}>
    slot()
</Group>
<Rectangle width=100% height=100%
    fill=rgb(246, 241, 230) corner_radius=12
/>

Use it from lib.pax with content between its opening and closing tags:

<NoteFrame x=24px y=72px width=320px height=220px>
    <Text width=100% height=100% text={self.title}
        style={font_size: 24px, fill: rgb(32, 40, 48)}
    />
    <Text width=100% height=32px text="Make room for the next idea."
        style={font_size: 16px, fill: rgb(72, 80, 88)}
    />
    <Button y=48px width=100% height=36px label="Advance"
        @button_click=self.advance
    />
</NoteFrame>

slot(0) projects the first child into the header area. slot() projects all children not consumed by an earlier active slot site in this component; here the body Group receives the second Text and the Button. The caller positions those body elements within the area the frame supplies. The slot syntax is positional, with zero-based indices. These children remain authored in Notes: their self.title and self.advance still refer to Notes, even though the frame supplies their layout containers.

A component can forward its slots through another component. For example, placing slot(0) inside a Scroller keeps that slot associated with the component that authored it. The Scroller's own slots handle its received content separately; they do not consume the outer component's children.

Slots project the existing child content. They do not make a new independent copy at each insertion site. An explicit index that is out of range or already consumed renders empty and produces a warning. An empty remainder slot is valid and silent. Put fixed slot sites before the remainder site; an earlier slot() can consume everything before a later explicit site gets a chance.

Consumption follows the active slot sites in template-tree order. Conditional sites or a changing slot index can change which children reach the remainder. Likewise, if and for in caller content can change the currently available projected children. Wrap several elements in one Group when they should be supplied together as one child. The slot-projection-resolver example explores these dynamic cases.

The frame's private background and layout structure stay inside its template. Caller content and private implementation structure serve different roles; container code should use NodeContext::received_children for its semantic content rather than treating all runtime descendants as supplied children. Custom container internals are beyond this chapter.

Component actions

A reusable component can name an action without prescribing the application's response. For example, an Advance control can ask its owner to advance a note. The owner may update progress, validate some data, or choose another note.

The complete custom-event example in Events builds an AdvanceButton that dispatches advance to a handler on Notes. Use that pattern when the component should express an intent and leave the state change to its caller. Use an input for a value the component reads, and an explicit bind: when editing the shared value is part of its interface.

Custom events carry a name only, so design their data interface alongside the component's properties or shared state. Keep the action's required or optional binding explicit. Events owns the sender/receiver code, context, delivery timing, and error behavior; this chapter's component boundary is what makes that connection reusable.

Shared state farther down the tree

Explicit inputs and actions keep a small component easy to reuse. For state needed throughout a larger subtree, a store lets descendants find a shared value without passing it through every intermediate component. The provider owns that registration for its mounted lifetime.

Provide and borrow a store

Define a type that names the store's purpose in lib.rs, alongside Notes. Store, Property, and NodeContext are available through use pax_kit::*;:

#![allow(unused)]
fn main() {
pub struct NotesStore {
    pub progress: Property<f64>,
}

impl Store for NotesStore {}

impl Notes {
    pub fn on_mount(&mut self, ctx: &NodeContext) {
        ctx.provide_store(NotesStore {
            progress: self.progress.clone(),
        })
        .expect("Notes is mounting");
    }
}
}

If Notes already has on_mount, add this registration to that method. Cloning the property shares the owner's handle. The store is ordinary Rust data; it does not need #[pax] or expose every field on Notes.

A descendant can borrow the store to obtain that handle. Add this component to lib.rs:

#![allow(unused)]
fn main() {
#[pax]
#[inlined(
    <Button label="Reset progress" @button_click=self.reset />
)]
pub struct ResetProgress {}

impl ResetProgress {
    pub fn reset(&mut self, ctx: &NodeContext, _event: Event<ButtonClick>) {
        let progress = ctx
            .with_store(|store: &mut NotesStore| store.progress.clone())
            .expect("ResetProgress needs a NotesStore provider");
        progress.set(0.0);
    }
}
}

Place two instances in the Notes template, wherever the layout has room:

<Group width=360px height=36px>
    <ResetProgress width=172px height=36px />
    <ResetProgress x=188px width=172px height=36px />
</Group>

Both find the same NotesStore registration and clone the same progress handle. Either button resets the owner's progress, including the existing label and bar bound to it. Intermediate components need no forwarding code.

Keep the borrowed-store closure short. It exclusively borrows that store until it returns; cloning a property inside the closure lets later reads, writes, and callbacks happen after the borrow ends. To display store state in another component, connect the handle to one of that component's fields using a computed property, as the store-ownership example does in CounterConsumer::mount. Stores are looked up from Rust, not directly from PAXEL. Plain Rust fields in a store do not become reactive automatically.

Types, sharing, and shadowing

The concrete Rust type is the lookup key: the &mut NotesStore annotation selects the provider. No string ID, child position, or traversal path is needed. A store's contents can use your own enums, record IDs, or maps; they are not tied to indices in the rendered tree.

Lookup starts at the context's node and walks its logical ancestors. The nearest provider of the requested type wins. It never searches siblings or children. That gives these composition rules:

ArrangementResult
Two consumers below one Notes providerBoth share its progress handle.
Two sibling Notes providersEach owns its own store; their descendants remain independent.
A Notes provider nested inside anotherThe inner provider shadows NotesStore for its subtree. The outer state is unchanged.
An ancestor provides a different store typeIt remains available through the inner provider.

Shadowing makes a reusable subtree independent without making its consumers know where it is placed. For example, each editor panel can provide its own selection store, while all panels still find a shared application theme.

Use distinct structs or newtypes when two roles need separate stores with the same payload. A Rust type alias does not create a different lookup key:

#![allow(unused)]
fn main() {
pub struct PrimaryProgress(pub Property<f64>);
pub struct ComparisonProgress(pub Property<f64>);
impl Store for PrimaryProgress {}
impl Store for ComparisonProgress {}
}

Providing the same type again on the same owner replaces its entry for future lookups. Already cloned handles still refer to their original state; replacement does not retarget consumers. Prefer publishing once at mount and updating properties inside that stable store. If two providers should intentionally share state, give them the same handle through bind: or Rust before publishing it.

Context and projected children

Registration belongs to the exact node identified by ctx. Register in the component's own mount handler to make that component the owner. In an inline handler on a descendant Button, Rust self can be the containing component while ctx identifies the Button; publishing there would make the Button the owner. See binding scope.

A provider is visible to both its private template descendants and content supplied by its caller. For example, in <Table><Row /></Table>, Row can find Table's store while expressions and inline handlers on that invocation retain the caller's scope. A private wrapper around Table's slot() does not become a provider ancestor of that Row. Forwarding slots or moving their rendered position also leaves provider ancestry unchanged. To expose state from a private implementation component, the receiving component must publish the intended interface itself.

Lifetime and lookup errors

Stores follow component identity. In a keyed list, reordering an item preserves its provider and state; an unkeyed list reuses positions. Final unmount clears the owner's providers and local state. A later mount starts fresh. Exit transitions retain providers until final unmount, and a return before the exit completes can reuse that instance. Unmount handlers can still access the departing scope before it is cleared.

A cloned property handle can keep its old state alive after unmount, but it does not connect to a new instance. A saved NodeContext cannot access a later mount's providers. For state that should outlive a view, own it in a longer-lived component above that view or in application-managed persistence.

During template hot reload, a reused provider with stable property handles keeps its registration. A changed property alias, such as changing a bind: target, remounts the affected provider subtree so its consumers receive the new handles. Recreated regions and a full application restart start fresh stores. These development rules are not a persistence API; see Hot reloading.

Both APIs return Result; StoreError distinguishes these failures:

ErrorMeaning
Missing { type_name }No provider of the requested type exists here or above.
ExpiredScopeThe context is not mounted, its mount ended, or its provider ancestry expired.
BorrowConflict { type_name }The chosen provider is already borrowed by a with_store closure.

provide_store can report expiration or a conflict when replacing an entry. A conflict or expired scope does not fall back to a different outer provider. The reset example uses expect because its provider is required; a component that also works independently should handle Missing explicitly. Keep expiration and conflicting borrows distinct from optional absence.

Try the store-ownership example for shared private/projected consumers, sibling isolation, nested shadowing, keyed reorder, and remove/remount controls. The router-playground example uses a store for mobile-menu state. Read Property handles for sharing and thread constraints, and the NodeContext reference for the method signatures.

Application-wide dependencies are ordinary root fields created by the typed initializer. Pass them explicitly or provide a lexical store from a component that owns them. Remounting a child does not repeat initialization. Local Rc handles stay on the UI thread; only explicitly transferable clients and shared property values cross threads.

For an advanced alternative that implements an element directly through the Rust runtime, read Primitives. That chapter covers when to use a primitive, how to author one, and its rendering and lifecycle responsibilities.

Read more

You can now give a reusable view its own interface, choose the owner of its state, and assemble instances from data and caller-provided content. Continue with Layout and Responsiveness to make those components fit different spaces. Scrolling covers larger collections, Routing organizes screens, and Motion adds transitions as content enters, leaves, or changes position.

Primitives

A component gives a reusable interface its own template, properties, and actions. A primitive is a more advanced alternative: it implements a template element directly in Rust through Pax's runtime interfaces. This gives its author control over lower-level behavior such as drawing, child expansion, and integration with a platform surface.

You already use primitives when you place elements such as Ellipse, Group, and Text in a template. Their standard-library implementations connect those tags to the renderer and runtime. You can use the same extension point to build a new kind of element in your own project.

Components and primitives

From the caller's perspective, both can appear as template tags and expose Property<T> inputs. Their implementations differ:

KindImplementation
Template-backed componentA #[pax] type with #[file(...)] or #[inlined(...)]; its template composes other nodes.
PrimitiveA #[pax] type with #[primitive("...")]; a Rust InstanceNode implements its runtime behavior, with no attached .pax template.
Data typeA #[pax] type without a template or primitive declaration; it describes values such as the NoteEntry record.

For application UI, begin with components and composition. A reusable illustration can combine Path and the existing drawing elements; a new route presentation can use a custom route branch. A primitive becomes useful when the capability needs runtime hooks those elements do not expose: a specialized drawing operation, a new container behavior, or a platform integration.

That additional control comes with responsibility for rendering, lifecycle, and target-specific behavior. Choose it for the capability you need; changing a component into a primitive does not automatically improve performance.

This chapter assumes familiarity with Rust, properties, and component composition. It starts with a minimal registration scaffold, then explains the obligations of a working primitive and points to implementations in the engine and standard library.

Authoring primitives

Declare and connect a runtime node

The extension point is available in userland. The compiler generates a factory that calls your implementation through the import path in #[primitive(...)]; adding a node does not require an enum entry in pax-std.

There are two Rust types to understand:

  • The #[pax] type declares the properties visible to authors.
  • Its InstanceNode implementation supplies construction and runtime hooks. A shared BaseInstance carries the compiler-provided configuration.

The following scaffold establishes that connection. It deliberately has no drawing or platform side effects yet. Create src/runtime_marker.rs:

#![allow(unused)]
fn main() {
use pax_kit::*;
use pax_kit::pax_engine::api::Layer;
use pax_kit::pax_engine::pax_runtime::{
    BaseInstance, ExpandedNode, InstanceFlags, InstanceNode, InstantiationArgs,
};
use std::rc::Rc;

#[pax]
#[primitive("crate::runtime_marker::RuntimeMarkerInstance")]
pub struct RuntimeMarker {
    pub value: Property<f64>,
}

pub struct RuntimeMarkerInstance {
    base: BaseInstance,
}

impl InstanceNode for RuntimeMarkerInstance {
    fn instantiate(args: InstantiationArgs) -> Rc<Self> {
        Rc::new(Self {
            base: BaseInstance::new(args, InstanceFlags {
                invisible_to_slot: false,
                invisible_to_raycasting: true,
                layer: Layer::DontCare,
                is_component: false,
                is_slot: false,
            }),
        })
    }

    fn base(&self) -> &BaseInstance {
        &self.base
    }

    fn resolve_debug(
        &self,
        f: &mut std::fmt::Formatter,
        _expanded_node: Option<&ExpandedNode>,
    ) -> std::fmt::Result {
        f.debug_struct("RuntimeMarker").finish()
    }
}
}

The three methods shown are required by InstanceNode. The flags describe a non-drawing node that does not intercept pointer hit testing. A drawing primitive would choose Layer::Canvas and provide rendering behavior; a native surface has a different layer and platform-message lifecycle.

In lib.rs, add the module and expose its author-facing type:

#![allow(unused)]
fn main() {
pub mod runtime_marker;
pub use runtime_marker::RuntimeMarker;
}

It can now be instantiated in a template:

<RuntimeMarker value=0.5 />

It occupies a runtime node but paints nothing. Use this as a registration scaffold, then implement the specific hooks your capability needs. The crate::... path above refers to this application's Rust module. A primitive distributed in a library needs a path accessible from the consuming application's generated code; the standard library uses paths such as pax_std::drawing::ellipse::EllipseInstance.

Implement behavior at the right level

An InstanceNode can serve several expanded occurrences, for example when a template node is repeated. Each occurrence has an ExpandedNode with its own properties, geometry, children, and lifecycle state. Keep per-occurrence state there, or in state captured by an effect attached to that occurrence. Putting a mutable per-item value directly on the shared instance can make repeated items interfere with one another.

Runtime hooks receive the expanded node. Access its typed properties with expanded_node.with_properties_unwrapped(|properties: &mut RuntimeMarker| { ... }), and use transform_and_bounds for its resolved geometry. This is lower-level access than a component handler's NodeContext; keep ordinary application actions in event handlers.

Inside reactive effects and rendering hooks, read a shared component field through its local view, for example properties.fill.local().get(). Declare its local dependency with properties.fill.untyped(). This resolves a dirty binding in the current graph settlement; a raw shared get() only returns the latest published snapshot and may precede that binding's evaluation. Local fields use LocalProperty and can be read directly. Typed node-property access enters the owning graph, including for coverage and hit-testing hooks.

The main implementation responsibilities depend on the capability:

  • Reactive work: bind an effect in handle_mount to the properties and geometry it reads. Standard drawing primitives use changed_listener to mark their retained drawing dirty. Reserve update for work that cannot be driven by those dependencies; it requires opting in through requires_non_reactive_update.
  • Canvas drawing: implement render through RenderContext. Follow the retained-node begin/end protocol, use the correct surface-local transform, register the node's opacity scopes, and invalidate drawing when its inputs change. begin_bounded_canvas_node supplies paint_opacity: use that value for drawing so a backend that composes subtrees does not apply ancestor opacity twice. The current EllipseInstance demonstrates begin_bounded_canvas_node and clearing the dirty flag only after end_node succeeds. Unbounded geometry needs suitable coverage bounds.
  • Coverage and interaction: keep coverage paths, coverage opacity, and hit testing consistent with what the node draws. These hooks affect masks, native/canvas ordering, and pointer selection. The default hit test uses layout bounds; unusual shapes may need ray_cast_test.
  • Lighting resources: return true from has_scene_lighting() when the primitive implements resolve_scene_light or resolve_scene_ambient_light. This capability stays true even when the resource's enabled property is false. Existing custom lighting primitives must add this opt-in; resolvers alone no longer register a contributor. Follow LightSourceInstance's reactive invalidation so changed light values redraw affected canvases.
  • Children and cleanup: a container must manage child expansion and scope; overriding mount behavior also means preserving the needed child setup. Release subscriptions, resources, and native surfaces on unmount. Respect the received-versus-projected child model for container content.

These runtime interfaces evolve with the engine. Work from the implementation in the Pax version your project uses and test the capability on each backend you intend to support. Exercise repeated instances, reactive updates, resize, transforms, opacity, clipping, and removal—not just the first rendered frame. Check both debug and release builds.

Native integration and engine changes

A userland primitive can use the existing rendering and runtime interfaces. A completely new native widget may also need message types, platform-side creation/update/deletion handlers, event serialization, and accessibility behavior. Setting Layer::Native alone does not create that support.

That work can span pax-message, the web interface, and the Swift interfaces for macOS, iOS, and iPadOS. New renderer operations or template/control-flow syntax similarly need engine or compiler support. Keep the supported-target boundary explicit.

When adding data that must cross the compiler/runtime boundary, update the release representation and its round-trip tests too. A primitive's normal property declaration participates in generated descriptors; adding a new manifest field or value form entails a broader audit. See How Pax Runs for the execution model and the maintainer appendix for cartridge details.

Source examples to study

Read these alongside the code in your checkout:

The runtime API reference is useful for orientation. The source links follow the repository's development branch; use the corresponding files at your dependency's release when implementing a primitive.

Read more

Components and Composition covers reusable template-backed UI, slots, and shared state. Drawing and Styling introduces the existing drawing elements, and How Pax Runs explains the runtime and rendering model. For the lower-level interface, continue with the runtime API reference and the source examples above.

Shader primitives

Enable the shaders Cargo feature on pax-kit to use or define shader primitives. A custom primitive library depending on pax-engine directly can enable pax-engine/shaders without depending on pax-std. The lower-level pax-runtime, pax-gpu, and pax-runtime-api crates expose the same feature; enabling it on the engine or runtime propagates it to the required layers. See enabling shader containers for the dependency declaration and feature-off behavior.

InstanceNode::shader_effect is an experimental extension point for a GPU-only subtree filter. Return Some(ShaderEffect) from a primitive with Layer::Canvas. Keep returning Some throughout the node's mounted lifetime; use an identity pass or parameter value to disable filtering. The renderer captures its ordinary descendants and replaces their GPU presentation with the final pass output. The original nodes remain responsible for native coverage and hit testing.

The standard-library implementations are in pax-std/src/shaders/gaussian_blur.rs and pax-std/src/shaders/impressionist.rs, with WGSL alongside them. They use the same public extension point available to application-defined primitives; the renderer has no effect-specific implementation or registration. The default mount hook expands children and calls bind_shader_effect to subscribe the primitive's typed properties, geometry, child list, and opacity. If you override mount, perform both steps yourself. Shader-only typed properties should return false from property_requires_occlusion_recompute; common geometry and opacity remain tracked independently.

A ShaderProgram is an immutable Rust resource containing 1–8 ordered ShaderPass values. Each pass declares its WGSL source, diagnostic label, two inputs (the captured subtree or an earlier pass), a resolution divisor of 1, 2, 4, 8, or 16, and up to 32 typed parameters. Parameters support Float, Uint, Vec2, and Vec4; each occupies an explicit, zero-padded 16-byte uniform slot. ShaderEffect.parameters supplies one matching list of values per pass. Non-finite floats, mismatched schemas, invalid identifiers, and forward/cyclic pass references are rejected. WGSL is parsed and validated with the pinned Naga version, with the source label included in diagnostics.

Implement this function in each pass:

fn effect(p: vec2<f32>) -> vec4<f32> {
    return sample_input0(p) * parameters.tint;
}

Here tint would be a declared Vec4 parameter. Coordinates are local logical pixels. sample_input0(p) and sample_input1(p) sample the declared inputs, returning transparent outside their capture domain. input0_pixel_size() reports the local-axis texel spacing, including transform scale and DPR. The engine supplies entry points, bind groups, and attachments. User code cannot add entry points or GPU resources. Intermediate textures use the surface color format; the final output must be premultiplied RGBA. There are no compute passes, history textures, external texture bindings, or raw-device callbacks in this slice.

ShaderEffect.outset is the maximum absolute local sampling displacement and output expansion on either axis for the whole recipe. Include support from consecutive passes. Keep it stable during parameter animation. Underdeclaring it can clip output or create tile seams; overdeclaring it increases capture memory and work. Nested support adds along the capture chain. Tile gutters preserve samples across edges, within the adapter's maximum texture dimensions. Output textures have a 256 MiB budget per physical tile; exceeding the limit produces a diagnostic. Visible attachments and source captures add to that output budget.

The renderer caches source pixels separately from final output. Parameter-only changes reuse source pixels, shader pipelines, and same-sized output textures. Changes to source pixels, source-side clips, inner opacity, lighting, or nested output invalidate the relevant captures. Removed effects release their cached outputs. Lighting changes invalidate a shader source only when its retained vector draws receive the changed lighting; excluded and unlit sources keep their cached pixels. ResourceChurnStats exposes effect source renders/cache hits, pipeline compilations, texture allocations/bytes, pass counts, and rendered pixel counts. These counters measure submitted work and allocations, not GPU execution time.

Recipes live in compiled Rust, while their public inputs use normal property serialization. No effect program or device handle enters the manifest or baked program format. User primitives follow the existing generated factory path in both debug cartridges and baked release cartridges.

Read shader container composition for clipping, native overlap, and the current target/content restrictions.

Routing

A useful URL names a place in your application: a project, a member's profile, or the settings panel someone wants to share. Pax connects that location to a component tree. On the web, the location follows the browser URL and its history. On macOS, iOS, and iPadOS, the same route tree can select screens using an in-app location.

This chapter builds on Templates, Events and Rust, and Components and Composition. Start with ordinary routes, then add nested screens or route transitions as the application grows.

Example

The Router Playground combines a persistent navigation area, nested guide and team routes, and an inspector for the active route data. Open src/route_outlet.pax to find the outer router; src/team_panel.pax shows the nested router and its navigation links.

The example starts at Landing. Open Menu in the narrow layout to find the guide, team screens, and fallback routes.

Choose Open standalone to explore the playground with its own browser address bar:

  1. Open a team member and compare the outer and inner scope shown by the inspectors.
  2. Follow a team settings link. The outer team remains selected while the inner route and remainder change.
  3. Visit an unknown path and inspect the fallback's unconsumed segments.
  4. Use Back and Forward, then reload the selected screen.

The docs host keeps the example's route in its pax_route URL query parameter, so navigation and reload stay inside the example's directory. Its navigation does not change the address of this documentation page. A regular standalone Pax application uses browser paths, as described below. The playground demonstrates runtime routing. Its responsive route trees are not a template for indexable static metadata; see Web route metadata for that purpose.

Routes and history

Three pieces work together:

  • Router reads the current location and chooses a branch.
  • Route declares a path and the content that belongs there.
  • Link or Rust's NodeContext::navigate_to requests a new location.

Navigation updates the location; the router reacts by selecting the matching subtree. Your event handler does not need to hide the old page and show the new one manually.

On the web, same-origin navigation in the current tab updates browser history without restarting Pax, except for server-owned paths. Back and Forward feed application-location changes into the same router. A fresh visit or browser reload asks the host for the requested document, so direct-link hosting is part of making a routed app usable.

Basic shape

Router has no visual surface of its own. Place it inside the layout area that should contain the selected screen.

Here is a small two-page application. In src/lib.rs:

#![allow(unused)]
fn main() {
use pax_kit::*;

pax_kit::runnable! { root: crate::App }

#[pax]
#[file("lib.pax")]
pub struct App {}
}

In src/lib.pax:

<Group x=24px y=24px width={100% - 48px} height={100% - 48px}>
    <Link width=100px height=36px url="/">
        <Text width=100% height=100% text="Home" />
    </Link>
    <Link x=120px width=100px height=36px url="/notes">
        <Text width=100% height=100% text="Notes" />
    </Link>

    <Group y=64px width=100% height={100% - 64px}>
        <Router>
            <Route path="/">
                <Text width=100% height=48px text="A place for your ideas." />
            </Route>
            <Route path="/notes">
                <Text width=100% height=48px text="Your field notes." />
            </Route>
            <Route default=true>
                <Text width=100% height=48px text="This page could not be found." />
            </Route>
        </Router>
    </Group>
</Group>

Run it with pax-cli run --target web. Follow the links, then use the browser's Back button. As the pages become more substantial, replace the text in each branch with a component such as <Home /> or <Notes />. The navigation can stay outside the router and remain mounted as the page changes.

Matching paths

Route patterns are literal strings. The supported forms are:

PatternMatches
/The root of the current router's scope.
/notesThat exact path.
/teams/:team_idOne segment captured as team_id.
/teams/:team_id/*The team path and any remaining segments.
default=trueA fallback when no explicit path matches.

A * must occupy the final segment. It can match an empty tail, so /teams/:team_id/* also matches /teams/design. Without the *, a pattern must consume the complete scoped path.

The first matching explicit branch wins. Put a specific route such as /teams/new before /teams/:team_id, and put a broad catch-all after more specific routes. There is no automatic specificity ranking. A default branch is considered only after all explicit branches fail, regardless of where the default appears in the file. Prefer one default per router. With no match and no default, that router contributes no active content.

Leading and trailing slashes do not change how a route pattern matches its scope. In a nested router, members/:member_id and /members/:member_id both describe a local path; the leading slash does not escape to the root.

Writing routes

Wrap the visible content of a navigation control in a Link:

<Link width=200px height=44px url="/teams/design">
    <Text width=100% height=100% text="Open the design team" />
</Link>

The default target is Target::Current. Use target=Target::New when an external web destination should open in a new browsing context:

<Link width=200px height=44px url="https://pax.dev" target=Target::New>
    <Text width=100% height=100% text="Visit Pax" />
</Link>

Give the wrapper and its content a useful interaction area. Link participates in Pax's event system; see Accessibility and Native Controls for the current accessibility boundaries.

Use navigate_to after an action, or when Rust determines the destination. For example, bind a button to this handler on your component:

#![allow(unused)]
fn main() {
impl App {
    pub fn open_team(&mut self, ctx: &NodeContext, _event: Event<ButtonClick>) {
        ctx.navigate_to(
            "/teams/design?view=board#activity",
            NavigationTarget::Current,
        );
    }
}
}
<Button width=180px height=36px label="Open team"
    @button_click=self.open_team />

This requests navigation through the active platform interface. Use root-relative paths such as /teams/design for destinations within the app. On the web, a relative destination is resolved against the current browser URL, not the nearest router's scope. Navigating to settings from /teams/design can therefore differ from navigating there from /teams/design/.

For a web Current destination, matching scheme, host, and port keep the navigation in the Pax session unless its path is configured as server-owned. Other origins and New destinations use ordinary browser navigation. The URL should describe the intended location; route matching does not validate that a user may access its data. Perform authorization in the systems that own that data.

Server-owned web paths

A site can combine a Pax application with standalone documents such as a static blog. Declare the paths that the HTTP server or CDN should resolve in the application's Cargo.toml:

[package.metadata.pax.web]
server_owned_prefixes = ["/blog", "/downloads"]

Existing links and Rust handlers then use their ordinary URLs:

<Link url="/blog/pax-0-39-0/" target=Target::Current>
    <Text text="Read the announcement" />
</Link>

The same policy applies to ctx.navigate_to("/blog/", NavigationTarget::Current). On the web, Pax initiates ordinary current-tab browser navigation before changing application history or notifying Router. The destination may be a document, redirect, download, or another application. Queries and fragments are preserved. Leaving for a document unloads the current app; Back may restore it from the browser cache or start it again. In a query-backed embed, this navigates the current frame to the real URL instead of changing its pax_route parameter.

Prefixes match complete, case-sensitive path segments. /blog covers /blog, /blog/, and descendants, but not /blogger or /Blog. A trailing slash is normalized away; / delegates every path. Entries must be root-relative paths, without wildcards, query strings, fragments, backslashes, whitespace, empty segments, or ./.. segments. Unicode paths and valid percent escapes are accepted. Unreserved ASCII escapes compare equivalently (/%62log is /blog); encoded separators stay encoded (/blog%2Fpost is not beneath /blog). This is a navigation policy, not an authorization boundary.

The default is an empty list. The setting applies only to web builds, in both debug and release; native routing and new-tab links retain their usual behavior. Rebuild and restart after changing Cargo metadata. The compiler embeds the policy as JSON in every generated application entry and writes pax-web-config.json for local serving. Custom entry documents must retain the generated pax-web-config script element for client navigation to use it.

The setting does not configure a production server or reverse proxy. Serve the declared paths separately, using web public files or a CDN origin. pax-cli run serves existing generated/public files there and returns 404 for missing files instead of the application history fallback. Remove conflicting Router metadata entries: a public document cannot overwrite a generated application entry. The HTTP host handles direct links without initializing Pax first.

Native locations

On macOS, iOS, and iPadOS, current-target relative paths update Pax's virtual in-app location. Query parameters and fragments travel with it. External destinations are handed to the operating system to open.

A native router does not provide browser chrome, a native back-stack control, or automatic universal-link registration. Supply the application's navigation controls and configure any OS-level deep-link integration separately. Prefer explicit destinations for a portable “Back to team” control.

The route binding

Inside the selected branch, Pax supplies a reactive route value. Its fields describe this match:

FieldMeaning
route.locationThe structured location seen by this router.
route.global_locationThe full application location.
route.paramsNamed captures from this branch's :param segments.
route.remainderA list of path segments left for a nested router.
route.consumed_segmentsThe number of segments consumed before that remainder.
route.is_exactWhether the remainder is empty.

For example, inside /teams/:team_id/*:

<Text width=100% height=36px text={"Team: " + route.params.team_id} />

Both location objects contain path_segments, query, and fragment. For the web URL /teams/design/members/ada?tag=ui&tag=motion#activity:

  • The global path segments are ["teams", "design", "members", "ada"].
  • query maps tag to ["ui", "motion"]; repeated keys retain their values.
  • fragment is an optional string containing "activity", without #.

The web interface decodes URL components before delivering these values. Query and fragment data do not decide which path pattern matches. They can drive the selected screen's filters or other UI. Check optional values and missing keys before consuming them, and encode user-supplied values when constructing URLs.

A fragment is location data. Scrolling a Pax Scroller to a corresponding piece of content is application behavior; it is not implied by adding #activity to a route. See Scrolling and Viewports.

Nested routers

A nested router receives the nearest ancestor route's remainder. This lets a component own the routes beneath its part of the application.

For example, let the outer route supply the team identifier to TeamPanel:

<Router>
    <Route path="/teams/:team_id/*">
        <TeamPanel team_id={route.params.team_id} />
    </Route>
    <Route default=true>
        <NotFound />
    </Route>
</Router>

Declare pub team_id: Property<String> on TeamPanel. Its template can use that property for persistent team chrome, while its own router chooses the team's inner screen:

<Group width=100% height=100%>
    <Text width=100% height=40px text={"Team: " + self.team_id} />
    <Group y=56px width=100% height={100% - 56px}>
        <Router>
            <Route path="/">
                <TeamOverview />
            </Route>
            <Route path="members/:member_id">
                <MemberDetail member_id={route.params.member_id} />
            </Route>
            <Route path="settings/*">
                <TeamSettings />
            </Route>
            <Route default=true>
                <TeamNotFound />
            </Route>
        </Router>
    </Group>
</Group>

The screen components here stand for your application's content. For /teams/design/members/ada, scope changes in two steps:

RouterInput segmentsCapturesRemainder
Outerteams / design / members / adateam_id = designmembers / ada
Team panelmembers / adamember_id = adaEmpty

The inner route describes the inner match. Its params do not automatically merge the outer route's captures. Passing team_id into the component gives it a stable, explicit name alongside the inner member_id. route.global_location continues to describe the complete location at both levels. Query and fragment data are preserved when the path is scoped.

Use path="/" for a team's overview and default=true for its unknown subpaths. Using a default alone for the overview would also show it for unrecognized tails. The ancestor's terminal * is what makes additional path segments available to the inner router.

Route state and lifetime

Pax uses the route declaration and its captured parameters to identify an active route instance.

  • Changing only query data, a fragment, or a catch-all remainder updates the existing instance's reactive route context.
  • Changing a captured parameter creates a new instance. Moving from /teams/design to /teams/engineering creates a new team subtree.
  • Changing to a different ordinary route removes the old branch, subject to its exit lifecycle. Returning later generally creates it again.

This is why a team shell can keep local state while its inner router switches from members to settings. State that must survive leaving the team should have an owner outside that route: a longer-lived component providing a store above the router, or your application's persistence layer. A store provided inside the departing route is cleared at final unmount along with its owner. Components and Composition explains the store pattern.

Browser history records locations, not snapshots of every component's properties. Do not rely on Back or a page reload to restore unsaved form state.

Route branch kinds

Use an ordinary Route for mutually exclusive screens. RouteCard and RouteModal add a retained background for route-driven overlays.

A card supplies its own sliding shell:

<Router>
    <Route path="/">
        <Home />
    </Route>
    <RouteCard path="/details" edge=RouteCardEdge::Trailing duration=300ms>
        <Details />
    </RouteCard>
</Router>

Navigate from Home to /details to keep Home mounted underneath the incoming card. The card slides from the chosen edge while a scrim fades over the background. Available edges are Leading, Trailing, Top, and Bottom. duration controls the transition; scrim_opacity controls the dimming.

RouteModal retains the previous route and supplies the fading scrim, while its contents own their layout and any @in / @out movement. Set its duration to coordinate the scrim with those content transitions. Animation and Motion covers lifecycle timelines.

Provide a close control that navigates to the intended background route. Returning to that retained route can reveal its existing state without remounting it. The scrim does not supply an automatic close action.

An overlay entered by a fresh URL has no previous route to retain. Design that direct-load state deliberately: include the context the overlay needs, or place essential shared chrome outside the router. A browser history entry alone cannot reconstruct the previous mounted screen.

Custom route branches

You can define route branch types in your application or a reusable Rust crate. The #[route_branch(...)] attribute tells the compiler which properties describe the path and fallback, and whether the selected branch retains the previous one underneath. No pax-std enum change is required.

A custom branch is useful when several routes share presentation: a padded panel, a branded sheet, or a particular enter/exit treatment. It remains an ordinary template-backed component, with properties, slots, and lifecycle timelines.

For example, create src/panel_route.rs:

#![allow(unused)]
fn main() {
use pax_kit::*;

#[pax]
#[file("panel_route.pax")]
#[route_branch(path = "pattern", default = "fallback")]
pub struct PanelRoute {
    pub pattern: Property<String>,
    pub fallback: Property<bool>,
}
}

Here pattern and fallback are names chosen by the component author. The attribute maps them to the router's existing contract. They could also be named path and default, as in the standard branches.

Give it a src/panel_route.pax template that presents its caller's content:

<Group x=20px y=20px width={100% - 40px} height={100% - 40px}>
    slot()
</Group>
<Rectangle width=100% height=100%
    fill=rgb(246, 241, 230) corner_radius=16 />

Expose the type from your application's lib.rs:

#![allow(unused)]
fn main() {
pub mod panel_route;
pub use panel_route::PanelRoute;
}

Then use it directly under a router, in the layout area for the screen:

<Router>
    <PanelRoute pattern="/notes/:slug">
        <Text width=100% height=40px text={"Note: " + route.params.slug} />
    </PanelRoute>
    <PanelRoute fallback=true>
        <Text width=100% height=40px text="No matching note." />
    </PanelRoute>
</Router>

Pax selects the branch and preserves its component shell. The slotted content receives the route binding, while the shell supplies padding and a background. Its other properties and timelines work as they do on ordinary components. See Slots for caller scope and projection.

The default policy replaces the previous active route. To make a custom branch retain the previous route, add modal = true to its attribute:

#![allow(unused)]
fn main() {
#[route_branch(path = "pattern", default = "fallback", modal = true)]
}

This is a policy for that branch type. It does not automatically add a scrim, a close button, or an animation; the component supplies those parts. Use the standard RouteModal and RouteCard implementations as examples of retained-route presentation.

Custom branches use the same literal patterns, declaration-order matching, nested scope, and parameter-based identity as the standard branches. New matching syntax or a different retention policy would need changes to the router implementation. For deeper runtime behavior, Pax also exposes primitive authoring, a separate extension point that custom route presentation usually does not need.

Web route metadata

A public web route also needs a title and description before someone interacts with the app. Add literal RouteMetadata to the route that owns that page:

<Router>
    <Route path="/" metadata=RouteMetadata {
        title: "Field notes",
        description: "A small home for observations and ideas.",
        index: true,
    }>
        <Home />
    </Route>
    <Route path="/notes/*" metadata=RouteMetadata {
        title: "Notes — Field notes",
        description: "Observations from the field.",
        index: true,
        social_image: "assets/notes-card.png",
        social_image_alt: "An open field notebook",
    }>
        <Notes />
    </Route>
    <Route default=true metadata=RouteMetadata {
        title: "Page not found — Field notes",
        description: "This page could not be found.",
        index: false,
    }>
        <NotFound />
    </Route>
</Router>

Supply your own social image at the referenced asset path. title and description are required, nonempty string literals; index is a required boolean literal. The optional social_image and social_image_alt fields must appear together. These are build-time declarations, so property bindings and computed titles are not supported in this metadata block.

A nested route inherits the nearest metadata block until it declares its own. A replacement block must still provide all required fields. Keep indexable route topology static: placing it behind an if, for, or projected slot prevents deterministic metadata generation and causes the web build to fail. This includes passing an indexable router as projected children to a template-backed layout component. Keep the router outside that projection; responsive layout can remain inside a statically declared route.

Site-wide values

Set the public site identity in the project's Cargo.toml:

[package.metadata.pax.web]
title = "Field notes"
site_name = "Field notes"
site_url = "https://notes.example.com"
social_image = "assets/site-card.png"
social_image_alt = "Field notes"

The route's social-image pair overrides the site-wide pair. site_url must be an absolute HTTP(S) URL without a query or fragment; it supplies canonical URLs and absolute social-image URLs. A release web build containing an indexable concrete route requires it. Use the actual production URL for a published build, and keep the referenced images in the published output.

Generated entries and indexing

The web build emits route-metadata.json and route-specific entry HTML for concrete paths that have effective metadata. For the preceding example:

.pax/build/release/web/
    index.html
    notes/
        index.html
    route-metadata.json
    …the rest of the application bundle

The entry documents contain the title, description, robots directive, canonical URL when configured, and Open Graph/Twitter metadata. After in-app navigation, the web interface updates the document head from the catalog without restarting the Pax session.

Concrete and symbolic paths have different limits:

  • A literal /notes can receive its own entry document.
  • A terminal catch-all /notes/* can emit the concrete prefix /notes. Arbitrary deeper URLs do not become separate pages.
  • A parameterized /notes/:slug remains symbolic. The compiler does not enumerate records or generate an entry document for every possible slug.
  • At runtime, symbolic matches and catch-all tails receive noindex and no canonical link. An indexable directive applies only at the route's concrete path. Declare concrete routes for pages that need individually generated metadata.

The HTML contains an application entry and its metadata; the screen itself still renders when Pax starts. An unknown direct URL initially receives the fallback document's head, then the running app applies the default route's metadata. Account for this when checking previews from crawlers that do not execute the app. This mechanism does not generate a sitemap or implement server-side rendering.

Hosting the generated routes

Serve a generated entry or real public file when it exists. For other application paths, serve /index.html while preserving the requested browser URL. A redirect to / would discard the location before the router reads it.

The default route owns the in-app not-found screen. The build does not create a separate semantic 404.html, and choosing a fallback route cannot change the status of an HTTP response already delivered by the server. Keep missing asset responses separate from application fallback.

The generator also sets the entry documents' HTML base URL so nested entries can load the application bundle. Start with a domain-root deployment; a subdirectory deployment requires the browser paths, site URL, and hosting prefix to agree. See Targets, Build, and Deployment for the hosting configuration and its verification checklist.

Read more

Layout and Responsiveness

The same note card might appear in a narrow phone view, beside another card on a desktop, or inside a small inspector panel. Its content needs an area to occupy and a few rules for adapting to that area.

Pax gives each node a local coordinate space. Sizes, positions, and transforms describe its relationship to its container; layout containers can assign smaller areas to their children. This chapter builds that spatial model, then uses it to arrange the cards from Components and Composition at different widths. Templates covers the syntax and settings rules used here.

The coordinate and size model

A node has a width and height, and a transform that places those bounds in its parent's space. The local origin is the upper-left corner; x increases to the right and y increases downward. Children use that local space, even when the whole group is moved or rotated.

The relevant area is usually the parent's content area. Padding reduces that area. A Stacker assigns each child a cell, which becomes the area the child uses for its own layout.

Use px for a concrete length and % for a proportion of the relevant axis. These are logical layout units; 1px need not equal one physical display pixel on a high-density screen. Width percentages use available width, and height percentages use available height.

For example:

<Group x=24px y=24px width={100% - 48px} height=160px>
    <Rectangle x=16px y=16px width={100% - 32px} height=128px
        fill=rgb(246, 241, 230) corner_radius=12
    />
</Group>

The outer Group leaves 24 pixels on each side of its parent. The Rectangle then leaves 16 pixels on each side of the Group. Its percentage width refers to the Group, not the app window. Mixed-unit expressions retain those two parts: 100% - 32px means “the available width minus 32 pixels.”

Ordinary omitted dimensions fill the available axis. Content measurement can supply omitted dimensions for text, native controls, and autosized containers, so make important constraints explicit. A fixed-height card should declare that height; a wrapping paragraph usually needs a width and room to grow. Read Text, Fonts, and Images for text measurement.

Position and alignment

Percentage positions also supply a default anchor on the node's own bounds. That makes x=50% a convenient way to center an element, and x=100% a way to align its right edge. Pixel positions default to a zero anchor.

For an untransformed 100-pixel-wide child in a 400-pixel-wide area:

PositionDefault anchorChild's left edge
x=0%Left edge0px
x=50%Center150px
x=100%Right edge300px
x=200pxLeft edge200px

Within the usual zero-to-100-percent range, the default behavior can be read as a percentage of the remaining travel: `left = (parent width - child width)

  • percentage`. The same rule applies vertically.
<Group width=400px height=160px>
    <Rectangle x=50% y=50% width=100px height=40px
        fill=rgb(55, 120, 90)
    />
</Group>

This centers the rectangle on both axes. To align an element with a pixel inset from the far edge, combine units:

<Rectangle x={100% - 24px} y=24px width=100px height=40px
    fill=rgb(55, 120, 90)
/>

The percentage part gives it a right-edge anchor, and the pixel part moves that edge 24 pixels inward. It does not place the left edge 24 pixels from the parent's right edge.

Explicit anchors

Set anchor_x or anchor_y when you want a particular point on the element to meet a particular point in the parent:

<Rectangle x=50% y=24px anchor_x=0px width=100px height=40px
    fill=rgb(55, 120, 90)
/>

Here the left edge begins halfway across the parent. An explicit anchor takes precedence over the anchor inferred from position. Anchors are measured against the node's own bounds: anchor_x=50% selects its center, while anchor_x=12px selects a point 12 pixels from its left edge.

Be deliberate with anchors when placing content beyond an edge. For example, y={100% + 8px} anchor_y=0px places the top of a child eight pixels below its parent. The explicit anchor is important to that relationship.

Choose a container

A Group establishes a shared coordinate space. It does not automatically arrange siblings into rows. Use it for deliberately positioned content, layered backgrounds, badges, and pieces that should transform together.

A Frame provides similar grouping with clipping at its bounds by default. Its corner_radius rounds the clipping boundary:

<Frame width=240px height=100px corner_radius=12>
    <Rectangle x=200px y=20px width=120px height=60px
        fill=rgb(55, 120, 90)
    />
    <Rectangle width=100% height=100% fill=rgb(246, 241, 230) />
</Frame>

Only the portion of the green rectangle inside the Frame is visible. A Group of the same size would allow it to extend beyond the group's bounds. Compositing explains clipping and native/rendered content in more detail.

Rows and columns with Stacker

A Stacker assigns one cell to each participating child. Its default direction is vertical; use StackerDirection::Horizontal for a row.

<Stacker width=100% height=160px
    direction=StackerDirection::Horizontal gutter=16px
>
    <NoteCard width=100% height=100% title="Field notes" progress=0.25 />
    <NoteCard width=100% height=100% title="Sketchbook" progress=0.75 />
</Stacker>

The gutter separates the two cells. By default, they divide the remaining main-axis space equally: in a 400-pixel-wide row with a 16-pixel gutter, each gets 192 pixels. Each card's width=100% fills its own cell. It does not request the entire row's width.

A child still has its own layout within its cell. A smaller child can use x=50% or y=50% to align itself there. Nest horizontal and vertical Stackers for more complex arrangements; a Stacker forms a single row or column and does not automatically wrap.

To assign different cell sizes, supply sizes along the extending axis:

<Stacker width=400px height=160px
    direction=StackerDirection::Horizontal gutter=16px
    sizes=[Some(120px), None]
>
    <Rectangle width=100% height=100% fill=rgb(220, 216, 206) />
    <Rectangle width=100% height=100% fill=rgb(55, 120, 90) />
</Stacker>

The first cell is 120 pixels wide; the second receives the remaining 264 pixels. Supply an entry for each child when mixing fixed and flexible cells. Keep the requested sizes and gutters within the available space. A child's own width or height affects its content inside the assigned cell; sizes controls the cell allocation.

Container placement and draw order are separate. Earlier siblings still render in front of later siblings. Keep backgrounds after their foreground content, as in the Frame example. See Element order.

Padding

padding_x adds equal inner spacing at the left and right; padding_y does the same at the top and bottom. They change the area available to children while leaving the node's outer bounds in place.

<Group width=240px height=160px padding_x=12px padding_y=16px>
    <Text width=100% height=100% text="Room around an idea."
        style={font_size: 18px, fill: rgb(32, 40, 48)}
    />
</Group>

The Text receives a 216-by-128-pixel area, offset by 12 and 16 pixels. Percentage padding resolves against the corresponding outer dimension: padding_x=10% reserves one tenth of the width on each side.

Padding applies to the child layout area. A background child that should cover the entire outer surface is often clearest as a sibling of a padded content Group. This separates the surface's bounds from its content inset.

Autosize

A fixed area gives children a space to fill. With autosize, content can instead determine an omitted container dimension. This is useful for a stack of differently sized notes or a label whose surrounding frame should grow.

<Stacker width=280px autosize=true gutter=12px>
    <Group height=48px>
        <Text x=12px y=12px width={100% - 24px} height=24px text="A short note" />
        <Rectangle width=100% height=100% fill=rgb(246, 241, 230) />
    </Group>
    <Group height=96px>
        <Text x=12px y=12px width={100% - 24px} height=72px
            text="A longer note has more room for its contents."
        />
        <Rectangle width=100% height=100% fill=rgb(220, 216, 206) />
    </Group>
</Stacker>

This vertical stack has a width of 280 pixels and a measured height of 156: 48 plus 96, with one 12-pixel gutter. Its height is omitted so autosize can supply it. An explicit height would continue to constrain that axis.

For a Group or Frame, measurement includes the placed extent of its content. A child beginning at x=200px with width 20 contributes an extent of 220 pixels from the origin. Content wholly before the origin does not add a positive width. Padding contributes to the measured outer size as well.

Axis controls

With autosize=true, these are the defaults:

ContainerMeasured axes
Vertical StackerIts extending axis: height
Horizontal StackerIts extending axis: width
Group and FrameBoth axes
ScrollerScroll-content height, separately from the viewport
LinkBoth axes from supplied content
TooltipBoth axes from its trigger content

autosize_x and autosize_y override those per-axis choices. For example, a vertical stack can measure height while keeping a caller-supplied width. Use autosize=true alongside Stacker's axis overrides. An explicitly specified dimension still wins over measurement for that dimension.

Autosize needs a measurable starting point. A parent whose height comes only from a child at height=100% leaves the calculation circular. Use concrete child sizes or intrinsic measurements on the axis being measured. When the axis cannot be resolved, current containers can fall back to top-down layout. Empty measured content collapses to zero on managed axes.

Conditional children participate in this measurement too. Adding or removing rows inside an autosized Stacker reflows its cells and updates its measured height; later size changes in a newly added row also propagate to the stack.

Text and native controls can report measurements after initial layout, and autosized ancestors react to those updates. Fonts, wrapping, and platform controls influence the result. Check the settled layout with realistic content rather than depending on its first frame. The auto-sized-containers example explores the available surfaces.

Responsive layout

Start with fluid relationships: percentage widths, deliberate insets, and cells that divide the available space. Add a breakpoint when the arrangement needs to change, such as two cards becoming too narrow to read beside each other.

Use the Notes and NoteCard declarations from Components, including the advance handler. Replace lib.pax with this small responsive board:

<Group class="board" x=24px y=24px width={100% - 48px}>
    <Text width=100% height=36px text="On the workbench"
        style={font_size: 24px, fill: rgb(32, 40, 48)}
    />
    <Stacker class="cards" y=64px width=100% gutter=16px>
        <NoteCard width=100% height=100%
            title={self.title} progress={self.progress}
        />
        <NoteCard width=100% height=100%
            title="Sketchbook" progress=0.75
        />
    </Stacker>
    <Button class="advance" width=160px height=36px
        label="Advance" @button_click=self.advance
    />
</Group>

@settings {
    .board { height: 296px }
    .cards {
        height: 160px
        direction: StackerDirection::Horizontal
    }
    .advance { y: 248px }

    if $viewport.width < 720 {
        .board { height: 472px }
        .cards {
            height: 336px
            direction: StackerDirection::Vertical
        }
        .advance { y: 424px }
    }
}

At 720 pixels and above, the cards share a row. Below that threshold, they occupy two 160-pixel rows separated by the same 16-pixel gutter. The Button moves beneath them. Both layouts leave a 24-pixel outer inset.

These conditions change settings on the existing nodes. The cards retain their identity and state while the window crosses the breakpoint. A structural if that creates a different subtree has different lifetime implications; see Components.

The threshold belongs to this composition. Choose it from the space the content needs, then try widths just above and below it. Also test a short window: adapting a row into a column does not guarantee that all content fits vertically. Use a correctly sized Scroller when the content needs more space; Scrolling owns viewport/content sizing and input.

Viewport or component bounds?

$viewport.width and $viewport.height describe the mounted app's viewport in logical pixels. In an embedded app, that is the embed's viewport, not the entire surrounding page. Orientation helpers describe that same area. They are useful for screen-level composition; see the complete built-in globals.

A reusable component may receive only a fraction of that area. For example, a 320-pixel sidebar should not choose a wide layout merely because the app window is wide. Derive a property from the component's own bounds when its layout policy should follow the space its caller allocates.

For Notes, add pub compact: Property<bool> and initialize it with compact: Property::new(false) in the existing custom default. Then add:

#![allow(unused)]
fn main() {
impl Notes {
    pub fn on_mount(&mut self, ctx: &NodeContext) {
        let bounds = ctx.bounds_self.clone();
        let deps = [bounds.untyped()];
        self.compact.replace_with(LocalProperty::computed(
            move || bounds.get().0 < 720.0,
            &deps,
        ));
    }
}
}

If Notes already has a mount method, add the computation there. Change the settings condition to if self.compact; it now follows Notes' allocated width. Read bounds in the component's lifecycle context here, rather than a Button's event context. The latter would describe the Button. The materials example uses this bounds-derived pattern.

Avoid making the measured property depend on the very size rule it controls. A width supplied by the caller is a clear input for choosing the component's internal arrangement. Properties explains the dependency model.

Shared visual settings

Classes keep repeated visual choices together, and conditional settings can adapt typography, spacing, or surface treatment as well as geometry. Keep a card's common appearance in its component or shared settings; let the containing layout own the arrangement. ImportSettings can share a settings vocabulary across templates. See Imported settings and Drawing and Styling for themes, fills, and materials.

Text alignment controls content within the Text node's bounds. It does not position that node in its parent. Use layout position and anchors for the node, and style.align_horizontal, style.align_vertical, and style.align_multiline for text within it. Text covers the distinction in more detail.

Transforms and origins

rotate, scale_x/scale_y, and skew_x/skew_y transform a node and its descendants around its anchor. Scaling changes the displayed geometry; width and height remain the node's unscaled layout bounds.

<Group width=320px height=180px>
    <Rectangle x=50% y=50% width=160px height=80px
        rotate=-8deg scale=90% fill=rgb(55, 120, 90)
    />
</Group>

The default 50-percent anchors keep this rectangle centered as it turns and scales. Use explicit anchors when an object should rotate around another point. A parent's transform also transforms its children, so group pieces that should move together.

An assigned Stacker cell does not automatically rearrange its siblings to avoid visual overlap caused by a transform. Flow-space measurement excludes the full rotated, scaled, or skewed silhouette, so do not expect autosize to reserve all the space a transformed drawing can cover. Motion covers changes over time; Events explains converting input positions through transformed geometry.

Multi-Axis Common Properties

The anchor, scale, and skew shorthands accept a scalar for both axes or a two-item list in [x, y] order:

<Group width=240px height=120px
    x=50% y=50% anchor=[50%, 50%] scale=80% skew=[0deg, 4deg]
>
    <Rectangle width=100% height=100% fill=rgb(220, 216, 206) />
</Group>

Axis-specific properties remain available. In the same settings layer, a longhand such as scale_y takes precedence over the corresponding shorthand axis. A longhand expression using $base sees that axis's shorthand value. The wider settings precedence rules belong to Templates.

Layout Role

layout_role=LayoutRole::Breakout excludes a child from its parent's flow and measured content extent. It is useful for a badge or floating panel that should remain near its owner without making the owner larger.

<Group width=160px autosize=true>
    <Group y={100% + 8px} anchor_y=0px width=220px height=64px
        layout_role=LayoutRole::Breakout
    >
        <Text x=12px y=12px width={100% - 24px} height=40px
            text="More room for a thought."
        />
        <Rectangle width=100% height=100% fill=rgb(246, 241, 230) />
    </Group>
    <Text width=160px height=32px text="Field notes" />
</Group>

The label determines the outer Group's measured height of 32 pixels. The floating panel begins eight pixels below it and does not enlarge that measurement. Earlier source order keeps the panel in front where it overlaps.

Breakout changes layout participation while retaining the normal ancestor relationships. It still scrolls, clips, masks, renders, and hit-tests within that tree. It does not create a portal above a clipping Frame or pin content to the app window. Percentage sizes still use the relevant parent area. The default LayoutRole::Default participates normally in flow and measurement.

Opt into native safe-area spacing

The iOS and iPadOS chassis draw edge to edge. Add a standard-library DynamicIslandSpacer where content should avoid the status bar or top cutout:

<Stacker width=100% autosize=true gutter=0px>
    <DynamicIslandSpacer />
    <Text width=100% height=40px text="Below the safe area" />
</Stacker>

With no explicit dimensions, the spacer fills its container's width and measures its height from UIKit's current top safe-area inset, in logical pixels. It renders nothing and does not intercept input. A content-sized Stacker gives that measured space its own cell. In a plain Group, siblings are positioned independently, so adding a spacer alone does not move them.

The optional edge selects SafeAreaEdge::Top (the default), Bottom, Left, or Right. Top and Bottom fill width and measure height; Left and Right fill height and measure width. Explicit width or height overrides that axis's measurement. The read-only inset output supports deliberate positioning:

<DynamicIslandSpacer inset=bind:self.safe_top />
<Group y={(self.safe_top)px} width=100% height={100% - (self.safe_top)px}>
    <Text width=100% height=40px text="A safe heading" />
</Group>

Declare pub safe_top: Property<f64> on the owning Rust component. The calculator example binds all four edges to keep its content clear while its background covers the whole window. It also uses an edge-to-edge case below 600 logical pixels of window width on web and macOS; native iOS and iPadOS always use that presentation. This width breakpoint belongs to the example's layout, independently of safe-area insets. The keypad keeps its touch sizes, and the LCD receives the remaining height; short windows scroll the calculator.

Insets come from the visible window's safe rectangle, including devices without a Dynamic Island, and update on rotation and window resizing in both debug and release. Landscape often needs side spacing instead of top spacing; use the corresponding edge when that side contains content. The spacer does not inset the root or detect padding already applied by an ancestor. Add it once at the relevant window edge to avoid reserving the same space twice.

Currently this component reserves space on native iOS and iPadOS only. Web (including Safari), macOS, and other platforms contribute zero on the inset axis. A Stacker's separately authored gutter still applies around a zero-sized child.

Read more

You can now predict an element's area, choose how it aligns within that area, and reshape a collection while keeping its state intact. Continue with Text, Fonts, and Images for content and measurement, Drawing and Styling for the visual vocabulary, and Scrolling for content larger than its viewport. Motion builds on these same layout relationships.

Text, Fonts, and Images

A heading, a paragraph, and an image give an interface much of its character. They also bring their own constraints: a sentence can wrap, a font can arrive after the first frame, and a photograph has proportions worth preserving.

This chapter covers those content decisions. It builds on the local bounds and containers in Layout, with examples you can place in a component's template. Keep use pax_kit::*; in its Rust file.

Display text

Text displays a string through the target's native text system. On web, that means DOM text; macOS, iOS, and iPadOS use the native text rendering provided by their chassis. Pax positions and composites that content alongside drawings and images.

Use style to set the type's appearance. A title and a short description can share a family and color while differing in size and weight:

<Group x=24px y=24px width={100% - 48px} height=180px>
    <Text width=100% height=40px text="Field notes" class="heading" />
    <Text y=56px width=100%
        text={"Collect a small observation. Give it a title, " +
            "then leave room for the next idea."}
        class="body"
    />
</Group>

@settings {
    .heading {
        style: {
            font: {family: "Arial", weight: 700}
            font_size: 28px
            fill: rgb(32, 40, 48)
        }
    }
    .body {
        style: {
            font: "Arial"
            font_size: 18px
            fill: rgb(72, 80, 88)
        }
    }
}

text={self.title} can replace a literal when the content comes from state. The same reactive bindings used for layout work here; see Properties and PAXEL.

TextStyle groups font, size, fill, underline, and alignment settings. Use pixel sizes such as 18px for font_size: the current text patch expects a pixel value. Use a solid color for fill. Although its Rust type is Paint, native text currently receives only the first stop's color from a gradient. It does not accept vector Fill/Stroke stacks or per-layer materials; gradient paint mixtures crossfade those representative colors.

Typography classes belong to their component's settings. For a shared type scale across components, see Imported settings and ImportSettings. Ancestor text styling is not a substitute for explicitly applying the intended style to each Text node.

Size and wrap text

A paragraph usually needs a width and an omitted height:

<Text x=24px y=24px width={100% - 48px}
    text={"A useful note can be one sentence or several. " +
        "Let the available width determine the line breaks."}
    style={font: "Arial", font_size: 18px, fill: rgb(32, 40, 48)}
/>

wrap defaults to true. With a width constraint, the native renderer breaks lines and reports the height it needs. Pax uses that measurement for the omitted dimension. Supplying both width and height gives Text a fixed box; it does not reduce the font size to make everything fit.

Wrapping uses the Text's local layout width. Rotating, translating, or scaling the node or an ancestor transforms that layout without changing its line breaks. Changing the layout width can reflow the text.

Measurements can change when the string, width, style, or loaded font changes. Allow the layout to settle before relying on a measured size. A following element at a fixed y will not move just because the paragraph grew. Use an autosized container when surrounding content should follow it; see Autosize.

For a single-line label, wrap=false disables automatic line breaking. clip=true cuts off content outside the Text's bounds. Clipping does not insert an ellipsis. By default clip is false, so text may paint beyond its box; an ancestor Frame can still clip it. An unclipped single-line label keeps its horizontal alignment when it overflows: centered text extends on both sides of the box, and right-aligned text extends to the left.

<Text x=24px y=24px width=220px height=28px wrap=false clip=true
    text="A long label with more to say than this row can hold"
    style={font: "Arial", font_size: 18px, fill: rgb(32, 40, 48)}
/>

Align content within its box

Layout positions the Text node. TextStyle aligns the content inside it:

SettingResponsibilityValues
align_horizontalPlaces the text block horizontally inside the nodeTextAlignHorizontal::Left, Center, Right
align_verticalPlaces the text block vertically inside the nodeTextAlignVertical::Top, Center, Bottom
align_multilineAligns lines within the text blockTextAlignHorizontal::Left, Center, Right

For a centered caption, set all three deliberately:

<Text width=240px height=100px text="A place to begin, and room to continue."
    style={
        font: "Arial"
        font_size: 20px
        fill: rgb(32, 40, 48)
        align_horizontal: TextAlignHorizontal::Center
        align_vertical: TextAlignVertical::Center
        align_multiline: TextAlignHorizontal::Center
    }
/>

The default alignments are left, top, and left. Text metrics and line breaks can differ between platforms even with the same declared size. Check your longest content and the scripts your application needs on each shipping target.

Font values

A string names a locally available family: font: "Arial". It selects normal style and weight, with no font download URL. Availability belongs to the workstation or device; a missing family falls back through the native renderer.

A named object adds modifiers:

<Text width=280px height=48px text="An observation worth keeping"
    style={
        font: {family: "Arial", style: FontStyle::Italic, weight: 700}
        font_size: 22px
        fill: rgb(32, 40, 48)
    }
/>

The fields are family, optional url, style, and weight, in any order. Styles are FontStyle::Normal, Italic, or Oblique. Numeric weights are 100 through 900 in increments of 100; FontWeight variants such as Normal, Medium, and Bold are also accepted. A weight selects a face or renderer approximation; it does not supply a missing font file. Oblique is accepted by the API, but the current Apple text path does not apply an oblique transformation.

Supplying family without url keeps the URL empty. On Apple native targets, this resolves the installed family and its requested style and weight without starting a font download, in both debug and release builds. Omitting the entire font uses Pax's current default: Roboto with a Google Fonts URL. Set a family or source explicitly when offline behavior and repeatable typography matter. Font::Web(family, url, style, weight) remains the explicit longhand; the string and named-object forms describe the same public font type.

Load a font file on web

For a project-owned web font, put the licensed font file in assets/fonts/ and reference it through url. For example, with a file named NotesRegular.ttf:

<Text width=320px height=48px text="A typeface for these notes"
    style={
        font: {family: "Notes", url: "assets/fonts/NotesRegular.ttf", weight: 400}
        font_size: 24px
        fill: rgb(32, 40, 48)
    }
/>

Here Notes is the family name registered with the browser for that file. Provide the actual font asset before trying the snippet. A direct remote font-file URL uses the same web loading path, subject to the server's access rules and the application's network policy.

Google Fonts stylesheet URLs containing fonts.googleapis.com/css receive special handling. Other nonempty URLs are treated as font files, so an arbitrary CSS stylesheet URL is not interchangeable with a font-file URL.

Bundle fonts for Apple targets

macOS, iOS, and iPadOS package the project's assets/ directory and register its .ttf and .otf fonts at app startup. Put a licensed font in assets/fonts/, then select its actual family and desired weight, for example font: {family: "Manrope", weight: 800} with Manrope installed in that bundle. No download URL is needed on Apple. This also works in release cartridges. public/ is a web-only directory and does not participate in Apple packaging.

Web still needs the asset URL. A shared declaration can use both the actual family and that URL: Apple resolves the already registered family, while web loads the asset. Browser-only family aliases do not establish names on Apple. Weights select available named faces or named instances of a variable font; verify the font supplies the requested weight. This does not expose arbitrary variation-axis controls.

The native Apple loader also handles direct remote font files and the Google Fonts CSS path. It does not resolve relative URLs into bundle resources: a missing or incorrectly named bundled family will not be rescued by its URL. Verify your remote font source on every shipping target.

Remote and web font loading is asynchronous. A fallback may appear first and then change the paragraph's metrics. Include first-load and unavailable-font cases in your checks, and make sure the files you ship or host permit that use.

Selection and editing

Text is selectable by default and non-editable by default. selectable=false is useful for decorative labels that should not start a selection gesture. On macOS, ordinary selectable text uses a native label; editing and selectable Markdown use the native document text view. Pax chooses the representation automatically, and selection works with either clip=true or clip=false. Read-only text forwards wheel scrolling to its containing Scroller, including when the text is selected; clip=true clips the label without giving it an independent scroll position. Editable text can scroll within its editing area. An empty macOS editor retains its declared editing height, so the insertion point and input-method composition have room before any text is committed. Give an editor enough height for its font; clip=true still clips to its bounds.

Selection is target-dependent: the current iOS/iPadOS path uses an interactive selection view for clipped text, while ordinary unclipped, non-editable text uses its static rendering path. Test selection on the device rather than assuming the property guarantees identical interaction everywhere. On macOS, both clipped and unclipped selectable Text use the native text view.

editable=true makes Text a native editing surface. Edits update that Text node's text property. Use an explicit two-way binding when the edited value should belong to the containing component:

<Text x=24px y=24px width=280px height=40px
    editable=true text=bind:self.title
    style={font: "Arial", font_size: 24px, fill: rgb(32, 40, 48)}
/>
<Text x=24px y=80px width=280px text={"Current title: " + self.title}
    style={font: "Arial", font_size: 16px, fill: rgb(72, 80, 88)}
/>

This uses the title: Property<String> on Notes from Properties. An ordinary text={self.title} is a value binding and does not establish write-through to the owner's property. See Inputs and state ownership for that distinction.

For a conventional form field, start with Textbox. Its input/change events give the application explicit places for validation and committed actions. See Events and Native Controls for forms and focus behavior.

Markdown content

Set markdown=true for authored content with inline emphasis and other Markdown formatting:

<Text width=300px markdown=true
    text="Keep **one observation** and *one question*."
    style={font: "Arial", font_size: 18px, fill: rgb(32, 40, 48)}
/>

Web and Apple targets use different Markdown renderers, so start with simple formatting and verify anything richer on your targets. Treat this as a trusted-content surface: the web path inserts rendered markup and does not provide a sanitization boundary for arbitrary user input. Leave markdown false for plain strings from users.

Image sources

Keep project-owned images in the project's assets/ directory. Pax copies that directory into the build output. Start with a bundled PNG or JPEG and an explicit display area:

<Image x=24px y=24px width=160px height=100px
    source="assets/spaceship.png" fit=ImageFit::Fit
/>

For this snippet, copy spaceship.png from the canonical examples/fixtures/space-game/assets/ directory into your project's assets/. You can substitute your own image and keep the same layout. The path is relative to the app's asset layout, not the directory containing its .pax template.

Image is drawn through Pax's GPU/canvas rendering path. Its intrinsic pixel dimensions determine its proportions, but do not automatically size the node. Set width and height for the area the image should occupy.

Image assets load asynchronously. Once decoding finishes, Pax schedules a redraw of the mounted image.

A string is shorthand for ImageSource::Url. source={self.image_path} works the same way when image_path is a Property<String>; updating the property changes the requested image. The explicit ImageSource::Url("assets/spaceship.png") is also valid. An omitted source uses ImageSource::Empty.

Despite the Url name, arbitrary remote URLs are not currently a portable Image source. The web canvas loader prefixes the app's document directory, and the Apple canvas loaders resolve bundled assets. Use app-relative assets for this path. Web NativeImage has a separate browser URL path, described below.

For portable Apple bundles, use distinct resource basenames even across subdirectories. For example, name a large still stills/film.jpg and its small derivative thumbs/film-thumb.jpg. Apple's Swift resource processing rejects two processed resources named film.jpg. Reuse each path for repeated image nodes, and use the smaller derivative when full-resolution pixels are unnecessary. The paxflix example demonstrates this catalog pattern.

Fit an image into its bounds

Choose whether preserving the whole image, covering the area, or stretching it matters most:

fitResult
ImageFit::Fit (default)Preserves proportions and shows the whole image; may leave unused space
ImageFit::FillPreserves proportions and covers the bounds; crops the excess
ImageFit::StretchMaps the image directly to both dimensions; may distort it

Image centers the fitted result and clips it to its own rectangular bounds. With a square source in a wide box, Fit leaves space at the sides, Fill crops the top and bottom, and Stretch makes the subject wider:

<Group width=400px height=80px>
    <Image width=128px height=80px
        source="assets/spaceship.png" fit=ImageFit::Fit />
    <Image x=136px width=128px height=80px
        source="assets/spaceship.png" fit=ImageFit::Fill />
    <Image x=272px width=128px height=80px
        source="assets/spaceship.png" fit=ImageFit::Stretch />
</Group>

Use a Frame around an Image for rounded clipping; an Image's own fit policy only defines how pixels occupy its box. Read Compositing for clipping and masks.

Animate opacity on the Image or a shared ancestor to fade artwork with the rest of a component. Source pixel alpha remains part of the composed content; changing opacity does not reload its source pixels. See subtree opacity for how overlapping translucent elements compose.

On WGPU, the image's crop follows fractional transformed coordinates. Moving a Fill image by a fraction of a pixel keeps excess artwork clipped to its bounds, including where an overlaid gradient meets the edge of the image.

Raw pixel data

Rust can provide an ImageSource::Data(width, height, bytes) for generated pixels. The bytes are decoded RGBA values, four per pixel, and must contain exactly width * height * 4 entries. Compressed PNG/JPEG file bytes are not that representation.

The same constructor can be demonstrated with two pixels in a template:

<Image width=240px height=120px fit=ImageFit::Stretch
    source=ImageSource::Data(2, 1, [55, 120, 90, 255, 246, 241, 230, 255])
/>

The four values in each group are red, green, blue, and alpha, from 0 to 255. For substantial images, keep pixel generation or decoding in Rust and bind the result, instead of embedding a large byte array in the template.

Native images and target boundaries

NativeImage uses a platform image element instead of the canvas image path. It has a string url property rather than an ImageSource:

<NativeImage x=24px y=24px width=160px height=100px
    url="assets/spaceship.png" fit=ImageFit::Fit
/>

On web, this creates a browser image element, so app-relative paths and browser-supported remote URLs can be used. On macOS, iOS, and iPadOS, the current native widget loads local filesystem paths or file URLs. Do not assume the web-relative asset snippet resolves in the native app bundle. PhotoPicker provides platform image handles for this use; see Photo selection.

The fit values are shared with Image. Web and iOS/iPadOS implement contain, cover, and stretch behavior. The current macOS NativeImage maps Fill to stretching; use the canvas Image path when proportional fill cropping is required there.

Format support belongs to the active decoder. Browser image support does not establish native target support, and loading a format through Image does not promise animated playback of it. Verify actual assets on each target. The canvas path uploads decoded pixels; large source images still cost memory even when their display bounds are small.

Neither current image component exposes a public alt property. Keep important information available as text, and give image-driven actions a clear control label. A visible caption is useful content, but is not a claim that an image has an associated screen-reader description. Native rendering alone does not establish complete accessibility; test the interaction and reading order with the platform's assistive tools.

Read more

Continue with Drawing and Styling for shapes, fills, strokes, and visual surfaces. Read Compositing for mixing native and rendered content, and Native Controls for input, selection, and form workflows.

The API references list the remaining details for Text and fonts, Image, and NativeImage.

Drawing and Styling

A few drawing decisions can give a small interface a recognizable character: a warm surface, a fine outline, a generous corner, a line that guides the eye. Pax's vector elements let you make those decisions in the same template that positions your text and controls.

This chapter builds on Layout and PAXEL. The examples can go inside a component's template, with use pax_kit::*; in its Rust file. Start with ordinary shapes and paint; the later sections introduce custom paths, SVG, and lighting.

Compose a surface

Here is a small surface for the Field notes content from the previous chapter:

<Group x=24px y=24px width=280px height=156px>
    <Text x=24px y=24px width=232px height=36px text="Field notes"
        style={font: "Arial", font_size: 24px, fill: rgb(36, 54, 47)} />
    <Line x=24px y=80px width=232px height=1px
        x1=0px y1=0px x2=100% y2=0px
        stroke={paint: rgb(152, 168, 146), width: 1px} />
    <Ellipse x=24px y=108px width=12px height=12px fill=rgb(67, 112, 86) />
    <Rectangle width=100% height=100% corner_radius=18
        fill=rgb(237, 241, 226)
        stroke={paint: rgb(178, 192, 168), width: 1px} />
</Group>

The rectangle is last because it sits behind the other elements. Keep this foreground-first order when adding a highlight or a background; see Element ordering.

Rectangle fills its bounds; Ellipse fits an oval inside them. Equal width and height give Ellipse a circle. Line connects two local endpoints, while Path describes a longer sequence of segments and curves. They share layout properties with other elements, so percentages, transforms, and reactive bindings apply to drawings too.

A Group organizes these elements but does not paint a background of its own. Likewise, rounding a Rectangle changes that rectangle's geometry; it does not clip its siblings. Use a Frame when content needs to stay inside a rounded boundary.

Color and transparency

Use rgb for an opaque color and rgba when its paint should be translucent. RGB channels and an integer alpha use the range 0–255. Percent channels are also accepted. These two rectangles use equivalent half-opacity paint:

<Rectangle x=24px y=24px width=120px height=72px
    fill=rgba(56, 120, 91, 50%) />
<Rectangle x=160px y=24px width=120px height=72px
    fill=rgb(56, 120, 91) opacity=50% />

For an integer alpha, 255 is fully opaque and 128 is approximately half. Use 50% when you mean an exact half: a decimal 0.5 in rgba is not a normalized alpha value. Element opacity, by contrast, accepts 0.5 or 50%. Paint alpha and element opacity multiply, so applying both halves leaves one-quarter opacity for that paint.

hsl(150deg, 30%, 45%) describes hue, saturation, and lightness; hsla(150deg, 30%, 45%, 50%) adds alpha. Named colors such as WHITE, BLACK, BLUE, and TEAL are convenient palette values. The colored names use Pax's palette—RED, for example, is not a synonym for rgb(255, 0, 0). Use explicit channels when matching a design precisely. TRANSPARENT provides clear paint.

For a vector, fill paints the interior and stroke paints its outline. An omitted fill uses one SLATE layer; an omitted stroke is an empty stack. Set fill=[] when you want only an outline. fill=TRANSPARENT also retains its existing meaning: one transparent fill layer.

Opacity on a container also affects its descendants. The behavior of overlapping children and native elements belongs to Compositing; do not assume a group fades as a single flattened image. Text's style.fill has its own native-rendering limits, covered in Text and fonts.

Paint layers

Paint supplies color: a solid, linear gradient, radial gradient, mesh gradient, or a mixture produced by a transition. A Fill supplies an interior layer; a Stroke supplies an outline layer. Both own paint, material, and opacity. Rectangle, Ellipse, and Path accept ordered lists for fill and stroke. Line and Handwriter accept a stroke list. The property names stay singular.

<Rectangle width=240px height=120px fill=[
    Fill {paint: @gradient {0%: rgba(255, 255, 255, 0), 100%: WHITE}, opacity: 40%},
    TEAL
] stroke=[
    {paint: WHITE, width: 2px},
    {paint: @gradient {0%: CYAN, 100%: FUCHSIA}, width: 10px}
]/>

Index zero is on top within each list, matching element ordering. Every stroke is above every fill. The thin white outline here overlays the wider gradient. fill=RED, fill={paint: RED}, fill=Fill {paint: RED}, and fill=[RED] are equivalent. Mix paints and typed or untyped layer objects in one flat list. Use [] for no layers; nested lists, None, and mixing Fill with Stroke objects are errors. Bindings and settings use the same conversions; see appearance values.

Each layer defaults to SLATE paint, matte material, and opacity 1. Stroke also defaults to width 1px, butt caps, and miter joins. Handwriter's omitted stroke remains one black 3px round stroke; an explicit stroke replaces that default. Nonpositive widths draw nothing; nonfinite widths or opacity draw nothing and emit a development diagnostic. Finite opacity is clamped to [0, 1].

Layer opacity multiplies paint alpha before source-over composition. Element opacity then fades the completed appearance once. Two opaque fills on an element at 50% opacity give 50% alpha; two fills each at 50% layer opacity give 75% alpha. Material is per layer and uses the element's lighting context. Blend modes and interleaving fills with strokes are not exposed yet.

Paint coordinates use the complete local shape after smoothing, before stroke expansion or reveal trimming. Percentage and pixel endpoints include that bounds' origin. Stroke width, caps, joins, and reveal progress do not move the paint. A zero-width or zero-height domain expands that axis symmetrically to 1px solely for sampling; empty geometry still draws nothing. Transforms carry paint and geometry together. This applies to all layers of a Path, including its subpaths, and to Handwriter's complete generated path.

Native Text and controls retain their single-paint interfaces and existing solid-color limitations. They do not acquire appearance stacks or materials. Piet supports the vector layers and linear/radial gradient paints but does not apply lighting materials or alpha masks. WGPU web is the end-to-end validation target.

Migrating existing drawing code

Change stroke={color: RED, width: 2px} to stroke={paint: RED, width: 2px}. Rename the old paint enum's constructors from Fill::... to Paint::.... Move primitive material onto each fill/stroke layer that should use it. In Rust, vector properties now hold Vec<Fill> and Vec<Stroke>; Stroke::default() means a 1px stroke, so use an empty vector for absence. Element opacity now applies once after the layers are assembled. Applications must rebuild rich and baked cartridges with the matching compiler/runtime.

Outlines and corners

Strokes

A stroke combines a paint, pixel width, cap, join, material, and opacity. It is centered on the path, so a 4px outline extends about 2px to either side of its centerline. Leave room for that extension near a clipping edge.

<Line x=24px y=24px width=240px height=40px
    x1=8px y1=20px x2={100% - 8px} y2=20px
    stroke={
        paint: rgb(56, 120, 91)
        width: 8px
        cap: StrokeCap::Round
    }
/>

StrokeCap::Butt ends at the endpoint, Round adds a semicircle, and Square extends a squared end by half the stroke width. Butt is the default. Closed contours have no exposed endpoints, so cap selection is useful for Line and open Path contours.

For joined segments, choose StrokeJoin::Miter, Round, or Bevel. Miter is the default and extends edges toward a point; Round softens the turn; Bevel cuts the corner across. Strokes support the same solid, linear, radial, and interpolated paints as fills. A bare stroke=RED creates a 1px stroke. Use pixel widths such as 2px; percentage stroke widths are not supported by the current vector renderers.

Corner radii

Rectangle accepts one to four corner values. They are unitless numbers representing local pixel radii. A single number rounds every corner:

<Rectangle x=24px y=24px width=240px height=100px
    corner_radius=[24, 8, 24, 8] fill=rgb(220, 232, 211) />

The list expands clockwise from the top-left:

InputTop-leftTop-rightBottom-rightBottom-left
12 or [12]12121212
[12, 6]126126
[12, 6, 3]12636
[12, 6, 3, 1]12631

Use a named object when the corner names make an asymmetric shape clearer: corner_radius={top_left: 12, top_right: 6, bottom_right: 3, bottom_left: 1}. The explicit CornerRadii { ... } form is also available. Size, anchoring, and transforms remain the responsibilities described in Layout.

Gradients

A gradient varies paint across a shape or its outline. @gradient lists the colors at positions along it; the renderer blends between those stops:

<Rectangle x=24px y=24px width=280px height=140px corner_radius=18
    fill=@gradient {
        linear: {
            start: [0%, 0%]
            end: [100%, 100%]
        }
        0%: rgb(237, 241, 226)
        55%: rgb(173, 205, 173)
        100%: rgb(67, 112, 86)
    }
/>

Points are [x, y] pairs in the shape's local coordinate space. Here the gradient runs from the top-left to the bottom-right. Percent coordinates follow the shape's bounds as it resizes. For linear gradients, use at least two stops in ascending order. Percentage stops resolve along the gradient axis; pixel stops use its local logical-pixel length on WGPU and Piet. WGPU uses at most eight stops.

Omitting the linear block gives a left-to-right gradient, from [0%, 0%] to [100%, 0%]:

<Rectangle x=24px y=24px width=280px height=80px
    fill=@gradient {
        0%: rgb(237, 241, 226)
        100%: rgb(67, 112, 86)
    }
/>

Put transparency in each stop's color, such as rgba(255, 255, 255, 0). There is no separate stop-opacity field. A translucent gradient over a surface can supply a highlight while preserving the underlying color.

Fills also participate in automatic settings transitions. Gradient changes crossfade the two sampled paints, even with different stop counts or positions; transparent stops preserve their alpha. Solid colors interpolate their RGBA channels. This paint behavior does not require isolated group compositing or change how neighboring elements blend with each other.

Radial gradients

A radial block grows from a focal point (start) to an outer circle whose center is end. Its numeric radius is in local logical pixels. Equal start and end points produce concentric circles. Points resolve against the actual shape bounds, so percentages remain useful on non-square shapes:

<Rectangle width=280px height=180px
    fill=@gradient {
        radial: {
            start: [55%, 40%]
            end: [50%, 50%]
            radius: 90
        }
        0%: rgba(137, 179, 213, 180)
        60%: rgba(137, 179, 213, 60)
        100%: rgba(137, 179, 213, 0)
    }
/>

The 0% stop is at the focus; the 100% stop lies on the outer circle. Moving the focus creates directional highlights. A focus outside the outer circle creates a cone of paint, with transparent pixels outside that cone. Rotation, reflection, shear, and nonuniform scaling transform the whole gradient with its shape. GPU color fills and painted alpha masks share the same radial sampler; the Piet browser fallback resolves the same local geometry through a fixed radial brush.

Radial stops accept percentages or logical pixels measured against the radius: with radius: 90, a stop at 45px equals 50%. Use ascending stops within 0–100% (or the equivalent pixel range). Empty stops, nonpositive/nonfinite radii, nonfinite points, and singular transforms paint nothing. GPU gradients still use at most eight stops. Radial fills also participate in paint crossfades, including transitions to linear gradients and solids.

Migration: radial color fills previously differed between backends. GPU color fills projected along an axis whose length depended on start/end; Piet multiplied the radius by shape size and resolved points in a square. Painted GPU masks previously treated start as the center and used end only for orientation. Recheck existing radial artwork and masks: use equal points for a centered glow, set end to the intended outer center, and express the radius directly in local logical pixels. The serialized fields and Pax syntax are unchanged in both debug and release cartridges.

Mesh gradients

A mesh places colored anchors in a rectangular set of rows. Each anchor has independent x and y coordinates: rows determine which anchors connect, and do not require a shared y coordinate. Pax joins neighboring anchors with smooth bicubic patches and automatic Bézier controls.

<Rectangle width=100% height=240px
    fill={
        paint: @gradient {
            mesh: {
                rows: [
                    [{position: [0%, 0%], color: CYAN},
                     {position: [100%, 0%], color: FUCHSIA}],
                    [{position: [0%, 100%], color: YELLOW},
                     {position: [100%, 100%], color: CYAN}]
                ]
            }
        }
        material: {Material::unlit()}
    }/>

Use 2–8 rows with the same number of anchors in every row, and 2–8 anchors per row. A mesh has no percentage color-stop list. Positions accept pixels, percentages, or combined units such as {50% + 10px}. Alpha belongs in the anchor color: for example, rgba(255, 0, 255, 35%).

Bind an individual coordinate or color to animate the field: {position: [{self.center_x}, {self.center_y}], color: {self.accent}}. Alternatively, bind the entire grid with mesh: {rows: {self.rows}}, where Rust declares rows: Property<Vec<Vec<MeshPoint>>>. MeshPoint has ordinary position: (Size, Size) and color: Color fields. Rust can also construct Paint::MeshGradient(MeshGradient { rows }) directly. Changing points morphs the field; transitioning between complete Paint values crossfades the two sampled fields, even when their grids have matching dimensions.

Mesh paints work in WGPU fills, layered paints, strokes, and painted alpha masks, with normal clipping, material, and group-opacity behavior. For a Path, percentages resolve against its complete smoothed geometry, including its bounds' origin. Changing draw_start/draw_end or stroke width does not reposition the paint. A zero-width or zero-height paint domain uses one logical pixel on that axis. Direct Handwriter strokes use each generated path's domain; use a single painted rectangle behind a Handwriter alpha mask to keep a continuous field across an entire word.

The field is transparent outside the actual mesh coverage. Moving outer anchors inward can expose gaps; keep them around the area you want to cover, or add a lower fill. Curves can overshoot anchor positions and colors. Color interpolation uses premultiplied encoded-sRGB channels, with alpha clamped to 0–1 and premultiplied color clamped to alpha; it is not linear-light interpolation. At folds, later patches in row order replace earlier ones without accumulating alpha. Explicit Bézier handles and irregular row lengths are not supported yet.

Mesh paint currently requires WGPU. Piet emits a diagnostic and paints no mesh; native text controls do not support mesh text paint. Debug and baked release cartridges accept the same values and syntax. Invalid static grids are rejected during compilation; invalid dynamic grids paint nothing with a deduplicated diagnostic. GPU storage is bounded to 64 MiB per paint page and the device's texture dimensions/layer count. A field or paint mixture that exceeds those limits, or cannot meet the subdivision error budget within 128 steps per patch axis, paints no mesh field and reports the limit rather than silently dropping endpoints or reducing quality.

The runnable examples/fixtures/mesh-gradients study demonstrates nested bindings, whole-row bindings, a continuous Handwriter mask, a revealed stroke, and translucent anchors. Its Pause, Show/Hide anchors, and Narrow controls help inspect the field as it moves and resizes. The stroke extends its outer anchors beyond 0–100% so paint also covers the stroke's outer edges and caps.

Reusable visual settings

Choose a small vocabulary for your interface: perhaps a paper surface, a quiet outline, an accent color, and two corner sizes. Classes can name those roles so that repeated elements stay consistent:

<Rectangle x=24px y=24px width=120px height=80px class="paper" />
<Rectangle x=160px y=24px width=120px height=80px class="paper"
    fill=rgb(219, 233, 212) />

@settings {
    .paper {
        fill: rgb(237, 241, 226)
        stroke: {paint: rgb(178, 192, 168), width: 1px}
        corner_radius: 16
    }
}

Both surfaces share an outline and corner shape; the second supplies its own fill inline. Class names describe a role here, which makes them useful when the palette changes. Keep typography roles in the same visual system, using TextStyle on the Text elements that need them.

When several components need those choices, use a theme component through ImportSettings. Templates owns the provider setup, scope, and override order. A child component imports the settings needed by its own template; placing a theme above it does not automatically style its internals.

The canonical examples/fixtures/runtime-settings-themes example separates color, typography, and corners into providers and switches them with ordinary reactive state. It is a useful next step when one shared class has grown into a theme. Keep event handlers responsible for state changes and let bindings select the visual values, as described in PAXEL.

examples/paxflix shows a shared runtime dark/light choice across nested components. Each visual component imports CinemaTheme is_dark={!light_mode} and uses its semantic classes for text, surfaces, controls, and image fades. The profile menu changes the root's bound mode; the imported provider updates reactively while the catalog's Scrollers remain mounted.

Paths and SVG

Describe a path

Path's elements property holds commands and points. Start with a Point, then add a segment command followed by its endpoint:

<Path x=24px y=24px width=280px height=120px fill=TRANSPARENT
    stroke={paint: rgb(56, 120, 91), width: 4px, cap: StrokeCap::Round}
    elements=[
        PathElement::Point(0%, 75%),
        PathElement::Cubic(25%, 0%, 75%, 100%),
        PathElement::Point(100%, 25%)
    ]
/>

The Cubic supplies two control points, which pull the curve toward them; the following Point supplies its destination. Coordinates belong to the Path's own bounds. Percentages reshape the curve with those bounds; pixels give fixed local distances.

CommandMeaning
Point(x, y) by itselfMove to a point and begin a new contour
Line, then Point(x, y)Draw a straight segment
Quadratic(cx, cy), then Point(x, y)Draw a curve with one control point
Cubic(c1x, c1y, c2x, c2y), then Point(x, y)Draw a curve with two control points
CloseConnect back to the contour's starting point

These entries all use the PathElement:: prefix in a template. Close a contour deliberately when drawing a filled shape:

<Path x=24px y=24px width=160px height=100px
    fill=rgb(220, 232, 211)
    stroke={paint: rgb(56, 120, 91), width: 3px, join: StrokeJoin::Round}
    elements=[
        PathElement::Point(50%, 8%),
        PathElement::Line, PathElement::Point(92%, 92%),
        PathElement::Line, PathElement::Point(8%, 92%),
        PathElement::Close
    ]
/>

Fills use non-zero winding. Overlapping contours that run in the same direction remain filled; an inner contour running in the opposite direction cuts a hole. This also preserves overlaps within imported font outlines. Earlier GPU builds used even-odd winding for ordinary fills, which could cut out overlapping regions. If your drawing relied on that cancellation, reverse the intended hole's contour so it also works with non-zero winding.

For data-driven drawings, keep a Property<Vec<PathElement>> in Rust and bind it to elements. Updating the property changes the drawing through the usual reactive loop. Rust helpers Path::start, Path::line_to, and Path::curve_to can assemble a command list; the last makes a quadratic curve. See Properties for collection updates.

Path geometry may extend outside its layout bounds. Use Frame or Mask when you want clipping, rather than relying on width and height to crop it. smoothing=PathSmoothing::Light or Strong can soften polyline runs; the default None preserves the authored geometry. Smoothing changes the shape, so inspect corners and lettering after enabling it.

Reveal a stroke

draw_start and draw_end select a range along the total path length. This line shows its first half:

<Path x=24px y=24px width=280px height=40px fill=TRANSPARENT
    stroke={paint: rgb(56, 120, 91), width: 6px, cap: StrokeCap::Round}
    draw_start=0% draw_end=50%
    elements=[
        PathElement::Point(0%, 50%),
        PathElement::Line, PathElement::Point(100%, 50%)
    ]
/>

The default range is 0 to 1, showing the full stroke. 0.5 and 50% mean the same progress; values are clamped to this range. An empty or reversed range shows no stroke. The fill remains intact: these properties reveal the outline, so use a transparent fill for a pen-like drawing effect.

On the GPU renderer, the revealed range cuts across the existing stroke; round caps at the original endpoints do not add a rounded tip at that cut. Original endpoint caps appear as a whole when the reveal reaches their path position; they are not progressively swept like a mask. This can be visible when a wide, round-capped stroke reaches draw_end=100%.

Bind the range to application state or animate it with a timeline. The path-drawing example combines authored curves and imported lettering; Animation and Motion owns the timing and playback model. Handwriter is a higher-level component that turns text into paths using bundled stroke fonts, with the same draw-range controls. See its API reference for font and text options.

Bring in an SVG

SVG import converts supported vector artwork into Pax Paths at build time. First validate the source from your project directory:

pax-cli svg-import assets/signature.svg

For a concrete source to try, copy examples/fixtures/path-drawing/assets/svg/signature-strokes.svg to that location. The command reports its viewBox, generated path count, and warnings. Review those warnings and compare the result visually before adopting the artwork.

The importer requires a valid viewBox and supports paths and polygons, line and Bézier commands, transforms, solid fills, and strokes. It is a bounded subset of SVG: convert shapes and text to paths before importing; convert arc commands to curves. Gradient paints are rejected. Filters, masks, and other unsupported elements are not reproduced. Complex SVG styling and compositing need a separate fidelity check.

To keep the SVG as your source, declare a component in Rust. This example can live in src/signature.rs:

#![allow(unused)]
fn main() {
use pax_kit::*;

#[pax]
#[custom(Default)]
#[svg("assets/signature.svg")]
pub struct Signature {
    pub draw_start: Property<UnitValue>,
    pub draw_end: Property<UnitValue>,
}

impl Default for Signature {
    fn default() -> Self {
        Self {
            draw_start: Property::new(UnitValue::Unitless(0.0.into())),
            draw_end: Property::new(UnitValue::Unitless(1.0.into())),
        }
    }
}
}

Declare pub mod signature; and use signature::Signature; in src/lib.rs, then place the component in the template:

<Signature x=24px y=24px width=320px height=80px />

The example preserves the source's 4:1 aspect ratio. Imported coordinates scale into the component's allocated bounds; use a matching ratio when you want the original shape proportions. Emitted stroke widths remain pixel values, so inspect their weight at the size you intend to display. The draw-range properties let the generated strokes participate in the same reveal workflow as a hand-authored Path.

To edit the generated Pax instead, eject a component:

pax-cli svg-import assets/signature.svg --component Signature --out src/signature

This writes src/signature.rs and src/signature.pax; use that pair instead of the #[svg] declaration above. Existing files are protected unless you explicitly pass --force. After ejection, the Pax file is editable source: changes to the original SVG do not automatically update it. To inspect the generated template without writing files, use --stdout.

Lighting and materials

Vector materials can respond to authored lights on the GPU renderer. A LightSource describes illumination; a Material describes how a surface responds. LightFrame keeps a local light from spilling into neighboring component instances:

<LightFrame x=24px y=24px width=280px height=160px>
    <Rectangle width=100% height=100% corner_radius=20
        fill={paint: rgb(67, 112, 86), material: {Material::glossy(0.6)}}  />
    <LightSource x=64px y=40px width=1px height=1px
        z=90px radius=240px intensity=1.5 color=rgb(255, 231, 192) />
</LightFrame>

LightSource is a non-rendering resource; it does not draw a visible lamp. Its point-light position uses its local center plus the authored depth. Use pixel values for radius and z. Depth here affects lighting, not the element order established by the template. Directional lights are also available through shape=LightShape::Directional and a direction vector.

Each fill and stroke layer has its own material; the default is matte. Material::glossy(...) and Material::metallic(...) adjust the response; Material::unlit() preserves the authored paint independently of lights. Material::emissive(...) adds color to its own surface; it does not turn the shape into a light source for its neighbors. The material reference describes explicit response coefficients.

LightFrame containment is one-way: outside lights can enter, while lights inside cannot affect parents or sibling frames. Canvas-layer reachability still applies, and AmbientLight is layer-wide rather than confined by LightFrame. A primitive without eligible direct lights or authored ambient keeps its unlit appearance.

Light contributions stay live as their properties animate or their containers scroll. Offscreen lights can still affect visible surfaces; viewport culling does not disable them. The runtime tracks light contributors and reuses direct light membership until light selection or scene structure changes. Scenes without authored lights do not scan their elements for lighting on redraw.

Unchanged surface-local lighting also reuses its GPU uniform without marking retained drawing dirty.

On the GPU renderer, animating light values repaints only retained surface tiles containing vector draws eligible for those lights. Unlit materials and bitmap images do not make a tile depend on a light. Cached opacity groups and mask-source captures likewise retain their pixels when only unrelated lights change. A tile that does need repainting redraws its composition using retained geometry; moving a light alone does not require tessellating its paths again. This applies on web and Apple GPU targets, in debug and release builds.

Eligibility is conservative: it follows light slots, canvas-layer reachability and LightFrame scope, without testing the light radius against each shape. Authored ambient light can affect all lit materials in its reachable layers. Scene structure, light selection, scrolling, and tile reassignment can require broader updates. LightFrame does not allocate an isolated render surface.

If you maintain a custom Rust lighting primitive, implement InstanceNode::has_scene_lighting() as true alongside resolve_scene_light or resolve_scene_ambient_light. This capability hook must stay true while the primitive is disabled or returns None; it describes its role, not its current contribution. Existing .pax lighting templates need no changes. For value changes, call RuntimeContext::mark_scene_lighting_dirty() to schedule lighting resolution without requesting a full canvas redraw. Structural changes still use the runtime's normal scene invalidation.

This feature is backend-dependent. GPU/WGPU rendering supports the material response, with a current limit of eight active direct lights in each canvas layer's lighting set, shared across its LightFrames. The Piet fallback draws the ordinary fills and strokes without that response. Native Text, native controls, and bitmap Images are not lit vector materials. Texture maps, cast shadows, and a 3D camera are not part of this API. Keep information legible when lighting is absent.

Try Materials in Compositing and Effects to compare surface responses as scrolling moves a light through the scene. The source tabs show the material parameters and scroll-driven light position.

Read more

Continue with Native Controls to add familiar inputs, Animation and Motion to change these values over time, and Compositing to combine clipping, masking, and native content.

The API references cover Rectangle and corners, Ellipse, Line, Path, colors, and light resources.

Accessibility and Native Controls

Native controls bring familiar editing and selection behavior into a Pax interface: entering a note, choosing an option, moving a slider, or opening a photo library. They share the scene's layout with your text, drawings, and images, while the browser or operating system supplies their underlying controls.

This chapter builds on Properties and Event Handling. It starts with a small form, then covers keyboard interaction, accessibility, and photo selection. The current support section identifies differences to check when shipping on web, macOS, iOS, or iPadOS.

A small editable form

Let's add a settings form to Field notes. A name, a location preference, a visibility choice, and a detail level all belong to the component. Each control edits one of those properties through bind:. A Save button calls Rust when the reader is ready to apply their choices.

In src/lib.rs:

#![allow(unused)]
fn main() {
use pax_kit::*;

pax_kit::runnable! { root: crate::Example }

#[pax]
#[file("lib.pax")]
pub struct Example {
    pub name: Property<String>,
    pub include_location: Property<bool>,
    pub visibility: Property<u32>,
    pub detail: Property<f64>,
    pub message: Property<String>,
}

impl Example {
    pub fn save(&mut self, _ctx: &NodeContext, _event: Event<ButtonClick>) {
        let name = self.name.get();
        if name.trim().is_empty() {
            self.message.set("Give this notebook a name first.".into());
            return;
        }

        let visibility = match self.visibility.get() {
            1 => "shared",
            _ => "private",
        };
        let location = if self.include_location.get() { "on" } else { "off" };
        self.message.set(format!(
            "{}: {}, location {}, detail {:.1}.",
            name.trim(), visibility, location, self.detail.get()
        ));
    }
}
}

In src/lib.pax:

<Group x=24px y=24px width={100% - 48px} height=452px>
    <Text class="label" width=100% height=28px text="Notebook name" />
    <Textbox y=32px width=100% height=40px
        text=bind:self.name placeholder="e.g. Moss and rain" />

    <Checkbox y=92px width=24px height=24px
        checked=bind:self.include_location />
    <Text class="label" x=36px y=90px width={100% - 36px} height=28px
        text="Include location" />

    <Text class="label" y=140px width=100% height=28px text="Visibility" />
    <Dropdown y=172px width=100% height=40px
        options=["Private", "Shared"] selected_id=bind:self.visibility />

    <Text class="label" y=236px width=100% height=28px
        text={"Detail: " + self.detail} />
    <Slider y=268px width=100% height=32px
        min=0.0 max=1.0 step=0.1 value=bind:self.detail />

    <Button y=324px width=180px height=44px label="Save notebook"
        color=rgb(56, 100, 78) hover_color=rgb(43, 79, 60)
        @button_click=self.save />
    <Text class="label" y=388px width=100% height=64px text={self.message} />
</Group>

@settings {
    .label {
        style: {font: "Arial", font_size: 16px, fill: rgb(36, 54, 47)}
    }
}

Try saving before entering a name, then change the fields and save again. The detail label updates while the slider moves. The message changes only when Save runs. This sample reports the choices on screen; a real app would also persist or submit them from Rust.

bind:self.name connects the Textbox's editable property to the component's property. Typing updates both. A one-way expression such as text={self.name} supplies a value to the control without making it an editor for the parent property. Bind directly to state; put formatting and derived display values in separate expressions. See Two-way bindings for the underlying model.

The visible labels above help someone looking at the form. They are separate Text elements: Pax does not currently turn that proximity into a semantic label association for assistive technology. Keep the accessibility boundary in mind when adapting the form.

Choose a control

These controls are available through use pax_kit::*;. The event column lists control-specific bindings; ordinary pointer events are covered in Event Handling.

ControlEditable property or contentControl event
Buttonlabel supplies its visible title@button_click, with Event<ButtonClick>
Textboxtext: String@textbox_input while editing; @textbox_change when the platform commits a change
Checkboxchecked: bool@checkbox_change, with event.checked
Slidervalue: f64, bounded by min and max@slider_change, with event.value
Dropdownoptions: Vec<String> and selected_id: u32Bind selected_id; there is no dedicated public dropdown-change event
RadioListoptions: Vec<String> and selected_id: u32Bind selected_id; there is no dedicated public radio-list-change event

selected_id is a zero-based index into options, despite its name. The first option is index 0. Keep the index valid when replacing or reordering the options; map it to your application's own identifier in Rust when needed.

On both web and Apple chassis, native input updates the relevant control property before calling its TextboxInput, CheckboxChange, or SliderChange handler. A bound parent property therefore already contains the new value. Use an event handler for validation or side effects, without writing the same value back just to maintain the binding.

On web, @textbox_input follows the browser's input event, while @textbox_change follows change. Apple text controls report input during editing and change when editing ends. Use input for immediate feedback and an explicit Button for a submission action; do not assume a change event means Enter was pressed. Likewise, a slider-change handler can run repeatedly during a drag, so keep expensive work out of that immediate path.

Multiline text and styling

For a longer note, add a Property<String> named notes and use multiline=true:

<Textbox x=24px y=24px width={100% - 48px} height=140px
    multiline=true text=bind:self.notes
    background=rgb(249, 247, 239)
    stroke={paint: rgb(155, 170, 153), width: 1px}
    corner_radius=8
    style={font: "Arial", font_size: 18px, fill: rgb(36, 54, 47)} />

Give a multiline editor enough height for its contents. A placeholder is available for single-line Textbox on web and Apple, and multiline Textbox on web; the current Apple multiline views do not display one. Keep a persistent visible label when the field needs an explanation.

Native controls expose their own styling properties. Button uses color, hover_color, outline, corner_radius, and style; Textbox uses background, stroke, outline, corner_radius, and style. Checkbox has separate unchecked and checked backgrounds, while Slider exposes an accent. Consult the control's API for its exact property names and defaults. The platform still influences its appearance, especially focus and hover states.

An ordinary Button renders its own label and surface. For a custom composition, build a component and handle its interactions deliberately; adding a click handler to a drawing does not supply a button's keyboard and assistive-technology behavior. PhotoPicker's slotted design below is a specific API for a custom visible affordance.

Filtered selection with ComboBox

ComboBox combines an editable Textbox with a Pax list. Add these fields to your form component, initializing them in its Default implementation if it has one:

#![allow(unused)]
fn main() {
pub query: Property<String>,
pub choice: Property<Option<usize>>,
}

Use Property::new(String::new()) and Property::new(None) for an initially empty control. Then place it in the template:

<ComboBox width=240px height=36px
    options=["Alpha", "Beta", "Gamma"]
    text=bind:self.query selected=bind:self.choice
    background=WHITE stroke={color: rgb(155, 170, 153), width: 1px}
    style={font_size: 16px, fill: rgb(36, 54, 47)} />

Click or tap the input to open the list, then type to filter it. Matching is case-sensitive and uses substrings; prefix matches appear first, with alphabetical order within each group. Selecting a row commits its original zero-based index in options, copies its label to query, and closes the list. For example, filtering to Gamma and choosing it sets choice to Some(2), even though only one row is visible. Separate ComboBox instances keep their own selection unless you explicitly bind them to shared state.

query is the editable text, while choice is the last committed selection. Typing, including an exact label match, does not select a row or clear the previous selection. Leaving the field does not commit it either. Do not treat a nonempty query as a valid selection or assume it still matches the last choice. The clear icon resets both values.

Changing options updates the label at the selected index. An out-of-range index becomes None and clears the text. Duplicate labels are supported, but reordering the source list keeps the index, not an application's item identity. If records have stable IDs, translate between those IDs and option indices in your application when updating the list.

new_item controls the no-match row:

ValueBehavior
NewItem::Disallow (default)Shows a disabled “No Results Found” row. It does not prevent typing unmatched text.
NewItem::AllowInvalidShows no no-match row and preserves initial free text when no option is selected. It does not commit a new selection.
NewItem::Text("Create item")Shows a custom row that dispatches @new_item when clicked or tapped.

For the custom row, set new_item=NewItem::Text("Create item") and @new_item=self.create_item on ComboBox. Define pub fn create_item(&mut self, ctx: &NodeContext) on the containing component. The handler reads the bound query and decides whether to add an option and select it. The event carries no payload and does not add anything automatically. See custom events.

The composed list supports pointer selection on web and Apple chassis, but it does not implement arrow-key/Enter list selection or a complete accessible combobox contract. Native text editing does not supply those list behaviors. Use Dropdown when a native fixed-choice control fits the task, and evaluate the accessibility needs of any editable selector. Tabs and dialog components are also compositions; their presence does not imply an operating-system widget or a complete accessible interaction contract.

Focus and keyboard interaction

Native text fields own editing, selection, and keyboard input while focused. On macOS, native rendering may detach distant Text and NativeImage views inside eligible scrollers, while retaining their instances. Editable, focused, and selected text and form controls stay attached. VoiceOver or Switch Control disables this culling and restores the full hierarchy. See Updating native content for the scope of this optimization.

To request focus when a Textbox appears, set focus_on_mount=true. This can be useful for a deliberate editing action; avoid taking focus automatically from someone already navigating the page. On a mobile device, focusing a field can also bring up the software keyboard.

In the web chassis, native controls participate in browser keyboard navigation. The current implementation derives their tab indices from visual stacking order; Pax does not yet offer an independent public tab-order property. Changes to element order, conditional content, or compositing can therefore affect the path through the form. Walk that path with a keyboard in the actual application.

While a native element has focus on web, the chassis leaves its keyboard input with that element. A component's @key_down is not a text-entry hook or a way to make a drawn widget focusable. See Keyboard events for the app-level event boundary.

Accessibility today

Pax's native text and controls provide a starting point for accessibility. On web, a Button is a browser button, Textbox uses an input or textarea, Dropdown uses a select, and Checkbox and Slider use input elements. The Apple chassis uses UIKit and AppKit controls, with custom styling and some composed implementations.

The broader accessibility model is still incomplete. Public authoring APIs do not yet provide a general contract for accessible names, descriptions, roles, label associations, or a reading order independent of visual layout. Image and NativeImage currently expose no alternative-text property. A custom-drawn control has no automatically generated semantic counterpart. Native backing alone also does not establish that every state is announced correctly on every platform.

For an application, check the actual tasks a person must complete:

  • Navigate the whole interaction with a keyboard, including opening and dismissing transient UI. Check where focus goes afterward.
  • Use the target's screen reader to inspect names, values, selected states, and reading order. Check whether validation messages are discoverable.
  • Keep instructions and errors visible in text, and check that zoomed or larger text still fits the layout. Avoid communicating a state through color alone.

These checks can reveal a requirement that needs additional engine work. If your product depends on a particular accessibility contract, verify it early against the current implementation. This chapter makes no blanket screen-reader or standards-conformance claim.

PhotoPicker

PhotoPicker lets someone select images for the application. Its children draw the visible affordance; a transparent native control above them opens the platform picker. Keep those children simple and give the whole picker a usable size:

<PhotoPicker x=24px y=24px width=240px height=48px
    source=PhotoPickerSource::Library allow_multiple=true
    include_bytes=false max_bytes_per_photo=26214400
    @photo_picker_change=self.choose_photos>
    <Text width=100% height=100% text="Choose photos"
        style={
            font: "Arial", font_size: 18px, fill: WHITE
            align_horizontal: HorizontalAlign::Center
            align_vertical: VerticalAlign::Center
        } />
    <Rectangle width=100% height=100% corner_radius=8
        fill=rgb(56, 100, 78) />
</PhotoPicker>

The default accept filter is "image/*". On web it is passed to the file input; Apple's current pickers select images without applying a custom accept filter. Use allow_multiple=false when only one existing photo is needed. Camera capture produces one photo at a time.

Handle selection and partial results

The completion event includes status, an optional message, request_id, and a list of photos. Add Property<String> fields named preview_url and message to the component for this handler:

#![allow(unused)]
fn main() {
pub fn choose_photos(&mut self, _ctx: &NodeContext, event: Event<PhotoPickerChange>) {
    if event.status == PhotoPickerStatus::Cancelled {
        return;
    }

    if event.status == PhotoPickerStatus::Selected {
        if let Some(handle) = event.photos.first().and_then(|photo| photo.handle.as_ref()) {
            self.preview_url.set(handle.clone());
        }
        self.message.set(event.message.clone().unwrap_or_else(|| {
            format!("Selected {} photo(s).", event.photos.len())
        }));
    } else {
        self.message.set(event.message.clone().unwrap_or_else(|| {
            "No photo was added. Try another image or source.".into()
        }));
    }
}
}

Selected can include a warning: if some files exceed the configured size limit, the accepted files can still be returned. SizeLimitExceeded reports a selection in which the size limit prevented any accepted photos. Other statuses are Cancelled, PermissionDenied, Unavailable, and Failed. Keep the person's existing selection when a request is cancelled. Browser dismissal does not always deliver a completion event in the current web implementation, so do not depend on every close resetting an app-level “picker open” flag.

Each photo includes a temporary identifier, MIME type, byte size, source kind, and optional file name, dimensions, preview handle, and copied bytes. Show a returned handle with NativeImage:

if self.preview_url != "" {
    <NativeImage x=24px y=96px width=240px height=160px
        url={self.preview_url} fit=ImageFit::Contain />
}

Handles are transient object URLs or temporary file URLs. Persist an app-owned copy if the image must survive beyond the current session. Selection does not upload a file; networking and storage belong to your Rust application logic.

include_bytes=true asks for copied data in photo.data. Web supplies it when the read succeeds; the current Apple implementation returns metadata and file handles without copied bytes in the event. Treat data as optional. include_bytes=false avoids that web byte copy when a preview is enough. The default max_bytes_per_photo is 25 MiB (26,214,400 bytes); a positive limit is checked even when include_bytes is false. It is a per-photo limit, so also consider how many selections your app retains.

Library, camera, and permissions

TargetPhotoPickerSource::LibraryPhotoPickerSource::Camera
WebBrowser file picker with image filtering and optional multiple selectionRequests the file input's camera-capture hint; the browser/device decides what UI is available
macOSImage-file selection through NSOpenPanelReturns Unavailable
iOS and iPadOSSystem photo picker with optional multiple selectionSystem camera UI on a capable device, subject to permission

On iOS and iPadOS, library selection uses the system's scoped photo picker. The camera path needs a purpose string in the project's Cargo.toml:

[package.metadata.pax.ios.info_plist]
NSCameraUsageDescription = "Add a photo to your field notes."

iPadOS inherits the iOS metadata unless [package.metadata.pax.ipados.info_plist] overrides it. Check permission denial and unavailable hardware as well as a successful capture; a simulator cannot stand in for every device capability.

Incrementing the picker's trigger property requests a programmatic open, and the value is returned as request_id. On web, prefer direct activation of the picker itself: browsers restrict opening file dialogs without a user gesture, so a delayed property update is not a reliable substitute.

The canonical examples/fixtures/photo-picker project shows library and camera selection, metadata, size limits, and thumbnail previews. From the repository root, run it with:

pax-cli run --path examples/fixtures/photo-picker --target web

Current support

Button, Textbox (single-line and multiline), Checkbox, Slider, Dropdown, and RadioList have implementations for web, macOS, iOS, and iPadOS. Their shared Pax API does not make every platform detail identical:

  • Web Slider applies step; the current Apple sliders are continuous and do not apply it. If discrete values are essential to your interaction, this requires a platform-specific check before adopting the control.
  • Native styling and interaction states vary. For example, Button's hover_color is applied on web but not by the current Apple button views.
  • Multiline placeholders, photo bytes, and camera availability have the limits described above.
  • Accessibility remains a partial foundation. Test required keyboard and assistive-technology behavior on each shipping target.

Read more

Continue to Animation and Motion for transitions and animated feedback. Compositing explains how native controls and rendered content share clipping and stacking; Scrolling covers longer forms and content regions.

For more detail, see the forms API, PhotoPicker API, event payloads, and image sources.

Animation and Motion

Motion can help someone follow a change: a new note arrives, a panel settles into place, or a list closes the space left by a removed item. Pax lets you describe that motion alongside the interface, with Rust controlling the state and actions that start it.

This chapter builds on Properties, Events, and Layout. Start with a property timeline, then give several tracks a shared playhead. Later sections cover enter/exit transitions and moving siblings in a Stacker.

A property change applies immediately unless you give it an explicit animation or transition. Native static text follows the same rule: changing a theme's text color does not request an extra Core Animation crossfade on iOS or macOS. Pax samples the values for explicit timelines, including opacity and movement on entering or leaving components.

For a hands-on preview, try Transition Grid: insert, remove, and reorder tiles while comparing their motion policies.

Timelines

A timeline is a sequence of property values at positions in time. Pax samples between those values as its playhead moves. Here, a small marker brightens and fades over a repeating 1.2-second cycle:

<Ellipse x=24px y=24px width=20px height=20px fill=rgb(56, 100, 78)
    opacity=@timeline {
        duration: 1.2s,
        loop: true,
        0%: 0.3, InOutQuad,
        50%: 1.0, InOutQuad,
        100%: 0.3,
    }
/>

This can go in a component's template with use pax_kit::*; in its Rust file. The inline timeline supplies the Ellipse's opacity. Each keyframe gives a marker, a value, and optionally an easing curve for the segment that leaves that keyframe. The first segment brightens from 0.3 to 1; the second fades back to 0.3.

Markers, duration, and loops

Write an explicit duration so the timeline's scale is clear:

DurationClock
300ms or 0.3sElapsed milliseconds from the chassis's monotonic clock
18 or 18fRuntime frame count

Use seconds or milliseconds when an interaction should take a particular amount of time across devices. A frame-based animation lasts for that many runtime ticks; its wall-clock duration depends on how those ticks arrive. Pax uses a nominal 60 frames per second when converting mixed authoring units, which is a conversion rule rather than a rendering-rate promise.

Markers can likewise be percentages, frame positions, or time values: 50%, 12, 12f, 150ms, or 0.15s. Percent markers make it easy to change the overall duration while preserving the rhythm. For time-based tracks, prefer percentages or matching time units; a bare 12 marker still means frame 12.

Ordinary timelines loop by default. Use loop: false to clamp the playhead to the range and keep the final value after the end. For a seamless loop, give the first and last keyframes matching values, as above. Frame loops include both frame zero and the final frame.

Without an explicit playhead, ordinary timelines sample the application's $frames or $millis clock. Inserting an element later does not give that ordinary timeline a fresh local clock. Use an enter transition for motion that should start when the element appears, or supply a playhead you control.

Easing

Easing changes how a value moves between two keyframes. A missing curve uses Linear.

CurveCharacter
LinearConstant progress through the segment
HoldKeep the first value until the segment ends
InQuadBegin slowly, then accelerate
OutQuadBegin quickly, then settle
InOutQuadAccelerate and then settle
InBack, OutBack, InOutBackAnticipation, overshoot, or both

OutQuad is a useful starting point for a small arriving surface; InOutQuad can soften a movement between two resting positions. Back curves can exceed the interval between the endpoints, so leave room for the overshoot and avoid applying it indiscriminately to bounded values such as opacity.

Interpolation also depends on the property type. Numeric values, sizes, rotations, and colors have interpolation behavior; arbitrary data does not necessarily have a meaningful in-between value. Path command lists do not currently interpolate their coordinates automatically. To change a shape, animate numeric parameters and derive its geometry from them, as in the mouse-animation example. For a reveal, animate a Path's draw_end; see Drawing.

Share a playhead

A named timeline can coordinate several properties on elements selected by ID or class. A shared playhead lets a button, a slider, or application logic drive the whole sequence together.

Here is a complete small playback example. In src/lib.rs:

#![allow(unused)]
fn main() {
use pax_kit::*;

pax_kit::runnable! { root: crate::Example }

#[pax]
#[file("lib.pax")]
pub struct Example {
    pub playhead: Property<f64>,
}

impl Example {
    pub fn replay(&mut self, _ctx: &NodeContext, _event: Event<ButtonClick>) {
        let playhead = self.playhead.local();
        playhead.cancel_transitions();
        playhead.set(0.0);
        playhead.ease_to(
            100.0,
            Duration::Milliseconds(900.into()),
            EasingCurve::Linear,
        );
    }

    pub fn pause(&mut self, _ctx: &NodeContext, _event: Event<ButtonClick>) {
        self.playhead.local().cancel_transitions();
    }
}
}

Reset and ease through the same local view so the transition starts at the new value immediately. A shared Property::set publication reaches the local view at the next UI settlement boundary; mixing that reset with an immediate local ease_to can start from the previous position.

In src/lib.pax:

<Button x=24px y=24px width=110px height=40px label="Replay"
    @button_click=self.replay />
<Button x=148px y=24px width=110px height=40px label="Pause"
    @button_click=self.pause />
<Slider x=24px y=84px width=280px height=32px
    min=0.0 max=100.0 step=1.0 value=bind:self.playhead />

<Rectangle id=marker y=156px width=40px height=40px
    corner_radius=8 fill=rgb(56, 100, 78) />
<Text id=caption x=24px y=224px width=280px height=40px
    text="Ready for another page"
    style={font: "Arial", font_size: 18px, fill: rgb(36, 54, 47)} />

@settings {
    #marker { x: 24px }
}

@timeline reveal {
    duration: 100,
    playhead: {self.playhead},
    loop: false,
    #marker {
        x: {
            0%: {$base}, OutQuad,
            100%: {$base + 200px},
        },
        rotate: {
            0%: -12deg, OutQuad,
            100%: 0deg,
        },
    },
    #caption {
        opacity: {
            0%: 0,
            40%: 0, OutQuad,
            100%: 1,
        },
    },
}

Replay advances playhead from 0 to 100 over 900 milliseconds. The template maps that one value to the marker's position and rotation and the caption's opacity. Pause stops the Rust-side easing at its current value. Press Pause before dragging the slider to scrub without an active animation writing to the same property.

The timeline's duration: 100 establishes a 0–100 frame-position range. Because playhead is supplied explicitly, it acts here as a convenient sampling scale; the Rust easing determines how long playback takes.

A numeric playhead uses the track's clock units: frames for a frame-based duration and milliseconds for a time-based duration, including one written in seconds. A duration-valued playhead such as {(self.seconds)s} carries its units through conversion. A numeric playhead is not automatically normalized to 0–1.

Tracks inherit the enclosing timeline's duration, playhead, and loop setting unless overridden on the track. Multiple named timelines can bind to the same property when separate groups need the same progress. Naming the timeline organizes its tracks; playback comes from its clock or playhead, rather than an implicit Rust method named after it.

Relative values and property ownership

$base means the value underneath this setting or timeline layer. In the playback example, #marker { x: 24px } supplies the base position, so the track moves from 24px to 224px. Changing that base moves the whole animation without repeating the layout value in every keyframe.

It does not mean the previous animation frame or the parent's property. Keyframe expressions remain reactive, so changing a dependency can change the sampled motion. See PAXEL's base values for the general model.

Keep one clear owner for each animated value. For ordinary selector timelines, an inline assignment to that same property takes precedence: adding x=24px directly to the marker above would hide the timeline's x track. Put the base in a settings rule, as shown, or assign a property timeline inline. Lifecycle transitions have their own overlay behavior; do not generalize this ordinary-settings rule to @in and @out.

Also avoid animating a state property while a handler continually sets it, or trying to ease a derived value whose formula keeps recomputing. Animate an owned source property and let its dependents follow.

Automatic settings transitions

An ImportSettings instance can ease changes to the settings it exports. For example, with LightTheme and DarkTheme components that export .surface rules and a boolean is_dark property on the host:

<ImportSettings transition=SettingsTransition::Ease(400ms, TransitionCurve::InOutQuad)>
    if self.is_dark {
        <DarkTheme />
    } else {
        <LightTheme />
    }
</ImportSettings>
<Rectangle class="surface" width=100% height=100% />

Use use pax_kit::*; in the Rust module. The default is SettingsTransition::None, which applies changes immediately. Ease accepts the same time or frame durations and built-in curves as other Pax motion, with the curve written as TransitionCurve::Linear, TransitionCurve::OutQuad, and so on. Zero, negative, and non-finite durations apply the target immediately.

The first resolved appearance snaps into place, including components inserted by input, lifecycle handlers, or a clock-driven conditional. Their imported settings resolve during mounting, so an entrance fade uses the current theme from its first frame rather than fading from primitive defaults. After that, changing a provider's reactive values, replacing a provider, or changing selector membership animates each affected receiver property toward its new effective value. A change hidden by a later settings layer does not start motion. Interrupting motion samples its current value and starts a fresh transition with the full duration. An unchanged target does not restart it. Receivers keep independent playback state, even when they share a theme provider.

The winning import supplies the policy. Removing its setting uses the outgoing policy to return to the surviving local value or default. An incoming import with None applies immediately, and switching an active import to None finishes its motion immediately. Changing only a nonzero duration or curve affects the next target change. The outer import's policy also applies to settings exported through its transitive providers.

Interpolation follows each property's Interpolatable implementation:

ValueBehavior
Numbers, sizes, rotations, colorsContinuous interpolation; colors use RGBA channels
Solid fillsContinuous RGBA interpolation
Gradient paints on fills and strokes, including solid↔gradientCrossfade sampled paints in premultiplied RGBA; geometry and stop lists may differ
TextStyleFont size and fill interpolate; font selection, weight, style, underline, and alignment switch immediately
CornerRadiiEach corner interpolates independently; zero is an angular corner
VectorsEqual-length vectors interpolate element by element; a changed length switches immediately
Other dataUses the type's interpolation implementation, which may be discrete

Paint crossfades preserve each endpoint's gradient instead of pairing stops. Transparent colors mix with their alpha premultiplied, then the resulting paint is composited once. This is separate from group opacity and blend modes. Paint mix weights stay between zero and one, including with overshooting curves; colors retain the renderer's existing RGB encoding rather than switching to HSL or linear-light interpolation. Existing gradient limits still apply: the GPU uses at most eight stops per endpoint. Radial gradients resolve the same focal geometry in GPU color fills, painted masks, and the Piet browser fallback.

An interrupted crossfade starts with the visible mixture. Repeated toggles between a fixed set of themes combine repeated endpoints, so their paint cost stays bounded by that set. Continually introducing distinct gradients before motion completes retains those visible endpoints; sampling cost grows with their count until a transition finishes. WebGPU mixes them in the paint shader. The Piet browser fallback accumulates them in a reusable canvas limited to the current surface, then clips and draws the resulting paint once.

examples/paxflix demonstrates the 400 ms policy on every visual component's CinemaTheme import. Open the profile menu and toggle Dark/Light mode; text, surfaces, outlines, artwork fades, and the hero's radial highlight transition while the catalog and scroll positions remain mounted. Toggle again during a fade to reverse it continuously.

Common layout settings also participate. Unset positions, padding, rotation, scale, and opacity interpolate using their layout defaults. Switching width or height between a value and intrinsic sizing is discrete. Native text still uses the chassis's text rendering; glyph shapes do not morph, and existing platform font and fill restrictions continue to apply.

An inline value, two-way binding, explicit property timeline, or lifecycle transition retains ownership of its property. Avoid imperatively easing the same receiver property while automatic settings motion owns it. Animate a separate source or use an explicit timeline when you need that control.

This API applies one transition to all changed properties. Imported named timelines, per-property transition policies, and an @change lifecycle hook are not implemented. Receiver-owned playback and preserved settings provenance leave room for those additions without sharing clocks between theme consumers. The behavior is the same in debug and release cartridges; release builds do not gain hot reload from this feature.

The canonical runtime-settings-themes example lets you switch color, typography, and corner providers independently and interrupt their motion.

Imperative easing

Rust can animate a property directly with ease_to. It replaces that property's pending transition queue and begins from its current eased value. ease_to_later appends another segment. For a component with a Property<f64> named strength, this handler rises, holds, and settles:

#![allow(unused)]
fn main() {
pub fn pulse(&mut self, _ctx: &NodeContext, _event: Event<ButtonClick>) {
    self.strength.local().ease_to(
        1.0, Duration::Milliseconds(180.into()), EasingCurve::OutQuad,
    );
    self.strength.local().ease_to_later(
        1.0, Duration::Milliseconds(120.into()), EasingCurve::Linear,
    );
    self.strength.local().ease_to_later(
        0.2, Duration::Milliseconds(300.into()), EasingCurve::InOutQuad,
    );
}
}

Bind strength to the visual property you want to animate. Triggering the handler again replaces the unfinished pulse; it does not accumulate an ever-longer queue.

Passing a plain number to either easing method means frames. Duration::Frames(18.into()) is explicit frame timing; Duration::Milliseconds(300.into()) and Duration::Seconds(0.3.into()) express elapsed time.

cancel_transitions() stops the active segment, clears the queue, and leaves the current eased value in place. Call it before set(...) when an immediate edit should take ownership. A plain set does not cancel a previously queued animation.

Rust also accepts EasingCurve::Custom with a function. That is a Rust API; arbitrary custom easing closures are not part of template timeline syntax. See the animation API and property API for the full method and value-type contracts.

Animate around viewport crossings

A local @viewport_proximity_change handler can compare previous/current intersection heights and ease a property bound to a named timeline's playhead. This supports first-visible reveals, an early departure at 40px remaining, and restoration when direction reverses. Bind @viewport_proximity_enter and @viewport_proximity_exit for preparation and cleanup.

Observe a stable wrapper and animate its contents to avoid feeding animated bounds back into the threshold. These events observe mounted geometry; they do not start structural @in/@out transitions or retain unmounted nodes. See viewport proximity for the runnable viewport-proximity example, sampling limits, and handler code.

Enter and exit transitions

Use @in and @out for motion tied to an instance entering or leaving the mounted tree. Their clocks start locally at the transition, and playback is finite. Newly entering content can animate while old content is still finishing its exit.

For a reusable note surface, declare this component in src/note_card.rs, then add pub mod note_card; and use note_card::NoteCard; to src/lib.rs:

#![allow(unused)]
fn main() {
use pax_kit::*;

#[pax]
#[file("note_card.pax")]
pub struct NoteCard {
    pub title: Property<String>,
}
}

In src/note_card.pax:

<Text x=16px y=16px width={100% - 32px} height={100% - 32px}
    text={self.title}
    style={font: "Arial", font_size: 18px, fill: rgb(36, 54, 47)} />
<Rectangle width=100% height=100% corner_radius=12
    fill=rgb(237, 241, 226) />

@settings {
    @in: enter
    @out: exit
}

@timeline enter {
    duration: 300ms,
    self {
        opacity: { 0%: 0, OutQuad, 100%: {$base}, },
        y: { 0%: {$base + 16px}, OutQuad, 100%: {$base}, },
    },
}

@timeline exit {
    duration: 220ms,
    self {
        opacity: { 0%: {$base}, InQuad, 100%: 0, },
        y: { 0%: {$base}, InQuad, 100%: {$base - 12px}, },
    },
}

Here self targets the NoteCard instance itself, including its position within the calling template. The named timelines can also use #id or .class selectors for elements inside NoteCard's template.

Add a Property<bool> named show_note to a parent and toggle it from an event handler. This conditional placement gives the card something to enter and leave:

if self.show_note {
    <NoteCard x=24px y=96px width=280px height=84px title="Moss and rain" />
}

Retention and interrupted motion

Removing the card from the conditional starts its exit. Pax retains the mounted instance until its exit finishes, then unmounts it. The application state has already changed; the retained visual gives the change time to read. Cleanup tied to unmount happens at actual unmount, not at the first request to leave.

The runtime enforces a five-second exit timeout to avoid retaining a node indefinitely. An exit that exceeds that limit can be truncated with a warning. Keep lifecycle exits short; use ordinary playback for a longer presentation.

Toggle the same conditional back before the exit completes and Pax can rescue its still-mounted instance. The default interruption: Takeover starts the destination transition from the currently sampled property value. The destination's remaining keyframes, duration, and easing still apply. This preserves value continuity; it does not promise the velocity continuity of a physical spring.

Set interruption: Restart in the destination timeline when it should start from its authored first value instead. The option belongs beside duration in either a named or inline lifecycle timeline. It affects direct enter/exit reversals on the same mounted instance, without changing $base or ordinary timeline playback.

Conditional and route branches can reuse retained instances during a reversal. In repeated lists, stable keys make that identity explicit: reordering a retained item keeps its instance, and removing its key can start an exit. A newly allocated, unrelated instance has no earlier motion to take over. See Lists and identity and Routing.

Element-level transitions

For a local effect, put property tracks directly in an element's inline transition:

<Group x=24px y=24px width=280px height=84px
    @in=@timeline {
        duration: 300ms,
        opacity: { 0%: 0, OutQuad, 100%: 1, },
    }
    @out=@timeline {
        duration: 220ms,
        opacity: { 0%: 1, InQuad, 100%: 0, },
    }
>
    <Text x=16px y=16px width=248px height=52px text="A brief note"
        style={font: "Arial", font_size: 18px, fill: rgb(36, 54, 47)} />
    <Rectangle width=100% height=100% corner_radius=12
        fill=rgb(237, 241, 226) />
</Group>

Element bindings also accept names, such as @in=panel_enter. That timeline belongs to the containing component: self addresses the element that carries the binding, while selectors can reach other elements in the same containing template. A reusable component's own settings-level transition, like NoteCard's, targets its own template.

Container-owned motion

A child's enter/exit transition controls its visual motion. Stacker separately controls the space allocated to children and how that layout changes. There are two independent choices:

SettingChoices
exit_modeFlow keeps exiting children in normal layout until exit completes. Ghost holds their previous frames as overlays while active children lay out without them.
reflow_transition.kindSnap moves immediately to the new layout. Ease animates between layout frames.

The defaults are Flow and Snap. To let surviving notes move into an exiting note's space immediately, choose Ghost with Ease. For a parent whose notes property contains items with stable id and title fields:

<Stacker x=24px y=24px width=280px height=320px gutter=12px
    exit_mode=ContainerExitMode::Ghost
    reflow_transition={
        kind: ContainerReflowTransitionKind::Ease
        frames: 18
        curve: ContainerReflowCurve::OutQuad
        name: ""
    }>
    for note in self.notes key note.id {
        <NoteCard title={note.title} />
    }
</Stacker>

Stacker reflow currently uses frames, not a duration field. Its ContainerReflowCurve choices mirror the built-in easing names above. ContainerReflowTransitionKind::Named is reserved and is not implemented as a named-timeline lookup.

Try it: Transition Grid

Choose Remove to watch a tile leave. Switch from Flow to Ghost and remove another: compare when the remaining tiles begin moving into its space. Snap and Ease change how those remaining tiles reach their new positions.

Next, tap a tile to change its accent color and increment its counter, then choose Reverse. The loop uses key cell.id, so each tile keeps its identity as its position changes. A fifth tap replaces that tile with a new one at the end of the list. Insert adds a fresh tile at the beginning. The tile panel scrolls independently, keeping the policy controls in reach as the list grows.

The source tabs show the container policies in lib.pax, the child's enter and exit timelines in cell_button.pax, and the Rust handlers that update the collection. Restart reloads the whole example; Open standalone gives it a separate page.

Use keys and exercise rapid changes as well as settled states. A ghost may overlap its moving siblings while exiting, so consider clipping and visual order in the surrounding composition. Container reflow controls layout frames; animate a nested surface when you want a separate scale or flourish without making it responsible for the container's placement.

Paths, interaction, and motion choices

A playhead can come from more than a clock. A slider, pointer position, scroll position, or Rust simulation can supply the value that several tracks sample. Keep the mapping explicit and bounded: translate the input into the timeline's position range, then let the tracks own the visuals.

The marionette example uses shared and per-part playheads; pax-logo exposes progress for coordinated vector motion (its reusable AnimatedPaxLogo also accepts background_mode=LogoBackgroundMode::Light or Dark to invert the signboard against the surrounding surface); and mouse-animation derives a moving shape from a parametric path. timeline-playground explores longer sequences and mixed duration units. These are deeper source references after the small examples here.

The reusable logo enables click_to_replay by default: clicking or tapping restarts its 1440ms motion through the same progress property, even mid-play. Use progress=bind:logo_progress when a parent scrubber should stay synchronized. Set click_to_replay=false when an external timeline owns playback, or for an inert finished pose:

<AnimatedPaxLogo width=920.72px height=463.95px
    progress=1.0 click_to_replay=false/>

AnimatedPaxLogo and LogoBackgroundMode are in pax-std and are re-exported by pax_kit; use pax_kit::*; makes them available to a component's template. Scale an enclosing Group from the original 920.72 × 463.95 coordinate system. Click replay does not start animation on mount; the parent owns initial playback. For a settled drawing without animation masks or click behavior, use StaticPaxLogo with the same size and palette properties. The pax-logo starter owns only the surrounding scene and controls; no example-crate dependency is needed.

For handwriting or a drawn-line reveal, animate draw_start and draw_end on Path or Handwriter. Drawing owns the path geometry and reveal limits; Scrolling owns scroll positions and viewports. Animation supplies the changing value.

Choose motion that preserves the interface's meaning when paused or skipped. Keep long decorative sequences user-driven, and provide an application setting that can select a stable final state or a shorter transition when needed. Pax does not currently expose a unified reduced-motion preference in its public platform API; do not assume the examples automatically adapt to the operating system's preference.

The core timeline and property systems are shared by web, macOS, iOS, and iPadOS. The animated property still has its own renderer and native-control limits, and frame scheduling varies by target. Verify the finished interaction on the targets you ship rather than inferring visual parity from a successful web run.

Read more

Continue with Compositing and Effects for clipping, masking, and mixed native/rendered surfaces. Layout and the Stacker API cover ordinary container sizing. Events explains the handlers and lifecycle that drive state changes, while Accessibility and Native Controls covers keyboard and control behavior.

Appearance stack transitions

Fill and stroke stacks transition independently and match entries by position. Equal-length stacks interpolate each pair's paint, material, opacity, and stroke width; cap and join switch at the end. Changing the list length switches to the destination immediately, matching other Vec transitions. Reordering changes the contents of positions, without matching stable layer identities.

Paint crossfades remain one sampled paint, including linear-to-radial changes and unequal stop counts. They are not additional source-over layers. GPU paint/material/opacity edits reuse geometry. Visible strokes and captured alpha-mask source strokes use the same retained geometry for reveal progress; a changing mask reveal redraws its texture without rebuilding CPU outlines. Width/cap/join changes may require new tessellation. See Paint layers for the full value model.

Experimental shader containers expose ordinary properties for effect parameters. A phase property can drive surface-anchored brush motion; stopping that phase does not stop changes in the captured source. Filters do not introduce a separate animation clock.

Compositing and Effects

A note card might combine a photograph, a vector border, editable text, and a native button. Compositing brings those pieces into one scene: what appears in front, what remains visible through a shape, and how transparency changes the result.

This chapter builds on Layout and Drawing. It covers the boundaries around content; Animation explains how to move those boundaries and change their properties over time.

Neon Opacity's Afterimage Observatory combines moving vector layers, transparency, and masked native controls. Adjust the BEAM slider in the Console to change the shared opacity (initially 75%), then try the text field or ENGAGE button while the scene moves. Watch how overlapping surfaces and their controls fade together. Open standalone gives the composition more room to explore.

The masks in this example use geometric coverage; the painted alpha mask section below describes a separate mode. Its motion is driven by frame-based Rust updates.

On iOS and iPadOS, the chassis applies native scene updates and presents Metal surfaces in the same Core Animation transaction. Native text and Scroller occlusion masks are also resolved and applied synchronously in that update, so a new popup does not wait on separate mask callbacks to cover underlying rows. Unchanged masks reuse their applied image; changed masks use a raster cache or are generated before presentation. Cache misses therefore contribute to frame time. Image loading remains asynchronous; supply a placeholder if an image must be visible immediately.

Choose the boundary

Earlier siblings in a Pax template appear in front of later siblings. Put the label before its surface and the surface before its shadow. Nesting gives a subtree a shared coordinate system, so moving or rotating its container carries its descendants with it. See element order and transforms for those foundations.

Choose a container based on what should happen at its edge:

ContainerUse it for
GroupA shared transform and layout space, with content free to overflow
FrameA rectangular or rounded-rectangular clipping boundary
MaskA boundary supplied by another subtree's geometry or painted alpha

These containers do not paint a background by themselves in an ordinary scene. Add a Rectangle or another drawing primitive when you want a surface. A rounded Rectangle changes its own silhouette; it does not clip text, images, or other siblings to that silhouette.

Clip with a Frame

A Frame clips its descendants to its bounds by default. corner_radius rounds that boundary, using a number of pixels:

<Frame x=24px y=24px width=280px height=160px corner_radius=24>
    <Text x=16px y=16px width=248px height=32px text="A view of the coast"
        style={font: "Arial", font_size: 18px, fill: rgb(36, 54, 47)} />
    <Ellipse x=196px y=80px width=140px height=140px
        fill=rgb(72, 124, 112) />
    <Rectangle width=100% height=100% fill=rgb(237, 241, 226) />
</Frame>

The ellipse extends beyond the Frame's right and bottom edges. Only the part inside the rounded boundary is visible. The Text participates in the same clipped composition even though it uses a native text surface.

The radius is clamped to fit the smaller dimension. It is a single radius for the clipping boundary; the four-corner list accepted by Rectangle is a different property contract. For ordinary grouping without a crop, use Group. Layout covers autosizing and choosing the Frame's dimensions.

Clipping changes visibility, not the child's layout size or application state. A clipped child remains mounted. Nested Frames further restrict the visible area; a child's transform does not free it from an ancestor's clip. Leave room inside that boundary for strokes and animated overshoot, or place the decoration outside the clipped subtree.

Image fitting and clipping are separate choices. An Image handles its own rectangular fit; a surrounding Frame can add rounded corners. See image fitting.

Shape content with a Mask

A Mask takes exactly two direct children:

  1. The content to show.
  2. The subtree that supplies the mask.

The alpha property selects how that second subtree is used:

ModeSource behaviorContent support
alpha=false (default)Geometric coverage gives a hard clipping boundaryRendered and native content
alpha=truePainted alpha controls how much content remains visible at each pointGPU-rendered canvas content

Use a Group when either side contains several elements. Here, the first Group contains a surface, native text, and a native button. The second child is an ellipse that selects the visible area:

<Mask x=24px y=24px width=280px height=180px>
    <Group width=100% height=100%>
        <Button x=70px y=84px width=140px height=36px label="Open note" />
        <Text x=60px y=44px width=160px height=28px text="Moss and rain"
            style={font: "Arial", font_size: 18px, fill: rgb(36, 54, 47)} />
        <Rectangle width=100% height=100% fill=rgb(237, 241, 226) />
    </Group>
    <Ellipse x=20px y=10px width=240px height=160px fill=BLACK />
</Mask>

The ellipse itself is not painted as a black foreground object. Pax uses its transformed coverage path to clip the first child's descendants, including native surfaces. Black is simply a convenient fill for the source shape.

Both sides use the Mask's coordinate space. Percentage dimensions resolve against that space; positions, rotations, and nested transforms carry through to the resulting coverage. Bind a source shape's position or size to a property to move the visible window, or animate those values with a timeline. Moving the source changes the window without moving the content underneath it.

Use a path or a subtree

For a custom silhouette, replace the second child with a filled, closed Path. This one cuts the corners off a rectangular label:

<Path width=100% height=100% fill=BLACK
    elements={[
        PathElement::Point(12%, 0%),
        PathElement::Line, PathElement::Point(88%, 0%),
        PathElement::Line, PathElement::Point(100%, 20%),
        PathElement::Line, PathElement::Point(100%, 80%),
        PathElement::Line, PathElement::Point(88%, 100%),
        PathElement::Line, PathElement::Point(12%, 100%),
        PathElement::Line, PathElement::Point(0%, 80%),
        PathElement::Line, PathElement::Point(0%, 20%),
        PathElement::Close
    ]} />

In the default geometry mode, Mask collects coverage paths from descendants of its second child. A source Group can therefore contain several shapes. Keep the source tree small and purposeful: only geometry with a coverage-path implementation contributes. A Group by itself contributes no shape, and native text does not turn into letter-shaped clipping geometry.

The source paths are combined for clipping; this is not a general boolean geometry editor. For holes, overlapping contours, or self-intersections, verify the result on the target renderer instead of assuming a particular union or subtraction rule. Drawing owns path commands and SVG conversion.

Geometry and transparency

With alpha=false, the mask uses geometric coverage rather than painted transparency. An Image contributes its rectangular bounds, not the silhouette of its transparent pixels. Use alpha=true for soft coverage authored with vector fills, strokes, and gradients, as shown below.

Use explicitly filled shapes for predictable source coverage. Shape APIs decide which geometry contributes: for example, a Path with no visible fill or stroke may have no coverage at all. If the source produces no coverage path, geometry mode applies no clip; it does not hide everything. Use conditional content when the intended state is “show nothing.”

A nested Mask can further constrain already-masked content. In geometry mode, the source's paint appearance does not determine coverage, and source-side Frame and Mask clips are not applied to the collected coverage paths. Painted alpha sources use the retained renderer and preserve those vector clips.

Painted alpha masks

Set alpha=true to reveal content according to the source's painted alpha. Opaque paint reveals fully, transparent paint hides, and partial alpha gives partial visibility. The source's RGB color is irrelevant: opaque black and opaque white both reveal fully. This is alpha masking, not luminance masking.

Here a gradient reveals a green surface through the middle and fades it away at both ends:

<Mask x=24px y=24px width=280px height=120px alpha=true>
    <Rectangle width=100% height=100% fill=rgb(56, 100, 78) />
    <Rectangle width=100% height=100%
        fill=@gradient {
            linear: { start: [0%, 50%] end: [100%, 50%] }
            0%: rgba(0, 0, 0, 0)
            25%: rgba(0, 0, 0, 255)
            75%: rgba(0, 0, 0, 255)
            100%: rgba(0, 0, 0, 0)
        } />
</Mask>

Rectangle, Ellipse, Path, and Line appearance layers can supply alpha, including solid alpha and linear or radial gradient stops. Source transforms and opacity apply too. Use up to eight ordered gradient stops. A Group or repeated subtree can combine supported source shapes; overlapping paints combine with source-over alpha, so two half-opacity shapes give 75% coverage where they overlap.

A primitive source combines all its fill and stroke layers before applying its own opacity. Two opaque layers on one 50%-opaque source contribute 50% alpha; two 50%-opaque layers on an opaque source contribute 75%. Layer paint uses the full smoothed geometry bounds, independent of outline width or reveal. Alpha sources use the same retained vector draws and offscreen surfaces as ordinary group opacity. Group boundaries apply after their children compose: two overlapping opaque shapes under Group opacity=50% produce 50% coverage throughout, including their overlap. This differs from setting 50% opacity on each shape, which produces 75% in the overlap.

Source-side Frames and nested Masks apply their vector clips. For example:

<Mask width=280px height=100px alpha=true>
    <Rectangle width=100% height=100% fill=CYAN/>
    <Frame width=100% height=100%>
        <Group width=100% height=100% opacity=50%>
            <Rectangle width=65% height=100% fill=WHITE/>
            <Rectangle x=35% anchor_x=0% width=65% height=100% fill=WHITE/>
        </Group>
    </Frame>
</Mask>

The source is captured independently of the consuming content's ancestor clips and opacity. Its alpha is then feathered, if requested, and combined with the consumer's enclosing alpha mask. The content still uses its own ordinary clips and opacity; those factors are not applied again inside the source.

Path strokes used as alpha sources respect draw_start and draw_end. A reusable vector-producing component can supply the source too. Its template expands and its mount handler runs normally; reactive properties, timelines and tick/pre-render handlers remain live until the source is removed. For example, A mesh gradient on the painted surface keeps one continuous field across the word while handwriting reveals it.

Handwriter can reveal a colored surface as its generated stroke is drawn:

<Mask width=300px height=100px alpha=true>
    <Rectangle width=100% height=100% fill=rgb(0, 220, 235)/>
    <Handwriter width=100% height=100% text="Pax"
        stroke={paint: WHITE, width: 5px}
        draw_end=@timeline { duration: 2s, 0: 0%, 100%: 100% }/>
</Mask>

The source is a live component tree, but its output goes only to the mask. It does not create visible native surfaces or input targets. Native Text and controls—including Handwriter's invisible selectable text equivalent—do not contribute mask pixels. Keep a text equivalent outside the mask when a vector reveal carries meaning. Source mount handlers can still perform application side effects, so use the same lifetime/cleanup discipline as visible components. Detached sources do not receive viewport-proximity events: their local geometry supplies the mask and does not represent visible viewport content.

This does not make every component a supported source: the rendered leaves must still be supported vector shapes. Scrollers and native leaves inside the source are excluded; this does not capture across native surfaces. The default geometric mode also differs from painted alpha: a Path's nonempty draw range contributes its complete stroke outline to geometric coverage, not the progressively drawn segment. Use alpha=true for handwriting reveals.

Add feather=2.0 to the Mask to soften its painted coverage. feather is the Gaussian standard deviation in logical pixels, rather than a percentage or a Pax length literal. It defaults to zero; negative values are treated as zero. It affects alpha masks only and does not blur the revealed content itself. Captures include a gutter for the blur's three-standard-deviation support, including nested feathering, so tile edges do not cut off nearby source pixels. If the padded capture exceeds the GPU texture-size limit, the mask hides its content and reports a rendering diagnostic.

Living Quilt uses alpha=true feather=2.0 for its moving color waves. See the live Living Quilt and its source for the complete two-child composition: a colored quilt followed by a Group of repeated animated paths that supplies the coverage.

Nested alpha masks on the content multiply their coverage, while ordinary geometric clips further restrict the result. An empty or fully transparent alpha source hides all content. Alpha masking modulates individual canvas draws; it does not flatten the content into an isolated group before applying transparency.

Alpha masks can reveal canvas content inside a Scroller, including a nested Scroller with intervening Frames or Masks. The source and content use the owning Scroller's content coordinates, so their authored offsets stay aligned as the viewport scrolls. The mask-strokes example includes live handwriting, a direct Path source, and a nested scrolling ring to exercise this boundary. Source-side vector clipping is supported; a Scroller inside the source subtree still cannot supply a cross-surface capture.

The current alpha path requires the GPU renderer. Native text and controls are not supported as content inside an alpha mask, and Piet does not support alpha masking. Images, text, and native elements are not supported alpha sources. For a mixed native/rendered composition, keep the default geometry mask; for a soft visual reveal, use supported vector source paint and keep interactive hit targets separate. Alpha coverage does not change hit testing.

Paint, layer opacity, and stroke-reveal changes reuse retained geometry. A changing reveal redraws the source texture with GPU draw-range parameters; it does not trim and tessellate a new outline on the CPU. Unchanged captures reuse their pixels. Removing a source retires its retained draws, mask textures, and capture resources.

Opacity through a subtree

A vector element assembles its fill and stroke layers before applying its opacity, so overlapping layers fade together. Each layer also has independent opacity; see Paint layers.

opacity accepts a normalized value such as 0.5, or a percentage such as 50%. On WGPU and the browser's Piet/Canvas2D renderer, it fades the composed canvas content in a subtree. There is no separate isolation setting. For example, these overlapping rectangles form one silhouette with uniform 50% opacity:

<Group x=24px y=24px width=280px height=120px opacity=50%>
    <Rectangle x=0px y=0px width=160px height=100px
        fill=rgb(56, 100, 78) />
    <Rectangle x=100px y=20px width=160px height=100px
        fill=rgb(56, 100, 78) />
</Group>

The rectangles compose at their own paint alpha first; the Group then fades that result once. Nested opacity works the same way: a child with opacity=50% fades inside its parent, and a parent at 50% fades the assembled result again. Setting a child to opacity=1 cannot cancel an ancestor's fade. A Group does not add clipping; use a Frame or Mask to restrict overflow.

Paint alpha remains useful for a different purpose. Lower a background's fill alpha to let content behind it show through while its label stays crisp. Set the container's opacity to fade its assembled canvas artwork. See paint alpha, including rgba units. Independent sibling fades still overlap using source-over; complementary opacities do not keep a crossfading silhouette fully opaque.

Current backend limits: both renderers compose each canvas portion separately. Live native text and controls, and content in separate native Scroller surfaces, fade independently. Their overlap can therefore differ from an exact image of the whole mixed subtree. The native coverage masks below remain approximate. The focused examples/fixtures/group-compositing fixture has matching overlap probes on web WGPU, native macOS, iPhone/iPad simulators and a physical iPad. Its interrupted exit preserves edited native text on web, macOS and that iPad. These checks do not establish full accessibility coverage or performance for larger applications. Piet's browser fixture also matches the overlap reference and preserves native input during reversal. There is no author-selectable legacy mode.

WGPU retains visible group content across opacity changes. Source image, paint, transform, clip and lighting changes invalidate that content; outer translation and native mask updates still perform work. Group surfaces add memory and composition passes, so caching is not a guarantee of faster frames. Piet replays dirty canvas content and reuses temporary canvases for composition; it does not retain composed group pixels between dirty frames. Fully opaque scopes bypass the temporary canvas, and fully transparent scopes skip painting. Each simultaneously nested translucent scope can require another tile-sized canvas. The browser still owns live native text and controls.

Opacity does not unmount a component, disable a control, or manage keyboard focus. Keep those state and interaction decisions explicit. Likewise, a visual mask is not an accessibility description or a promise of pixel-exact hit testing for every control. See Accessibility and Native Controls.

Native compositing

Pax draws vector primitives and Image content through its renderer. Text, NativeImage, and form controls use native surfaces supplied by the browser or operating system. Those surfaces share the Pax scene's transforms and ordering, even though they are not all painted into the same canvas.

To make rendered content appear above a native control, Pax computes its coverage and masks the corresponding portion of the native surface. This is often called punch-through in the implementation. You normally express the desired order through the template:

<Group x=24px y=24px width=280px height=120px>
    <Ellipse x=180px y=0px width=80px height=80px fill=rgb(207, 120, 79) />
    <Button x=24px y=24px width=220px height=44px label="Partly covered" />
    <Rectangle width=100% height=100% fill=rgb(237, 241, 226) />
</Group>

The ellipse is earlier in the template, so its overlapping area appears in front of the Button. The later Rectangle stays behind both. Moving the ellipse updates that relationship; wrapping the composition in a Mask adds another visible boundary.

On Apple targets, changing coverage can require CPU rasterization of native punch-through masks. These masks store one coverage byte per pixel, and unchanged results are cached. A moving or fading overlay can invalidate that cache every frame, so a large masked surface can still be expensive. Native and rendered content are published synchronously to keep their coverage aligned. The browser uses SVG/CSS masks; the Apple rasterization cost is not a measurement of that path. macOS text snapshots consume that same coverage, including fully hidden pixels and partial opacity; changing text reapplies the mask to the new snapshot.

On macOS, masked Text snapshots include the text's painted overflow when clip=false; entering an overlap does not itself crop the label to its layout box. clip=true and ancestor Frame clips still constrain the text. Occlusion candidates are currently selected using layout bounds, so give text a box that covers its visible content when accurate masking of all overflow is required.

Updating native content

Ordinary leaf animation updates shared scene geometry and queries nearby native surfaces to refresh punch-through masks. Both the old and new coverage matter: a moving or disappearing shape must uncover the surface it leaves behind. Scroll-content geometry stays indexed in content coordinates, so scrolling a leaf-only native scroller queries the old and new visible regions without reindexing every descendant.

On macOS and iOS, the Swift native host also applies leaf patches to retained views by element ID. An unchanged native control keeps its view, selection, and focus. Opacity and stacking updates avoid rewriting native geometry; actual scale changes still use the platform's geometry path. On macOS, updated native content keeps its punch-through mask even when the mask geometry is unchanged. On macOS, clipped scroller islands containing native leaves can also detach offscreen Text and NativeImage views outside the renderer's warm region. This uses the same geometry index and platform prewarm margins as canvas rendering; it does not change the fixed margins of @viewport_proximity_* events. The runtime keeps the Pax nodes alive, and the native host retains each view and continues applying its patches. Re-entry restores that instance with its latest content, geometry, mask, and stacking order. This reduces the attached AppKit hierarchy, rather than the number of Pax nodes or retained native instances.

Editable text, focused or selected text, text overflowing its layout bounds, glass-backed content, and other native controls stay attached. Enabling VoiceOver or Switch Control restores the full native hierarchy. This culling optimization is currently macOS-only; web, iOS, and iPadOS retain their existing behavior. Nested presentation containers and unclipped/escaping content keep the conservative rendering path.

Structural changes, inherited clip/container changes, escaping unclippable branches, and scroll domains with nested presentation containers, disabled clipping, or disabled scroll islands currently use full reconciliation. These optimizations do not guarantee a particular frame rate; layout, native controls, and GPU work still contribute to frame time.

Coverage has limits

Native punch-through uses coverage geometry and an opacity estimate; it does not sample every final rendered pixel. This matters for subtle cross-surface blends:

In particular, a translucent canvas popup above a native Scroller does not behave like an isolated composited surface above that Scroller. The underlying canvas is attenuated again where the partially masked native scroll surface covers it. During a fade, artwork can therefore appear stronger in row gutters than over the rows themselves, even when the masks update synchronously. Per-primitive opacity does not correct this cross-surface blending limitation.

  • A gradient with changing alpha has one estimated coverage opacity for the native mask, rather than a separate alpha value at each pixel.
  • A rendered Image's coverage is rectangular, including transparent pixels in the source image.
  • A partially revealed Path can retain conservative coverage for its full stroke so the runtime does not rebuild native occlusion geometry on every animation sample.

These cases can differ from placing both pieces in a single paint surface. Keep important text and controls clear of such overlaps, and verify an intentional cross-surface effect on the target where it will ship. The occlusion and neon-opacity examples are useful places to explore the current behavior.

Scroller islands and modal underlays

A native Scroller can host its own rendered-content surface, often called an island. Its scrolling and clipping belong to that host. A translucent Rectangle on the root canvas therefore cannot be assumed to tint every native control and scroller island as if the whole interface were one image.

For a modal background that must both dim the scene and absorb pointer input, use an EventBlocker with a translucent solid background. Place the panel before it and the underlying content after it:

<Group width=100% height=100%>
    <Group x=50% y=50% width=280px height=120px>
        <Text x=20px y=20px width=240px height=32px text="A moment to pause"
            style={font: "Arial", font_size: 18px, fill: rgb(36, 54, 47)} />
        <Rectangle width=100% height=100% corner_radius=16
            fill=rgb(237, 241, 226) />
    </Group>
    <EventBlocker width=100% height=100% background=rgba(0, 0, 0, 128) />
    <Button x=24px y=24px width=160px height=44px label="Underlying action" />
    <Rectangle width=100% height=100% fill=rgb(248, 244, 234) />
</Group>

This demonstrates the surface ordering, not a complete modal component. In an application, mount the panel and blocker conditionally and provide dismissal and focus behavior. EventBlocker's background is transparent by default. It is a native surface that absorbs pointer input; it does not establish a keyboard focus trap. Routing covers route-driven panels and their lifecycle.

On native iOS and iPadOS, the blocker retains UIKit touch ownership so the content behind it cannot scroll. It forwards touch sequences into Pax's scene hit test, allowing GPU-rendered panel controls above it and an @click handler on the blocker itself to work. A single-finger tap activates once; drags, cancelled touches, and multi-finger sequences do not become backdrop clicks.

Platform-specific effects

Apple Liquid Glass

LiquidGlass provides an inherited effect scope for supported Apple native surfaces. A Group inside it can supply a rounded glass surface, while supported controls receive the native treatment:

<LiquidGlass x=24px y=24px width=280px height=160px
    spacing=12px variant="regular">
    <Group width=100% height=100% corner_radius=20>
        <Text x=20px y=20px width=240px height=32px text="Field notes"
            style={font: "Arial", font_size: 18px, fill: rgb(36, 54, 47)} />
        <Button x=20px y=76px width=240px height=44px label="Continue" />
    </Group>
</LiquidGlass>

variant supports "regular" and "clear". tint supplies an optional color; spacing controls grouping distance. interactive=true requests the interactive treatment where supported. A nested <LiquidGlass enabled=false> opts its descendants out of the inherited scope.

The Apple implementation uses native glass on macOS 26 and iOS/iPadOS 26 or later, with native visual-effect fallbacks on earlier supported OS versions. The details differ by platform; for example, the current UIKit implementation applies the interactive flag, while the macOS implementation does not provide the same response. Web retains ordinary controls without the Apple glass treatment. Give that fallback an intentional, readable composition rather than depending on glass for contrast.

Use examples/liquid-glass to explore the native effect and nested opt-outs on Apple targets. A successful web run checks the fallback, not the Apple appearance. The LiquidGlass API lists the scope's properties.

Lighting and other effects

Light-reactive vector materials and LightFrame are taught in Drawing. Their GPU lighting changes the drawn material; it does not automatically blur or refract all the native content behind it. Piet renders those materials unlit.

In Materials, scroll through the collection of imagined elements. Scrolling moves a light across surfaces with matte, glossy, metallic, emissive, and custom materials. Compare how their highlights respond, then open the Rust source to see the material parameters and scroll-driven light position. The lighting requires the WGPU renderer; Piet shows the unlit fills and strokes.

An experimental GPU subtree blur is available through GaussianBlur. Configurable blend modes and portable backdrop materials are not part of the current public toolkit. You can also build depth with offset shapes, gradients, transparency, and motion. Keep authored shadows and glows distinct from a sampled backdrop effect when explaining what an example demonstrates.

Check the finished composition

Start with the smallest composition that shows the intended relationship. Check it at rest, after resizing, during scrolling, and at intermediate animation values. Include a native control in the test if the finished interface mixes native and rendered content; an all-vector mockup does not exercise that boundary.

When something disappears, inspect source order and ancestor clips first, then Mask child order and source coverage, followed by subtree opacity and the current native/canvas limits. If the issue appears only while crossing a native surface or Scroller, compare the surface arrangement as well as the element's local geometry.

From the repository root, run a canonical testbed:

pax-cli run --path examples/fixtures/occlusion --target web

neon-opacity adds animated nested transparency and mixed controls. Verify the target and renderer you intend to ship: web, macOS, iOS, and iPadOS share the authoring model, but their native surfaces and effect implementations have the qualifications described here.

Read more

Continue with Scrolling and Viewports for content coordinates and scroll-driven composition. Revisit Layout for transforms and bounds, Drawing for paint and geometry, and Animation for changing those values over time. The Frame, Mask, and EventBlocker references list their public APIs.

Experimental shader containers

A userland primitive can filter its rendered descendants with a sequence of WGSL fragment passes. pax_std::shaders provides GaussianBlur and Impressionist, also exported by pax_std::* and pax_kit::*. Both use the same public renderer extension API available to your own primitives. The shader-studio example imports these standard-library components.

Shader containers are opt-in. Enable the shaders Cargo feature on your existing pax-kit dependency (keep any existing path and other features):

[dependencies]
pax-kit = { version = "0.39.0", features = ["shaders"] }

This enables both the standard-library effects and the generic renderer support for custom effects. Applications using pax-std directly can enable its feature of the same name. Existing applications using shader containers must add this feature before rebuilding. shader-studio already enables it.

The feature is off by default in both debug and release builds. Without it, shader components, runtime hooks, capture/compositing code, and their runtime WGSL validation path are excluded. On web, this also avoids embedding the Naga WGSL parser/validator in the application Wasm. Ordinary drawing, opacity, and masks remain available. Cargo features are additive: if any dependency enables shaders, the support is included even when no effect is mounted. Template usage does not automatically enable the feature. When enabled, effect textures and pipelines are still allocated only when needed.

<Frame width=420px height=280px corner_radius=18>
    <GaussianBlur width=100% height=100% radius=8.0>
        <Ellipse x=40% y=40% width=120px height=120px fill=CYAN />
        <Rectangle width=100% height=100% fill=rgb(20,31,32) />
    </GaussianBlur>
</Frame>

This experimental slice captures vectors and renderer-backed Image nodes. Native Text, NativeImage, controls, native materials, descendant Scrollers, and unclippable descendants cannot be captured; unsupported content produces a diagnostic. A shader container may live inside a Scroller. Effects inside detached mask sources are also unsupported. Use the WGPU backend; the legacy Piet backend does not implement this API. The first target is web WebGPU, with Metal pixel tests covering the shared renderer.

Clips and opacity inside a shader container become part of its source image. Clips outside the container apply to the filtered output, and the container's own opacity applies after filtering. Nested containers run from the inside out. Filters may expand beyond layout bounds by their declared sampling support; an enclosing Frame clips that expansion. Layout and hit testing continue to use the original tree.

WGPU reuses capture textures when their pixel dimensions stay the same, including when only their origin changes. A shader container also reuses its pass textures and bindings when its coordinate transform changes without replacing its source texture or shader program. This avoids allocation churn; transformed or changed content still needs rendering. Resizing the capture can allocate new textures. The capture envelope includes descendant drawing bounds and sampling support, so a moving child that extends beyond the container can change that envelope although the container's layout size is fixed.

For an inexpensive exit, stop both source animation and shader phase, then fade a stationary ancestor Group. WGPU can reuse the assembled pixels across that opacity-only fade. Continuing paint, mask, or transform changes adds work; opacity alone does not freeze those descendants. To animate a radial color field without moving its capture bounds, animate the gradient's center inside fixed geometry. Keep effects around the smallest useful subtree.

When filtered GPU content overlaps native content, native punch-through masks continue to use the original geometry and coverage opacity. Ordinary transforms, clips, and opacity still update those masks. Shader displacement, output alpha, and blurred edges do not change the native cutout. This deliberate approximation means native overlap need not match the filtered silhouette. Native pixels are not sampled by the shader.

Built-in effects

GaussianBlur.radius is Gaussian standard deviation in local logical pixels, clamped to 0–48. Zero is identity. Its reference mode performs full-resolution horizontal and vertical convolution truncated at three standard deviations. The default mode smoothly blends to quarter-resolution convolution between 6 and 10 logical pixels. That reduced-resolution result is an approximation; it trades fine detail for lower sampling cost at large radii. reference=true keeps the full-resolution result for comparison. Both operate on premultiplied RGBA in the renderer's capture color space, without a linear-light guarantee.

Impressionist.mode selects a procedural brush treatment. The enum is exported alongside the component as ImpressionistMode:

ModeTreatment
Basic (default)The original three scales of softly layered oval marks.
GauguinBroad directional sweeps, simplified color planes, and subtle bristles.
VanGoghEvolving curling flows with highlight/shadow ridges suggesting thick paint.
SignacFine, closely packed rectangular dabs interweaving pale and chromatic pigment.
CezanneVaried patches of parallel marks and nested arcs, with broken blue-violet contours.
SeuratSmaller, denser rounded points with restrained pale pigment.
<Impressionist width=100% height=100%
    mode=ImpressionistMode::Cezanne size=24.0 seed=17>
    <Ellipse x=50% y=38% width=110px height=110px fill=rgb(248,213,141)/>
    <Rectangle width=100% height=100% fill=rgb(77,112,118)/>
</Impressionist>

These are techniques inspired by the painters, not reproductions of their compositions. The simulated paint relief is color shading, not actual surface geometry or scene lighting. Pale pigment stays within source coverage; transparent content does not acquire an opaque white background.

The modes share one two-pass shader program. Signac and Seurat use the same pointillist brush function with preset values for spacing, proportions, roundness, position variation, direction, pigment, relief, and drift. Other brush families have distinct construction functions in that program. These internal preset controls are not yet individual public properties; mode, size, seed, and phase are the supported authoring controls.

seed fixes surface anchors and seeds the directional fields. phase is an absolute motion position, not elapsed time owned by the shader. It drives breathing and bounded mark movement around those anchors: broad slow drift in Gauguin, grouped shifts in Cezanne, and smaller motions in the pointillist modes. VanGogh also moves and deforms its swirl contours and ripples paint ridges along the strokes, so motion extends beyond the vortex centers. These motions work over a stationary source. They are procedural, not fluid simulation: they use no history texture or additional rendering pass. Mark colors and contour alignment still respond to changing source content. There is no motion tracking or previous-frame state. Stopping phase updates freezes brush motion while source updates continue. Identical source content, mode, seed, size, and phase give identical output. Changing mode switches the treatment immediately; it does not morph one brush family into another. Mode and phase changes share the existing pipeline and capture envelope, so they can reuse an unchanged source capture. size is an overall brush scale in local logical pixels, clamped to 4–32. Each mode applies its own proportions: a Seurat dot is smaller than a Gauguin sweep at the same setting. Size also scales directional patterns, including VanGogh's whorls. Changing it redistributes the marks rather than scaling the source scene. Its default value of zero resolves to the minimum size.

For an interactive control, declare pub brush_size: Property<f64> on your component, initialize it to 24.0 in its mount handler, and bind both a Slider and the effect to it. Keep the native Slider outside the filtered subtree:

<Slider width=100% height=28px value=bind:brush_size min=4.0 max=32.0 step=1.0/>
<Impressionist y=44px anchor_y=0% width=100% height=240px
    mode=ImpressionistMode::VanGogh size={self.brush_size} phase={self.phase}>
    <Rectangle width=100% height=100% fill=rgb(77,112,118)/>
</Impressionist>

Here phase is another Property<f64>; advance it from a tick handler or bind the effect's phase to a timeline to animate the flow. Leave it constant to keep the brush field still. For adjustable speed, accumulate phase += delta_seconds * speed: zero holds the current pose and negative speed reverses it. Change the accumulated phase's rate rather than multiplying total elapsed time by a changing speed, which would jump the current pose. The same phase value always reconstructs the same motion position, so ordinary Pax property transitions can ease or scrub it without simulation state.

See the API references for GaussianBlur and Impressionist.

These inputs are ordinary properties. Bind or animate them using existing expressions and motion, without an effect-specific clock. The example keeps brush phase and source animation independently controllable and includes a selector for comparing all six modes on the same scene. Its brush-size slider starts at 24 and remembers a separate value per mode for the current session; reloading resets those values. A separate motion-speed slider ranges from −5× to 5×, initially 1×, and affects brush phase only.

Run the canonical example from the repository root:

pax-cli run -p examples/fixtures/shader-studio -t web

For the Rust extension contract, see shader primitives.

Scrolling and Viewports

A list of notes, a horizontal shelf, and an illustrated story all need a way to show content that extends beyond the available space. Scroller gives that content a viewport, clips it at the edges, and connects it to the platform's scrolling behavior on web, macOS, iOS, and iPadOS.

The scroll position is also a property. You can use it to return to the top, show reading progress, or move through an animation as the reader explores.

This chapter builds on Layout and property handles and bindings. For Rust handlers, see Event Handling.

Viewport, content, and position

A Scroller has three related sets of dimensions:

PropertyMeaning
width, heightThe visible viewport, placed by ordinary Pax layout
scroll_width, scroll_heightThe extent of the scrollable content pane
scroll_pos_x, scroll_pos_yThe offset into that pane, as numeric pixel values

Here is a 240-pixel-tall viewport onto 720 pixels of notes. Paste this into src/lib.pax in a project whose Rust main component imports pax_kit::*:

<Scroller x=24px y=24px width={100% - 48px} height=240px
    scroll_width=100% scroll_height=720px corner_radius=16>
    <Group width=100% height=720px>
        for i in 0..4 {
            <Group y={(i * 180)px} width=100% height=180px>
                <Text x=20px y=20px width={100% - 40px} height=40px
                    text={"Field note " + (i + 1)}
                    style={font: "Arial", font_size: 24px, fill: rgb(36, 54, 47)} />
                <Rectangle x=8px y=8px width={100% - 16px} height=164px
                    corner_radius=12 fill=rgb(225, 234, 220) />
            </Group>
        }
    </Group>
</Scroller>

Increasing scroll_pos_y moves the visible window farther down the content; the notes move upward on screen. At rest, the vertical range here is 0 through 720 - 240 = 480 pixels. If the content fits inside the viewport, there is no travel on that axis. Platform overscroll and bounce can temporarily present the edges differently.

scroll_height=300% would also describe 720 pixels for this viewport: percentages on the scroll extent are relative to the Scroller's own viewport. The extent establishes how far you can scroll; it does not stretch the child tree to that size. A direct child at height=100% still receives the viewport-height layout frame. The explicit 720-pixel Group above gives its descendants a content-sized coordinate space.

These dimensions and offsets are in the Scroller's local, unscaled coordinates. For example, putting the 240/720-pixel example inside a scale=50% Group halves its on-screen size, but its vertical scroll range remains 480 local pixels. On web, the declared content pane bounds browser scrollable overflow; transformed native overlays do not add empty travel beyond that extent.

Scroller does not paint a background. Place painted content inside it, or a background sibling behind it, depending on which should move. Earlier Pax siblings appear in front; that is why each note's Text precedes its Rectangle. corner_radius is a numeric pixel radius for the viewport clip.

Rendering density

Web vector surfaces, including tiled Scroller content, render at the browser's device-pixel ratio in debug and release builds. A 2× display uses two backing pixels per CSS pixel on each axis; becoming scrollable or tiled does not opt content into lower-resolution rendering. This applies to WebGPU and the Piet/Canvas2D fallback. Native text retains the browser's own rasterization.

When a pane cannot fit the tile budget at screen density, Pax splits it into smaller surfaces and retains a bounded window around the viewport. It does not reduce resolution merely to keep a pane on one canvas. Hard backend/browser surface-dimension limits still guard allocation: an exceptional clamp emits a warning and marks the affected canvas with data-pax-resolution-fallback, including requested/actual density and the limit. This is a safety fallback, not an adaptive scrolling-performance mode. Investigate repeated warnings; high-density rendering still requires profiling complex or heavily animated scenes on the devices you support.

Autosized Scrollers

For a document that grows as you add content, let Scroller measure its pane:

<Scroller x=24px y=24px width={100% - 48px} height=240px
    scroll_width=100% autosize=true corner_radius=16>
    <Stacker width=100% autosize=true gutter=12px>
        for i in 0..5 {
            <Text width=100% height=80px text={"Measured note " + (i + 1)}
                style={font: "Arial", font_size: 24px, fill: rgb(36, 54, 47)} />
        }
    </Stacker>
</Scroller>

The stack measures five 80-pixel children and four 12-pixel gutters. Scroller uses that 448-pixel content height while its viewport remains 240 pixels tall. Changing the children or their measured sizes updates the scrollable extent.

autosize=true on Scroller manages the vertical content extent by default. Horizontal size continues to use scroll_width; autosize_x=true opts into horizontal measurement. autosize_y=false turns off vertical measurement even when autosize=true.

On a measured axis, Scroller uses its resolved content measurement in preference to scroll_height or scroll_width; the explicit value is a fallback if that measurement cannot be resolved. The public size property remains the input, so reading self.some_scroll_height does not automatically give you the measured result. This differs from asking a container to measure its own outer dimensions.

Give measured content a useful starting point: concrete row heights, text measurement, or an autosized stack. Avoid making an expanding document depend only on height=100%. Font loading and native measurements can change the settled extent. Layout's autosize section explains the shared measurement rules.

Read and set the scroll position

Use bind: to share the Scroller's position with a component property. User scrolling updates that property; setting it from Rust requests a new position. This also gives buttons and visual indicators one place to read the state.

An initial nonzero position applies when the Scroller mounts, including when it appears inside an if branch. Later property writes move the native host and the renderer's viewport together; no user scroll is needed to reveal the new position. This matters for large two-dimensional panes such as a graph: center with (content extent - viewport extent) / 2 on each axis, and update the offsets together with the content extent when zooming. Keep requested offsets inside the available range; active native gestures and platform bounce still control their normal interaction behavior.

On Apple hosts, content-size changes preserve the requested position while clamping the visible viewport to the available content. If the content grows again, that requested position becomes visible again unless native scrolling or an application write has replaced it. macOS layout and backing-pixel alignment do not count as user scrolling: mounting or moving a fractionally positioned Scroller must not reset its bound offsets.

In src/lib.rs:

#![allow(unused)]
fn main() {
use pax_kit::*;

pax_kit::runnable! { root: crate::Example }

#[pax]
#[file("lib.pax")]
pub struct Example {
    pub scroll_y: Property<f64>,
}

impl Example {
    pub fn back_to_top(&mut self, _ctx: &NodeContext, _event: Event<ButtonClick>) {
        self.scroll_y.set(0.0);
    }

    pub fn last_note(&mut self, _ctx: &NodeContext, _event: Event<ButtonClick>) {
        self.scroll_y.set(480.0);
    }
}
}

In src/lib.pax:

<Button x=24px y=24px width=120px height=36px label="Back to top"
    @button_click=self.back_to_top />
<Button x=156px y=24px width=120px height=36px label="Last note"
    @button_click=self.last_note />
<Text x=24px y=72px width=280px height=28px
    text={"Offset: " + self.scroll_y}
    style={font: "Arial", font_size: 18px, fill: rgb(36, 54, 47)} />

<Scroller x=24px y=120px width={100% - 48px} height=240px
    scroll_width=100% scroll_height=720px scroll_pos_y=bind:self.scroll_y>
    <Group width=100% height=720px>
        for i in 0..4 {
            <Text x=16px y={(i * 180 + 16)px} width={100% - 32px} height=80px
                text={"Field note " + (i + 1)}
                style={font: "Arial", font_size: 24px, fill: rgb(36, 54, 47)} />
        }
        <Rectangle width=100% height=100% fill=rgb(225, 234, 220) />
    </Group>
</Scroller>

Use plain numbers for these offsets: 480.0, rather than 480px. Their Rust type is f64; the extent properties use Size and accept px or %.

The example's 480-pixel destination comes from its known dimensions. For a responsive document, derive destinations from current layout and content, and keep them within the available travel. Content shrinkage and viewport resizing can constrain what the native host can show. Avoid repeatedly writing a saved position while the person is actively scrolling; native gesture updates and application writes would compete for the same state.

Position versus scroll events

@scroll sends an Event<Scroll> with delta_x and delta_y. It is a shared delta stream used by wheel/touch input and native position notifications. It is useful when an action needs movement information. Use the bound scroll_pos_y for reading progress or a return position, rather than building a second position by accumulating event deltas.

Native position notifications report movement that has already happened. They are not a cancellable, cross-platform “before scrolling” hook. Likewise, @wheel describes wheel input and does not cover every way someone can scroll. See Event Handling for event binding and propagation.

A resizable two-dimensional graph

The canonical calculator example uses both scroll axes for a function plot. Its visible graph grows with the LCD, while zoom changes the content pane size. The important template relationship is:

<Scroller width={(self.graph_width)px} height={(self.graph_height)px}
    scroll_width={(self.graph_extent)px} scroll_height={(self.graph_extent)px}
    scroll_pos_x=bind:self.scroll_x scroll_pos_y=bind:self.scroll_y>
    <Group width={(self.graph_extent)px} height={(self.graph_extent)px}>
        <!-- Axes, labels, and bounded curve geometry share this coordinate space. -->
    </Group>
</Scroller>

The Rust component supplies the five numeric properties shown here. Its 1× magnification is 16 logical pixels per graph unit. The opening polar rosette, 1+3*cos(11*θ), selects a zoom step that fits the viewport until the first interaction. After that it keeps the chosen scale on resize, so a larger window reveals more data. Zoom changes the scale and the pane extent together. Both operations preserve the world-space center where the finite boundary permits, derive the new scroll offsets, and regenerate geometry for the exact visible viewport, with no overscan margin. Native scroll position bindings drive subsequent updates; the application does not accumulate a second position from scroll events. Sampling the viewport rather than the whole pane keeps work bounded as the person explores.

Run pax-cli run --path examples/calculator --target web from a source checkout and switch to Graph. Try 1/x, pan, then resize the window. The Rust expression engine deliberately leaves gaps across undefined domains and caps sampling work. Its finite world is −256 through 256 on each axis; this example has one visible function, calculator-style editing, and zoom from 0.25× to 16×. Its 2nd → x θ (MODE) sequence selects Cartesian or polar plotting; polar formulas use θ over 0…2π radians. It preserves separate drafts and valid plots for both modes. While graphing, GRAPH submits the current expression, centers the origin, and focuses the editor without changing scale. Invalid input preserves the last valid curve and displays an error inside the LCD. The sampler reserves work across the viewport, culls off-screen bounds, and represents unresolved continuous subpixel detail with a disclosed range envelope instead of spending the whole budget on the first dense region. The example's README documents the complete grammar, controls, and numerical limits. For polar curves, refinement measures geometric deviation from a chord in screen pixels, not how evenly the parameter moves along it. Conservative range checks cover the whole interval, and a bounded second pass reuses budget left by simpler or culled angular slices. Both passes share the same work and geometry caps; unresolved detail can still appear at extreme complexity.

Calculate mode demonstrates a second, vertical Scroller for history, with a fixed editor outside the viewport. Its content height comes from wrapped character rows. Changing Calculate text size reflows those rows without changing Graph zoom. New results set the bound history offset to the new bottom; ordinary frame updates leave native scrolling in control. This keeps a long history reachable even when the LCD is small.

Horizontal regions and snapping

For a horizontal shelf, make scroll_width larger than width and keep the vertical extent at 100%. The same geometry works on both axes.

Snap positions add landing points. This strip contains three viewport-width panels and snaps at their starts:

<Scroller x=24px y=24px width={100% - 48px} height=180px
    scroll_width=300% scroll_height=100%
    snap_positions_x=[0px, 100%, 200%] corner_radius=16>
    for i in 0..3 {
        <Group x={(i * 100)%} anchor_x=0% width=100% height=100%>
            <Text x=20px y=20px width={100% - 40px} height=48px
                text={"Panel " + (i + 1)}
                style={font: "Arial", font_size: 24px, fill: rgb(36, 54, 47)} />
            <Rectangle x=6px width={100% - 12px} height=100%
                corner_radius=16 fill=rgb(225, 234, 220) />
        </Group>
    }
</Scroller>

Each panel uses anchor_x=0% so its percentage position locates its left edge. Snap percentages resolve against the viewport on the corresponding axis. snap_positions_y provides the vertical equivalent. Leave the lists empty for ordinary continuous scrolling. Web uses native CSS scroll snapping; Apple targets choose native scroll endpoints from the supplied positions. Gesture momentum and settling can differ between platforms.

Carousel packages page layout, content extents, and snapping. Each supplied child becomes a page:

<Carousel x=24px y=24px width={100% - 48px} height=220px
    axis=CarouselAxis::Horizontal page_size=100% show_dots=true>
    <Rectangle width=100% height=100% fill=rgb(225, 234, 220) />
    <Rectangle width=100% height=100% fill=rgb(242, 218, 183) />
    <Rectangle width=100% height=100% fill=rgb(205, 224, 236) />
</Carousel>

Horizontal paging and page_size=100% are the defaults. Choose CarouselAxis::Vertical for vertical pages, and bind scroll_pos_x or scroll_pos_y when the application needs the position. page_size controls each page's extent along the scrolling axis; its percentage is relative to the Carousel viewport. With only one child, that page fills the viewport.

Dots are optional position indicators, hidden when there is only one page. They are not clickable navigation controls. Provide explicit previous/next buttons when your interface needs them, using the bound scroll position.

Scroll-driven motion

Scrolling can reveal a drawing, turn a diagram, or carry a caption through a sequence. Start with a normalized position:

progress = scroll position / (content extent - viewport extent)

Clamp the result to 0..1 and handle a zero-length scroll range. Then map progress to the timeline's playhead range. The dimensions in the state example give a travel of 480 pixels, so this addition to its lib.pax makes a reading-progress bar. Add the Group before the Scroller and place the timeline at the end of the file:

<Group x=24px y=104px width={100% - 48px} height=6px>
    <Rectangle id=reading_progress height=100% fill=rgb(56, 100, 78) />
    <Rectangle width=100% height=100% fill=rgb(210, 218, 206) />
</Group>

@timeline reading {
    duration: 100,
    playhead: {Math::min(1, Math::max(0, self.scroll_y / 480)) * 100},
    loop: false,
    #reading_progress {
        width: {
            0%: 0%, Linear,
            100%: 100%,
        },
    },
}

Here duration: 100 defines the timeline's sampling range. There is no running clock: scrolling backward samples earlier positions, and resting leaves the bar still. A responsive version needs current viewport and content measurements in place of the fixed 480 denominator. Keep the Scroller's geometry independent of the decorative animation so the progress mapping does not change its own scroll range.

The same playhead can drive several tracks. The canonical scroll-garden example binds a vertical Carousel to a playhead for its articulated scenes. Animation and Motion explains track ownership, easing, and playback; Drawing owns Path and Handwriter reveals.

Observe viewport proximity

Bind @viewport_proximity_enter, @viewport_proximity_change, and @viewport_proximity_exit to prepare nearby content, animate a first visible sample, or release work after departure. Bind any subset; enter works on its own. The element carrying the binding is the observed target, including when it is a Group. The reference is always the global viewport, with ancestor clipping and native scroll presentation applied.

<Group width=100% height=200px
    @viewport_proximity_enter=self.prepare
    @viewport_proximity_change=self.measure
    @viewport_proximity_exit=self.release>
    <Group id=card x=50% y=80px anchor_x=50% anchor_y=50% width=100% height=160px>
        <Rectangle width=100% height=100% fill=rgb(49, 96, 201)/>
    </Group>
</Group>

@timeline reveal {
    duration: 100,
    playhead: {self.playhead},
    loop: false,
    #card {
        opacity: { 0%: 0, Linear, 35%: 1, 100%: 1, },
        y: { 0%: 98px, OutQuad, 100%: 80px, },
    },
}

Declare playhead: Property<f64> and departing: Property<bool> on the owning component. These handlers reveal the contents at the first sampled positive overlap, and reverse the timeline when at most 40 logical pixels remain visible and the visible height is decreasing:

#![allow(unused)]
fn main() {
pub fn prepare(&mut self, _ctx: &NodeContext, _event: Event<ViewportProximityEnter>) {
    // Acquire application resources here; this also runs for initially nearby content.
}

pub fn measure(&mut self, _ctx: &NodeContext, event: Event<ViewportProximityChange>) {
    let visible = event.current.viewport_intersection.map_or(0.0, |r| r.height());
    let previous = event.previous.and_then(|p| p.viewport_intersection)
        .map_or(0.0, |r| r.height());
    if visible > 0.0 && (previous == 0.0 || (self.departing.get() && visible > previous)) {
        self.departing.set(false);
        self.playhead.local().cancel_transitions();
        self.playhead.local().ease_to(100.0, Duration::Milliseconds(420.into()), EasingCurve::Linear);
    } else if visible <= 40.0 && visible < previous && !self.departing.get() {
        self.departing.set(true);
        self.playhead.local().cancel_transitions();
        self.playhead.local().ease_to(0.0, Duration::Milliseconds(180.into()), EasingCurve::Linear);
    }
}

pub fn release(&mut self, _ctx: &NodeContext, _event: Event<ViewportProximityExit>) {
    self.departing.set(false);
    self.playhead.local().cancel_transitions();
    self.playhead.set(0.0);
    // Release this target's resource lease here, and separately on unmount.
}
}

This policy compares two samples; it does not infer a physical scroll velocity. Layout, resizing, and transforms can also change the visible height. Observe a stable wrapper and animate its children so the animation does not change its own measurement. The runnable examples/viewport-proximity example adds 270 cards with visit/sample counters, a small uniform scale from 92% to 100%, and restoration when scrolling reverses. The explicit 50% anchors keep scaling centered inside the stable 200px observation wrapper. At rest, y=80px locates the center of the 160px card. The example also loads field notes asynchronously when a page approaches the viewport. Six adjacent cards share one request and a retained cache entry. Web fetches the bundled field-notes.txt over HTTP; native targets decode the same bundled fixture on a worker thread. Both introduce 350ms of latency so scrolling away before completion is reproducible, without an external service.

notes.rs owns the application cache and revocable request leases. Exit and unmount release a consumer; the shared request may finish and populate the cache. A completion writes to a card only if its lease, nearby state, and data identity still match. One root on_tick drains the completion channel on the Pax thread; card geometry is never polled. Worker threads send plain data, not Property values. The example uses fixed data IDs and a bounded session cache (45 pages), including error results; restarting the example retries failures. In an app with changing data IDs, release and reacquire on those changes too, and choose a cache expiry/retry policy appropriate to the data.

Scroll inside the example to reveal cards, then reverse direction to restore departing cards. The counters show proximity visits, delivered geometry samples, and the current visible height.

Samples and delivery

Each current is a ViewportProximitySnapshot: bounds is the transformed layout bounding rectangle, viewport_intersection is the positive-area intersection after clipping (or None), and in_proximity reports proximity membership. Coordinates are logical pixels relative to the global visual viewport's origin. is_in_viewport() and intersection_ratio() provide convenient derived values. The ratio uses bounding rectangle area, not opaque pixel coverage.

SituationDelivery, for the handlers that are bound
Initially nearby, or reentering proximityEnter with current, then change with previous: None
Changed bounds or intersection while nearbyChange with previous and current
Unchanged sampleNo event
Leaves proximityFinal change, then exit; both share the last inside and terminal outside samples
Remains outside, including a jump across the entire regionNo event
UnmountsObservation stops; use unmount for cleanup, with no synthetic exit

Sampling happens once per engine tick after layout settles. Enter and change share the same frozen initial sample. Handlers can update properties; those updates settle for rendering and are observed on the next tick. Events are local and do not bubble or have a cancellable default action. Suspension clears the visit history; resuming nearby produces a fresh enter/change pair. Only the previous and current samples are retained. Keep longer histories in application state if needed, and do not infer movement across a cold gap.

These are sampled observations. Fast scrolling can skip a first-pixel position or the 40px band, and native scrolling may present pixels before its notification reaches Pax. Author initial animation state in the template; this API does not guarantee a callback before the first physical pixel appears.

Region and cost

The MVP uses fixed margins: one global viewport width on each horizontal side, and one global viewport height on each vertical side. Scroll clips receive the same expansion for proximity; ordinary Frame and hard-mask clips remain strict. Viewport intersection always uses the unexpanded clips. unclippable retains its escape behavior. There is no margin or alternate observation-root setting yet. Renderer warm-tile sizes do not affect this public region, so backend tile policies cannot change the margin.

Contact alone and zero-area layout are outside. Rounded, rotated, and path clips use bounding envelopes; opacity, alpha coverage, sibling occlusion, and other windows are ignored. Use the API for geometry and preparation, not impression or pixel-visibility measurement.

Observation is demand-driven: bindings register targets and share ancestor and prepared geometry records. Native scrolling queries nearby content in scroll-owner coordinates instead of rewriting every descendant's bounds. Nested cold scroll domains are pruned, with conservative fallback for escaping branches. Initial registration and whole-list relayout still scale with the registered collection, and each registration uses memory. Many overlapping or unclippable targets can increase query work. This does not virtualize the scene or defer mount/tick handlers. With no proximity bindings, no observation records or detailed samples are installed.

Efficient observation does not make the rest of a large scene free. Updating labels or animating cards also exercises text layout, native view updates, compositing, and drawing. See updating native content for the incremental compositing path and its current boundaries. Profile the complete app on its intended target; query timings alone do not establish a frame-rate guarantee. Sample collection precedes final compositing, so handler changes can settle into the same rendered frame without an extra full compositing pass.

Run the example from the repository root:

pax-cli run --path examples/viewport-proximity --target web

Nested viewports and native content

A horizontal shelf can live inside a vertical document. Give each Scroller a bounded viewport and its own content extent. For example, place the horizontal strip above inside a taller content Group in the outer Scroller. Its width then follows that group's width, while its 180-pixel height remains a visible window onto the strip.

Use nesting when the regions have distinct jobs. Same-axis nesting makes gesture ownership harder to anticipate; test what happens at each edge with the target's trackpad, mouse, and touch input. Pax relies on the platform scroll container for native scrolling, momentum, and gesture arbitration.

Text, form controls, vectors, and images can share the scrolling content tree. The native host moves the content, while Pax maintains the rendering, clipping, and input-coordinate relationships. A web root Scroller that fits the viewport and scrolls only vertically can delegate to page scrolling; you still use Scroller properties rather than browser DOM operations.

For a toolbar that stays in place, keep it outside the Scroller as a sibling, usually reserving space for it in the surrounding layout. A child at y=0px belongs to the scrolling content and moves with it. layout_role=LayoutRole::Breakout removes a child from the relevant layout measurement/placement rules; it does not let that child escape the Scroller's clipping or rendering tree.

Use NodeContext::local_point for custom pointer interaction inside moving content. On iOS and iPadOS, scroll recognition can suppress a tap while the child still receives its lower-level touch sequence. The touches-inside-a-Scroller section explains how to clear transient feedback. Check native controls and nested gestures together on the targets you ship.

Scroller-owned rendering surfaces also matter for overlays. A translucent root Rectangle is not a universal dimmer over every native scroll region. Compositing owns the cross-surface explanation and modal-underlay pattern.

Large collections and practical checks

Pax uses viewport-aware drawing and tiled surfaces to limit rendering work for scrollable content. That does not make for a virtualized list: repeated children still participate in tree expansion, properties, and lifecycle. Offscreen components may still cost startup time and perform application work.

When scrolling reuses drawing tiles at new positions, Pax schedules the visible tiles in that layer for repaint together. Offscreen warm tiles can repaint over later frames, in travel order. This avoids deliberately splitting visible tile updates across frames; it does not guarantee a frame rate or prevent a fast gesture from outrunning the prepared region.

On native iOS, iPadOS, and macOS, surface sizing uses the current screen's pixel density. Shrinking scroll_width or scroll_height (for example, when zooming out on a graph) keeps the content tiled until a single surface fits the native backing limits at that density. This avoids a sudden oversized GPU allocation when the content crosses a tile-size threshold; it requires no application-level zoom restriction.

On iOS and iPadOS, GPU backing surfaces are also limited to a nearby region around the screen and the presented clips of ancestor native views. A shelf far down a vertical page does not allocate GPU surfaces merely because its own horizontal viewport contains cards. Surfaces outside that region are released and recreated as they approach view; native Scrollers, their scroll positions, and the Pax component tree stay mounted. The warm margin is a renderer implementation detail, not an application lifecycle event. This does not defer image decoding or application data loading, and it is not a bound on all application memory. Check rapid scrolling and rotation on the physical device as well as the simulator.

On native GPU and WebGPU backends, adding, removing, or reordering physical tiles preserves the unchanged survivors and their retained scenes. New tiles share the GPU context already used by that logical layer and replay only the content they need. Origin or size changes invalidate the affected tile; a recreated backing surface receives a new renderer even if its tile key is the same. Pending warm replay follows surviving tile identities through reordering, and visible affected tiles replay together before deferred warm work.

Tile replay does not replace a simultaneous content change. When a card animates across a tile boundary, its opacity, transform, and artwork updates must reach the surviving tiles as well as newly exposed ones. Browser surface allocation and tile-position changes are reconciled together; a pooled canvas checked out again starts a new surface generation, even at the same logical tile address. Browser canvas pooling also waits for every GPU surface and asynchronous initializer to release the element before clearing or reassigning it. Released WebGPU canvases are explicitly unconfigured before reuse so a previous row's presentation cannot carry over into an animated entrance; shrinking the backing store alone is not sufficient. This does not reset the shared GPU device or its image cache. Rebinding a logical layer to a different scroller host uses a fresh assignment; an old row's retained pixels must never be exposed in the new host. Reuse is governed by ownership, not a delay.

Browser scrollers share a bounded canvas pool. In both desktop and mobile browsers, the retained warm set uses that same budget: scrollers visible through their ancestor viewports take priority over offscreen preparation, and the root surface and required ancestor scrollers reserve space first. Warm distances use transformed presentation bounds, so scaling an embedded app does not mix local dimensions with screen-space distances. Obsolete canvas assignments across layers retire before replacement allocation; GPU ownership must still finish before reuse. Scrolling through a long collection can release distant drawing surfaces and recreate them on return. This does not unmount application components or defer image decoding. The pool size and warm distances are implementation details, not viewport lifecycle thresholds or a bound on all application memory.

WebGPU initialization is asynchronous. Existing tiles remain usable while additions initialize, and results for removed or replaced surfaces are discarded. Queued browser initialization prioritizes currently visible scroller hosts over offscreen hosts, reevaluating priorities as initialization slots open. It does not cancel initialization already in flight or guarantee that an abrupt jump cannot expose an unfinished tile. If an addition fails, survivors remain usable; another attempt waits for a layout change. New tiles still incur surface allocation and first-draw costs. Sibling tiles using the same GPU context share immutable image textures by image identity, version, and pixel dimensions. A tile can reuse an image already held by a sibling without uploading its pixels again; transforms, clipping, opacity, and draw bindings remain tile-local. WebGPU canvases within one mounted app share a device and its pipeline/immutable-image caches across logical layers, including when rows go cold and return. This avoids repeating device and pipeline initialization during scrolling. Native GPU sharing remains within each logical layer. These behaviors apply in debug and release, not the Piet fallback or native image controls. Separate apps or GPU contexts do not share textures.

The image index holds weak references: after the last tile drops its binding, Pax retains no unused image texture for future scrolling (in-flight GPU work may still reference it). Returning later can require another upload. This does not change decoded-image ownership or defer application initialization.

Removed surfaces are released. Native logical-layer contexts are released when their last tile and pending creation release them; the browser retains its shared device/pipeline context until the mounted app is disposed. Unused image textures are still released when their last binding disappears. The retention region does not grow. These optimizations apply in debug and release builds; the Piet fallback still rebuilds its layer on tile-set changes. They do not defer component or image initialization in application code.

Start with realistic collection sizes and measure first paint as well as scrolling. For a large data set, consider application-level paging or loading bounded batches. Stable keys preserve item identity during changes; they do not defer offscreen initialization. See Components for collection ownership.

Exercise the finished view with its real text, images, and controls:

  • Resize it and change the content count, including an empty or short list.
  • Reach the first and last items, then use a position button and scroll again.
  • Try nested regions, native-control interaction, and rounded edges.
  • Test fast movement on the actual browser/backend and Apple devices you support. Startup cost and tile presentation remain workload- and target-dependent; a desktop web check cannot establish every target's behavior.

From the repository root, these examples offer larger inspection surfaces:

pax-cli run --path examples/fixtures/rounded-scroller-tiles --target web
pax-cli run --path examples/fixtures/scroll-matrix --target web
pax-cli run --path examples/fixtures/scroll-garden --target web

Run one at a time. rounded-scroller-tiles focuses on rounded viewports and mixed content; scroll-matrix exercises nesting, transforms, and controls; scroll-garden explores scroll-driven scenes. Their source lives under examples/ and remains the place to follow the complete applications.

A cinematic catalog

examples/paxflix combines a large hero with 21 horizontal movie shelves inside one vertical Scroller. Its 210 cards reuse 35 local images through stable thumbnail paths; a selected film opens a full-resolution root-level detail panel. Each shelf's viewport spans the screen, with gutter space inside its scrolling content. At zero scroll the first card aligns with the heading; scrolling lets the cards cross the fixed layout gutters and clip only at the viewport edges. Card titles and metadata sit over a dense lower image gradient that follows the selected dark or light theme. Each card observes a fixed wrapper through @viewport_proximity_change and slides and fades its title and metadata over a 480 ms timeline at the first visible sample; the artwork stays still and opaque. The shelf staggers nearby arrivals by 80 ms, with no more than 240 ms of added delay. There is no exit animation. @viewport_proximity_exit rearms the entrance only after a full departure from proximity, so small reversals at a viewport edge do not restart it. This animation does not defer image initialization or unmount the card. The shelves remain mounted while the panel opens, preserving their scroll positions. Short landscape windows use a side-by-side detail layout. Its previous/next controls animate the bound scroll position with ease_to; wheel or touch input cancels that animation so direct scrolling takes over.

Run it with pax-cli run --path examples/paxflix --target web. The example's README documents the asset inventory and a repeatable culling/interaction check. Each shelf defers its ten card trees until it is within one catalog-local viewport ahead of or behind the visible range, then retains them. This uses the catalog's bound scroll position and local height rather than global proximity: an embedding device Frame is a hard clip and would remove the offscreen preparation region. Card entrance animations still use visible proximity samples. Initial mounting therefore avoids expanding the entire catalog, but visiting every shelf can still retain all 210 cards. This is first-visit lazy mounting, not list virtualization or offscreen eviction.

Read more

How Pax Runs

A Pax application has a reactive scene behind the interface you see. Its properties carry values, its component instances organize the scene, and its platform integration brings together drawn content and native controls. Understanding those pieces helps you explain an update, investigate a slow interaction, and choose what to measure before shipping.

The same core model runs on web, macOS, iOS, and iPadOS. This chapter follows it from source to the screen. It builds on Properties, Events, and Layout; What is Pax? introduces the authoring model.

Compiler, runtime, and chassis

Three parts work together to run your application:

PartResponsibility
CompilerAnalyzes the application and its templates, prepares the declarative program, and generates the connections to Rust component types and handlers
RuntimeInstantiates the scene, maintains reactive relationships, handles events, computes layout, and coordinates rendering and native-element updates
ChassisConnects the runtime to a target's window or browser, graphics surfaces, input, native elements, and platform services

Cargo compiles Rust application logic and expands Pax's macros for the selected target: WebAssembly for the browser, or native code for Apple targets. The macros analyze the Rust source and templates selected by that target's configuration, then generate program data describing templates, bindings, events, and timelines. pax-cli handles asset packaging, web serving, hot reload, and Apple deployment. On macOS, ordinary cargo run runs the native application directly, without a preceding CLI build or a development server. init! initializes within that process; it never selects or launches another development target.

Reusable component libraries supply the source configuration selected by their own compilation, including dependency features and custom cfg values. A library can live in a different Cargo workspace: macro expansion does not resolve or fetch that workspace's unrelated dependencies. This works with ordinary Cargo path dependencies and does not require an application build.rs or CLI setup.

You may encounter the word cartridge in build output or engine code. It names the application-specific part that connects your program to the shared runtime. runnable! generates this connection during compilation; you do not maintain a cartridge file by hand. On Apple platforms it remains a separately linked library, which a Rust launcher or an existing host can mount. See entry and mounting. Build for each destination target—there is no single executable cartridge file that you can interchange between a browser and an iOS app.

At a high level:

 .pax templates + Rust application + asset references
                         │
                   compiler / build
                         │
          target application + packaged assets
                         │
             runtime mounts the main component
                         │
             reactive scene + platform chassis
                         │
             drawn surfaces + native elements

Local assets are included in the target's output as appropriate; external URLs may still be loaded at runtime. An image or font referenced by the program is not necessarily embedded inside its Wasm or native executable.

Architecture schematic

Explore how the compiler, scene graph, reactive runtime, rendering engine and platform chassis connect. Select a part for its role, connections and related docs; select a connection in the inspector to follow it to another part. Use + / − to zoom and Fit to restore the overview. Open standalone gives the drawing more room, and Exit sheet view restores its controls.

For an offline overview, download the full-sheet PNG.

From templates to running instances

A template is a definition. The runtime creates instances from it: component state, element properties, and nodes with resolved layout and parent-child relationships. This running structure is called the expanded tree in inspection and runtime code.

One repeated element in a template can produce many nodes in that tree. A conditional can mount or remove a branch as its condition changes. Slots connect supplied content to the place where a component presents it. These are useful distinctions when the visible scene contains more—or fewer—nodes than you expect from counting tags in a file.

The tree and property graph answer different questions. The tree says where an instance belongs; the graph records which values depend on which inputs. A child's width can depend on its parent's bounds, while its text depends on application state shared from elsewhere. Those dependencies do not have to follow the same shape as the tree.

State also has a lifetime. Removing and recreating a component can recreate its local state. Keep state that must outlive a particular view in an appropriate owner, and persist it explicitly when it must survive an application restart. A Property is a reactive value, not persistent storage. See Components and Composition for ownership and shared state.

From a property change to the screen

Consider a handler that increments a counter used in a Text element. The handler sets the source property. Pax marks dependent values dirty: their cached values may no longer be current. When a dependent computed value is needed, the graph evaluates it using the current inputs.

The runtime also drains registered reactive effects that need to push an update into the scene. Multiple writes can settle into one downstream evaluation when that value has not been read or its effect drained between the writes. This does not make a sequence of arbitrary property writes an atomic transaction.

The consequences depend on what changed. New text can require a native text update and measurement. A different width can change descendant layout. A new fill can mark drawn content for rendering. Changing an if condition can alter the mounted tree and its event registrations.

Pax tracks dirty nodes and layers so unchanged drawing can be reused or skipped. The GPU path retains rendering data, and the runtime can avoid a canvas rendering pass when there is no canvas work pending. Native elements receive their own updates through the chassis.

The GPU renderer retains independent transforms and opacity values for scenes with more than 480 transformed drawing operations. It stores transforms and geometric clips in small uniform-buffer pages that grow with scene demand, including on the browser's conservative GPU limits. The page size is an internal batching detail, not a limit on the number of elements you can author. Moving or fading retained vectors can reuse their geometry; a draw spanning multiple transform pages is split while preserving paint order. Memory and rendering cost still grow with the scene, and ordinary visibility culling does not unmount nodes or release their retained identities. Removing nodes makes their slots available for reuse. This behavior is shared by debug and release GPU builds; it does not change the Piet renderer.

The amount of work is therefore related to the dependencies and surfaces affected by a change. A single property write can still have a broad effect: resizing a container, removing an overlapping element, or changing a clip can require work beyond the element you edited. Dirty tracking does not guarantee that every update redraws only the smallest possible pixel region.

Properties covers computed values, subscriptions, and avoiding unnecessary writes. Layout explains the bounds and transforms that these updates can affect.

Work that continues between interactions

The runtime advances clocks and invokes registered lifecycle handlers such as @tick and @pre_render. A running transition or timeline produces new values as time advances. These are legitimate sources of work even when the person using the application is not touching anything.

The current web chassis continues to schedule animation-frame callbacks while running. Dirty tracking reduces work inside those callbacks; it does not establish zero CPU usage at rest or suspend the frame loop altogether. An otherwise static page can still have clock, lifecycle, or platform work.

Keep expensive data processing and repeated no-op state writes out of per-frame handlers when they are not needed. Use a reactive expression for a value derived from other properties, and use the motion APIs for the transitions they describe. Neither choice makes computation free, but it gives the runtime explicit dependencies to work with. See Animation and Motion.

One scene, multiple surfaces

Drawn geometry and native elements share Pax layout, transforms, and ordering. Their presentation is divided across graphics surfaces and platform-owned elements. The compositor coordinates those pieces, including the places where clipping, scrolling, and overlap cross surface boundaries.

For example, a Rectangle can be drawn into a graphics layer while a Textbox is a browser control or Apple-native view. The runtime sends the native element its required updates; user input returns through the chassis to the runtime and application handlers. The platform still handles native concerns such as editing interactions and input-method behavior.

On macOS, native scene updates are applied during the chassis frame cycle, before that frame's GPU submission. Animated native controls therefore receive the current geometry without waiting for a later SwiftUI view update.

This division preserves useful native behavior, while making backend and element type important when choosing an effect. An effect supported on drawn geometry does not automatically support a native control or every form of text and image rendering. The detailed boundaries belong in Compositing and Effects and Accessibility and Native Controls.

Rendering backends

On the web, Pax currently selects its WebGPU renderer where browser support and policy allow. It uses a Piet/CPU browser renderer when WebGPU is unavailable, when the current iOS Safari detection policy selects that path, or when built explicitly with the Piet backend. Piet draws through browser canvases; native browser elements remain native on either path.

Backend selection is not a guarantee of recovery from every graphics-device initialization failure. When diagnosing a browser-specific problem, check the selected renderer and startup errors rather than assuming that another path was selected successfully.

The Apple-native chassis use Pax's GPU rendering integration alongside native views. Running a web build in Safari on an iPhone is a different target path from running a native iOS build on the same phone. Test the destination your users will actually use.

Graphics effects, surface limits, and rendering costs can differ between backends. There is no universal frame-rate guarantee for a Pax application. Scene complexity, assets, device capabilities, and the chosen renderer all contribute to the result.

Viewports and large collections

Clipping and culling can reduce the drawing submitted for content outside a viewport. Scroller surfaces can also be tiled to limit the backing surfaces needed for a large scrollable area. These are rendering optimizations.

The repeated component instances, properties, and application data still have their own cost. Putting a large for loop inside a Scroller does not automatically turn it into a virtualized list. Check node count and state work as well as what is visible. Scrolling and Viewports covers content measurement and practical collection limits.

Debug and release

Debug builds support the development loop, including richer program metadata and the configured hot-reload lanes. Release builds optimize the compiled application and bake its declarative program into the artifact. The build paths differ internally, so a successful debug run is only one part of checking an application intended for release.

Release applications disable the live .pax and application-logic reload lanes. Hot reload is a development capability; keeping it available does not require shipping it in the release application. See Developer Workflow for the current reload choices.

From your project directory, build the web release with:

pax-cli build --target web --release

Test the resulting application in addition to the debug session. Use the release output when measuring shipping performance or download size. The CLI reports bundle statistics, but those figures do not describe every asset or network request your application may load. Include images, fonts, scripts, and other resources when assessing the complete experience.

For investigating Wasm size, the CLI also offers build --profiling on web, which produces an optimized build with Wasm names retained for size analysis. This is a size-investigation mode, not a runtime frame profiler. Measure the ordinary release build for the shipping comparison.

Inspect an update in a working example

The repository's examples/increment app has a useful pair of behaviors: clicking its rounded rectangle changes the count and starts a rotation, while a @pre_render handler continuously changes the rectangle's color.

From a Pax repository checkout, start it with:

pax-cli run --path examples/increment --target web

With that session running, use another terminal in the same checkout:

pax-cli dev inspect tree --path examples/increment --max-depth 3
pax-cli dev logs --path examples/increment --limit 50

The tree shows the running instances behind the template. The log command reads messages captured by the development session; a quiet session can return no entries. To include renderer selection, add ?pax_log=info to the local application's URL and reload it, then run the log command again. The default debug logging level includes warnings and errors. You can also read these diagnostics in the browser console. These commands help establish what is running; they do not measure the time spent inside each part of a frame.

Open src/lib.pax and src/lib.rs inside the example and follow a click:

  1. The Group binds @click to increment.
  2. The handler updates num_clicks, which feeds the Text's expression.
  3. It starts an eased transition on current_rotation, which feeds the Group's rotate property.
  4. The rotation settles, but handle_pre_render keeps updating ticks. The Rectangle's fill depends on that value, so its color keeps changing.

This app intentionally remains animated. To understand activity after an interaction in your own application, follow the same trail: event handler, property dependencies, active motion, and per-frame handlers.

Measure the question you have

Separate startup, interaction, continuous motion, and resting-state work. They can have different causes and require different checks:

  • For a slow initial load, inspect resource requests and decoding, executable size, and scene initialization. Compare a cold load with a warm one.
  • For an expensive interaction, reproduce one action and inspect the state, layout, and mounted-node changes it causes. Use the browser's performance tools or the target's native profiler to measure where time is spent.
  • For motion or scrolling, test realistic content, nested surfaces, and representative devices. Note the target, renderer, viewport, and build mode alongside the result.
  • For unexpected activity after the interface settles, look for active transitions, clock-dependent expressions, and per-frame handlers. Distinguish ordinary frame scheduling from repeated application or rendering work.

Screenshots and event driving are useful for repeatable behavior checks. They do not establish frame rate, memory use, or accessibility. Keep those measurements separate, and compare the same workload before and after a change.

Native render phase timings

For native debug or release builds, launch the app process with PAX_RENDER_TIMINGS=1 to emit aggregated [PaxRender] diagnostics approximately once per second. This flag is off by default and has no effect in WebAssembly. For an installed simulator app, for example:

SIMCTL_CHILD_PAX_RENDER_TIMINGS=1 xcrun simctl launch --console-pty \
  <simulator-udid> <bundle-id>

Each phase reports count/total_ms/avg_ms/max_ms. render measures the runtime render call, lighting collection, lighting_upload delivery to the backend, and prepare retained-node preparation. On the GPU backend, flush, encode, submit, cleanup, present, and layer_init distinguish drawing and renderer initialization costs. A render call can collect several layers and flush several surfaces, so those phase counts and averages are not per-frame counts. Nested timings overlap: layer initialization can occur inside render or flush, and encode/submit/present occur inside flush. Do not sum all reported phases.

[PaxRenderTiles] reports counters for the same window: renderer creations, changes to the keyed tile set, matching key/host tiles discarded during those changes, origin retargets, resizes, retained-scene resets, and reused tiles. A matching discarded tile identifies potential reuse; it does not prove that its contents or geometry were unchanged. Incremental additions should not discard matching survivors; layer_init can now represent just a batch of missing tiles. Reuse counts include survivors moved during set reconciliation, not every unchanged tile on every frame. image_texture_uploaded and image_texture_upload_bytes count new image pixel allocations/uploads observed at tile flush, while image_texture_reused counts new tile bindings that reuse a sibling's resident texture. Repainting a tile with its existing binding is not a shared-cache hit. These are work/transfer counters, not current GPU memory usage; image bindings are tile-local even when pixels are shared. Counts include all logical layers, and startup should be excluded when investigating scroll-induced churn. These counters are also disabled unless PAX_RENDER_TIMINGS is enabled.

These are CPU wall-clock spans, including any waits inside the measured calls, not GPU execution timestamps or displayed-frame counts. They exclude the chassis's tick, native-view update, and surface synchronization costs. Simulator GPU and presentation timings do not predict performance on physical hardware. Keep startup separate from interaction captures, and relaunch without the flag after measuring to disable the instrumentation.

Read more

Shared publication and application lifetime

Application Property<T> storage is shared; graph computations, LocalProperty, ExpandedNode, layout, and rendering stay on the owning UI thread. A worker publication coalesces by shared allocation and requests a host wake. The host validates the current application before entering the engine. The UI imports a bounded batch of published snapshots before settling local effects; evaluators never execute on a publishing worker. A raw shared read returns the latest published snapshot even before a local view has imported it.

Mounted nodes own their local attachment lifetimes. Unmount detaches those connections; retained raw shared values remain usable as data. The generated cartridge invokes one typed root initializer inside its property graph. The host adopts that exact root allocation, attaches its wake route and viewport, then activates staged work. Apple launchers use a real Rust main with a SwiftUI library bridge; web calls init_web explicitly. See State and Properties and Application setup for the implemented APIs and the current limits on scoped async work. See Entry and mounting for standalone and embedded hosts.

Developer Workflow and Tools

Once your project is running, most interface work follows a short loop: change the source, see the result, try an interaction, and inspect anything you cannot explain from the screen alone. Pax's development tools let you move between the running interface and the source that produced it.

Start with a project from Getting Started. Commands in this chapter run from that project's directory unless stated otherwise. The web examples need an open browser tab connected to the running application.

Hot reloading

Debug pax-cli run sessions use Pax-only hot reload by default. Direct native cargo run does not start a development server. Saving a .pax template in a CLI session can update the mounted interface. Rust application changes require rebuilding and restarting on native targets, or rebuilding and reloading the browser page. The initializer runs once per application instance, including after a restart.

ModeSaved .pax changesSaved Rust changes
pax — defaultReloadRebuild and restart manually
offWait for restartRebuild and restart manually
all — legacyReload, with a migration noticeRebuild and restart manually
logic — retiredRejected with a migration diagnosticRebuild and restart manually
pax-cli run --target web --hot-reload=pax
pax-cli run --target web --hot-reload=off

Target support and restart boundaries

macOS, iOS, iPadOS and web use the same policy. all is accepted as a migration alias for pax; it no longer replaces Rust code in the running process. No automatic restart is implied. Inline templates in .rs files also require a rebuild. A new component type or event handler may need compiled information that the running application lacks; follow the reload diagnostic and restart.

Template reload preserves the initialized root and its dependencies. Affected nodes can still unmount and remount, revoking their scopes and recreating local stores. Focus, scroll position and arbitrary node-local state need not survive. See provider lifetime rules. Disabling reload leaves source edits intact for the next build and preserves dev inspection. Release builds always disable reload and dev replacement.

Configure a project default

[package.metadata.pax.dev]
hot_reload = "pax"

Selection precedence is the --hot-reload flag, then PAX_HOT_RELOAD, then Cargo metadata, then the pax debug default. The same migration rules apply to every source of the setting. The CLI owns development orchestration. Both init! and init_with! initialize in the current native process; enabling designtime does not change this or start a server.

Format Pax source

pax-cli fmt formats .pax files and Pax templates embedded in Rust source. It accepts a file or directory and recursively visits directories, skipping generated and dependency folders such as .pax, target, and node_modules.

pax-cli fmt src
pax-cli fmt --check src

The first command writes formatting changes. The second checks without writing and exits unsuccessfully if files would change, which makes it suitable for a CI check. With no path, the command starts in the current directory. pax-cli format is the full spelling; fmt is its alias.

Use cargo fmt for ordinary Rust formatting. Review formatting diffs as you would other source changes; neither formatter verifies the interface's behavior.

Choose the intended session

List the live sessions on your machine, then inspect the selected one:

pax-cli dev list
pax-cli dev status

Status reports the session ID, target platform, project location, and connection details. Check these before using tools, particularly when several projects, worktrees, or browser tabs are open.

Most dev commands accept --path to locate a project's active session and --session to select an exact session ID. An explicit --path (or -p) restricts selection to that project, including when the path is .. For example, from a repository checkout:

pax-cli dev status --path examples/increment
pax-cli dev look --path examples/increment

Project selection uses the active session recorded under that project's .pax directory and checks that its live registration belongs to the same project. Relative paths are resolved from the current directory; symlink aliases and .. are resolved before comparing project roots. A missing or stale active session, invalid path, or mismatched registration produces an error. An explicit path never falls back to another project's session. Start the intended project's debug app, or use pax-cli dev list to find a live ID.

--session <id> alone selects that exact live session regardless of the current directory. When both --session and --path are supplied, the ID selects the session and the path checks its project. The session need not be the project's active session, but a different or unknown project root is rejected before inspection, capture, or source mutation.

With neither flag, the CLI first tries the current directory's active session. If none is live, it can select the sole live session anywhere on the machine; several candidates require an explicit ID. Use --path . to constrain commands to the current project, especially in automation.

This changes older CLI behavior: scripts that relied on --path falling back globally must select the intended --session explicitly. Scripts supplying both flags must now make them agree, or omit --path to select solely by ID.

Session IDs describe running sessions and can change after a restart. Re-read them instead of hardcoding one into a permanent script. Stale registrations are filtered from the live list.

Which targets expose the tools?

The session-backed inspection, selector, ray-cast, and screenshot workflows below are implemented for web and macOS debug apps. The dev logs command currently supports web sessions only.

iOS and iPadOS template hot reload is a separate capability; it does not imply that these local CLI session tools are available on a simulator or device. Use the native app/simulator and platform debugging tools for those checks. Release applications do not expose this development workflow.

Inspect the running tree

Start with a shallow view:

pax-cli dev inspect tree --max-depth 3

The result is JSON describing the expanded application tree. It includes instance types, parent/child relationships, layout bounds and transforms, layer and hit-testing flags, and available source/template identifiers. --max-depth 0 shows only the root; omit the option when you need the full tree.

A template node can have multiple running instances, particularly inside a loop. engine_id identifies a running instance; the containing component, template_node_id, and source path connect it back to the template. Those identifiers serve different purposes, and runtime IDs can change as the interface is rebuilt. How Pax Runs explains the expanded-tree model.

Find an element by selector

To narrow the output to Text nodes:

pax-cli dev selector 'Text'

Selectors can use a type name, an ID such as #text, or a class such as .card. Quote them so the shell passes the selector unchanged. An ID lookup can still return multiple instances when its template is repeated. An empty result may mean a conditional branch is not mounted or that you selected a different session.

On the repository's Increment example, pax-cli dev selector '#text' finds the count label. Its response includes both the resolved native element and the path to src/lib.pax.

Find what occupies a screen point

Capture the app at the default scale, choose a point in that image, and query the hit stack. For a point at (640, 360):

pax-cli dev ray-cast --x 640 --y 360

Use coordinates from the app capture, not from the whole desktop or browser window including its toolbar. The command uses default-scale dev look coordinates; avoid taking them from a resized image. It returns the nodes under the point in z order without clicking them. --hit-invisible also includes nodes normally omitted from hit testing.

This is useful when a visible element does not receive input. Check the hit stack, the element's bounds, and anything layered above it. Earlier Pax siblings appear in front; Layout covers that order and the coordinate system.

Capture a frame or a sequence

Capture the current application:

pax-cli dev look

The response lists the image path, dimensions, and capture timestamp. By default, files are written into a request-specific capture directory inside the selected development session. You can choose an output directory:

pax-cli dev look --output-dir captures/before

Capture files use sequential names such as 0000.png. Use a different output directory for each observation you want to keep: reusing the same directory can overwrite earlier captures.

For a five-second observation with a nominal half-second interval:

pax-cli dev look --period-ms 500 --duration-ms 5000 --output-dir captures/motion

Start the observation, then perform the interaction in the app. Both timing options are required for a sequence. Capture work and application scheduling affect when samples arrive, so use the returned timestamps and do not assume a fixed image count. This is useful for inspecting states through an animation; it is not a video recording or a frame-rate measurement.

--scale 0.5 requests smaller images. PNG is the default; --format jpeg selects JPEG and accepts an optional --quality value from 0.0 to 1.0. Scaling the capture does not resize the application's viewport.

Web capture combines rendered layers and native content inside the Pax mount. An unpainted background can remain transparent. Background tabs use fallback paths for some native text and controls, so compare the image with the live foreground app when inspecting fine rendering or platform-widget details. Screenshots do not replace keyboard, assistive-technology, or real-device checks.

Read logs and diagnose a reload

For a running web session:

pax-cli dev logs --limit 50
pax-cli dev logs --follow

--follow continues until interrupted. The log reader reports browser messages captured by the development session; compiler/build errors remain in the terminal that runs the app. A quiet session may return no entries.

Rust logging on the web defaults to warnings in debug and errors in release. For more detail during development, add ?pax_log=info to the app's local URL and reload it. If the URL already has a query, add &pax_log=info instead. The supported levels are error, warn, info, debug, and trace.

At info, renderer diagnostics identify WebGPU or Piet and related graphics configuration. The same messages are available in the browser console. Verbose output can be noisy, and captured logs are bounded recent history; collect the relevant messages while reproducing the problem.

Drive events and check the result

Exercise the interface through its userland input path: click or tap a control, type into a native field, scroll its viewport, or use the keyboard. For repeatable browser tests, a browser automation tool can perform those same actions while Pax's inspector and captures help verify the result. Canvas geometry often needs a coordinate-based action; native controls may expose semantic browser targets.

Current CLI boundary: pax-cli dev does not yet provide a command to inject a click, tap, or key event. ray-cast only observes hit targets. dev touch changes source, as described below; it is not a touch-input driver. The runtime's event handling remains part of Pax's open-source implementation, but a dedicated CLI gesture interface is still a tooling gap.

Use Event Handling to understand event binding, propagation, and handler behavior. NodeContext::dispatch_event delivers named application events; it does not emulate a platform input gesture.

Source edits through developer tools

For normal authoring, edit files in your editor and let the chosen reload policy apply. Two dev touch operations also expose source changes to tools:

  • apply-component-source replaces a component's entire .pax source file.
  • replace-node replaces a template node with a Pax subtemplate and reports the affected source and reload scope.

Both are mutations of the project, not temporary changes to pixels. Review the working-tree diff and target the intended session before using them. Replacing a repeated template node can affect all of its instances. An empty replacement subtemplate deletes the targeted node.

For example, to replace a component named Example, first prepare a complete replacement file and review it. Then use apply-component-source with --component Example and --source-file pointing to that file. Supply the session ID verified by dev status. The command parses the source before writing, and refuses ambiguous component-name matches; valid syntax alone does not guarantee that the running build contains every referenced type.

Use pax-cli dev touch --help and the selected subcommand's --help for exact arguments. A successful source write does not override a disabled reload lane. After editing, confirm the activation and inspect the actual result.

Read docs and example source locally

The installed CLI contains a documentation and example-source snapshot:

pax-cli docs
pax-cli docs search 'hot reload | Scroller' --limit 5
pax-cli docs open template-language
pax-cli docs examples --list
pax-cli docs examples increment

docs lists the available articles and references. Search uses | for alternatives; quote the query so your shell does not treat it as a pipeline. open accepts a slug, path, or title and displays the article in a terminal pager. Example lookup prints the included source files.

These reads use the CLI's bundled snapshot, not a live fetch of the latest website. Check pax-cli --version when comparing instructions across releases. Viewing source through docs examples does not create a project or install its assets. To create a runnable project from the curated bundled set, use create --example below. In a repository checkout, nine curated examples live at examples/<name>; focused studies, platform probes, and historical experiments live at examples/fixtures/<name>. Start with the curated example index or browse its fixture index. Run one from the repository root with:

cargo run --example increment       # direct native execution on macOS
cargo run -p increment              # build just this example's dependency graph
pax-cli run --path examples/increment --target web
pax-cli run --path examples/fixtures/auto-sized-containers --target web
pax-cli docs examples auto-sized-containers

Each example directory is an ordinary Cargo package with its own library, native main, templates, and assets. The adjacent .rs launchers are real Cargo example targets, including those under fixtures. Cargo shares the root package's dev-dependencies across its example targets, so -p <name> is the narrower development command. Moving a package into fixtures changes its filesystem path and CLI --path, but preserves cargo run -p <name> and cargo run --example <name>. docs examples --list separates curated and supporting examples; source lookup accepts the short name or the canonical examples/fixtures/<name> path. Hosted example URLs retain their short names. Starter eligibility remains controlled by the separate bundled-example registry below. Development profiles live at the workspace root: runtime dependencies use optimization level 1, while each example's own code and build dependencies use level 0. The CLI restores these profiles when exporting a standalone project.

To measure target compilation and web packaging, use an already-built CLI and a fresh target directory without deleting your development cache:

python3 scripts/benchmark-cargo-web.py \
  --pax-cli /absolute/path/to/pax-cli \
  --target-dir /tmp/pax-build-benchmark-target \
  --logs /tmp/pax-build-benchmark-logs

The script builds pax-logo once with an empty Rust target directory and twice unchanged, recording wall times and logs. It excludes building/installing the CLI itself. Registry dependencies and platform tools must already be cached; resolution is offline. Existing .pax output and external tool caches are retained, so it excludes downloads and initial workstation setup. It rejects a nonempty target directory. Select another example with --package <name> and use fresh directories for each trial. The repeated warm run distinguishes steady cache reuse from files generated during the cold build.

These measurements are not directly comparable to the earlier embedded-launcher benchmark, which also compiled workstation tooling inside the application. Compare compiler and packaging timings separately; a package count alone does not establish build latency.

The CLI also has docs build for contributors rebuilding the documentation assets in a Pax repository. It can regenerate API pages, example metadata/ bundles, and the CLI's search content. It is not required to browse installed docs, and should not be run casually over an in-progress documentation edit.

The HTML docs use optimized release builds for their interactive examples. The book's <pax-example> declarations select the apps and displayed source files; the generator writes each build's manifest.json under _pax_examples/<name>/. Short names such as space-game resolve to either examples/<name> or examples/fixtures/<name>, so moving a fixture preserves its embed URL. Generated run commands use the actual source location. Cached apps rebuild when their source, engine/compiler inputs, Cargo manifests, or lockfile changes. An example runs automatically while its stage is visible and suspends frame updates and drawing when scrolled out of view or when the docs tab is hidden. Returning to it preserves its state; Restart starts over, and Open standalone runs the example independently. Suspension does not stop application network requests or other external work. Elapsed-time-based animations can advance to the current time when resumed.

Create from a bundled example

The CLI includes complete project templates for a curated subset of examples:

ExampleStarting point
pax-logo (default)Animated standard-library logo with a position slider, replay, and a background toggle
living-quiltInteractive geometric tapestry with responsive components, motion, lighting, and GPU alpha masks
ink-and-lightA responsive postcard with handwriting, local assets, and Rust interaction
incrementA minimal counter for exploring the reactive update loop
pax-cli create my-app
pax-cli create my-quilt --example=living-quilt
pax-cli create my-postcard --example=ink-and-light
pax-cli create my-counter --example=increment

Choose one, enter its directory, and run pax-cli run --target web. These projects include the source and assets needed to build; they require no Pax repository checkout. An unknown example name reports the available choices without creating a partial project. The curated create set is smaller than the source-reading catalog in docs examples.

Each CLI release carries its own snapshot. For contributors, examples/<name> and examples/fixtures/<name> are canonical; examples/bundled-cli-examples.toml selects the create set and scripts/sync-cli-examples.py synchronizes the packaged archive. Release tooling runs that synchronization after version updates and before packaging.

Read more

Targets, Build, and Deployment

A running development project is the starting point. To put it in someone else's hands, choose a target, build its artifacts, and check those artifacts in the environment where people will use them.

Pax targets web, macOS, iOS, and iPadOS. Web releases can be served as static files. macOS has a release build path; distribution adds Apple's signing and packaging steps. iOS and iPadOS support debug and release builds, including local simulator and connected-device runs. Apple distribution requires additional signing and delivery steps.

This chapter assumes you have completed Getting Started. Commands run from your project's directory unless stated otherwise. To select a project from another directory, use pax-cli build --path ./my-app --target web --release. Its release feature check and compilation both load that project's .cargo/config.toml, including local dependency patches and registry configuration.

Targets and workstations

The target is where the application runs. The workstation is where you compile it.

Application targetCLI valueDevelopment workstationRequired target tools
Web browserwebmacOS, Debian/Ubuntu Linux, or WindowsRust, OS build tools, wasm32-unknown-unknown, and wasm-pack
macOS applicationmacosmacOSRust, full Xcode, and the selected Apple Rust targets
iPhone applicationiosmacOSRust, full Xcode, iOS SDK/simulator, and the selected Apple Rust target
iPad applicationipadosmacOSThe same Apple toolchain, with an iPad simulator or device

ipad is also accepted as an alias for ipados. Linux and Windows support the web development workflow; they are not native application targets in the current CLI. See workstation setup for installation commands and Apple preparation for additional native requirements.

The published CLI includes its web interface bundle. Node.js/npm are needed when rebuilding that interface from the Pax repository, rather than for the ordinary installed-CLI workflow.

Target support does not promise identical rendering or controls everywhere. How Pax Runs explains backend selection; feature chapters state their narrower target and backend limits. Include the browsers, devices, native controls, and rendering effects your application actually uses in its test plan.

Entry and mounting

pax_kit::host contains the options, errors, and embedding types. The common native entry is pax_kit::init!()?.run(): the macro selects this package's UI library and initializes its root in the current native process. init!(my_ui) selects a separate UI dependency; init_with! adds startup options. run enters the platform application loop. Neither macro parses application arguments or starts development tools, regardless of designtime. Only copied build metadata crosses from that library into the launcher; the engine and application objects stay in the separately loaded cartridge. Depend on pax-kit and enable its host feature in the launcher. It creates no async runtime. UI state and executor setup belong in the initializer selected by runnable!. Browser applications use the library's init_web entrypoint instead. Shared Cargo features do not mount the UI or link the native host into a cartridge.

Standalone Apple applications have two Rust targets: a library cartridge containing the root component and its initializer, and an Apple executable with an ordinary Rust fn main. The executable uses pax_kit::host to enter the SwiftUI host framework; the cartridge remains separately linked. Use the launch macro to select the application library. Do not pass a Root, Property, Tokio runtime, or Rust closure across its C boundary. Construct cartridge-owned resources in the typed root initializer.

The generated example manifests make the launcher explicit:

[package]
default-run = "my-app"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
pax-kit = { version = "0.39.0" }

[features]
default = ["standalone", "designtime"]
standalone = ["pax-kit/host"]
designtime = ["pax-kit/designtime"]

[[bin]]
name = "my-app"
path = "src/main.rs"
required-features = ["standalone"]

The compilation target automatically selects the macOS, iOS, or web chassis. standalone enables the native executable and host API. Turn it off with --no-default-features --lib when building only the UI library. Dependencies can use default-features = false; feature unification is safe because loading and runtime ownership remain separate.

On macOS, cargo run, cargo run -p my-app, and the resulting executable run the native application directly. Cargo builds the native host SDK before execution. designtime enables runtime inspection and template changes; the CLI starts the server needed for those sessions. The application does not depend on Pax's packager. Macro/compiler dependencies run during compilation, not inside the native or browser application. Web packaging compiles the Wasm and its manifest together, using the target's actual cfgs, features, and profile, then reads that manifest to package routes and assets. Apple packaging obtains its manifest during a target cargo build and reuses the compiled library in its platform build. Neither needs a separate cargo check compilation to extract the manifest. If several standalone binaries exist, set default-run. A host package may select a separate workspace UI library through [package.metadata.pax.cartridge]; see workspace cartridge selection. The selected UI supplies component templates and assets; the host package owns platform metadata, public/, and generated .pax/ output.

fn main() -> Result<(), pax_kit::host::Error> {
    pax_kit::init!()?.run()
}

This is the complete bootstrap for the common native case. The same process owns code before and after initialization, both in direct Cargo runs and in packaged applications. Application arguments remain yours to parse. Use init_with! for startup data or an explicit root selection, and mount for embedding in a caller-owned view.

Put portable UI setup in the runnable! initializer, which runs in the target application. Native main is not transported into a browser. Pax creates no async runtime; each application retains its own executor policy.

The actual application main runs on all three Apple targets, including iOS and iPadOS. run hands control to SwiftUI and normally never returns on mobile. Web builds compile the UI library and JavaScript explicitly invokes its init_web; importing the module does not mount it. Android and native Linux/Windows remain unsupported. Those desktop workstations can run the web development tools.

On macOS, init_with!(InitOptions::default())?.run_with(WindowOptions { ... }) accepts window geometry and title. The function forms pax_kit::init() and init_with(options) also remain explicit native APIs for hosts that already package their cartridge. The macro forms identify the Cargo UI automatically.

When a library declares several runnable roots, select one explicitly:

let app = pax_kit::init_with!(pax_kit::host::InitOptions {
    entry: Some("crate::ui::App".into()),
    ..Default::default()
})?;
app.run()?;

Browser hosts select the same canonical name with Pax.init_web({ element, cartridgeUrl, entry: "crate::ui::App" }). An omitted name is accepted only when the library declares one distinct root. An unknown or ambiguous name fails before running an initializer.

A runnable library can also supply components to another runnable library. Reusing its components does not mount its application or call its initializer. For web builds, Pax packaging exposes the selected cartridge as init_web; dependency entrypoints remain distinct even when Cargo features are shared.

Mount in an existing application

The host can supply one existing region: an NSView on macOS, a UIView plus its owning UIViewController on iOS/iPadOS, or an HTMLElement in the browser. Pax installs its normal child hierarchy, including native controls. It owns that child and session; the caller retains its window, parent and event loop.

On Apple, call pax_kit::init_with!(my_ui, pax_kit::host::InitOptions::default()) and consume the prepared value with app.mount(view_host). ViewHost::from_raw is unsafe: its documented contract requires valid platform objects, a retained owner borrowed for the mount's lifetime, and the process main thread. An arbitrary GPU/window handle is not a complete adapter. Swift/Objective-C hosts use the versioned C declarations in pax-host/include/pax_host.h; see the repository's tests/src/host-embedding fixture for containment and callback ownership.

An embedded Apple mount preserves the host's keyboard focus. Clicking or tapping Pax's rendered content focuses its canvas; native text controls keep their own input. See Keyboard delivery. Rebuild the Apple host and cartridge together when updating Pax: cartridge ABI 3 supports selecting a declared runnable root, and preserves keyboard default-prevention status from pax_interrupt. The host rejects older cartridges. The host ABI is version 2, including copied launch configuration.

mount.ready() observes installation of the viewport and wake route. mount.close() revokes work and removes only Pax's child. mount.closed() observes cleanup acknowledgement. These futures must be driven by the caller's UI executor; do not block the main thread waiting for them. Dropping the mount starts close, and cleanup continues even if the observer is dropped. Keep the parent/controller alive until the mount is dropped. Do not reparent a live mount.

A browser host loads the matching generated interface bundle and calls:

<script src="pax-interface-web.js"></script>
<div id="ui" style="width:100%;height:600px"></div>
<script type="module">
  const cancellation = new AbortController();
  const app = await Pax.init_web({
    element: document.getElementById("ui"),
    cartridgeUrl: new URL("pax-cartridge", document.baseURI).href,
    signal: cancellation.signal,
  });
  // Promise resolution means mounted, not that application data has loaded.
  // On disposal: app.close(); await app.closed;
</script>

The container must belong to the current document and have an explicit usable size. Pax follows its bounds, clips inside the region and routes input relative to it. CSS transforms on the container or its ancestors are not supported. pageScroll and documentRouting default to false for embedding; the standalone page enables them to own page scrolling, history and metadata. With document routing disabled, a bubbling pax-navigate event carries {url, target} for the host to handle. Aborting signal cancels startup or closes an existing mount. Startup failures reject the Promise and initiate retained cleanup. Failed native preparation similarly returns an error; it can leave cleanup pending even though no mount handle was returned. A retry is rejected while that cleanup is pending.

Only one live mount is supported per host runtime. A second prepare/mount is an error, including while cleanup is pending. Await closed before replacing it. Hidden/zero-size regions retain application state and cleanup scheduling. Collapsing a native region does not prevent readiness or queued UI delivery; its host skips drawable acquisition until the region has nonzero bounds. Closing is distinct from suspension. Native controls retain their existing chassis capabilities and restrictions; see Accessibility and Native Controls. No raw-surface-only or shared-wgpu-device adapter is provided.

Data across the cartridge boundary

Use pax_kit::init_with!(pax_kit::host::InitOptions { startup_data, ..Default::default() }) on Apple, or startupData: Uint8Array in web options. The cartridge reads InitContext::startup_data() and validates its own encoding. Inputs are copied before initialization. Empty data is the default; buffers are limited to 64 KiB.

An initializer can install one explicit request handler with cx.on_host_request(|bytes| -> AppResult<Vec<u8>> { ... }). A mounted host calls mount.request(&bytes) on Rust or await app.request(bytes) on web. Requests enter the bounded application delivery queue and run on the UI owner after activation. Replies are copied buffers, also limited to 64 KiB. Rejection, handler errors and cancellation are observable; closing cancels queued requests. Dropping a Rust reply future discards observation but does not undo an accepted request. Native C reply buffers are borrowed only for the callback and must be copied before it returns. Callbacks arrive asynchronously on the main thread.

This is a request/reply channel, not a resource registry or an unsolicited event bus. Keep handlers short; they may update properties or start scoped work and return an acknowledgement. The Async Workbench embedding fixtures demonstrate UTF-8 startup data and increment/read requests. A Tokio Handle is a Rust value and may only be borrowed within the same linkage domain, never encoded as a pointer and passed through this interface.

Run, build, and release

From an application package on macOS:

cargo run                  # native application; no development server
cargo build                # compile only; no packaging or server

In the repository, use cargo run -p increment or cargo run --example increment. The package form compiles fewer unrelated example dependencies. Application arguments follow Cargo's --; Pax initialization does not intercept them. Native Linux and Windows hosting is not yet supported. Use the CLI's web target on those workstations.

For a development session with template reload, serving, or simulator deployment:

pax-cli run --target web --port 8080
pax-cli run --target macos
pax-cli run --target ios        # iPhone simulator, on macOS
pax-cli run --target ipados     # iPad simulator, on macOS
pax-cli run --help

An explicit occupied web port fails rather than silently selecting another. The CLI runs on the workstation; the native application's main runs in the launched application process. Browser applications use init_web instead.

To package without launching, including production web output:

pax-cli build --target web --release
pax-cli build --target macos --release

Production packaging requires release artifacts without designtime. Pax omits the starter's direct designtime default, preserves other application defaults, and rejects aliases or dependencies that re-enable designtime in the target runtime graph. Reusable UI dependencies should use default-features = false and forward development features optionally. Build-time compiler dependencies are permitted; runtime editing/replacement code is not part of the production application.

For a direct native Cargo build without development capabilities:

cargo run --no-default-features --features standalone
cargo build --release --no-default-features --features standalone

Add any other application features you require. Cargo's --release selects optimized compilation; it does not package an application or remove default features. Raw iOS Cargo builds can use --target aarch64-apple-ios-sim, but producing/installing an app bundle requires CLI packaging. No Cargo runner is configured by Pax.

For only the UI library, use cargo build --lib --no-default-features and add the Rust --target you need. A raw WebAssembly build similarly needs JavaScript bindings, its platform interface and assets before it can run in a browser.

The CLI manages local platform bundles, serving, template reload, and mobile deployment:

pax-cli run --target web
pax-cli build --target web --release
pax-cli build --target macos --release
pax-cli build --target ios --release

Choose the command for your target. CLI run defaults to a debug build and starts the target's development harness. CLI build also defaults to debug; --release selects the optimized release path. iOS and iPadOS additionally accept run --release to install and launch a local release. For web and macOS, use build --release. For distribution, keep the complete generated bundle. Apple signing, notarization, archive/export and store delivery remain additional steps; pax-cli build --release does not perform an App Store submission.

You can work from another directory with --path:

pax-cli build --path path/to/my-project --target web --release

Release builds exclude the development service and hot-reload machinery. Test the release itself: an application that works in a live development session still needs a release check. Read Debug and release for the runtime differences, and Developer Workflow for reload configuration.

Where the output goes

Cargo uses target/debug/ or target/release/, with a target-triple directory inserted when --target is specified. Workspace configuration and CARGO_TARGET_DIR can change that location. The CLI's packaged output is separate, beneath the application's .pax/ directory:

BuildOutput
Web debug.pax/build/debug/web/
Web release.pax/build/release/web/
Web --profiling.pax/build/profiling/web/
macOS debug.pax/build/debug/macos/app/Pax macOS (Development).app
macOS release.pax/build/release/macos/app/Pax macOS (Release).app
iOS debug.pax/build/debug/ios/app/Pax iOS (Development).app
iPadOS debug.pax/build/debug/ipados/app/Pax iOS (Development).app
iOS release.pax/build/release/ios/app/Pax iOS (Development).app
iPadOS release.pax/build/release/ipados/app/Pax iOS (Development).app

iOS and iPadOS share the generated iOS host, including that bundle filename. The mobile bundle filename retains “Development” even when built with the Release configuration. The display name inside the app can be configured independently.

--profiling is web-only: it produces optimized output with Wasm names retained for size analysis. It is separate from the normal release directory. See How Pax Runs for performance investigation.

Build output is replaceable. Keep authored changes in project source, assets, metadata, or an explicitly customized interface. If you use pax-cli clean, it removes the project's entire .pax/ directory, including builds and local development artifacts. Save anything you need before cleaning.

Build a web release

From the project root:

pax-cli build --target web --release

A successful build prints the output location. A typical directory contains:

.pax/build/release/web/
├── index.html
├── pax-interface-web.js
├── pax-interface-web.css
├── pax-cartridge.js
├── pax-cartridge_bg.wasm
├── snippets/
├── assets/
└── … favicon, public files, and supporting files

Keep the directory together. The HTML loads the interface and cartridge, and the cartridge depends on its matching Wasm and supporting files. Uploading only index.html or only the .wasm file is insufficient.

Preview the release locally

Serve the output over HTTP rather than opening index.html as a file: URL. For example, if Python 3 is installed:

python3 -m http.server 8080 --bind 127.0.0.1 --directory .pax/build/release/web

On Windows, py -3 can replace python3. Open http://127.0.0.1:8080/, interact with the app, and inspect the browser console and network requests. This server is for local review. It does not configure production HTTPS, caching, or application-route fallback.

Suspend an embedded web app

A custom web host can call window.Pax.setSuspended(true) to suspend frame updates and drawing without unmounting the app. Call it with false to resume. For a same-origin iframe, the host can access the API through iframe.contentWindow.Pax; wait for the iframe's load event first. Loading the iframe with ?pax_suspended=1 starts it suspended, including while its Wasm is loading. The standalone URL needs no such parameter.

Use an IntersectionObserver on the iframe's stage and the host document's visibilitychange event to resume only while the example is in view and the host tab is visible. The docs use this arrangement. Properties and form state remain mounted; this does not cancel application requests, freeze wall-clock time, or pause arbitrary JavaScript timers. Elapsed-time-based animations may catch up when frame updates resume.

Assets and web public files

Use assets/ for application media. Pax copies those assets into the target's build. Images and fonts still need suitable source paths and loading behavior; Text, Fonts, and Images owns those authoring details. Remote resources remain network dependencies unless explicitly bundled or vendored by the relevant target build path.

Web public files

Create public/ beside Cargo.toml for web files that should be served as ordinary HTTP responses. Relative paths and bytes are preserved:

public/ai.md             → /ai.md
public/robots.txt        → /robots.txt
public/.well-known/pax   → /.well-known/pax
public/guide/index.html  → /guide/

Those URLs assume deployment at the site's root. A build copies these files into its web output. During pax-cli run, the server reads the source public/ directory directly, so edits, additions, and deletions become visible after a browser refresh without restarting Pax.

Public files cannot overwrite generated or customized interface files. public/index.html conflicts with the application's entry page; the top-level assets, snippets, and __reloads__ directories are reserved. Symbolic links are rejected. Review everything placed here: these files are intended to be public, including dot-prefixed files such as .well-known entries. The hosting server determines response headers and content types; copying a public file does not configure those policies.

Use public/ for web-only documents and files such as robots.txt. It is not packaged as an Apple application's asset directory. For titles, descriptions, and social previews tied to application routes, use web route metadata. The compiler generates those entry documents from the route declarations. Avoid a public-file path that conflicts with a generated route entry.

For navigation from Pax to these same-origin documents, declare their paths in [package.metadata.pax.web].server_owned_prefixes. See Server-owned web paths for the complete navigation and local-server contract. Production hosting must also serve those paths directly and exclude them from any application fallback.

Deploy web

A web build needs a static host. It does not need a running Pax CLI on the server. For a first deployment, serve the app at the root of a domain or subdomain, such as https://app.example.com/.

  1. Build and test .pax/build/release/web/ locally.
  2. Publish that directory's contents as the site's document root, including snippets/, assets, and any dot-prefixed public files.
  3. Enable HTTPS and correct content types on the host.
  4. Open the deployed URL in a fresh browser session. Test interaction, media, and any direct route URLs before sharing it.

If a hosting service accepts a prebuilt directory, choose this web output as its publish directory. Do not upload the project root, Cargo.toml, or the whole .pax/ directory. If a service builds from source, its build environment also needs the web prerequisites; a generic JavaScript-only build image is not enough.

A static-server configuration

For an existing NGINX installation, this is a minimal document-root example. /srv/pax/my-app stands for a directory containing the complete release output; adjust it and the MIME include path for your server. The enclosing http block and public HTTPS configuration belong to the server administrator.

server {
    listen 127.0.0.1:8080;
    server_name localhost;
    root /srv/pax/my-app;
    index index.html;
    include /etc/nginx/mime.types;
    add_header Cache-Control "no-cache";

    location / {
        try_files $uri $uri/ =404;
    }
}

Here, an existing file or directory is served; an unknown path returns 404. Check the configuration with nginx -t before applying it. See NGINX's static-content guide for how root, index, and try_files work. This example is a starting point for a configured host, not a command to expose a development server publicly.

An in-app navigation can work even when a fresh request to the same URL fails. When someone opens /teams/42/settings, the server receives that path before Pax starts. There are two separate requirements:

  • The server must serve the app's entry HTML for application routes that have no corresponding static file.
  • The HTML must still load scripts, Wasm, styles, and media from the correct build directory.

The web build prepares the entry documents' base URL from [package.metadata.pax.web].site_url, using / when no site URL is supplied. For a domain-root application, the generated HTML includes:

<base href="/">

The default bootstrap uses document.baseURI, so a nested entry can load the same bundle as the root entry. No interface ejection is needed for this standard routing setup. HTML's base element also affects other relative document URLs, including fragment links; review any custom interface HTML alongside the app. The metadata pass sets its base as well. See Routing for the declarations and generated route-metadata.json and route-specific HTML files.

Then configure fallback for the application's route space. For example, an NGINX app with routes beneath /teams/ can add:

location /teams/ {
    try_files $uri $uri/ /index.html;
}

Keep real public documents and assets ahead of application fallback. Missing JS, CSS, Wasm, and media should return 404 rather than the app's HTML. A blanket fallback can conceal a missing file behind a script or MIME error.

Serve generated route entries ahead of the fallback. Preserve the requested URL rather than redirecting it to /; the app needs that path to choose its content. The default route controls the in-app not-found screen, while the server controls HTTP status codes.

A site_url containing /my-app/ supplies that HTML base, but does not strip the prefix from the pathname delivered to Router. The metadata matcher also sees the browser pathname. Do not treat this setting as an end-to-end router mount-prefix option: route definitions, generated entry locations, canonical URLs, and host mapping all need to agree. Prefer a domain-root deployment for the launch path, and verify those pieces separately before using a subdirectory. See Routing for nested scope, parameters, and navigation.

Embedded examples

The default web interface supports an explicit query-backed routing mode for examples hosted inside another site. Open its entry document with pax_route=/ (URL-encoded as ?pax_route=%2F) to start at the application root. In this mode, Pax loads bundle resources beside that entry document and stores subsequent same-tab routes, including their query and fragment, inside pax_route. Back, Forward, and reload keep the physical entry URL intact. The docs use this mode for both embedded examples and their Open standalone links.

Omit the parameter for normal pathname routing. Query-backed examples do not need a server fallback for each application route, but this is a preview/host integration mode, not a replacement for the crawler-visible route entries described above. A custom ejected web interface must preserve the default bootstrap's embedded-base setup if it uses this mode.

HTTPS, content types, and caching

Serve production apps over HTTPS. WebGPU requires a secure context; loopback development URLs have special treatment. Backend availability still depends on the browser and device. See MDN's WebGPU reference.

Return .wasm with Content-Type: application/wasm, and JavaScript/CSS with their appropriate types. The Wasm MIME type enables streaming compilation. MDN documents this requirement. Configure transfer compression on the host if desired: the CLI's gzip-size report is a measurement, not proof that the server sends compressed responses.

Pax's main output filenames are stable across builds. For files updated at the same URL, a conservative starting policy is Cache-Control: no-cache, allowing storage but requiring revalidation. Reserve long-lived immutable caching for versioned or content-addressed URLs whose contents will never change. See HTTP caching.

Deploy matching HTML, JavaScript, Wasm, and assets together. Retain the previous complete release for rollback, and test an upgrade with an already-open app as well as a fresh load. A versioned asset layout can keep old and new clients from fetching incompatible files; it requires an explicit URL/hosting plan, since Pax does not automatically hash all of these filenames.

Build for Apple platforms

Prepare the Apple toolchain

Install full Xcode and the platform components you need. Command Line Tools alone are enough for some web prerequisites, but do not provide the complete native app/simulator workflow. Confirm the active installation:

xcode-select -p
xcodebuild -version
rustup target list --installed

Complete Xcode's initial setup and select the intended Xcode installation if these commands report only a Command Line Tools directory. Install the Rust targets needed for the build you plan to run:

Build destinationRust target
macOS on Apple siliconaarch64-apple-darwin
macOS on Intelx86_64-apple-darwin
iPhone or iPad deviceaarch64-apple-ios
iOS/iPadOS simulator on Apple siliconaarch64-apple-ios-sim
iOS/iPadOS simulator on Intelx86_64-apple-ios

For example, on an Apple-silicon Mac preparing for a macOS release and mobile simulator/device development:

rustup target add aarch64-apple-darwin x86_64-apple-darwin
rustup target add aarch64-apple-ios-sim aarch64-apple-ios

macOS debug builds select the workstation's architecture. The current macOS release path builds both Apple-silicon and Intel code, so it needs both targets. Mobile simulator builds select the host simulator architecture; a physical device build uses the device target instead.

Apple packaging resolves Cargo's target directory through Cargo metadata, including workspace output locations and CARGO_TARGET_DIR. Packaged output still goes under the host application's .pax/ directory.

macOS

For direct native execution without a development server:

cargo run

For template hot reload or an application bundle:

pax-cli run --target macos
pax-cli build --target macos
pax-cli build --target macos --release

run builds and opens the development app. A build also prepares the Xcode project and Swift packages under .pax/build/<mode>/macos/, alongside the app/ output. A release .app is the input to your distribution process; it is not evidence that signing, notarization, or store submission is complete.

iOS and iPadOS development

The CLI assembles and launches the mobile app against an available simulator:

pax-cli run --target ios
pax-cli run --target ipados

Full Xcode and an installed iOS simulator runtime are required. No Cargo runner configuration is needed. The application's native main executes in the iOS process, independently of the workstation CLI.

The default selector chooses an appropriate phone or tablet simulator. List available simulators with xcrun simctl list devices available, then select one by name or UDID when needed:

pax-cli run --target ipados --ios-device "simulator:YOUR_SIMULATOR_UDID"

Replace the placeholder with an actual identifier. The --ios-device flag applies to both mobile targets. It accepts simulator, device, prefixed names/UDIDs such as device:…, or an exact device name/UDID. Specify a device when several are connected.

For a physical-device development run, connect and trust the device, enable Developer Mode where required, and configure an Apple development team in Xcode and the project:

pax-cli run --target ios --ios-device device --ios-development-team YOUR_TEAM_ID

Use your real Team ID. This path may ask Xcode to update provisioning or register the selected device. The flag overrides the target's development_team metadata. Signing errors need to be resolved through your Xcode account, app identity, and provisioning configuration.

To build a debug simulator app without launching it, use pax-cli build --target ios or pax-cli build --target ipados. Device run and simulator run are development workflows; they do not produce a TestFlight or App Store upload.

Local release runs

Use the same simulator selection with --release to build and launch optimized code without designtime or hot reload:

pax-cli run --target ios --release
pax-cli run --target ipados --release --ios-device "simulator:YOUR_SIMULATOR_UDID"

Run either command for the target you want. Simulator runs do not require signing credentials. For a paired physical device with Developer Mode enabled:

pax-cli run --release --target ipados \
  --ios-device 'device:My iPad' --ios-development-team YOUR_TEAM_ID

Use --target ios for iPhone and replace the device name and team with your own. The CLI compiles, uses Xcode's Release configuration with Apple Development signing, installs, and launches the app. The team can also come from project metadata. Release runs disable both hot-reload lanes even if a flag, environment variable, or project setting requests them.

pax-cli build --target ios --release (or --target ipados) builds the device-target app without launching it. These commands do not archive, export, or upload an app for TestFlight or the App Store.

Project metadata

Keep application identity and packaging preferences in Cargo.toml. Pax reads [package.metadata.pax] while preparing the target interface:

[package.metadata.pax]
title = "My App"
icon = "assets/app-icon.png"

[package.metadata.pax.web]
title = "My App on the Web"
favicon = "assets/favicon.png"

[package.metadata.pax.ios]
bundle_identifier = "com.example.myapp"
development_team = "YOUR_TEAM_ID"
build_number = "1"

[package.metadata.pax.ios.info_plist]
NSCameraUsageDescription = "Take a photo to attach to a note."

[package.metadata.pax.macos]
bundle_identifier = "com.example.myapp.macos"

Use identifiers and a team belonging to your project. Include icon/favicon keys only when those source files exist. Add permission descriptions that truthfully describe the capabilities your application uses.

Target-specific values override common values. iPadOS first checks [package.metadata.pax.ipados], then iOS, then common metadata. File paths are relative to the project root unless absolute; project-relative paths keep the configuration portable between workstations.

KeyApplies toMeaning
titleCommon, web, AppleBrowser title or Apple display name; defaults to the Cargo package name
iconCommon, web, AppleImage used for generated icons, including the fallback web favicon
faviconWebExplicit favicon file; takes priority over icon-derived favicon generation
site_nameWebOpen Graph site name; defaults to the resolved web title
server_owned_prefixesWebRoot-relative path prefixes delegated to ordinary browser navigation; defaults to []. See Server-owned web paths.
site_urlWebAbsolute public HTTP(S) URL, without query or fragment; required for release builds with indexable concrete routes
social_imageWebDefault social-preview image: an absolute HTTP(S) URL or a path relative to site_url
social_image_altWebDescription of the social image; configure both image fields together
bundle_identifierCommon, AppleApplication identity for the Apple host
marketing_versionCommon, AppleUser-facing version; defaults to Cargo package.version
build_numberCommon, AppleApple build number, expressed as a TOML string
development_teamCommon, AppleApple development team used during signing
info_plistCommon, AppleTyped table of Info.plist entries
info_plist_fileCommon, AppleProject-owned XML or binary plist with a dictionary root
entitlementsApple target onlyTyped table of requested signing entitlements
entitlements_fileApple target onlyProject-owned XML or binary entitlements plist with a dictionary root

Apple configuration accepts strings, booleans, integers, finite real numbers, arrays, and dictionaries, including nested dictionaries and arrays of tables. Reload configuration has its own [package.metadata.pax.dev] table.

For per-page titles, descriptions, indexing, and social-preview overrides, use Web route metadata. These site-wide settings supply defaults and public URLs; they do not enumerate parameterized pages.

Apple property lists and entitlements

Use Apple's exact keys in the target's info_plist and entitlements tables. There is no networking-only allowlist: a new Apple key normally needs no Pax compiler change. Pax preserves value types and writes actual XML property lists for Xcode to process and sign.

For example, an app that connects to and hosts a TCP service on macOS can opt into both sandbox permissions:

[package.metadata.pax.macos.entitlements]
"com.apple.security.network.client" = true
"com.apple.security.network.server" = true

Quote dotted entitlement keys so TOML treats each as one key. Client access permits outgoing TCP connections; server access permits listening for incoming connections. Once connected, both directions can send and receive. The generated macOS host retains its default sandbox and user-selected read-only file access; network access is opt-in. Async Workbench uses both network entitlements for its loopback TCP exercise. Verify networking in the signed release application: the unsigned debug host does not exercise the same sandbox restrictions.

An iOS app that discovers nearby services and uses universal links might declare:

[package.metadata.pax.ios.entitlements]
"com.apple.developer.associated-domains" = ["applinks:example.com"]

[package.metadata.pax.ios.info_plist]
NSLocalNetworkUsageDescription = "Find and connect to nearby devices."
NSBonjourServices = ["_example._tcp"]

These are independent examples, not required settings for every async app. Associated domains also need the corresponding website association file. macOS client/server sandbox entitlements are not an iOS networking switch. Local-network privacy applies beneath the networking API, including Tokio; some multicast/broadcast operations need a separate entitlement. The mobile debug host adds a local-network usage description for the development connection when the app has not supplied one; release builds do not add that description.

To keep a larger dictionary in an authored file, or use native plist dates or binary data, specify a file beside the inline table:

[package.metadata.pax.ios]
info_plist_file = "apple/Info.extra.plist"
entitlements_file = "apple/App.entitlements"

[package.metadata.pax.ios.info_plist]
NSCameraUsageDescription = "Scan the setup code on your device."

Files must have dictionary roots and can use XML or binary plist encoding. They are overlays on the host's defaults, not complete replacement app manifests. TOML date/time literals are rejected; use a plist file for dates. Paths resolve relative to the directory containing the project's Cargo.toml unless absolute. Rebuild and relaunch after changing metadata or either source file.

Precedence is applied in this order, with later values winning:

  1. The copied native host's defaults.
  2. Common info_plist_file, then common info_plist.
  3. The target's file, then its inline table. For iPadOS, apply the iOS file/table first, followed by the iPadOS file/table.

Each override replaces the whole top-level value. Arrays are not concatenated and dictionaries are not recursively merged. An empty array or dictionary can clear an inherited collection; there is no key-deletion marker. Entitlements must live under macos, ios, or ipados; there is no common entitlements table that silently applies platform-specific permissions to every Apple target.

Pax rejects authored entries for managed fields: CFBundleIdentifier uses bundle_identifier; CFBundleName and CFBundleDisplayName use title; CFBundleShortVersionString uses marketing_version; CFBundleVersion uses build_number. The host owns CFBundleExecutable and CFBundlePackageType. Application/team identifiers and debugger-access entitlements are owned by Xcode signing and provisioning. Conflicts are diagnosed in both source files and inline tables, including entries that another layer would override.

Configuration support does not imply that every capability has native Pax integration. Usage descriptions still need runtime authorization; restricted entitlements still need compatible signing/provisioning; background modes still need the native lifecycle APIs. In particular, enabling Tokio does not keep an iOS application executing after suspension. Privacy manifests are separate PrivacyInfo.xcprivacy resources and are not generated by these tables.

Use Apple's references for platform availability, types, allowed values, and additional requirements:

The generated source files are under .pax/interface/macos/pax-app-macos/pax-app-macos/ or .pax/interface/ios/pax-app-ios/pax-app-ios/: Info.plist and pax_app_macos.entitlements or pax_app_ios.entitlements. Keep authored inputs outside .pax, which is recreated during builds. An ejected Apple interface must retain these files and wire its app target's INFOPLIST_FILE and CODE_SIGN_ENTITLEMENTS to them, with GENERATE_INFOPLIST_FILE = NO.

Application icons

Use a square source image. iOS and iPadOS require an opaque image and generate a single 1024×1024 AppIcon asset. macOS generates its AppIcon size set from the square source. Without an explicit web favicon, a configured icon produces a 64×64 PNG favicon. These are build-time transformations; rebuild after changing the source or metadata.

The bundled iOS/iPadOS interface includes a Pax icon when no custom icon is configured. An ejected interface retains its own asset catalog unless an icon override is supplied.

Apple distribution

Pax currently builds local macOS/iOS app bundles. Full production packaging and App Store publication have not been validated end to end on either platform. Treat the current bundles as inputs to a distribution workflow that still needs to be completed and qualified, not as a finished store-delivery pipeline.

For macOS, finish the signing and distribution configuration appropriate to your audience. Direct distribution commonly uses Developer ID signing and notarization; store distribution follows the App Store process. Apple's distribution guide and notarization guide own those platform steps. Pax does not upload an archive or notarize the application for you.

For iOS and iPadOS, the CLI completes the local release app build and can development-sign and launch it on a selected device. Distribution still requires the appropriate Apple archive/export, distribution signing, provisioning, and upload workflow. A successful local release run does not establish TestFlight or App Store readiness.

The generated mobile Xcode project is currently at .pax/interface/ios/pax-app-ios/pax-app-ios.xcodeproj; shared Swift packages and cartridge framework files are under .pax/interface/common/. These are generated working files and can be overwritten by later builds.

A custom native host or manual Xcode integration requires its own release, resource, signing, and device validation. There is no verified end-to-end macOS or mobile App Store distribution recipe in this chapter yet. If TestFlight or App Store delivery is required for your project, plan and verify that distribution step separately from the CLI's local release workflow.

Before sharing a build

  • Test the release output, including a fresh launch and its primary interaction.
  • Check assets and remote dependencies from the deployed location; keep private files and credentials out of the build and public/.
  • Open routed URLs directly, refresh them, and try back/forward navigation.
  • Verify the actual target/browser's rendering and native-control behavior.
  • Check application title, icon, version, and Apple identity/signing where applicable.
  • Keep one complete previous release and a repeatable build procedure.

The shortest shipping loop is a small one: build, serve or install the artifact, exercise the important behavior, and repeat that check after deployment.

Read more

API Reference

Use the API reference to look up a type, inspect a component's properties, or check a method signature while building. The pages are generated from Rust declarations and documentation comments in the Pax source tree. The guide chapters explain the concepts and show how to combine these APIs into an interface.

If you are starting a project, begin with Getting Started. For an existing project, match the documentation version to your Pax dependency. The development reference may contain changes that are absent from your release; a newer reference page does not establish support in an older build.

Find an API

What you are working onReferenceGuide
Reactive state and computed valuesPropertiesState and Properties
Input events and handler argumentsEventsEvents and Rust
Text and fontsTextText, Fonts, and Images
Shapes, paths, and paintDrawing values and drawing elementsDrawing and Styling
Routes and navigationRouterRouting

Read a reference page

A type's Rust declaration shows its field and method types. For a Pax element, the guide supplies the corresponding template usage: a Rust field such as Property<String> is an input you can set with a compatible literal or binding. Templates and PAXEL explain that authoring syntax.

Read the documentation alongside the signature for defaults, behavior, and target restrictions. An item appearing in the reference does not establish that every backend or platform implements it equally. The relevant guide and source for your release provide the context; the targets chapter explains the build boundary.

Public crates

pax-runtime-api contains author-facing values and reactive APIs. pax-std contains the standard elements and components used in templates. Browse their modules when you need more detail than the entry points above:

Engine and maintainer APIs

For primitive implementations and work on Pax itself, continue with the Maintainer Reference. It separates current internal APIs from historical architecture and design notes.

pax-runtime-api

Public runtime API types shared across Pax crates, user components, and platform backends.

Most names are reexported at the crate root for compatibility, while their source modules provide the browsing ontology used by the generated API docs.

Submodules

Macros

impl_default_coercion_rule

Implements coercion by converting the selected PaxValue variant's contents. The optional third argument opts into CoercionRules::is_identity_roundtrip; omit it unless the type satisfies that method's exact, side-effect-free contract.

animation

Animation primitives: interpolation, easing curves, transition queues, and timelines. See also: properties.ease_to and properties.ease_to_later.

Structs

Timeline

Minimal timeline state used by animation-oriented controls.

Properties

playhead_position

Type: usize

Current playhead frame.

frame_count

Type: usize

Total number of frames.

is_playing

Type: bool

Whether the timeline is currently advancing.

Enums

Duration

A duration used by animation and transition systems.

Frames preserves Pax's historical frame-count semantics, while Milliseconds and Seconds advance from the chassis-provided monotonic wall clock.

Variants

Frames(Numeric)

Duration measured in runtime frames.

Milliseconds(Numeric)

Duration measured in milliseconds.

Seconds(Numeric)

Duration measured in seconds.

Implementations

as_frames_f64
pub fn as_frames_f64(&self) -> f64

Converts the duration to frames, using 60fps as the nominal conversion rate when converting wall-clock units for mixed-unit authoring.

as_milliseconds_f64
pub fn as_milliseconds_f64(&self) -> f64

Converts the duration to milliseconds, using 60fps as the nominal frame duration when converting frame units for mixed-unit authoring.

is_frame_based
pub fn is_frame_based(&self) -> bool

Returns true when the duration is expressed in frame units.

raw_value
pub fn raw_value(&self) -> f64

Raw value in this duration's own unit.


EasingCurve

Pre-built easing curves for use in transitions, as well as a Custom variant that can be used to specify an arbitrary easing curve via a function f: f64 -> f64 mapping a time on the unit interval to a multiplier on the unit interval.

Variants

Linear

A linear easing curve, where the interpolated value changes at a constant rate over time.

Hold

A hold easing curve, where the interpolated value remains constant until the end of the transition, at which point it jumps to the final value.

InQuad

A quadratic easing curve where the interpolated value starts slow and accelerates towards the end of the transition.

OutQuad

A quadratic easing curve where the interpolated value starts fast and decelerates towards the end of the transition.

InOutQuad

A quadratic easing curve where the interpolated value starts slow, accelerates towards the middle of the transition, and then decelerates towards the end of the transition.

InBack

A back easing curve where the interpolated value starts by briefly moving in the opposite direction before accelerating towards the final value.

OutBack

A back easing curve where the interpolated value overshoots the final value before settling back to it.

InOutBack

A back easing curve where the interpolated value starts by briefly moving in the opposite direction, then accelerates towards the final value, overshooting it before settling back to it.

Custom(Box<dyn Fn(f64) -> f64>)

A custom easing curve defined by a user-provided function that maps a time on the unit interval to a multiplier on the unit interval.

Implementations

interpolate
pub fn interpolate<T: Interpolatable>(&self, v0: &T, v1: &T, t: f64) -> T

Interpolates between v0 and v1 using t as time on the unit interval.

Traits

Interpolatable

Marks a value as able to participate in Pax property transitions.

application

Root initialization and application lifetime, independent of any async executor.

Structs

AppClosed

Completion of retained application cleanup. This observer does not own the application and never blocks the UI thread.


AppContext

Weak access to the application that owns a node. Cloning this context does not keep an application active. This context stays local and provides no dependency lookup.

Implementations

async_scope
pub fn async_scope(&self) -> Result<AsyncScope, AsyncError>

Application lifetime, independent of individual mounted views.

closed
pub fn closed(&self) -> AppClosed

Observe disposal completion without driving cleanup. The host's wake scheduler keeps cleanup progressing even when this future is not polled.

id
pub fn id(&self) -> Option<u64>

Process-local identity of this application instance while retained.

phase
pub fn phase(&self) -> AppPhase

Current lifecycle state; a released owner reports Closed.

set_task_error_sink
pub fn set_task_error_sink(&self, sink: impl Fn(String) + 'static) -> Result<(), AsyncError>

Observe unhandled task panics on the UI thread, including dropped controls. Reporting continues while managed shutdown retains the application. The sink must not touch detached UI state once the phase is Closing.

target
pub fn target(&self) -> Result<TargetInfo, AsyncError>

Target facts selected by the owning host.


AppOptions

Resource limits and cleanup diagnostics for one application instance. These options never construct an executor or register application services.

Properties

async_limits

Type: AsyncLimits

startup_data

Type: Vec<u8>

Opaque application-defined bytes, copied by the host before preparation. At most [HOST_MESSAGE_LIMIT] bytes. Pax does not choose their encoding.

shutdown_deadline

Type: Duration

Diagnostic deadline only; unfinished cleanup retains its dependencies.


InitContext

Context supplied to the ordinary root initializer on the UI owner thread. The application's property graph is entered for the duration of the call. Construct the root promptly, then use scoped work for asynchronous loading.

Properties

target

Type: TargetInfo

Implementations

lifetime
pub fn lifetime(&self) -> &AppContext

Cancellation and retained cleanup boundary for explicitly owned executors.

on_host_request
pub fn on_host_request(&self, handler: impl Fn(Vec<u8>) -> AppResult<Vec<u8>> + 'static) -> AppResult

Install the application's one host request handler during initialization. Requests run on the UI owner through the scoped delivery queue after activation. Return promptly; start scoped async work for longer operations. Requests and replies are bounded byte buffers with an app-defined encoding. Closing cancels queued requests. The handler is released before cleanup.

startup_data
pub fn startup_data(&self) -> &[u8]

Read application-defined startup bytes. Decode and validate them in the initializer; the host never receives access to the resulting Rust values.

Enums

AppPhase

The activation and teardown state of one cartridge instance.

Variants

Preparing
Active
Closing
Closed

StopReason

Why an application is leaving its active host.

Variants

HostClosed
Replaced
PreparationDiscarded

Type Aliases

AppResult

A fallible application configuration result.

Constants

HOST_MESSAGE_LIMIT

Maximum encoded startup, request or reply size across a host boundary.

application_shutdown

Retained asynchronous resource cleanup with UI-thread ownership and host wakeups.

Structs

ShutdownComplete

Transferable signal. Dropping it without acknowledgement reports failure.


ShutdownTicket

Nonblocking acknowledgement of retained cleanup. A pending ticket retains its owner and all earlier dependencies; a deadline never fabricates completion. An explicit Err acknowledges quiescence with a cleanup error. Dropping the signal is not acknowledgement and leaves dependencies retained.

async_runtime

Executor-neutral task lifetimes and bounded delivery to the UI thread.

A scope is local authority. Only its producers, guarded publishers and task reporters can cross threads. A successful send means accepted, not delivered.

Structs

AsyncLimits

Application resource limits. Counts do not bound the byte size of user values.

Properties

scopes

Type: usize

tasks

Type: usize

publishers

Type: usize

endpoints

Type: usize

message_slots

Type: usize

channel_capacity

Type: usize

callbacks_per_turn

Type: usize

callback_time

Type: Duration


AsyncScope

Local lifetime controller. Cancellation revokes descendants before requesting task abortion. Keep a component's scope from its mount context when handlers later receive contexts for child controls.

use pax_runtime_api::{application::{ApplicationInstance, AppOptions}, TargetInfo};
let app = ApplicationInstance::prepare(TargetInfo::default(), AppOptions::default()).unwrap();
let scope = app.context().async_scope().unwrap();
std::thread::spawn(move || scope.cancel());

Implementations

child
pub fn child(&self) -> Result<Self, AsyncError>

Create an independent operation underneath this lifetime.

register_task
pub fn register_task(&self, start: impl FnOnce(TaskReporter) -> Box<dyn FnOnce()> + 'static) -> Result<TaskControl, AsyncError>

Adapter entry point. start runs only after activation and returns a nonblocking cancellation request. Retaining the control is optional.

replace
pub fn replace<T: SharedPropertyValue>(&self, previous: &mut Option<AsyncScope>, property: &Property<T>, initial: T) -> Result<AsyncScope, AsyncError>

Revoke the previous request before publishing replacement state and creating its new operation. Registration failure leaves the old work closed.


Closed

A rejected publication retains the caller's value.

Properties

0

Type: T


Completion

Single-use producer. Dropping it unused releases the callback on the UI turn.


GuardedPublisher

A shared property writer whose commit is atomic with lifetime revocation. Cloning this handle does not acquire another registration. Raw Property clones remain deliberately unguarded.


SendFuture

Capacity wait owned by one send attempt; dropping it unregisters its waker.


TaskControl

UI-owned task control; dropping it does not detach work from its scope.

Implementations

status_property
pub fn status_property(&self) -> Published<TaskStatus>

A snapshot property for reactive status displays; applications should not mutate this adapter-owned state.


TaskReporter

Transferable completion signal owned by an executor adapter. Drop means the task stopped without normal completion, usually after an abort request.


UiSender

Transferable producer for a bounded UI channel. Its callback stays local.

Implementations

send
pub fn send(&self, value: T) -> SendFuture<'_, T>

Wait for capacity without blocking the UI or an executor thread.

try_send
pub fn try_send(&self, value: T) -> Result<(), TrySendError<T>>

Enqueue immediately, reporting Full or Closed without losing the value.


UiSubscription

Keeps a UI channel subscribed. Drop closes it and releases its local callback.

Enums

AsyncError

New work was rejected before it could start or reserve resources.

Variants

Closed
Limit(&'static str)
ZeroCapacity

TaskOutcome

The callback result keeps application Result<T, E> separate from a task panic.

Variants

Completed(T)
Panicked(String)

TaskStatus

Observable task state. A cancellation request is not proof that work stopped.

Variants

Staged
Running
CancelRequested
Completed
Cancelled
Panicked(String)

TrySendError

A nonblocking channel send either retains the value or accepts it exactly once.

Variants

Full(T)
Closed(T)

Traits

AsyncScopeSource

A local owner that can bind work to its lifetime without exposing a node to an executor. Implemented by AppContext and runtime NodeContext.

Functions

panic_message

pub fn panic_message(panic: Box<dyn Any + Send>) -> String

Converts an unwinding panic into an observable task/callback failure.

color

Color and opacity types used by Pax style properties and render backends.

Structs

Percent

Raw Percent type, which we use for serialization and dynamic traversal. At the time of authoring, this type is not used directly at runtime, but is intended for into coercion into downstream types, e.g. ColorChannel, Rotation, and Size. This allows us to be "dumb" about how we parse %, and allow the context in which it is used to pull forward a specific type through into inference.

Properties

0

Type: Numeric

Enums

Color

Entrypoint for specifying and representing colors in Pax.

Variants

rgb(ColorChannel, ColorChannel, ColorChannel)

Models a color in the RGB space, with an alpha channel of 100%

rgba(ColorChannel, ColorChannel, ColorChannel, ColorChannel)

Models a color in the RGBA space

hsl(Rotation, ColorChannel, ColorChannel)

Models a color in the HSL space, with an alpha channel of 100%

hsla(Rotation, ColorChannel, ColorChannel, ColorChannel)

Models a color in the HSLA space.

SLATE

Cool blue-gray, RGB (100, 116, 139).

GRAY

Balanced gray, RGB (107, 114, 128).

ZINC

Crisp cool gray, RGB (113, 113, 122).

NEUTRAL

Plain neutral gray, RGB (115, 115, 115).

STONE

Warm stone gray, RGB (120, 113, 108).

RED

Bright warm red, RGB (239, 68, 68).

ORANGE

Vivid citrus orange, RGB (249, 115, 22).

AMBER

Golden amber, RGB (245, 158, 11).

YELLOW

Sunny yellow, RGB (234, 179, 8).

LIME

Electric lime, RGB (132, 204, 22).

GREEN

Fresh green, RGB (34, 197, 94).

EMERALD

Jewel emerald, RGB (16, 185, 129).

TEAL

Deep aquatic teal, RGB (20, 184, 166).

CYAN

Bright clean cyan, RGB (6, 182, 212).

SKY

Open sky blue, RGB (14, 165, 233).

BLUE

Saturated primary blue, RGB (59, 130, 246).

INDIGO

Cool electric indigo, RGB (99, 102, 241).

VIOLET

Soft vivid violet, RGB (139, 92, 246).

PURPLE

Rich playful purple, RGB (168, 85, 247).

FUCHSIA

Brilliant magenta fuchsia, RGB (217, 70, 239).

PINK

Bright candy pink, RGB (236, 72, 153).

ROSE

Warm rosy red, RGB (244, 63, 94).

BLACK

Pure black, RGB (0, 0, 0).

WHITE

Pure white, RGB (255, 255, 255).

TRANSPARENT

Fully transparent white, RGB (255, 255, 255).

NONE

Non-rendering transparent, RGB (255, 255, 255).

Implementations

alpha_0_1
pub fn alpha_0_1(&self) -> f64

Returns this color's alpha channel normalized to the [0.0, 1.0] range.

blend_premultiplied
pub fn blend_premultiplied(colors: impl IntoIterator) -> Self

Adds weighted colors in premultiplied RGBA, then returns straight RGBA. Weights should sum to one. This retains the current RGB encoding; it does not perform a linear-light conversion or blend against a backdrop.

from_hex
pub fn from_hex(hex: &str) -> Self

Constructs a color from a six- or eight-character RGB/RGBA hex string.

from_rgba_0_1
pub fn from_rgba_0_1(rgba_0_1: [f64; 4]) -> Self

Constructs a color from normalized RGBA channels in the [0.0, 1.0] range.

hsl
pub fn hsl(h: Rotation, s: ColorChannel, l: ColorChannel) -> Self

Constructs an HSL color with 100% alpha.

hsla
pub fn hsla(h: Rotation, s: ColorChannel, l: ColorChannel, a: ColorChannel) -> Self

Constructs an HSLA color.

rgb
pub fn rgb(r: ColorChannel, g: ColorChannel, b: ColorChannel) -> Self

Constructs an RGB color with 100% alpha.

rgba
pub fn rgba(r: ColorChannel, g: ColorChannel, b: ColorChannel, a: ColorChannel) -> Self

Constructs an RGBA color.

to_hsla_0_1
pub fn to_hsla_0_1(&self) -> [f64; 4]

Returns HSLA channels normalized to the [0.0, 1.0] range.

to_rgba_0_1
pub fn to_rgba_0_1(&self) -> [f64; 4]

Returns RGBA channels normalized to the [0.0, 1.0] range.

with_alpha_factor
pub fn with_alpha_factor(&self, factor: f64) -> Self

Multiplies this color's alpha channel by factor.


ColorChannel

Describes a color channel in a unit appropriate to the surrounding color model.

Variants

Rotation(Rotation)

Used, for example, to express hue in HSL or other rotational color models.

Integer(u8)

Integer color channel in the [0, 255] range.

Percent(Numeric)

Percent color channel in the [0.0, 100.0] range.

Implementations

to_float_0_1
pub fn to_float_0_1(&self) -> f64

Normalizes this color channel as a float in the [0.0, 1.0] range.


Opacity

Describes an opacity value either as normalized alpha or as a percent.

Variants

Alpha(Numeric)

Unitless alpha in the normalized [0.0, 1.0] range.

Percent(Numeric)

Percent alpha in the [0.0, 100.0] range.

Implementations

to_float_0_1
pub fn to_float_0_1(&self) -> f64

Normalizes this Opacity as a float in [0.0, 1.0].

cursor

Enums

CursorStyle

The variants of mouse cursors available on mouse-supporting platforms (derived from CSS cursor values).

Variants

Auto
Default
None
ContextMenu
Help
Pointer
Progress
Wait
Cell
Crosshair
Text
VerticalText
Alias
Copy
Move
NoDrop
NotAllowed
AllScroll
ColResize
RowResize
NResize
EResize
SResize
WResize
NeResize
NwResize
SeResize
SwResize
EwResize
NsResize
NeswResize
NwseResize
ZoomIn
ZoomOut
Grab
Grabbing
Url(String, i32, i32)

drawing

Vector drawing primitives: paths, fills, strokes, caps, and gradients.

Submodules

Structs

Depth

Logical scene depth for lighting calculations.

Depth is expressed in logical pixels. Unlike [Size], it is not anchored to a viewport or parent box, so percent and combined units are intentionally rejected during value coercion.

Properties

0

Type: Numeric

Implementations

to_float
pub fn to_float(&self) -> f64

Returns the depth as a floating-point logical pixel value.


Fill

One interior paint layer. Index zero in an element's fill stack is topmost.

Properties

paint

Type: Property<Paint>

Color or gradient sampled over the complete local geometry.

material

Type: Property<Material>

Light-reactive surface response, matte by default.

opacity

Type: Property<Opacity>

Opacity of this layer before element opacity is applied.


GradientStop

A color stop for a gradient fill, defined by a position (% or px) and a color.

Properties

position

Type: Size

Stop position, conventionally expressed as a percentage along the gradient.

color

Type: Color

Color at this stop.

Implementations

get
pub fn get(color: Color, position: Size) -> GradientStop

Constructs a gradient stop at position.

with_alpha_factor
pub fn with_alpha_factor(&self, factor: f64) -> GradientStop

Returns a copy of this stop with alpha multiplied by factor.


LinearGradient

Describes a linear gradient fill with a start and end point, and a list of color stops.

Pax templates canonically author each point as [x, y]: magic index 0 is the horizontal coordinate and index 1 is the vertical coordinate.

Properties

start

Type: (Size, Size)

Gradient start point in the primitive's local coordinate space.

end

Type: (Size, Size)

Gradient end point in the primitive's local coordinate space.

stops

Type: Vec<GradientStop>

Ordered color stops along the gradient.


MaterialParams

Tunable response parameters for a light-reactive vector material.

Properties

ambient

Type: Property<f64>

Ambient contribution multiplier.

diffuse

Type: Property<f64>

Diffuse contribution multiplier.

specular

Type: Property<f64>

Specular contribution multiplier.

roughness

Type: Property<f64>

Surface roughness in the [0.0, 1.0] range.

metallic

Type: Property<f64>

Metallic response in the [0.0, 1.0] range.

emissive

Type: Property<Color>

Additive emissive color.

emissive_intensity

Type: Property<f64>

Additive emissive intensity.


MeshGradient

Smooth mesh paint with 2–8 equally sized rows, each containing 2–8 anchors. Automatic shared Bézier controls interpolate the anchors and their premultiplied encoded-sRGB colors. Outside the covered mesh the paint is transparent. Folded patches replace earlier patches in row-major order. Supported by the WGPU renderer. Animate positions/colors to morph; whole [Paint] transitions remain crossfades.

Properties

rows

Type: Vec<Vec<MeshPoint>>

A rectangular lattice; adjacent entries and rows define shared patch edges.

Implementations

validate
pub fn validate(&self) -> Result<(), String>

Checks connectivity and finite inputs. An invalid mesh paints nothing; it is never truncated.


MeshPoint

A colored anchor in a [MeshGradient]. Position uses ordinary pixels, percentages, or combined units, relative to the complete consumer geometry's paint bounds. Rows describe connectivity; anchors in a row may have different y coordinates.

Properties

position

Type: (Size, Size)

Local x/y coordinates relative to the complete paint bounds.

color

Type: Color

Anchor color, including alpha.


RadialGradient

A focal radial gradient, growing from start to the circle at end with radius.

Pax templates canonically author each point as [x, y]: magic index 0 is the horizontal coordinate and index 1 is the vertical coordinate.

Properties

end

Type: (Size, Size)

Center of the outer circle in the primitive's local coordinate space.

start

Type: (Size, Size)

Focal point (0% stop). Equal start/end points give concentric circles.

radius

Type: f64

Outer radius in local logical pixels. Nonpositive/nonfinite radii paint nothing.

stops

Type: Vec<GradientStop>

Ordered color stops along the gradient.

Implementations

resolve_geometry
pub fn resolve_geometry(&self, rect: Rect) -> Option<ResolvedRadialGradient>

Resolves points against the actual shape bounds, including their origin. Invalid geometry paints nothing on all backends. The authored radius is independent of the distance between the focal point and the center.


ResolvedMaterialParams

Plain material coefficients for one settled drawing operation.

Properties

ambient

Type: f64

diffuse

Type: f64

specular

Type: f64

roughness

Type: f64

metallic

Type: f64

emissive

Type: Color

emissive_intensity

Type: f64


ResolvedRadialGradient

Runtime geometry shared by radial color fills and painted alpha masks. Coordinates are local logical pixels, before the element's affine transform.

Properties

focal_point

Type: Point

Point corresponding to the 0% stop.

center

Type: Point

Center of the circle corresponding to the 100% stop.

radius

Type: f64

Radius of the 100% circle in local logical pixels.


ResolvedStroke

Plain drawing values captured before entering a rendering backend. Backends never read live shared properties while recording a frame.

Properties

paint

Type: Paint

material

Type: ResolvedMaterial

opacity

Type: Opacity

width

Type: Size

cap

Type: StrokeCap

join

Type: StrokeJoin

Implementations

width_pixels
pub fn width_pixels(&self) -> f64

Resolves a finite, positive pixel width, or zero for an invisible stroke.


SceneAmbientLight

Resolved ambient light for one logical canvas layer.

Properties

color

Type: Color

Ambient color.

intensity

Type: f64

Ambient intensity.


SceneLight

Resolved light contribution for one logical canvas layer.

Properties

shape

Type: LightShape

Positional or directional light shape.

position

Type: Vector3

Position in logical canvas pixels for point lights.

direction

Type: Vector3

Direction in scene space for directional lights.

color

Type: Color

Light color.

intensity

Type: f64

Light intensity.

radius

Type: f64

Point light radius in logical pixels.

enabled

Type: bool

Whether this light contributes.


SceneLighting

Resolved lighting state for one logical canvas layer.

Properties

active

Type: bool

Whether authored lights or ambient overrides are present.

ambient_is_authored

Type: bool

Whether ambient came from an enabled authored AmbientLight.

The default ambient is only applied to primitives with at least one eligible direct light. An authored ambient remains layer-wide.

ambient

Type: SceneAmbientLight

Singleton ambient contribution.

lights

Type: Vec<SceneLight>

Positional and directional light contributions.

Implementations

DEFAULT_AMBIENT_INTENSITY

Ambient intensity used when point/directional lights exist but no explicit ambient override exists.

MAX_LIGHTS

Maximum number of simultaneously enabled lights in one target canvas layer.

identity
pub fn identity() -> Self

Returns lighting that preserves unlit legacy rendering.

with_default_ambient
pub fn with_default_ambient(lights: Vec<SceneLight>) -> Self

Creates active lighting with Pax's default ambient term.


Stroke

One centered outline layer, drawn above every interior fill. Index zero in an element's stroke stack is topmost.

Properties

paint

Type: Property<Paint>

Color or gradient sampled over the complete, untrimmed geometry.

material

Type: Property<Material>

Light-reactive surface response, matte by default.

opacity

Type: Property<Opacity>

Opacity of this layer before element opacity is applied.

width

Type: Property<Size>

Width in logical pixels. Percentages are unsupported; nonpositive widths draw nothing.

cap

Type: Property<StrokeCap>

Style of exposed endpoints on open paths.

join

Type: Property<StrokeJoin>

Style of joins between adjacent segments.

Implementations

resolve_in_graph
pub fn resolve_in_graph(&self) -> ResolvedStroke

Resolves nested fields from the entered UI graph's settled projections.

snapshot
pub fn snapshot(&self) -> ResolvedStroke

Captures the latest published fields without entering a UI graph. Independently published fields are not a multi-property transaction.

width_pixels
pub fn width_pixels(&self) -> f64

Resolves a finite, positive pixel width, or zero for an invisible stroke.


Vector3

A three-dimensional vector in logical scene space.

Properties

x

Type: f64

X component.

y

Type: f64

Y component.

z

Type: f64

Z component.

Implementations

new
pub fn new(x: f64, y: f64, z: f64) -> Self

Constructs a 3D vector.

Enums

LightShape

Shape of a light contribution in logical scene space.

Variants

Point

A positional light with radius-based attenuation.

Directional

A light with direction but no position or attenuation.


Material

Light-reactive surface response for vector primitives.

This is intentionally named Material; texture is reserved for future bitmap-backed texture maps and pattern data.

Variants

Lit(MaterialParams)

Responds to scene lighting using parameterized material coefficients.

Unlit

Ignores scene lighting and preserves legacy unlit rendering behavior.

Implementations

resolve_in_graph
pub fn resolve_in_graph(&self) -> ResolvedMaterial

Resolves nested coefficients from the entered UI graph's projections.

snapshot
pub fn snapshot(&self) -> ResolvedMaterial

Captures published coefficients without evaluating the UI graph.

custom
pub fn custom(params: MaterialParams) -> Self

Creates a lit material from explicit coefficients.

emissive
pub fn emissive(color: Color, intensity: f64) -> Self

An emissive material that adds color independent of lights.

glossy
pub fn glossy(specular: f64) -> Self

A higher-specular material with lower roughness.

matte
pub fn matte() -> Self

A soft, low-specular material suitable as the default lit response.

metallic
pub fn metallic(metallic: f64) -> Self

A metallic material response.

unlit
pub fn unlit() -> Self

A material that ignores authored lights.


Describes where to open new windows or tabs when navigating to a URL from a Link node.

Variants

Current

Navigate in the current window or tab.

New

Navigate in a new window or tab.


Paint

Describes how to fill vector geometry. Solid fills interpolate their RGBA channels. Other pairs crossfade sampled paints in premultiplied RGBA, preserving each gradient's geometry and stops.

Variants

Solid(Color)

A single solid color.

LinearGradient(LinearGradient)

A linear gradient.

RadialGradient(RadialGradient)

A radial gradient.

Blend(Vec<(Paint, f64)>)

A weighted paint mixture produced by interpolation. Weights are finite, nonnegative, and sum to one. Use [Paint::blend] to normalize weights, flatten nested mixtures, and combine repeated endpoints.

MeshGradient(MeshGradient)

A row-connected bicubic field with independently positioned colored anchors.

Implementations

blend
pub fn blend(terms: Vec<(Paint, f64)>) -> Self

Constructs a normalized mixture of paints. Nested mixtures are flattened and identical endpoints merged, so repeated interruptions between a fixed set of themes do not accumulate a history of blend nodes. Nonpositive or nonfinite weights are ignored; an empty mixture is transparent.

Sampling cost grows with the number of distinct endpoints still visible. A completed interpolation returns its destination paint directly.

coverage_alpha_0_1
pub fn coverage_alpha_0_1(&self) -> f64

Estimates the alpha coverage contributed by this fill.

linearGradient
pub fn linearGradient(start: (Size, Size), end: (Size, Size), stops: Vec<GradientStop>) -> Paint

Constructs a linear gradient paint.

Pax templates should normally use @gradient. When this helper is needed explicitly, pass start and end as [x, y] lists.

max_alpha_0_1
pub fn max_alpha_0_1(&self) -> f64

Returns a conservative upper bound for this paint's alpha. For mixtures, endpoint maxima need not occur at the same point.

representative_color
pub fn representative_color(&self) -> Color

Representative solid color for native text, which currently uses the first stop of a gradient. Mixtures crossfade those representative colors.

with_alpha_factor
pub fn with_alpha_factor(&self, factor: f64) -> Paint

Returns a copy of this paint with alpha multiplied by factor.


PathElement

Describes a single element of a vector path, such as a line, a point, or curve segment.

Variants

Empty

No-op path element.

Point(Size, Size)

Moves the current point to the provided coordinate.

Line

Draws a straight line to the following Point.

Quadratic(Size, Size)

Draws a quadratic Bézier segment with one control point, ending at the following Point.

Cubic(Size, Size, Size, Size)

Draws a cubic Bézier segment with two control points, ending at the following Point.

Close

Closes the current contour.


PathSmoothing

Controls optional curve smoothing for path geometry before tessellation.

PathSmoothing is intended for authored or imported paths whose source data approximates curves with many short line segments, such as single-stroke SVG fonts. Existing paths keep their exact geometry by default.

Variants

None

Preserve the authored path exactly.

Light

Lightly smooth polyline runs while preserving sharp corners.

Strong

More aggressively smooth polyline runs for pen-like paths.


ResolvedMaterial

Material response with no live property handles.

Variants

Lit(ResolvedMaterialParams)
Unlit

StrokeCap

Controls how an open stroke terminates at the exposed endpoints of a path.

StrokeCap affects primitives such as Line and open Path subpaths. Closed geometry ignores cap style because it has no exposed endpoints.

Variants

Butt

Ends exactly at the path endpoint without extending past it.

Round

Adds a semicircular cap whose radius is half the stroke width.

Square

Adds a square cap that extends half the stroke width past the endpoint.


StrokeJoin

Controls how stroke segments are joined at path vertices.

Variants

Miter

Extends outer edges to a point, subject to the renderer's miter limit.

Round

Rounds the outside of each join.

Bevel

Cuts joins off with a straight edge.

Functions

paint_bounds

pub fn paint_bounds(rect: Rect) -> Option<Rect>

Stable local reference frame for paint, independent of outline width and reveal. A collapsed axis gets a centered one-logical-pixel span without adding coverage.

drawing::path_smoothing

Functions

smooth_bez_path

pub fn smooth_bez_path(path: &BezPath, smoothing: PathSmoothing) -> BezPath

Converts long polyline runs in path to cubic Bezier segments.

Existing quadratic and cubic segments are preserved. Sharp corners split a line run so smoothing does not round deliberate angular forms.

drawing::path_trim

Functions

trim_bez_path

pub fn trim_bez_path(path: &BezPath, draw_start: f64, draw_end: f64) -> BezPath

Returns the sub-path visible between normalized draw_start and draw_end.

drawing::stroke_utils

Functions

stroke_width_pixels

pub fn stroke_width_pixels(stroke: &ResolvedStroke) -> f64

Resolves a stroke width as pixels.


stroked_outline_path

pub fn stroked_outline_path(centerline: &BezPath, stroke: &ResolvedStroke) -> Option<BezPath>

Builds the filled outline path corresponding to a stroked centerline path.

This is useful for hit testing, masking, and occlusion, where stroke coverage needs to be treated as fill geometry.

events

Event payloads and cancellation wrappers passed to Pax event handlers.

Structs

ButtonClick

User activates a native button by click or tap.


CheckboxChange

User checks or unchecks a checkbox.

Properties

checked

Type: bool

The new checked state.


Click

User activates an element with a mouse click or single-touch tap.

@click and @tap handlers both receive Event<Click>. A touch tap is emitted after touch end and normalized with button set to MouseButton::Left and no modifiers.

Properties

mouse

Type: MouseEventArgs

Common mouse event data.


ContextMenu

User right-clicks an element to open the context menu.

Properties

mouse

Type: MouseEventArgs

Common mouse event data.


DoubleClick

User double-clicks a mouse button over an element.

Properties

mouse

Type: MouseEventArgs

Common mouse event data.


Drop

User drops a file-like payload over an element.

Properties

x

Type: f64

The x-coordinate of the drop in the receiving node's local coordinate space.

y

Type: f64

The y-coordinate of the drop in the receiving node's local coordinate space.

name

Type: String

File or payload name supplied by the platform.

mime_type

Type: String

MIME type supplied by the platform.

data

Type: Vec<u8>

Raw dropped payload bytes.


Event

Event wrapper passed to Pax event handlers.

Event<T> carries the typed event payload and shared cancellation state, allowing handlers to call prevent_default where the active chassis supports it.

Properties

args

Type: T

The event-specific payload, for example Click or KeyDown.

Implementations

new
pub fn new(args: T) -> Self

Constructs an event wrapper around a raw payload.

prevent_default
pub fn prevent_default(&self)

Cancels the platform default for this event, when the chassis supports cancellation.


Focus

Window or component focused.


KeyDown

User is pressing a key.

Properties

keyboard

Type: KeyboardEventArgs

Common keyboard event data.


KeyPress

User presses a key that displays a character (alphanumeric or symbol).

Properties

keyboard

Type: KeyboardEventArgs

Common keyboard event data.


KeyUp

User has released a key.

Properties

keyboard

Type: KeyboardEventArgs

Common keyboard event data.


KeyboardEventArgs

Common properties in keyboard events.

Properties

key

Type: String

Platform-normalized key string.

modifiers

Type: Vec<ModifierKey>

Modifier keys active during the event.

is_repeat

Type: bool

Whether this event was generated by key-repeat.


MouseDown

User presses a mouse button over an element.

Properties

mouse

Type: MouseEventArgs

Common mouse event data.


MouseEventArgs

Common properties in mouse-backed events and normalized activation events.

Properties

x

Type: f64

The window-space x-coordinate of the event. Use NodeContext::local_point to convert it into coordinates relative to a handler's node.

y

Type: f64

The window-space y-coordinate of the event. Use NodeContext::local_point to convert it into coordinates relative to a handler's node.

button

Type: MouseButton

Mouse button associated with the event.

modifiers

Type: Vec<ModifierKey>

Modifier keys active during the event.


MouseMove

User moves the mouse while it is over an element.

Properties

mouse

Type: MouseEventArgs

Common mouse event data.


MouseOut

User moves the mouse away from an element.


MouseOver

User moves the mouse onto an element.


MouseUp

User releases a mouse button over an element.

Properties

mouse

Type: MouseEventArgs

Common mouse event data.


PhotoPickerChange

Native photo picker completion event.

Properties

id

Type: u32

Picker node id that produced the result.

request_id

Type: u64

App-controlled request id, usually copied from the picker trigger value.

status

Type: PhotoPickerStatus

Completion status.

message

Type: Option<String>

Human-readable platform message for errors or partial results.

photos

Type: Vec<PhotoPickerPhoto>

Selected photos, empty for cancellation, permission denial, or unavailable sources.


PhotoPickerPhoto

One selected image returned by a native photo picker.

Properties

temp_id

Type: String

Stable temporary identifier for this selection.

file_name

Type: Option<String>

File name supplied by the platform, when available.

mime_type

Type: String

MIME type supplied or inferred by the platform.

byte_size

Type: u64

Byte size of the selected asset.

width

Type: Option<u32>

Image width in pixels, when available.

height

Type: Option<u32>

Image height in pixels, when available.

source_kind

Type: PhotoPickerSourceKind

Source used for this image.

handle

Type: Option<String>

Platform preview/read handle, such as an object URL or file URL.

data

Type: Option<Vec<u8>>

Copied asset bytes when the chassis can provide them within configured limits.


Scroll

Scroll is the shared delta-based path for wheel scrolling, touch swipe gestures, and read-only native scroller position changes. Use Wheel or TouchMove when you need platform-specific details instead of the normalized delta stream.

Properties

delta_x

Type: f64

Horizontal scroll delta.

delta_y

Type: f64

Vertical scroll delta.


SelectStart

A native text selection is beginning.


SliderChange

User changes a slider's value.

Properties

value

Type: f64

The new slider value.


TextInput

User types into a text input field.

Properties

text

Type: String

Text committed by the input event.


TextboxChange

User changes the text in a text input field.

Properties

text

Type: String

Current text after the change.


TextboxInput

Text input reported while the user edits a textbox.

On web, this follows the DOM input event. TextboxChange follows the DOM change event instead; commit timing depends on the platform control.

Properties

text

Type: String

Current text reported by the input event.


Touch

Represents a single touch event.

Properties

x

Type: f64

The window-space x-coordinate of this touch point. Use NodeContext::local_point to convert it into coordinates relative to a handler's node.

y

Type: f64

The window-space y-coordinate of this touch point. Use NodeContext::local_point to convert it into coordinates relative to a handler's node.

identifier

Type: i64

Stable identifier for this touch across a touch sequence.

delta_x

Type: f64

Horizontal delta since the previous touch update.

delta_y

Type: f64

Vertical delta since the previous touch update.


TouchCancel

A TouchCancel occurs when the platform aborts an active touch sequence before normal release, such as during a system interruption or host-level gesture cancellation.

The contained touches represent the cancelled touch points and the event targets, then releases, the node captured at touch start. Cancelled touches must not produce activation. A native Scroller that can observe its pan simultaneously with child touches keeps delivering move events and finishes the child sequence with TouchEnd; winning scroll arbitration alone does not cancel the touch.

Properties

touches

Type: Vec<Touch>

Touch points whose active sequence was cancelled.


TouchEnd

A TouchEnd occurs when the user stops touching an element. The contained touches represent a list of touch points and the event targets, then releases, the node captured at touch start.

Properties

touches

Type: Vec<Touch>

Touch points that ended.


TouchMove

A TouchMove occurs when the user moves while touching an element. The contained touches represent a list of touch points and the event targets the node captured at touch start, even when the point has moved outside its bounds.

Properties

touches

Type: Vec<Touch>

Active touch points after this touch movement.


TouchStart

A TouchStart occurs when the user touches an element. The contained touches represent a list of touch points. The hit node is captured for the touch identifier so subsequent move and end events keep routing to the same subtree.

Properties

touches

Type: Vec<Touch>

Active touch points at the start of this touch sequence.


ViewportProximityChange

A settled geometry change during a continuous proximity visit.

First admission/re-entry has no previous sample. Departure sends one final change outside proximity before exit. Unchanged frames emit nothing.

Properties

previous

Type: Option<ViewportProximitySnapshot>

Last delivered sample in this visit; None on admission/re-entry.

current

Type: ViewportProximitySnapshot

Current frozen geometry, including the terminal outside sample.


ViewportProximityEnter

Entry into global viewport proximity, including initially nearby targets. Delivered before the initial change event. This local event does not bubble.

Properties

current

Type: ViewportProximitySnapshot

Frozen sample shared with the initial change notification.


ViewportProximityExit

Departure from global viewport proximity, after the terminal change event. Unmount cancels observation without synthesizing a geometric exit.

Properties

previous

Type: ViewportProximitySnapshot

Last sample inside proximity.

current

Type: ViewportProximitySnapshot

Terminal outside sample shared with the final change notification.


ViewportProximitySnapshot

One settled viewport-proximity sample in logical window coordinates.

Bounds are transformed layout AABBs, not padded paint coverage. Rectangular clips are intersected; curved/rotated clips use their bounding envelopes. Opacity, alpha coverage, sibling occlusion and external windows are ignored.

Properties

in_proximity

Type: bool

Whether the target intersects the proximity region with positive area.

bounds

Type: Rect

Presented layout bounds relative to the global visual viewport's origin.

viewport_intersection

Type: Option<Rect>

Positive-area intersection with the global viewport and ancestor clips.

Implementations

intersection_ratio
pub fn intersection_ratio(&self) -> f64

Fraction of the layout AABB inside the effective viewport, from zero to one.

is_in_viewport
pub fn is_in_viewport(&self) -> bool

Whether any positive-area viewport intersection exists.


Wheel

User scrolls the mouse wheel over an element.

Properties

x

Type: f64

The x-coordinate of the wheel event in the receiving node's local coordinate space.

y

Type: f64

The y-coordinate of the wheel event in the receiving node's local coordinate space.

delta_x

Type: f64

Horizontal wheel delta.

delta_y

Type: f64

Vertical wheel delta.

modifiers

Type: Vec<ModifierKey>

Modifier keys active during the event.

Enums

ModifierKey

The variants of modifier keys available on keyboard-supporting platforms.

Variants

Shift

Shift key.

Control

Control key.

Alt

Alt or Option key.

Command

Command, Meta, or Windows key.


MouseButton

The variants of mouse buttons available on mouse-supporting platforms.

Variants

Left

Primary mouse button.

Secondary mouse button.

Middle

Middle mouse button.

Unknown

Button not identified by the chassis.


PhotoPickerSourceKind

Platform source used to produce a selected photo.

Variants

Library

Existing image from a photo library.

File

Existing image from the filesystem.

Camera

Newly captured camera image.

Other(String)

Source not recognized by this runtime version.


PhotoPickerStatus

Completion status for a native photo picker request.

Variants

Selected

One or more photos were selected.

Cancelled

The platform picker was dismissed without a selection.

PermissionDenied

Camera or picker permission was denied or restricted.

Unavailable

The requested source is not available on this platform/device.

SizeLimitExceeded

One or more candidate photos exceeded the configured byte limit.

Failed

The platform picker failed for another reason.

layout

Layout units, axes, and common properties shared by renderable nodes.

Structs

CommonProperties

Properties shared by every renderable Pax node.

Each property here is special-cased by the compiler when parsing element properties, for example <SomeElement width={...} />.

Properties

id

Type: LocalProperty<Option<String>>

Optional stable node identifier.

x

Type: LocalProperty<Option<Size>>

Horizontal position.

y

Type: LocalProperty<Option<Size>>

Vertical position.

padding_x

Type: LocalProperty<Option<Size>>

Symmetric inner spacing applied to this node's child layout area on the x axis.

padding_y

Type: LocalProperty<Option<Size>>

Symmetric inner spacing applied to this node's child layout area on the y axis.

width

Type: LocalProperty<Option<Size>>

Horizontal extent.

height

Type: LocalProperty<Option<Size>>

Vertical extent.

anchor_x

Type: LocalProperty<Option<Size>>

Horizontal transform origin, relative to the node's own bounds.

anchor_y

Type: LocalProperty<Option<Size>>

Vertical transform origin, relative to the node's own bounds.

scale_x

Type: LocalProperty<Option<Size>>

Horizontal scale coefficient.

scale_y

Type: LocalProperty<Option<Size>>

Vertical scale coefficient.

skew_x

Type: LocalProperty<Option<Rotation>>

Horizontal skew.

skew_y

Type: LocalProperty<Option<Rotation>>

Vertical skew.

rotate

Type: LocalProperty<Option<Rotation>>

Rotation around the z axis.

transform

Type: LocalProperty<Option<Transform2D>>

Full composed transform.

opacity

Type: LocalProperty<Option<Opacity>>

Opacity of this node's subtree. WGPU and browser Piet compose each canvas portion before fading it; live native surfaces currently fade separately.

layout_role

Type: LocalProperty<Option<LayoutRole>>

Controls whether this node participates in parent layout measurement and flow.

unclippable

Type: LocalProperty<Option<bool>>

Allows a node to render outside an ancestor clipping frame.

Enums

Axis

Model of 2D cartesian axes, used to disambiguate calculations that depend on axis direction (e.g. width vs height)

Variants

X

Horizontal axis.

Y

Vertical axis.


LayoutRole

Controls whether a node participates in parent layout measurement.

Variants

Default

Normal layout behavior. The node contributes to parent hulls, autosize, and flow.

Breakout

Parent-local positioning that does not contribute to parent hulls, autosize, or flow.

Breakout remains in the normal render, hit-test, scroll, and clipping trees; it does not portal above ancestor frames, masks, or scrollers.


Size

A spatial size value that can be either a concrete pixel value like 25px, a percent of parent bounds like 50%, or an additive/subtractive combination of the two like (100% - 10px).

Variants

Pixels(Numeric)

Concrete pixel length, such as 25px.

Percent(Numeric)

Percent length relative to the relevant parent bound, such as 50%.

Combined(Numeric, Numeric)

Additive pixel and percent components, such as 100% - 10px.

Implementations

ZERO
pub fn ZERO() -> Self

Returns a zero-pixel size.

expect_percent
pub fn expect_percent(&self) -> f64

Returns the wrapped percent value normalized as a float, such that 100% => 1.0. Panics if wrapped type is not a percentage.

expect_pixels
pub fn expect_pixels(&self) -> Numeric

Returns the pixel value Panics if wrapped type is not pixels.

evaluate
pub fn evaluate(&self, bounds: (f64, f64), axis: Axis) -> f64

Evaluate a Size in the context of bounds and a target axis. Returns a Pixel value as a simple f64; calculates Percent with respect to bounds & axis

get_pixels
pub fn get_pixels(&self, parent: f64) -> f64

Resolves this size against a parent extent in pixels.

math

Structs

Generic

Generic, untyped coordinate space.

Traits

Space

Marker trait for phantom coordinate spaces.

pax_value

Submodules

Enums

PaxAny

This type serves a similar purpose as Box<dyn Any>, but allows for special handling of some types, enabling things like coercion.

Variants

Builtin(PaxValue)

Built-in PAXEL/runtime value.

Any(Box<dyn Any>)

Arbitrary Rust value boxed for runtime storage.


PaxValue

Runtime container for polymorphic values, for evaluating PAXEL.

Two important traits are related to this type: ToFromPaxValue - responsible for converting to and from specific types (u8, String, Color, etc) CoercionRules - responsible for coercing a PaxValue to a specific type (possibly from multiple different variants)

Variants

Bool(bool)

Boolean value.

Numeric(Numeric)

Polymorphic numeric value.

String(String)

UTF-8 string value.

Size(Size)

Pax size value, such as 25px or 50%.

Percent(Percent)

Raw percent value.

Color(Box<Color>)

Pax color value.

Rotation(Rotation)

Pax rotation value.

Duration(Duration)

Pax animation duration value, such as 250ms, 1s, or 10f.

PathElement(Box<PathElement>)

Vector path element value.

Option(Box<Option<PaxValue>>)

Optional value.

Vec(Vec<PaxValue>)

Homogeneous or heterogeneous vector value.

Range(Box<PaxValue>, Box<PaxValue>)

Range value.

Object(Vec<(String, PaxValue)>)

Object value represented by named fields.

Enum(Box<(String, String, Vec<PaxValue>)>)

Enum value represented by type name, variant name, and payload values.

Traits

ImplToFromPaxAny

Marker trait for types that can be stored inside PaxAny without a built-in PaxValue representation.

If the type is part of PaxValue, implement CoercionRules instead.


ToFromPaxAny

Trait that marks a type as being representable as a PaxAny, and provides the implementation for going to/from that type. For all builtins this means going to/from a pax value. For others to a Box<dyn Any>. This is automatically Implemented for PaxValue types through the macro impl_to_from_pax_value!, and for other types by implementing the marker trait ImplToFromPaxAny.


ToPaxValue

Converts a Rust value into its built-in PaxValue representation.

This is exact conversion, not coercion; coercion is handled separately by CoercionRules.

Type Aliases

RcPaxValue

Shared runtime PaxValue handle.

pax_value::functions

Structs

Functions

Registry bootstrap for built-in PAXEL helper functions.

Implementations

has_function
pub fn has_function(scope: &str, name: &str) -> bool

Returns true if a helper function exists in the named scope.

register_all_functions
pub fn register_all_functions()

Registers built-in math, color, and transform helper functions.

Traits

HelperFunctions

Registers helper functions made available to PAXEL scopes.

pax_value::numeric

Enums

Numeric

Numeric type wrapper for PAXEL and runtime polymorphic numeric operations. This broad polyfill specifically enables terse numeric operations within expressions, without the overhead of explicit typing or numeric microsyntax.

Variants

I8(i8)
I16(i16)
I32(i32)
I64(i64)
U8(u8)
U16(u16)
U32(u32)
U64(u64)
F64(f64)
F32(f32)
ISize(isize)
USize(usize)

Implementations

is_float
pub fn is_float(&self) -> bool

Returns true when this value is stored as a floating-point number.

max
pub fn max(self, other: Self) -> Self

Returns the larger of two numeric values.

min
pub fn min(self, other: Self) -> Self

Returns the smaller of two numeric values.

pow
pub fn pow(self, exp: Self) -> Self

Raises this number to exp, preserving integer arithmetic when both sides are integral.

to_float
pub fn to_float(&self) -> f64

Coerces this numeric value to f64.

to_int
pub fn to_int(&self) -> i64

Coerces this numeric value to i64.

platform

Platform, viewport, and coordinate-space marker types.

Structs

Accel

Current device acceleration, in meters per second squared.

On web targets, this uses DeviceMotionEvent.accelerationIncludingGravity when available, falling back to DeviceMotionEvent.acceleration.

Properties

x

Type: f64

Acceleration along the x axis.

y

Type: f64

Acceleration along the y axis.

z

Type: f64

Acceleration along the z axis.


Gyro

Current device orientation reported by a gyroscope/orientation sensor.

On web targets, this is sourced from DeviceOrientationEvent and mapped as: x = beta, y = gamma, and z = alpha, all in degrees.

Properties

x

Type: f64

Front-to-back tilt, in degrees.

y

Type: f64

Left-to-right tilt, in degrees.

z

Type: f64

Compass/z-axis rotation, in degrees.


NativeLiquidGlassScope

Runtime-inherited Apple liquid-glass effect scope.

Properties

group_id

Type: u32

Stable node id for the nearest liquid-glass scope.

spacing

Type: f64

Desired spacing between grouped glass surfaces, in pixels.

interactive

Type: bool

Whether supported Apple surfaces should use the interactive glass effect.

tint

Type: Option<Color>

Optional tint for supported Apple surfaces.

variant

Type: String

Apple glass style name, currently "regular" or "clear".

Implementations

to_message
pub fn to_message(&self) -> AppleLiquidGlassPatch

Converts runtime style data into the serialized native-message payload.


TargetInfo

Derived target facts exposed to PAXEL and Rust event handlers.

Properties

web

Type: bool

Browser-hosted rendering target.

native

Type: bool

Native application rendering target.

ios

Type: bool

Any iOS-family target, including iPhone and iPad.

iphone

Type: bool

iPhone-class iOS target.

ipad

Type: bool

iPadOS / iPad-class iOS target.

macos

Type: bool

macOS target.

android

Type: bool

Android target.

windows

Type: bool

Windows target.

linux

Type: bool

Linux target.

mobile

Type: bool

Any mobile OS target.

desktop

Type: bool

Any desktop OS target.

Implementations

new
pub fn new(platform: Platform, os: OS) -> Self

Build target facts from the chassis platform and detected OS.


Viewport

Struct representing the outermost viewport of a rendering scene, for example a browser window or native application window.

Properties

width

Type: f64

Viewport width in pixels.

height

Type: f64

Viewport height in pixels.

major

Type: f64

Larger viewport dimension in pixels.

minor

Type: f64

Smaller viewport dimension in pixels.

aspect

Type: f64

Width divided by height. Returns 0.0 when height is 0.

landscape

Type: bool

True when width is greater than height.

portrait

Type: bool

True when height is greater than width.

square

Type: bool

True when width and height are effectively equal.

Implementations

SQUARE_EPSILON

Equality tolerance used when classifying square viewports.

new
pub fn new(width: f64, height: f64) -> Self

Build viewport facts from width and height in logical pixels.


Window

Phantom coordinate space representing the outer window.

Enums

OS

Describes known operating systems / targets.

Variants

Mac

macOS.

Linux

Linux desktop.

Windows

Windows desktop.

Android

Android.

IPhone

iOS on iPhone-class devices.

IPad

iPadOS / iOS on iPad-class devices.

Unknown

OS has not been detected or reported.

Implementations

is_android
pub fn is_android(&self) -> bool

Returns true for Android.

is_desktop
pub fn is_desktop(&self) -> bool

Helper to determine if the OS is a desktop platform.

is_ios
pub fn is_ios(&self) -> bool

Returns true for either iPhone-class iOS or iPadOS.

is_ipad
pub fn is_ipad(&self) -> bool

Returns true for iPadOS / iPad-class iOS.

is_iphone
pub fn is_iphone(&self) -> bool

Returns true for iPhone-class iOS.

is_linux
pub fn is_linux(&self) -> bool

Returns true for Linux.

is_macos
pub fn is_macos(&self) -> bool

Returns true for macOS.

is_mobile
pub fn is_mobile(&self) -> bool

Helper to determine if the OS is a mobile platform.

is_windows
pub fn is_windows(&self) -> bool

Returns true for Windows.


Platform

Describes categories of known platforms, for differentiating certain engine behaviors.

Variants

Web

Browser-hosted rendering target.

Native

Native application target.

Unknown

Platform unknown or not yet reported.

Implementations

is_native
pub fn is_native(&self) -> bool

Returns true when hosted by a native chassis.

is_web
pub fn is_web(&self) -> bool

Returns true when hosted by the web chassis.

properties

Traits

PropertyBinding

Owner-thread binding operations used by generated component factories.

Shared fields publish values while their evaluators stay in the entered [PropertyGraph]. Local fields retain ordinary lazy graph semantics.


PropertyValue

Value operations shared by local and transferable properties.

Values must be cloneable for .get(), interpolatable for transitions, and 'static because properties are stored in the runtime graph.

rendering

Rendering backend contracts and helpers for drawing Pax scene content.

Structs

AlphaMaskPaint

One vector paint contributing alpha to a mask, in local path coordinates. Color channels do not affect coverage; fill alpha and opacity are multiplied.

Properties

path

Type: BezPath

transform

Type: Affine

fill

Type: Paint

opacity

Type: f64

paint_bounds

Type: Rect

Complete local geometry bounds before stroke expansion or trimming.

composition

Type: Option<(u32, f32)>

Primitive boundary: compose its paints before applying source-relative opacity.


OpacityScope

One authored opacity boundary, ordered from the outermost ancestor to the painted node. Native surfaces can present independently while a canvas backend composes each scope.

Properties

node_id

Type: u32

opacity

Type: f32


ReplayCanvasLayerUpdate

Replay invalidation for one logical canvas layer.

Properties

layer

Type: usize

regions

Type: Option<Vec<Rect>>

Canvas-content regions requiring replay. The runtime selects nodes from its shared scene geometry; None requests full-layer dirtification.

Enums

Layer

Render layer selected for a primitive or native surface.

Variants

Native

Platform-native element layer.

NativeNonOccluding

Platform-native layer that does not participate in occlusion.

Canvas

GPU/canvas-rendered layer.

DontCare

Runtime can choose the appropriate layer.

Traits

RenderContext

The drawing contract shared by Pax runtime backends.

Functions

bez_path_to_svg_path_data

pub fn bez_path_to_svg_path_data(path: &BezPath) -> String

Convert a kurbo BezPath to a SVG-friendly drawing string

shader_effect

Experimental, renderer-owned subtree filters for userland primitives.

Programs are Rust resources, not template values. Expose ordinary Property<T> inputs on a primitive and resolve them into an instance through InstanceNode::shader_effect. Available with the opt-in shaders Cargo feature, normally enabled on pax-kit or pax-engine to include the corresponding renderer support.

Structs

ShaderEffect

Current instance parameters and its conservative sampling/output support.

Properties

program

Type: &'static ShaderProgram

Immutable recipe shared across mounted instances.

parameters

Type: Vec<Vec<ShaderValue>>

One ordered value list per pass, matching that pass's schema.

outset

Type: f64

Maximum absolute sampling displacement/output expansion on either local axis, in logical pixels. Keep this envelope stable during animation to reuse captures and attachments.

Implementations

encode_parameters
pub fn encode_parameters(&self) -> Result<Vec<Vec<u8>>, String>

Validate the recipe and encode its parameters. Shader syntax is checked by the GPU backend.


ShaderParameter

A named field in the generated WGSL parameters uniform.

Properties

name

Type: &'static str

ASCII WGSL identifier used as parameters.name.

ty

Type: ShaderParameterType

Uniform member type; values must match exactly.


ShaderPass

An experimental WGSL fragment pass. Implement fn effect(p: vec2<f32>) -> vec4<f32>. p and sample_input0/1(p) use effect-local logical pixels; output is premultiplied RGBA. The engine supplies entry points, texture bindings, transparent-outside sampling and uniforms.

Properties

label

Type: &'static str

Asset/source name included in shader diagnostics.

source

Type: &'static str

WGSL function definitions, typically packaged with include_str!.

parameters

Type: &'static [ShaderParameter]

Ordered schema for the generated parameters uniform.

inputs

Type: [ShaderInput; 2]

Sources of sample_input0 and sample_input1; repeat one source when only one is needed.

downsample

Type: u32

Resolution divisor: 1 is full resolution; 2, 4, 8 and 16 are supported.


ShaderProgram

Immutable userland pass recipe. At most eight passes and 32 parameters per pass.

Properties

label

Type: &'static str

Human-readable name included in renderer diagnostics.

passes

Type: &'static [ShaderPass]

Ordered passes; the last pass supplies the premultiplied output.

Enums

ShaderInput

Texture input to a pass. Earlier outputs share the capture's logical coordinate domain.

Variants

Subtree

The rendered descendants before this effect.

Pass(usize)

Zero-based index of an earlier pass in this program.


ShaderParameterType

One host/shader parameter type. Every member occupies a 16-byte uniform slot.

Variants

Float
Uint
Vec2
Vec4

ShaderValue

A resolved value, encoded explicitly rather than casting Rust structs into GPU memory.

Variants

Float(f32)
Uint(u32)
Vec2([f32; 2])
Vec4([f32; 4])

Implementations

encode
pub fn encode(self) -> Result<[u8; 16], String>

Encode one little-endian, zero-padded uniform slot; reject non-finite floats.

ty
pub fn ty(self) -> ShaderParameterType

The uniform schema type represented by this value.

store

Typed stores owned by mounted nodes and discovered through NodeContext.

Enums

StoreError

Failure to register or borrow a store through a NodeContext.

Variants

Missing

No provider of the requested type exists on this node or its logical ancestors.

ExpiredScope

The context is not mounted, its mount has ended, or its provider ancestry has expired.

BorrowConflict

The selected provider is already borrowed by a with_store closure. Lookup does not fall through to an outer provider in this case.

Traits

Store

Marker trait for values published with NodeContext::provide_store.

The concrete Rust type is the lookup key. Each mounted owner can provide one value per type; a nearer provider of that type shadows an ancestor's value. Prefer a named struct or newtype for each purpose. A type alias does not create a distinct key.

A store is ordinary Rust data and does not need #[pax]. Use Property handles inside it for reactive state; plain fields do not become reactive. The owner clears its providers at final unmount. Explicitly cloned handles can outlive that registration, but do not reconnect to a later mount.

transform

Rotation and 2D transform types used by layout and rendering.

Structs

Transform2D

A sugared representation of an Affine transform combined with an anchor layout property.

Properties

previous

Type: Option<Box<Transform2D>>

Linked list of ancestral Transform2Ds

rotate

Type: Option<Rotation>

Represents affine rotation over z axis (single-dimensional for 2D rendering)

translate

Type: Option<[Size; 2]>

Represents affine translation across the x-y plane

anchor

Type: Option<[Size; 2]>

Represents the alignment of the (0,0) position of this element as it relates to its own bounding box. (origin offset)

scale

Type: Option<[Size; 2]>

Represents affine scale coefficients across the x-y plane

skew

Type: Option<[Rotation; 2]>

Represents affine skew over x and y axes

Implementations

anchor
pub fn anchor(x: Size, y: Size) -> Self

Transform origin point for this element, relative to its own bounding box.

rotate
pub fn rotate(z: Rotation) -> Self

Rotation over the z axis.

scale
pub fn scale(x: Size, y: Size) -> Self

Scale coefficients over the x-y plane.

translate
pub fn translate(x: Size, y: Size) -> Self

Translation over the x-y plane.

Enums

Rotation

Encodes a rotation in various units

Variants

Radians(Numeric)

Radian units (2π rad for one full rotation)

Degrees(Numeric)

Degree units (360 deg for one full rotation)

Percent(Numeric)

Percentage units (100% for one full rotation)

Implementations

ZERO
pub fn ZERO() -> Self

Returns zero degrees.

get_as_degrees
pub fn get_as_degrees(&self) -> f64

Returns the rotation as degrees, regardless of the original unit

get_as_radians
pub fn get_as_radians(&self) -> f64

Returns the rotation as radians, regardless of the original unit

to_float_0_1
pub fn to_float_0_1(&self) -> f64

Returns a normalized float proportional to 0deg : 0.0 :: 360deg : 1.0.

For example, 0rad maps to 0.0, 100% maps to 1.0, and 720deg maps to 2.0.

unit_value

Normalized unit-domain values.

Enums

UnitValue

A scalar value in a normalized unit domain.

UnitValue accepts both unitless numbers and percents. It is useful for properties whose authoring domain is "part of a whole", such as path drawing progress where 0.5 and 50% describe the same position.

Variants

Unitless(Numeric)

Unitless normalized value, where 0.5 means halfway through the domain.

Percent(Numeric)

Percent value, where 50% means halfway through the domain.

Implementations

to_clamped_unit_float
pub fn to_clamped_unit_float(&self) -> f64

Returns to_unit_float() clamped into the closed unit interval.

to_unit_float
pub fn to_unit_float(&self) -> f64

Returns the wrapped value as a normalized unitless float.

UnitValue::Unitless(0.5) returns 0.5, and UnitValue::Percent(50) returns 0.5.

variables

LocalProperty adapters that expose runtime values to expression scopes.

Structs

Variable

Runtime property adapter used by expression scopes.

Implementations

get_as_pax_value
pub fn get_as_pax_value(&self) -> PaxValue

Reads the current value as a PaxValue.

new
pub fn new<T: PropertyValue + ToPaxValue>(untyped_property: UntypedProperty) -> Self

Wraps an untyped property and exposes it as a PaxValue.

new_from_typed_property
pub fn new_from_typed_property<T: PropertyValue + ToPaxValue>(property: impl PropertyBinding<T>) -> Self

Wraps a typed property and exposes it as a PaxValue.

read_pax_value_ref
pub fn read_pax_value_ref<V>(&self, f: impl FnOnce(&PaxValue) -> V) -> V

Reads the current PaxValue by reference.

try_typed_binding
pub fn try_typed_binding<T: PropertyValue + CoercionRules>(&self, name: &str) -> Option<LocalProperty<T>>

Creates an independent one-way binding when exact typed forwarding is safe. Unlike a double binding, writes/easing on the result never mutate the source. Mismatched types and custom conversions return None.

pax-std

Submodules

common

Structs

CanvasNodeRenderScope

Properties

paint_opacity

Type: f64

Paint opacity within the canvas composition; the backend applies outer scopes once.

core

Submodules

core::event_blocker

Structs

EventBlocker

Native surface that absorbs pointer events before they reach content beneath it.

background defaults to transparent. A solid background is useful for modal underlays that must composite above native controls and scroller canvas islands.

Properties

background

Type: Property<Color>

Solid background painted by the native surface.

core::frame

Structs

Frame

A primitive that gathers children underneath a single render node with a shared base transform, like Group, except Frame has the option of clipping rendering outside of its bounds.

If clipping or the option of clipping is not required, a Group will generally be a more performant and otherwise-equivalent to Frame, since Frame creates a clipping mask.

Properties

autosize

Type: Property<bool>

Automatically sizes the frame to its direct content children when possible.

autosize_x

Type: Property<Option<bool>>

Optional override for whether autosize manages the x axis.

autosize_y

Type: Property<Option<bool>>

Optional override for whether autosize manages the y axis.

corner_radius

Type: Property<f64>

Corner radius used for the frame clipping mask, in pixels.

core::group

Structs

Group

Gathers a set of children underneath a single render node: useful for composing transforms and simplifying render trees.

Properties

autosize

Type: Property<bool>

Automatically sizes the group to its direct content children when possible.

autosize_x

Type: Property<Option<bool>>

Optional override for whether autosize manages the x axis.

autosize_y

Type: Property<Option<bool>>

Optional override for whether autosize manages the y axis.

corner_radius

Type: Property<f64>

Corner radius used when the group materializes a native surface, in pixels.

core::import_settings

Structs

ImportSettings

Mounts a non-rendering subtree whose component descendants export selector settings. transition=SettingsTransition::Ease(400ms, TransitionCurve::InOutQuad) eases changes to effective settings after the initial appearance. Explicit inline values, property timelines, and two-way bindings retain ownership.

Properties

transition

Type: Property<SettingsTransition>

Optional motion for changes to effective imported settings. Each new target starts a full-duration transition from the currently displayed value.

Enums

SettingsTransition

Motion shared by the settings exported from one ImportSettings instance. Named timeline playback is intentionally not supported yet.

Variants

None

Apply settings immediately (the default).

Ease(Duration, TransitionCurve)

Interpolate changed settings using this duration and easing curve. Nonpositive or non-finite durations apply the destination immediately.


TransitionCurve

Built-in easing curves for automatic settings transitions.

Variants

Linear

Constant progress.

Hold

Retain the source until the duration ends.

InQuad

Accelerate from rest.

OutQuad

Decelerate into the destination.

InOutQuad

Accelerate, then decelerate.

InBack

Move back before accelerating forward.

OutBack

Overshoot, then settle.

InOutBack

Anticipation and overshoot.

core::link

Structs

Navigates to a URL when its slotted content is clicked or tapped.

Link remains router-agnostic: it writes a URL, while Router and Route declaratively read the current location. On web targets, same-origin target=Current navigation can be serviced through client-side history updates instead of a full document reload. Paths declared in Cargo metadata's [package.metadata.pax.web].server_owned_prefixes use ordinary browser navigation instead, preserving the query and fragment. The hosting server must separately serve those paths. This web setting does not change native routing or target=New behavior.

Properties

url

Type: Property<String>

Destination URL.

target

Type: Property<Target>

Whether to open the URL in the current or a new browsing context.

autosize

Type: Property<bool>

Automatically sizes the link wrapper to its slotted content when possible.

autosize_x

Type: Property<Option<bool>>

Optional override for whether autosize manages the x axis.

autosize_y

Type: Property<Option<bool>>

Optional override for whether autosize manages the y axis.

Enums

Target

Navigation target for Link.

Variants

Current

Navigate in the current window or tab.

New

Navigate in a new window or tab.

core::liquid_glass

Structs

LiquidGlass

Applies an Apple liquid-glass native effect to supported descendant native surfaces.

Properties

enabled

Type: Property<bool>

Whether this liquid-glass scope is active. When false, descendants opt out.

spacing

Type: Property<Size>

Desired spacing between grouped glass surfaces, in Pax units.

interactive

Type: Property<bool>

Whether supported Apple glass surfaces should use the interactive effect.

tint

Type: Property<Option<Color>>

Optional tint for supported Apple glass surfaces.

variant

Type: Property<String>

Apple glass style. Supported values are currently "regular" and "clear".

core::mask

Structs

Mask

Clips its first child using its second child subtree, which is not rendered as visible content. By default, this is a geometric coverage mask. Set alpha=true to use painted alpha instead, and feather to soften its edge:

<Mask width=100% height=100% alpha=true feather=12.0>
    <Rectangle width=100% height=100% fill=#FF0088/>
    <Ellipse x=50% y=50% anchor=50% width=240px height=240px
        fill=TRANSPARENT stroke={paint: WHITE, width: 40px}/>
</Mask>

The example reveals pink only underneath the soft ring. feather is Gaussian standard deviation in logical pixels, independent of display pixel density; zero disables feathering. Both properties default to zero/false, preserving existing coverage-mask behavior.

Alpha sources support Rectangle, Ellipse, Path, and Line appearance layers, including source-relative opacity, transforms, and linear/radial gradient alpha (up to eight ordered stops). Source RGB does not matter. Retained vector captures preserve source-side Frame/Mask clips and group opacity. Two opaque shapes in a half-opacity Group yield 50% coverage even in their overlap; individually half-opacity shapes yield 75% there. Nested alpha masks on content multiply; ordinary geometric clips continue to intersect them. An empty alpha source hides all content. Captures share group-opacity surfaces and reuse geometry for paint and GPU stroke-reveal updates. Feathering has a padded, surface-local domain; captures exceeding device texture limits hide content with a diagnostic.

Source components have a normal logical lifetime: their templates expand, mount handlers initialize state, reactive changes and animation remain live, and unmount releases their descendants and subscriptions. This allows a Handwriter (including its animated draw range) to supply the mask directly. Source nodes are not presented or hit-tested. Native leaves, including Handwriter's selectable text equivalent, do not create native surfaces; keep meaningful accessible text outside the mask. User mount side effects still run.

Current boundary: WGPU canvas rendering, verified on web. Native controls and the legacy Piet renderer do not support alpha masks. Images, text, native elements, and source-side Scrollers are not captured; use vector leaves and same-surface containers. Alpha masks modulate canvas draw alpha, not an isolated offscreen group, and do not change hit testing. Keep interactive hit targets separate from purely visual alpha reveals.

Properties

alpha

Type: Property<bool>

Use painted alpha instead of geometric coverage.

feather

Type: Property<f64>

Gaussian feather standard deviation, in logical pixels, for alpha masks.

core::router

Structs

Route

Default route branch shell consumed by a parent [Router].

Use path for explicit path matching, :param for single-segment capture, and a terminal * to consume the remaining tail. Use default=true to provide the fallback branch when no explicit path matches.

Properties

path

Type: Property<String>

Static route pattern, e.g. /docs/:slug or /settings/*.

Patterns are matched against the current router scope, not always the full global path. At the root router, that scope is the full location.

default

Type: Property<bool>

Fallback branch used when no path branch matches.


RouteCard

Declarative route branch presented as a card over the current route.

RouteCard is consumed by a parent [Router] like [Route], but the presentation behavior is owned by this component shell. It retains the previously mounted branch underneath so the incoming or outgoing card slides over stable content while a black scrim fades over the retained branch. The route's own contents may still declare additional element or component lifecycle transitions.

Properties

path

Type: Property<String>

Static route pattern, e.g. /details/:id.

default

Type: Property<bool>

Fallback card branch used when no path branch matches.

edge

Type: Property<RouteCardEdge>

Edge used by the generated card enter/exit transition.

duration

Type: Property<Duration>

Duration for the generated card enter/exit transition.

scrim_opacity

Type: Property<Opacity>

Maximum opacity for the black scrim over the retained background. Unitless values are normalized alpha (0.3 is 30%); percentages such as 30% are also supported.

curve

Type: Property<RouteCardCurve>

Reserved easing tuning for the card enter/exit transition.


RouteModal

Stacked route branch consumed by a parent [Router].

RouteModal matches like [Route], but it keeps the previously mounted branch active underneath while the modal branch is active. The shell fades a black scrim over the retained branch; the route's own contents are responsible for any modal-specific enter/exit transition.

Properties

path

Type: Property<String>

Static route pattern, e.g. /tools.

default

Type: Property<bool>

Fallback modal branch used when no path branch matches.

duration

Type: Property<Duration>

Duration for the generated modal scrim enter/exit transition.

scrim_opacity

Type: Property<Opacity>

Maximum opacity for the black scrim over the retained background. Unitless values are normalized alpha (0.3 is 30%); percentages such as 30% are also supported.


Router

Declarative route reader that selects one active [Route] child subtree.

Router is a control-flow primitive: it does not render by itself. Instead, it matches the current location against its child Route branches and mounts only the winning subtree.

The active subtree receives an implicit route binding with:

  • route.location: the location scoped to this router
  • route.global_location: the full browser/native location
  • route.params: named captures from :param segments
  • route.remainder: the unmatched tail after this branch
  • route.is_exact and route.consumed_segments: match metadata

Nested routers match against the nearest ancestor route.remainder by default, which keeps route trees composable without manual string slicing.

Enums

RouteCardCurve

Reserved easing curve values for [RouteCard] transition tuning.

Variants

Linear
Hold
InQuad
OutQuad
InOutQuad
InBack
OutBack
InOutBack

RouteCardEdge

Edge used by [RouteCard] transitions.

Variants

Leading
Trailing
Top
Bottom

core::scroller

Structs

Scroller

A scrolling container, which clips its bounds and offers platform-native scrolling on either or both of the horizontal and vertical axes.

Properties

scroll_pos_x

Type: Property<f64>

Horizontal scroll offset, in pixels.

scroll_pos_y

Type: Property<f64>

Vertical scroll offset, in pixels.

scroll_width

Type: Property<Size>

Width of the scrollable content pane.

scroll_height

Type: Property<Size>

Height of the scrollable content pane.

autosize

Type: Property<bool>

Automatically sizes the scroll pane on its default axes when possible.

For Scroller, the default autosized axis is y; x remains bound to scroll_width unless autosize_x explicitly opts in.

autosize_x

Type: Property<Option<bool>>

Optional override for whether autosize manages the x axis.

autosize_y

Type: Property<Option<bool>>

Optional override for whether autosize manages the y axis.

corner_radius

Type: Property<f64>

Corner radius for the scroller clipping region, in pixels.

snap_positions_x

Type: Property<Vec<Size>>

Scroll snap anchors expressed in px/% along each axis. Web maps to CSS scroll-snap-type + scroll-snap-align; Apple chassis map these offsets to native scroll end-points while keeping engine scroll state authoritative.

snap_positions_y

Type: Property<Vec<Size>>

Vertical scroll snap anchors expressed in px/%.

core::text

Structs

Text

Renders and styles text through the target's native text system.

A constrained width with an omitted height allows native measurement to determine the height of wrapped content. Selection, editing, font loading, and accessibility behavior depend on the target's native implementation; test the intended interaction on each shipping target.

Properties

editable

Type: Property<bool>

Whether the text can be edited by the user.

selectable

Type: Property<bool>

Requests selectable text. On the current iOS/iPadOS path, non-editable text uses the interactive selection view only when clip is enabled.

clip

Type: Property<bool>

Whether text overflow is clipped to the node bounds.

text

Type: Property<String>

Text content to display.

style

Type: Property<TextStyle>

Text styling.

markdown

Type: Property<bool>

Whether text should be interpreted as Markdown.

wrap

Type: Property<bool>

Whether long text lines should wrap inside the node bounds.


TextStyle

Struct describing platform-agnostic text display properties. Interpolation eases font size and fill; font selection, underline, and alignment switch to the destination immediately. Glyphs do not morph.

Properties

font

Type: Property<Font>

Font family/source/style/weight configuration.

font_size

Type: Property<Size>

Font size, in pixels.

fill

Type: Property<Paint>

Text color. Native text patches reduce gradient fills to their first stop's color; use a solid fill for predictable text color.

underline

Type: Property<bool>

Whether text should be underlined.

align_multiline

Type: Property<TextAlignHorizontal>

Alignment for multiline text layout.

align_vertical

Type: Property<TextAlignVertical>

Vertical text alignment within its bounds.

align_horizontal

Type: Property<TextAlignHorizontal>

Horizontal text alignment within its bounds.

Enums

Font

Describes a font available to native text renderers.

Pax templates may use either the explicit [Font::Web] constructor or a contextual shorthand. A string names a locally available family with an empty source URL and normal style and weight:

font: "Times New Roman"

Named object fields describe font sources and optional modifiers. Omitted fields retain their defaults. Supplying family without url selects a locally available family, matching the string shorthand. weight accepts either [FontWeight] or its CSS numeric equivalent from 100 through 900:

font: {
    family: "Inter"
    url: "https://example.com/Inter-Italic.ttf"
    style: FontStyle::Italic
    weight: 700
}

Nonempty URLs identify font files, except that Google Fonts URLs containing fonts.googleapis.com/css receive special stylesheet handling. Relative asset URLs work on web. Apple chassis register TTF/OTF files from the project's assets/ directory at startup; select their actual family name with an empty URL, or use that family with the web asset URL in a shared declaration. The native Web-font loader itself does not resolve relative URLs into bundle resources. Browser-only aliases do not name the registered Apple family.

Font shorthand has no positional ("magic index") fields: named keys are canonical, and positional list/tuple forms are not accepted. Use the explicit [Font::Web] constructor as verbose longhand when needed.

Variants

Web(String, String, FontStyle, FontWeight)

Font described by family name, source URL, style, and weight.


FontStyle

Describes available font styles.

Variants

Normal
Italic
Oblique

FontWeight

Describes available font weights.

Variants

Thin
ExtraLight
Light
Normal
Medium
SemiBold
Bold
ExtraBold
Black

Implementations

decrease
pub fn decrease(weight: FontWeight) -> FontWeight

Returns the next lighter named font weight.

increase
pub fn increase(weight: FontWeight) -> FontWeight

Returns the next heavier named font weight.


TextAlignHorizontal

Describes available horizontal text alignments.

Variants

Left
Center
Right

TextAlignVertical

Describes available vertical text alignments.

Variants

Top
Center
Bottom

drawing

Submodules

drawing::ellipse

Structs

Ellipse

A 2D vector ellipse, which inscribes its bounding box with the specified fill and stroke.

Properties

stroke

Type: Property<Vec<Stroke>>

Ordered outline layers above the fills, index zero topmost. Empty by default.

fill

Type: Property<Vec<Fill>>

Paint painted inside the ellipse.

drawing::handwriter

Structs

Handwriter

Renders text as single-stroke vector paths suitable for handwriting effects.

Handwriter uses bundled SVG stroke fonts and translates text into PathElement data for an inner Path. Use draw_start and draw_end to animate the visible writing range.

Properties

text

Type: Property<String>

Text to render. Newline characters create additional baselines.

font

Type: Property<HandwriterFont>

Bundled stroke font used to draw text.

stroke

Type: Property<Vec<Stroke>>

Stroke used for the generated path.

smoothing

Type: Property<PathSmoothing>

Optional curve smoothing applied to generated path geometry.

alt_text

Type: Property<String>

Accessible text label. When empty, text is used.

selectable

Type: Property<bool>

Whether the invisible native text layer can be selected.

draw_start

Type: Property<UnitValue>

Start position of the visible handwriting range.

draw_end

Type: Property<UnitValue>

End position of the visible handwriting range.

line_height

Type: Property<f64>

Baseline-to-baseline multiplier relative to the font's em size.

Enums

HandwriterFont

Bundled single-stroke fonts available to Handwriter.

Variants

EMSAllure

Cursive, airy script.

EMSDelight

Friendly rounded print hand.

EMSInvite

Tall invitation script.

EMSLeague

Flowing connected script.

EMSNeato

Casual narrow script.

EMSOsmotron

Rectilinear plotter hand.

EMSReadability

Highly readable manuscript hand.

EMSTech

Technical drafting hand.

HersheySans1

Classic Hershey sans stroke font.

HersheyScript1

Classic Hershey connected script.

drawing::lighting

Structs

AmbientLight

A non-rendering singleton ambient-light override for its scene.

Properties

color

Type: Property<Color>

Ambient color.

intensity

Type: Property<f64>

Ambient intensity multiplier.

enabled

Type: Property<bool>

Whether this ambient override participates in topmost-wins selection.


LightFrame

A non-rendering container that keeps descendant lights from escaping its subtree.

Lights outside the frame may still illuminate its descendants. Each expanded frame instance has its own lexical lighting identity.


LightSource

A non-rendering light resource that affects light-reactive vector materials in its scene.

Properties

color

Type: Property<Color>

Light color.

intensity

Type: Property<f64>

Light intensity multiplier.

radius

Type: Property<Size>

Radius for point-light attenuation.

z

Type: Property<Depth>

Logical scene depth in pixels.

shape

Type: Property<LightShape>

Positional or directional light shape.

direction

Type: Property<Vector3>

Direction for directional lights.

enabled

Type: Property<bool>

Whether this light contributes to the scene.

drawing::line

Structs

Line

A 2D vector line segment.

x1/y1 and x2/y2 describe the segment endpoints in the primitive's local coordinate space. The segment is rendered with stroke, whose cap style controls how the two exposed endpoints terminate.

Properties

x1

Type: Property<Size>

The x-coordinate of the start point.

y1

Type: Property<Size>

The y-coordinate of the start point.

x2

Type: Property<Size>

The x-coordinate of the end point.

y2

Type: Property<Size>

The y-coordinate of the end point.

stroke

Type: Property<Vec<Stroke>>

The stroke used to render the segment.

drawing::path

Structs

Path

A 2D vector path for arbitrary Bézier and line-segment chains.

elements describes the path in local coordinates. fill paints the interior of closed contours, while stroke paints the path itself; for open subpaths, the stroke cap controls the exposed endpoints. Fills use non-zero winding: overlapping contours with the same direction stay filled; reverse an inner contour's direction to create a hole. Path geometry may draw outside the element's layout bounds; use a Frame or Mask when that overflow should be clipped.

Properties

elements

Type: Property<Vec<PathElement>>

The path commands and control points, expressed in local coordinates.

stroke

Type: Property<Vec<Stroke>>

The stroke applied along the path centerline.

fill

Type: Property<Vec<Fill>>

The fill applied to the interior of closed contours.

smoothing

Type: Property<PathSmoothing>

Optional curve smoothing applied before rendering path geometry.

draw_start

Type: Property<UnitValue>

Start position of the visible stroke range over the path's total length.

draw_end

Type: Property<UnitValue>

End position of the visible stroke range over the path's total length.

Implementations

curve_to
pub fn curve_to(path: Vec<PathElement>, h_x: Size, h_y: Size, x: Size, y: Size) -> Vec<PathElement>

Appends a quadratic Bézier curve with one control point.

line_to
pub fn line_to(path: Vec<PathElement>, x: Size, y: Size) -> Vec<PathElement>

Appends a straight line segment to the provided point.

start
pub fn start(x: Size, y: Size) -> Vec<PathElement>

Starts a new path at the provided point.


PathClose

Path child component that closes the current contour.


PathCurve

Path child component that inserts a quadratic curve control point.

Properties

x

Type: Property<Size>

Control point x-coordinate.

y

Type: Property<Size>

Control point y-coordinate.


PathLine

Path child component that inserts a straight line segment.


PathPoint

Path child component that inserts a PathElement::Point.

Properties

x

Type: Property<Size>

Point x-coordinate.

y

Type: Property<Size>

Point y-coordinate.

drawing::pax_logo

Reusable Pax branding with a consumer-controlled animation playhead.

AnimatedPaxLogo contains only the logo. Parent components own layout, background, controls, and initial playback. StaticPaxLogo draws its settled pose without animation masks. Both use a 920.72 × 463.95 coordinate system; scale an enclosing Group to fit another layout.

Submodules

drawing::pax_logo::animated_pax_logo

Structs

The animated Pax logo, with a normalized playhead and optional click replay.

Bind progress to coordinate a slider or a parent-owned timeline. The component does not start playback on mount or paint its surrounding surface. Scale an enclosing Group from the original 920.72 × 463.95 coordinate system.

Properties

background_mode

Type: Property<LogoBackgroundMode>

Selects the contrasting signboard palette. Defaults to Light to preserve existing consumers; Dark exchanges fill and letter_fill throughout.

fill

Type: Property<Paint>

Signboard/post fill in Light mode, lettering/counters in Dark mode.

letter_fill

Type: Property<Paint>

Lettering/backing fill in Light mode, signboard/post in Dark mode.

progress

Type: Property<f64>

Normalized animation position from the initial pose (0.0) to the finished logo (1.0). Bind this property for coordinated playback. Clicking restarts it unless click_to_replay is false.

click_to_replay

Type: Property<bool>

Restart the 1440ms animation when clicked or tapped. Defaults to true. Disable for externally driven playback or a noninteractive still logo.

Enums

LogoBackgroundMode

The surface behind the logo, not the signboard's own color.

Variants

Light

Dark signboard with light letters, for a light surrounding surface.

Dark

Light signboard with dark letters, for a dark surrounding surface.

drawing::pax_logo::animated_pax_logo_banner

Structs

AnimatedPaxLogoBanner

The animated signboard and lettering used by [AnimatedPaxLogo].

Properties

fill

Type: Property<Paint>

letter_fill

Type: Property<Paint>

progress

Type: Property<f64>

curtain_drop_1

Type: Property<f64>

curtain_drop_2

Type: Property<f64>

curtain_drop_3

Type: Property<f64>

curtain_drop_4

Type: Property<f64>

curtain_drop_5

Type: Property<f64>

Type: Property<f64>

p_counter_reveal_progress

Type: Property<f64>

p_counter_x_px

Type: Property<f64>

p_rotation_deg

Type: Property<f64>

p_scale

Type: Property<f64>

a_x_px

Type: Property<f64>

a_rotation_deg

Type: Property<f64>

a_scale

Type: Property<f64>

a_brake_rotation_deg

Type: Property<f64>

a_morph_progress

Type: Property<f64>

a_opacity

Type: Property<f64>

x_offset_px

Type: Property<f64>

x_offset_y_px

Type: Property<f64>

x_rotation_deg

Type: Property<f64>

x_scale

Type: Property<f64>

x_brake_rotation_deg

Type: Property<f64>

x_opacity

Type: Property<f64>

Type: Property<Vec<PathElement>>

p_ring_elements

Type: Property<Vec<PathElement>>

p_counter_elements

Type: Property<Vec<PathElement>>

p_reveal_mask_elements

Type: Property<Vec<PathElement>>

p_counter_reveal_mask_elements

Type: Property<Vec<PathElement>>

a_elements

Type: Property<Vec<PathElement>>

a_counter_elements

Type: Property<Vec<PathElement>>

a_counter_reveal_mask_elements

Type: Property<Vec<PathElement>>

x_elements

Type: Property<Vec<PathElement>>

drawing::pax_logo::animated_pax_logo_post

Structs

AnimatedPaxLogoPost

The rolling fabric post used by [AnimatedPaxLogo].

Properties

fill

Type: Property<Paint>

backing_fill

Type: Property<Paint>

progress

Type: Property<f64>

rolling

Type: Property<f64>

extension

Type: Property<f64>

wave

Type: Property<f64>

swing

Type: Property<f64>

fall

Type: Property<f64>

roll_radius_px

Type: Property<f64>

top_fabric_elements

Type: Property<Vec<PathElement>>

fabric_elements

Type: Property<Vec<PathElement>>

backing_fabric_elements

Type: Property<Vec<PathElement>>

roll_start_cap_elements

Type: Property<Vec<PathElement>>

roll_body_elements

Type: Property<Vec<PathElement>>

roll_end_cap_elements

Type: Property<Vec<PathElement>>

drawing::pax_logo::static_pax_logo

Structs

The settled Pax logo, using the animation's final geometry without masks.

This component has no playback or click behavior. It uses the same palette and 920.72 × 463.95 coordinate system as [AnimatedPaxLogo].

Properties

background_mode

Type: Property<LogoBackgroundMode>

The surrounding surface; Dark exchanges the signboard and letter fills.

fill

Type: Property<Paint>

Signboard/post fill in Light mode, lettering/counters in Dark mode.

letter_fill

Type: Property<Paint>

Lettering/backing fill in Light mode, signboard/post in Dark mode.

Type: Property<Vec<PathElement>>

p_ring_elements

Type: Property<Vec<PathElement>>

p_counter_elements

Type: Property<Vec<PathElement>>

a_elements

Type: Property<Vec<PathElement>>

a_counter_elements

Type: Property<Vec<PathElement>>

x_elements

Type: Property<Vec<PathElement>>

roll_start_cap_elements

Type: Property<Vec<PathElement>>

roll_body_elements

Type: Property<Vec<PathElement>>

roll_end_cap_elements

Type: Property<Vec<PathElement>>

top_fabric_elements

Type: Property<Vec<PathElement>>

fabric_elements

Type: Property<Vec<PathElement>>

backing_fabric_elements

Type: Property<Vec<PathElement>>

drawing::rectangle

Structs

CornerRadii

Corner radii, ordered clockwise from top-left.

Pax templates canonically use a list of one to four values such as corner_radius=[12, 8, 4]. Lists expand using the same clockwise arity rules as CSS border-radius; a uniform radius may elide the brackets as corner_radius=12.

The zero-based positional ("magic index") contract depends on list arity:

  • [all]
  • [top-left/bottom-right, top-right/bottom-left]
  • [top-left, top-right/bottom-left, bottom-right]
  • [top-left, top-right, bottom-right, bottom-left]

A contextual named object remains available as explicit longhand: corner_radius={ top_left: 12 top_right: 8 bottom_right: 4 bottom_left: 2 }. The fully type-qualified constructor also remains valid when explicit type syntax is useful: corner_radius=CornerRadii { top_left: 12 top_right: 8 bottom_right: 4 bottom_left: 2 }. Interpolation treats each radius independently; zero produces an angular corner.

Properties

top_left

Type: Property<Numeric>

Top-left corner radius.

top_right

Type: Property<Numeric>

Top-right corner radius.

bottom_right

Type: Property<Numeric>

Bottom-right corner radius.

bottom_left

Type: Property<Numeric>

Bottom-left corner radius.

Implementations

resolve_in_graph
pub fn resolve_in_graph(&self) -> RoundedRectRadii

Resolves corner radii from the entered UI graph's settled projections.

radii
pub fn radii(top_left: Numeric, top_right: Numeric, bottom_right: Numeric, bottom_left: Numeric) -> Self

Constructs a CornerRadii value from clockwise corner radii.


Rectangle

A 2D vector rectangle, which covers its bounding box with the specified fill and stroke.

Properties

stroke

Type: Property<Vec<Stroke>>

Ordered outline layers above the fills, index zero topmost. Empty by default.

fill

Type: Property<Vec<Fill>>

Paint painted inside the rectangle.

corner_radius

Type: Property<CornerRadii>

Per-corner radii.

forms

Submodules

forms::button

Structs

Button

A button control, delegating to a platform-specific native button.

Properties

label

Type: Property<String>

Text label displayed inside the button.

color

Type: Property<Color>

Button background color.

hover_color

Type: Property<Color>

Button background color while hovered, when supported.

corner_radius

Type: Property<f64>

Button corner radius, in pixels.

outline

Type: Property<Stroke>

Button outline stroke.

style

Type: Property<TextStyle>

Text style applied to the label.

forms::checkbox

Structs

Checkbox

A checkbox control, delegating to a platform-specific native checkbox.

Properties

background

Type: Property<Color>

The background color when unchecked

background_checked

Type: Property<Color>

The background color when checked

outline

Type: Property<Stroke>

The outline stroke of the checkbox

corner_radius

Type: Property<f64>

The border radius of the checkbox

checked

Type: Property<bool>

Whether the checkbox is currently checked

forms::combo_box

Structs

ComboBox

A text-filtered list control for selecting one item, with optional "new item" behavior.

Clicking or tapping the input opens the list. Filtering is case-sensitive, with prefix matches first and alphabetical order within each group. Selecting a row commits its index in options, copies its label to text, and closes the list. Editing or blurring the input does not commit a selection or clear the previous one. Bind text for the query and selected for the last choice.

This composed control uses a native Textbox and Pax list rows. It does not implement arrow-key/Enter list selection or a complete accessible combobox contract.

Properties

text

Type: Property<String>

Text currently shown in the input.

selected

Type: Property<Option<usize>>

Index of the last selected option, or None. Editing text filters the list; clicking a row commits its original index in options.

options

Type: Property<Vec<String>>

Available option labels; duplicate labels retain distinct indices. Changing this list refreshes the selected label at the existing index. An out-of-range selection becomes None and clears the text. Reordering options does not preserve an application's item identity automatically.

new_item

Type: Property<NewItem>

Behavior when the typed text does not match any option.

background

Type: Property<Color>

Textbox/list background color.

stroke

Type: Property<Stroke>

Textbox/list stroke.

style

Type: Property<TextStyle>

Text style for the textbox and list items.

corner_radius

Type: Property<f64>

Textbox corner radius, in pixels.

Enums

NewItem

Behavior when typed combo-box text does not match an existing option.

Variants

Disallow

Show a disabled "No Results Found" row. This is the default.

AllowInvalid

Allow unmatched text without showing a no-results row. Initial free text is preserved when selected is None; this does not commit a new item.

Text(String)

Show custom text when there are no matches; clicking or tapping the row triggers @new_item. The application must add/select the item itself.

forms::dialogs

Structs

ConfirmationDialog

A confirmation dialog, a modal dialog that asks the user to confirm an action with "Yes" or "No" buttons.

Properties

text

Type: Property<String>

Prompt text shown in the dialog.

open

Type: Property<bool>

Whether the dialog is visible.

forms::dropdown

Structs

A dropdown list control, delegating to a platform-specific native dropdown implementation. Allows the selection of a single option from a list of options.

Properties

stroke

Type: Property<Stroke>

Outline stroke for the dropdown control.

options

Type: Property<Vec<String>>

List of selectable option labels.

selected_id

Type: Property<u32>

Index of the currently selected option.

style

Type: Property<TextStyle>

Text style for option labels.

background

Type: Property<Color>

Dropdown background color.

corner_radius

Type: Property<f64>

Dropdown corner radius, in pixels.

forms::photo_picker

Structs

PhotoPicker

Opens the platform photo picker when its slotted content is activated.

PhotoPicker renders its children as the visible affordance and layers a transparent native hit target over them. Increment trigger to request a programmatic open; on the web, direct user activation of the picker element is the most reliable way to satisfy browser file-picker requirements.

Properties

trigger

Type: Property<u64>

Incrementing request value. Returned as request_id in photo_picker_change.

source

Type: Property<PhotoPickerSource>

Requested platform source.

allow_multiple

Type: Property<bool>

Whether multiple images may be selected.

accept

Type: Property<String>

Accepted MIME/file filter. Web uses this as the input accept value.

include_bytes

Type: Property<bool>

Whether the chassis should copy bytes into the event when practical.

max_bytes_per_photo

Type: Property<u64>

Maximum copied/read bytes per selected photo. Larger photos report a size-limit status.

Enums

PhotoPickerSource

Source requested when opening a PhotoPicker.

Variants

Library

Let the user choose existing images from the platform photo/file picker.

Camera

Request camera capture where the current platform supports it.

forms::radio_list

Structs

RadioList

A radio list control, delegating to a platform-specific native control.

Properties

background

Type: Property<Color>

Radio button background color when unchecked.

background_checked

Type: Property<Color>

Radio button background color when checked.

outline

Type: Property<Stroke>

Radio button outline stroke.

options

Type: Property<Vec<String>>

List of selectable option labels.

selected_id

Type: Property<u32>

Index of the currently selected option.

style

Type: Property<TextStyle>

Text style for option labels.

forms::slider

Structs

Slider

A slider control, delegating to a platform-specific native range input.

Properties

background

Type: Property<Color>

Track background color.

accent

Type: Property<Color>

Accent color for the active track/thumb, when supported.

corner_radius

Type: Property<f64>

Slider corner radius, in pixels.

value

Type: Property<f64>

Current slider value.

step

Type: Property<f64>

Step interval.

min

Type: Property<f64>

Minimum value.

max

Type: Property<f64>

Maximum value.

forms::tabs

Structs

Tabs

A component displaying a list of tabs, e.g. for tabbed navigation.

Properties

names

Type: Property<Vec<String>>

A list of string labels for the tabs

selected

Type: Property<usize>

The index of the currently selected tab

color

Type: Property<Color>

The background color of the tabs

forms::textbox

Structs

Textbox

A text input field, with support for styling and font specification. Will be composited as a platform-specific native element, for example an <input> element in the browser or a UITextField on iOS.

Properties

text

Type: Property<String>

Current text value.

background

Type: Property<Color>

Textbox background color.

placeholder

Type: Property<String>

Placeholder text shown when empty, when supported by the chassis.

stroke

Type: Property<Stroke>

Border stroke.

corner_radius

Type: Property<f64>

Corner radius, in pixels.

style

Type: Property<TextStyle>

Text style.

outline

Type: Property<Stroke>

Focus outline stroke.

focus_on_mount

Type: Property<bool>

Requests focus when the textbox mounts.

multiline

Type: Property<bool>

Renders the textbox as a multiline text area when supported by the chassis.

forms::toast

Structs

Toast

A user interface "toast", a notification banner that appears for a short time and then disappears again.

Properties

shown

Type: Property<bool>

Whether the toast is currently shown.

message

Type: Property<String>

The message to display in the toast.

height

Type: Property<Size>

The height of the toast.

y_pos

Type: Property<Size>

The y position of the toast.

forms::tooltip

Structs

Tooltip

A simple hover tooltip that renders slotted content plus a floating text tip.

Properties

tip

Type: Property<String>

Tooltip text.

autosize

Type: Property<bool>

Automatically sizes the trigger wrapper to its slotted content when possible.

autosize_x

Type: Property<Option<bool>>

Optional override for whether autosize manages the x axis.

autosize_y

Type: Property<Option<bool>>

Optional override for whether autosize manages the y axis.

layout

Submodules

layout::carousel

Structs

A paged scrolling container with native scroll snapping and optional page dots.

Each slotted child becomes one page. The carousel lays pages out along axis, sizes each page with page_size, and binds its scroll position through an internal Scroller.

Properties

axis

Type: Property<CarouselAxis>

Axis along which pages are laid out and snapped.

page_size

Type: Property<Size>

Size of each page along the scroll axis (defaults to 100%).

show_dots

Type: Property<bool>

Whether to show page-position dots when there is more than one page.

scroll_pos_x

Type: Property<f64>

Horizontal scroll position, in pixels.

scroll_pos_y

Type: Property<f64>

Vertical scroll position, in pixels.

Enums

CarouselAxis

Direction for carousel paging and scroll snapping.

Variants

Horizontal

Pages flow left-to-right.

Vertical

Pages flow top-to-bottom.

layout::dynamic_island_spacer

Structs

DynamicIslandSpacer

Opt-in space for UIKit's current safe area; does not draw or intercept input.

<DynamicIslandSpacer /> fills its container's width and measures its height from the top safe-area inset. Use it in content-sized layout, or bind inset when positioning other content explicitly. edge also supports the home indicator and landscape side insets. Insets update on rotation and window changes, in debug and release. Other chassis measure zero on the inset axis.

This reserves the platform's entire safe inset, including status bars on devices without a Dynamic Island. It does not automatically move siblings, inset ancestors, or detect an ancestor that already avoids the safe area.

Properties

edge

Type: Property<SafeAreaEdge>

Window edge to reserve. Defaults to Top. Left/Right measure width and fill the container's height; Top/Bottom measure height and fill width.

inset

Type: Property<f64>

Computed inset in logical pixels. Bind to observe it; the spacer owns this output and overwrites authored values when its measurement changes.

Enums

SafeAreaEdge

The window safe-area edge reserved by a DynamicIslandSpacer.

Variants

Top

Space below the status bar or top cutout.

Right

Space left of a right-side cutout in landscape.

Bottom

Space above the home indicator.

Left

Space right of a left-side cutout in landscape.

layout::resizable

Structs

Resizable

Divides slotted content into resizable horizontal or vertical sections.

dividers contains positions along the main axis. With n dividers, Resizable expects n + 1 slot children.

Properties

dividers

Type: Property<Vec<Size>>

Divider positions along the main axis.

direction

Type: Property<ResizableDirection>

Whether sections are split horizontally or vertically.

Enums

ResizableDirection

Axis direction for a Resizable split.

Variants

Vertical

Split content into top-to-bottom sections.

Horizontal

Split content into left-to-right sections.

layout::stacker

Structs

ContainerReflowTransition

Reflow animation applied to surviving children when the stack layout changes.

Properties

kind

Type: Property<ContainerReflowTransitionKind>

Which reflow animation source to use.

frames

Type: Property<u64>

Duration in frames for Ease.

curve

Type: Property<ContainerReflowCurve>

Curve used for Ease.

name

Type: Property<String>

Reserved for a future named motion-curve lookup when kind is Named.


Stacker

Stacker lays out a series of nodes either vertically or horizontally (i.e. a single row or column) with a specified gutter in between each node. Stackers can be stacked inside of each other, horizontally and vertically, along with percentage-based positioning and Transform2D.anchor to compose any rectilinear 2D layout.

Properties

direction

Type: Property<StackerDirection>

The direction the stacker should flow its cells

gutter

Type: Property<Size>

Spacing between cells

autosize

Type: Property<bool>

When true, the stacker uses content-child bounds to autosize its cells and, when possible, its own bounds as well.

Stacker interprets plain autosize=true as "autosize the extending axis only" (y for vertical stacks, x for horizontal stacks). Use autosize_x / autosize_y to override those per-axis defaults.

The underlying shrink-sizing pass only applies when the measured axis can be resolved without parent-size cycles, so percent-sized children continue to use the existing top-down layout behavior unless the corresponding stacker axis is already explicit.

autosize_x

Type: Property<Option<bool>>

Optional override for whether autosize manages the x axis.

autosize_y

Type: Property<Option<bool>>

Optional override for whether autosize manages the y axis.

sizes

Type: Property<Vec<Option<Size>>>

Size of each cell, by index. None-values (or array-index out-of-bounds values) will fall back to computed, equal-sizing

exit_mode

Type: Property<ContainerExitMode>

Whether exiting children stay in normal stack flow or hold their previous frame as ghosts.

reflow_transition

Type: Property<ContainerReflowTransition>

How surviving children should move when the stack's layout changes.

Defaults to Snap so Stackers remain a stable layout primitive unless reflow motion is explicitly requested.

Enums

ContainerExitMode

Whether exiting children remain in normal layout flow or become ghosts.

Variants

Flow

Keep exiting children in the stack's in-flow layout until their out-transition finishes.

Ghost

Hold exiting children at their previous frame as overlays while the remaining children resolve layout without them.


ContainerReflowCurve

Easing curve used by ContainerReflowTransitionKind::Ease.

Variants

Linear
Hold
InQuad
OutQuad
InOutQuad
InBack
OutBack
InOutBack

ContainerReflowTransitionKind

Which reflow animation source to use when children move to new stack positions.

Variants

Snap

Snap immediately to the new layout.

Ease

Use a duration and easing curve.

Named

Reserved for a future named motion-curve lookup in the current component scope.


StackerDirection

Flow direction for a Stacker.

Variants

Vertical

Stack children top-to-bottom.

Horizontal

Stack children left-to-right.

layout::table

Structs

Cell

Selects one cell from a parent Table.

Properties

x

Type: Property<usize>

Zero-based column index.

y

Type: Property<usize>

Zero-based row index.


Col

Selects one column from a parent Table.

Properties

x

Type: Property<usize>

Zero-based column index.


Row

Selects one row from a parent Table.

Properties

y

Type: Property<usize>

Zero-based row index.


Span

Selects a rectangular region from a parent Table.

Properties

x

Type: Property<usize>

Zero-based column index.

y

Type: Property<usize>

Zero-based row index.

w

Type: Property<usize>

Number of columns to span.

h

Type: Property<usize>

Number of rows to span.


Table

A simple grid container for positioning children by rows and columns.

Table establishes row/column counts in local store. Child components such as Row, Col, Cell, and Span read that context to size and position their own slotted content.

Properties

rows

Type: Property<usize>

Number of table rows.

columns

Type: Property<usize>

Number of table columns.

media

Submodules

media::image

Structs

Image

A GPU/canvas-rendered image decoded by the active chassis.

Image draws into the node bounds and participates in the same canvas rendering path as vectors. Use NativeImage when a platform-native image element is preferable. On WGPU and browser Piet, common opacity fades the composed canvas subtree; source pixel alpha remains part of the content. Native surfaces fade separately.

Properties

source

Type: Property<ImageSource>

Image source: empty, URL, or raw RGBA data.

fit

Type: Property<ImageFit>

How the image should fit into this node's bounds.

Enums

ImageFit

Image fit/layout options.

Variants

Paint

Scale the image to fill its bounds, possibly clipping part of the image.

Fit

Scale the image to fit within its bounds without clipping, possibly leaving empty space.

Stretch

Stretch the image to exactly match the container.


ImageSource

Source data for an Image.

In Pax templates, a string in an ImageSource context is shorthand for [ImageSource::Url]:

<Image source="assets/spaceship.png" />
<Image source={avatar_url} />

[ImageSource::Url] and [ImageSource::Data] remain available as explicit constructor forms. Unlike the list shorthands used by compound Pax types, the URL shorthand has no positional ("magic index") fields: the entire string is the URL or chassis-relative asset path.

Variants

Empty

No image.

Url(String)

Image loaded from a URL/path understood by the chassis.

Data(usize, usize, Vec<u8>)

Raw RGBA image data: width, height, and bytes where len = width * height * 4.

media::native_image

Structs

NativeImage

A platform-native image. This will be managed by Pax's compositor as a platform-specific host for an image, for example a DOM <img> element on the web, or a UIImageView on iOS. NativeImage is an alternative to using pax_std::media::Image, with various trade-offs, where the latter is managed as a GPU texture and drawn on a canvas surface.

Properties

url

Type: Property<String>

Image URL/path understood by the chassis.

fit

Type: Property<ImageFit>

How the image should fit into this node's bounds.

media::youtube_video

Structs

YoutubeVideo

A web-based video player that can play YouTube videos given an embed URL.

Properties

url

Type: Property<String>

YouTube embed URL, such as https://www.youtube.com/embed/o8pkhbyb1Yo.

reference

Submodules

reference::example_host

Structs

ExampleHost

A display wrapper for documentation/demo examples.

ExampleHost renders every projected child with slot() and offers an optional source drawer driven by an explicit sources manifest.

Properties

title

Type: Property<String>

Title shown in the source drawer.

sources

Type: Property<Vec<ExampleSource>>

Explicit source files related to the projected example subtree.

selected_source

Type: Property<usize>

Selected source index for the drawer tabs.

drawer_open

Type: Property<bool>

Whether the source drawer is visible.

Implementations

on_pre_render
pub fn on_pre_render(&mut self, ctx: &NodeContext)

Keeps the animated drawer progress synchronized with external state writes.

on_resize_mouse_down
pub fn on_resize_mouse_down(&mut self, ctx: &NodeContext, event: Event<MouseDown>)

Starts dragging the source divider when pressed near the handle.

on_resize_mouse_move
pub fn on_resize_mouse_move(&mut self, ctx: &NodeContext, event: Event<MouseMove>)

Updates the fixed-sum preview/source split while dragging.

on_resize_mouse_out
pub fn on_resize_mouse_out(&mut self, ctx: &NodeContext, _event: Event<MouseOut>)

Restores the cursor after leaving the source divider.

on_resize_mouse_over
pub fn on_resize_mouse_over(&mut self, ctx: &NodeContext, _event: Event<MouseOver>)

Shows the platform resize cursor over the source divider.

on_resize_mouse_up
pub fn on_resize_mouse_up(&mut self, ctx: &NodeContext, _event: Event<MouseUp>)

Ends source divider dragging.

on_split_mouse_down
pub fn on_split_mouse_down(&mut self, ctx: &NodeContext, _event: Event<MouseDown>)

Starts dragging from the explicit source divider hit target.

toggle_drawer
pub fn toggle_drawer(&mut self, _ctx: &NodeContext, _event: Event<Click>)

Toggles the source drawer.


ExampleSource

One source file shown by ExampleHost.

Properties

label

Type: String

File label shown in drawer tabs.

language

Type: String

Language label shown in the drawer.

code

Type: String

Source code contents.

shaders

Experimental shader containers for GPU-rendered subtrees.

These primitives use the public InstanceNode::shader_effect extension point, just as application-defined effects do. Requires the WGPU renderer. Enable the opt-in shaders Cargo feature on pax-kit or pax-std.

Submodules

shaders::gaussian_blur

Structs

GaussianBlur

Experimental Gaussian blur of GPU-rendered descendants.

Requires WGPU and captures vectors and renderer-backed images. Native descendants are unsupported; native overlap masks retain the descendants' original coverage.

Properties

radius

Type: Property<f64>

Standard deviation in local logical pixels, clamped to 0–48. Defaults to zero (identity).

reference

Type: Property<bool>

Force full-resolution separable convolution. Defaults to false, using adaptive resolution.

shaders::impressionist

Structs

Impressionist

Experimental painterly filter with deterministic marks anchored to the container.

Requires WGPU and captures vectors and renderer-backed images. Native descendants are unsupported. Source colors update independently of the procedural brush motion.

Properties

mode

Type: Property<ImpressionistMode>

Brush treatment. Defaults to ImpressionistMode::Basic.

phase

Type: Property<f64>

Motion position, initially zero. Drives mark drift, breathing, and VanGogh's evolving swirls. Source content can animate independently; a fixed phase freezes the procedural motion. Advance at a chosen rate for speed control, or bind a timeline to scrub/ease/reverse motion.

seed

Type: Property<u32>

Deterministic mark seed. Defaults to zero.

size

Type: Property<f64>

Brush scale in local logical pixels, clamped to 4–32. Each mode applies its own mark proportions. Also scales directional patterns, including VanGogh's whorls. Defaults to zero (effective size 4).

Enums

ImpressionistMode

Procedural brush treatments inspired by different painting techniques.

These change mark construction and pigment handling, not scene composition. All modes retain seeded anchors on the surface; marks move around those anchors with phase.

Variants

Basic

The original three scales of softly layered, oval brush marks.

Gauguin

Broad directional sweeps, simplified color planes, and quiet bristle texture.

VanGogh

Phase-driven evolving flows with pronounced light and shadow along paint ridges.

Signac

Closely packed rectangular dabs with interwoven pale and chromatic pigment.

Cezanne

Varied patches of parallel marks and nested arcs, with broken, wispy contours.

Seurat

Fine, dense, rounded points of color with restrained pale pigment.

pax-kit

The application-facing Pax facade: components, properties, runnable!, and platform hosting through one dependency.

Starters enable standalone (hosting) and designtime by default. On macOS, cargo run runs the native application. init! and init_with! initialize in the current native process, without parsing arguments or starting development tools. Use pax-cli for serving, template reload, and platform packaging/deployment. Macros compile the UI without an application build script; merely building or depending on a UI library never launches it.

Submodules

host

Native application entry and Apple view embedding. Enable the host feature in the executable, normally through standalone = ["pax-kit/host"] in the application's Cargo manifest. designtime enables runtime inspection and template changes; it does not start a development server. Production packaging excludes designtime.

fn main() -> Result<(), pax_kit::host::Error> {
    pax_kit::init!()?.run()
}

Native application entry and embedding through the Pax facade.

init! and init_with! initialize in the calling process. They do not parse application arguments or start build tools, development servers, or simulators. Use pax-cli to package applications and run development sessions.

Structs

Application

A prepared native application in the calling process.

Initialization invokes the selected root initializer on the main thread. Consume this value to run a standalone application or mount in a host view.

Implementations

mount
pub fn mount(self, host: ViewHost<'_>) -> Result<MountedApp<'_>, Error>

Mount an explicitly initialized native application in a caller-owned view.

run
pub fn run(self) -> Result<(), Error>

Enter the native application loop in the calling process.

run_with
pub fn run_with(self, options: WindowOptions) -> Result<(), Error>

Run a native macOS application with explicit window options.

pax-tokio

Explicit Tokio ownership and scoped tasks for native Pax applications.

Construct your runtime, then pass it and cx.lifetime() to [Executor::owned] from the root initializer. The adapter uses that exact runtime and retains cleanup until workers finish. UI callbacks do not implicitly enter Tokio.

Structs

BoundTasks

UI-local bound executor. Each spawn creates an independently cancellable operation below the owner. Keep a component-bound helper from on_mount when later event contexts belong to controls inside that component.

Implementations

spawn
pub fn spawn(&self, future: impl Future + Send + 'static) -> Result<TaskControl, AsyncError>

Monitor ordinary work. Raw Property clones captured here remain writable after cancellation; use spawn_into for revocable result publication.

spawn_into
pub fn spawn_into<T: SharedPropertyValue>(&self, target: &Property<T>, future: impl Future + Send + 'static) -> Result<TaskControl, AsyncError>

Publish the returned value only if this task's operation is still live. The future receives no raw clone of the target.

spawn_with_completion
pub fn spawn_with_completion<T: Send + 'static>(&self, future: impl Future + Send + 'static, callback: impl FnOnce(TaskOutcome<T>) + 'static) -> Result<TaskControl, AsyncError>

Return a result or task panic to a UI-local closure, never running that closure inline on the executor. Cancellation suppresses queued delivery.


Executor

An explicitly supplied Tokio runtime bound to an application's lifetime. Cloning this client shares the same runtime; dropping a client does not end application authority. Closing the application cancels work and retains the runtime on its owner thread until shutdown is acknowledged.

Implementations

borrowed
pub fn borrowed(handle: Handle, lifetime: &AppContext) -> AppResult<Self>

Borrow a continuously driven runtime. Its external owner must outlive cleanup; this adapter never shuts down that runtime.

for_application
pub fn for_application(&self, app: &AppContext) -> Result<BoundTasks, AsyncError>

Bind work to this application lifetime.

for_node
pub fn for_node(&self, node: &impl AsyncScopeSource) -> Result<BoundTasks, AsyncError>

Bind work to the exact node supplied by the caller.

for_scope
pub fn for_scope(&self, scope: AsyncScope) -> Result<BoundTasks, AsyncError>

Bind work to an explicit operation or other scope.

handle
pub fn handle(&self) -> Result<Handle, AsyncError>

Access the exact supplied runtime for advanced, untracked work.

owned
pub fn owned(runtime: Runtime, lifetime: &AppContext) -> AppResult<Self>

Adopt this exact multithread runtime without changing its configuration. The lifetime must be open. Construct inside the cartridge initializer; never pass a Rust Runtime or Handle through the native cartridge ABI.

pax-web-async

Scoped browser futures with the same publication/delivery contracts as Pax's native adapter. This crate does not provide Tokio networking in browser Wasm.

Structs

BoundTasks

Local bound executor. Dropping a task control does not detach its lifetime.


Executor

Explicitly owned browser executor. Futures stay on the browser's thread; component cancellation aborts their polling and revokes result authority.

Implementations

new
pub fn new(lifetime: &AppContext) -> Result<Self, AsyncError>

Attach local browser futures to the supplied application lifetime.

pax-host

Apple process entry and mounting across a separate cartridge ABI. Applications use pax_kit::init! and pax_kit::init_with!; options, errors, and embedding types live in pax_kit::host. Enable pax-kit/host without adding a direct pax-host dependency. Cargo builds the SDK for native hosting. The CLI provides bundles, development servers, and iOS deployment; initialization itself stays in the calling native process.

fn main() -> Result<(), pax_kit::host::Error> {
    pax_kit::init!()?.run()
}

Submodules

Structs

Error

An initialization, native application loop, or mounting failure.


InitOptions

Copied application inputs for the separately linked cartridge.

Properties

startup_data

Type: Vec<u8>

Application-defined encoding, at most 64 KiB. The root initializer reads these bytes through InitContext::startup_data().

entry

Type: Option<String>

Canonical root path from runnable!; required for multiple distinct roots.

native

Apple process entry, view ownership and asynchronous mount observations.

Structs

Completion

A readiness/cleanup observer. It may be dropped without cancelling cleanup.


MountedApp

An owned single-region mount. Drop or close removes only Pax's child hierarchy and revokes its work. Cleanup continues through host wakeups even if every observer is dropped or the region is hidden.

Implementations

close
pub fn close(&self)

Start close without blocking the host thread.

closed
pub fn closed(&self) -> Completion

Observe cleanup acknowledgement. Polling does not drive shutdown.

ready
pub fn ready(&self) -> Completion

Observe host readiness, including the property wake route and viewport.

request
pub fn request(&self, bytes: &[u8]) -> Request

Send an application-defined request and observe its reply without blocking the UI thread. Each buffer is limited to 64 KiB and copied across the boundary. Close rejects queued requests; dropping this future discards observation without cancelling already accepted work.


PreparedApp

A prepared cartridge, confined to the platform's main thread. Consume it once with run or mount. Dropping it starts retained asynchronous cleanup.

Implementations

mount
pub fn mount<'a>(self, host: ViewHost<'a>) -> Result<MountedApp<'a>, Error>

Attach the full Pax hierarchy, including native controls, to a region of the caller's view. The caller continues to own its window and event loop.

run
pub fn run(self) -> Result<(), Error>

Enter the platform's standalone SwiftUI application loop on the main thread. On iOS this normally does not return. Configure process services before calling this; cartridge-local services belong in its initializer.

run_with
pub fn run_with(self, options: WindowOptions) -> Result<(), Error>

Enter the standalone macOS loop with an explicit window policy.


Request

Observation of one host request. Poll on the host's UI executor.


ViewHost

A caller-owned native view and (on iOS) its containing view controller. This borrow prevents the Rust wrapper from outliving the supplied owner.

Implementations

from_raw
pub unsafe fn from_raw<T>(_: &'a mut T, view: NonNull<c_void>, controller: *mut c_void) -> Self

Wrap a native container. owner must own the NSView on macOS, or both UIView and UIViewController on iOS, for the entire mount lifetime.

Safety

Pointers must have those Objective-C types, belong to the process main thread, and remain valid until the mount is dropped. On iOS the controller must own the container in its view hierarchy. Do not reparent a live mount.


WindowOptions

Standalone macOS window policy, expressed in logical points. iOS/iPadOS windows are controlled by their scene; embedded mounts use their host bounds.

Properties

title

Type: Option<String>

None preserves the application's bundle display name.

size

Type: (f64, f64)

minimum_size

Type: (f64, f64)

resizable

Type: bool

Functions

init

pub fn init() -> Result<PreparedApp, Error>

Prepare the bundled cartridge and invoke its typed root initializer once. Must be called on the process main thread. No window or nested event loop is created. Initializers should return a Loading root and stage asynchronous work; staged work becomes eligible when the host is ready.


init_with

pub fn init_with(options: InitOptions) -> Result<PreparedApp, Error>

Prepare a cartridge with explicit encoded inputs. No Rust allocation or object identity is shared with the cartridge. See [init] for lifecycle.

Maintainer Reference

This reference is for work on Pax itself and for extensions that need its lower-level interfaces. It covers the language, compiler representation, runtime, platform messages, and renderer. Application authors can usually stay with the guide chapters and public API reference.

For a first look inside the engine, read How Pax Runs. Its interactive architecture schematic connects the compiler, scene graph, reactive runtime, renderer and chassis; select a part to follow its responsibilities and source references. If you are implementing an element through runtime hooks, begin with Primitives, then use the runtime reference and source to follow the relevant lifecycle and rendering contract.

Source contracts and design notes

The generated pages below describe Rust declarations and their documentation comments at this source revision. Internal interfaces evolve with the engine; inspect the implementation and tests for the Pax version you are extending. In particular, check debug and release paths when changing information that crosses the compiler/runtime boundary.

The Runtime and Cartridge Notes are a historical design appendix. They include proposed directions and older packaging models; their status is different from a tested current API. Internal design records elsewhere in the repository likewise need to be read in the context of their implementation status. For current build artifacts, use Targets, Build, and Deployment.

Crates

The crate names follow the engine boundary: pax-language handles Pax syntax and expressions, pax-manifest holds program representations, pax-runtime executes the scene, pax-message defines platform messages, and pax-gpu implements the GPU renderer.

pax-language

Submodules

Structs

PaxParser

Pest parser generated from the Pax grammar.

Enums

Rule

Variants

EOI

End-of-input

WHITESPACE
comment
pax_component_definition

/// ////// ////// BEGIN TEMPLATE ///

root_tag_pair
any_tag_pair
open_tag
closing_tag
self_closing_tag
matched_tag
inner_nodes
identifier
pascal_identifier
event_id
attribute_key_value_pair
class_attribute
class_value
class_string_list
attribute_transition_binding
attribute_event_binding
transition_id
transition_binding_value
transition_inline_timeline_value
transition_inline_timeline_body
double_binding
any_template_value
node_inner_content
string
inner
char
settings_block_declaration

/// ////// ////// BEGIN SETTINGS ///

settings_block_element
settings_conditional
settings_if_branch
settings_else_if_branch
settings_else_branch
settings_conditional_body
selector_block
literal_object
selector
settings_key_value_pair
settings_event_binding
settings_key
settings_value
timeline_block_declaration

/// ////// ////// BEGIN TIMELINES ///

timeline_block_setting
timeline_block_setting_value
timeline_selector_block
timeline_selector_body
timeline_property_key_value_pair
timeline_inline_value
timeline_track
timeline_keyframe
timeline_keyframe_value
timeline_marker
timeline_percent
timeline_duration
timeline_duration_unit
timeline_easing_curve
timeline_target
timeline_local_target
timeline_symbol
gradient_inline_value

/// ////// ////// BEGIN GRADIENTS ///

gradient_body
gradient_shape_block
gradient_shape_key
gradient_shape_settings
gradient_shape_setting
gradient_shape_setting_value
gradient_stop
gradient_stop_marker
gradient_stop_value
literal_function
silent_comma
function_list
literal_value
literal_boolean
literal_some
literal_none
literal_option
literal_number_with_unit
literal_number
literal_number_integer
literal_number_float
literal_number_unit
literal_tuple
literal_tuple_access
literal_list
template_list_value
literal_list_access
literal_enum_value
literal_enum_args_list
literal_color

/// ////// ////// BEGIN COLORS ///

literal_color_space_func
literal_color_channel
xo_color_space_func
literal_color_const
expression_body

/// ////// ////// BEGIN EXPRESSIONS This sub-grammar describes PAXEL, the Pax Expression Language ///

expression_ternary
expression_coalesce
expression_binary
expression_wrapped
expression_grouped
expression_grouped_unit
xo_primary
xo_prefix
xo_neg
xo_bool_not
xo_infix
xo_add
xo_bool_and
xo_bool_or
xo_div
xo_exp
xo_mod
xo_mul
xo_rel_eq
xo_rel_gt
xo_rel_gte
xo_rel_lt
xo_rel_lte
xo_rel_neq
xo_sub
xo_null_coalesce
xo_tern_then
xo_tern_else
xo_range
xo_range_exclusive
xo_literal
xo_object
xo_object_settings_key_value_pair
xo_symbol
xo_tuple
xo_list
xo_enum_or_function_call
xo_enum_or_function_args_list
statement_control_flow

/// ////// ////// BEGIN CONTROL FLOW ///

statement_if
statement_if_branch
statement_else_if_branch
statement_else_branch
statement_for
statement_slot
statement_for_predicate_declaration
statement_for_source
statement_for_key

Functions

parse_pax_err

pub fn parse_pax_err(expected_rule: Rule, input: &str) -> Result<Pair<'_, Rule>, Error<Rule>>

Parse a string against a Pax grammar rule, preserving the structured pest error.


parse_pax_pairs

pub fn parse_pax_pairs(expected_rule: Rule, input: &str) -> Result<Pairs<'_, Rule>, Error<Rule>>

Parse a string into pest pairs for a Pax grammar rule.


parse_pax_str

pub fn parse_pax_str(expected_rule: Rule, input: &str) -> Result<Pair<'_, Rule>, String>

Parse a string against a single Pax grammar rule, returning a human-readable error string.

deserializer

Submodules

Structs

PaxDeserializer

Serde bridge from Pax literal grammar nodes into runtime values.

Properties

ast

Type: Pair<'de, Rule>

Implementations

from
pub fn from(ast: Pair<'de, Rule>) -> Self

Wrap a pest pair as a deserializer.

Functions

from_pax

pub fn from_pax(str: &str) -> Result<PaxValue>

Deserialize a Pax literal string into a runtime PaxValue.


from_pax_ast

pub fn from_pax_ast(ast: Pair<'_, Rule>) -> Result<PaxValue>

Deserialize a parsed literal AST node into a runtime PaxValue.

deserializer::error

Enums

Error

Error emitted while converting Pax literal syntax into runtime values.

Variants

Message(String)
UnsupportedType(String)

Type Aliases

Result

Result type used by the Pax literal deserializer.

formatting

Structs

FormatSummary

Result of formatting a file or directory tree.

Properties

files_checked

Type: usize

Pax-bearing source files inspected.

changed_files

Type: Vec<PathBuf>

Files whose canonical representation differs from disk.

Functions

format_file

pub fn format_file(file_path: &str) -> Result<(), Report>

Format either a .pax file or an inlined Pax template inside a Rust file.


format_path

pub fn format_path(path: &Path, check: bool) -> Result<FormatSummary, Report>

Format a .pax/.rs file or every Pax-bearing source file below a directory.

Directory traversal skips generated and dependency directories (.git, .pax, target, and node_modules). When check is true, files are inspected but not written, and changed paths are returned in [FormatSummary::changed_files].


format_pax_template

pub fn format_pax_template(code: String) -> Result<String, Report>

Format one Pax template string.

helpers

Structs

InlinedTemplate

Source span and contents for a Pax template embedded in an #[inlined(...)] attribute.

Properties

struct_name

Type: String

start

Type: (usize, usize)

end

Type: (usize, usize)

template

Type: String


InlinedTemplateFinder

AST visitor that extracts #[inlined(...)] templates from #[pax] structs.

Properties

file_contents

Type: String

templates

Type: Vec<InlinedTemplate>

Implementations

new
pub fn new(file_contents: String) -> Self

Prepare a finder for one Rust source file.

Functions

clear_inlined_template

pub fn clear_inlined_template(file_path: &str, pascal_identifier: &str)

Replace a matching #[inlined(...)] template with an empty template body.


get_substring_by_line_column

pub fn get_substring_by_line_column(input: &str, start: (usize, usize), end: (usize, usize)) -> Option<String>

Extract a source substring addressed by one-indexed line/column coordinates.


replace_by_line_column

pub fn replace_by_line_column(input: &str, start: (usize, usize), end: (usize, usize), replacement: String) -> Option<String>

Replace a source span addressed by one-indexed line/column coordinates.

interpreter

Submodules

Structs

PaxIdentifier

Symbol reference in a PAXEL expression.

Properties

name

Type: String

Implementations

new
pub fn new(name: &str) -> Self

Construct an identifier from its source spelling.


PaxInfix

Binary infix operation with left and right expression operands.

Implementations

lhs
pub fn lhs(&self) -> &PaxExpression

Left-hand expression.

operator_name
pub fn operator_name(&self) -> &str

Name of the infix operator.

rhs
pub fn rhs(&self) -> &PaxExpression

Right-hand expression.


PaxNullCoalesce

Short-circuiting fallback expression. Some(value) ?? fallback evaluates to value, None ?? fallback evaluates the fallback, and non-option left operands pass through unchanged.

Implementations

lhs
pub fn lhs(&self) -> &PaxExpression

Left-hand expression.

rhs
pub fn rhs(&self) -> &PaxExpression

Right-hand fallback expression.


PaxOperator

Parsed operator token, stored by display name.

Implementations

name
pub fn name(&self) -> &str

Source spelling for this operator.

new
pub fn new(name: impl Into<String>) -> Self

Construct an operator from its source spelling.


PaxPostfix

Postfix operation node.

Implementations

lhs
pub fn lhs(&self) -> &PaxExpression

Left-hand expression.

operator_name
pub fn operator_name(&self) -> &str

Name of the postfix operator.


PaxPrefix

Prefix operation such as numeric negation or boolean not.

Implementations

operator_name
pub fn operator_name(&self) -> &str

Name of the prefix operator.

rhs
pub fn rhs(&self) -> &PaxExpression

Right-hand expression.


PaxTernary

Conditional expression with a boolean condition and selected true/false branch.

Implementations

condition
pub fn condition(&self) -> &PaxExpression

Condition expression.

else_branch
pub fn else_branch(&self) -> &PaxExpression

Expression evaluated when the condition is false.

then_branch
pub fn then_branch(&self) -> &PaxExpression

Expression evaluated when the condition is true.

Enums

PaxAccessor

Access path applied after an identifier, such as .field, .0, or [index].

Variants

Tuple(usize)
List(PaxExpression)
Struct(String)

PaxExpression

PAXEL expression AST node.

Variants

Primary(Box<PaxPrimary>)
Prefix(Box<PaxPrefix>)
Infix(Box<PaxInfix>)
Postfix(Box<PaxPostfix>)
Ternary(Box<PaxTernary>)
NullCoalesce(Box<PaxNullCoalesce>)

Implementations

infix
pub fn infix(lhs: PaxExpression, operator: impl Into<String>, rhs: PaxExpression) -> Self

Construct an infix expression.

null_coalesce
pub fn null_coalesce(lhs: PaxExpression, rhs: PaxExpression) -> Self

Construct a null-coalescing expression.

postfix
pub fn postfix(lhs: PaxExpression, operator: impl Into<String>) -> Self

Construct a postfix expression.

prefix
pub fn prefix(operator: impl Into<String>, rhs: PaxExpression) -> Self

Construct a prefix expression.

ternary
pub fn ternary(condition: PaxExpression, then_branch: PaxExpression, else_branch: PaxExpression) -> Self

Construct a ternary expression.


PaxPrimary

Primary expression forms: literals, symbols, object/list/tuple literals, calls, and ranges.

Variants

Literal(PaxValue)
Grouped(Box<PaxExpression>, Option<PaxUnit>)
Identifier(PaxIdentifier, Vec<PaxAccessor>)
Object(Vec<(String, PaxExpression)>)
FunctionOrEnum(String, String, Vec<PaxExpression>)
Range(PaxExpression, PaxExpression)
Tuple(Vec<PaxExpression>)
List(Vec<PaxExpression>)

PaxUnit

Unit suffix attached to a grouped numeric expression.

Variants

Percent
Pixels
Radians
Degrees
Milliseconds
Seconds
Frames

Functions

compute_paxel

pub fn compute_paxel(expr: &str, idr: Rc<dyn IdentifierResolver>) -> Result<PaxValue, String>

Compute a pax expression to a PaxValue


parse_pax_expression

pub fn parse_pax_expression(expr: &str) -> Result<PaxExpression, String>

Parse a pax expression into a computable AST


parse_pax_expression_from_pair

pub fn parse_pax_expression_from_pair(expr: Pair<'_, Rule>) -> Result<PaxExpression, String>

Parse an already-produced pest pair into a PAXEL expression AST.

interpreter::property_resolution

Traits

DependencyCollector

Walk an expression tree and report the symbols that should dirty-watch it.


IdentifierResolver

Trait for resolving identifiers to values This is implemented by RuntimePropertyStackFrame

pax-runtime

Submodules

api

Structs

NodeContext

This runtime owner stays on the UI thread. Transfer properties or delivery handles to workers instead.

fn requires_send<T: Send>() {}
requires_send::<pax_runtime::api::NodeContext>();

Runtime context passed into user component lifecycle methods and event handlers.

For an inline binding, Rust self belongs to the component that authored the binding, while this context describes the child receiving the call. Bounds, expression stack access, subscriptions, and async lifetime remain target-local. Component-level bindings receive their own component's state and context.

Child-related fields intentionally separate semantic payload from engine transport:

  • projected_children is the raw transport family used by Slot
  • received_children is the normalized semantic payload that this node should treat as content from its caller
  • retained_received_children are former received children kept alive only so @out transitions can finish

A node's own private template or primitive-assembled structure is intentionally not surfaced here as a first-class "child family" for container consumers.

Properties

expanded_node

Type: Weak<ExpandedNode>

slot_index

Type: LocalProperty<Option<usize>>

slot index of this node in its container

expression_stack

Type: Rc<RuntimePropertiesStackFrame>

containing_component

Type: Weak<ExpandedNode>

Reference to the ExpandedNode of the component containing this node

elapsed_frames

Type: LocalProperty<u64>

The current global engine frame count.

elapsed_millis

Type: LocalProperty<u64>

The current global engine wall-clock time in milliseconds.

gyro

Type: LocalProperty<Gyro>

Current device orientation sensor reading.

accel

Type: LocalProperty<Accel>

Current device accelerometer reading.

bounds_parent

Type: LocalProperty<(f64, f64)>

The bounds of this element's immediate container (parent) in px

bounds_self

Type: LocalProperty<(f64, f64)>

The bounds of this element in px

measured_size

Type: LocalProperty<Option<(f64, f64)>>

Measured bounds resolved by the chassis or container layout for this node.

subtree_layout_hull

Type: LocalProperty<LayoutHull>

Node-local subtree layout hull published by the engine for container measurement.

platform

Type: Platform

Current platform (Web/Native) this app is running on

os

Type: OS

Current os (Android/Windows/Mac/Linux) this app is running on

target

Type: LocalProperty<TargetInfo>

Derived target facts for platform/OS checks.

viewport

Type: LocalProperty<Viewport>

Derived viewport facts for size and orientation checks.

projected_children_count

Type: LocalProperty<usize>

The number of projected children available to this node.

This is the raw transport count used by slot-driven implementations. Container-style consumers usually want received_children_count instead.

node_transform_and_bounds

Type: TransformAndBounds<NodeLocal, Window>

The transform of this node in the global coordinate space

projected_children

Type: LocalProperty<Vec<Rc<ExpandedNode>>>

Children projected into this node from the containing component.

Projection is an engine transport mechanism. Consumers that want the semantic payload owned by this node should prefer received_children.

projected_children_changed

Type: LocalProperty<()>

A structural invalidation signal for projected children.

received_children

Type: LocalProperty<Vec<Rc<ExpandedNode>>>

Semantic payload children received by this node from its caller.

This is the canonical "content" view for container-style logic. It excludes private encapsulated implementation children and also excludes exit-retained payload nodes, which instead appear in retained_received_children.

received_children_count

Type: LocalProperty<usize>

Convenience count derived from received_children.

received_children_changed

Type: LocalProperty<()>

A structural invalidation signal for received_children.

Prefer this or received_children itself for structural subscriptions that must react to reorders as well as insertions and removals.

retained_received_children

Type: LocalProperty<Vec<Rc<ExpandedNode>>>

Received children retained only so exit transitions can finish.

These are no longer part of the active semantic payload, but some containers still need to place them as ghosts or overlays while their @out transitions run.

retained_received_children_changed

Type: LocalProperty<()>

A structural invalidation signal for retained_received_children.

Implementations

application
pub fn application(&self) -> AppContext

Access the owning application lifetime, cancellation and error reporting.

async_scope
pub fn async_scope(&self) -> Result<AsyncScope, AsyncError>

Lifetime of this exact context node, ending at actual unmount. Retain a component mount scope when later handlers receive child-control contexts. A saved context cannot acquire the scope of a later mount of that node.

clear_subscriptions
pub fn clear_subscriptions(&self)

Remove all subscriptions registered on this node.

dispatch_event
pub fn dispatch_event(&self, identifier: &'static str) -> Result<(), String>

Queue a named custom event from this component for dispatch at the end of the tick.

elapsed_time_millis
pub fn elapsed_time_millis(&self) -> u128

Milliseconds elapsed according to the chassis-provided clock.

get_node_interface
pub fn get_node_interface(&self) -> Option<NodeInterface>

Return the interface for this node's containing component, when present.

get_screenshot_map
pub fn get_screenshot_map(&self) -> Rc<RefCell<HashMap<u32, ScreenshotData>>>

Shared map where completed screenshot captures are published by id.

local_point
pub fn local_point(&self, p: Point2<Window>) -> Point2<NodeLocal>

Convert a window-space point into this node's local coordinate space, including any presentation offsets inherited from ancestor scrollers.

pub fn navigate_to(&self, url: &str, target: NavigationTarget)

Ask the chassis to navigate to a URL.

On web targets, same-origin navigation in the current tab can be handled through the browser History API and routed back into Pax without a full page reload. Cargo metadata's [package.metadata.pax.web].server_owned_prefixes delegates matching paths to ordinary browser navigation before changing application history. Other targets use the active chassis navigation behavior.

screenshot
pub fn screenshot(&self, id: u32)

Request a screenshot capture from the chassis, keyed by caller-provided id.

set_cursor
pub fn set_cursor(&self, cursor: CursorStyle)

Ask the chassis to display the requested cursor over the app surface.

subscribe
pub fn subscribe(&self, dependencies: &[UntypedProperty], f: impl Fn() + 'static)

Attach a dependency subscription whose callback runs when any dependency dirties.

provide_store
pub fn provide_store<T: Store>(&self, store: T) -> Result<(), StoreError>

Publish a store on this context's mounted node, keyed by its concrete Rust type.

Register in the component's own mount handler to give each component instance an independent provider. In an inline descendant handler, the context identifies that descendant even when Rust self is its authoring component. The context's node, not self, owns the registration.

The owner and its logical descendants can find the value with with_store. This includes private template children and caller-supplied children; private wrappers around a slot do not change projected content's ancestry. Siblings and ancestors cannot discover this registration.

Providing the same type again on the same owner replaces its entry for future lookups. Previously cloned handles keep their original state; the registry is not reactive. Prefer updating properties in a stable store. Providers are cleared at final unmount, after unmount handlers run.

Returns StoreError::ExpiredScope for an unmounted or stale context, or StoreError::BorrowConflict if the entry being replaced is borrowed.

with_store
pub fn with_store<T: Store, V>(&self, f: impl FnOnce(&mut T) -> V) -> Result<V, StoreError>

Borrow the nearest provider of T, starting with this context's node.

Lookup follows logical template ancestry, independently of expression scope and render parents. A nearer same-type provider shadows an outer one; unrelated types remain visible. It never searches siblings or descendants. Two consumers share state when they resolve the same provider.

The closure exclusively borrows that store for its duration. Clone a needed Property handle inside the closure, then read or update it after the borrow ends. This avoids conflicts if subsequent work looks up the same store. The returned value can be a handle or an owned snapshot, but cannot borrow from the store.

Returns StoreError::Missing when no provider exists, StoreError::ExpiredScope when the context or an ancestor's mount has ended, and StoreError::BorrowConflict when the nearest provider is already borrowed. A conflict or expired scope never falls back to an outer provider. Saved contexts cannot discover a later mount's stores.

cartridge

Structs

ComponentPropertyDescriptor

Runtime descriptor for applying all resolved layers of a generated component property.

Properties

name

Type: &'static str

LocalProperty name as it appears in Pax templates and settings.

apply_entries

Type: fn()

Generated applicator for all resolved layers of this property.

Implementations

new
pub const fn new(name: &'static str, apply_entries: fn()) -> Self

Creates a descriptor for a generated component property.


TemplatePropertyPlan

Immutable template inputs shared by common and component property binding. Resolved columns and reactive properties are always node-local; this plan never caches evaluated values or selector matches across nodes or rebinds.

Functions

apply_component_property

pub fn apply_component_property<T>(property: &mut impl PropertyBinding<T>, name: &str, value_definition: &ValueDefinition, stack: &Rc<RuntimePropertiesStackFrame>, timeline_stack: Rc<RuntimePropertiesStackFrame>, build_block: fn()) where T: CoercionRules + PropertyValue + ToPaxValue

Replaces a typed component property with the value produced from a single value definition.


build_component_property

pub fn build_component_property<T>(name: &str, value_definition: &ValueDefinition, stack: &Rc<RuntimePropertiesStackFrame>, timeline_stack: Rc<RuntimePropertiesStackFrame>, build_block: fn()) -> LocalProperty<T> where T: CoercionRules + PropertyValue + ToPaxValue

Builds a typed component property from a single value definition. Callers that apply layered settings should bind $base before calling this helper.


create_new_common_properties

pub fn create_new_common_properties(defined_properties: &BTreeMap<String, ValueDefinition>, stack_frame: &Rc<RuntimePropertiesStackFrame>) -> Rc<RefCell<CommonProperties>>

Creates common properties from a flattened property map.


create_new_common_properties_from_columns

pub fn create_new_common_properties_from_columns(property_columns: &RuntimeResolvedPropertyColumns, stack_frame: &Rc<RuntimePropertiesStackFrame>) -> Rc<RefCell<CommonProperties>>

Creates common properties from resolved columns, preserving layer order so $base can reference each prior layer.


property_columns_from_defined_properties

pub fn property_columns_from_defined_properties(defined_properties: &BTreeMap<String, ValueDefinition>) -> RuntimeResolvedPropertyColumns

Converts a flattened property map into one-entry columns for legacy callers that do not participate in selector/import precedence layering.


stack_with_base

pub fn stack_with_base<T>(stack: &Rc<RuntimePropertiesStackFrame>, base_property: LocalProperty<T>) -> Rc<RuntimePropertiesStackFrame> where T: PropertyValue + ToPaxValue

Returns a stack frame with $base bound to the supplied previous-layer property value.


stack_with_optional_base

pub fn stack_with_optional_base<T>(stack: &Rc<RuntimePropertiesStackFrame>, base_property: LocalProperty<Option<T>>) -> Rc<RuntimePropertiesStackFrame> where T: PropertyValue + ToPaxValue

Returns a stack frame with $base bound to an optional previous common property value, exposing T::default() when the previous layer is None.


update_existing_common_properties

pub fn update_existing_common_properties(expanded_node: &Rc<ExpandedNode>, defined_properties: &BTreeMap<String, ValueDefinition>, stack_frame: &Rc<RuntimePropertiesStackFrame>)

Applies a flattened common-property map to an existing expanded node.


update_existing_common_properties_from_columns

pub fn update_existing_common_properties_from_columns(expanded_node: &Rc<ExpandedNode>, property_columns: &RuntimeResolvedPropertyColumns, stack_frame: &Rc<RuntimePropertiesStackFrame>)

Applies resolved common-property columns to an existing expanded node, preserving layer order so $base can reference each prior layer.

Constants

BASE_SYMBOL

PAXEL symbol bound while resolving a property layer to the value from the preceding layer in that same property's precedence stack.

component

Structs

ComponentInstance

A render node with its own runtime context. Will push a frame to the runtime stack including the specified projected_children and a PaxType properties object. Component is used at the root of applications, at the root of reusable components like Stacker, and in special applications like Repeat where it houses the RepeatItem properties attached to each of Repeat's virtual nodes.

Properties

template

Type: InstanceNodePtrList

timelines

Type: Vec<Rc<RefCell<Timeline>>>


ScrollPosition

Built-in $scroll_position value synthesized for components inside scrollers.

Properties

x

Type: f64

y

Type: f64

Implementations

create_builtin_if_exists
pub fn create_builtin_if_exists(property_scope: Ref<'_, HashMap<String, Variable>>) -> Option<HashMap<String, Variable>>

Create a stack frame containing $scroll_position when scroll properties are present.

conditional

Structs

ConditionalInstance

A special "control-flow" primitive, Conditional (if) allows for a subtree of a component template to be rendered conditionally, based on the value of the property boolean_expression. The Pax compiler handles ConditionalInstance specially with the if syntax in templates.


ConditionalProperties

Contains the expression of a conditional, evaluated as an expression.

Properties

boolean_expression

Type: LocalProperty<bool>

conditional_branches

Type: Vec<LocalProperty<bool>>

container

Container-facing child ontology and geometry seams. Container-facing child ontology and geometry seams.

The runtime carries several child concepts, but container consumers should reason about them on two axes:

  1. Semantic role
    • received_children: the payload a node received from its caller
    • encapsulated implementation children: the node's own private template or primitive-assembled structure
  2. Lifecycle slice of the received payload
    • active received children: present in NodeContext::received_children
    • retained exiting received children: present in NodeContext::retained_received_children

Engine details such as projection still exist, but they are transport mechanisms rather than the primary semantic abstraction.

Structs

ContainerFrame

Parent-local frame assigned by a container to one of its received children.

This behaves like a virtual wrapper node inside the parent: the frame's transform is composed onto the parent transform and its bounds become the child container bounds.

Properties

transform

Type: Transform2<NodeLocal, NodeLocal>

bounds

Type: (f64, f64)

Enums

ReceivedChildrenSource

Engine-internal selector for which child family should be normalized into NodeContext::received_children.

This is a provenance selector, not the semantic API surface.

Owned means the node's received payload already lives in its active child tree.

Projected means the node receives payload from its caller and the runtime threads that payload through projection so it can be consumed by slot(...) within the node's encapsulated implementation.

Variants

Owned
Projected

Traits

Container

Trait for nodes that semantically interpret child content.

Containers should treat NodeContext::received_children as their canonical payload set and NodeContext::retained_received_children as the set of exit-retained payload nodes that may still need placement or transition handling.

Encapsulated implementation children remain a private detail of the node's own template or primitive assembly and should generally not drive container layout logic.

Containers can call this from their existing mount logic to install reactive behavior on top of those normalized views.

Functions

bind_content_measurement_effect

pub fn bind_content_measurement_effect<F>(expanded_node: &Rc<ExpandedNode>, ctx: &NodeContext, listener_name: &'static str, geometry: ContentMeasurementGeometry, extra_deps: &[UntypedProperty], effect: F) where F: Fn(&Rc<ExpandedNode>, &NodeContext) + Clone + 'static

Bind a reactive content-measurement effect to this node.

The effect is re-evaluated after the tree update pass, before occlusion and layer-plan generation, and its dependency list is rebound whenever the normalized received-child list changes.


measure_content_children_forward_extents

pub fn measure_content_children_forward_extents(ctx: &NodeContext) -> (Option<f64>, Option<f64>)

Measure forward autosize extents from received content.


measure_content_children_layout_hull

pub fn measure_content_children_layout_hull(ctx: &NodeContext) -> LayoutHull

Measure the aggregate layout hull contributed by received content in the container's local coordinate space.

Empty content is treated as a zero-sized valid hull so autosized containers can collapse to 0x0 when they have no children.


resolve_axis_autosize

pub fn resolve_axis_autosize(autosize: bool, axis_override: Option<bool>, default_when_enabled: bool) -> bool

Resolve one axis of autosize given the public autosize toggle plus an optional override.


resolve_content_autosize_measurement

pub fn resolve_content_autosize_measurement(ctx: &NodeContext, width_explicit: bool, height_explicit: bool) -> Option<(f64, f64)>

Resolve a node's measured size from its received content.

Explicit axes keep their current container bounds; implicit axes use the measured forward extents when they are valid. If an implicit axis cannot be measured safely, this returns None so the caller can fall back.


resolve_content_autosize_measurement_with_axes

pub fn resolve_content_autosize_measurement_with_axes(ctx: &NodeContext, width_explicit: bool, height_explicit: bool, autosize_width: bool, autosize_height: bool) -> Option<(f64, f64)>

Resolve a node's measured size from received content with explicit per-axis autosize control.


sync_content_autosize

pub fn sync_content_autosize(expanded_node: &Rc<ExpandedNode>, ctx: &NodeContext, enabled: bool)

Update measured_size from received content when autosize is enabled.


sync_content_autosize_with_axes

pub fn sync_content_autosize_with_axes(expanded_node: &Rc<ExpandedNode>, ctx: &NodeContext, autosize_width: bool, autosize_height: bool)

Update measured_size from received content with explicit per-axis autosize control.

engine

Submodules

Structs

Globals

Engine-wide reactive globals exposed to every component frame.

Properties

elapsed_frames

Type: LocalProperty<u64>

elapsed_millis

Type: LocalProperty<u64>

viewport

Type: LocalProperty<TransformAndBounds<NodeLocal, Window>>

gyro

Type: LocalProperty<Gyro>

accel

Type: LocalProperty<Accel>

route_location

Type: LocalProperty<RouteLocation>

browser_allows_scroller_vector_layers

Type: LocalProperty<bool>

browser_allows_nested_scroller_vector_layers

Type: LocalProperty<bool>

platform

Type: Platform

os

Type: OS

target

Type: TargetInfo

get_elapsed_millis

Type: Rc<dyn Fn() -> u128>

Implementations

stack_frame
pub fn stack_frame(&self) -> Rc<RuntimePropertiesStackFrame>

Build the root stack frame containing built-in globals plus internal engine state.


Handler

Runtime event handler thunk generated from template bindings.

Properties

function

Type: fn()

location

Type: HandlerLocation

Implementations

new_component_handler
pub fn new_component_handler(function: fn()) -> Self

Build a handler whose self and context both belong to the target component.

new_inline_handler
pub fn new_inline_handler(function: fn()) -> Self

Build a handler whose self is the component that authored the inline binding. The context still describes the primitive or child component receiving the call.


HandlerRegistry

Map from event key to one or more handlers registered on an instance node.

Properties

handlers

Type: HashMap<String, Vec<Handler>>


PaxEngine

Singleton struct storing everything related to properties computation & rendering

Properties

runtime_context

Type: Rc<RuntimeContext>

root_expanded_node

Type: Option<Rc<ExpandedNode>>

scroller_tiling_policy

Type: ScrollerTilingPolicy

Implementations

Central instance of the PaxEngine and runtime, intended to be created by a particular chassis. Contains all rendering and runtime logic.

activate_application
pub fn activate_application(&self)

Activates the mounted application exactly once. Candidate hosts defer this until their revision is committed.

mount_root_component
pub fn mount_root_component(&mut self, main_component_instance: Rc<ComponentInstance>) -> Rc<ExpandedNode>

Mount a root component tree into an existing runtime kernel.

set_viewport_size
pub fn set_viewport_size(&mut self, new_viewport_size: (f64, f64))

Called by chassis when viewport size changes, e.g. with native window resizes

shutdown
pub fn shutdown(&mut self, reason: StopReason)

Stops new application work, unmounts, then begins retained resource cleanup.

tick
pub fn tick(&mut self) -> Vec<NativeMessage>

Workhorse methods of every tick. Will be executed up to 240 Hz. Three phases:

  1. Expand nodes & compute properties; recurse entire instance tree and evaluate ExpandedNodes, stitching together parent/child relationships between ExpandedNodes along the way.
  2. Compute layout (z-index & TransformAndBounds) by visiting ExpandedNode tree in rendering order, writing computed rendering-specific values to ExpandedNodes
  3. Render: a. find lowest node (last child of last node) b. start rendering, from lowest node on-up, throughout tree
unmount
pub fn unmount(&mut self)

Detach the mounted root component tree, leaving the runtime kernel empty.

Enums

HandlerLocation

Selects the Rust state owner independently of the handler's target NodeContext.

Variants

Inline

A binding on a template element: use the component that authored the element.

Component

A component's own settings or conventional method: use the target component.

engine::layer_surface

Structs

LayerSurfaceEntry

Desired surface geometry for one tile in a layer layout.

Properties

key

Type: String

host_signature

Type: String

Host ownership plus backing-surface generation; changes when the physical surface is replaced.

origin_x

Type: f32

origin_y

Type: f32

replay_priority

Type: i32

surface

Type: LayerSurfaceSize


LayerSurfaceLayout

Desired set of physical surfaces for a logical layer.

Properties

surfaces

Type: Vec<LayerSurfaceEntry>

active

Type: bool


LayerSurfaceSize

Logical and backing-pixel dimensions for one physical surface.

Properties

logical_width

Type: f32

logical_height

Type: f32

surface_width

Type: u32

surface_height

Type: u32

dpr

Type: [f32; 2]


ReplayPriorityEntry

Retargeted surface metadata used to choose replay order without involving backend resources.

Properties

index

Type: usize

priority

Type: i32


SurfaceReplayCoordinator

Shared coordinator for physical-surface replay after a layer layout retarget.

This tracks surface indices and replay regions, not scene-node coverage. The runtime scene index selects nodes; backends apply surface indices to their own renderer objects.

Implementations

remap_surfaces
pub fn remap_surfaces(&mut self, layer: usize, old_indices: &[usize])

Remap pending replay work after eviction or reordering, dropping work for removed surfaces. old_indices[new_index] identifies the surviving renderer in the previous surface vector.

Functions

replay_batches_by_directional_priority

pub fn replay_batches_by_directional_priority(entries: &[ReplayPriorityEntry]) -> Vec<Vec<usize>>

Replay visible retargeted surfaces together, then warm tiles by priority and travel direction.

engine::layer_tiling

Structs

LayerCanvasPlan

Canvas tiling plan for one logical render layer.

Properties

layer_id

Type: usize

active

Type: bool

surfaces

Type: Vec<SurfaceCanvasDescriptor>


SurfaceCanvasDescriptor

One physical canvas surface used to render a logical Pax layer tile.

Properties

id

Type: String

key

Type: String

left

Type: f64

top

Type: f64

width

Type: f64

height

Type: f64

replay_priority

Type: i32

surface_signature

Type: String

transform_signature

Type: String

host_signature

Type: String

Functions

scroller_canvas_plan

pub fn scroller_canvas_plan(layer_id: usize, host_signature: String, content_width: f64, content_height: f64, viewport_width: f64, viewport_height: f64, scroll_x: f64, scroll_y: f64, device_pixel_ratio: f64) -> LayerCanvasPlan

Build a tile window for a scrollable vector layer.


single_surface_plan

pub fn single_surface_plan(layer_id: usize, host_signature: String, width: f64, height: f64) -> LayerCanvasPlan

Build a one-surface plan for layers that do not need tiling.

engine::node_interface

Structs

NodeInterface

Runtime inspection handle for an expanded node.

Implementations

children
pub fn children(&self) -> Vec<NodeInterface>

Mounted child nodes.

containing_component
pub fn containing_component(&self) -> Option<NodeInterface>

Containing component for template scoping and slot ownership.

engine_id
pub fn engine_id(&self) -> ExpandedNodeIdentifier

Runtime-expanded id for this concrete node.

flattened_projected_children_count
pub fn flattened_projected_children_count(&self) -> LocalProperty<usize>

Reactive count of slot children after repeat/conditional flattening.

global_id
pub fn global_id(&self) -> Option<UniqueTemplateNodeIdentifier>

Compiler-global template id for this node, if it originated from a template node.

has_id
pub fn has_id(&self, id: &str) -> bool

Test the template id common property.

instance_flags
pub fn instance_flags(&self) -> InstanceFlags

Static flags from the node's instance.

is_descendant_of
pub fn is_descendant_of(&self, node: &NodeInterface) -> bool

True if this node is below node in the template-parent chain.

is_of_type
pub fn is_of_type<T: ToFromPaxAny>(&self) -> bool

Check whether the node's property object is of type T.

layout_properties
pub fn layout_properties(&self) -> LayoutProperties

Current layout properties after common-property collection.

measured_size
pub fn measured_size(&self) -> Option<(f64, f64)>

Measured bounds reported by a native or text-backed node.

render_parent
pub fn render_parent(&self) -> Option<NodeInterface>

Parent in render traversal order.

subtree_layout_hull
pub fn subtree_layout_hull(&self) -> LocalProperty<LayoutHull>

Node-local subtree hull published by the engine for container measurement.

template_parent
pub fn template_parent(&self) -> Option<NodeInterface>

Parent in template ownership order.

transform_and_bounds
pub fn transform_and_bounds(&self) -> LocalProperty<TransformAndBounds<NodeLocal, Window>>

Reactive transform-and-bounds property for this node.

with_properties
pub fn with_properties<V, T: ToFromPaxAny>(&self, f: impl FnOnce(&mut T) -> V) -> Option<V>

Borrow the node's typed property object if it has the requested type.


NodeLocal

Marker coordinate space for a node's local layout space.

engine::occlusion

Structs

OcclusionBox

Axis-aligned bounds used by the occlusion and native-mask pass.


OcclusionStats

Cumulative native-compositing work, independent of canvas replay counters.

Properties

rebuilds

Type: u64

Full structural/presentation reconciliations.

records_updated

Type: u64

Dirty records refreshed outside structural reconciliation.

masks_evaluated

Type: u64

Native masks evaluated by the incremental path.

mask_candidates

Type: u64

Canvas candidates examined for those masks.

Functions

update_node_occlusion

pub fn update_node_occlusion(root_node: &Rc<ExpandedNode>, ctx: &RuntimeContext)

Reconcile structural changes, otherwise update only dirty geometry and overlapping masks.

engine::pax_gpu_render_context

Structs

LayerCreationRequest

Snapshot of surviving tiles supplied to a chassis factory. Return only missing surfaces; the runtime keeps survivors ready and validates returned tiles against the current layout.

Properties

shared_context

Type: Option<SharedGpuContext>

Context shared by this logical layer's surviving tiles, if any.

max_surface_dimension

Type: u32

Adapter limit already established by the surviving tiles.

Implementations

needs_surface
pub fn needs_surface(&self, key: &str, host_signature: &str) -> bool

Whether the factory must create this key and backing-surface generation.


LayerRenderer

Retained renderer bound to one physical surface tile for a logical layer.

Implementations

matches_surface
pub fn matches_surface(&self, surface: &LayerSurfaceEntry) -> bool

Whether this renderer belongs to the entry's current physical surface generation.

new
pub fn new(key: String, host_signature: String, renderer: WgpuRenderer<'static>, origin_x: f32, origin_y: f32, logical_width: f32, logical_height: f32, surface_width: u32, surface_height: u32, dpr: [f32; 2]) -> Self

Create a renderer wrapper with its current tile geometry.

renderer_mut
pub fn renderer_mut(&mut self) -> &mut WgpuRenderer<'static>

Access the underlying retained pax-gpu renderer.

sync_layout_metadata
pub fn sync_layout_metadata(&mut self, surface: &LayerSurfaceEntry)

Update the retained layout metadata after the owner has already applied matching backend surface/view transforms.


LayerTarget

Current renderer set for one logical layer.

Implementations

new
pub fn new(renderers: Vec<LayerRenderer>, active: bool) -> Self

Create a layer target from physical surface renderers.

renderers_mut
pub fn renderers_mut(&mut self) -> &mut [LayerRenderer]

Mutable access to each physical renderer backing this logical layer.


PaxGpuRenderer

Runtime RenderContext implementation backed by pax-gpu/wgpu.

Implementations

new
pub fn new(layer_factory: impl Fn(usize, LayerCreationRequest) -> Pin<Box<dyn Future>> + 'static) -> Self

Create a renderer that lazily asks the chassis for layer backends.

with_layer_initialization_priority
pub fn with_layer_initialization_priority(self, priority: impl Fn(usize) -> i32 + 'static) -> Self

Prioritize queued layer creation using current chassis presentation state. Lower values start first; equal values retain FIFO order. This does not preempt in-flight work or increase the initialization concurrency limit.

Enums

RenderLayerState

Lifecycle state for a lazily-created render layer.

Variants

Pending
Failed
Ready((LayerTarget, Pin<Box<dyn Fn() -> LayerSurfaceLayout>>))

Functions

convert_kurbo_to_lyon_path

pub fn convert_kurbo_to_lyon_path(kurbo_path: &BezPath) -> Path

Convert a kurbo path emitted by primitives into a lyon path consumed by pax-gpu.


to_pax_gpu_color

pub fn to_pax_gpu_color(color: &Color) -> Color

Convert a runtime API color into the pax-gpu render-context color.

engine::piet_render_context

Structs

PietLayerRenderer

Retained metadata for one piet-backed browser canvas surface.


PietLayerTarget

Current piet surface set for one logical layer.


PietRenderer

RenderContext implementation backed by piet.

Implementations

new
pub fn new(layer_factory: impl Fn(usize) -> (PietLayerTarget<S>, Box<dyn Fn() -> LayerSurfaceLayout>) + 'static) -> Self

Create a piet renderer with a chassis-provided logical layer factory.

Traits

PietSurface

Chassis operations missing from Piet's portable API: reusable transparent surfaces and source-over composition with one opacity for the complete image.

Functions

fill_to_piet_brush

pub fn fill_to_piet_brush(fill: &Paint, rect: Rect) -> Option<PaintBrush>

Resolves a single paint to a Piet brush. Mixtures require the chassis paint accumulator, since generic Piet has no additive compositing operation.

layout

Structs

LayoutHull

Per-axis local extents contributed by a node subtree for container measurement.

Coordinates are expressed in the node's local layout space. Validity is tracked per axis so parent-dependent axes can be ignored without discarding the entire subtree.

Properties

min_x

Type: f64

max_x

Type: f64

min_y

Type: f64

max_y

Type: f64

valid_x

Type: bool

valid_y

Type: bool


LayoutProperties

Unresolved layout inputs copied out of common properties before geometry calculation.

Properties

x

Type: Option<Size>

y

Type: Option<Size>

width

Type: Option<Size>

height

Type: Option<Size>

rotate

Type: Option<Rotation>

scale_x

Type: Option<Percent>

scale_y

Type: Option<Percent>

anchor_x

Type: Option<Size>

anchor_y

Type: Option<Size>

skew_x

Type: Option<Rotation>

skew_y

Type: Option<Rotation>

Implementations

fill
pub fn fill() -> Self

Full-size defaults for nodes that should fill their containing bounds.


TransformAndBounds

Pax's canonical representation of position, size, and transform, encoded as a transform (translation, rotation, scale, skew) and a separate width/height (bounds) value. Bounds are expressed as the (x1, y1) values of the axis-aligned pre-transform bounding box, where (x0, y0) are the origin.

In this model, position is a derived property, calculated by applying the transform to the bounding box.

Properties

transform

Type: Transform2<F, T>

bounds

Type: (f64, f64)

Implementations

as_pure_scale
pub fn as_pure_scale(self) -> Self

Move bounds into the transform as scale, leaving unit bounds.

as_pure_size
pub fn as_pure_size(self) -> Self

Move scale from the transform into the bounds field.

as_transform
pub fn as_transform(&self) -> Transform2<F, T>

Convert this split representation into a single affine transform.

cast_spaces
pub fn cast_spaces<A: Space, B: Space>(self) -> TransformAndBounds<A, B>

Retype coordinate-space markers without changing numeric values.

center
pub fn center(&self) -> Point2<T>

Center point of this transformed box.

contains_point
pub fn contains_point(&self, point: Point2<T>) -> bool

Test whether a point falls inside this transformed box.

corners
pub fn corners(&self) -> [Point2<T>; 4]

Corners of this transformed box, starting at origin and proceeding around the rectangle.

intersects
pub fn intersects(&self, other: &Self) -> bool

Test transformed-box intersection using the separating axis theorem.

inverse
pub fn inverse(&self) -> TransformAndBounds<T, F>

Invert the transform-and-bounds mapping.

Functions

add_symmetric_padding_to_content_layout_hull

pub fn add_symmetric_padding_to_content_layout_hull(hull: LayoutHull, padding_x: Option<Size>, padding_y: Option<Size>) -> LayoutHull

Expand a content-space hull into the node's padded outer layout space.

Children are laid out inside the leading padding offset, but autosized bounds must be solved from the content hull itself. Solving the outer size first keeps percentage padding from feeding back through the node's current measured bounds.


apply_container_frame

pub fn apply_container_frame(container_transform_and_bounds: TransformAndBounds<NodeLocal, Window>, container_frame: Option<ContainerFrame>) -> TransformAndBounds<NodeLocal, Window>

Apply a container-assigned child frame on top of the parent geometry.

This is the geometry seam where container-owned placement can cooperate with descendant-authored layout and future bottom-up measurement.


apply_padding_frame

pub fn apply_padding_frame(container_transform_and_bounds: TransformAndBounds<NodeLocal, Window>, padding_x: Option<Size>, padding_y: Option<Size>) -> TransformAndBounds<NodeLocal, Window>

Apply a node's padding to the container geometry seen by its children.


calculate_transform_and_bounds

pub fn calculate_transform_and_bounds(_: &LayoutProperties, _: TransformAndBounds<NodeLocal, Window>) -> TransformAndBounds<NodeLocal, Window>

Resolve one set of layout properties into concrete bounds and a window-space transform.


compute_tab

pub fn compute_tab(layout_properties: LocalProperty<LayoutProperties>, extra_transform: LocalProperty<Option<Transform2D>>, container_transform_and_bounds: LocalProperty<TransformAndBounds<NodeLocal, Window>>) -> LocalProperty<TransformAndBounds<NodeLocal, Window>>

Compute a reactive TransformAndBounds property from layout properties plus parent geometry.


project_child_layout_hull_to_parent_space

pub fn project_child_layout_hull_to_parent_space(parent: TransformAndBounds<NodeLocal, Window>, child: TransformAndBounds<NodeLocal, Window>, child_hull: LayoutHull) -> LayoutHull

Project a child's local hull into its parent's local layout space.


project_layout_hull

pub fn project_layout_hull<F: Space, T: Space>(transform: Transform2<F, T>, hull: LayoutHull) -> LayoutHull

Project a local layout hull through the provided transform and return the axis-aligned hull in the destination coordinate space.


resolve_padded_autosize_axis

pub fn resolve_padded_autosize_axis(content_extent: f64, padding: Option<Size>) -> Option<f64>

Solve an autosized outer axis from measured content and symmetric padding.

properties

Structs

ExpandedNodeIdentifier

Stable runtime identifier assigned to an expanded node.

Properties

0

Type: u32

Implementations

to_u32
pub fn to_u32(&self) -> u32

Convert to the integer id passed across chassis message boundaries.


ExpressionContext

Data structure used for dynamic injection of values into Expressions, maintaining a pointer e.g. to the current stack frame to enable evaluation of properties & dependencies

Properties

stack_frame

Type: Rc<RuntimePropertiesStackFrame>


RuntimeContext

Shared context for properties pass recursion

Properties

application

Type: ApplicationInstance

property_graph

Type: PropertyGraph

Owner-thread bridge for application properties.

layer_count

Type: Cell<usize>

dirty_canvases

Type: Rc<RefCell<Vec<bool>>>

Implementations

add_to_cache
pub fn add_to_cache(&self, node: &Rc<ExpandedNode>)

Add a node to runtime lookup caches.

canvas_geometry_for_node
pub fn canvas_geometry_for_node(&self, node: &ExpandedNode) -> Rc<PreparedCanvasGeometry>

Obtain the same prepared record used by spatial replay selection.

canvas_node_light_mask
pub fn canvas_node_light_mask(&self, id: ExpandedNodeIdentifier) -> u32

Return the direct-light membership mask resolved for a retained canvas node.

canvas_nodes_intersecting
pub fn canvas_nodes_intersecting(&self, layer: usize, regions: &[Rect]) -> Option<Vec<u32>>

Select canvas nodes using settled runtime geometry, even before their first draw. Call after draining scene effects; querying does not advance layout or dispatch events.

canvas_surface_transform_for_node
pub fn canvas_surface_transform_for_node(&self, node: &ExpandedNode) -> Affine

Resolve a node's local coordinates into its owning canvas's content space. Scroller-owned layers use the registered owner's origin; root layers use window coordinates. Detached mask sources retain their scene transform until capture projection. Physical tile offsets are applied later by the renderer.

capture_touch_target
pub fn capture_touch_target(&self, identifier: i64, target: ExpandedNodeIdentifier)

Route a touch sequence to the node hit at touch-down, even after the finger moves away.

captured_touch_target
pub fn captured_touch_target(&self, identifier: i64) -> Option<Rc<ExpandedNode>>

Resolve the node captured for an active touch sequence.

clear_all_dirty_canvases
pub fn clear_all_dirty_canvases(&self)

Mark every canvas layer clean.

clear_layer_scroller_owners
pub fn clear_layer_scroller_owners(&self)

Clear render-layer-to-scroller ownership before recomputing occlusion.

clear_root_expanded_node
pub fn clear_root_expanded_node(&self)

Clear the registered root expanded node.

clear_visual_viewport_state
pub fn clear_visual_viewport_state(&self)

Clear cached visual viewport state.

collect_scene_lighting_for_layer
pub fn collect_scene_lighting_for_layer(&self, layer: usize) -> SceneLighting

Resolve registered direct/ambient contributors in the target layer's coordinates.

Light values remain live on every render; retained node masks are rebuilt only when selected light slots, lexical scope, or scene structure changes. Contributors are independent of viewport visibility, and an unlit scene needs no node scan.

get_elements_beneath_ray
pub fn get_elements_beneath_ray(&self, root: Option<Rc<ExpandedNode>>, ray: Point2<Window>, limit_one: bool, accum: Vec<Rc<ExpandedNode>>, hit_invisible: bool) -> Vec<Rc<ExpandedNode>>

Simple 2D raycasting: the coordinates of the ray represent a ray running orthogonally to the view plane, intersecting at the specified point ray. Areas outside of clipping bounds will not register a hit, nor will elements that suppress input events.

get_expanded_node_by_eid
pub fn get_expanded_node_by_eid(&self, id: ExpandedNodeIdentifier) -> Option<Rc<ExpandedNode>>

Look up an expanded node by runtime id.

get_expanded_nodes_by_global_ids
pub fn get_expanded_nodes_by_global_ids(&self, uni: &UniqueTemplateNodeIdentifier) -> Vec<Rc<ExpandedNode>>

Finds all ExpandedNodes with corresponding UniqueTemplateNodeIdentifier

get_expanded_nodes_by_id
pub fn get_expanded_nodes_by_id(&self, id: &str) -> Vec<Rc<ExpandedNode>>

Finds all ExpandedNodes with the CommonProperty#id matching the provided string

get_layer_scroller_owner
pub fn get_layer_scroller_owner(&self, layer_id: usize) -> Option<ExpandedNodeIdentifier>

Find the scroller that owns a render layer, when one exists.

get_root_scroller_id
pub fn get_root_scroller_id(&self) -> Option<u32>

Current page-scroll-backed root scroller id.

get_screenshot_map
pub fn get_screenshot_map(&self) -> Rc<RefCell<HashMap<u32, ScreenshotData>>>

Shared screenshot capture map keyed by request id.

get_scroller_surface_scroll
pub fn get_scroller_surface_scroll(&self, id: u32) -> Option<(f64, f64)>

Fetch the presentation scroll offset for a native scroller surface, falling back to the authoritative scroll position when presentation scroll is unavailable.

get_scroller_surface_state
pub fn get_scroller_surface_state(&self, id: u32) -> Option<ScrollerSurfaceState>

Fetch cached scroller surface state by node id.

get_topmost_element_beneath_ray
pub fn get_topmost_element_beneath_ray(self: &Rc<Self>, ray: Point2<Window>) -> Option<Rc<ExpandedNode>>

Alias for get_elements_beneath_ray with limit_one = true

get_visual_viewport_state
pub fn get_visual_viewport_state(&self) -> Option<VisualViewportState>

Return cached browser visual viewport state, if available.

is_canvas_dirty
pub fn is_canvas_dirty(&self, id: &usize) -> bool

Check whether a canvas layer needs redraw.

layer_has_canvas_drawables
pub fn layer_has_canvas_drawables(&self, layer: usize) -> bool

Return whether a render layer currently has canvas work to paint.

load_screenshot
pub fn load_screenshot(&self, id: u32, data: ScreenshotData) -> bool

Store a screenshot payload delivered by the chassis.

mark_node_occlusion_dirty
pub fn mark_node_occlusion_dirty(&self, id: u32)

Invalidate native compositing after a node's paint or coverage changes. Detached source changes reconcile their owning mask in the visible scene.

mark_scene_lighting_dirty
pub fn mark_scene_lighting_dirty(&self)

Schedule lighting resolution without invalidating canvas geometry or contents. The retained renderer decides which surfaces depend on the changed lights.

new
pub fn new(globals: Globals) -> Self

Create a runtime context for normal app execution.

occlusion_stats
pub fn occlusion_stats(&self) -> OcclusionStats

Cumulative work performed by native compositing, including conservative rebuilds.

prepare_scene_geometry
pub fn prepare_scene_geometry(&self)

Prepare geometry dirtied by scene changes. Surface replay alone does not invalidate it.

register_layer_scroller_owner
pub fn register_layer_scroller_owner(&self, layer_id: usize, scroller_id: ExpandedNodeIdentifier)

Record that a render layer is owned by a particular scroller.

register_root_expanded_node
pub fn register_root_expanded_node(&self, root: &Rc<ExpandedNode>)

Store the root expanded node after it has been initialized.

release_touch_target
pub fn release_touch_target(&self, identifier: i64) -> Option<Rc<ExpandedNode>>

Release and resolve the node captured for a completed touch sequence.

remove_from_cache
pub fn remove_from_cache(&self, node: &Rc<ExpandedNode>)

Remove a node from runtime lookup caches.

remove_scroller_surface_state
pub fn remove_scroller_surface_state(&self, id: u32)

Remove cached scroller surface state.

request_canvas_replay
pub fn request_canvas_replay(&self, update: ReplayCanvasLayerUpdate)

Apply a backend's replay region request through the shared scene index.

resize_canvas_layers_to
pub fn resize_canvas_layers_to(&self, id: usize)

Ensure the dirty-canvas table has entries up to the requested layer count.

safe_area_insets
pub fn safe_area_insets(&self) -> LocalProperty<SafeAreaInsets>

Live native safe-area data used by opt-in layout primitives. Defaults to zero.

scene_geometry_stats
pub fn scene_geometry_stats(&self) -> SceneGeometryStats

Work counters for geometry preparation and spatial queries.

set_canvas_dirty
pub fn set_canvas_dirty(&self, id: usize)

Mark a canvas layer dirty.

set_canvas_drawable_layers
pub fn set_canvas_drawable_layers(&self, layers: HashSet<usize>)

Replace the set of render layers that currently contain canvas drawables.

set_root_scroller_id
pub fn set_root_scroller_id(&self, id: Option<u32>)

Mark which node currently delegates root scrolling behavior to the page.

set_safe_area_insets
pub fn set_safe_area_insets(&self, insets: SafeAreaInsets)

Update native safe-area data without changing the viewport or root layout.

set_scroller_surface_state
pub fn set_scroller_surface_state(&self, id: u32, state: ScrollerSurfaceState) -> ScrollerSurfaceStateChange

Remember browser-owned scroller state for native compositing and scroll transforms.

set_visual_viewport_state
pub fn set_visual_viewport_state(&self, state: VisualViewportState)

Cache the browser visual viewport state for root scroller math.

update_scroller_surface_scroll
pub fn update_scroller_surface_scroll(&self, id: u32, scroll_x: f64, scroll_y: f64, presentation_scroll_x: f64, presentation_scroll_y: f64) -> ScrollerSurfaceStateChange

Update hot scroll offsets for an existing native scroller surface without touching structural state.

viewport_proximity_stats
pub fn viewport_proximity_stats(&self) -> ViewportProximityStats

Observation counters. With no proximity bindings, no observation records are installed.


RuntimePropertiesStackFrame

Data structure for a single frame of our runtime stack, including a reference to its parent frame and properties for runtime evaluation, e.g. of Expressions. RuntimePropertiesStackFrames also track timeline playhead position.

Components push RuntimePropertiesStackFrames before computing properties and pop them after computing, thus providing a hierarchical store of node-relevant data that can be bound to symbols in expressions.


ScrollerSurfaceState

Last-known scroll state for a native or browser-owned scroller surface.

Properties

viewport_width

Type: f64

viewport_height

Type: f64

content_width

Type: f64

content_height

Type: f64

scroll_x

Type: f64

scroll_y

Type: f64

presentation_scroll_x

Type: f64

presentation_scroll_y

Type: f64

clip_content

Type: bool


VisualViewportState

Browser visual viewport state used when page scrolling participates in root scroller behavior.

Properties

width

Type: f64

height

Type: f64

offset_x

Type: f64

offset_y

Type: f64

page_scroll_x

Type: f64

page_scroll_y

Type: f64

Enums

ScrollerSurfaceStateChange

Coarse classification of changes to a native or browser-owned scroller surface.

Variants

Unchanged
ScrollOnly
Structural

rendering

Structs

BaseInstance

Shared storage carried by every concrete InstanceNode.

Properties

handler_registry

Type: Option<Rc<RefCell<HandlerRegistry>>>

instance_prototypical_properties

Type: PropertiesInit

instance_prototypical_common_properties

Type: CommonPropertiesInit

component_settings

Type: Option<Vec<SettingsBlockElement>>

template_node_identifier

Type: Option<UniqueTemplateNodeIdentifier>

template_node_type_id

Type: Option<TypeId>

template_node_selector_info

Type: Option<TemplateNodeSelectorInfo>

transition_config

Type: ComponentTransitionConfig

properties_scope

Type: PropertiesScopeInit

Implementations

flags
pub fn flags(&self) -> &InstanceFlags

Static behavior flags for this instance.

get_handler_registry
pub fn get_handler_registry(&self) -> Option<Rc<RefCell<HandlerRegistry>>>

Returns a handle to a node-managed HandlerRegistry, a mapping between event types and handlers. Each node that can handle events is responsible for implementing this; Component instances generate the necessary code to wire up userland events like <SomeNode @click=self.handler>. Primitives must handle this explicitly, see e.g. [pax_std::drawing::rectangle::RectangleInstance#get_handler_registry].

get_instance_children
pub fn get_instance_children(&self) -> &InstanceNodePtrList

Return the list of instance nodes that are children of this one. Intuitively, this returns the nodes owned directly by this instance's definition.

For Components, this returns the root(s) of the component template, not the projected children supplied by the containing component.

new
pub fn new(args: InstantiationArgs, flags: InstanceFlags) -> Self

Build shared instance state from compiler-generated instantiation args.


InstanceFlags

Static traversal/rendering flags for a concrete instance node.

Properties

invisible_to_slot

Type: bool

Used for exotic tree traversals for Slot, e.g. for Stacker > Repeat > Rectangle where the repeated Rectangles need to be be considered direct children of Stacker. Repeat and Conditional set this true so their active children can be considered direct projected children by slot-driven containers.

invisible_to_raycasting

Type: bool

Certain elements, such as Groups and Components, are invisible to ray-casting. Since these container elements are on top of the elements they contain, this is needed otherwise the containers would intercept rays that should hit their contents.

layer

Type: Layer

The layer type (Layer::Native, Layer::NativeNonOccluding, or Layer::Canvas) for this RenderNode. Default is Layer::Canvas, and must be overwritten for InstanceNodes that manage native content.

is_component

Type: bool

Only true for ComponentInstance

is_slot

Type: bool

Is this node a Slot?


InstantiationArgs

Construction payload used when compiler-generated code instantiates an InstanceNode.

Properties

prototypical_common_properties

Type: CommonPropertiesInit

prototypical_properties

Type: PropertiesInit

handler_registry

Type: Option<Rc<RefCell<HandlerRegistry>>>

children

Type: Option<InstanceNodePtrList>

component_template

Type: Option<InstanceNodePtrList>

component_settings

Type: Option<Vec<SettingsBlockElement>>

template_node_identifier

Type: Option<UniqueTemplateNodeIdentifier>

template_node_type_id

Type: Option<TypeId>

template_node_selector_info

Type: Option<TemplateNodeSelectorInfo>

transition_config

Type: ComponentTransitionConfig

properties_scope

Type: PropertiesScopeInit


ReusableInstanceNodeArgs

Lightweight clone of reusable base-node data for helper constructors.

Properties

handler_registry

Type: Option<Rc<RefCell<HandlerRegistry>>>

children

Type: InstanceNodePtrList

template_node_identifier

Type: Option<UniqueTemplateNodeIdentifier>

template_node_type_id

Type: Option<TypeId>

template_node_selector_info

Type: Option<TemplateNodeSelectorInfo>

Implementations

new
pub fn new(base: &BaseInstance) -> Self

Capture the reusable portions of a BaseInstance.


StrokeInstance

Resolved stroke style used by canvas drawing primitives.

Properties

color

Type: Color

width

Type: f64

style

Type: StrokeStyle

Enums

CommonPropertiesInit

Structured initialization for node-local common properties.

Variants

Default
Inline { defined_properties: BTreeMap<String, ValueDefinition> }
Template(Rc<TemplatePropertyPlan>)
Factory(CommonPropertiesFactory)

NodeType

Coarse runtime category for an instance node.

Variants

Component
Primitive

PropertiesInit

Structured initialization for node-local typed properties.

Variants

DescriptorDefault(&'static ErasedComponentDescriptor)
DescriptorInline { descriptor: &'static ErasedComponentDescriptor, defined_properties: BTreeMap<String, ValueDefinition> }
Template { descriptor: &'static ErasedComponentDescriptor, plan: Rc<TemplatePropertyPlan> }
Factory(PropertiesFactory)

Implementations

adopt
pub fn adopt(self, properties: Rc<RefCell<PaxAny>>) -> Self

Use an already constructed root while preserving its settings bindings. The allocation is shared directly; no descriptor default or value conversion may replace its property identities or Rust dependencies.


PropertiesScopeInit

How an expanded node should expose component-local symbols into scope.

Variants

None
Descriptor(&'static ErasedComponentDescriptor)
Factory(PropertiesScopeFactory)

Traits

InstanceNode

Central runtime representation of a properties-computable and renderable node. InstanceNodes are conceptually stateless, and rely on [ExpandedNode]s for stateful representations.

An InstanceNode sits in between a pax_compiler::TemplateNodeDefinition, the compile-time definition analogue to this instance, and [ExpandedNode].

There is a 1:1 relationship between pax_compiler::TemplateNodeDefinitions and InstanceNodes. There is a one-to-many relationship between one InstanceNode and possibly many variant [ExpandedNode]s, due to duplication via for.

InstanceNodes are architecturally "type-aware" — they can perform type-specific operations e.g. on the state stored in [ExpandedNode], while [ExpandedNode]s are "type-blind". The latter store polymorphic data but cannot operate on it without the type-aware assistance of their linked InstanceNode.

(See RepeatInstance::expand_node where we visit a singular InstanceNode several times, producing multiple [ExpandedNode]s.)

Type Aliases

InstanceNodePtr

Type aliases to make it easier to work with nested Rcs and RefCells for instance nodes.

repeat

Structs

RepeatInstance

A special "control-flow" primitive associated with the for statement. Repeat allows for nodes to be rendered dynamically per data specified in source_expression. That is: for a source_expression of length n, Repeat will render its template n times, each with an embedded component context (RepeatItem) with an index i and a pointer to that relevant datum source_expression[i]

Properties

base

Type: BaseInstance


RepeatItem

Per-iteration bindings exposed inside a for template body.

Properties

elem

Type: LocalProperty<PaxValue>

i

Type: LocalProperty<usize>


RepeatProperties

Contains modal vec and range variants, describing whether the Repeat source is encoded as a Vec<T> (where T is a PaxValue properties type) or as a Range<isize>

Properties

source_expression

Type: LocalProperty<PaxValue>

iterator_i_symbol

Type: LocalProperty<Option<String>>

iterator_elem_symbol

Type: LocalProperty<Option<String>>

repeat_key_expression

Type: Option<ExpressionInfo>

router

Runtime routing primitives used by declarative Router / Route control flow.

Structs

CompiledRouteBranch

Compiled route branch used by the runtime matcher.

Properties

default

Type: bool

Whether this branch is the fallback default=true branch.

Type: bool

Whether this branch should stack over the previously active branch.


RouteLocation

Structured application location shared across platforms.

On web targets this is serialized to and from window.location, while on non-web targets it remains a platform-agnostic route state model.

Properties

path_segments

Type: Vec<String>

Decoded path segments, with / represented by an empty vector.

query

Type: HashMap<String, Vec<String>>

Query-string values keyed by parameter name, preserving repeated keys.

fragment

Type: Option<String>

Optional fragment without the leading #.

Implementations

root
pub fn root() -> Self

Returns the canonical root location.

with_path_segments
pub fn with_path_segments(&self, path_segments: Vec<String>) -> Self

Clones this location while replacing only the path segments.


RouteMatch

Structured match data exposed to the active route subtree as route.

Properties

location

Type: RouteLocation

Location as seen by the current router scope.

global_location

Type: RouteLocation

Full global application location.

params

Type: HashMap<String, String>

Named values captured from :param segments.

consumed_segments

Type: usize

Number of path segments consumed by the selected branch.

remainder

Type: Vec<String>

Remaining path tail left after the selected branch.

is_exact

Type: bool

Whether the selected branch consumed the entire scoped path.

Implementations

remainder_location
pub fn remainder_location(&self) -> RouteLocation

Converts the remainder into a RouteLocation for nested router input.


RouterInstance

Implementations

instantiate_with_branches
pub fn instantiate_with_branches(args: InstantiationArgs, branches: Vec<CompiledRouteBranch>, branch_child_ranges: Vec<Range<usize>>) -> Rc<Self>

Creates a router instance with precompiled route branches.


RouterProperties

Internal router inputs carried into a RouterInstance.

Properties

input_location

Type: LocalProperty<RouteLocation>

Location scoped to the current router.

global_location

Type: LocalProperty<RouteLocation>

Full application location retained for diagnostics and coordination.

Constants

INTERNAL_ROUTE_LOCATION_SYMBOL

Internal stack symbol carrying the router input location for nested scopes.


INTERNAL_ROUTE_MATCH_SYMBOL

Internal stack symbol carrying the current route match object.

scene_geometry

Runtime-owned geometry preparation and spatial lookup, independent of canvas residency.

Structs

CanvasGeometry

Primitive-local geometry prepared before drawing or spatial queries.

An absent bound is conservatively unbounded. Primitives that draw outside their layout rectangle must supply their paint bounds. Paths can retain their prepared command geometry here so selection and drawing do not construct it twice.

Properties

local_bounds

Type: Option<Rect>

Conservative local paint bounds; None requires inclusion in every region query.

path

Type: Option<Rc<BezPath>>

Optional prepared path reused by the primitive during drawing.

Implementations

for_layout
pub fn for_layout(bounds: (f64, f64)) -> Self

Use the layout rectangle for a bounded primitive.


PreparedCanvasGeometry

Geometry shared by render selection, primitive drawing, and viewport observation. Native surfaces use a separate bounds lookup in the same geometry service. Observed layout-only targets and their ancestors share these records without entering either rendering index.

Coverage is in the owning canvas's content coordinates, so native scrolling changes the queried region without invalidating every descendant record.

Properties

world_transform

Type: Affine

Layout transform in unscrolled scene coordinates, shared with viewport observation.

layer

Type: usize

Owning logical canvas layer.

bounds

Type: (f64, f64)

Layout dimensions, kept separate from overflowing paint coverage.

surface_transform

Type: Affine

Transform from primitive-local space to canvas content space.

coverage_bounds

Type: Option<Rect>

Transformed paint coverage including the renderer's conservative culling pad.

local

Type: CanvasGeometry

Primitive-local geometry backing this prepared record.


SceneGeometryStats

Cumulative work counters for verifying geometry reuse and spatial-query scaling.

Properties

preparations

Type: u64

Primitive geometry preparations, excluding cache hits.

index_updates

Type: u64

Insertions or changes to indexed coverage/layer ownership.

queries

Type: u64

Region-query requests.

candidate_tests

Type: u64

Distinct candidates reaching final rectangle tests, accumulated across queries.

settings_motion

Receiver-owned motion for effective imported settings values.

Structs

SettingsTransitionConfig

Runtime projection of an ImportSettings transition; no playback state is shared.

Properties

duration

Type: Duration

curve

Type: &'static str

shader_effect

Runtime glue for experimental GPU-only subtree filters. Available with the opt-in shaders Cargo feature.

Functions

bind_shader_effect

pub fn bind_shader_effect(node: &Rc<ExpandedNode>, context: &Rc<RuntimeContext>)

Subscribe an effect primitive's typed inputs and presentation geometry to canvas updates. The default InstanceNode::handle_mount installs this for primitives with a shader recipe. A primitive overriding that hook must call this after expanding its children.

slot

Structs

Slot

Contains the index value for slot, either a literal or an expression.

Properties

index

Type: LocalProperty<Numeric>

is_remainder

Type: LocalProperty<bool>

last_node_id

Type: LocalProperty<usize>

showing_node

Type: LocalProperty<Weak<ExpandedNode>>


SlotInstance

A special "control-flow" primitive (a la yield or perhaps goto) that renders projected payload into a node's encapsulated implementation.

Slot relies on raw projected_children being present on the runtime stack and will not render any content if there are none. Projection is the engine transport mechanism; semantic container logic should usually reason in terms of received_children instead.

Consider a Stacker: the owner of a Stacker passes the Stacker some nodes to render inside the cells of the Stacker. To the owner of the Stacker, those nodes might seem like received children. Inside Stacker's encapsulated implementation, those received children travel as projected children until Slot becomes their rendered home. This same technique is portable and applicable elsewhere via Slot.

viewport_proximity

Opt-in global viewport observation over the runtime's shared prepared geometry.

Structs

ViewportProximityStats

Cumulative observation work, separate from render preparation and application handlers.

Properties

index_updates

Type: u64

Indexed geometry entries refreshed after dependency changes.

domain_queries

Type: u64

Scroll domains queried (cold nested domains are pruned).

candidates

Type: u64

Spatial candidates considered, including scroll-owner proxies.

samples

Type: u64

Detailed target samples, including terminal samples for previously active targets.

events

Type: u64

Local event deliveries to registered handlers.

pax-manifest

Submodules

Structs

ComponentDefinition

Container for an entire component definition — includes template, settings, event bindings, property definitions, and compiler + reflection metadata

Properties

type_id

Type: TypeId

is_main_component

Type: bool

is_primitive

Type: bool

is_struct_only_component

Type: bool

Flag describing whether this component definition is a "struct-only component", a struct decorated with #[pax] for use as the T in Property<T>.

module_path

Type: String

primitive_instance_import_path

Type: Option<String>

For primitives like Rectangle or Group, a separate import path is required for the Instance (render context) struct and the Definition struct. For primitives, then, we need to store an additional import path to use when instantiating.

template

Type: Option<ComponentTemplate>

settings

Type: Option<Vec<SettingsBlockElement>>

timelines

Type: Vec<TimelineDefinition>

route_branch

Type: Option<RouteBranchDescriptor>


ComponentTemplate

Implementations

from_parts
pub fn from_parts(containing_component: TypeId, root: VecDeque<TemplateNodeId>, children: HashMap<TemplateNodeId, VecDeque<TemplateNodeId>>, nodes: HashMap<TemplateNodeId, TemplateNodeDefinition>, next_id: usize, template_source_file_path: Option<String>) -> Self

Construct a component template from already-materialized storage.


ControlFlowRouteBranchDefinition

Properties

metadata

Type: Option<RouteMetadataDefinition>

Compiler-only document metadata attached to this declarative route.

The web compiler consumes this before cartridge generation. Runtime routing and baked program representations intentionally omit it.


ExpressionCompilationInfo

Properties

dependencies

Type: Vec<String>

symbols used in the expression


GradientDefinition

Compile-time representation of an inline @gradient value.

Properties

shape

Type: GradientShapeDefinition

elements

Type: Vec<GradientElement>

Implementations

stops
pub fn stops(&self) -> impl Iterator

Iterate only stop entries, skipping comments.


GradientStopDefinition

A single color stop in a gradient ramp.

Properties

position

Type: Size

color

Type: ValueDefinition


HostCrateInfo

Pulled from host Cargo.toml

Properties

name

Type: String

for example: pax-example

identifier

Type: String

for example: pax_example

import_prefix

Type: String

for example: some_crate::pax_reexports,


LiteralBlockDefinition

Container for a parsed Literal object

Properties

explicit_type_pascal_identifier

Type: Option<Token>

elements

Type: Vec<SettingElement>

Implementations

new
pub fn new(elements: Vec<SettingElement>) -> Self

Construct a literal object block from setting elements.

get_all_settings
pub fn get_all_settings<'a>(&'a self) -> Vec<(&'a Token, &'a ValueDefinition)>

Return only actual setting entries, omitting comments.


LocationInfo

Container for holding metadata about original Location in Pax Template Used for source-mapping

Properties

start_line_col

Type: (usize, usize)

end_line_col

Type: (usize, usize)


NodeLocation

Full editable location metadata for a template node.

Properties

type_id

Type: TypeId

tree_location

Type: TreeLocation

index

Type: TreeIndexPosition

Implementations

get_tree_location
pub fn get_tree_location(&self) -> &TreeLocation

Parent-location component of this node location.

get_type_id
pub fn get_type_id(&self) -> &TypeId

Type id of the node at this location.

new
pub fn new(type_id: TypeId, location: TreeLocation, index: TreeIndexPosition) -> Self

Construct a node location.


PaxManifest

Definition container for an entire Pax cartridge

Properties

components

Type: BTreeMap<TypeId, ComponentDefinition>

main_component_type_id

Type: TypeId

type_table

Type: TypeTable

assets_dirs

Type: Vec<String>

Compiler metadata: list of fully qualified asset directories, gathered during compiletime, from which assets will be copied for bundling into executable binaries

engine_import_path

Type: String

Compiler metadata: the import prefix for the engine module, pax_kit::pax_engine by default, but parameterizable for crates such as pax-std that integrate with pax-engine directly.


PropertyDefinition

Properties

name

Type: String

String representation of the symbolic identifier of a declared Property

flags

Type: PropertyDefinitionFlags

Flags, used ultimately by ExpressionSpecInvocations, to denote e.g. whether a property is the i or elem of a Repeat, which allows for special-handling the RIL that invokes these values

type_id

Type: TypeId

Statically known type_id for this Property's associated TypeDefinition

Implementations

Describes static metadata surrounding a property, for example the string representation of the property's name and a TypeInfo entry for the property's statically discovered type

primitive_with_name
pub fn primitive_with_name(type_name: &str, symbol_name: &str) -> Self

Shorthand factory / constructor


PropertyDefinitionFlags

These flags describe the aspects of properties that affect RIL codegen. Properties are divided into modal axes (exactly one value should be true per axis per struct instance) Codegen considers each element of the cartesian product of these axes

Properties

is_binding_repeat_i

Type: bool

Does this property represent the index i in for (elem, i) ?

is_binding_repeat_elem

Type: bool

Does this property represent elem in for (elem, i) OR for elem in 0..5 ?

is_repeat_source_range

Type: bool

Is the source being iterated over a Range?

is_repeat_source_iterable

Type: bool

Is the source being iterated over an iterable, like Vec<T>?

is_property_wrapped

Type: bool

Describes whether this property is a Property-wrapped T in Property<T> This distinction affects our ability to dirty-watch a particular property, and has implications on codegen

is_enum

Type: bool

Describes whether this property is an enum variant property


RouteBranchDescriptor

Compile-time contract allowing a component or primitive to act as a direct route branch child of Router.

Properties

path_property

Type: String

default_property

Type: String

Type: bool


SettingsConditionalBlock

Top-level conditional content inside a settings block.

Properties

branches

Type: Vec<SettingsConditionalBranch>


SettingsConditionalBranch

One branch inside a settings conditional. None represents else.

Properties

condition_expression

Type: Option<ExpressionInfo>

elements

Type: Vec<SettingsBlockElement>


TemplateNodeDefinition

Represents an entry within a component template, e.g. a <Rectangle> declaration inside a template Each node in a template is represented by exactly one TemplateNodeDefinition, and this is a compile-time concern. Note the difference between compile-time definitions and runtime instances. A compile-time TemplateNodeDefinition corresponds to a single runtime RenderNode instance.

Properties

type_id

Type: TypeId

Reference to the unique string ID for a component, e.g. primitive::Frame or component::Stacker

control_flow_settings

Type: Option<ControlFlowSettingsDefinition>

Iff this TND is a control-flow node: parsed control flow attributes (slot/if/for)

settings

Type: Option<Vec<SettingElement>>

IFF this TND is NOT a control-flow node: parsed key-value store of attribute definitions (like some_key="some_value")

selector_info

Type: TemplateNodeSelectorInfo

Normalized selector metadata preserved for runtime/designtime matching.

raw_comment_string

Type: Option<String>

IFF this TND is a comment node: raw comment string


TemplateNodeId

Stable id for a template node inside a single component.

Implementations

as_usize
pub fn as_usize(&self) -> usize

Numeric index backing this id.

build
pub fn build(id: usize) -> Self

Construct a template node id from its numeric index.


TimelineDefinition

Compile-time representation of a declared timeline.

Properties

name

Type: Option<Token>

playhead

Type: Option<ValueDefinition>

duration

Type: Option<ValueDefinition>

repeat

Type: bool

interruption

Type: InOutInterruption

elements

Type: Vec<TimelineBlockElement>


TimelineKeyframe

A single timeline value at a frame, duration, or percent marker.

Properties

marker

Type: TimelineMarker

value

Type: ValueDefinition

easing

Type: Option<Token>


TimelineSelectorBlockDefinition

Selector body inside a timeline block.

Properties

elements

Type: Vec<TimelineSelectorElement>


TimelineTrackDefinition

Track-level timeline data for one animated property.

Properties

elements

Type: Vec<TimelineTrackElement>

playhead

Type: Option<Box<ValueDefinition>>

duration

Type: Option<Box<ValueDefinition>>

repeat

Type: Option<bool>

starting_value

Type: Option<Box<ValueDefinition>>

interruption

Type: InOutInterruption

use_local_property_scope

Type: bool

Implementations

keyframes
pub fn keyframes(&self) -> impl Iterator

Iterate only keyframe entries, skipping comments.


Token

Container for parsed values with optional location information Location is optional in case this token was generated dynamically

Properties

token_value

Type: String

token_location

Type: Option<LocationInfo>

Implementations

new
pub fn new(token_value: String, token_location: LocationInfo) -> Self

Construct a token with source location information.

new_without_location
pub fn new_without_location(token_value: String) -> Self

Construct a token synthesized without a source location.


TransitionDefinition

Pair of timeline tracks bound to a node's enter/exit lifecycle.

Properties

enter

Type: Option<TimelineTrackDefinition>

exit

Type: Option<TimelineTrackDefinition>

starting_value

Type: Option<Box<ValueDefinition>>


TypeDefinition

Describes metadata surrounding a property's type, gathered from a combination of static & dynamic analysis

Properties

type_id

Type: TypeId

Program-unique ID for this type

inner_iterable_type_id

Type: Option<TypeId>

Statically known type_id for this Property's iterable TypeDefinition, that is, T for some Property<Vec<T>>

property_definitions

Type: Vec<PropertyDefinition>

A vec of PropertyType, describing known addressable (sub-)properties of this PropertyType

Implementations

builtin_vec_rc_ref_cell_any_properties
pub fn builtin_vec_rc_ref_cell_any_properties(inner_iterable_type_id: TypeId) -> Self

Used by Repeat for source expressions, e.g. the self.some_vec in for elem in self.some_vec


TypeId

Implementations

build_blank_component
pub fn build_blank_component(pascal_identifier: &str) -> Self

Build a typeid for a transient component

build_map
pub fn build_map(key_identifier: &str, value_identifier: &str) -> Self

Build a TypeId for map types like std::collections::HashMap<String><Color>

build_option
pub fn build_option(identifier: &str) -> Self

Build a TypeId for option types like std::option::Option<Color>

build_primitive
pub fn build_primitive(identifier: &str) -> Self

Build a TypeId for rust primitives like u8 or String

build_range
pub fn build_range(identifier: &str) -> Self

Build a TypeId for range types like std::ops::Range<Color>

build_singleton
pub fn build_singleton(import_path: &str, pascal_identifier: Option<&str>) -> Self

Build a TypeId for a most types, like Stacker or SpecialComponent

build_vector
pub fn build_vector(elem_identifier: &str) -> Self

Build a TypeId for vector types like Vec<Color>


UniqueTemplateNodeIdentifier

Globally unique identity for a template node: component type plus local template-node id.

Implementations

build
pub fn build(component: TypeId, template_node_id: TemplateNodeId) -> Self

Construct a globally unique template-node id.

get_containing_component_type_id
pub fn get_containing_component_type_id(&self) -> TypeId

Component that owns this template node.

get_template_node_id
pub fn get_template_node_id(&self) -> TemplateNodeId

Node id within the containing component template.

Enums

ControlFlowConditionalBranchKind

Container for storing parsed control flow information, for example the string (PAXEL) representations of condition / slot / repeat expressions and the related vtable ids (for "punching" during expression compilation)

Variants

If
ElseIf
Else

ControlFlowRepeatPredicateDefinition

Container for holding parsed data describing a Repeat (for) predicate, for example the (elem, i) in for (elem, i) in foo or the elem in for elem in foo

Variants

ElemId(String)
ElemIdIndexId(String, String)

GradientElement

One entry inside a gradient block.

Variants

Stop(GradientStopDefinition)
Comment(String)

GradientShapeDefinition

Shape-specific parameters for a gradient. V1 maps directly to runtime Paint variants.

Variants

Linear { start: Option<Box<ValueDefinition>>, end: Option<Box<ValueDefinition>> }
Radial { start: Box<ValueDefinition>, end: Box<ValueDefinition>, radius: Box<ValueDefinition> }
Mesh { rows: Box<ValueDefinition> }

InOutInterruption

Controls how an @in or @out timeline begins when it directly reverses the other lifecycle transition on the same mounted instance.

Variants

Takeover

Continue from the property's currently sampled value.

Restart

Begin from the destination timeline's authored starting value.


Number

Parsed numeric literal before final type coercion.

Variants

Float(f64)
Int(isize)

PaxType

Manifest-level type identity category.

Variants

If
Router
Slot
Repeat
Comment
BlankComponent
Primitive
Singleton
Range
Option
Vector
Map
Unknown

SettingElement

One key/value or comment entry inside a literal block.

Variants

Setting(Token, ValueDefinition)
Comment(String)

SettingsBlockElement

One entry inside a settings block.

Variants

SelectorBlock(Token, LiteralBlockDefinition)
Handler(Token, Vec<Token>)
Transition(Token, Token)
Conditional(SettingsConditionalBlock)
Comment(String)

TimelineBlockElement

One entry inside a timeline block.

Variants

SelectorBlock(Token, TimelineSelectorBlockDefinition)
Comment(String)

TimelineMarker

Timeline position expressed as an absolute frame, absolute duration, or normalized percentage.

Variants

Frame(u64)
Duration(Duration)
Percent(f64)

TimelineSelectorElement

One selector-scoped element inside a timeline block.

Variants

Track(Token, TimelineTrackDefinition)
Comment(String)

TimelineTrackElement

One entry in a timeline track.

Variants

Keyframe(TimelineKeyframe)
Comment(String)

TreeIndexPosition

Desired insertion position among siblings.

Variants

Top
Bottom
At(usize)

Implementations

get_index
pub fn get_index(&self, len: usize) -> usize

Resolve this symbolic position against a sibling-list length.

new
pub fn new(index: usize) -> Self

Construct an explicit index position.


TreeLocation

Parent relationship for a node inside a component template tree.

Variants

Root
Parent(TemplateNodeId)

Unit

Parsed Pax unit suffix.

Variants

Pixels
Percent

ValueDefinition

Container for settings values, storing all possible variants, populated at parse-time and used at compile-time

Variants

Undefined
LiteralValue(PaxValue)
List(Vec<ValueDefinition>)

Template list entries retain structured values and reactive bindings.

Block(LiteralBlockDefinition)
Timeline(TimelineTrackDefinition)
Gradient(GradientDefinition)
Transition(TransitionDefinition)
Expression(ExpressionInfo)

(Expression contents, vtable id binding)

Identifier(PaxIdentifier)

(Expression contents, vtable id binding)

DoubleBinding(PaxIdentifier)

(Expression contents, vtable id binding)

EventBindingTarget(PaxIdentifier)

Functions

escape_identifier

pub fn escape_identifier(input: String) -> String

Mangle an identifier into a token-safe representation for generated symbols.


get_common_properties_as_property_definitions

pub fn get_common_properties_as_property_definitions() -> Vec<PropertyDefinition>

Common properties represented as manifest property definitions.


get_common_properties_type_ids

pub fn get_common_properties_type_ids() -> Vec<TypeId>

Type ids for the built-in common properties attached to every template node.

Constants

SUPPORTED_NONNUMERIC_PRIMITIVES

Primitive nonnumeric Rust types supported directly by manifest reflection.


SUPPORTED_NUMERIC_PRIMITIVES

Primitive numeric Rust types supported directly by manifest reflection.

appearance

Static validation of appearance values at their expected property boundary.

Functions

validate_appearance

pub fn validate_appearance(value: &ValueDefinition, stroke: bool) -> Result<(), String>

Checks the statically known parts of a fill/stroke stack. Dynamic bindings are checked by the same layer coercions when their values become available.

cartridge_generation

Structs

CommonProperty

Common property metadata passed into cartridge codegen templates.


ComponentInfo

Template context for generating a component's cartridge code.

Properties

type_id

Type: TypeId

pascal_identifier

Type: String

symbol_identifier

Type: String

Collision-free Rust helper prefix derived from the complete component type identity.

primitive_instance_import_path

Type: Option<String>

properties

Type: Vec<PropertyInfo>

handlers

Type: Vec<HandlerInfo>


HandlerInfo

Event handler entry passed into cartridge codegen templates.

Properties

name

Type: String

args_type

Type: Option<String>


PropertyInfo

Property entry passed into cartridge codegen templates.

Properties

name

Type: String

property_type

Type: PropertyDefinition

program_ir

Structs

ProgramIR

Runtime-facing semantic program model derived from a rich PaxManifest.

ProgramIR is a transitional intermediate representation: it preserves the semantic program structure while stripping obviously source-only and compiler-only manifest data. Debug/designtime flows may continue to mount rich manifests directly until later phases cut execution over to ProgramIR.

Properties

components

Type: BTreeMap<TypeId, ProgramComponent>

main_component_type_id

Type: TypeId

type_table

Type: BTreeMap<TypeId, TypeDefinition>

assets_dirs

Type: Vec<String>

selectors

Structs

TemplateNodeSelectorInfo

Selector identity persisted for one authored template node.

class_binding is the sole class representation: literal strings and lists as well as PAXEL expressions all cross the manifest/runtime boundary here.

Properties

source_location

Type: Option<LocationInfo>

Source span of the authored node, when available.

id

Type: Option<Token>

The node's authored id selector.

class_binding

Type: Option<ValueDefinition>

The complete class attribute value. Literal and expression-backed classes share this representation so runtime selector resolution has a single source of truth.

Implementations

literal_classes
pub fn literal_classes(&self) -> Vec<String>

Return class names that can be read without evaluating an expression.

pax-message

Submodules

Structs

AccelInterruptArgs

Device acceleration payload, in meters per second squared.

Properties

x

Type: f64

y

Type: f64

z

Type: f64


AddedLayerArgs

Chassis acknowledgement that render layers were added.

Properties

num_layers_added

Type: u32

layer_id

Type: Option<u32>


AnyCreatePatch

Common creation payload shared by native element families.

Properties

id

Type: u32

parent_frame

Type: Option<u32>

render_layer_id

Type: u32


AppleLiquidGlassPatch

Apple-specific native liquid-glass effect payload.

Properties

group_id

Type: u32

spacing

Type: f64

interactive

Type: bool

tint

Type: Option<ColorMessage>

variant

Type: String


BrowserConfigInterruptArgs

Browser capability flags discovered by the web chassis.

Properties

allow_scroller_vector_layers

Type: bool

allow_nested_scroller_vector_layers

Type: bool


ButtonPatch

Create/update patch for a native button.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

hover_color

Type: Option<ColorMessage>

outline_stroke_color

Type: Option<ColorMessage>

outline_stroke_width

Type: Option<f64>

corner_radius

Type: Option<f64>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

content

Type: Option<String>

color

Type: Option<ColorMessage>

style

Type: Option<TextStyleMessage>

liquid_glass

Type: Option<Option<AppleLiquidGlassPatch>>


ChassisResizeRequestArgs

Chassis response containing measured native control bounds.

Properties

id

Type: u32

width

Type: f64

height

Type: f64


CheckboxPatch

Create/update patch for a native checkbox.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

background

Type: Option<ColorMessage>

background_checked

Type: Option<ColorMessage>

outline_color

Type: Option<ColorMessage>

outline_width

Type: Option<f64>

corner_radius

Type: Option<f64>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

checked

Type: Option<bool>

liquid_glass

Type: Option<Option<AppleLiquidGlassPatch>>


CheckboxStyleMessage

Style payload shared by checkbox-like controls.


ClickInterruptArgs

Click interrupt payload.

Properties

x

Type: f64

y

Type: f64

button

Type: MouseButtonMessage

modifiers

Type: Vec<ModifierKeyMessage>


ContextMenuInterruptArgs

Context-menu interrupt payload.

Properties

x

Type: f64

y

Type: f64

button

Type: MouseButtonMessage

modifiers

Type: Vec<ModifierKeyMessage>


DoubleClickInterruptArgs

Double-click interrupt payload.

Properties

x

Type: f64

y

Type: f64

button

Type: MouseButtonMessage

modifiers

Type: Vec<ModifierKeyMessage>


DropFileArgs

File-drop interrupt payload.

Properties

x

Type: f64

y

Type: f64

name

Type: String

mime_type

Type: String

size

Type: u64


Create/update patch for a native dropdown/select element.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

selected_id

Type: Option<u32>

options

Type: Option<Vec<String>>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

background

Type: Option<ColorMessage>

stroke_color

Type: Option<ColorMessage>

stroke_width

Type: Option<f64>

corner_radius

Type: Option<f64>

style

Type: Option<TextStyleMessage>

liquid_glass

Type: Option<Option<AppleLiquidGlassPatch>>


EventBlockerPatch

Create/update patch for a native hit-test blocker with an optional solid background.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

transform

Type: Option<Vec<f64>>

opacity

Type: Option<f64>

background

Type: Option<ColorMessage>


FocusInterruptArgs

Focus interrupt marker payload.


FormButtonClickArgs

Native button click payload.

Properties

id

Type: u32


FormCheckboxToggleArgs

Checkbox state-change interrupt payload.

Properties

state

Type: bool

id

Type: u32


FormDropdownChangeArgs

Dropdown selection-change payload.

Properties

id

Type: u32

selected_id

Type: u32


FormRadioListChangeArgs

Radio-list selection-change payload.

Properties

id

Type: u32

selected_id

Type: u32


FormSliderChangeArgs

Slider value-change payload.

Properties

id

Type: u32

value

Type: f64


FormTextboxChangeArgs

Textbox committed-value change payload.

Properties

text

Type: String

id

Type: u32


FormTextboxInputArgs

Textbox in-progress input payload.

Properties

text

Type: String

id

Type: u32


FramePatch

Create/update patch for a native frame host.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

clip_content

Type: Option<bool>

corner_radius

Type: Option<f64>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

transform

Type: Option<Vec<f64>>

clip_path

Type: Option<String>

opacity

Type: Option<f64>

presented_bounds

Type: Option<[f64; 4]>

presented_clip_bounds

Type: Option<[f64; 4]>


GlassSurfacePatch

Create/update patch for a materialized native liquid-glass surface.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

corner_radius

Type: Option<f64>

liquid_glass

Type: Option<AppleLiquidGlassPatch>


GyroInterruptArgs

Device orientation payload, in degrees.

Properties

x

Type: f64

y

Type: f64

z

Type: f64


ImageDataArgs

Image-load payload carrying image metadata without a byte pointer.

Properties

id

Type: u32

path

Type: String

width

Type: usize

height

Type: usize


ImagePatch

Image-load request sent to the chassis.

Properties

id

Type: u32

path

Type: Option<String>


ImagePointerArgs

Image-load payload carrying a pointer to chassis-owned image bytes.

Properties

id

Type: u32

path

Type: String

image_data

Type: u64

image_data_length

Type: usize

width

Type: usize

height

Type: usize


InterruptBuffer

Raw FFI buffer containing serialized interrupts.

Properties

data_ptr

Type: *const u8

length

Type: u64


KeyDownInterruptArgs

Key-down interrupt payload.

Properties

key

Type: String

modifiers

Type: Vec<ModifierKeyMessage>

is_repeat

Type: bool


KeyPressInterruptArgs

Key-press interrupt payload.

Properties

key

Type: String

modifiers

Type: Vec<ModifierKeyMessage>

is_repeat

Type: bool


KeyUpInterruptArgs

Key-up interrupt payload.

Properties

key

Type: String

modifiers

Type: Vec<ModifierKeyMessage>

is_repeat

Type: bool


LayerSurfaceScreenshotData

Completed screenshot bytes for one physical surface within a logical canvas layer.

Properties

id

Type: u32

key

Type: String

data

Type: Vec<u8>

width

Type: usize

height

Type: usize

origin_x

Type: f32

origin_y

Type: f32

logical_width

Type: f32

logical_height

Type: f32


LinkStyleMessage

Serializable style payload for link text.

Properties

font

Type: Option<FontPatch>

fill

Type: Option<ColorMessage>

underline

Type: Option<bool>

size

Type: Option<f64>


LocalFontMessage

Local bundled font payload.

Properties

family

Type: Option<String>

path

Type: Option<String>

style

Type: Option<FontStyleMessage>

weight

Type: Option<FontWeightMessage>


MaskPathPatch

One vector coverage path entry for a native occlusion mask.

Properties

path

Type: String

clips

Type: Vec<String>

opacity

Type: Option<f64>


MessageQueue

Serializable batch of native messages emitted for one runtime tick.

Properties

messages

Type: Vec<NativeMessage>


MouseDownInterruptArgs

Mouse-down interrupt payload.

Properties

x

Type: f64

y

Type: f64

button

Type: MouseButtonMessage

modifiers

Type: Vec<ModifierKeyMessage>


MouseMoveInterruptArgs

Mouse-move interrupt payload.

Properties

x

Type: f64

y

Type: f64

button

Type: MouseButtonMessage

modifiers

Type: Vec<ModifierKeyMessage>


MouseOutInterruptArgs

Mouse-out interrupt payload.

Properties

x

Type: f64

y

Type: f64

button

Type: MouseButtonMessage

modifiers

Type: Vec<ModifierKeyMessage>


MouseOverInterruptArgs

Mouse-over interrupt payload.

Properties

x

Type: f64

y

Type: f64

button

Type: MouseButtonMessage

modifiers

Type: Vec<ModifierKeyMessage>


MouseUpInterruptArgs

Mouse-up interrupt payload.

Properties

x

Type: f64

y

Type: f64

button

Type: MouseButtonMessage

modifiers

Type: Vec<ModifierKeyMessage>


NativeCullPatch

Advisory native-view residency changes from the shared geometry index. The macOS host retains models and view instances, and may keep a cold view attached to preserve editing, selection, overflow, or accessibility.

Properties

cull

Type: Vec<u32>

restore

Type: Vec<u32>


NativeImagePatch

Create/update patch for a native image element.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

url

Type: Option<String>

fit

Type: Option<String>


NativeMaskPatch

Create/update patch for a native occlusion mask surface.

Properties

id

Type: u32

size_x

Type: f64

size_y

Type: f64

entries

Type: Vec<MaskPathPatch>


NativeMessageQueue

Raw FFI buffer containing serialized native messages.

Properties

data_ptr

Type: *mut [u8]

length

Type: u64


Request for the chassis to navigate to a URL.

Properties

url

Type: String

target

Type: String


PhotoPickerAssetArgs

Selected image metadata and optional copied bytes from a native photo picker.

Properties

temp_id

Type: String

file_name

Type: Option<String>

mime_type

Type: String

byte_size

Type: u64

width

Type: Option<u32>

height

Type: Option<u32>

source_kind

Type: String

handle

Type: Option<String>

data

Type: Vec<u8>


PhotoPickerInterruptArgs

Native photo picker completion payload.

Properties

id

Type: u32

request_id

Type: u64

status

Type: String

message

Type: Option<String>

photos

Type: Vec<PhotoPickerAssetArgs>


PhotoPickerPatch

Create/update patch for a transparent native photo picker hit target.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

trigger

Type: Option<u64>

source

Type: Option<String>

allow_multiple

Type: Option<bool>

accept

Type: Option<String>

include_bytes

Type: Option<bool>

max_bytes_per_photo

Type: Option<u64>


RadioListPatch

Create/update patch for a native radio list.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

selected_id

Type: Option<u32>

options

Type: Option<Vec<String>>

style

Type: Option<TextStyleMessage>

background_checked

Type: Option<ColorMessage>

outline_color

Type: Option<ColorMessage>

outline_width

Type: Option<f64>

background

Type: Option<ColorMessage>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

liquid_glass

Type: Option<Option<AppleLiquidGlassPatch>>


RenderSurfaceUpdateArgs

Browser notification that a retained render surface must be reconfigured.

Properties

layer_id

Type: Option<u32>


RouteChangeInterruptArgs

Canonical route-location payload pushed from chassis state into the runtime.

Properties

path_segments

Type: Vec<String>

query

Type: HashMap<String, Vec<String>>

fragment

Type: Option<String>


SafeAreaInsets

Insets of the platform's safe rectangle, in logical window pixels.

Reported by the native iOS/iPadOS host. These are live window data, not persisted program state, and do not automatically inset any application.

Properties

top

Type: f64

right

Type: f64

bottom

Type: f64

left

Type: f64


ScreenshotData

Completed screenshot bytes returned by the chassis.

Properties

id

Type: u32

data

Type: Vec<u8>

width

Type: usize

height

Type: usize


ScreenshotPatch

Request to capture a screenshot from the chassis.

Properties

id

Type: u32

scale

Type: Option<f64>


ScrollInterruptArgs

Location-targeted scroll delta payload.

Properties

x

Type: f64

y

Type: f64

delta_x

Type: f64

delta_y

Type: f64


ScrollerPatch

Create/update patch for a native/browser-owned scroller.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

clip_content

Type: Option<bool>

corner_radius

Type: Option<f64>

size_inner_pane_x

Type: Option<f64>

size_inner_pane_y

Type: Option<f64>

snap_points_x

Type: Option<Vec<f64>>

snap_points_y

Type: Option<Vec<f64>>

scroll_x

Type: Option<f64>

scroll_y

Type: Option<f64>

presentation_scroll_x

Type: Option<f64>

presentation_scroll_y

Type: Option<f64>

scroll_enabled_x

Type: Option<bool>

scroll_enabled_y

Type: Option<bool>

content_layer_id

Type: Option<u32>

presented_bounds

Type: Option<[f64; 4]>

presented_clip_bounds

Type: Option<[f64; 4]>

subtree_depth

Type: u32


ScrollerPositionInterruptArgs

Native scroller position payload, including optional presentation offsets.

Properties

id

Type: u32

scroll_x

Type: f64

scroll_y

Type: f64

presentation_scroll_x

Type: Option<f64>

presentation_scroll_y

Type: Option<f64>


SelectStartArgs

Selection-start interrupt marker payload.


SetCursorPatch

Request for the chassis to update cursor style.

Properties

cursor

Type: String


SliderPatch

Create/update patch for a native slider.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

value

Type: Option<f64>

step

Type: Option<f64>

min

Type: Option<f64>

max

Type: Option<f64>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

accent

Type: Option<ColorMessage>

background

Type: Option<ColorMessage>

corner_radius

Type: Option<f64>

liquid_glass

Type: Option<Option<AppleLiquidGlassPatch>>


SystemFontMessage

System font family payload.

Properties

family

Type: Option<String>

style

Type: Option<FontStyleMessage>

weight

Type: Option<FontWeightMessage>


TapInterruptArgs

Single-touch tap payload normalized to window coordinates.

Properties

x

Type: f64

y

Type: f64


TextInputArgs

Raw text input payload delivered by a native text control.

Properties

text

Type: String

id

Type: u32


TextMeasurementResponseArgs

Chassis response to an engine-authored native text measurement request.

Properties

id

Type: u32

generation

Type: u64

width

Type: f64

height

Type: f64


TextPatch

Create/update patch for native or browser-managed text.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

content

Type: Option<String>

editable

Type: Option<bool>

selectable

Type: Option<bool>

clip

Type: Option<bool>

markdown

Type: Option<bool>

wrap

Type: Option<bool>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

style

Type: Option<TextStyleMessage>

Type: Option<TextStyleMessage>

measure_generation

Type: Option<u64>


TextStyleMessage

Serializable text style payload shared with chassis text renderers.

Properties

font

Type: Option<FontPatch>

font_size

Type: Option<f64>

fill

Type: Option<ColorMessage>

underline

Type: Option<bool>

align_multiline

Type: Option<TextAlignHorizontalMessage>

align_vertical

Type: Option<TextAlignVerticalMessage>

align_horizontal

Type: Option<TextAlignHorizontalMessage>


TextboxPatch

Create/update patch for a native textbox.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

text

Type: Option<String>

background

Type: Option<ColorMessage>

stroke_color

Type: Option<ColorMessage>

stroke_width

Type: Option<f64>

corner_radius

Type: Option<f64>

style

Type: Option<TextStyleMessage>

focus_on_mount

Type: Option<bool>

placeholder

Type: Option<String>

outline_color

Type: Option<ColorMessage>

outline_width

Type: Option<f64>

is_text_area

Type: Option<bool>

liquid_glass

Type: Option<Option<AppleLiquidGlassPatch>>


TouchCancelInterruptArgs

Touch-cancel interrupt payload.

Properties

touches

Type: Vec<TouchMessage>


TouchEndInterruptArgs

Touch-end interrupt payload.

Properties

touches

Type: Vec<TouchMessage>


TouchMessage

One touch point in a multi-touch interrupt.

Properties

x

Type: f64

y

Type: f64

identifier

Type: i64

delta_x

Type: f64

delta_y

Type: f64


TouchMoveInterruptArgs

Touch-move interrupt payload.

Properties

touches

Type: Vec<TouchMessage>


TouchStartInterruptArgs

Touch-start interrupt payload.

Properties

touches

Type: Vec<TouchMessage>


ViewportResizeArgs

Layout viewport resize payload for the root app surface.

Properties

width

Type: f64

height

Type: f64


VisualViewportUpdateArgs

Browser visual viewport payload for page-scroll-backed root scrollers.

Properties

width

Type: f64

height

Type: f64

offset_x

Type: f64

offset_y

Type: f64

page_scroll_x

Type: f64

page_scroll_y

Type: f64


WebFontMessage

Web font payload, including family and source URL.

Properties

family

Type: Option<String>

url

Type: Option<String>

style

Type: Option<FontStyleMessage>

weight

Type: Option<FontWeightMessage>


WheelInterruptArgs

Wheel interrupt payload.

Properties

x

Type: f64

y

Type: f64

delta_x

Type: f64

delta_y

Type: f64

modifiers

Type: Vec<ModifierKeyMessage>


YoutubeVideoPatch

Create/update patch for an embedded YouTube video.

Properties

id

Type: u32

parent_frame

Type: Option<Option<u32>>

z_index

Type: Option<i32>

transform

Type: Option<Vec<f64>>

size_x

Type: Option<f64>

size_y

Type: Option<f64>

opacity

Type: Option<f64>

url

Type: Option<String>

Enums

ColorMessage

Serializable color payload for native/chassis messages.

Variants

Rgba([f64; 4])
Rgb([f64; 3])

FontPatch

Serializable font selection payload.

Variants

System(SystemFontMessage)
Web(WebFontMessage)
Local(LocalFontMessage)

FontStyleMessage

Serializable font-style value.

Variants

Normal
Italic
Oblique

FontWeightMessage

Serializable font-weight value.

Variants

Thin
ExtraLight
Light
Normal
Medium
SemiBold
Bold
ExtraBold
Black

ImageLoadInterruptArgs

Image-load response, either by pointer or copied metadata.

Variants

Reference(ImagePointerArgs)
Data(ImageDataArgs)

ModifierKeyMessage

Normalized keyboard modifier identifier.

Variants

Shift
Control
Alt
Command

MouseButtonMessage

Normalized mouse button identifier.

Variants

Left
Right
Middle
Unknown

NativeInterrupt

Events and data packets sent from the chassis back into the Pax runtime.

Variants

ChassisResizeRequestCollection(Vec<ChassisResizeRequestArgs>)
TextMeasurementResponse(TextMeasurementResponseArgs)
SelectStart(SelectStartArgs)
Focus(FocusInterruptArgs)
Scroll(ScrollInterruptArgs)
TouchStart(TouchStartInterruptArgs)
TouchMove(TouchMoveInterruptArgs)
TouchEnd(TouchEndInterruptArgs)
TouchCancel(TouchCancelInterruptArgs)
KeyDown(KeyDownInterruptArgs)
KeyUp(KeyUpInterruptArgs)
KeyPress(KeyPressInterruptArgs)
Click(ClickInterruptArgs)
Tap(TapInterruptArgs)
DoubleClick(DoubleClickInterruptArgs)
MouseMove(MouseMoveInterruptArgs)
Wheel(WheelInterruptArgs)
MouseDown(MouseDownInterruptArgs)
MouseUp(MouseUpInterruptArgs)
ContextMenu(ContextMenuInterruptArgs)
Image(ImageLoadInterruptArgs)
AddedLayer(AddedLayerArgs)
TextInput(TextInputArgs)
FormCheckboxToggle(FormCheckboxToggleArgs)
FormDropdownChange(FormDropdownChangeArgs)
FormSliderChange(FormSliderChangeArgs)
FormRadioListChange(FormRadioListChangeArgs)
FormTextboxChange(FormTextboxChangeArgs)
FormTextboxInput(FormTextboxInputArgs)
FormButtonClick(FormButtonClickArgs)
PhotoPicker(PhotoPickerInterruptArgs)
ScrollerPosition(ScrollerPositionInterruptArgs)
BrowserConfig(BrowserConfigInterruptArgs)
RenderSurfaceUpdate(RenderSurfaceUpdateArgs)
ViewportResize(ViewportResizeArgs)
SafeAreaInsets(SafeAreaInsets)
RouteChange(RouteChangeInterruptArgs)
VisualViewportUpdate(VisualViewportUpdateArgs)
Gyro(GyroInterruptArgs)
Accel(AccelInterruptArgs)
DropFile(DropFileArgs)
Screenshot(ImageLoadInterruptArgs)

NativeMessage

Messages emitted by the runtime to create, update, delete, or configure native/chassis resources.

Variants

TextCreate(AnyCreatePatch)
TextUpdate(TextPatch)
TextDelete(u32)
FrameCreate(AnyCreatePatch)
FrameUpdate(FramePatch)
FrameDelete(u32)
EventBlockerCreate(AnyCreatePatch)
EventBlockerUpdate(EventBlockerPatch)
EventBlockerDelete(u32)
CheckboxCreate(AnyCreatePatch)
CheckboxUpdate(CheckboxPatch)
CheckboxDelete(u32)
NativeImageCreate(AnyCreatePatch)
NativeImageUpdate(NativeImagePatch)
NativeImageDelete(u32)
YoutubeVideoCreate(AnyCreatePatch)
YoutubeVideoUpdate(YoutubeVideoPatch)
YoutubeVideoDelete(u32)
TextboxCreate(AnyCreatePatch)
TextboxUpdate(TextboxPatch)
TextboxDelete(u32)
SliderCreate(AnyCreatePatch)
SliderUpdate(SliderPatch)
SliderDelete(u32)
RadioListCreate(AnyCreatePatch)
RadioListUpdate(RadioListPatch)
RadioListDelete(u32)
ButtonCreate(AnyCreatePatch)
ButtonUpdate(ButtonPatch)
ButtonDelete(u32)
PhotoPickerCreate(AnyCreatePatch)
PhotoPickerUpdate(PhotoPickerPatch)
PhotoPickerDelete(u32)
GlassSurfaceCreate(AnyCreatePatch)
GlassSurfaceUpdate(GlassSurfacePatch)
GlassSurfaceDelete(u32)
ScrollerCreate(AnyCreatePatch)
ScrollerUpdate(ScrollerPatch)
ScrollerDelete(u32)
ImageLoad(ImagePatch)
ShrinkLayersTo(u32)
NativeMaskUpdate(NativeMaskPatch)
NativeCullUpdate(NativeCullPatch)
SetCursor(SetCursorPatch)
Screenshot(ScreenshotPatch)

TextAlignHorizontalMessage

Serializable horizontal text alignment.

Variants

Left
Center
Right

TextAlignVerticalMessage

Serializable vertical text alignment.

Variants

Top
Center
Bottom

http_api

Shared request and response types for Pax's public HTTP service.

This module deliberately contains only the closed wire contract. HTTP clients, provider mappings, persistence, and policy belong at the respective edges.

Structs

LatestReleaseResponse

Response returned by [CLI_LATEST_RELEASE_PATH].

Properties

latest_version

Type: String


TelemetryRequest

One privacy-bounded CLI telemetry event.

Properties

installation_id

Type: String

Random UUID v4 representing one OS-user CLI installation.

cli_version

Type: String

host_os

Type: HostOs

host_arch

Type: HostArch

event

Type: TelemetryEvent

Enums

CommandFamily

Public top-level CLI command families eligible for telemetry.

Variants

Create
Run
Build
Clean
Eject
Format
Lsp
Docs
Dev
SvgImport

CommandOutcome

Coarse command result. Error details never cross the HTTP boundary.

Variants

Succeeded
Failed

HostArch

Coarse host CPU architecture.

Variants

X86_64
Aarch64
Other

HostOs

Coarse host operating-system family.

Variants

Macos
Linux
Windows
Other

Target

Supported Pax build or run targets.

Variants

Web
Macos
Ios
Ipados

TelemetryEvent

Closed set of telemetry events accepted by the launch API.

Variants

CommandOutcome { command: CommandFamily, target: Option<Target>, outcome: CommandOutcome }
RunReady

Constants

CLI_LATEST_RELEASE_PATH

Path for querying the latest published pax-cli release.


CLI_TELEMETRY_PATH

Path for submitting a single CLI telemetry event.

pax-gpu

Submodules

Type Aliases

Box2D

Axis-aligned box type used for retained-node bounds.


Point2D

Point type used by tessellation and retained scene geometry.


Transform2D

Affine transform type used by the retained GPU renderer.


Vector2D

Vector type used by tessellation and retained scene geometry.

render_backend

Submodules

Structs

CapturedFrame

CPU-readable screenshot payload captured from a rendered frame.

Properties

width

Type: u32

height

Type: u32

rgba

Type: Vec<u8>


GpuContext

GPU device, pipelines, and a weak index of resident image textures shared by sibling surfaces.


Image

Decoded RGBA image data ready for upload as a GPU texture.

Properties

rgba

Type: Vec<u8>

pixel_width

Type: u32

pixel_height

Type: u32


RenderBackend

Low-level wgpu backend that owns surface, pipeline, and GPU buffers.

Implementations

get_clip_depth
pub fn get_clip_depth(&mut self) -> u32

Current stencil clip depth.

max_surface_dimension
pub fn max_surface_dimension(&self) -> u32

Maximum texture dimension supported by the active adapter.

resize
pub fn resize(&mut self, width: u32, height: u32)

Resize both the physical surface and logical viewport.

resize_surface
pub fn resize_surface(&mut self, width: u32, height: u32)

Resize the backing surface, clamping to device limits.

set_viewport
pub fn set_viewport(&mut self, width: f32, height: f32, dpr: [f32; 2])

Update logical viewport uniforms without reallocating the surface.


RenderConfig

GPU resource sizing and initial surface configuration.

Properties

debug

Type: bool

initial_width

Type: u32

initial_height

Type: u32

initial_dpr

Type: [f32; 2]

Implementations

new
pub fn new(_debug: bool, width: u32, height: u32, dpr: [f32; 2]) -> Self

Construct default buffer capacities for an initial surface size.

with_browser_premultiplied_alpha
pub fn with_browser_premultiplied_alpha(self, enabled: bool) -> Self

Request premultiplied browser canvas alpha for WebGPU surfaces.

wgpu's web backend currently reports only Opaque alpha in surface capabilities, but its configure path accepts PreMultiplied and maps it to GPUCanvasAlphaMode::Premultiplied. Pax needs that for transparent browser-owned scroller islands.

render_backend::stencil

Structs

ClipDraw

One clip geometry instance to draw into the stencil buffer.

Properties

clip_id

Type: u32

geometry_signature

Type: u64

geometry

Type: &'a VertexBuffers<Vertex, u16>


StencilRenderer

Maintains the stencil stack used to render nested vector clips.

Implementations

new
pub fn new(device: &Device, width: u32, height: u32, sample_count: u32, color_format: TextureFormat, globals: &Buffer, clip_transforms: &Buffer) -> Self

Create the stencil pipelines and backing texture.


Vertex

Tessellated stencil vertex.

Properties

position

Type: [f32; 2]

render_context

Structs

AlphaMaskPaint

Vector paint used to construct a surface-local alpha mask.

Properties

path

Type: Path

transform

Type: Transform2D

fill

Type: Fill

opacity

Type: f32

composition

Type: Option<(u32, f32)>


Color

Linear RGBA color used by the low-level renderer.

Implementations

hlca
pub fn hlca(h: f32, l: f32, c: f32, a: f32) -> Self

Construct a color from HLC/Lab-style components plus alpha.

hsva
pub fn hsva(h: f32, s: f32, v: f32, a: f32) -> Self

Construct a color from HSV plus alpha, with hue normalized to 0.0-1.0.

rgba
pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self

Construct a color from linear RGBA channels in the range 0.0-1.0.


DrawRange

Normalized visible range for a stroked vector path.

Properties

start

Type: f32

end

Type: f32

enabled

Type: bool


GradientStop

One color stop in a GPU gradient fill.

Properties

color

Type: Color

stop

Type: f32


Material

Light-reactive material coefficients for a tessellated vector path.

Properties

coefficients

Type: [f32; 4]

emissive

Type: [f32; 4]

unlit

Type: bool


ResourceChurnStats

Resource churn counters for renderer profiling. Effect counters are available with the opt-in shaders Cargo feature.

Properties

effect_source_renders

Type: u64

effect_source_cache_hits

Type: u64

effect_pipeline_compilations

Type: u64

effect_texture_allocations

Type: u64

effect_texture_bytes

Type: u64

effect_cache_hits

Type: u64

effect_passes

Type: u64

effect_pixels

Type: u64

mesh_texture_allocations

Type: u64

mesh_texture_bytes

Type: u64

Bytes allocated for mesh textures during this sample (not current residency).

mesh_coefficient_upload_bytes

Type: u64

mesh_raster_passes

Type: u64

mesh_cache_hits

Type: u64

mesh_topology_allocations

Type: u64

flushes

Type: u64

opacity_group_renders

Type: u64

opacity_group_cache_hits

Type: u64

alpha_source_renders

Type: u64

alpha_source_cache_hits

Type: u64

retained_scene_resets

Type: u64

vector_batch_flushes

Type: u64

vector_buffer_rebuilds

Type: u64

vector_geometry_rebuilds

Type: u64

vector_geometry_cache_hits

Type: u64

vector_geometry_cache_misses

Type: u64

vector_geometry_cache_evictions

Type: u64

vector_geometry_cache_bytes

Type: u64

tessellated_vertices

Type: u64

tessellated_indices

Type: u64

cached_vertices_reused

Type: u64

cached_indices_reused

Type: u64

vector_resource_creates

Type: u64

vector_resource_updates

Type: u64

vector_resource_recreates

Type: u64

vector_resource_create_bytes

Type: u64

vector_resource_update_bytes

Type: u64

vector_resource_cache_hits

Type: u64

vector_resource_cache_misses

Type: u64

vector_resource_cache_evictions

Type: u64

vector_resource_cache_bytes

Type: u64

texture_creates

Type: u64

image_draw_creates

Type: u64

texture_upload_bytes

Type: u64

texture_cache_hits

Type: u64

Tile-local bindings created using an already resident sibling image texture.

retained_nodes_considered

Type: u64

retained_nodes_visible

Type: u64

retained_draw_batches

Type: u64

retained_draws

Type: u64

retained_vector_draws

Type: u64

retained_image_draws

Type: u64


SceneLight

A resolved scene light for the low-level renderer.

Properties

shape

Type: LightShape

position

Type: [f32; 3]

direction

Type: [f32; 3]

color

Type: Color

intensity

Type: f32

radius

Type: f32


SceneLighting

Resolved lighting state for one retained vector scene.

Properties

active

Type: bool

ambient_is_authored

Type: bool

ambient_color

Type: Color

ambient_intensity

Type: f32

lights

Type: Vec<SceneLight>


Stroke

Stroke style for a tessellated vector path.

Properties

fill

Type: Fill

weight

Type: f32

cap

Type: StrokeCap

join

Type: StrokeJoin


WgpuRenderer

Retained scene renderer that records Pax vector/image commands and flushes them through wgpu.

Transform and clip identities remain stable until their owning node releases them. GPU uniform pages grow with scene demand; the 480-entry page size is not a scene limit. Moving or fading retained geometry updates transform pages without retessellating or uploading vertex/index buffers.

Implementations

begin_shader_effect
pub fn begin_shader_effect(&mut self, id: u32, z: i32, effect: &ShaderEffect, transform: Transform2D, size: (f64, f64), scopes: &[OpacityScope], children: &[u32]) -> bool

Start a GPU-only filter boundary. A true return requests an initial source replay. Available with the opt-in shaders Cargo feature.

end_shader_effect
pub fn end_shader_effect(&mut self)

Finish the GPU-only filter boundary. Requires the shaders Cargo feature.

begin_alpha_source
pub fn begin_alpha_source(&mut self, mapping: Transform2D, feather: f32)

Records a detached source in the current mask owner's surface coordinates. Source clips and opacity are independent of the consuming content; its enclosing mask is applied afterward.

end_alpha_source
pub fn end_alpha_source(&mut self)

Restores the consumer and installs a reference to the source's retained alpha texture.

clip_alpha
pub fn clip_alpha(&mut self, paints: Vec<AlphaMaskPaint>, feather: f32)

Installs a painted alpha mask in the current save/restore scope.

current_transform
pub fn current_transform(&self) -> Transform2D

Current transform at the top of the render-state stack.

draw_image
pub fn draw_image(&mut self, image_key: &str, image_version: u64, image: &Image, rect: Box2D)

Image identity and version must identify the same pixels across renderers sharing a GPU context. Change the version when replacing image content.

draw_image_with_opacity
pub fn draw_image_with_opacity(&mut self, image_key: &str, image_version: u64, image: &Image, rect: Box2D, opacity: f32)

Multiplies source pixel alpha by opacity without reuploading the texture. Sibling tiles share resident image pixels, but keep their own draw bindings. Identity/version follows the same contract as [Self::draw_image].

fill_path
pub fn fill_path(&mut self, path: Path, fill: Fill)

Queue a filled vector path into the current retained node.

fill_path_with_material_and_opacity
pub fn fill_path_with_material_and_opacity(&mut self, path: Path, fill: Fill, material: Material, opacity: f32)

Queue a filled vector path with material response and an extra opacity multiplier.

fill_path_with_material_and_opacity_and_smoothing
pub fn fill_path_with_material_and_opacity_and_smoothing(&mut self, path: Path, fill: Fill, material: Material, opacity: f32, smoothing: PathSmoothing)

Queue a filled vector path with material response, opacity, and optional smoothing.

fill_path_with_opacity
pub fn fill_path_with_opacity(&mut self, path: Path, fill: Fill, opacity: f32)

Queue a filled vector path with an extra opacity multiplier.

new
pub fn new(render_backend: RenderBackend<'w>) -> Self

Create a retained renderer around a low-level RenderBackend.

reset_retained_scene
pub fn reset_retained_scene(&mut self)

Drop retained scene state for a surface that has been rebound to a new tile origin.

set_node_opacity_scopes
pub fn set_node_opacity_scopes(&mut self, node_id: u32, scopes: &[OpacityScope])

Updates group presentation without changing a node's paint or image resources.

set_scene_lighting
pub fn set_scene_lighting(&mut self, lighting: SceneLighting) -> bool

Upload changed surface-local lighting, returning whether retained pixels need redraw. Only lights used by this surface's lit vector draws invalidate its contents. Repeating the same value does not upload a uniform or dirty the scene. Each new renderer receives its own initial upload, even for default lighting.

set_surface_transform
pub fn set_surface_transform(&mut self, transform: Transform2D)

Set the base transform for the physical surface tile being rendered.

share_vector_caches_from
pub fn share_vector_caches_from(&mut self, other: &Self)

Share vector resource caches with another renderer for the same logical layer.

shared_gpu_context
pub fn shared_gpu_context(&self) -> SharedGpuContext

Reuse this logical layer's GPU context when creating another physical tile. The returned handle keeps the device and pipelines alive, not this tile's surface or scene.

stroke_path
pub fn stroke_path(&mut self, path: Path, stroke: Stroke)

Queue a stroked vector path into the current retained node.

stroke_path_with_draw_range_and_material_and_opacity
pub fn stroke_path_with_draw_range_and_material_and_opacity(&mut self, path: Path, stroke: Stroke, material: Material, opacity: f32, draw_range: DrawRange)

Queue a draw-ranged stroked vector path with material response and an extra opacity multiplier.

stroke_path_with_draw_range_and_material_and_opacity_and_smoothing
pub fn stroke_path_with_draw_range_and_material_and_opacity_and_smoothing(&mut self, path: Path, stroke: Stroke, material: Material, opacity: f32, draw_range: DrawRange, smoothing: PathSmoothing)

Queue a draw-ranged stroked vector path with material response, opacity, and optional smoothing.

stroke_path_with_material_and_opacity
pub fn stroke_path_with_material_and_opacity(&mut self, path: Path, stroke: Stroke, material: Material, opacity: f32)

Queue a stroked vector path with material response and an extra opacity multiplier.

stroke_path_with_material_and_opacity_and_smoothing
pub fn stroke_path_with_material_and_opacity_and_smoothing(&mut self, path: Path, stroke: Stroke, material: Material, opacity: f32, smoothing: PathSmoothing)

Queue a stroked vector path with material response, opacity, and optional smoothing.

stroke_path_with_opacity
pub fn stroke_path_with_opacity(&mut self, path: Path, stroke: Stroke, opacity: f32)

Queue a stroked vector path with an extra opacity multiplier.

take_resource_churn_stats
pub fn take_resource_churn_stats(&mut self) -> ResourceChurnStats

Return and reset accumulated resource churn counters.

Enums

Fill

Fill style for a tessellated vector path.

Variants

Mesh { rows: Vec<Vec<([f32; 2], Color)>>, pos: Point2D, main_axis: Vector2D, off_axis: Vector2D }

Row-connected mesh in normalized paint coordinates. The axes map that domain to scene coordinates; row order fixes connectivity independently of each anchor's x/y position.

Blend(Vec<(Fill, f32)>)

Weighted premultiplied paint mixture, sampled before compositing.

Solid(Color)
Gradient { gradient_type: GradientType, pos: Point2D, main_axis: Vector2D, off_axis: Vector2D, stops: Vec<GradientStop> }

GradientType

Shape of a GPU gradient fill.

Variants

Linear
Radial

Unit outer circle centered at pos, mapped by main_axis/off_axis. Stops are normalized (0..1); the focal point is in that unit circle's space.


LightShape

Shape of a scene light.

Variants

Point
Directional

StrokeCap

Stroke end-cap style.

Variants

Butt
Round
Square

StrokeJoin

Stroke join style.

Variants

Miter
Round
Bevel

Architecture: Runtime & Cartridge

Historical design notes. This appendix records a direction for Pax's runtime/cartridge boundary, including proposed packaging and future logic runtimes. Its envelope and sidecar descriptions are not a specification of today's release artifact. For the current builder-facing execution model, rendering behavior, and build guidance, read How Pax Runs. Rust is the current application language. State that must survive a remount needs an owner outside the remounted component; Property alone does not make state durable.

Pax borrows its core mental model from the NES.

The console is always present. It knows how to power up, read input, render frames, play audio, and expose hardware capabilities. The cartridge is what you plug in to produce one specific experience.

Pax uses the same split:

  • the runtime kernel is the always-present engine
  • the cartridge is the program you mount into that engine

That split is useful for two reasons:

  1. it keeps the runtime boundary clear
  2. it gives Pax a path toward multiple logic runtimes, not just Rust

The Runtime Kernel

The runtime kernel is the reusable part of Pax. It is responsible for:

  • the reactive property graph
  • PAXEL expression evaluation
  • layout and rendering
  • compositing, masking, and clipping
  • event delivery
  • native element projection
  • platform integration for web, desktop, and mobile

You can think of the kernel as the "console." It can exist before any program is mounted, and it can later mount a cartridge.

That matters for:

  • normal application startup
  • design tools
  • hot reload
  • future online sandboxes
  • future embedded use cases where a host wants to attach a program later

The Cartridge

The cartridge is the packaged running experience that gets mounted into the kernel.

Today, the cartridge is no longer thought of as "generated Rust glue" first. Instead, it is better understood as an envelope containing the parts needed to run one Pax program.

At a high level, a cartridge contains:

  • a program representation
  • one or more logic modules
  • assets
  • optional debug sidecars

Program Representation

The program representation is the declarative part of the app:

  • component tree
  • template nodes
  • property bindings
  • expressions
  • timelines
  • event bindings
  • asset references

In debug-oriented flows, Pax still keeps a rich authoring representation around because it is useful for designtime edits and inspection.

In release-oriented flows, the goal is a smaller, more portable representation that keeps only what the runtime needs to execute the program.

Logic Modules

The logic module is the executable half of the cartridge.

Today that usually means Rust application logic: event handlers, component code, state transitions, and helper functions.

Architecturally, Pax now treats this as a separate concept from the declarative program representation. That matters because it creates a cleaner path to support additional logic runtimes in the future, such as JS or TS, while remaining compatible with the same runtime kernel and the same standard library components.

Assets

Assets are the external resources the program needs:

  • images
  • fonts
  • videos
  • static files

The runtime kernel does not hardcode application assets. The cartridge carries or references them.

Debug Sidecars

Debug builds may carry extra sidecars and metadata used for:

  • source mapping
  • symbolic inspection
  • live edits
  • reload handoff
  • designtime tooling

Those are intentionally not part of the minimal release payload.

Program Representation vs. Logic Module

This boundary is worth making explicit.

The declarative program representation says things like:

  • "instantiate this component"
  • "bind this property to this expression"
  • "attach this timeline to this field"
  • "invoke this handler when this event fires"

The logic module says what the handler actually does.

That separation helps Pax in two directions at once:

  • smaller release artifacts, because less behavior needs to be expanded into generated code
  • cleaner multi-runtime support, because the declarative program can stay portable while the logic module can vary by host language

Native Elements Are Projections

Pax treats native elements such as browser controls or platform widgets as projections of engine state, not as the source of truth.

The authoritative state lives in the property graph inside the runtime kernel. Native elements are updated by messages from the engine and report user input back through interrupts.

That design is important for:

  • deterministic rerendering
  • hot reload
  • future reload handoff
  • consistent behavior across platforms

If durable UI state needs to survive reload or remount, it should be reflected into Property state or another explicit durable state holder, not left hidden inside the native control itself.

Debug and Release Builds

Debug and release builds share the same semantics, but they optimize for different goals.

Debug Builds

Debug builds optimize for editability and observability.

They keep richer metadata and support tooling such as:

  • designtime workflows
  • source-aware diagnostics
  • live program updates
  • reload handoff
  • full symbolic structure for inspection

Because of that, debug builds are intentionally much larger.

Release Builds

Release builds optimize for shipping.

They aim to:

  • strip metadata that the runtime does not need to execute the app
  • reduce generated behavioral glue
  • favor compact program representation
  • keep the runtime/kernel boundary clean
  • minimize transfer size, especially on web targets

Release builds intentionally do not expose the same live-edit and hot-swap surface as debug builds.

Footprint Differences

The difference in footprint between debug and release can be large, especially for web builds.

The earlier, unversioned example-size snapshot has been removed because it does not describe the current build. Measure the application and release you intend to ship, including its assets. Debug and release describes the current build modes and measurement boundaries.

Why This Architecture Matters

This runtime/cartridge split gives Pax a cleaner long-term direction.

For application authors, it means:

  • the same declarative program model can target multiple platforms
  • release builds can get smaller without changing authoring semantics
  • debug builds can remain powerful without forcing that cost into production

For Pax itself, it means the engine is moving toward:

  • a mountable runtime kernel
  • a smaller and more portable program representation
  • clearer host-side descriptor boundaries
  • future support for additional logic languages

In short: the kernel is the console, the cartridge is the program, and the boundary between them is now becoming a first-class part of Pax rather than an implementation detail hidden inside generated code.