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
- A
.paxtemplate declares the interface and binds an event. - PAXEL expressions derive visible values from reactive properties.
- An interaction invokes a Rust handler.
- 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 directory | What it contains |
|---|---|
Cargo.toml | The Rust package definition, dependencies, and optional Pax project metadata. |
src/main.rs | Calls pax_kit::init!()?.run() to initialize and run the UI in the native application process. |
src/lib.rs | The scene's state and event handlers, plus pax_kit::runnable! { root: crate::Example }. An optional init callback customizes root construction. |
src/lib.pax | The 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 present | Application 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
| Symptom | What to check |
|---|---|
cargo or pax-cli is not found | Confirm 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 library | Run rustup target add wasm32-unknown-unknown, then retry. |
wasm-pack is not found | Run cargo install wasm-pack --version 0.15.0, then check wasm-pack --version. |
| A compiler, linker, or system library is missing | Revisit your workstation setup. On Windows, use the PowerShell session where you loaded the MSVC environment. |
| The project destination already exists | Choose a new directory name. Keep any existing project files. |
| The browser cannot connect | Check 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 runsession 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:
| Form | Example | Meaning |
|---|---|---|
| Literal | text="Field notes" | Use this value directly. |
| Value with a unit | width=320px or width=100% | Express a dimension in pixels or relative to the parent. |
| Expression | width={100% - 48px} | Derive the value with PAXEL. |
| State binding | text={self.title} | Read the containing component's title property reactively. |
| Event binding | @click=self.activate | Invoke the component's activate handler when the event occurs. |
| Read/write binding | text=bind:title | Connect 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
classattribute. 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:
- The containing component's local settings.
- Imported providers, in provider order.
- 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.
| Kind | Examples |
|---|---|
| Number | 24, 0.25 |
| Boolean | true, false |
| String | "Field notes" |
| Size | 24px, 50% |
| Rotation | 15deg, 0.5rad |
| Time | 250ms, 1s, 5f |
| Color | BLUE, rgb(32, 40, 48), rgba(32, 40, 48, 128) |
| List | [120px, None, 30%] |
| Contextual object | {font_size: 16px, fill: BLUE} |
| Empty option | None |
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:
| Value | What it provides |
|---|---|
$viewport | Scene width and height in logical pixels; major, minor, aspect, landscape, portrait, and square. |
$target | Boolean platform and operating-system facts, such as web, native, macos, ios, iphone, and ipad. |
$frames, $millis | Runtime frame count and elapsed time in milliseconds. |
$gyro, $accel | Device 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.
| Purpose | Operators | Example |
|---|---|---|
| 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:
| Method | Behavior |
|---|---|
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 value | Result |
|---|---|
true | Discard the candidate, retain the last accepted value, and stop invalidation at the cutoff. |
false | Accept 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
| Need | Start with |
|---|---|
| A value derived for a template | A PAXEL formula; the compiler supplies its dependency edges. |
| Application state changed by an action | A property written by the Rust handler. |
| Derived reactive state used from Rust | LocalProperty::computed, with every dependency listed explicitly. |
| A short side effect observing state | NodeContext::subscribe, with its node-scoped lifetime. |
| Fewer unchanged writes | set_if_neq, when equality is inexpensive. |
| A stable output between a busy input and expensive consumers | A 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 selfgives it mutable access to theNotesinstance that owns this template.&NodeContextdescribes 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:
| Binding | Handler argument | Useful data |
|---|---|---|
@button_click | Event<ButtonClick> | Activation of a native Button |
@click, @tap | Event<Click> | event.mouse.x, .y, .button, .modifiers |
@mouse_move | Event<MouseMove> | Pointer position through event.mouse |
@touch_start, @touch_move, @touch_end, @touch_cancel | Event<TouchStart>, Event<TouchMove>, Event<TouchEnd>, Event<TouchCancel> | event.touches, including identifiers and positions |
@key_down, @key_up | Event<KeyDown>, Event<KeyUp> | event.keyboard.key, .modifiers, .is_repeat |
@textbox_input, @textbox_change | Event<TextboxInput>, Event<TextboxChange> | event.text |
@checkbox_change | Event<CheckboxChange> | event.checked |
@slider_change | Event<SliderChange> | event.value |
@viewport_proximity_enter | Event<ViewportProximityEnter> | Initial nearby current sample |
@viewport_proximity_change | Event<ViewportProximityChange> | previous and current geometry |
@viewport_proximity_exit | Event<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:
| Method | When it runs | Typical use |
|---|---|---|
on_mount | When the component mounts | Install computations or subscriptions that need established bindings or context |
on_tick | On runtime ticks while the component is active | Advance an application simulation or other time-driven logic |
on_pre_render | After tick handlers, in the pre-render phase | Synchronize work that must follow that tick's updates |
on_unmount | When the component is removed from the mounted tree | Release 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.
| Owner | Suitable state | Lifetime |
|---|---|---|
| Component properties | Presentation and interaction state | Component instance |
| Lexical local store | Dependencies shared within a subtree | Providing node |
| Initialized root fields | Configuration, clients, shared state, executor clients | Application 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:
- Choose Run · 600 ms, then Interact. Input remains usable while the
executor's timer runs; the result updates without application
@tickpolling. - Choose Replace A with B. B wins; the retained producer for A cannot publish after its authority has been revoked.
- 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.
- Choose Stream 100 events. The bounded channel delivers ordered events; a full buffer is handled explicitly rather than dropping arbitrary messages.
- 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:
| Arrangement | Result |
|---|---|
| Two consumers below one Notes provider | Both share its progress handle. |
| Two sibling Notes providers | Each owns its own store; their descendants remain independent. |
| A Notes provider nested inside another | The inner provider shadows NotesStore for its subtree. The outer state is unchanged. |
| An ancestor provides a different store type | It 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:
| Error | Meaning |
|---|---|
Missing { type_name } | No provider of the requested type exists here or above. |
ExpiredScope | The 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:
| Kind | Implementation |
|---|---|
| Template-backed component | A #[pax] type with #[file(...)] or #[inlined(...)]; its template composes other nodes. |
| Primitive | A #[pax] type with #[primitive("...")]; a Rust InstanceNode implements its runtime behavior, with no attached .pax template. |
| Data type | A #[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
InstanceNodeimplementation supplies construction and runtime hooks. A sharedBaseInstancecarries 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_mountto the properties and geometry it reads. Standard drawing primitives usechanged_listenerto mark their retained drawing dirty. Reserveupdatefor work that cannot be driven by those dependencies; it requires opting in throughrequires_non_reactive_update. - Canvas drawing: implement
renderthroughRenderContext. 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_nodesuppliespaint_opacity: use that value for drawing so a backend that composes subtrees does not apply ancestor opacity twice. The currentEllipseInstancedemonstratesbegin_bounded_canvas_nodeand clearing the dirty flag only afterend_nodesucceeds. 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
truefromhas_scene_lighting()when the primitive implementsresolve_scene_lightorresolve_scene_ambient_light. This capability stays true even when the resource'senabledproperty is false. Existing custom lighting primitives must add this opt-in; resolvers alone no longer register a contributor. FollowLightSourceInstance'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:
Ellipse/EllipseInstance— a bounded canvas primitive: properties, dirty tracking, geometry, material-aware drawing, and coverage.Group/GroupInstance— child expansion, layout measurement, and conditional native-surface integration.Text/TextInstance— native create/update/delete messages and text measurement. Follow the message to the target interface for the platform half.InstanceNode,BaseInstance, andInstantiationArgs— the runtime contract and default hook implementations.ComponentInstance— the engine implementation behind ordinary template-backed components, including scope and slot projection.
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:
- Open a team member and compare the outer and inner scope shown by the inspectors.
- Follow a team settings link. The outer team remains selected while the inner route and remainder change.
- Visit an unknown path and inspect the fallback's unconsumed segments.
- 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:
Routerreads the current location and chooses a branch.Routedeclares a path and the content that belongs there.Linkor Rust'sNodeContext::navigate_torequests 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:
| Pattern | Matches |
|---|---|
/ | The root of the current router's scope. |
/notes | That exact path. |
/teams/:team_id | One segment captured as team_id. |
/teams/:team_id/* | The team path and any remaining segments. |
default=true | A 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
Links in templates
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.
Navigation from Rust
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:
| Field | Meaning |
|---|---|
route.location | The structured location seen by this router. |
route.global_location | The full application location. |
route.params | Named captures from this branch's :param segments. |
route.remainder | A list of path segments left for a nested router. |
route.consumed_segments | The number of segments consumed before that remainder. |
route.is_exact | Whether 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"]. querymapstagto["ui", "motion"]; repeated keys retain their values.fragmentis 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:
| Router | Input segments | Captures | Remainder |
|---|---|---|---|
| Outer | teams / design / members / ada | team_id = design | members / ada |
| Team panel | members / ada | member_id = ada | Empty |
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/designto/teams/engineeringcreates 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
/notescan 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/:slugremains 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
noindexand 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
- Components and Composition — page components, shared shells, and state ownership.
- State and Properties — reactive state that follows navigation.
- Animation and Motion — enter and exit lifecycles.
- Targets, Build, and Deployment — release output, direct links, and static hosting.
- Router, Route, RouteCard, and RouteModal reference and Link reference — individual component fields.
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:
| Position | Default anchor | Child's left edge |
|---|---|---|
x=0% | Left edge | 0px |
x=50% | Center | 150px |
x=100% | Right edge | 300px |
x=200px | Left edge | 200px |
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:
| Container | Measured axes |
|---|---|
| Vertical Stacker | Its extending axis: height |
| Horizontal Stacker | Its extending axis: width |
| Group and Frame | Both axes |
| Scroller | Scroll-content height, separately from the viewport |
| Link | Both axes from supplied content |
| Tooltip | Both 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:
| Setting | Responsibility | Values |
|---|---|---|
align_horizontal | Places the text block horizontally inside the node | TextAlignHorizontal::Left, Center, Right |
align_vertical | Places the text block vertically inside the node | TextAlignVertical::Top, Center, Bottom |
align_multiline | Aligns lines within the text block | TextAlignHorizontal::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:
fit | Result |
|---|---|
ImageFit::Fit (default) | Preserves proportions and shows the whole image; may leave unused space |
ImageFit::Fill | Preserves proportions and covers the bounds; crops the excess |
ImageFit::Stretch | Maps 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:
| Input | Top-left | Top-right | Bottom-right | Bottom-left |
|---|---|---|---|---|
12 or [12] | 12 | 12 | 12 | 12 |
[12, 6] | 12 | 6 | 12 | 6 |
[12, 6, 3] | 12 | 6 | 3 | 6 |
[12, 6, 3, 1] | 12 | 6 | 3 | 1 |
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.
| Command | Meaning |
|---|---|
Point(x, y) by itself | Move 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 |
Close | Connect 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.
| Control | Editable property or content | Control event |
|---|---|---|
| Button | label supplies its visible title | @button_click, with Event<ButtonClick> |
| Textbox | text: String | @textbox_input while editing; @textbox_change when the platform commits a change |
| Checkbox | checked: bool | @checkbox_change, with event.checked |
| Slider | value: f64, bounded by min and max | @slider_change, with event.value |
| Dropdown | options: Vec<String> and selected_id: u32 | Bind selected_id; there is no dedicated public dropdown-change event |
| RadioList | options: Vec<String> and selected_id: u32 | Bind 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:
| Value | Behavior |
|---|---|
NewItem::Disallow (default) | Shows a disabled “No Results Found” row. It does not prevent typing unmatched text. |
NewItem::AllowInvalid | Shows 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
| Target | PhotoPickerSource::Library | PhotoPickerSource::Camera |
|---|---|---|
| Web | Browser file picker with image filtering and optional multiple selection | Requests the file input's camera-capture hint; the browser/device decides what UI is available |
| macOS | Image-file selection through NSOpenPanel | Returns Unavailable |
| iOS and iPadOS | System photo picker with optional multiple selection | System 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_coloris 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:
| Duration | Clock |
|---|---|
300ms or 0.3s | Elapsed milliseconds from the chassis's monotonic clock |
18 or 18f | Runtime 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.
| Curve | Character |
|---|---|
Linear | Constant progress through the segment |
Hold | Keep the first value until the segment ends |
InQuad | Begin slowly, then accelerate |
OutQuad | Begin quickly, then settle |
InOutQuad | Accelerate and then settle |
InBack, OutBack, InOutBack | Anticipation, 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:
| Value | Behavior |
|---|---|
| Numbers, sizes, rotations, colors | Continuous interpolation; colors use RGBA channels |
| Solid fills | Continuous RGBA interpolation |
| Gradient paints on fills and strokes, including solid↔gradient | Crossfade sampled paints in premultiplied RGBA; geometry and stop lists may differ |
TextStyle | Font size and fill interpolate; font selection, weight, style, underline, and alignment switch immediately |
CornerRadii | Each corner interpolates independently; zero is an angular corner |
| Vectors | Equal-length vectors interpolate element by element; a changed length switches immediately |
| Other data | Uses 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:
| Setting | Choices |
|---|---|
exit_mode | Flow 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.kind | Snap 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:
| Container | Use it for |
|---|---|
Group | A shared transform and layout space, with content free to overflow |
Frame | A rectangular or rounded-rectangular clipping boundary |
Mask | A 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:
- The content to show.
- The subtree that supplies the mask.
The alpha property selects how that second subtree is used:
| Mode | Source behavior | Content support |
|---|---|---|
alpha=false (default) | Geometric coverage gives a hard clipping boundary | Rendered and native content |
alpha=true | Painted alpha controls how much content remains visible at each point | GPU-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:
| Mode | Treatment |
|---|---|
Basic (default) | The original three scales of softly layered oval marks. |
Gauguin | Broad directional sweeps, simplified color planes, and subtle bristles. |
VanGogh | Evolving curling flows with highlight/shadow ridges suggesting thick paint. |
Signac | Fine, closely packed rectangular dabs interweaving pale and chromatic pigment. |
Cezanne | Varied patches of parallel marks and nested arcs, with broken blue-violet contours. |
Seurat | Smaller, 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:
| Property | Meaning |
|---|---|
width, height | The visible viewport, placed by ordinary Pax layout |
scroll_width, scroll_height | The extent of the scrollable content pane |
scroll_pos_x, scroll_pos_y | The 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 pages
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.
| Situation | Delivery, for the handlers that are bound |
|---|---|
| Initially nearby, or reentering proximity | Enter with current, then change with previous: None |
| Changed bounds or intersection while nearby | Change with previous and current |
| Unchanged sample | No event |
| Leaves proximity | Final change, then exit; both share the last inside and terminal outside samples |
| Remains outside, including a jump across the entire region | No event |
| Unmounts | Observation 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
- Layout and Responsiveness: parent frames, measurement, and responsive structure.
- Properties: bindings and derived state.
- Event Handling: coordinate conversion and gestures.
- Animation and Motion: playheads, tracks, and motion choices.
- Compositing and Effects: clips, masks, and native surfaces.
- Scroller API and Carousel API: property reference.
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:
| Part | Responsibility |
|---|---|
| Compiler | Analyzes the application and its templates, prepares the declarative program, and generates the connections to Rust component types and handlers |
| Runtime | Instantiates the scene, maintains reactive relationships, handles events, computes layout, and coordinates rendering and native-element updates |
| Chassis | Connects 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:
- The Group binds
@clicktoincrement. - The handler updates
num_clicks, which feeds the Text's expression. - It starts an eased transition on
current_rotation, which feeds the Group'srotateproperty. - The rotation settles, but
handle_pre_renderkeeps updatingticks. 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
- State and Properties — reactive values and dependency management.
- Compositing and Effects — graphics layers, native elements, and effect boundaries.
- Scrolling and Viewports — content extents, scrolling, and collection costs.
- Getting Started — workstation setup, commands, and build output.
- Developer Workflow and Tools — hot reload, inspection, screenshots, and local reference.
- Public API reference — Rust types and methods.
- Runtime and cartridge design notes — historical maintainer context, including architectural directions that are not current builder-facing capabilities.
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.
| Mode | Saved .pax changes | Saved Rust changes |
|---|---|---|
pax — default | Reload | Rebuild and restart manually |
off | Wait for restart | Rebuild and restart manually |
all — legacy | Reload, with a migration notice | Rebuild and restart manually |
logic — retired | Rejected with a migration diagnostic | Rebuild 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-sourcereplaces a component's entire.paxsource file.replace-nodereplaces 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:
| Example | Starting point |
|---|---|
pax-logo (default) | Animated standard-library logo with a position slider, replay, and a background toggle |
living-quilt | Interactive geometric tapestry with responsive components, motion, lighting, and GPU alpha masks |
ink-and-light | A responsive postcard with handwriting, local assets, and Rust interaction |
increment | A 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
- Getting Started — installation and the first run.
- Templates — the source structure behind the scene.
- Event Handling — input and application logic.
- Animation and Motion — declarative transitions and timelines.
- How Pax Runs — runtime structure and performance reasoning.
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 target | CLI value | Development workstation | Required target tools |
|---|---|---|---|
| Web browser | web | macOS, Debian/Ubuntu Linux, or Windows | Rust, OS build tools, wasm32-unknown-unknown, and wasm-pack |
| macOS application | macos | macOS | Rust, full Xcode, and the selected Apple Rust targets |
| iPhone application | ios | macOS | Rust, full Xcode, iOS SDK/simulator, and the selected Apple Rust target |
| iPad application | ipados | macOS | The 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:
| Build | Output |
|---|---|
| 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/.
- Build and test
.pax/build/release/web/locally. - Publish that directory's contents as the site's document root, including
snippets/, assets, and any dot-prefixed public files. - Enable HTTPS and correct content types on the host.
- 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.
Direct links and the base URL
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 destination | Rust target |
|---|---|
| macOS on Apple silicon | aarch64-apple-darwin |
| macOS on Intel | x86_64-apple-darwin |
| iPhone or iPad device | aarch64-apple-ios |
| iOS/iPadOS simulator on Apple silicon | aarch64-apple-ios-sim |
| iOS/iPadOS simulator on Intel | x86_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.
| Key | Applies to | Meaning |
|---|---|---|
title | Common, web, Apple | Browser title or Apple display name; defaults to the Cargo package name |
icon | Common, web, Apple | Image used for generated icons, including the fallback web favicon |
favicon | Web | Explicit favicon file; takes priority over icon-derived favicon generation |
site_name | Web | Open Graph site name; defaults to the resolved web title |
server_owned_prefixes | Web | Root-relative path prefixes delegated to ordinary browser navigation; defaults to []. See Server-owned web paths. |
site_url | Web | Absolute public HTTP(S) URL, without query or fragment; required for release builds with indexable concrete routes |
social_image | Web | Default social-preview image: an absolute HTTP(S) URL or a path relative to site_url |
social_image_alt | Web | Description of the social image; configure both image fields together |
bundle_identifier | Common, Apple | Application identity for the Apple host |
marketing_version | Common, Apple | User-facing version; defaults to Cargo package.version |
build_number | Common, Apple | Apple build number, expressed as a TOML string |
development_team | Common, Apple | Apple development team used during signing |
info_plist | Common, Apple | Typed table of Info.plist entries |
info_plist_file | Common, Apple | Project-owned XML or binary plist with a dictionary root |
entitlements | Apple target only | Typed table of requested signing entitlements |
entitlements_file | Apple target only | Project-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:
- The copied native host's defaults.
- Common
info_plist_file, then commoninfo_plist. - 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:
- Entitlement keys
- Information Property List keys
- Configuring the macOS App Sandbox
- Local-network privacy
- Background execution modes
- Code signing and provisioning profiles
- Privacy manifest files
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
- Getting Started — installation and a first successful run.
- Developer Workflow and Tools — editing, reload, and inspection.
- Routing — URL-driven UI and navigation.
- Text, Fonts, and Images — media sources and loading behavior.
- How Pax Runs — runtime, rendering backends, and release behavior.
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 on | Reference | Guide |
|---|---|---|
| Reactive state and computed values | Properties | State and Properties |
| Input events and handler arguments | Events | Events and Rust |
| Text and fonts | Text | Text, Fonts, and Images |
| Shapes, paths, and paint | Drawing values and drawing elements | Drawing and Styling |
| Routes and navigation | Router | Routing |
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
- async_runtime
- application_shutdown
- application
- animation
- color
- cursor
- drawing
- events
- layout
- math
- pax_value
- platform
- properties
- rendering
- shader_effect
- store
- transform
- unit_value
- variables
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
Gradient start point in the primitive's local coordinate space.
end
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
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
Center of the outer circle in the primitive's local coordinate space.
start
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.
NavigationTarget
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.
Right
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
- core::frame
- core::group
- core::import_settings
- core::link
- core::liquid_glass
- core::mask
- core::router
- core::scroller
- core::text
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
Link
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 routerroute.global_location: the full browser/native locationroute.params: named captures from:paramsegmentsroute.remainder: the unmatched tail after this branchroute.is_exactandroute.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
- drawing::handwriter
- drawing::lighting
- drawing::line
- drawing::path
- drawing::pax_logo
- drawing::rectangle
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
- drawing::pax_logo::animated_pax_logo_banner
- drawing::pax_logo::animated_pax_logo_post
- drawing::pax_logo::static_pax_logo
drawing::pax_logo::animated_pax_logo
Structs
AnimatedPaxLogo
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>
banner_opacity
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>
banner_elements
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
StaticPaxLogo
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.
banner_elements
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
- forms::checkbox
- forms::combo_box
- forms::dialogs
- forms::dropdown
- forms::photo_picker
- forms::radio_list
- forms::slider
- forms::tabs
- forms::textbox
- forms::toast
- forms::tooltip
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
Dropdown
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
Carousel
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
- cartridge
- component
- conditional
- container
- engine
- layout
- properties
- rendering
- repeat
- router
- scene_geometry
- shader_effect
- settings_motion
- slot
- viewport_proximity
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_childrenis the raw transport family used bySlotreceived_childrenis the normalized semantic payload that this node should treat as content from its callerretained_received_childrenare former received children kept alive only so@outtransitions 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.
navigate_to
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:
- 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
- Lifecycle slice of the received payload
- active received children: present in
NodeContext::received_children - retained exiting received children: present in
NodeContext::retained_received_children
- active received children: present in
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
- engine::layer_surface
- engine::layer_tiling
- engine::node_interface
- engine::occlusion
- engine::pax_gpu_render_context
- engine::piet_render_context
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:
- Expand nodes & compute properties; recurse entire instance tree and evaluate ExpandedNodes, stitching together parent/child relationships between ExpandedNodes along the way.
- Compute layout (z-index & TransformAndBounds) by visiting ExpandedNode tree in rendering order, writing computed rendering-specific values to ExpandedNodes
- 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.
modal
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
modal
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
DropdownPatch
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
NavigationPatch
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>
style_link
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)
DropdownCreate(AnyCreatePatch)
DropdownUpdate(DropdownPatch)
DropdownDelete(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)
Navigate(NavigationPatch)
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;
Propertyalone 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:
- it keeps the runtime boundary clear
- 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.