# Technical Documentation: fschutt/azul

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/fschutt/azul)

### Overview (README.md)

# AZUL - Desktop GUI framework


[](https://github.com/fschutt/azul/actions/workflows/rust.yml)
[](https://github.com/fschutt/azul/actions/workflows/rust.yml)
[](LICENSE)
[]()
[](https://deps.rs/repo/github/fschutt/azul)


> Azul is a free, functional, reactive GUI framework for Rust, C and C++,
built using the WebRender rendering engine and a CSS / HTML-like document
object model for rapid development of beautiful, native desktop applications

###### [Website](https://azul.rs/) | [Releases](https://azul.rs/ui/releases) | [User guide](https://azul.rs/ui/guide) | [API documentation](https://azul.rs/ui/api)

## Current Status

> [!WARNING]
> **This repository is currently under heavy development. Azul is NOT usable yet.**
> 
> APIs may change frequently and features may be incomplete or unstable.
>
> If you are looking for the old README, see [README-OLD.md](/README-OLD.md)
> 
> The current release is from 2+ years ago, see the [releases page](https://github.com/fschutt/azul/releases).
> 
> Visit https://azul.rs/reftest in order to see the current testing and development
> of the core rendering / HTML layouting engine.

## Building from source

```bash
cargo build -r -p azul-doc
./target/release/azul-doc codegen all                       # generates the API surface of every binding
cargo build -r -p azul-dll --features build-dll             # libazul.{so,dylib,dll} in target/release
```

azul-doc is a multitool that generates all the code *necessary* for having a stable public API that works across multiple languages. 

```bash
# a Rust app against the freshly built library, from the in-repo crate
export AZ_LINK_PATH=/path/to/azul/target/release
cargo add azul-dll --rename azul --path /path/to/azul/dll --no-default-features --features link-dynamic
cargo run --release
```

---

### Doc README (`doc/README.md`)

# azul-doc

Build tool, FFI code generator, and W3C spec verification pipeline for the
Azul CSS layout engine.

```bash
cargo build --release -p azul-doc
# output: target/release/azul-doc
```

## `api.json`

`api.json` is the master schema defining the entire Azul public API. It
describes every module, struct, enum, function, callback, and constant that
is exposed through FFI. The codegen system reads this file and generates
language bindings for Rust, C, C++, and Python.

The typical workflow is:

1. Modify Rust source code (add/change/remove public types or functions)
2. Run `autofix` to synchronize `api.json` with the workspace
3. Run `codegen all` to regenerate all language bindings
4. Build the DLL: `cargo build -p azul-dll --features build-dll --release`

### `autofix` — Synchronize api.json with Source Code

Autofix scans all public types and functions in the Rust workspace, compares
them against `api.json`, and generates patches to bring them in sync. It
handles FFI safety checks, transitive type dependencies, and module placement.

```bash
azul-doc autofix                     # Full run: generate patches into target/autofix/
azul-doc autofix explain             # Preview pending patches without applying
azul-doc autofix apply               # Apply all patches to api.json
azul-doc autofix apply <dir>         # Apply patches from a specific directory
azul-doc autofix apply safe <dir>    # Apply only safe (path-correction) patches

# Add/remove individual items
azul-doc autofix list <Type>         # Compare Type's functions: source vs api.json
azul-doc autofix add <Type.method>   # Add function to api.json (+ transitive deps)
azul-doc autofix add <Type.*>        # Add all functions for a type
azul-doc autofix remove <Type.method>  # Remove function from api.json

# Debugging / analysis
azul-doc autofix difficult           # Rank types by FFI difficulty
azul-doc autofix internal            # Show types that should be internal-only
azul-doc autofix modules             # Show types in the wrong module
azul-doc autofix deps                # Analyze function deps on difficult/internal types
azul-doc autofix debug type <T>      # Debug a type in the workspace index
azul-doc autofix debug chain <T>     # Debug type resolution chain
azul-doc autofix debug api <T>       # Debug type: api.json vs workspace
azul-doc autofix debug file <path>   # Debug parsing a specific Rust source file
```

### `normalize` — Clean Up api.json

```bash
azul-doc normalize    # Canonicalize types + remove cross-module duplicates
```

Performs all api.json cleanup in one pass:

- Canonicalize array types (`"[f32; 20]"` → `{type: "f32", arraysize: 20}`)
- Normalize pointer aliases (`"*mut c_void"` → `{target: "c_void", ref_kind: "mutptr"}`)
- Extract embedded generics (`"Foo<Bar>"` → `{target: "Foo", generic_args: ["Bar"]}`)
- Normalize enum variant pointer types
- Remove duplicate type entries across modules (keeps canonical location only)

Only writes if content actually changed. Always produces the same output on subsequent runs.

### `codegen` — Generate Language Bindings

Reads `api.json` and generates FFI bindings for all target languages.
Output goes to `target/codegen/`.

```bash
azul-doc codegen all      # Generate all targets (recommended)
azul-doc codegen rust     # Rust public API (azul.rs)
azul-doc codegen c        # C header (azul.h)
azul-doc codegen cpp      # C++ headers (azul03..23.hpp, one per C++ standard)
azul-doc codegen python   # Python/PyO3 bindings (python_api.rs)
```

`codegen all` generates 14 files: static/dynamic/build DLL APIs, re-exports,
C header, 6 C++ headers (C++03 through C++23), Rust API, Python bindings,
and memory layout tests.

#### How `codegen` Output Is Used

`dll/src/lib.rs` pulls in generated code via `include!()` macros pointing at
`target/codegen/`. Which files are included depends on the active cargo
feature:

| `azul-dll` feature | Generated code | File contains |
|---------|---------------|------------------|
| `build-dll` | `dll_api_build.rs` | `#[no_mangle] extern "C"` functions for the shared library |
| `link-static` | `dll_api_static.rs` | Types + trait impls via transmute (full Rust stack compiled in) |
| `link-dynamic` | `dll_api_dynamic.rs` | Types + `extern "C"` declarations (links against pre-built `.dylib`/`.so`/`.dll` at runtime) |
| `python-extension` | `python_api.rs` | PyO3 extension module (`import azul` from Python) |
| (always) | `reexports.rs` | Rust-friendly re-exports without `Az` prefix (`use azul::prelude::*`) |
| (test) | `memtest.rs` | Size and alignment verification tests |

`build-dll`, `link-static`, and `link-dynamic` are the three mutually exclusive
link modes. `build-dll` compiles the full desktop stack (windowing, OpenGL,
fonts) and exports C-ABI symbols. `link-static` does the same but without
`#[no_mangle]` exports. `link-dynamic` compiles no internal crates — it only
declares `extern "C"` stubs that resolve at runtime against a pre-built shared
library.

`python-extension` is independent of these three modes. It gates the
`python_api.rs` include behind `#[cfg(feature = "python-extension")]` and
uses PyO3 to wrap each API function as a Python method. The generated Python
module is compiled as a native extension (`.so`/`.pyd`) that Python loads
directly via `import azul` — no C shared library or dynamic linking involved.

## `reftest` - Run Visual Reference Tests

`azul-layout` is tested against Google Chrome via pixel-comparison layout tests 
that render XHTML through both Azul and Chrome, then compare the results. 

Test files live in `doc/working/` and `doc/xhtml1` (inactive tests). The idea is
that we copy more and more tests from `xhtml1` to `working` to activate them.

```bash
azul-doc reftest                     # Run all reftests, generate HTML report
azul-doc reftest <test_name>         # Run a single test, open in browser
azul-doc reftest headless <test>     # Run single test without Chrome, print debug to stdout
```

### `debug` - LLM-Assisted Test Debugging

When a reftest fails, `debug` bundles everything needed to diagnose the
problem into a single Gemini prompt:

- The XHTML test source
- Screenshots (Azul rendering, Chrome reference, pixel diff)
- Chrome's layout tree (JSON from DevTools)
- Azul's display list output
- Layout engine source code (budget-limited to fit token window)
- CSS parse warnings and debug messages
- An optional user question for focused analysis

Gemini receives this context and returns an analysis of what the layout
engine is doing wrong and how to fix it. You can then pass this analysis
to Claude for fixing or fix it yourself.

```bash
azul-doc debug <test> [question]        # Debug a failing reftest with Gemini
azul-doc debug <test> --dry-run         # Generate prompt without sending to Gemini
azul-doc debug <test> --no-screenshots  # Skip screenshot capture
azul-doc debug <test> --working-diff    # Include git diff of doc/working/
```

### `debug-regression` - Track Reftests Across Commits

Sometimes (note: only sometimes!) we want to debug when exactly a test started
to regress. We can use `debug-regression` to track how reftest results change 
across a set of git commits. Provide either a file containing commit hashes (one per line) 
or a single git ref. For each commit, the tool checks out the code in a worktree, 
builds the layout engine (`azul-layout` has to be buildable at the commit), 
runs all reftests, and saves the pixel-diff results. The collected data shows
visual and statistical reports.

```bash
# Run reftests at each commit listed in the file (one hash per line)
azul-doc debug-regression <commits.txt>

# Run reftests at a single git ref
azul-doc debug-regression <git-ref>

# Generate reports from previously collected data
azul-doc debug-regression visual                     # HTML report with side-by-side images
azul-doc debug-regression statistics                 # Text diff statistics (stdout)
azul-doc debug-regression statistics prompt          # Generate Gemini prompt from statistics
azul-doc debug-regression statistics send            # Send statistics to Gemini for analysis
azul-doc debug-regression statistics send -o <path>  # Save Gemini response to file
```

## `autodebug` — Automated Reftest Bug-Finding Pipeline

`autodebug` combines the reftest infrastructure with the parallel Claude agent
executor to automatically discover failing reftests, generate rich diagnostic
prompts, and dispatch agents to analyze and fix each bug.

The pipeline runs each `.xht` test at three screen sizes (mobile 375×667,
tablet 768×1024, desktop 1920×1080), compares Azul's render against Chrome
pixel-by-pixel, and for any test exceeding 0.5% pixel difference generates a
structured agent prompt containing:

- Codebase orientation (solver3, text3, cpurender)
- XHTML source, CSS warnings, layout debug trace
- Screenshot file paths (agents view images via Claude's `Read` tool)
- Pixel diff analysis: connected-component regions with position descriptions
  and heuristic cause classification (border offset, margin collapse, missing
  content, anti-aliasing, layout shift)
- Chrome layout JSON vs Azul display list

Each agent receives one failing test, works in an isolated git worktree, and
produces a commit with the fix (or a diagnostic report if the fix isn't clear).

```bash
# Full run on the default 'working' directory
azul-doc autodebug claude-exec

# Run on the xhtml1/ test directory (9000+ tests)
azul-doc autodebug claude-exec doc/xhtml1

# Generate prompts only (inspect before dispatching agents)
azul-doc autodebug claude-exec --dry-run

# Run a single test
azul-doc autodebug claude-exec --test=abspos-containing-block-initial-001

# Customize parallelism and timeout
azul-doc autodebug claude-exec --agents=8 --timeout=900

# Only test specific screen sizes
azul-doc autodebug claude-exec --sizes=mobile,desktop

# Reuse cached Chrome screenshots (faster re-runs)
azul-doc autodebug claude-exec --skip-chrome

# Re-queue previously failed tests
azul-doc autodebug claude-exec --retry-failed

# Monitor progress (in a separate terminal)
azul-doc autodebug claude-exec --status

# Collect patches from completed agents
azul-doc autodebug claude-exec --collect

# Clean up worktrees and branches
azul-doc autodebug claude-exec --cleanup

# Override the Claude model
azul-doc autodebug claude-exec --model=claude-sonnet-4-6
```

The last positional argument is the test directory (defaults to `doc/working/`).

### Performance

Phase 1 uses two key optimizations for large test sets:

- **Chrome CDP (DevTools Protocol)**: A single persistent Chrome instance
  communicates via WebSocket instead of spawning one process per screenshot.
  This reduces per-screenshot overhead from ~1-2s to ~130ms.
  Falls back to per-process mode if CDP launch fails.
- **Async font registry**: `FcFontRegistry` from rust-fontconfig spawns
  scout + builder threads in the background during Chrome Phase 1a.  By the
  time Azul renders start, all system fonts are already loaded (0s wait).
  The cache is cloned once and shared across all rayon render threads.
- **Parallel rendering**: Azul CPU renders and pixel diffs run in parallel
  via rayon.

Output goes to `doc/target/autodebug/`:

```
doc/target/autodebug/
├── screenshots/    # Chrome PNGs + Azul WebPs per test per size
├── prompts/        # Generated .md prompts + status files (.done/.failed/.taken)
├── patches/        # Extracted .patch files from agent commits
├── reports/        # Agent analysis reports (when fix is uncertain)
└── summary.json    # Overall results
```

## `apply-midlevel` — Interactive Commit Replay

Walks every commit on a reference branch (e.g. `midlevel-fixes-reference`)
and prompts you, one at a time, whether to apply, refine, skip, or reject
each one. A pre-analyzer Claude agent reads the commit + current tree and
recommends an action; you decide.

```bash
azul-doc autoreview apply-midlevel --reference=<branch-or-tag> [options]
```

Options:

| Flag | Default | Notes |
|------|---------|-------|
| `--reference=<ref>` | required | Branch or tag to replay |
| `--base=<ref>` | `origin/layout-debug-clean` | Diverge point |
| `--model=<name>` | `opus` | Apply-agent model |
| `--analyzer-model=<name>` | same as `--model` | Pre-analyzer model |
| `--no-analyze` | off | Skip the pre-analysis agent |
| `--no-telegram` | off | Don't mirror prompts to Telegram (see below) |

Per commit you choose:

```
[y] yes      apply using the current plan
[p] plan     refine the plan: add feedback, analyzer revises
[s] skip     don't apply now, come back later
[r] reject   don't apply, record as rejected with reason
[d] diff     checkout the commit so your editor refreshes
[q] quit     save progress and exit
```

Progress is saved to `doc/target/autoreview/apply-midlevel/progress.json`
after every decision. Re-running resumes where you left off.

### Answer prompts from your phone (Telegram)

A long replay can take days. To answer prompts when you're away from the
PC, pair `azul-doc` with a Telegram bot — once paired, every prompt is
mirrored to a chat with you and any reply (tap a button, or type
free-form feedback) feeds straight into the running `apply-midlevel`.
Local stdin still works; whichever input arrives first wins.

**One-time setup:**

1. **Create a bot.** Open Telegram, message [@BotFather](https://t.me/BotFather),
   send `/newbot`, follow the prompts. Pick any name and any handle ending
   in `bot`. BotFather replies with a token like `1234567890:ABCdef-...`.

2. **Pair.** Run the wizard:

   ```bash
   azul-doc telegram-setup
   ```

   Paste the token when asked. The wizard verifies the token, prints your
   bot's `@handle`, and waits for you to message `/start` to it from your
   phone. As soon as it sees your message, it stores the chat-id and sends
   a confirmation.

3. **Done.** Future runs of `apply-midlevel` automatically mirror prompts
   to your bot. Each prompt comes with a tap-keyboard:

   | Stage | Buttons | Free-form text |
   |-------|---------|----------------|
   | Pre-apply | `apply` `refine` `diff` `skip` `reject` `quit` | becomes analyzer feedback |
   | Post-apply | `accept` `edit` `revert` `quit` | becomes "edit" instructions |

   The legacy single-letter shortcuts (`y/p/s/r/d/q` and `y/e/r/q`) still
   work too — useful when typing on the local terminal.

**Diff preview on the phone.**

- Tap **diff** at the pre-apply prompt → the bot ships the reference
  commit's full patch as `<short-sha>.patch`. Telegram renders it inline
  for scrolling, and "Open in…" lets you pass it to GitHub mobile,
  GitKraken, or any diff viewer that handles `.patch` files.
- Right after a successful apply, the bot **automatically** sends the
  applied diff (`applied-<short>.patch`) before showing the
  accept/edit/revert prompt — no extra tap needed. This is the diff
  the agent actually produced; review it on your phone before deciding.

**Configuration locations** (checked in this order):

| Source | Vars / Path |
|--------|-------------|
| Env vars | `AZ_DOC_TG_TOKEN` + `AZ_DOC_TG_CHAT_ID` (also `TELEGRAM_BOT_TOKEN` + `TELEGRAM_CHAT_ID`) |
| File | `~/.config/azul-doc/telegram.toml` (created by `telegram-setup`, mode `0600`) |

**Disable per run:** add `--no-telegram` to the `apply-midlevel` invocation.

**Notifications missing on your phone?** The bot always sends with
`disable_notification: false`, so the message itself is alerting. If you
don't hear pings, the cause is almost always client-side: long-press
the bot chat in Telegram → check the bell icon isn't muted, and verify
the Telegram app has notification permission at the OS level.

**Notes:**

- `[d] diff` from the local terminal still does the editor checkout
  (detached HEAD on the commit, restored on Enter). From Telegram it
  ships a `.patch` document instead — no working-tree changes.
- The phone message itself is just commit metadata + the analyzer's
  `[CATEGORY] / Why: / Plan: / Suggested user action:` tail; the full
  diff goes through the document attachment, not the message body.
- A network blip on the long-poll prints to stderr, sleeps 5s, and retries
  — it won't kill the run.
- Diff documents are capped at 1 MiB to keep slow phone connections
  responsive; oversized diffs get truncated with an explanatory tail line.

## `autodoc` — Generate the User Guide in Parallel

Reads `doc/autodoc-groups.toml` and dispatches one Claude agent per group.
Each agent writes the markdown pages declared in its group to
`doc/guide/en/`, with YAML frontmatter that records the git SHA and tracked
source files at generation time. The companion `autodoc-check` command
later flags pages whose tracked files have changed since.

Full operational reference: [`AUTODOC.md`](./AUTODOC.md). Manifest:
[`autodoc-groups.toml`](./autodoc-groups.toml).

```bash
azul-doc autoreview autodoc                  # Dispatch one agent per group
azul-doc autoreview autodoc --agents=N       # Throttle concurrency
azul-doc autoreview autodoc --file=GROUP_ID  # Run a single group
azul-doc autoreview autodoc --dry-run        # Generate prompts only

azul-doc autoreview autodoc-check            # Flag stale pages → outdated.md
azul-doc autoreview autodoc-check --strict   # Same, exit non-zero on any stale page (CI)
azul-doc autoreview autodoc-screenshots      # Render azul-render fences to PNG
```

### Pipeline stages

```
autoreview ──► autodoc ──► autodoc-screenshots ──► deploy
(per-file)     (per-group)  (PNG renders)         (website)
```

1. `autoreview` produces per-file reports under `doc/target/autoreview/reports/`.
2. `autodoc` reads `autodoc-groups.toml`, builds one prompt per group with
   the group's tracked source, the per-group `design_docs` (intent, not
   truth), and the global writing-style + max-effort thinking rules from
   `[meta]`.
3. Agents write pages to their declared paths under `doc/guide/en/`.
4. `autodoc-screenshots` renders any `azul-render` fences via `HeadlessWindow`
   into `doc/guide/en/screenshots/<page-slug>/<name>.png`.
5. `deploy` walks `doc/guide/en/` at runtime, parses each page's
   frontmatter for ordering, and emits the final HTML. The guide index
   page is built from the frontmatter (`title`, `short_desc`, `audience`,
   `guide_order`, slug hierarchy) — no separate SUMMARY file is involved.

### Manifest at a glance

`doc/autodoc-groups.toml` (schema v3) defines three trees:

| Tree | Audience | Contents |
|------|----------|----------|
| `getting-started` | external | Linear teaching path: hello-world (per-language) → architecture → understanding-refany → DOM → styling → layout → text → images → widgets → events → timers → animations → scrolling → windowing |
| `advanced` | external | Non-core systems, ramps to web: debugging / profiling / e2e-testing → I/O (xml, file-dialogs, clipboard) → background-tasks + networking → a11y + headless → codegen + bindings → deploying-web → security |
| `contributor` | contributor | Code organization, build-and-codegen, per-system internals (DOM, CSS, layout, text, images, events, rendering, shell2, menus, a11y, async, web) |

Each `[[group]]` declares `id`, `tree`, `audience`, `agent_strategy`,
`tracked_files` (or `tracked_globs`), an optional `design_docs` list,
and one or more `[[group.outputs]]` with `slug`, `path`, `title`,
`maturity`, optional `guide_order` and `prerequisites`. The global
`[meta.writing_style]` and `[meta.agent_thinking]` blocks ship into
every prompt; agents are anchored on the Servo book style and the
project's existing `shared_context_files` (AUTODOC.md,
architecture.md, api.json).

`design_docs` lists files in `scripts/` (e.g. `TEXT_INPUT_ARCHITECTURE_V4.md`)
that capture the original system design. They are *intent*, not authoritative
source — the prompt explicitly tells the agent: **the code is truth, the
design docs are context**. The agent must verify each claim against
`tracked_files` and document divergences inline (e.g. *"the original design
proposed X; the implementation took approach Y"*) rather than reproducing
stale content.

### Two-signal staleness check

`autodoc-check` writes `doc/target/autoreview/autodoc/outdated.md` listing:

- **Source-changed** pages — `tracked_files` had commits since
  `last_generated_rev` in the page's frontmatter (`git log` query).
- **Translation-stale** pages — translated page's `source_hash` no longer
  matches the SHA-256 of its canonical English body. CI fails on either.

### Translations

Translations live in language-prefixed sibling directories under
`doc/guide/`. The English tree is canonical; every other language mirrors
it page-for-page with localized slugs.

```
doc/guide/
├── en/                                  # canonical (the only tree autodoc writes)
│   ├── architecture.md                  # slug: architecture
│   ├── hello-world.md                   # slug: hello-world
│   ├── layout/flex.md                   # slug: layout/flex
│   └── screenshots/<page>/<name>.png
└── de/                                  # German translation (manual or future `translate` agent)
    ├── architektur.md                   # slug: architektur,    canonical_slug: architecture
    ├── hallo-welt.md                    # slug: hallo-welt,     canonical_slug: hello-world
    └── layout/flexbox.md                # slug: layout/flexbox, canonical_slug: layout/flex
```

A translated page's frontmatter carries five extra fields beyond the
canonical set:

```yaml
---
slug: architektur                        # localized URL — used in the URL path
title: Architektur
language: de
canonical_slug: architecture             # English-side identity (the page this mirrors)
audience: external
maturity: mature
prerequisites: [hello-world]             # always English canonical_slugs; deploy localizes
source_rev: 99be42b08ac44fcb01f1508e8...  # git SHA of canonical/architecture.md at translation time
source_hash: a1b2c3d4...                  # SHA-256 of canonical body bytes (post-frontmatter)
generated_at: 2026-05-02T12:00:00Z
---
```

How the pieces interact:

| Field | Role |
|-------|------|
| `slug` | URL segment under `/<lang>/`. Localized so `/de/architektur` reads naturally in German. |
| `canonical_slug` | Anchor to the English page. Deploy uses it to find sibling translations and resolve `prerequisites`. |
| `prerequisites` | Always lists English canonical slugs. Deploy maps each to the localized slug for the current `language` at render time, so `prerequisites: [hello-world]` on a German page links to `/de/hallo-welt`, not `/en/hello-world`. |
| `source_rev` | Git SHA of the canonical at translation time. Cheap drift signal (grep `git log` for changes). |
| `source_hash` | SHA-256 of the canonical body bytes (after frontmatter). Authoritative drift signal — `autodoc-check --strict` fails CI when this no longer matches the current canonical. |
| `tracked_files` | **Empty on translations.** Translations inherit staleness from their canonical, not from source files. |

When you edit a canonical English page, its body bytes change → SHA-256
changes → every translation's `source_hash` no longer matches → next CI
deploy fails until each translation is regenerated.

There is no `translate` subcommand yet. Translations are produced
manually (or by a future agent dispatch); the spec above is what the
website and `autodoc-check` already understand.

### Outputs

```
doc/target/autoreview/autodoc/
├── prompts/<group-id>.md          # Generated agent prompts
├── prompts/<group-id>.md.taken    # Sentinel: agent in flight
├── prompts/<group-id>.md.done     # Sentinel: agent succeeded
├── prompts/<group-id>.md.failed   # Sentinel: agent failed
├── outdated.md                    # autodoc-check report
└── screenshots-manifest-<lang>.json  # Cached render manifest

doc/guide/<lang>/
├── architecture.md                # Canonical architecture doc (en only)
├── <slug>.md                      # Generated guide pages
├── <parent>/<child>.md            # Nested pages render as a sub-tree under <parent>.md
└── screenshots/<page>/<name>.png  # Rendered azul-render fences (en only)
```

## Website Deployment

Builds the full azul.rs website (API docs, release pages, examples, reftest
results) into `doc/target/deploy/`. For the CI usage, this would expect the `dll` 
files to exist, so for debugging CSS / doc / blog posts, etc. the `deploy debug`
command builds the website files without this command.

```bash
azul-doc deploy                      # Production build (inlined CSS, absolute URLs)
azul-doc deploy debug                # Debug build (external CSS, relative paths)
azul-doc deploy with-reftests        # Debug build + run reftests
```

## `spec` - W3C Spec Verification Pipeline

The `spec` subcommand verifies and improves CSS layout compliance against W3C
specifications. It auto-downloads W3C spec HTML sources (CSS Display 3, CSS 2.2
visuren/visudet/box/tables, CSS Text 3), extracts individual paragraphs by
keyword matching, and generates one prompt per paragraph. 

Parallel Claude agents then review each paragraph against the layout engine source 
code and produce patches. Gemini analyzes the patches for quality, architecture, 
and merge planning before agents apply them.

The patches will always contain `// +spec` comments that link the source code back to
the paragraph the implementation came from, so that humans or agents can quickly verify
where a certain W3C feature is implemented or where a certain line of code came from:

```rust
// +spec:block-formatting-context-p001 - BFC establishment rules
// +spec:css22-box-8.3.1-p1 - margin collapsing between siblings
if block.get_containing_width() > block.escaped_margin() {
   // ...
}
```

The `spec status` command scans for these markers to track coverage.

> [!NOTE]
> 
> Gemini has a much larger context window (1M) than Claude (200K) and is good for a
> more "holistic" architecture analysis. So, Gemini is used for the ".md reviews"
> and Claude agents are used for the actual implementation.

For sending the generated .md files to Gemini, use [AIStudio](https://aistudio.google.com/prompts/new_chat),
this way you don't need to spend money on Gemini. Since the Claude Code agents would
use a lot of tokens, it's generally only advisable to use this on a Max 20x Plan, `azul-doc`
will early-exit if it detects API usage.

### Full Pipeline

```
                             ┌─ review-arch ─→ --review-arch ──┐
claude-exec ──> review-md ───┤                                 ├──> agent-apply
 (patches)    (patch review) ├─ refactor-md ─→ --refactor-md ──┤   (Claude agents)
                             └─ groups-json ─→ --groups-json ──┘
```

Each analysis step (`--review-md`, `--review-arch`, `--refactor-md`, `--groups-json`) 
generates a prompt .md file that is fed to Gemini later. Gemini's response then
becomes an input flag for subsequent steps and ultimately for `agent-apply`.

Command names match flag names:

| Command | What it does | `agent-apply` flag |
|---------|--------------|--------------------|
| `review-md` | Patch quality review (CODE/ANNOT, conflicts) | `--review-md` |
| `review-arch` | Cross-patch architecture review (tunnel vision fix) | `--review-arch` |
| `refactor-md` | Refactoring plan (groundwork before applying) | `--refactor-md` |
| `groups-json` | Merge groups JSON (APPLY/MERGE/PICK_ONE/SKIP) | `--groups-json` |

#### Stage 1: Generate Patches

Spec paragraphs are auto-extracted from auto-downloaded W3C HTML sources. The
extractor matches paragraphs to features using keyword lists defined in the
skill tree (e.g. "block formatting context", "margin collapsing", "normal flow"
→ `block-formatting-context` feature). Each matched paragraph becomes a
self-contained prompt file containing the spec text, feature context, and
source files to review.

```bash
# Downloads specs, builds prompts, and runs parallel Claude agents
# Each agent sees ONE paragraph + the layout source code
azul-doc spec claude-exec # --agents=8 // default is 8 parallel agents
azul-doc spec claude-exec --retry-failed  # Retry timed out / failed prompts
# In a separate terminal
azul-doc spec claude-exec --status        # Check progress
```

#### Stage 2: Analyze Patches with Gemini

Each command generates a prompt file, feed it to Gemini via [AIStudio](https://aistudio.google.com/prompts/new_chat), 
save the output. Each step receives the outputs of all previous steps.

```bash
# 2a. Patch quality review (CODE/ANNOT categorization, conflict clusters)
azul-doc spec review-md --no-src <patch-dir>

# 2b. Architecture review — solves the "tunnel vision" problem:
#   Each claude-exec agent only saw one spec paragraph. This gives Gemini
#   all patches + original paragraphs to find cross-cutting concerns.
azul-doc spec review-arch \
  --review-md <REVIEW.md> \
  <patch-dir>

# 2c. Refactoring plan (groundwork abstractions before applying patches)
azul-doc spec refactor-md \
  --review-md <REVIEW.md> \
  --review-arch <ARCH.md> 
  <patch-dir>

# 2d. Merge groups — receives ALL prior analysis for well-informed grouping
azul-doc spec groups-json \
  --review-md <REVIEW.md> \
  --review-arch <ARCH.md> \
  --refactor-md <REFACTOR.md> \
  <patch-dir>
```

#### Stage 3: Apply Patches via Agents

Finally, after doing all of this analysis of the patches, their quality and
what we need to do to cleanly refactor them, the `groups-json` outputs a `.json`
file where the patches are "grouped", because some patches might cover similar
topics. So if we have 800 patches from an agent run, we can then group them into 
a "merge group" if similar patches patch the same functionality (this happens 
because the `claude-exec` agents don't see each other since they run in parallel).

```bash
azul-doc spec agent-apply \
  --groups-json <GROUPS.json> \
  --refactor-md <REFACTOR.md> \
  --review-md <REVIEW.md> \
  --review-arch <ARCH.md> \
  <patch-dir>
```

Important is the `GROUPS.json` here, which contains the overview of how the (sequential) agents
will run. Each agent processes one "merge group" through 5 phases:

1. **Refactoring** — implement groundwork relevant to this group
2. **LLM-apply** — apply patch semantic intent to current code (similar to `cherry-pick`, but with semantics)
3. **Compile** — `cargo check -p azul-dll --features build-dll`
4. **Review** — verify again against the original W3C spec, fix issues and compile again
5. **Commit** — use `git add -p` to create ~2-5 semantic commits by hunk (each must compile independently)

Original `.patch` files are never moved or deleted. Progress is tracked in
`<patch-dir>/agent-apply-status.json`, which records the outcome (applied /
skipped / failed), commit count, and failure reason for each group. This
status file also enables resuming — re-running `agent-apply` skips groups
that were already successfully processed. A group counts as "applied" only
if the agent exits successfully AND makes at least one git commit.

### Spec Utilities

```bash
azul-doc spec download              # Download W3C spec HTML sources
azul-doc spec tree                  # Display CSS feature skill tree
azul-doc spec build-all             # Build per-paragraph agent prompts
azul-doc spec status                # Verification progress (scans +spec: markers)
azul-doc spec paragraphs            # All paragraphs grouped by feature
azul-doc spec paragraphs <feature>  # Paragraphs for one feature (with text)
azul-doc spec annotations           # Scan source for +spec: annotation comments
```

Finally, you can always get help a `spec` command with:

```bash
azul-doc spec <command> --help      # Detailed help for any subcommand
```

### Adding New W3C Spec Documents

The goal of the `spec` command is to bring the source code up to what the original 
W3C `.html` files say the source code should do (esp. checking the various if / else statements, 
box model, semantic restrictions). Now, it might be that some new feature needs to be implemented.

1. **Register the spec** in `doc/src/spec/downloader.rs` — add the URL to
   `SPECS`. The downloader stores HTML in `doc/target/w3c_specs/`.

2. **Add a feature node** in `doc/src/spec/skill_tree.rs` — define keywords
   used to match spec paragraphs to the feature.

3. **Run agents**: `azul-doc spec claude-exec`
   (automatically runs `download` + `build-all` if prompts are missing)

4. **Repeat Stage 2 & 3**

The overall assumptions is: if a source code marker is in the Rust code, the feature has
been implemented correctly (since each `claude-exec` agent prompt contains only ~2k tokens, 
the likelyhood that the patch matches the source code is very high, the hard part is in 
correctly merging the patches due to the "tunnel vision" problem). For final verification,
run the `reftest` again and activate more tests from the `xhtml1` source.

### Other Utilities

```bash
azul-doc print [path]               # Print API tree (e.g. "print app.App.new")
azul-doc discover [pattern]         # List all public functions in workspace
azul-doc nfpm [version]             # Generate nfpm.yaml for OS packaging
```

## CI Pipeline

The GitHub Actions workflow (`.github/workflows/rust.yml`) uses azul-doc
in two modes: **ci** (on push/PR) and **deploy** (manual trigger).

### CI Mode (automatic)

1. **Lint & Static Checks** — runs `autofix`, applies patches, runs
   `normalize`, verifies no uncommitted changes remain. Then runs
   `codegen all` and `cargo test` on the DLL.
2. **Build DLL** — `codegen all` + `cargo build -p azul-dll --features build-dll`
   on Linux, macOS, and Windows.
3. **Reftests** — `codegen all` + `reftest` with Chrome screenshot caching.
4. **C/C++ Examples** — copies `azul.h` and C++ headers from
   `target/codegen/`, compiles all example files with clang/clang++.
5. **Website Build** — `codegen all` + `nfpm` + `deploy` (production).
6. **OS Packaging** — uses nfpm config to build `.deb` and `.rpm` packages.

### Deploy Mode (manual)

Downloads all CI artifacts (DLLs, website, reftests, packages), assembles
them into a single website directory, and deploys to GitHub Pages.

## Project Structure

```
doc/src/
├── main.rs             # CLI entry point and command dispatch
├── api.rs              # normalize (type canonicalization)
├── autofix/            # api.json ↔ workspace synchronization
├── codegen/            # Language binding generators
│   └── v2/             # IR builder → Rust/C/C++/Python generators
├── patch/              # dedup, api.json patch application
├── reftest/            # Pixel-comparison layout tests + LLM debugging
│   └── autodebug.rs    # Automated bug-finding pipeline (multi-res diff + agent dispatch)
├── docgen/             # Website documentation generator
├── dllgen/             # DLL build + deploy
└── spec/               # W3C spec verification pipeline
    ├── mod.rs           # Command routing and CLI parsing
    ├── executor.rs      # Agent execution, all pipeline commands
    ├── skill_tree.rs    # 16-feature skill tree with dependency ordering
    ├── downloader.rs    # W3C spec HTML fetcher
    ├── extractor.rs     # Paragraph extraction from HTML specs
    ├── reviewer.rs      # Prompt generation for review
    └── paragraphs.rs    # Known spec paragraph registry
```

Key layout source files reviewed by spec agents:

```
layout/src/solver3/
├── fc.rs           # Formatting context solver (BFC, IFC, floats)
├── sizing.rs       # Width/height calculation (CSS 2.2 §10.3/§10.6)
├── positioning.rs  # Absolute/relative positioning
├── geometry.rs     # Box model geometry structs
├── layout_tree.rs  # DOM-to-layout tree conversion
├── getters.rs      # CSS property accessors
├── mod.rs          # Containing block resolution
└── taffy_bridge.rs # Flexbox integration

layout/src/text3/
├── cache.rs        # Text layout cache, constraint builder
├── knuth_plass.rs  # Knuth-Plass line breaking algorithm
└── glyphs.rs       # Glyph metrics, line height
```

---

### Doc THEME CHAIN ANALYSIS 2026 09 12 (`doc/THEME_CHAIN_ANALYSIS_2026_09_12.md`)

# The light/dark theme chain: why it is whack-a-mole (2026-09-12)

Branch `fix/compilation-1009`. State of today's five fixes: 1 and 2 are committed in
`621e7fb8c`, 3 in `faa2f6c6a`; 4 (`paint_defaults_fingerprint` in the DL key) and 5
(compact rebuild on any theme flip) are uncommitted, together with new tests in
`layout/src/window.rs` and `[THEME-DBG]` `eprintln!`s in `event.rs:9568`, `layout.rs:476/1701`
and `macos/system_style.rs:929` that must not ship. Every claim below is `file:line` on this tree.

## 1. The pipeline as it actually is

### 1.1 Three theme types, three sources, two precedence rules

| Type | Lives in | Written by | Default |
|---|---|---|---|
| `css::system::Theme` (`css/src/system.rs:161`) | `SystemStyle.theme` (`system.rs:192`) | shell probes: macOS `macos/system_style.rs:276-287`, Windows `windows/system_style.rs:271`, Linux `linux/system_style.rs:1456,1840`; Android/iOS/headless never write it | `Light` (`system.rs:252`) |
| `WindowTheme` (`core/src/window.rs:1277`) | `FullWindowState.theme` (`layout/src/window_state.rs:135`) | `adopt_probed_theme` (`macos/system_style.rs:928`), `linux/system_style.rs:2883`, `windows/mod.rs:6733-6736`, `android/mod.rs:1093`, `ios/mod.rs:1145`, `headless/mod.rs:1986`; app via `ModifyWindowState` (`event.rs:4701-4707`) | `LightMode` (`window_state.rs:371`) |
| `ThemeCondition` (`css/src/dynamic_selector.rs:813`) | `DynamicSelectorContext.theme` (`dynamic_selector.rs:991`) | the builders in 1.3 | `Light` (`dynamic_selector.rs:1078`) |

`AZ_THEME` is read once (`dynamic_selector.rs:846-857`) but applied twice: inside
`from_system_style` (`:1116-1117`, `AZ_THEME > SystemStyle.theme`) and again in
`LayoutWindow::dynamic_selector_context` (`layout/src/window.rs:5135-5139`,
`AZ_THEME > FullWindowState.theme`, unconditionally overwriting the first answer). The effective
rule is therefore `AZ_THEME > window theme > (nothing)`: `SystemStyle.theme` never reaches the
cascade, yet it still drives three other decisions: whether a style change is a full
regeneration (`window.rs:1901-1903`), the window background (`event.rs:9576-9587`), and the
display list's no-context fallback (`display_list.rs:7627-7630`).

> **Fixed 2026-09-14.** The pin now also reaches `SystemStyle`:
> `azul_css::system::apply_env_theme_pin` runs at the end of all three platform `discover()`s
> (`linux/system_style.rs`, `macos/system_style.rs`, `windows/system_style.rs`) and swaps the
> polarity-dependent visuals — palette, focus ring, scrollbar — to the matching built-in family,
> keeping the desktop's fonts, metrics, extras and accent. Found by taking the website's Linux
> screenshots on a Breeze Dark session: `AZ_THEME=light` produced light-theme TEXT on the
> desktop's DARK background, because the cascade followed the pin and the background followed
> `SystemStyle`. The three decisions above are exactly why a half-pin is not a pin.

Window creation seeds `ws.theme` from `options.window_state.theme` (`macos/mod.rs:5563`,
`windows/mod.rs:619`, `x11/mod.rs:3873`, `wayland/mod.rs:1972`), i.e. from the `LightMode`
default, never from the probed `SystemStyle`. `WindowCreateOptions.theme`
(`window_state.rs:46`) has no reader anywhere.

### 1.2 From "OS appearance changed" to "glyph painted", in order

1. **Shell probe → window theme.** macOS: KVO `effective_appearance_changed`
   (`macos/mod.rs:985-1003`) or the 2 s poll (`:1426-1439`, throttle `system_style.rs:851`) →
   `adopt_probed_theme` (`system_style.rs:909-936`): early-out when `ws.theme` already equals the
   probe (`:919`), else `update_unsynced_state(|ws| ws.theme = theme)` (`:928`) and a
   re-`discover()`ed `SystemStyle` (`:946-965`). Android is the only shell that requests the
   regeneration itself (`android/mod.rs:1094-1096`); every other shell hands the style to step 2.
2. **`adopt_system_style`** (`dll/src/desktop/shell2/common/event.rs:9552-9624`). Bails when
   the *whole* `SystemStyle` is unchanged (`:9556-9559`). `needs_full` (`:9563-9565`) asks
   `system_style_change_needs_full_regeneration` (`window.rs:1894-1906`): a theme flip is always
   a full rebuild (`:1901-1903`); anything else consults what the layout callback declared it
   read (`callbacks.rs:1182-1216`, recorded by `get_theme` `callbacks.rs:1446`, harvested at
   `layout.rs:496-497`). Full → `request_regeneration(ThemeChange)` (`:9597-9601`); otherwise
   `reset_incremental` (drops the DL cache, `cache.rs:720-723`) + `IncrementalRelayout::Restyle`
   (`:9603-9608` → `layout.rs:1662`, which re-enters the funnel at `:1704-1711`). There is **no
   engine handler for the `ThemeChanged` window event**: `grep ThemeChanged` in `event.rs` finds
   only the app path (`:4848`) and the two `ThemeChange` requests above. So a `ws.theme` write
   whose rediscovered style equals the held one triggers nothing (a dark desktop at startup:
   `discover()` is already dark, `ws.theme` is `LightMode`, the poll writes `DarkMode`, step 2
   returns `false`, and the only follow-up is `request_redraw` at `macos/mod.rs:1435-1439`).
3. **`regenerate_layout`** (`layout.rs:236`). The callback sees `current_window_state.theme`
   (`:449`). The pre-cascade fingerprint skip (`:543-553`) excludes `ThemeChange` (`:545`) but
   its "unchanged" exit (`:630-660`) returns **without** entering the funnel, so a theme that
   changed without the reason being set keeps the retained DOM's old context. The warm exit
   (`:668-680`) and the full path both reach the funnel.
4. **`StyledDom` creation cascade** (`core/src/styled_dom.rs:1322-1365`), with
   `dynamic_context = None` (`prop_cache.rs:2367`): `restyle` gates `@theme`/`@media` blocks on
   the context (`prop_cache.rs:1531-1538` — no context, no conditional rule), `apply_ua_css`
   (`:1343`; themed lookup at `prop_cache.rs:4911-4915` but the context is `None`, so the light
   table; the property list `:4859-4891` contains **no `TextColor`**), `compute_inherited_values`
   (`:1344`; Step 4 applies only *unconditional* inline properties, `prop_cache.rs:5056-5060`),
   the compact build (`:1354`; conditional inline props are evaluated against the context
   `compact.rs:955-957` — `None` → dropped; UA defaults go in through the **unthemed**
   `get_ua_property` `compact.rs:1194-1196`; children copy the parent's text tier `:768`), then
   `prune_compact_normal_props` (`:1365`, `prop_cache.rs:1253-1257` drops Normal compact-encoded
   entries from `cascaded_props`).
5. **The funnel installs the context** (`window.rs:5252`, builder `:5120-5141`).
   `set_dynamic_selector_context` (`styled_dom.rs:2349-2404`): equal → return (`:2361-2364`);
   `theme_changed` (`:2358-2370`); the author cascade is re-run only when the *author* sheet has
   conditional rules (`:2379-2387`; `restyle_retained` is a no-op for an empty sheet
   `:1760-1770`); otherwise `recompute_inheritance_and_compact_cache` (`:2389-2404`) which,
   despite its name, rebuilds the compact cache only (`:1659-1684`) — `apply_ua_css` and
   `compute_inherited_values` are **not** re-run.
6. **Query-time getters.** `get_property` (`prop_cache.rs:2579`) → `get_property_slow`
   (`:2624`): user overrides → inline fold, last match wins, non-pseudo conditions need the
   context (`:2673-2685`, Normal at `:2992-3023`) → `css_props` → global `*` (`:3025`) →
   `cascaded_props` (`:3046`) → `computed_values` for inheritable props (`:3045-3053`) →
   `get_ua_property_themed(node_type, prop, dynamic_context)` (`:3058-3062`; dark twins only for
   `<hr>`/`<button>` borders, `ua_css.rs:961-982`). `get_text_color_or_default` seeds black
   (`:2497-2507`). Normal-state fast paths read the compact cache instead: text colour
   `getters.rs:3134-3157`, border colours `:2262-2280`.
7. **Text layout bakes the colour.** `getters.rs:3164` (`unwrap_or(ColorU::BLACK)`) → `:3424`
   → `text3::StyleProperties.color` (`text3/cache.rs:4568,4575`; hashed at `:4661`; excluded from
   the shaping key `:4698-4736`; re-stamped on a shaping hit `:9035-9036`) → glyph runs
   (`glyphs.rs:275,312,327`) → `CachedInlineLayout.glyph_runs` (`layout_tree.rs:292`, built once
   at `:586-588`, kept across frames).
8. **Display list re-resolves it.** `live_color` (`display_list.rs:7570-7639`): ancestor
   user-override walk (`:7587-7605`), then `cache.get_text_color` on the slow path (`:7617-7619`),
   then `evaluate_ua_root_text_color` with the cascade's stored context or a freshly built
   system-only/default one (`:7626-7634`). The baked run colour is used only for runs without a
   source node (`:7639`). Pushed at `:7666/:7691`.
9. **DL cache.** Key tuple `cache.rs:567-574`; inputs `mod.rs:770-779`; hit condition
   `:802-808`; `dl_input_fingerprint` (`:470-500`) now folds `paint_defaults_fingerprint`
   (`:488-490`; `dynamic_selector.rs:1185-1190` hashes the theme only). A second gate,
   `cascade_ctx_unchanged` (`mod.rs:1520-1566` against `cache.last_dynamic_context`,
   `cache.rs:472`), protects DL patching. The DL-only re-emit path
   (`window.rs:17934`, key at `:17992-18004`, store `:18119-18133`; 14 callers such as
   `event.rs:5302,6236,6664`) never installs a context — it reads whatever the last layout pass left.
10. **Layout hash.** Colour is excluded from relayout by design (`property.rs:1700-1715`,
    `:1807-1808`); `NodeDataFingerprint` hashes inline declarations, never resolved values
    (`diff.rs:2085-2160`, inline at `:2109-2111`); it becomes the subtree hash at
    `cache.rs:1577-1584, 2196-2201, 3612-3617`. Two cascades of one DOM under two themes hash
    identically — the documented consequence is at `cache.rs:1623-1651`.

### 1.3 Every place with its own idea of the theme or the default colour

Context builders: `DynamicSelectorContext::default` (Light, `dynamic_selector.rs:1071-1099`);
`from_system_style` (`:1103-1117`); `LayoutWindow::dynamic_selector_context` (`window.rs:5120`);
the DL fallback (`display_list.rs:7626-7631`); `get_scrollbar_style` (`getters.rs:5609-5611`,
SystemStyle only — the window theme is ignored for scrollbars) and
`ComputedScrollbarStyle::default` (`:5493-5497`, Light); the four shells' own contexts
(`macos/mod.rs:5693-5705`, `windows/mod.rs:775-787`, `x11/mod.rs:3985-3997`,
`wayland/mod.rs:2134-2146`) — never installed on any `StyledDom` (`dll/` contains no
`set_dynamic_selector_context` call).

Default-colour answers: `DEFAULT_TEXT_COLOR` (`prop_cache.rs:2506`, `ua_css.rs:1364/1380`),
`ColorU::BLACK` (`getters.rs:3164`), the static UA table (`ua_css.rs:747, 801-804`), its themed
twins (`:961-982`), `UA_ROOT_TEXT_COLOR_CSS` (`:1342-1363`, consulted only at
`display_list.rs:7634`), the compact builder's unthemed UA pass (`compact.rs:1195`), the window
background (`wr_translate2.rs:319`, `headless/mod.rs:593-596`, `event.rs:9578`), and in the
widgets the Bootstrap palette (`button.rs:146-201`, light only) beside the `LIGHT_*/DARK_*`
tokens (`flat.rs:49-548`).

### 1.4 Caches on the path and what their keys see

| Cache | Key / invalidation | Sees the theme? |
|---|---|---|
| `cascaded_props`, `computed_values` (`prop_cache.rs`) | rebuilt by `restyle()` only (`styled_dom.rs:1772-1830`) | no — built with no context at creation; a theme flip does not rebuild them (`styled_dom.rs:2389-2404`) |
| compact cache | rebuilt on context change or `has_dynamic_conditions` (`styled_dom.rs:2389-2404`) | partly: conditional inline props yes (`compact.rs:955`), UA colours no (`:1195`) |
| `StyleCache` (`getters.rs:2942-2995`) | (node, state, viewport); per-`LayoutContext` | no (lifetime-mitigated) |
| `scrollbar_style_cache` (`getters.rs:5815-5833`) | node id | no |
| text shaping cache (`text3/cache.rs:8963-8971`) | excludes colour; colour re-stamped `:9035` | n/a |
| `CachedInlineLayout` (`layout_tree.rs:257,320`) | includes colour via `inline_content_hash` (`fc.rs:3815-3845`) | indirectly |
| `inline_content_cache` (`fc.rs:3647-3697`) | node fingerprints + tier-1 enums + font hash | no |
| `cache_map`, `float_cache`, subtree hash | geometry / inline declarations | no |
| `cached_display_list` (`cache.rs:567-574`) | subtree hash, viewport, gpu, scroll, input fp | only via fix 4's theme hash |
| pre-cascade `last_dom_fingerprints` (`layout.rs:531-553`) | structure + inline style | no; `ThemeChange` reason excluded (`:545`) |

## 2. Why it is whack-a-mole

**R1 — UA colour defaults are answered at query time, not cascaded.** They never enter a node's
resolved style, so no hash, fingerprint or cache can see them (fixes 4 and 5 exist only because
of this). Worse, there are three answer sites that disagree: the slow path is themed
(`prop_cache.rs:3058`), the cascade-time pass is themed but runs before a context exists
(`:4911` with `:2367`), and the compact builder is unthemed (`compact.rs:1195`). The `<hr>` and
native-button border twins from fix 2 are therefore unreachable in the normal state, which reads
the compact cold tier (`getters.rs:2272`). The default *text* colour is not in the UA property
list at all (`:4859-4891`) and lives in a fourth place consulted only by the display list.

**R2 — The cascade runs before the context exists and is not re-run when it arrives.**
`styled_dom.rs:1343` vs `prop_cache.rs:2367`; then `set_dynamic_selector_context` re-runs the
author cascade only for a conditional *author* sheet (`:2379-2387`) and otherwise rebuilds the
compact cache only. For the common DOM (inline-styled widgets, empty sheet) `cascaded_props` and
`computed_values` are permanently the no-context answers. Fix 5 patches the compact half only.

**R3 — Two readers with different inheritance semantics.** The compact builder evaluates
conditional inline declarations against the context (`compact.rs:955-957`) and inherits by
copying the parent's text tier (`:768`); `compute_inherited_values` applies only unconditional
inline declarations (`prop_cache.rs:5056-5060`). Every `dark_theme(TextColor(..))` on a container
is inherited on one path and not the other. The display list's `live_color` uses the slow path
(`display_list.rs:7617`) and overrides the compact-baked glyph colour (`:7639`) — symptom (a).

**R4 — "Colour is paint-only" is a relayout decision that leaked into cache keys.** Excluding
colour from `can_trigger_relayout` (`property.rs:1700-1715`) is right for layout; but the DL
cache and DL patching serve *painted* output keyed on the same colour-blind subtree hash
(`diff.rs:2109-2111`). Three separate compensations now exist for one missing "cascade
generation" key: `paint_defaults_fingerprint` (`mod.rs:488`), `cascade_ctx_unchanged`
(`mod.rs:1520-1566`) and `reset_incremental` in `adopt_system_style` (`event.rs:9603`). Each was
added when its own path bit.

**R5 — Three theme sources, two precedence implementations, seeding from the default.**
`window.rs:5135` re-implements what `dynamic_selector.rs:1116` already decided; window creation
seeds `LightMode` regardless of the probe (`macos/mod.rs:5563`); `adopt_system_style` keys its
decision on `SystemStyle` equality (`:9556-9559`) although the cascade follows the window theme;
no `ThemeChanged` handler exists. Fix 1 made the window theme the truth for the cascade but left
`SystemStyle.theme` as the *trigger* (`window.rs:1901-1903`) and the background source
(`event.rs:9578`).

**R6 — Widget styling is split across three mechanisms with an unenforced ordering invariant.**
`build_button_container_style` emits light Bootstrap values (`button.rs:204-380`); the theme
function appends `dark_theme` twins and states (`flat.rs:651-678`, `flora.rs:895-941`); style
bundles say `None` = "no opinion" (`button.rs:108-116`, `:440-453`). Resolution is last-match-wins
(`prop_cache.rs:2992-3023`, and the comment at `:3012-3016` says widgets *rely* on it), so a
twin pushed after a type's face silently replaces it. The per-type classification exists in
`button_states` (`flat.rs:2103-2107`, `neutral`) but not in the resting-face code (`:663-671`).
Fix 3 made this live — symptom (b).

**R7 — Dead and duplicate paths.** `Button::dom` bypassed both theme functions until
`faa2f6c6a`; the shells' contexts are dead weight; scrollbars use a fourth builder; the e2e
runner never calls `set_dynamic_selector_context` (`styled_dom.rs:1084-1085`). Nothing pinned
the theme path end to end, so each fix could only be found by looking at the screen.

Root causes vs symptoms: fixes 1 and 5 (and half of 2) treat R2/R5; fix 4 treats R4; fix 2's
twins treat R1's surface; fix 3 exposes R6; symptom (a) is R3; the startup-on-dark hazard is R5.

## 3. Invariants that should exist

- **I1 — One function owns the theme decision.** `LayoutWindow::dynamic_selector_context`
  (`window.rs:5120`) is the only constructor of a context that reaches a `StyledDom`; the DL
  fallback (`display_list.rs:7626`), `get_scrollbar_style` (`getters.rs:5609`) and the shells'
  contexts read `cache.dynamic_context` or go away. Enforce: make `from_system_style`/`default`
  non-`pub` outside `css` + a grep lint in `scripts/check.sh`.
- **I2 — The context exists before the first cascade, and a theme flip is a full restyle.**
  `debug_assert!(dynamic_context.is_some())` in `apply_ua_css` and the compact builders;
  `set_dynamic_selector_context` on `theme_changed` runs the same sequence as `restyle()`
  (`styled_dom.rs:1772-1830`). Test: create under Light, install Dark, assert `cascaded_props`
  and `computed_values` carry the dark UA values.
- **I3 — UA defaults are cascaded like any other property, from one themed table.** One
  `&[CssPropertyWithConditions]` per node type (the shape `UA_SCROLLBAR_CSS` already has,
  `ua_css.rs:1342`), with `TextColor` on the root, consumed by `apply_ua_css` *and*
  `apply_ua_css_to_compact` through one resolver. Then `get_ua_property_themed`'s twin match,
  `UA_ROOT_TEXT_COLOR_CSS`, `display_list.rs:7620-7635` and `getters.rs:3164` are unreachable.
  Test: `ua_css_test.rs:1590` rewritten to assert through a `StyledDom` under both themes.
- **I4 — The two readers agree.** For every node in the normal state, compact == slow for every
  compact-encoded property. Enforce: an `AZ_VALIDATE` cross-check at the end of
  `build_compact_cache_with_inheritance`; make `compute_inherited_values` Step 4 evaluate
  conditional inline declarations against the context with the same last-match rule.
- **I5 — Every cache that can serve painted output keys on the cascade generation.** Add
  `CssPropertyCache::cascade_epoch`, bumped by `restyle`, `set_dynamic_selector_context`, and user
  overrides; put it in the DL key (`mod.rs:770-779`, `window.rs:17992`) and `StyleCache`; retire
  `paint_defaults_fingerprint` and the `last_dynamic_context` equality. Test: the uncommitted
  `a_theme_switch_recolours_the_retained_dom` plus a variant through
  `regenerate_display_list_for_dom`.
- **I6 — The theme is paint-only.** Node rects are identical under both themes and across a
  switch: the uncommitted `the_theme_does_not_change_layout` and
  `a_theme_switch_in_one_window_does_not_change_layout` (window.rs diff) — commit them; add a
  debug assertion that a compact *rebuild* equals a fresh build tier by tier.
- **I7 — One trigger.** A `ws.theme` write decides regeneration vs restyle on the *window* theme
  delta, not on `SystemStyle` equality (`event.rs:9556-9559`); creation seeds `ws.theme` from the
  probe. Test: headless `set_system_theme` (`headless/mod.rs:1978`) on a window whose
  `SystemStyle` already matches must still restyle; `dll/tests/headless_lifecycle.rs:476` is the
  place.
- **I8 — Ordering by construction.** Dark twins are emitted *with* their light value
  (`hover_bg_both` shape, `flat.rs:2160`) and states after resting values; the widget lint
  manifest (`layout/tests/widget_lint_manifest_is_exhaustive.rs`) rejects a `dark_theme(X)` that
  precedes an unconditional `X` of the same property type in any `*_style` vector.
- **I9 — One integration test per path:** fresh DOM; retained DOM through the pre-cascade skip's
  unchanged exit; `Restyle`; full regeneration; DL-only re-emit; startup with a dark
  `SystemStyle` and default window state. Fixtures exist in `window.rs`'s test module.

## 4. Recommended restructuring (ranked by leverage vs risk)

1. **Theme flip = restyle** (high leverage, low risk, ~1 day). In `set_dynamic_selector_context`,
   when `theme_changed` (or the DOM carries any conditional declaration), reset
   `cascaded_props`, re-run `apply_ua_css` → `compute_inherited_values` → compact rebuild (the
   tail of `restyle()`), and have the funnel install the context *before* `StyledDom::create`'s
   cascade (or pass it in). Completes fix 5; removes R2. Fix 5 as written is the patch this replaces.
2. **Inherit conditional inline declarations on the slow path** (high, low, ~1 day).
   `prop_cache.rs:5056-5060` evaluates `conds` against `self.dynamic_context`, last match wins,
   plus the I4 cross-check. Fixes symptom (a) for every widget at once; without it every widget
   needs its own label-colour workaround, which is the whack-a-mole in miniature.
3. **One themed UA table consumed by both cascades** (high, medium, ~2-3 days). Replaces fix 2's
   twin match and `UA_ROOT_TEXT_COLOR_CSS`, deletes the DL fallback and the BLACK/`DEFAULT_TEXT_COLOR`
   seeds (keep them as `debug_assert!`s). After this, fix 4's theme hash is redundant.
4. **`cascade_epoch` in the DL and style-cache keys** (high, low, ~½ day). Replaces fix 4 and the
   `last_dynamic_context` gate with one key that also covers `@media`, restyles and user overrides.
5. **Single theme source and trigger** (medium, low, ~½ day). Seed `ws.theme` from the probe at
   creation in all shells (or make it `Option`, `None` = follow system); key `adopt_system_style`
   on the window-theme delta; delete the shells' dead contexts and `WindowCreateOptions.theme`;
   route scrollbars through the stored context; remove the `[THEME-DBG]` prints.
6. **Button surface classification** (medium, low, ~½ day). `ButtonType::surface()` →
   `Neutral | OwnColour | NoSurface`, used by `flat::button`, `flora::button` and `button_states`
   for face, text and border twins alike. Fixes symptom (b) without touching a light value.
7. **Pair builder + lint for widget styles** (medium, medium, ~1-2 days): I8.

Of today's five fixes, 1 and 3 are the right long-term shape (1 needs I1 to finish); 2, 4 and 5
are patches over R1/R4/R2 and should be retired by items 3, 4 and 1 respectively.

## 5. The two live symptoms (diagnosed from code)

**(a) Widgets demo, Default button label dark-on-dark after the switch.** The "Click me!" button
(`examples/c/widgets.c:217`, `AzButton_create` → `ButtonType::Default`, `button.rs:410`) carries
on its container: `simple(TextColor(rgb(33,37,41)))` (`button.rs:262`, from
`get_button_text_color` `:193-201`), then, because `container_style` is `None` (`flat.rs:605`),
`dark_theme(BackgroundContent(DARK_BG))` and `dark_theme(TextColor(DARK_FG))` (`flat.rs:663-671`),
then `button_states` (`:678`). The label is a `widget_p()` `<p>` (`flat.rs:632-640`,
`widgets/mod.rs:473`) whose style declares no colour (`button.rs:390-403`), with a text child.
Under Dark the container itself resolves `DARK_FG` on both readers (inline fold, last match). For
the *text node* the two readers diverge: the compact builder copies `DARK_FG` down
(`compact.rs:955-957`, `:768`), so the baked glyph colour is right; but `live_color`
(`display_list.rs:7617-7619`) asks the slow path, which finds nothing inline or in `css_props`
on the text node and answers from `computed_values` (`prop_cache.rs:3045-3053`) — the value
inherited through Step 4 of `compute_inherited_values`, which took only the unconditional
`rgb(33,37,41)` (`:5056-5060`). Result: near-black text on `DARK_SUR`. The declarations are correct
under the migration rule (a page-neutral surface takes `DARK_SUR`/`DARK_INK`); the engine loses
the dark text colour in inheritance. Principled fix: item 4.2 (conditional inline declarations
participate in `compute_inherited_values`). Setting the colour on the label `<p>` would hide it
for this widget only and would have to be repeated in every widget with a text child.

**(b) hello-world, Primary button lost its blue face; label wraps.** The container gets
`simple(BackgroundContent(bootstrap_primary))` (`button.rs:307`), `simple(TextColor(WHITE))`
(`:262`), and blue borders (`:312-313`, `border_color = bg_normal`). `flat::button` then appends
`dark_theme(BackgroundContent(DARK_BG))` for *every* type (`flat.rs:663-671`, "we just override
the background and text color for dark mode"), and last-match-wins (`prop_cache.rs:2992-3023`)
makes the face `DARK_SUR` under Dark. The text node keeps `WHITE` (same inheritance gap as (a)),
the borders stay blue, and `button_states(Primary)` keeps the blue hover/active
(`flat.rs:2103-2107`, `neutral == false`), so a dark-grey box with a blue outline flashes blue on
hover. Under the rule "a surface that is its own colour keeps its light colour", the resting-face
twins must be gated the way `button_states` already gates the states: push `DARK_BG`/`DARK_FG`
only for `ButtonType::Default` (and no face for `Link`); `flora::button` classifies the face
per type (`flora.rs:895-913`) but still pushes `dark_theme(TextColor(DARK_INK))` and `DARK_BD`
borders for every type (`:918-932`), which is the same defect one layer down. Item 4.6.

The wrap cannot be derived from the code alone. The DOM is identical to the pre-dispatch tree
(the old `Button::dom` already used `widget_p()`: `faa2f6c6a^:button.rs:679-686` vs
`flat.rs:632-640`), no stylesheet keys on `__azul-theme-flat`, and the theme is paint-only by
construction (`property.rs:1700-1715`) — so a wrap means an invalidation seam treats the colour
flip inconsistently. Two candidate seams: (i) the compact cache is now *rebuilt* on context
install for every button (`has_dynamic_conditions`, `styled_dom.rs:2389-2404`) after
`prune_compact_normal_props` ran (`:1365`), and a rebuild has never been proven equal to a fresh
build; (ii) colour is hashed into `inline_content_hash` (`fc.rs:3815-3845` via
`text3/cache.rs:4661`), so the label is re-shaped on the theme pass while box geometry comes
from caches blind to colour (`cache_map`, subtree hash). The uncommitted
`the_theme_does_not_change_layout` / `a_theme_switch_in_one_window_does_not_change_layout` tests
are the correct pin; run them first (`cargo test --release -p azul-layout the_theme_does_not_change_layout`)
and, if they pass, reproduce with a Primary button in the headless harness through the
`Restyle` path, which the unit fixture does not take.

---

### Doc Orphan Allowlist (`doc/orphan_allowlist.txt`)

# Public functions in the dll's integration glue that nothing calls.
#
# This file is the difference between a lint that fires and a lint that fails.
# Anything listed here is a KNOWN orphan with a reason; anything NOT listed is
# an error, so a new one cannot join quietly.
#
# The lint exists because three bugs in one day had this shape and none of them
# could produce a compiler warning: `pub` silences dead-code analysis, and the
# binding that decides which function ships is a string in api.json.
#   - save_to_disk_cache: zero callers -> the font cache was never written
#   - map_widget_dom:     zero callers -> the map never fetched a tile
#   - video_widget_dom:   same, one widget over -> video never decoded
#
# Format: <symbol>  # reason
# To remove an entry: wire the function up, or delete it.

# --- MVT helpers, superseded by the tile worker's own path -------------------
calculate_extent            # mvt.rs helper; the worker computes its own extent
calculate_zoom_level        # mvt.rs helper
get_tile_coordinates_for_extent  # mvt.rs helper
tile_coords_to_urls         # mvt.rs helper; MapTileCache builds URLs itself
stitch_features_to_collection    # mvt.rs helper

# --- capability probes / one-shot actions called from other languages --------
probe_last_fix              # geolocation probe, reachable from the C API surface
sqlite_version              # version probe
peer_left                   # webtransport event hook
# The send half of the WebTransport v1 loopback stub (961bb4b68): api.json binds
# the class but not these five yet, so no fn_body reaches them. Wire them into
# the AzWebTransport module when its send/keyframe surface lands; until then the
# stub returns `false` and they are reachable from nothing.
send_video                  # webtransport v1 stub, unbound in api.json
send_audio                  # webtransport v1 stub, unbound in api.json
send_chat                   # webtransport v1 stub, unbound in api.json
send_system                 # webtransport v1 stub, unbound in api.json
request_keyframe            # webtransport v1 stub, unbound in api.json
reboot_now                  # provisioning action, invoked by the user not by us

---

### Doc Reftest Baseline (`doc/reftest_baseline.txt`)

# Reftests known to PASS at the time this baseline was recorded.
# A test listed here MUST keep passing.
block-float-clear-complex-001
block-margin-collapse-complex-001
block-padding-border-001
block-positioning-complex-001
block-width-height-001
calc-width-001
cascade-font-weight-inherit-001
cascade-global-star-001
cascade-inline-style-001
cascade-multiple-classes-001
cascade-nested-selectors-001
cascade-specificity-001
color-background-001
display-none-visibility-001
flexbox-flex-grow-shrink-001
flexbox-justify-align-001
flexbox-simple
flexbox-wrap-alignment-001
font-hinting-001
font-properties-001
grid-alignment-001
grid-auto-flow-dense-001
grid-minmax-fr-001
grid-template-areas-001
inline-block-text-002
list-style-001
margin-auto-centering-001
overflow-scroll-001
padding-debug-001
padding-debug-002
padding-debug-003
position-absolute-001
position-relative-001
ribbonbug-border-around-text-box-001
ribbonbug-border-box-metrics-001
ribbonbug-flex-root-auto-height-001
ribbonbug-mini-ribbon-composite-001
ribbonbug-text-center-in-wide-box-001
ribbonbug-text-maxcontent-nowrap-001
ribbonbug-unset-border-color-001
showcase-flexbox-complex-001
table-basic-001
text-basic-001
text-in-box-debug-001
text-overflow-001
inline-background-001
inline-elements-001

---

### Doc Blog 2025 12 13 Hello World (`doc/blog/2025-12-13-hello-world.md`)

# Hello World - First Blog Post

Welcome to the Azul GUI Framework blog! This is our first post where we'll share updates, 
tutorials, and news about the project.

## What is Azul?

Azul is a cross-platform MIT-licensed Desktop GUI framework for C, C++, Python and Rust, 
using the Mozilla WebRender rendering engine. It focuses on:

- **Performance**: GPU-accelerated rendering with WebRender
- **Simplicity**: DOM-based layout with CSS styling
- **Cross-platform**: Works on Windows, macOS, and Linux
- **Multi-language**: Bindings for C, C++, Python, and native Rust support

## Getting Started

Check out our [getting-started guide](/ui/guide/hello-world) to get started with Azul.

## Stay Tuned

We'll be posting more updates soon, including:

- New feature announcements
- Performance improvements
- Community highlights
- Tutorial series

Thanks for reading!

---

### Doc Fonts EBGaramond OFL (`doc/fonts/EBGaramond-OFL.txt`)

Copyright 2017 The EB Garamond Project Authors (https://github.com/octaviopardo/EBGaramond12)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

---

### Doc Fonts Grenze OFL (`doc/fonts/Grenze-OFL.txt`)

Copyright 2019, The Grenze Project Authors (https://github.com/Omnibus-Type/Grenze)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

---

### Doc Fonts GrenzeGotisch OFL (`doc/fonts/GrenzeGotisch-OFL.txt`)

Copyright 2020 The Grenze Gotisch Project Authors (https://github.com/Omnibus-Type/Grenze-Gotisch)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

---

### Doc Fonts RedHatMono OFL (`doc/fonts/RedHatMono-OFL.txt`)

Copyright 2024 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

---

### Doc Guide En Animations (`doc/guide/en/animations.md`)

---
slug: animations
title: Animations
language: en
canonical_slug: animations
audience: external
maturity: wip
guide_order: 100
topic_only: false
short_desc: Transitions, @keyframes, and the enter/exit animation properties
prerequisites: [hello-world, events]
tracked_files:
  - css/src/props/basic/animation.rs
  - core/src/animation.rs
  - layout/src/window.rs
  - core/src/task.rs
last_generated_rev: e33ef81cc56d8a7dac357d6c0610ff5e26d02e26
generated_at: 2026-08-20T00:00:00Z
default-search-keys:
  - StyleAnimation
  - AnimationTiming
  - AnimationTimingBezier
  - AnimationIterationCount
  - Keyframes
  - CssProperty
  - Timer
  - TimerCallbackInfo
  - TimerCallbackReturn
  - Update
---

# Animations

Three properties drive everything the engine animates:

| Property | Runs when | Names |
| --- | --- | --- |
| `animation` | a property's computed value changes between DOM rebuilds | `all`, or the property to scope to |
| `-azul-animation-in` | the node mounts | a `@keyframes` block or an attached function |
| `-azul-animation-out` | the node unmounts | a `@keyframes` block or an attached function |

There is no `transition` property. `animation` is where that job lives.

## The shorthand

```
<name> <duration> [<delay>] [<timing>] [infinite | <count>] [no-clip]
```

```css
animation: all 200ms ease-out;               /* every change transitions */
animation: width 1s, background-color 2s;    /* per-property scopes      */
-azul-animation-in:  slideIn 220ms spring;
-azul-animation-out: slideOut 180ms 50ms ease-in no-clip;
```

The first time value is the duration, the second the delay — CSS order. A
list is read last-match-wins, so a later entry overrides an earlier one for
the properties it covers.

## Transitions

`animation` turns covered property changes into timed interpolations instead
of instant updates. The property list is read off the **old** cascade: adding
`animation` in the same rebuild that changes a value does not retro-animate
that change.

```css
.row          { background-color: #fff; animation: all 150ms ease-out; }
.row.selected { background-color: #dce8ff; }
```

Rebuild the DOM with `selected` on the row and the colour walks over 150 ms.
Change it again mid-flight and the animation **retargets** from its current
value rather than stacking a second run — rapid A→B→C stays smooth. A
transition on a paint-only property (colour, opacity, transform) patches the
display list; one on `width` or `font-size` re-runs layout per frame.

## Presence: enter and exit

`-azul-animation-in` and `-azul-animation-out` animate a node's arrival and
departure. Both name a `@keyframes` block or a function attached to the node,
resolved in that order.

```css
.toast {
    -azul-animation-in:  toastIn 200ms spring-snappy;
    -azul-animation-out: toastOut 150ms ease-in;
}
```

An exit needs something to draw after layout has removed the node, so a
declared `-azul-animation-out` makes the engine retain the previous frame's
subtree for the duration — the zombie. Retention, catching an exit that
remounts mid-flight, and per-frame native animation functions are
[Zombie Animations](animations/zombie-animations.md).

`infinite` on an enter track is how a spinner is expressed; on an exit it is
clamped to one run.

## `@keyframes`

```css
@keyframes toastIn {
    from { transform: translateY(24px); opacity: 0; }
    to   { transform: translateY(0);    opacity: 1; }
}
@keyframes pulse {
    0%   { opacity: 1.0; }
    50%  { opacity: 0.4; }
    100% { opacity: 1.0; }
}
```

Stops accept `from`, `to`, and percentages. The compiled track reads
`transform` (`translate`, `translateX`, `translateY`, `scale`, `rotate`),
`opacity`, `width` and `height`; percentage translations resolve against the
node's own rect. Other properties in a stop are ignored. When two blocks
share a name, the last definition wins.

## Timing

`ease` (the default), `linear`, `ease-in`, `ease-out`, `ease-in-out`,
`cubic-bezier(x1, y1, x2, y2)`, and three springs: `spring`,
`spring-gentle`, `spring-snappy`.

Springs settle on physics rather than on the clock — the declared duration is
their retarget time base, not a stop watch. Reach for one when the animation
can be interrupted (a panel the user re-opens mid-close); reach for a bezier
when the motion has to land on an exact beat.

## Driving an animation yourself

Motion that follows application state — a simulation, a game loop, a value
arriving over the network — belongs in a timer that mutates your model and
returns `Update::RefreshDom` per frame:

```rust,ignore
extern "C" fn animate(data: RefAny, info: TimerCallbackInfo) -> TimerCallbackReturn {
    let mut state = data.downcast_mut::<State>().unwrap();
    let Some(start) = state.anim_start else {
        return TimerCallbackReturn::terminate_unchanged();
    };
    let end = start.clone().add_duration(&state.anim_duration);
    let t = info.frame_start.linear_interpolate(start, end);
    state.current_opacity = state.anim_from + (state.anim_to - state.anim_from) * ease_out(t);
    if t >= 1.0 {
        state.anim_start = None;
        TimerCallbackReturn::terminate_and_refresh_dom()
    } else {
        TimerCallbackReturn::continue_and_refresh_dom()
    }
}
```

`Instant::linear_interpolate(start, end)` returns the clamped `0..=1`
fraction; layer easing on top. The layout callback then reads
`state.current_opacity` and writes it into the style it builds. See
[Timers](animations/timers.md) for scheduling, cancellation, and the 60 fps
pattern end to end.

## Animating images, not the DOM

When the only thing that changes is pixels — a sprite sheet, a video frame, a
GL texture — `info.update_all_image_callbacks()` re-invokes every image
callback without touching layout or the display list.

## Cross-references

- [Zombie Animations](animations/zombie-animations.md): exit retention and
  native per-frame animation functions.
- [Timers](animations/timers.md): the timer mechanics the manual path builds
  on.
- [Reconciliation](dom/reconciliation.md): the node identity that decides
  what counts as a mount, an unmount, and a move.

---

### Doc Guide En Architecture (`doc/guide/en/architecture.md`)

---
slug: architecture
title: Application Architecture
language: en
canonical_slug: architecture
audience: external
maturity: mature
guide_order: 40
topic_only: false
short_desc: Explains the concepts of architecting a larger Azul application
prerequisites: [hello-world]
tracked_files:
  - core/src/callbacks.rs
  - core/src/dom.rs
  - core/src/refany.rs
  - doc/guide/en/architecture.md
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:48:36Z
default-search-keys:
  - App
  - RefAny
  - Dom
  - Callback
  - CallbackInfo
  - Update
  - LayoutCallbackInfo
  - Button
  - TextInput
  - NumberInput
---

# Application Architecture

## Introduction

Building graphical user interfaces has, despite its perceived simplicity, been 
a difficult problem in computer science. Despite a constant progress in languages, 
libraries, design patterns and compilers, developers face the same fundamental 
problems in user interfaces that they did 40 years ago: managing state, synchronizing 
application data with what users see, and enabling communication between distant 
components without creating a spaghettified mess.

The core thesis of Azul is that this struggle lies in a core conflict that nearly every 
toolkit fails to properly address: a conflict between the "Visual Tree" (the hierarchy of object on the screen, such as `div.foo > p > ::text`) 
and the "State Graph" (the logical relations between components interacting with each other, 
such as `Toolbar <-> TreeView <-> Table`).

*   The "Visual Tree" is the hierarchy of elements as they appear on the screen. It is always 
    a tree: a window contains a panel, which contains a button. This hierarchy is pretty much 
    present in all but the most primitive UI frameworks, because you need some form of logical 
    grouping for layouting, unless the layout is calculated manually.
*   The "State Graph" is the map of how application data and logic are connected. A filter 
    control in a toolbar (`Visual Tree` -> `Toolbar` -> `Filter Data`) needs to alter the 
    data displayed in a completely separate table  (`Visual Tree` -> `MainPanel` -> `Table`) without
    the two pieces being merged in a "FilterDataTableWithToolbar", as that will create a 
    complete mess once the interactions become more complex.

The second biggest problem of UIs is the question: How do we handle the inherent mutability of UI components? Mutable APIs such as in web browsers only hand you a `dom.addChild(c)` function and expect you to do the synchronization back to your application data model yourself. Other solutions such as SolidJS trigger hooks whenever your data model changes, or create a diff between two full virtual DOM trees (such as in React).

## Prior Art

The key insight of Azul is that this network of dependencies is a complex _graph_, not a 
simple _tree_ and fusing them together (via a "minimum common ancestor approach") creates
the unmaintainable mess of most modern UIs.

```mermaid
---
title: Visual Tree
---
graph LR
    v_App[App] --> v_Toolbar[Toolbar] & v_MainPanel[MainPanel]
    v_Toolbar --> v_SaveButton[SaveButton]
    v_MainPanel --> v_Sidebar[Sidebar] & v_Table[Table]
    v_Sidebar --> v_FilterControl[FilterControl]

    classDef app fill:#f9f,stroke:#333,stroke-width:2px
    classDef button fill:#ccf,stroke:#333,stroke-width:2px
    classDef control fill:#cfc,stroke:#333,stroke-width:2px

    class v_App app
    class v_SaveButton button
    class v_FilterControl control
```

```mermaid
---
title: State Graph
---
graph TD
    s_SaveButton[SaveButton] -- "needs validity from" --> s_FormState[FormState]
    s_FilterControl[FilterControl] -- "updates data for" --> s_TableData[TableData]
    s_FormState -- "is part of" --> s_AppLogic[AppLogic]
    s_TableData -- "is part of" --> s_AppLogic

    classDef button fill:#ccf,stroke:#333,stroke-width:2px
    classDef control fill:#cfc,stroke:#333,stroke-width:2px
    classDef state fill:#ccf,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5
    classDef data fill:#cfc,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5

    class s_SaveButton button
    class s_FilterControl control
    class s_FormState state
    class s_TableData data
```

The "pain" of UI programming stems from frameworks that either fuse them 
together or awkwardly force a node graph to conform to the shape of the tree.

### Fused Hierarchy [OOP]

The first generation of toolkits (Qt, GTK, MFC, Swing) were built on an 
object-oriented model - not because it was necessary, but because it was 
considered „best practice“. The paradigm was simple: the UI is a tree of 
stateful objects, therefore would be logical to put the custom behaviour of a `Button` right on an extended instance - hence the need for "object-oriented" inheritance. A `MyCustomPanel` 
object inherits from `Panel` and adds its own data and logic, objects are
then composed in a hierarchy of *types* (not values) until you get to the parent „window“ object.

```python
# OOP Paradigm
class MyPanel(othertoolkit.Panel):
    # ...
    def on_click():
        # text_input implicitly comes from othertoolkit.App
        input = self.text_input.getText()
        calculated = do_somthing_with_input(input)
        self.output.setText(calculated)
        self.text_input.setText("")
```

```mermaid
graph TD
    A[MyApp extends App] -->|inherits| B[Panel extends Widget]
    B -->|inherits| C[Button extends Widget]
    A -.->|holds reference| C
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#ccf,stroke:#333,stroke-width:2px
    style C fill:#cfc,stroke:#333,stroke-width:2px
```

In this model, the Visual Tree and the State Graph are fused. The object inheritance 
hierarchy _is equal to_ the visual hierarchy. This immediately creates real problems:

*   Communication between logically related but visually distant components requires complex 
    pointer management (in JS, reference management - no crash but not much better), global 
    mediator objects, or a web of signal-and-slot connections that are difficult to
    trace and maintain (Qts meta-object-compiler).
*   Changing the visual layout in this paradigm forces a refactoring of the class hierarchy, 
    which makes developing applications in such toolkits painful and creates hard dependencies 
    on the toolkit itself (leading to „toolkit wars“, like the battle over GTK vs Qt). 
    The application logic is not testable in isolation without the framework
    because it is fundamentally inseparable from the UI objects themselves.
*   It creates a hard dependency on the toolkit itself. Your application logic is not 
    portable or reusable because it is fundamentally intertwined with the toolkit's base 
    classes, rendering system, and event model.

### Constrained Hierarchy [Elm, React]

The next major step, led by frameworks like React, Angular and the Elm Architecture, 
introduced a new functional paradigm: `UI = f(data)`. The UI is a declarative, pure 
function of the application's state. When the data is changed, the framework efficiently updates 
the view to match instead of manually needing a `setText()` call ("two-way data binding").

```python
# React Paradigm Model
def MyApp():
    input_value, set_input_value = useState("")
    output_value, set_output_value = useState("")

    def handle_click():
        calculated = do_something_with_input(input_value)
        set_output_value(calculated)
        set_input_value("")

    return Page(children=[
        TextInput(value=input_value, on_change=set_input_value),
        Button(on_click=handle_click),
        Label(text=output_value)
    ])
```

```mermaid
graph TD
    A[MyApp State] -->|props down| B[Toolbar]
    A -->|props down| C[MainPanel]
    B -->|callback up| A
    C -->|callback up| A
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#ccf,stroke:#333,stroke-width:2px
    style C fill:#cfc,stroke:#333,stroke-width:2px
```

However, while these frameworks finally decouple the view from imperative manipulation, they 
still constrain the flow of data to the shape of the Visual Tree. The example above works 
because `TextInput`, `Button`, and `Label` are all siblings, children of `MyApp`. But what if 
the `Button` were in a `Toolbar` and the `TextInput` and `Label` were in a `MainContent` panel? 

React's solution is to „lift state up“ to their lowest common ancestor, `MyApp`. The `MyApp` 
component must now hold the state and pass both the data and the callback functions down through the 
intermediate components.

```python
def MyApp():
    # State is lifted to the common ancestor
    input_value, set_input_value = useState("")

    # ... logic also lives in the ancestor ...

    return Page(children=[
        # Toolbar is now forced to accept and pass down a prop it doesn't use
        Toolbar(on_button_click=handle_click),
        # MainContent is also forced to pass props
        MainContent(
            input_value=input_value,
            on_input_change=set_input_value,
            output_value=output_value
        )
    ])
```

Here, the State Graph is still being forced into the tree structure of the view, leading 
to "prop drilling" and components with indirect APIs. The existence of "escape hatches" 
like Redux or the Context API is evidence of this core constraint - they are patterns invented 
to work around this default tree-based data flow.

Elms solution goes even further to „lift all state up“ to the root ancestor and route everything 
in a single, top-level "update" function. Elm therefore represents the philosophical extreme 
of the constrained hierarchy:

1.  **Model:** The entire state of the application is held in a single, immutable data structure.
2.  **View:** A pure function that takes the `Model` and returns a description of the UI.
3.  **Update:** A single, central function that is the only entity allowed to modify the state.

Elm does this by taking an incoming `Msg` (a message from the UI) and the current state, and
producing a *new* state.

### Ignoring Hierarchy (IMGUI)

Immediate Mode toolkits (IMGUI) have no persistent 
UI objects at all; the UI is redrawn from scratch from application data every single frame. This 
solves synchronization issue by brute force but shoves the problem of application architecture 
onto the developer instead of the framework - programmers now have to store lots of "UI state" as 
part of their application data model, such as focus state, scroll positions, caches, etc. Immediate 
mode toolkits are mainly popular with game development overlays, but not much outside of that, 
simply because the inherent complexity of UIs is now shoved onto the application instead of the 
framework, which isn't much of a help.

```python
# IMGUI Paradigm Model
class AppState:
    input_buffer = ""
    output_text = ""

# Inside the main application loop, every frame
def render_ui(app_state):
    ui.text_input("Input:", &app_state.input_buffer)
    if ui.button("Calculate"):
        calculated = do_something_with_input(&app_state.input_buffer)
        app_state.output_text = calculated
        app_state.input_buffer.clear()
    ui.label(&app_state.output_text)
```

```mermaid
graph TD
    A[AppState] -->|reads| B[render_ui]
    B -->|writes| A
    B -->|draws| C[UI Every Frame]
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#ccf,stroke:#333,stroke-width:2px
    style C fill:#cfc,stroke:#333,stroke-width:2px
```

IMGUI doesn't solve the Visual Tree vs. State Graph problem - it just largely ignores the problem 
and instead creates a _hidden data binding_ in a "closure with captured arguments" instead of a 
"class with state and functions".

While the form is different from OOP, the operation (and the problem) is the same. A closure is 
just a function on a struct containing all captured variables. The effect is the same as a class-with-methods, 
but on top of that, it provides even less layout flexibility than object-oriented code, because the UI drawing functions juggles the raw values around (to calculate the layout) instead of abstracting them in more "declarative" objects and letting the framework solve the layout.

## What is the essence of a UI toolkit?

A question that sometimes comes up in programming discussions is how a "GUI toolkit" differs from a "rendering library".
One could mainly categorize GUI toolkits or libraries by their approach to handling the 
following three "hard problems":

1.  **Data Access / Model-View separation:** Somehow a callback needs access to both the data model (i.e.
    the application data) and the stateful UI object of the framework (to scrape the text out), but at the same time the „data model“
    should be as far removed from the UI as possible, so that logic functions do not depend on view data (`my_ui_object.getText()`).
2.  **Synchronization:** It is very easy for the visual UI state and the data model to go out of sync.
    Solutions so far include "Observer patterns" (callbacks that run when something changes, granular 
    reactivity), React-like reconciliation of entire DOM trees or "just redraw everything, and throw 
    the state away" (IMGUI).
3.  **Inter-widget communication:** This is the hardest problem to solve, as it's not directly obvious 
    in TodoMVC-esque applications. Existing toolkits assume that the widget hierarchy (visual tree) and
    the inheritance (or function call) hierarchy are the same (using the least common ancestor as a 
    channel, either via OOP inheritance or via React-style prop drilling). Other solutions involve 
    observable cells of functionality (`useMemo` / `useEffect` in SolidJS), which the framework then 
    coordinates.

Overall, rendering libraries do not solve these problems at all while frameworks usually do - sometimes 
using special features of the language itself such as in SwiftUI and Flutter.

## Starting again from scratch

So, if we could free our mind conceptually from both OOP and Reactive programming, what would a 
"proper" toolkit look like? By "proper" it means that it solves the problems above and scales to 
larger (500K - 1 million lines of code) applications without becoming an unmaintainable mess.

### Encoding Visual Hierarchy

The first thing we'd need to decide is whether we'd like to serialize the UI or render it directly 
(IMGUI). The choice here is relatively obvious, because the former creates
instant opportunity for introspection of the visual state (such as in a HTML debugger). The 
counter-argument against this has been traditionally "performance", but when testing Azuls memory 
profile, this effectively came up as a non-issue: the entire DOM with styling in even a large 
application is only ~500KB - 1MB of actual data.

In terms of efficiency there is also a massive upside to this: we can diff the DOMs state instead
of just getting new pixels, which gives the framework more semantic information to work with for
caching. Second, it also
nicely maps to how computers execute - compare the XML hierarchy to function call stacks:

```html
<div class="parent">
    <div class="child"></div>
    <div class="child"></div>
    <div class="child"></div>
</div>
```

with:

```python
div(class="parent", children = [
    div(class="child")
    div(class="child")
    div(class="child")
])
```

Composing UI hierarchies via functions makes much more sense than composing UI hierarchies via 
inheritance (OOP) or types (xilem) because the latter are often language-specific and not supported 
in all languages, whereas C-callback style function pointers are language agnostic.

### Encoding Data Access

The second decision is where to store the UI data, so that the callbacks may access it again.
Widget-specific data has to be either stored on the programmer side (in the application, using 
inheritance or traits) or in the framework. 
What format should we use?

Inheritance-based toolkits only allow one format: You have to inherit from a UI object and then 
construct your application as a hierarchy of UI objects. Azul stores the application data 
as an implementation-agnostic `RefAny` struct: similar to `PyObject` or Javascripts `Object` it 
just stores "some data as bytes" - together with minimal information about the class / data type. 
You can upcast any piece of data, wrap it via `RefAny::new` and then get immutable or mutable access again via `.downcast_ref()` 
or `.downcast_mut()`, respectively:

```rust
let data = RefAny::new(5); // owns the data
let data_clone = data.clone(); // only bumps the reference count

let data_ref: &usize = data.downcast_ref::<usize>().unwrap(); // ok
println!("{}", *data); // prints 5

// error: data_ref still held
let data_mut: &mut usize = data.downcast_ref::<usize>().unwrap();
// object destroyed when last reference to it is dropped
```

Effectively this is similar to an `Observable`, however, since `RefAny`s are usually connected to a 
`Callback`, a `Dom`, a `Task` or a `Thread`, the topology of how they are connected is more obvious.

While the up- and downcasting loses some type safety (we'll discover later how to work around this 
fact), the biggest upside here is that this model makes the framework C-compatible. If the framework 
would use Rust closures or traits, this would effectively make it unusable outside of Rust.

Using [insert language]s module system, we can minimize (and in practice completely avoid) 
any errors related to up / downcasting by controlling the *visibility* of the thing we're downcasting 
to - effectively making the "blast radius" of a type casting error so minimal that it becomes 
irrelevant in practice:

```rust
// number_input.rs (private internals)
struct NumberInputInternal { /* ... */ }

// number_input.rs (public API)
pub struct NumberInput {
    internal: NumberInputInternal,
}

impl NumberInput {
    pub fn dom(self) -> Dom {
        let on_the_fly = RefAny::new(self.internal); // upcast
        Dom::new().with_callback(private_callback, on_the_fly)
    }
}

extern „C“
fn private_callback(data: RefAny, info: CallbackInfo) -> Update {
    // downcast - as NumberInputInternal is private to this module,
    // only code in this module can downcast to NumberInputInternal
    // external code can't even name the type, so no downcast error
    let d = data.downcast::<NumberInputInternal>().unwrap();
}
```

This way, once a decent amount of test coverage is done, the „internals“ of any widget
are hidden from the outside completely. In the API of Azuls default widget set you'll often see `FooConfig` structs that carry the public "configuration" of said widget, which reflects this pattern. When all references to a `RefAny` are deleted, 
the internal object is then destroyed, running either a default null or custom destructor function. Additionally, the framework can keep "heavy" `RefAny` objects alive between two `layout()` calls, e.g. for storing things like a video decoder handle or a map tile cache, where the cached data belongs neither in the application data model nor in the framework built-in as a primitive - read more in the [Merge Callbacks](./dom/merge-callbacks.md) guide.

### Building a State Graph

The architectural pattern that naturally emerges from this `RefAny` + `Callback` is the „backreference“ or "dependency injection" pattern: you can store a `RefAny` + `Callback` inside of a `RefAny`, which allows you to design public APIs of widgets
in a „dependency injection“ style, without the need for "prop drilling".

The goal of this is to pass data / callbacks of a higher-level data model directly down to 
a lower-level component during DOM construction, without having to „prop drill“ any data / 
callbacks through intermediary components / middleware, but also, in difference to OOP, keep
both components „in the dark“ about each other. The only way where it's necessary to know the 
relations is in the executed callback: exactly where we'd expect some form of complexity, and
where we could use a debugger to „step through“ a potential downcast failure. Since this is all very theoretical, let's look at some examples:

## Examples

### Simple: Input Validation

To explain this concept more concretely, let's build a number input that wraps a text input 
and validates that the user typed a number > 18 in a `VerifyAgeApplication`. This demonstrates the dependency injection pattern in its simplest form - a linear chain from low-level (`TextInput`) 
through mid-level (`NumberInput`) to high-level application logic (`VerifyAgeApplication`), all extending each other without inheritance.

At its core, text input is handled by marking a text node `contenteditable`. On top of that, the `TextInput` then provides a relatively basic API on top, similar to what a web browser would offer. It provides a hook for "any" validation when receiving a `focusout` event - whether that callback then validates input as a number, a regex match or a date validation - the `TextInput` itself doesn't care. It doesn't know anything about number validation or age validation. The only thing it knows is „I should call this callback when the focus was lost, which will return true or false“.

```python
class TextInput:
    text: String
    user_focus_lost_callback: Optional[Tuple[RefAny, Callback]]

    def __init__(self, text):
        self.text = text
        self.user_focus_lost_callback = None

    def set_on_focus_lost(self, data, callback):
        # Allow higher-level widgets to hook into focus loss
        self.user_focus_lost_callback = tuple(data, callback)

    def dom(self):
        dom = Dom.text(self.text)
        refany = RefAny(self)
        dom.set_callback(On.TextInput, refany, _on_text_input)
        dom.add_callback(On.FocusLost, refany, _on_focus_lost)
        return dom

# PRIVATE to TextInput module, updates TextInput.text internal state
def _on_text_input(data, callbackinfo):
    data.text += callbackinfo.get_text_changeset().text
    # no callbackinfo.preventDefault(), so text input will continue
    return Update.DoNothing

# PRIVATE to TextInput module, calls the user-provided validation callback
def _on_focus_lost(data, callbackinfo):
    # When focus is lost, invoke the user-provided callback if it exists
    if data.user_focus_lost_callback is None:
        return Update.DoNothing
    
    user_data, user_callback = data.user_focus_lost_callback
    return user_callback(user_data, callbackinfo, data.text)
```

`NumberInput` as a "superclass" of `TextInput` now wraps `TextInput` and adds number-specific validation logic. It again 
holds a field for a dependency-injected callback to *its* superclass (in this case `VerifyAgeApplication`) via 
`on_number_input`:

```python
class NumberInput:
    number: Integer
    on_number_input: Optional[Tuple[RefAny, Callable]]

    # default NumberInput() constructor
    def __init__(self, number):
        self.number = number
        self.on_number_input = None

    def set_on_number_input(self, data, callback):
        # Store a backreference to the application‚s callback
        self.on_number_input = (data, callback)

    def dom(self):
        ti = TextInput(str(self.number))
        # Pass a backreference to *this* NumberInput down to TextInput
        ti.set_on_focus_lost(RefAny(self), _validate_text_input_as_number)
        return ti.dom()

# PRIVATE to NumberInput module
def _validate_text_input_as_number(data, callbackinfo, string):
    # This callback receives the NumberInput's data
    if data.on_number_input is None:
        return Update.DoNothing

    number = string_to_number(string)
    if number is None:
        return Update.DoNothing  # Invalid input; ignore silently

    # Validation passed! Now invoke the application callback
    app_data, app_callback = data.on_number_input
    return app_callback(app_data, callbackinfo, number)
```

Finally, the top-level application logic of `VerifyAgeApplication` is then completely 
decoupled from UI concerns. The application can expect the `NumberInput` to call it back 
with a number, not a string (so the validation logic has already passed). It can therefore concentrate completely on the "business logic" aspect while the input validation is abstracted away and already done by the time this callback is reached:

```python
class VerifyAgeApplication:
    user_age: int

    def __init__(self, initial_age):
        self.user_age = initial_age

def layout_func(data, layoutinfo):
    # uses the public interface of NumberInput
    # doesn't care about how it's implemented internally
    ni = NumberInput(data.user_age)
    # Pass a backreference to the application down to NumberInput
    ni.set_on_number_input(data, _on_age_input)
    # turns NumberInput into a plain DOM subtree
    return Dom.createBody()
        .withChild(ni.dom())

# PRIVATE to VerifyAgeApplication
def _on_age_input(data, callbackinfo, new_age):
    # This callback only runs if the input was a valid number
    if new_age < 18:
        MsgBox.ok("You must be older than 18 to proceed")
        return Update.DoNothing
    else:
        data.user_age = new_age
        return Update.RefreshDom

app = App(VerifyAgeApplication(18), AppConfig(LayoutSolver.Default))
app.run(WindowCreateOptions(layout_func))
```

The key here is that no matter how complex the application gets in its „State Graph“, it is now decoupled from any „Visual Tree“, i.e. from the visual hierarchy, the `Dom`, itself.

```mermaid
graph TD
    A[VerifyAgeApplication State] -.->|backreference| B[NumberInput State]
    B -.->|backreference| C[TextInput State]
    C -->|event follows backref| B
    B -->|event follows backref| A
    
    linkStyle 2 stroke:#0a0,stroke-width:2px
    linkStyle 3 stroke:#0a0,stroke-width:2px
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#ccf,stroke:#333,stroke-width:2px
    style C fill:#cfc,stroke:#333,stroke-width:2px
```

When the user now finishes editing and the input loses focus, the event flows 
through the backreferences:

1. `_on_focus_lost(RefAny<TextInput>, text_string)`
2. `_validate_text_input_as_number(RefAny<NumberInput>, text_string)`
3. `_on_age_input(RefAny<MyApplication>, validated_number)`

Each level knows only about its immediate parent via the backreference and can be unit-tested individually. `TextInput` has 
no knowledge of `VerifyAgeApplication`, and `VerifyAgeApplication` has no knowledge of the specific UI widget 
being used. Additionally, its data model does *not* contain any handle to the input widget - instead it only stores the user's age and re-creates the text input widget. The framework will then take care of keeping scroll positions, focus and cursor positions in the exact same place.

This pattern scales to arbitrary depth. You could create an `EmailInput` that wraps 
`TextInput` and validates email format, or a `CreditCardInput` that validates card IDs. 
Each layer simply adds logic without coupling to the layers above or below.

### Complex: Node Graph Editor

The power of backreferences becomes even clearer with non-hierarchical state dependencies. 
Consider a node graph editor, where the logical connections between nodes (a complex graph) 
have no real relation to their visual layout (a flat list of sibling elements on a canvas).

In the 
`NodeGraph`, when a user clicks an input port on a node, how does the widget tell the top-level 
`NodeGraph` state to create a connection? It doesn't send a message „up“ the Visual Tree. 
Similar to the `TextInput`, it follows a pre-defined „chain of backreferences“ (operating on the „Logical Graph“):

1.  The `Dom` for the input port has a callback holding a `PortWidget`'s data.
2.  This `PortWidget` contains a backreference to its logical parent, the `NodeWidget`.
3.  The `NodeWidget` in turn holds a backreference to the top-level `NodeGraphWidget`, which contains
    the entire application state.

The callback for the click event on a visual nodes' `Input` / `Output` simply follows 
this chain of references, making a direct jump from the event source to the top-level data model.

```python
# Pseudo-code illustrating the backreference chain
# These are not UI elements, but only the logical controllers for them

class NodeGraphWidget:
    def __init__(self, graph_state):
        self.graph_state = graph_state  # The actual application data

    # Logic that lives at the top level
    def on_port_clicked(self, port_id):
        print(f"LOGIC(NodeGraph): Port {port_id} clicked. Updating global state.")
        # ... logic to connect nodes in self.graph_state ...

class NodeWidget:
    def __init__(self, node_id, graph_widget_ref):
        self.node_id = node_id
        self.graph_widget_ref = graph_widget_ref  # Backreference to the graph

    # This method is „lent“ to the PortWidget
    def on_port_clicked(self, port_id):
        print(f"LOGIC(Node): Click received for port {port_id}. Forwarding to graph."")
        # Uses its backreference to call the top-level logic
        self.graph_widget_ref.on_port_clicked(port_id)

class PortWidget:
    def __init__(self, port_id, node_widget_ref):
        self.port_id = port_id
        self.node_widget_ref = node_widget_ref  # Backreference to the node

    # This would be the callback attached to the UI element
    def handle_click_event(self):
        print(f"EVENT on Port {self.port_id}"")
        # Uses its backreference to start the logical chain
        self.node_widget_ref.on_port_clicked(self.port_id)
```

Wiring it all up:

```python
# Top-level state and logic controller
app_state = {"nodes": {}, "connections": []}
graph_controller = NodeGraphWidget(app_state)

# 2. Create controllers for child components, passing down backreferences
node_a_controller = NodeWidget("NodeA", graph_controller)
port_a1_controller = PortWidget("PortA1", node_a_controller)

# 3. Simulate a user clicking the visual port
port_a1_controller.handle_click_event()
```

The flow of control again follows the logical graph:

1. `Event` -> `PortWidget.handle_click_event()` 
2. `PortWidget.handle_click_event()` -> `NodeWidget.on_port_clicked()` 
3. `NodeWidget.on_port_clicked()` -> `NodeGraphWidget.on_port_clicked()`

During testing, you can simply pass in a mock `RefAny` and test that this part of your application or this widget hits all branches (in code coverage) and that it modifies the state / calls the expected callback - making it "UI testable" without there being any actual UI. The key is that the `PortWidget` doesn't know what the `NodeGraphWidget` is, only that 
it must call a function on the reference it was given.

In the "real-world" NodeGraph, the flow is more complex, but the pattern scales horizontally,
irrespective of the number of events that the graph needs to handle or the complexity of the 
graphs features.

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Inter-widget communication

The last problem is a rare occurrence but occasionally happens: How do we connect two widgets, where the modification of a widget has to lead to a visual change of the other, without the other widget knowing what the UI structure looks like? In the previous examples, we used a full `layout()` call to make sure the UI is always synchronized and relied on the framework to keep things like focus, cursor and scroll positions in-place across multiple `layout()` calls. However, for very fast changes, Azul offers things like `info.set_css_property()` to quickly modify a CSS property.

In the `AzPaint` demo we have a canvas which can respond to pen input at up to 140 packets per second - on the side of the canvas we also have a `ProgressBar` to reflect the pen pressure (i.e. from 0 to 100). So, how do we keep the UI in sync here? The "easy way" would be to put the pressure in the application data, update it on every `On::Touch` event and call `layout()` and rely that the frameworks caching is good enough.

1. Store the pressure in the app's data model
2. Return `Update::RefreshDom`
3. `layout()` runs again, the DOM is diffed, the bar is rebuilt at the new width

This would work - but it would be a bit wasteful. Instead, we want to "connect" the `Canvas` and the `ProgressBar` showing the pressure strength without calling `layout()` and then keep the logic for updating the `ProgressBar` contained in its own API, so the `Canvas` doesn't need to know anything about the UI structure of the `ProgressBar`.

Azul offers a "fast path" for these scenarios:

**1.** During `layout()`, the app creates a
fresh `Uuid::short()` string and annotates the "connected" `Dom` node with this marker. Then, it copies this `Uuid` into the callback-local `RefAny`. The marker UUID here acts like an HTML `id`, except that it is **invisible to CSS and doesn't affect node equality** - creating a fresh UUID on every `layout()` doesn't affect the diff, so no `Mount` / `Unmount` events fire by accident.

```rust,no_run
// layout(): mint the address and stamp it on the widget...
let marker = Uuid::short();
let bar = ProgressBar::create(0.0)
    .dom()
    .with_marker(Some(marker.clone()).into());
// ...and keep the same string in the state the canvas callbacks get.
```

**2.** When the canvas now receives pressure, its
callback resolves the marker to a node id, then hands that node to a
function that the `ProgressBar` widget exports:

```rust,no_run
extern "C" fn on_pen_move(mut data: RefAny, mut info: CallbackInfo) -> Update {
    let pressure = 63.0; // from info.get_pen_state()
    let marker = match data.downcast_ref::<PaintState>() {
        Some(s) => s.pressure_marker.clone(), // the Uuid layout() minted
        None => return Update::DoNothing,
    };
    if let Some(node) = info.get_node_id_by_marker(marker).into_option() {
        ProgressBar::update_progress(info, node, pressure);
    }
    Update::DoNothing // nothing else changed - and the bar STILL moves
}
```

Almost like magic, the `ProgressBar` now updates on pressure, without any new calls to `layout()` - so what's going on here?

**3.** Internally, `ProgressBar::dom()` creates a minimal `VirtualView` for the actual bar - a `VirtualView` is usually used for infinite content, but can also be used for "sections" of a `Dom`, that can create a new sub-`Dom` and update it dynamically.

```mermaid
graph TD
    L["layout(): ProgressBar::dom()<br/>.with_marker(Some(uuid))"] -->|stamps| N["VirtualView node<br/>marker: uuid, dataset: private"]
    E["Pen pressure event<br/>(canvas callback)"] -->|"get_node_id_by_marker(uuid)"| N
    E -->|"ProgressBar::update_progress(info, node, 63.0)"| P["widget downcasts its PRIVATE dataset<br/>stores 63.0"]
    P -->|trigger_virtual_view_rerender| F["framework re-invokes ONE VirtualView callback"]
    F -->|"returns new bar DOM"| R["restyle + layout inside existing bounds<br/>damage rect = just the bar"]

    classDef event fill:#cfc,stroke:#333,stroke-width:2px
    classDef dataset fill:#ccf,stroke:#333,stroke-width:2px
    classDef data fill:#f9f,stroke:#333,stroke-width:2px
    class E event
    class N,P dataset
    class F,R data
```

This is, however, an exception for performance reasons, not the usual case. The `ProgressBar` then triggers a `info.trigger_virtual_view_rerender(node_id)`, which queues its "bar" for being recreated - similar to fine-grained reactivity, the framework now knows that only this section of the `Dom` was updated without doing a full diff over the entire `Dom`. To sum it up:

- Real "application state" belongs in the data model and updates via `layout()` and `RefreshDom`
- Simple UI presentation values (live meters, previews, scrub positions) belong on the fast path

## Summary

The main takeaway from this document is that Azuls „reason d'etre“ (reason for existence) lies less in its technical merit ("Like X but in Rust!", "you should use webrender over vello, it's so much better"), but rather in its distinct architecture.

The goal is not just to show how Azul solves these problems, but also to reflect on the „why“ of existing pain points in GUI programming. Beyond Azuls solution, we hope this helps programmers to articulate the core three problems in GUI programming - because someone who cannot articulate a problem cannot solve it.

---

### Doc Guide En Debugging (`doc/guide/en/debugging.md`)

---
slug: debugging
title: Debugging
language: en
canonical_slug: debugging
audience: external
maturity: wip
guide_order: 200
topic_only: false
short_desc: Debug overlays, the inspector, and structured logging
prerequisites: [hello-world]
tracked_files:
  - core/src/debug.rs
  - dll/src/desktop/logging.rs
  - dll/src/desktop/shell2/common/debug_server.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T00:00:00Z
default-search-keys:
  - App
  - CallbackInfo
  - take_screenshot
  - take_native_screenshot
  - Dom
---

# Debugging

## Introduction

*WIP.* The flag set, the HTTP debug server, and the in-browser debugger all work today; names of endpoints and env vars may shift.

Azul ships an HTTP debug server that runs inside your application process. Set `AZ_DEBUG=<port>` and a thread binds `127.0.0.1:<port>`, serves an inspector UI at `/`, and accepts JSON commands at `POST /` that drive the application as if a user were clicking it. The same channel powers programmatic E2E tests (covered in [End-to-End Testing](debugging/e2e-testing.md)) and memory probes (covered in [Memory and Profiling](debugging/profiling.md)).

```bash
AZ_DEBUG=8765 ./my_app &
curl -s http://localhost:8765/health
curl -s -X POST http://localhost:8765/ -d '{"op":"get_dom_tree"}'
```

## Environment flags

Every flag is read once at process start. Unset means off — **except `AZ_LOG`, which is ON by default** (see below). All are independent and can be combined.

- `AZ_LOG=<level>`. Controls Azul's built-in stderr logger, **enabled by default**. Azul installs a logger automatically at `App::create` so the platform layer (windowing, event loop, layout, device backends) is never silent — if your app exits unexpectedly, the reason is on stderr. Levels: `off`/`0`/`false` silences it entirely; `error`, `warn`, `info`, `debug` (the default), `trace` (everything, including per-frame). It honors `NO_COLOR` and only colorizes a TTY. If your host already installs a logger (Python's `pyo3-log`, Android's `android_logger`, your own `env_logger`), Azul's logger steps aside and does not override it.
- `AZ_DEBUG=<port>`. Binds the HTTP debug server on `127.0.0.1:<port>`. A bind failure exits the process.
- `AZ_BACKEND=<mode>`. One of `auto`, `gpu`, `cpu`, or `headless`. Resolves the rendering backend. `headless` skips the OS window and is required by the E2E runner. Default `auto`.
- `AZUL_HEADLESS=1`. Legacy alias for `AZ_BACKEND=headless`.
- `AZ_RECORD=<path>`. Appends every internal log message to `<path>` as plain text.
- `AZ_E2E=<path>`. Reads JSON tests from `<path>`, runs them, exits `0` (all pass) or `1` (any fail). See [End-to-End Testing](debugging/e2e-testing.md).
- `AZ_PROFILE=<tokens>`. Comma-separated profiler tokens for per-frame instrumentation. See [Memory and Profiling](debugging/profiling.md).
- `AZ_PROFILE_OUT=<path>`. JSONL output destination paired with `AZ_PROFILE=heap,jsonl`.
- `RUST_LOG=<filter>`. Standard `log` crate filter (env_logger syntax).

`AZ_DEBUG` and `AZUL_HEADLESS` compose: a CI run with `AZUL_HEADLESS=1 AZ_DEBUG=8765 ./my_app` boots a windowless process you can drive over HTTP. This is the supported configuration for screenshot diffing in CI.

## The HTTP debug server

When `AZ_DEBUG=<port>` is set, the server binds the port and registers a per-window timer that drains the request channel during the normal event loop. Commands therefore execute on the same thread that runs the layout, callback, and render passes. No shared-state races, no need to think about thread safety in your callback.

- `GET /`. Serves the inspector UI.
- `GET /health`. Status JSON: port, pending log count, recent log lines.
- `GET /material-icons.ttf`. Embedded Material Icons font used by the inspector.
- `POST /`. One JSON command. Blocks until the timer responds.
- `POST /debug/compile?lang=<rust|cpp|python>`. Compiles a CSS source body to a standalone project ZIP.

A request body is one debug event plus optional `window_id`, `wait_for_render`, and `timeout_secs`:

```json
{
  "op": "click",
  "selector": ".increment-btn",
  "wait_for_render": true,
  "timeout_secs": 30
}
```

The response is wrapped in a `{ "status": "ok" | "error", "request_id": <u64>, "data": {...}, "window_state": {...} }` envelope. The server pretty-prints `application/json` with `Connection: close`, so `curl` and `jq` work without ceremony:

```bash
curl -s -X POST http://localhost:8765/ \
     -H 'Content-Type: application/json' \
     -d '{"op":"click","selector":"button"}' | jq
```

## The command vocabulary

Each command's `op` field selects one debug event variant. Categories overlap with the in-browser inspector's panels.

- **Mouse.** `mouse_move`, `mouse_down`, `mouse_up`, `click`, `double_click`, `scroll` (programmatic `scroll_to` on the node under the point), `wheel` (a hardware wheel notch through the shells' hit-test + scroll-physics path).
- **Keyboard.** `key_down`, `key_up`, `text_input`.
- **Window.** `resize`, `move`, `focus`, `blur`, `close`, `dpi_changed`.
- **Queries.** `get_state`, `get_dom_tree`, `get_node_hierarchy`, `get_layout_tree`, `get_display_list`, `get_html_string`, `hit_test`, `get_logs`.
- **DOM mutation.** `insert_node`, `delete_node`, `set_node_text`, `set_node_classes`, `set_node_css_override`.
- **Scrolling.** `get_scroll_states`, `get_scrollable_nodes`, `scroll_node_by`, `scroll_node_to`, `scroll_into_view`.
- **Frame control.** `wait_frame`, `wait`, `relayout`, `redraw`.
- **Screenshots.** `take_screenshot` (CPU compositor), `take_native_screenshot` (current framebuffer).
- **Component and library introspection.** `get_component_registry`, `get_libraries`, `get_library_components`, `get_function_pointers`.
- **E2E.** `run_e2e_tests`.

`click` accepts whichever of `selector`, `node_id`, `text`, or `(x, y)` you pass. It resolves to a node, fires the click, and triggers a refresh if your callback returns one. This is the building block every E2E `click` step uses.

`wait_frame` is a barrier: the next step runs only after the window has prepared a frame that follows the request (the frame clock is the per-window `ContentJournal::frame_seq`, bumped once per frame on every backend). After any command that mutates state (`click`, `resize`, `set_node_text`, `text_input`, …) call `wait_frame` before reading state back, otherwise queries can race the relayout pass or the virtual-view re-renders queued for the next paint. A scenario step cannot hang on it: after ~2 s without a frame the barrier logs a warning and opens.

## A simple driver script

Drive a running app from bash. The Hello World sample's [`tests/e2e/hello-world.sh`](https://github.com/fschutt/azul/blob/master/tests/e2e/hello-world.sh) is built on the same five primitives:

```bash
#!/usr/bin/env bash
set -e
PORT=8765
APP=./target/release/hello-world
AZ_DEBUG=$PORT "$APP" &
APP_PID=$!
trap 'kill $APP_PID 2>/dev/null || true' EXIT

post() { curl -s -X POST "http://127.0.0.1:$PORT/" -d "$1"; }

# 1. Wait for the server to come up
until post '{"op":"get_state"}' >/dev/null 2>&1; do sleep 0.1; done

# 2. Wait for the first frame
post '{"op":"wait_frame"}' >/dev/null

# 3. Click a button by CSS selector
post '{"op":"click","selector":"button"}' | jq -r '.status'

# 4. Read the rendered HTML back
post '{"op":"get_html_string"}' | jq -r '.data.value.html'

# 5. Capture a PNG (base64 data URI)
post '{"op":"take_native_screenshot"}' \
  | jq -r '.data.value' \
  | sed 's|^data:image/png;base64,||' \
  | base64 -d > out.png
```

This pattern — `AZ_DEBUG`, wait, drive, query — is the foundation for both ad-hoc debugging and the JSON-described E2E tests in [End-to-End Testing](debugging/e2e-testing.md).

## The in-browser inspector

Navigate to `http://localhost:<port>/` in any browser and the server returns the bundled inspector: DOM tree, layout box overlay, computed CSS, scroll-state monitor, log stream, and an E2E test designer. The same `POST /` endpoints power its panels, so anything you see in the browser can be reproduced from a script.

The inspector is a single HTML/JS bundle compiled into the binary and served brotli-compressed. Disabling it means stripping the `AZ_DEBUG` codepath in the build. There is no runtime toggle.

## Logging and crash handling

By default Azul installs a built-in stderr logger at `App::create` (the `AZ_LOG` flag above), so every `log`-crate message — including the platform-layer traces (`App::run` entry, the selected display backend and `WAYLAND_DISPLAY`/`DISPLAY` on Linux, window creation, each layout pass, font loading) — prints to stderr without any extra setup. This is why an app that previously "just exited with no error" now tells you exactly which step it reached. Set `AZ_LOG=trace` for the full firehose, `AZ_LOG=off` to silence it, or install your own logger (which takes precedence). `RUST_LOG` further filters by target/level once a logger is installed, and `AZ_RECORD=<path>` mirrors every internal message (including the debug-server categories) to disk. The debug server also keeps its own ring buffer of recent entries; query it with `{"op":"get_logs"}` to see what fired during the last command.

If `App::run` returns an error (e.g. no display server could be opened), it is always written to stderr on every platform — on Linux the GUI message box silently no-ops without `zenity`/`kdialog`, so stderr is the guaranteed channel.

`App::create` installs a panic handler that captures and demangles the backtrace, logs the formatted panic at error level (visible in stdout, in `RUST_LOG`, in `AZ_RECORD`, and in `{"op":"get_logs"}`), and optionally opens a native `MsgBox` summarising the failure for the end user.

## When the timer is not running

`AZ_DEBUG` requires that the application reaches the event loop. If `App::run` is never called — for example, in a Rust unit test that builds a `Dom` and asserts its shape — the debug timer is never registered, and a `POST /` request hangs until `timeout_secs` elapses (default 30 s). For pure layout assertions, prefer the headless renderer covered in [End-to-End Testing](debugging/e2e-testing.md) or the reftest harness rather than `AZ_DEBUG`.

---

### Doc Guide En Dom (`doc/guide/en/dom.md`)

---
slug: dom
title: Document Object Model
language: en
canonical_slug: dom
audience: external
maturity: mature
guide_order: 50
topic_only: false
short_desc: Node types, hierarchy, and CSS scoping
tracked_files:
  - core/src/dom.rs
  - core/src/styled_dom.rs
  - core/src/xml.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:53:30Z
default-search-keys:
  - Dom
  - NodeData
  - NodeType
  - DomVec
  - NodeHierarchyItem
  - AttributeType
  - Css
  - CssPropertyWithConditionsVec
  - SmallAriaInfo
  - AccessibilityInfo
  - CallbackInfo
  - RefAny
  - EventFilter
  - Update
---

# Document Object Model

Azul's DOM differs from a browser DOM in four places:

1. **Hierarchy lives separately from node data.** The relationships
   (`parent`, `prev_sibling`, `next_sibling`, `last_child`) are in one
   array. The content (`tag`, `class`, inline CSS, callbacks) is in a
   parallel array. They're indexed by the same node id.
2. **Both arrays are flat `Vec`s in DOM tree order.** Parent before
   children. So a slice `data[self..self.last_child + 1]` is the
   subtree rooted at `self`. No pointer-chasing to walk a subtree.
3. **The DOM is frozen after `layout()` returns.** There is no
   `insertChild`, no `setAttribute`, no mutation observers. To change
   the tree, the next `layout()` call returns a new `Dom`. The
   framework diffs old against new and migrates state across.
4. **CSS is stored in a compact, layout-hot cache.** Common enum
   properties (`display`, `position`, `float`, `overflow`) are
   bit-packed into a single `u64` per node. The numbers and the cold
   paint properties live in two more arrays. The point is to make the
   per-node working set small enough that the layout pass stays in L2
   instead of round-tripping to RAM.

The result is a tree like this:

```text
         hierarchy[0..5]                  data[0..5]
       ┌───────────────┐               ┌──────────────────────┐
   0   │ parent: -     │ <body>        │ NodeData { ... }     │
   1   │ parent: 0     │   <div>       │ NodeData { ... }     │
   2   │ parent: 1     │     <span>    │ NodeData { ... }     │
   3   │ parent: 0     │   <p>         │ NodeData { ... }     │
   4   │ parent: 3     │     "text"    │ NodeData { ... }     │
       └───────────────┘               └──────────────────────┘
```

Indices into both arrays match. The layout engine traverses by index,
not by pointer, and reads from compact arrays whose memory layout it
controls.

## Cache hierarchy

Layout itself isn't algorithmically hard. It's a tree walk plus a lot
of if/else. The expensive part isn't the math; it's pulling each
node's properties out of memory.

A modern CPU has a tiered memory hierarchy. Cycle counts are
approximate but the order of magnitude is right:

- **L1 data cache** — 32 to 128 KB per core. ~4 cycles to read.
- **L2** — 256 KB to several MB per core. ~12 cycles.
- **L3** — 4 to 32 MB shared. ~30 to 60 cycles. Doesn't exist on
  embedded targets.
- **Main RAM** — gigabytes. ~100 to 300 cycles. A full miss costs
  more than running 100 instructions.

Layout reads the same per-node fields once per relayout pass. If the
working set fits in L2, the second pass is essentially free. If it
spills to RAM, every node fetch stalls the pipeline.

The relevant per-node sizes in the layout hot path:

```text
NodeHierarchyItem            32 B    parent + 3 sibling/child indices
StyledNodeState              10 B    :hover / :focus / :active per node
NodeFlags                     4 B    contenteditable, tab index, anonymous
compact-cache tier 1          8 B    display/position/float/etc bit-packed
compact-cache tier 2 (hot)   68 B    width, height, margin, padding, ...
compact-cache tier 2 (cold)  28 B    paint-only properties (color, opacity)
compact-cache tier 2b (text) 24 B    text-related layout

per-node total (hot)        ~150 B
per-node total (warm)       ~170 B   add cold + text tiers
NodeData (cold during layout) 152 B  read once for inline CSS, classes
```

For 1,000 nodes the layout-hot working set is ~150 KB. That fits in L2
on every desktop chip and most embedded ones. For 10,000 nodes it's
~1.5 MB, still L2 on a modern Apple/Intel core. For 100,000 nodes it's
~15 MB, which is L3 on desktop and main memory on embedded.

The numbers indicate when virtual views, lazy panels, and other ways
to keep the rendered subtree small start to matter. See
[Virtual Views](dom/virtual-views.md).

## What's in a node

Each node is split across the two parallel arrays.

`NodeHierarchyItem` carries four indices into the same array — the
node's parent, its previous and next siblings, and its last
descendant:

```rust,ignore
pub struct NodeHierarchyItem {
    pub parent: usize,            // 0 means "no parent"
    pub previous_sibling: usize,
    pub next_sibling: usize,
    pub last_child: usize,        // index of last descendant
}
```

Because children sit contiguously after their parent in tree order,
`data[self_idx ..= last_child]` is the whole subtree rooted at
`self_idx`. No pointer-chasing, no recursion needed to copy a subtree.

`NodeData` carries everything that defines a single node:

```rust,ignore
pub struct NodeData {
    pub node_type: NodeType,                          // HTML tag or leaf (Text/Image/Icon/VirtualView)
    pub callbacks: CoreCallbackDataVec,               // event handlers; empty for ~80% of nodes
    pub style: Css,                                   // inline CSS with implicit :scope, INLINE priority
    pub flags: NodeFlags,                             // tab index, contenteditable, anonymous
    pub accessibility: Option<Box<AccessibilityInfo>>, // ARIA, only on accessible nodes
    pub extra: Option<Box<NodeDataExt>>,              // attributes, dataset, menus, virtual view, ...
}
```

`style: Css` is the same struct the cascade uses everywhere else;
inline rules carry their conditions (`:hover`, `:focus`, `@theme dark`,
`@os macos`) directly. The two `Option<Box<...>>` fields keep the
common case small — a typical paragraph or div pays nothing for the
accessibility or extras boxes. About 95% of nodes never trigger the
`extra` allocation.

## A function of state

It helps to remember what the browser DOM was originally for. In the
1990s, web pages arrived over slow modems as a stream of HTML, and
the browser had to render *while* the document was still being
received. `document.write` injected new nodes mid-parse;
`appendChild`, `removeChild`, and the rest of the mutation API let
scripts patch the tree as more bytes arrived. Mutability wasn't a
design goal — it was a constraint of streaming over a 14.4k modem.

When SPAs took over, the streaming use case mostly went away, but the
mutation API stayed. React's contribution was to talk users out of
using it: model the UI as a function of state, render the whole tree
on every change, and let a reconciler diff old against new. Vue,
Solid, Svelte, and Elm all converged on the same shape. The browser's
imperative DOM became an implementation detail the framework hid.

Azul has no streaming parser to support and no legacy mutation API to
preserve, so it makes "UI is a function of state" the rule from the
start. The `Dom` returned from `layout()` becomes the framework's
copy:

1. A callback returns `Update::RefreshDom`.
2. The framework re-invokes the layout function.
3. A fresh `Dom` is built from the application data.
4. The framework diffs the new tree against the previous one and
   migrates focus, scroll, dataset, and merge-callback state across
   matched nodes.

There is no handle to the live tree, no `insertChild` /
`setAttribute` / mutation observer surface. Removing the mutation API
has two payoffs: half the bugs that show up in any non-trivial UI
come from "this listener saw stale state because something else
mutated the tree first," and a tree the framework *owns* is far
easier to lay out incrementally than a tree the application can
change at any time.

The reconciliation algorithm — what counts as "matching" old and new
nodes, what migrates, what fires lifecycle events — is documented in
[Reconciliation](dom/reconciliation.md).

State that has to survive a tree rebuild (a video decoder, a GL
texture, the cursor inside a focused input) doesn't live in the tree
shape. It hangs off the node as a dataset. See
[Datasets](dom/datasets.md) and [Merge Callbacks](dom/merge-callbacks.md).

## Building DOMs

### The recursive Dom value

`Dom` is the form actually constructed in user code:

```rust,ignore
pub struct Dom {
    pub root: NodeData,
    pub children: DomVec,
    pub css: CssVec,
    pub estimated_total_children: usize,
}
```

A `Dom` is a subtree: a root `NodeData`, its children, and any
component-level stylesheets attached via `.with_component_css(Css)`.
The framework
flattens the recursive form into the parallel `NodeHierarchyItem` /
`NodeData` arrays once, at the start of the cascade. Every builder
method on `Dom` (`with_class`, `with_callback`, `with_css`) is a
shorthand that delegates to the same method on `self.root`.

### Node constructors

Each HTML element has a `Dom::create_<tag>()` constructor. Most are
`const fn` and don't allocate until a child is added:

```rust,no_run
use azul::prelude::*;

let _ = Dom::create_div();
let _ = Dom::create_section();
let _ = Dom::create_article();
let _ = Dom::create_main();
let _ = Dom::create_nav();
let _ = Dom::create_header();
let _ = Dom::create_footer();
```

Text-bearing constructors take a string and wrap a `Text` child
inside the element:

```rust,no_run
use azul::prelude::*;

let _ = Dom::create_h1_with_text("Title");
let _ = Dom::create_h2_with_text("Section");
let _ = Dom::create_p_with_text("A paragraph.");
let _ = Dom::create_span_with_text("inline");
let _ = Dom::create_strong_with_text("important");
let _ = Dom::create_code_with_text("println!()");
let _ = Dom::create_text_do_not_use_without_block_level_wrapper("standalone text node");
```

> **Why the scary name?** Browsers silently wrap a raw text run in an
> *anonymous block box* whenever one is needed; azul does not. A bare text
> node therefore has **no box of its own** — no rect, no clip, no layout
> constraints — and any box-model CSS property, callback, `tab_index` or
> `dataset` you attach to it is silently inert. This has shipped real bugs
> (text escaping its widget, click targets that never fire). Put the text
> inside a block-level wrapper that carries the styling instead: use the
> `create_*_with_text` family (`create_p_with_text`, `create_div_with_text`,
> `create_span_with_text`, `create_h1_with_text`, ...) and style the wrapper.
> The engine also warns on stderr after layout when it finds a text node
> used without a containing block; once you have read and accepted the
> warnings, silence them with `AZ_SUPPRESS=bare_text`.

`NodeType` (in `core/src/dom.rs`) lists every variant. The set covers
all HTML elements plus the SVG subset plus four leaf types: `Text`,
`Image`, `Icon`, and `VirtualView`.

For elements with non-trivial accessibility surface, the primary
constructor takes an a11y struct as a required argument. There's a
matching `_no_a11y` variant that opts out explicitly. The longer
name on the opt-out is the point: it signals that a11y was skipped
on purpose, and it makes the absence visible during code review —
the *soft-force* pattern.

```rust
use azul::prelude::*;

// Primary form: a11y is part of the call signature.
let save = Dom::create_button("Save", SmallAriaInfo::label("Save document"));

// Explicit opt-out, longer name.
let ok = Dom::create_button_no_a11y("OK".into());
```

Most interactive elements use the generic `SmallAriaInfo` (label,
role, description). A few (`<progress>`, `<meter>`, `<dialog>`)
have type-specific structs because their a11y surface needs more
than that. Static, non-interactive elements (`div`, `span`, `p`,
the headings, inline text formatters) don't take a11y info — their
role is implicit from the element type.

See [Accessibility](system/accessibility.md) for the full list of elements
that follow the soft-force pattern, the type-specific aria structs,
and how the framework translates them into the platform-specific
accessibility trees (UIA, AT-SPI, NSAccessibility).

### IDs, classes, attributes

```rust,no_run
use azul::prelude::*;

let _ = Dom::create_div()
    .with_id("sidebar".into())
    .with_class("panel".into())
    .with_class("scrollable".into())
    .with_attribute(AttributeType::AriaLabel("notification banner".into()))
    .with_attribute(AttributeType::Lang("en".into()));
```

IDs and classes aren't separate fields. They're stored as
`AttributeType::Id` and `AttributeType::Class` entries in the node's
attribute list. The selector `.panel { ... }` matches every node
whose attribute list contains `Class("panel")`.

`AttributeType` (in `core/src/dom.rs`) is a strongly-typed enum:
`Href`, `Src`, `Alt`, `AriaLabel`, `Required`, `MaxLength(i32)`,
`ContentEditable(bool)`, and so on. There's a `Custom` fallback for
arbitrary `name="value"` pairs. Attributes aren't inline CSS — they
feed accessibility, attribute selectors like `[lang="en"]`, and
HTML/XML serialization.

### Adding children

Three ways to attach children:

```rust,no_run
use azul::prelude::*;

// 1. One at a time. Each call grows .children by one.
let a = Dom::create_div()
    .with_child(Dom::create_h2_with_text("Title"))
    .with_child(Dom::create_p_with_text("Body"));

// 2. Replace the child vec wholesale.
let kids: DomVec = vec![Dom::create_span_with_text("x"), Dom::create_span_with_text("y"), Dom::create_span_with_text("z")].into();
let b = Dom::create_div().with_children(kids);

// 3. Collect from an iterator into a parent.
let c: Dom = (0..3).map(|i| Dom::create_li_with_text(format!("Item {}", i))).collect();
// Produces a NodeType::Div containing three <li> children.
```

`with_child` calls `add_child`, which pushes onto the underlying
`Vec` and updates `estimated_total_children`. That's amortised O(1)
per call. `with_children(DomVec)` is one allocation total.

`estimated_total_children` is maintained by every `add_child` and
`set_children` call. The framework reads it to pre-size the flat
arena during conversion. If `children` is mutated directly, call
`fixup_children_estimated()` before returning.

### Defining a clipping path

Two public mechanisms cover the common cases:

- `with_clip_mask(ImageMask)` takes a raster alpha mask. Use it for
  irregular shapes that already exist as image data.
- `with_css("clip-path: ...;")` parses the CSS property into the
  node's inline-CSS list. Applied during the cascade.

```rust,no_run
use azul::prelude::*;

fn build(mask: ImageMask) -> Dom {
  let raster = Dom::create_image(ImageRef::null_image(0, 0, RawImageFormat::R8, U8VecRef::from(&[][..])))
    .with_clip_mask(mask);

  let css_form = Dom::create_div()
    .with_css("clip-path: circle(40px at 50% 50%);");

  Dom::create_body().with_child(raster).with_child(css_form)
}
```

A `clip-path` set on a parent applies to every descendant.

### Inline CSS

The primary way to attach CSS is `.with_css(...)` on the node itself.
The method takes a string, parses it through the same pipeline the
cascade uses elsewhere, and stores the result in
`NodeData::style: Css`.

```rust,no_run
use azul::prelude::*;

let item = Dom::create_div().with_css("
    color: blue;
    font-size: 14px;
    :hover { color: red; }
    @theme dark { color: white; background: #222; }
");
```

The parsed rules carry their conditions directly: `:hover`,
`:focus`, `:active`, `@os`, and `@theme` blocks all live inside the
same `Css` value. Conditions are re-evaluated per frame, so
`@theme dark { ... }` flips when the user toggles dark mode without
any re-layout. Inline rules are tagged `rule_priority::INLINE` so
they win the cascade against author CSS.

After the recent unification, the inline store is a regular `Css` —
the legacy `css_props: CssPropertyWithConditionsVec` field is gone.
`with_css_props(vec)` still works as a compatibility shim that maps
each property to a single-declaration rule at INLINE priority.

### Component-level stylesheets

Reusable components ship a parsed stylesheet that travels with the
subtree. Attach it on the component's root with `.with_component_css(Css)`:

```rust,no_run
use azul::prelude::*;

let widgets = Css::from_string(".panel { padding: 8px; }".into());
let panel: Dom = Dom::create_div()
    .with_class("panel".into())
    .with_component_css(widgets);
```

A browser cascades every stylesheet against every node in one global
pass — `.panel { ... }` in one tab can match a `.panel` in any iframe
that imports the same stylesheet, and changes there force a global
restyle. Azul's component CSS travels *with the subtree*. The
framework merges every component-level `Css` together when it
flattens the tree, but the rules a component ships only have a
chance to match the nodes the component itself owns. Anything
outside the component's subtree is invisible to its selectors. This
is a soft scope (the framework doesn't enforce a Shadow-DOM
boundary), but it follows from the way components label their roots
and avoids the cross-component restyle storms a global cascade
produces. For hard scoping, write selectors that nest under the
component's root class.

User-level theming sits at the *outermost* layer: the system
`@theme dark` block, the `system:*` color keywords, and the optional
end-user ricing file all target the framework-wide hooks. Component
CSS doesn't fight user theming because the two layers target
different selectors. See [Components](architecture/components.md) for the
component-pack model and [Theming](styling/themes.md) for the full
theming model and the `AZ_RICING` opt-out.

The cascade runs *once*, after the `LayoutCallback` returns.
`NodeData::style: Css` and `Dom::css: CssVec` are opaque state
during `layout()`. The framework collects the rules at the end of
the callback, sorts by `(priority, specificity)`, and walks the tree
once to fill the compact cache. Selector matching, inheritance, and
the compact-cache build all happen there. CSS work inside `layout()`
is cheap because each call is just a parse and a push.

### Inside the layout callback

A `layout()` callback receives application data and a
`LayoutCallbackInfo` describing the window. Returning a `Dom`
finishes the pass; the framework reconciles, lays out, and renders.

```rust,no_run
use azul::prelude::*;

struct AppModel {
    user_name: String,
    locale: Locale,
}

extern "C" fn layout(data: &mut RefAny, info: LayoutCallbackInfo) -> StyledDom {
    let model = match data.downcast_ref::<AppModel>() {
        Some(m) => m,
        None => return StyledDom::default(),
    };

    let strings = Strings::for_locale(model.locale);

    // Window-aware layout: switch to a single-column layout below 768px.
    let body = if info.window_width_less_than(768.0) {
        Dom::create_body()
            .with_css("display:flex; flex-direction:column;")
            .with_child(navbar_compact(&model.user_name, &strings))
            .with_child(content_area(&strings))
    } else {
        Dom::create_body()
            .with_css("display:grid; grid-template-columns:240px 1fr;")
            .with_child(sidebar(&model.user_name, &strings))
            .with_child(content_area(&strings))
    };

    body.with_component_css(app_stylesheet())
}
```

The `LayoutCallbackInfo` exposes everything needed to make the
returned `Dom` adapt to the running window. Responsive sizing
(`window_width_less_than`, `window_width_between`, `window_height_*`,
and the raw `get_window_width` / `get_window_height`) covers the
per-tree branch cases (a hamburger menu vs a sidebar) — the
per-property cases (`@media`, `@theme`) are handled by inline CSS.
The framework re-invokes `layout()` whenever the window crosses a
breakpoint, the system theme flips, or a route switch fires, so the
width branch is always re-evaluated against the live window. The
callback can read `info.relayout_reason()` to find out *why* it was
called — `Resize`, `ThemeChange`, `RouteChange`, `RefreshDom`, or
`Initial` — and skip work that doesn't need to repeat (analytics
fetches, locale-pack loading) when the trigger was just a resize.

Other helpers: `get_dpi_factor` returns 1.0 / 2.0 / etc. for asset
selection; `get_route_pattern()` / `get_route_param(key)` for
router-driven trees; `get_monitors` / `get_max_monitor_size` for the
screens; `get_gl_context()` for canvas-backed nodes.

A worked example covering window-size, DPI, theme, route, and
Fluent localization in a single layout pass:

```
/* Detailed source-code truncated for AI context efficiency. */
```

The callback returns the `Dom` itself. Handing it back gives the
framework ownership: it runs the cascade once over the whole tree,
reconciles the result against the previous frame, and schedules
layout + paint.

## Routing

A multi-page app registers a layout callback per URL pattern on the
`AppConfig` — the framework picks the right one for the active
route and re-runs it on `switch_route`:

```rust,no_run
use azul::prelude::*;

extern "C" fn layout_home(_: &mut RefAny, _: LayoutCallbackInfo) -> Dom { todo!() }
extern "C" fn layout_user(_: &mut RefAny, info: LayoutCallbackInfo) -> Dom {
    let id = info.get_route_param("id".into());
    Dom::create_h1_with_text(format!("User #{}", id.as_str()).into())
}

fn main() {
    let mut config = AppConfig::create();
    config.add_route("/".into(), layout_home.into());
    config.add_route("/user/:id".into(), layout_user.into());

    let app = App::create(initial_data, config);
    app.run(WindowCreateOptions::new(layout_home));
}
```

A `:name` segment captures the path component as a parameter
readable via `info.get_route_param("name")`. On desktop the route
is in-memory state; on a web build the same routes also map to
HTTP endpoints with `history.pushState()` integration.

A user callback navigates with `CallbackInfo::switch_route` —
`info.set_route_param(key, value)` modifies a single param in place
without changing the active pattern:

```rust,ignore
extern "C" fn open_user(data: RefAny, mut info: CallbackInfo) -> Update {
    let id = match data.downcast_ref::<u64>() {
        Some(i) => *i,
        None => return Update::DoNothing,
    };
    let params = vec![StringPair {
        key: "id".into(),
        value: id.to_string().into(),
    }].into();
    info.switch_route("/user/:id".into(), params);
    Update::RefreshDom
}
```

The framework swaps the active layout callback on the next frame
and reconciles the new tree against the previous one. See
[Routing](architecture/routing.md) for the full pattern syntax, multi-route
layouts, and the web-vs-desktop differences.

## Parsing from XHTML

`Dom::create_from_parsed_xml` is the public entry point. Given an
`Xml` value, it returns a `Dom` ready to return from `layout()`:

```rust,no_run
use azul::prelude::*;

let xml_text = "";
let parsed = Xml::from_str(xml_text.into()).unwrap();
let dom: Dom = Dom::create_from_parsed_xml(parsed);
```

The XML parser walks `<html><head><style>...</style></head><body>...</body>`,
parses each `<style>` block into a `Css`, and attaches it scoped to the
node it was found inside. The cascade runs on the next layout pass like
any other DOM.

`ComponentMap` is the registry of XML-defined components. See
[Components](architecture/components.md#component-packs) for how the framework
looks up `<card title="..."/>` against a registered library.

## Parsing from SVG

The same XML pipeline accepts `<svg>` tags inside the body and turns
them into vector nodes that render alongside the rest of the Dom.
No extra wiring is required: the parser recognises the SVG namespace,
walks the geometry, and stamps an `SvgNodeData` on each shape. A
clip-mask attribute on an SVG element resolves the same way as a CSS
`clip-path:` (see [Defining a clipping path](#defining-a-clipping-path)
above).

```rust,no_run
use azul::prelude::*;

let xhtml = r#"
<html>
  <body>
    <svg viewBox="0 0 100 100" width="200" height="200">
      <circle cx="50" cy="50" r="40" fill="#1d4f8b"/>
      <rect x="10" y="10" width="40" height="40" fill="#5dade2"/>
    </svg>
  </body>
</html>
"#;
let parsed = Xml::from_str(xhtml.into()).unwrap();
let dom: Dom = Dom::create_from_parsed_xml(parsed);
```

The same `dom` is renderable without a window. Run the binary with
`AZ_BACKEND=headless` and the framework rasterises into an in-memory
framebuffer instead of opening a window. Combine with
`AZ_DEBUG=<port>` to drive `take_screenshot` from a shell script and
get a base64 PNG back. That's the path snapshot tests, PDF export,
and CI machines without a display server use.

For the full SVG geometry model (paths, tessellation, GPU
tessellated nodes), the standalone `Svg::from_string` parser that
returns a `RawImage`, and the GPU stroke pipeline, see
[SVG](images/svg.md). For the headless backend in detail, see
[Headless Rendering](headless/rendering.md).

## Callbacks

```rust,no_run
use azul::prelude::*;

struct Counter { value: i64 }
extern "C" fn on_click(mut data: RefAny, _info: CallbackInfo) -> Update {
    let mut c = match data.downcast_mut::<Counter>() { Some(c) => c, None => return Update::DoNothing };
    c.value += 1;
    Update::RefreshDom
}

fn build(state: RefAny) -> Dom {
  Dom::create_button_no_a11y("+1".into())
    .with_callback(EventFilter::Hover(HoverEventFilter::MouseUp), state, on_click)
}
```

`with_callback(filter, data, callback)` attaches a `RefAny` and a
function pointer. The handler fires when the matching event reaches
the node. The callback returns an `Update` that tells the framework
whether to re-run layout, re-render, or do nothing.

What the callback can actually *do* — read the dataset, query the
hit-test, dispatch to siblings, focus another node, schedule a
timer, post a thread message — lives in
[Callbacks](events/callbacks.md). Event filtering and propagation order
are in [Events and Input](events.md).

The framework's reconciler matches new nodes against old ones when a
fresh tree is returned. Cursor position, focus, and dataset state
migrate across the diff for matched nodes. See
[Reconciliation](dom/reconciliation.md).

## Datasets

`with_dataset(OptionRefAny)` attaches arbitrary user data to a node.
Callbacks read it via `CallbackInfo::get_dataset`. The dataset is the
canonical place for UI-layer state. The cursor inside an input. The
expansion flag on a tree-view row. A marker struct that says "I am the
save button" so a generic callback can dispatch. See
[Datasets](dom/datasets.md) for the navigation patterns.

For state that must survive a subtree rebuild, pair the dataset with
`with_merge_callback`. The framework calls it with the old and new
`RefAny` values during reconciliation. Heavy resources can move from
the old node to the new one. See [Merge Callbacks](dom/merge-callbacks.md)
for a worked FFmpeg example.

## Virtual Views

A `VirtualView` is a node whose contents come from a separate callback
that runs only when the framework needs them. Use it for infinite lists,
lazy panels, and embedded sub-DOMs that own their own scroll math. The
callback receives a `VirtualViewCallbackReason` (`InitialRender`,
`DomRecreated`, `BoundsExpanded`, `EdgeScrolled(_)`,
`ScrollBeyondContent`). Use the reason to skip work when the call is
just a parent re-render. See [Virtual Views](dom/virtual-views.md) for
the rendered-vs-virtual coordinate model and a virtualised-table
walkthrough.

## Debugging

Run the binary with `AZ_DEBUG=<port>` set and `App::create` starts
an HTTP debug server on that port. It accepts JSON commands and
returns JSON responses. The inspector sees the same tree the
renderer is about to draw, so a query reflects what's on screen.

```bash
AZ_DEBUG=8765 cargo run --bin my_app

# Synchronous queries:
curl -X POST http://localhost:8765/ -d '{"type":"get_state"}'
curl -X POST http://localhost:8765/ -d '{"type":"get_dom_tree"}'
curl -X POST http://localhost:8765/ -d '{"type":"get_node_hierarchy"}'
curl -X POST http://localhost:8765/ -d '{"type":"get_node_css_properties", "selector":".panel"}'
curl -X POST http://localhost:8765/ -d '{"type":"get_layout_tree"}'
curl -X POST http://localhost:8765/ -d '{"type":"get_display_list"}'

# Synthesised events (dispatched into the same callback path real input uses):
curl -X POST http://localhost:8765/ -d '{"type":"click", "selector":".button-primary"}'
curl -X POST http://localhost:8765/ -d '{"type":"text_input", "text":"hello"}'
```

The synthesised events go through the exact same dispatch path as
real input. A scripted `click` runs the same hit test, the same
event filters, and the same callback as a user mouse click. That
makes the debug server the basis for end-to-end tests: drive the
app from a shell script or a Python harness, assert on the JSON
responses, and the result is an integration test that exercises the
real layout, the real callbacks, and the real reconciliation pass.
The test pattern, the assertion vocabulary, and the CI recipe are
covered in [End-to-End Testing](debugging/e2e-testing.md).

For tests that don't need a window (snapshot tests, PDF export, CI
machines without a display server), the same debug API is also
reachable in [Headless Rendering](headless/rendering.md), which
runs the full layout and rendering pipeline into a `Vec<u8>`
framebuffer.

The `get_node_hierarchy` response carries a `component` field for
each node, allowing navigation back to the component that produced
it. The inspector uses it to draw a Component Tree alongside the DOM
Tree:

```json
{
  "index": 17, "node_type": "div", "tag": "card",
  "id": null, "classes": ["card", "card--info"],
  "parent": 12, "children": [18, 19, 20],
  "component": {
    "component_id": "shadcn:card",
    "data_model": { "title": "First", "body": "alpha" }
  },
  "rect": { "x": 16.0, "y": 16.0, "width": 320.0, "height": 88.0 },
  "events": ["MouseUp"], "tab_index": 0
}
```

Picking a node in the DOM Tree surfaces the component that produced
it, and (if its render function lives in a registered library) the
inspector links back to the source. See
[Components](architecture/components.md#component-packs) for how a library
wires its components into the registry.

---

### Doc Guide En Events (`doc/guide/en/events.md`)

---
slug: events
title: Events
language: en
canonical_slug: events
audience: external
maturity: mature
guide_order: 60
topic_only: false
short_desc: Callbacks, event filters, and how state triggers relayout
prerequisites: [hello-world]
tracked_files:
  - core/src/events.rs
  - core/src/callbacks.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:51:14Z
default-search-keys:
  - Dom
  - CallbackInfo
  - RefAny
  - Update
  - EventFilter
  - HoverEventFilter
  - FocusEventFilter
  - WindowEventFilter
  - ComponentEventFilter
  - KeyboardState
  - MouseState
  - add_callback
  - stop_propagation
  - prevent_default
---

# Events

## Overview

A callback is a function pointer plus a `RefAny`, registered against an event filter on a DOM node. When an event matches a registered filter, the framework borrows your `RefAny`, calls the callback, and reads the returned `Update` to decide whether to re-run the layout.

```rust,no_run
use azul::prelude::*;

struct Counter { n: usize }

extern "C" fn on_click(_: RefAny, _: CallbackInfo) -> Update { Update::DoNothing }

fn build(mut data: RefAny) -> Dom {
    Dom::create_div()
        .with_callback(EventFilter::Hover(HoverEventFilter::MouseUp), data, on_click)
}
```

## Adding a callback

The primitive is `Dom::add_callback` (and the builder form `Dom::with_callback`). Both take three arguments: the filter, a `RefAny` (your data), and a function pointer with the signature `extern "C" fn(RefAny, CallbackInfo) -> Update`.

```rust,no_run
use azul::prelude::*;

struct State;

extern "C" fn handler(_: RefAny, _: CallbackInfo) -> Update { 
    Update::DoNothing 
}

fn main() {
    let data: RefAny = RefAny::new(State);
    let node = Dom::create_div()
        .with_callback(EventFilter::Hover(HoverEventFilter::MouseUp), data.clone(), handler)
        .with_callback(EventFilter::Focus(FocusEventFilter::FocusReceived), data, handler);   
}
```

The `Update` enum has three variants:

- `Update::DoNothing`: no re-layout. The current frame stays on screen.
- `Update::RefreshDom`: re-run the layout callback for this window.
- `Update::RefreshDomAllWindows`: re-run the layout callback for every window.

Returning `RefreshDom` from a non-mutating handler is wasteful. Returning `DoNothing` from a handler that mutated the model leaves the screen out of sync. Match the return value to what you actually changed.

## Event filters

`EventFilter` is the enum you pass to `add_callback`. It has five variants, each scoped to a different fire condition:

- `EventFilter::Hover(HoverEventFilter)`: fires when the cursor is over this node.
- `EventFilter::Focus(FocusEventFilter)`: fires when this node has keyboard focus. The node needs a tab index.
- `EventFilter::Window(WindowEventFilter)`: fires for events anywhere in the focused window.
- `EventFilter::Component(ComponentEventFilter)`: fires when the node is mounted, unmounted, resized, or updated.
- `EventFilter::Application(ApplicationEventFilter)`: fires when a device or monitor is plugged in or removed.

The same physical event can match multiple filters. A left mouse-button release while the cursor is inside a button matches `Hover(MouseUp)`, `Hover(LeftMouseUp)`, and, if the button has focus, `Focus(MouseUp)` and `Focus(LeftMouseUp)`. All registered handlers fire.

### HoverEventFilter

The element the mouse is currently over. Common variants:

```rust,ignore
HoverEventFilter::MouseOver        // mouse moves while over the node
HoverEventFilter::MouseDown        // any button pressed
HoverEventFilter::LeftMouseDown    // left only (also Right/Middle)
HoverEventFilter::MouseUp          // any button released
HoverEventFilter::LeftMouseUp      // left only; use this for "click"
HoverEventFilter::DoubleClick      // double-click detected
HoverEventFilter::MouseEnter       // cursor crossed into the node
HoverEventFilter::MouseLeave       // cursor crossed out of the node
HoverEventFilter::Scroll           // wheel / trackpad over the node
HoverEventFilter::DroppedFile      // a file was dropped on the node
```

For "click", use `LeftMouseUp` rather than `MouseDown`. It matches the W3C activation pattern: press, move out, release does not click.

### FocusEventFilter

Same vocabulary as `HoverEventFilter`, but the node must currently hold keyboard focus. Set a tab index (or focus programmatically) for the filter to fire:

```rust,no_run
use azul::prelude::*;

let input = Dom::create_div()
    .with_tab_index(TabIndex::Auto);
```

`TabIndex::Auto` makes the node focusable in source order. `TabIndex::NoKeyboardFocus` makes it focusable programmatically but skips it in tab navigation. `TabIndex::OverrideInParent(n)` pins the node at slot `n` within its parent's tab order.

Common focus filters:

```rust,ignore
FocusEventFilter::FocusReceived    // node became the focused element
FocusEventFilter::FocusLost        // focus moved elsewhere
FocusEventFilter::TextInput        // a unicode character arrived
FocusEventFilter::VirtualKeyDown   // a non-text key (arrow, F1, ...)
FocusEventFilter::VirtualKeyUp
```

`FocusEventFilter::TextInput` carries the produced character, respecting the OS keyboard layout (German `ä`, IME composition, etc.). `VirtualKeyDown` carries a layout-independent key code. Use it for shortcuts and games.

### WindowEventFilter

Fires on any node registered with this filter regardless of where the cursor or focus is, as long as the window has OS focus:

```rust,ignore
WindowEventFilter::Resized           // window dimensions changed
WindowEventFilter::Moved             // window position changed
WindowEventFilter::CloseRequested    // user clicked the close button
WindowEventFilter::ThemeChanged      // OS switched light/dark mode
WindowEventFilter::DpiChanged        // window moved to monitor with different DPI
WindowEventFilter::VirtualKeyDown    // any keypress in the window
```

Use `Window` for global shortcuts (Ctrl+S, Esc) where the source node doesn't matter.

### ComponentEventFilter

Lifecycle events fire after a new DOM is reconciled against the previous frame:

```rust,ignore
ComponentEventFilter::AfterMount     // node appeared this frame
ComponentEventFilter::BeforeUnmount  // node will disappear next frame
ComponentEventFilter::NodeResized    // layout bounds of this node changed
ComponentEventFilter::Updated        // a keyed node's content changed
```

Reconciliation matches nodes across frames by stable key (`Dom::with_id("id")`) first, then by content hash. Keyed nodes track identity across reorders, so `Updated` only fires when keyed content actually changes.

### ApplicationEventFilter

Fires for global hardware changes. Useful only on the root DOM node:

```rust,ignore
ApplicationEventFilter::DeviceConnected
ApplicationEventFilter::DeviceDisconnected
ApplicationEventFilter::MonitorConnected
ApplicationEventFilter::MonitorDisconnected
```

## Event propagation

For each event the framework computes the path from root to target and calls handlers in three phases (W3C DOM Level 2):

1. **Capture**: root to target. Rare; only nodes with capture-phase handlers are visited.
2. **Target**: handlers on the target node itself.
3. **Bubble**: target to root.

A click on a deeply nested span walks back up through its ancestors, firing `Hover(MouseUp)` handlers on every node along the way that registered one. To stop the walk, call one of the propagation methods on `CallbackInfo`:

```rust,no_run
use azul::prelude::*;

extern "C" 
fn handler(_: RefAny, mut info: CallbackInfo) -> Update {
    // remaining handlers on the same node still run
    info.stop_propagation();
    // nothing else runs
    info.stop_immediate_propagation();
    Update::DoNothing
}
```

`stop_propagation` matches W3C `event.stopPropagation()`. `stop_immediate_propagation` matches `event.stopImmediatePropagation()`.

## Default actions

Some events have built-in behaviour that runs after every callback returns, unless a callback prevented it:

- `Tab`: move focus to next focusable element.
- `Shift+Tab`: move focus to previous focusable element.
- `Enter` / `Space` on focused button: synthetic click on the button.
- `Escape`: clear focus or close modal.
- `Ctrl+A` in text input: select all.
- Arrow keys in scroll container: scroll by line.

To suppress the default action from a callback:

```rust,no_run
use azul::prelude::*;

extern "C" fn on_keydown(_: RefAny, mut info: CallbackInfo) -> Update {
    info.prevent_default();
    Update::DoNothing
}
```

`prevent_default` corresponds to W3C `event.preventDefault()`. The W3C semantics: the default action doesn't fire, but other callbacks for the same event still run. Combine with `stop_propagation` to also halt the propagation walk.

## Reading input state

`CallbackInfo` exposes the current input snapshot:

```rust,ignore
let kbd: KeyboardState = info.get_current_keyboard_state();
let mouse: MouseState  = info.get_current_mouse_state();
let win:  WindowFlags  = info.get_current_window_flags();
let state: &FullWindowState = info.get_current_window_state();
```

`KeyboardState` carries `pressed_virtual_keycodes` (a vec of currently held keys) and `current_virtual_keycode` (the most recent). `MouseState` carries `cursor_position`, `left_down`, `right_down`, `middle_down`.

Read state inside the callback to check modifier keys for shortcuts:

```rust,no_run
use azul::prelude::*;

struct App;

extern "C" fn on_key(mut data: RefAny, mut info: CallbackInfo) -> Update {
    let kbd = info.get_current_keyboard_state();
    let pressed = kbd.pressed_virtual_keycodes.as_slice();
    let ctrl = pressed.iter().any(|k| matches!(k, VirtualKeyCode::LControl | VirtualKeyCode::RControl));
    let s = pressed.iter().any(|k| *k == VirtualKeyCode::S);
    if ctrl && s {
        info.prevent_default();
        // ... save ...
        return Update::RefreshDom;
    }
    Update::DoNothing
}
```

## Common patterns

**Click:**

```rust,no_run
use azul::prelude::*;

struct S;

extern "C" fn click(_: RefAny, _: CallbackInfo) -> Update { 
    Update::DoNothing 
}

fn main() {
    let data: RefAny = RefAny::new(S);
    Dom::create_div().with_callback(
        EventFilter::Hover(HoverEventFilter::LeftMouseUp),
        data,
        click,
    );
}
```

**Hover effect** (use CSS `:hover` for visual change; use a callback only when you need to mutate state):

```rust,no_run
use azul::prelude::*;

struct S;

extern "C" 
fn enter(_: RefAny, _: CallbackInfo) -> Update { 
    Update::DoNothing 
}

extern "C" 
fn leave(_: RefAny, _: CallbackInfo) -> Update { 
    Update::DoNothing 
}

fn main() {
    let data: RefAny = RefAny::new(S);
    Dom::create_div()
        .with_callback(EventFilter::Hover(HoverEventFilter::MouseEnter), data.clone(), enter)
        .with_callback(EventFilter::Hover(HoverEventFilter::MouseLeave), data,        leave);   
}
```

**Window-level keyboard shortcut:**

```rust,no_run
use azul::prelude::*;

struct S;

extern "C" fn on_key(_: RefAny, _: CallbackInfo) -> Update { 
    Update::DoNothing 
}

fn main() {
    let data: RefAny = RefAny::new(S);
    Dom::create_body().with_callback(
        EventFilter::Window(WindowEventFilter::VirtualKeyDown),
        data,
        on_key,
    );
}
```

**Tab order:** call `.with_tab_index(TabIndex::Auto)` to make a node focusable. Tab and Shift+Tab move through nodes in DOM order. Use `TabIndex::NoKeyboardFocus` to make a node focusable programmatically but skip it in tab navigation.

## Potential Problems

- **Callback never fires.** Check the filter scope. `Hover(LeftMouseUp)` only fires when the cursor is over the node at the moment of release. If the user pressed inside, dragged out, and released outside, no click event fires on either node.
- **`Focus(...)` never fires.** The node has no tab index. Add `.with_tab_index(TabIndex::Auto)` so the node can receive focus.
- **Counter doesn't update.** The callback returned `Update::DoNothing`. Return `Update::RefreshDom` after mutating the model.
- **Default action still happens after `prevent_default`.** Verify the call is on `CallbackInfo`, not on a stale copy. The change is applied after the callback returns. Calling it twice is harmless.

---

### Doc Guide En Hello World (`doc/guide/en/hello-world.md`)

---
slug: hello-world
title: Hello World
language: en
canonical_slug: hello-world
audience: external
maturity: mature
guide_order: 10
topic_only: false
short_desc: Window setup, state management, DOM creation, handling mouse clicks for a "counter" app
prerequisites: []
tracked_files:
  - api.json
  - core/src/callbacks.rs
  - core/src/lib.rs
  - dll/src/lib.rs
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - Dom
  - Css
---

# Hello World

## Introduction

Welcome to the Azul framework. In this guide you will learn how to write a simple 50-line program that produces a window with a counter and a button to increase said counter - showcasing how data models, click callbacks, and the installation and running of Azul applications work.

```azul-render screenshot=hello-world width=400 height=240 subtitle="Azul Window"
<body style="background-color: #efefef; margin: 0;">
  <p style="font-size: 50px; margin: 0;">5</p>
  <button>Increase counter</button>
</body>
```

Because each programming language is different, there's no such thing as "one hello world guide" as every language has differences in setup, installation methods, and code style. 

Each guide is self-contained, you do not need to read the others. Each page walks you through the same five-step path:

1. Installation or linking the Azul library.
2. Defining a data model.
3. Writing a `layout` callback that returns a `Dom`.
4. Attaching a click callback that mutates the model.
5. Building, running, notes for common pitfalls.

## Supported languages

Pick the "Hello World" / Setup guide for your language:

- [C (99+)](hello-world/c.md)
- [C++ (03 - 23)](hello-world/cpp.md)
- [C#](hello-world/csharp.md)
- [Crystal](hello-world/crystal.md)
- [D](hello-world/d.md)
- [Fortran](hello-world/fortran.md)
- [Go](hello-world/go.md)
- [Haskell](hello-world/haskell.md)
- [Java](hello-world/java.md)
- [Kotlin](hello-world/kotlin.md)
- [Lua](hello-world/lua.md)
- [Node.js](hello-world/node.md)
- [OCaml](hello-world/ocaml.md)
- [Pascal](hello-world/pascal.md)
- [Python (3.10+)](hello-world/python.md)
- [Ruby](hello-world/ruby.md)
- [Rust](hello-world/rust.md)
- [Scala](hello-world/scala.md)
- [Swift](hello-world/swift.md)
- [Zig](hello-world/zig.md)

---

### Doc Guide En Images (`doc/guide/en/images.md`)

---
slug: images
title: Images
language: en
canonical_slug: images
audience: external
maturity: wip
guide_order: 90
topic_only: false
short_desc: Loading raster images and CSS backgrounds
prerequisites: [dom]
tracked_files:
  - core/src/svg.rs
  - core/src/svg_path_parser.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - ImageRef
  - RawImage
  - RawImageData
  - RawImageFormat
  - Texture
  - RenderImageCallbackInfo
  - Dom
  - encode_png
  - encode_jpeg
  - get_size
---

# Images

## Introduction

*WIP.* APIs around `ImageRef`, SVG, and GPU textures are stable enough to use, but some helpers (e.g. PNG/JPEG decoding) live behind feature flags and may move between crates.

Azul has three sources of pixel content in a DOM tree:

- Raster image. Backed by a CPU pixel buffer (`RawImageFormat`). PNG, JPEG, BMP, raw bytes.
- Vector / SVG. Backed by `SvgMultiPolygon` plus tessellation. Icons, charts, diagrams.
- GPU texture. Backed by a `Texture` produced by callback. OpenGL scenes, custom shaders.

All three end up wrapped in a single `ImageRef` and inserted into the tree via `Dom::create_image(image_ref)`. The framework hashes the handle for caching and uploads the backing data to the renderer the first time it's shown.

## ImageRef

`ImageRef` is a reference-counted handle to decoded image data. It's `Send + Sync`. Cloning bumps a refcount; the underlying buffer is freed when the last clone drops. Construct one via:

- `ImageRef::null_image(w, h, format, tag)`. A placeholder of known size.
- `ImageRef::create_rawimage(raw)`. Wraps a CPU pixel buffer.
- `ImageRef::gl_texture(texture)` (also `new_gltexture`). Wraps an existing GL texture.
- `ImageRef::callback(cb, data)`. Defers rendering until layout knows the size.

Inspect with `is_null_image`, `is_raw_image`, `is_gl_texture`, `is_callback`, `is_invalid`, `get_size`, `get_hash`.

## Raster images

Raw pixel data flows through `RawImage`. The pixel layout is `RawImageFormat`: `R8`, `RG8`, `RGB8`, `RGBA8`, `R16`, `RG16`, `RGB16`, `RGBA16`, `BGR8`, `BGRA8`, `RGBF32`, `RGBAF32`. Pick the one that matches the source bytes; the renderer converts to its internal format on upload.

```rust,no_run
use azul::prelude::*;

fn load_pixels() -> Vec<u8> { vec![] }

let bytes: Vec<u8> = load_pixels();
let raw = RawImage {
    pixels: RawImageData::U8(bytes.into()),
    width: 256,
    height: 256,
    premultiplied_alpha: true,
    data_format: RawImageFormat::RGBA8,
    tag: Vec::new().into(),
};
let image_ref = ImageRef::create_rawimage(raw).expect("invalid pixel data");
let dom = Dom::create_image(image_ref);
```

`RawImageData` is one of `U8`, `U16`, `F32`. Decoded image dimensions are reported by `ImageRef::get_size()`. The size is fixed once the buffer is created. To resize at render time, wrap the image in a styled element and let CSS scale the box (set `width` / `height` via `Dom::with_css` or `Dom::with_css_property`).

A `null_image` keeps a slot in the cache without uploading data. It's useful as a fallback when an asynchronous loader hasn't finished yet.

`RawImage` also exposes encoders (`encode_png`, `encode_jpeg`, `encode_bmp`, `encode_gif`, `encode_pnm`, `encode_tga`, `encode_tiff`) and two decoders. `RawImage::decode_image_bytes_any` is the synchronous, pure-compute decoder that works on every target. `RawImage::decode_image_bytes` is the request form: it resumes a callback with an `ImageDecodeResult` and, in the browser, hands the work to the hardware decoder - the faster choice for large images. Decoded pixels may differ slightly per backend (premultiplication, ICC handling).

## SVG

Vector graphics go through tessellation: every closed path is converted to a triangle list, then rendered on the CPU or uploaded to the GPU. Build paths in memory with `SvgPath::create` and combine them via `SvgMultiPolygon::create`. Then call `SvgMultiPolygon::tessellate_fill` or `tessellate_stroke` to produce a `TessellatedSvgNode`.

See [SVG](images/svg.md) for the full geometry model, stroking options, and how to combine multiple polygons into one draw call.

## GPU textures and custom drawing

`ImageRef::callback(...)` defers image production until the layout pass knows the box dimensions. The callback receives a `RenderImageCallbackInfo` with the available GL context and the laid-out bounds, and returns an `ImageRef` (typically wrapping a fresh `Texture`):

```rust,no_run
use azul::prelude::*;

extern "C" 
fn render(_data: RefAny, mut info: RenderImageCallbackInfo) -> ImageRef {
    let size = info.get_bounds().get_physical_size();
    // allocate a texture, draw into it, return ImageRef::gl_texture(tex)
    ImageRef::null_image(size.width as usize, size.height as usize,
                        RawImageFormat::RGBA8, U8VecRef::from(&[][..]))
}

fn build_dom(state: RefAny) -> Dom {
    Dom::create_image(ImageRef::callback(
        RenderImageCallback::create(render).to_core(),
        state,
    ))
}
```

An image callback is not GL-specific: it may return a raw CPU image just as well as a texture, and it is how a paint canvas, a chart or a video frame is drawn. See [Image Callbacks](images/image-callbacks.md) for the general shape - repainting one node with `update_image_callback`, caching by revision, and animating - and [Canvas and GL Textures](images/canvas-gl.md) for the texture allocation, drawing and FXAA flow used by the `opengl` example.

## Sizing and aspect ratio

The renderer treats an `ImageRef` like an `<img>` tag: it expands to fill its CSS box. To preserve aspect ratio:

- `width: 100px; height: 100px`. Stretched to a square.
- `width: 100px` (height auto). Scaled to keep the source aspect ratio.
- `flex-grow: 1` (square parent). Fills the parent.

Image content is stretched without filtering hints. For icon-quality output you usually want the source resolution to match the on-screen pixel size.

## Updating an image

`ImageRef` is intern-keyed: passing a clone in two consecutive `Dom`s reuses the same upload. To swap pixels you build a new `ImageRef`. Drop the old clone if you want the GPU memory freed.

From a callback, `change_node_image(dom_id, node_id, image, update_type)` swaps a node's image in place - `UpdateImageType::Content` for the image itself, `UpdateImageType::Background` for its CSS background - with no `layout()` pass. `add_image_to_cache(name, image)` puts an image under a name any later layout pass can reference, which is how a decoder thread hands its result to the UI.

For animation, use a render-image callback (`ImageRef::callback`) and a [timer](animations/timers.md) to repaint each frame. See [Image Callbacks](images/image-callbacks.md).

## Image masks

`ImageMask` clips drawn content to an image-defined alpha mask. It carries `image: ImageRef`, `rect: LogicalRect` (element-local), and `repeat: bool`, which tiles the mask across the node. Coverage outside the mask is zero, so a mask smaller than its node hides the rest of it. Apply one with `Dom::with_clip_mask`, and change it from a callback with `change_node_image_mask` - masks are a paint-time effect, so changing one never reflows. A node carrying SVG path geometry is clipped to that path instead.

See [Clip Masks](images/clip-masks.md) for both kinds, including animating a mask and using an image callback as one.

---

### Doc Guide En Layout (`doc/guide/en/layout.md`)

---
slug: layout
title: Layout
language: en
canonical_slug: layout
audience: external
maturity: mature
guide_order: 80
topic_only: false
short_desc: Overview of the layout solver
prerequisites: [styling]
tracked_files:
  - css/src/props/layout/mod.rs
  - css/src/props/layout/display.rs
  - css/src/props/layout/dimensions.rs
  - css/src/props/layout/spacing.rs
  - css/src/props/layout/wrapping.rs
  - css/src/props/layout/fragmentation.rs
  - css/src/props/layout/column.rs
  - css/src/props/layout/table.rs
  - css/src/props/layout/shape.rs
  - css/src/props/layout/flow.rs
  - layout/src/lib.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:49:28Z
default-search-keys:
  - Dom
  - Css
  - CssProperty
  - StyledDom
  - LayoutCallback
  - LayoutCallbackInfo
---

# Layout

## Overview

Azul lays out content according to the W3C browser specifications, so this page will just give a main overview of the *differences* of Azul vs Google Chrome, which mainly revolves around APIs for measuring / retrieving layout. The layout modes (box, inline, flex, grid, table, ...) are exactly modeled after the W3C specification, any deviation from that can be reported as a bug.

If you already know how CSS values work, then you can skip all sub-pages referenced here and only read this page. The sub-pages only contain examples and descriptions of the various main layout modes, which are:

- **Block** - default, full-width stacked boxes. See
  [Blocks, Sizing, and Positioning](layout/blocks.md).
- **Inline** - text and inline-block runs inside a block. See [Inline Layout](layout/inline.md).
- **Flex** - one-axis containers (rows or columns). See [Flexbox](layout/flex.md).
- **Grid** - two-axis containers (columns and rows). See [Grid](layout/grid.md).

Please also see the way better documentation on HTML layout modes:

- [MDN - Box Model](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Box_model): extensive documentation, however Azul does not support all properties, only common ones
- [W3CSchools - Box Model](https://www.w3schools.com/css/css_boxmodel.asp): simpler, but more approachable documentation
- [Every Layout](https://every-layout.dev/rudiments/boxes/) - explains the box model more visually

Azul does support (with tests) even advanced writing modes such as vertical text in various directions, as well as of course RTL text (which affects properties such as `start` and `end`). Currently, there is however no support of `<ruby>` text.

## Adding stylesheets

In the previous documentation, and even in the hello-world example, you already came across
the single way stylesheets are added to the `Dom`: setting `.with_css("...")`. However, it's
very important to see what that does internally:

```rust
impl Dom {

    pub fn with_css(mut self, style: &str) -> Self {
        self.set_css(style);
        self
    }
    
    pub fn set_css(&mut self, style: &str) {
        self.add_component_css(Css::parse_inline(style));
    }

    pub fn add_component_css(&mut self, css: Css) {
        self.css.push(css);
    }
}
```

Azul does not immediately cascade the CSS and applies it to the nodes, instead it 
"stores" the CSS in an order and then applies / cascades it later in one go. Another
difference is that there is no `<style>` node like in HTML, as the CSS stylesheet can
only cascade within one subtree, not affect anything outside of it. 

This is crucial for components, so you don't have to hack together anything like 
[BEM](https://css-tricks.com/bem-101/) or generate unique IDs for your styling blocks, 
so that they don't collide with other styling rules.

Another difference is that, as seen above, the "inline style" can take a *full* CSS
stylesheet verbatim, including `@media` rules, `:hover`, `:focus` and `:active` rules,
which browsers famously cannot do:

```rust
let dom = Dom::create_div()
    .with_css("
        @media (max-width: 600px) {
          * {
            background-color: lightblue;
          }
        }
    ");
```

So, here this `*` applies only to this div and will style the background 
color as `lightblue` if the width of the window. You can also use 
`@container` to make the rule based on the size of the div instead.

A third difference is that you can "stack" entire stylesheets on top of each 
other within a single subtree: the rule is that the last-applied stylesheet wins:

```rust
// red background gets stacked on top, replaces blue background
let dom = Dom::create_div()
    .with_css("background:blue")
    .with_css("background:red");
```

Notably, this stacking happens *before* cascading happens, which allows designers to 
override the default widget stylesheet configuration before the properties of the 
widget component are applied to the subtree.

## Measuring layout

A big issue when layouting items in a `VirtualView` callback (see [Virtual Views](dom/virtual-views.md)) 
is that you need to know or at least estimage the size of a DOM tree before actually rendering it in a 
layout, and you need to "readjust" the scrolling position once you know the actual size more accurately.
For example, you might want to have a file browser view with 100.000 images (which all have different aspect ratios),
but only materialize 100 images. But to determine the scroll positions, you need to calculate the scroll position,
which is usually only know *after* the layout is finished.

To solve this problem, you can use the ``

## Performance and relayouting

As a general rule of thumb, parsing CSS stylesheets is very fast as it does not use a lot of allocations.
You can

---

### Doc Guide En Styling (`doc/guide/en/styling.md`)

---
slug: styling
title: Styling with CSS
language: en
canonical_slug: styling
audience: external
maturity: mature
guide_order: 70
topic_only: false
short_desc: Stylesheets, selectors, and the cascade
prerequisites: [dom]
tracked_files:
  - css/src/lib.rs
  - css/src/css.rs
  - css/src/props/basic/angle.rs
  - css/src/props/basic/animation.rs
  - css/src/props/basic/color.rs
  - css/src/props/basic/direction.rs
  - css/src/props/basic/error.rs
  - css/src/props/basic/font.rs
  - css/src/props/basic/geometry.rs
  - css/src/props/basic/length.rs
  - css/src/props/basic/mod.rs
  - css/src/props/basic/parse.rs
  - css/src/props/basic/pixel.rs
  - css/src/props/basic/time.rs
  - css/src/props/style/azul_exclusion.rs
  - css/src/props/style/background.rs
  - css/src/props/style/border.rs
  - css/src/props/style/border_radius.rs
  - css/src/props/style/box_shadow.rs
  - css/src/props/style/content.rs
  - css/src/props/style/effects.rs
  - css/src/props/style/filter.rs
  - css/src/props/style/lists.rs
  - css/src/props/style/mod.rs
  - css/src/props/style/scrollbar.rs
  - css/src/props/style/selection.rs
  - css/src/props/style/text.rs
  - css/src/props/style/transform.rs
  - css/src/system.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - Css
  - CssProperty
  - CssPropertyValue
  - DynamicCssProperty
  - StyledDom
  - Dom
  - AttributeMatchOp
  - with_css
  - with_css_property
  - with_class
  - with_id
---

# Styling with CSS

## Overview

A `Css` is a parsed stylesheet. You build one from a string, attach it to a
`Dom` subtree with `.with_component_css(css)`, and the cascade applies it on the next
layout pass. The dialect is a strict subset of standard CSS: tag, class, id,
and attribute selectors; descendant, child, sibling, and pseudo-class
combinators; the `@media`, `@os`, `@theme`, and `@lang` at-rules; and
shorthand properties for the common cases.

```rust,no_run
use azul::prelude::*;

let css = Css::from_string("
    body { font-family: sans-serif; padding: 20px; }
    .panel { background: #f0f0f0; border: 1px solid #ccc; padding: 12px; }
    .panel:hover { background: #e8e8e8; }
".into());

let _ = Dom::create_body()
    .with_child(Dom::create_div().with_class("panel".into()))
    .with_component_css(css);
```

```azul-render screenshot=styling-panel width=400 height=160 subtitle="A class-styled panel with hover-ready rules"
<html>
<head><style>
body { font-family: sans-serif; padding: 20px; background: white; }
.panel { background: #f0f0f0; border: 1px solid #ccc; padding: 12px; border-radius: 4px; }
</style></head>
<body><div class="panel">A panel with class-based styling</div></body>
</html>
```

## Two ways to attach styles

There is **one** CSS model, and it works like the web's [`@scope`][scope]:
every stylesheet is *scoped to the subtree of the node it is attached to*.

- **`Dom::with_css(s)`** — the main entry point. The CSS string is parsed
  into a `Css` struct and **pushed onto that node's `.css` vec**. The
  cascade selector-matches it against the node's subtree during the
  single per-frame pass. A string with selectors (`body { … } .btn { … }`)
  matches elements in the subtree; a bare-declaration string (`color: red`)
  parses to `* { color: red }`, so it applies to the whole subtree — and
  on a **leaf node (subtree length 1) that is exactly an "inline style."**
  There is no separate node-only path: inline *is* a one-node `@scope`.
- **`Dom::with_css_property(p)`** — programmatic single-property override
  for when you already hold a typed `CssProperty` and don't want to
  round-trip through string parsing. Highest priority short of `!important`.

```rust,no_run
use azul::prelude::*;

// 1. "Inline" — a bare-declaration string on a leaf node (subtree len 1)
let _ = Dom::create_div()
    .with_css("color: blue; padding: 4px; :hover { color: red; }");

// 2. A stylesheet scoped to a subtree — selectors match within it
let _ = Dom::create_body()
    .with_css(".btn { background: #1976d2; color: white; }")
    .with_child(
        Dom::create_button("Save", SmallAriaInfo::label("Save"))
            .with_class("btn".into())
    );
```

> **Note:** `with_css(&str)` parses a string; `with_component_css(Css)` takes
> a stylesheet you already parsed. Both are builder forms and both push onto
> the subtree's `.css` vec, which the cascade reads; `add_component_css` is
> the mutating twin for when you are not chaining.

Matching and inheritance happen once after `layout()` returns, in a single
cascade pass. [The DOM page](dom.md#component-level-stylesheets)
walks through the timing.

[scope]: https://developer.mozilla.org/en-US/docs/Web/CSS/@scope

For a static stylesheet shared across many nodes, build the `Css` once at
app startup and pass it through `style()`. Multiple `style()` calls stack.
A later one overrides earlier ones at equal specificity.

## Selectors

The selector language matches W3C Selectors Level 3 minus a few rarely-used
pseudo-classes:

- **Universal**: `*` matches every node.
- **Type**: `div`, `button`, `h1` match nodes whose tag matches.
- **Class**: `.panel` matches nodes with `with_class("panel")`.
- **Id**: `#sidebar` matches nodes with `with_id("sidebar")`.
- **Attribute**: `[lang]`, `[lang="en"]`, `[lang^="en"]` test attribute
  presence and match. The operators are `=`, `~=`, `|=`, `^=`, `$=`, `*=`
  (see `AttributeMatchOp`).
- **Descendant**: `nav a` matches an `<a>` anywhere under `<nav>`.
- **Child**: `nav > a` matches a direct `<a>` child of `<nav>`.
- **Adjacent sibling**: `h2 + p` matches a `<p>` immediately after `<h2>`.
- **General sibling**: `h2 ~ p` matches any `<p>` after `<h2>` at the
  same level.
- **Pseudo-class**: `:hover`, `:focus`, `:nth-child(2n+1)` are
  runtime-evaluated state.

## Pseudo-classes

State pseudo-classes evaluate on every frame:

- `:hover`: pointer is over the element.
- `:active`: pointer is pressed and over the element.
- `:focus`: element has keyboard focus.
- `:first`, `:last`: first or last child of its parent.
- `:nth-child(n)`, `:nth-child(2n+1)`, `:nth-child(odd)`, `:nth-child(even)`: positional.
- `:lang(en)`: system locale matches the BCP 47 prefix.
- `:backdrop`: the containing window is unfocused. Use it for inactive-window styling.
- `:dragging`, `:drag-over`: drag-and-drop states.

These run without re-parsing the stylesheet.

## At-rules

Conditional rule blocks. The condition is evaluated per frame, so changing
the system theme or rotating a window adapts without re-cascading.

```rust,no_run
use azul::prelude::*;

let _ = Dom::create_div().with_css("
    color: black;
    @theme dark { color: white; }
    @os linux { font-family: 'Cantarell'; }
    @os windows { font-family: 'Segoe UI'; }
    @os macos { font-family: '.SF NS'; }
    @media (max-width: 600px) { font-size: 14px; }
");
```

- `@os <name>` / `@os(<name>)` matches the host platform. Names:
  `windows`, `macos`, `linux`, `android`, `ios`, `apple` (macOS+iOS),
  `web`, `any`.
- `@os(<family>:<de>)` narrows to a Linux desktop environment. DEs:
  `gnome`, `kde`, `xfce`, `unity`, `cinnamon`, `mate`. Example:
  `@os(linux:gnome) { ... }`.
- `@os(<family> <op> <version>)` narrows to an OS version. Operators:
  `>=`, `<=`, `=`. Examples: `@os(windows >= win-11)`,
  `@os(macos = sonoma)`.
- `@os(<family>:<de> <op> <version>)` combines DE with a version.
  Example: `@os(linux:gnome > 40)`.
- `@theme <variant>` matches the system theme. Variants: `dark`, `light`,
  plus any custom string.
- `@media (orientation: ...)` accepts `portrait` or `landscape`.
- `@media (min-width: Npx)` and friends match numeric viewport ranges.
- `@media (prefers-reduced-motion)` is the accessibility query for motion.
- `@media (prefers-contrast)` is the accessibility query for contrast.
- `@container` enables container queries by width, height, or name.
- `@lang(<bcp47>)` matches the system language by prefix.

Conditions nest. An `@os linux` block can contain a `:hover` block, and
both conditions have to hold for the rule to apply.

See [System Themes](styling/themes.md) for how the system populates these
values from OS settings.

## The cascade and specificity

When more than one rule sets the same property, the cascade picks one. The
rules, in order:

1. Higher specificity wins.
2. Equal specificity. The later rule wins.
3. `style()` calls stack. A later `style()` is "later" than an earlier one.
4. `with_css` (inline) outranks any stylesheet for that node.

Specificity is the W3C tuple `(ids, classes+pseudo+attrs, types, total)`.
Call `Css::sort_by_specificity()` once after parsing if you need
deterministic order. The parser doesn't sort by default. The framework
runs the sort during cascade.

## Property values: the keyword set

Every typed property is wrapped in `CssPropertyValue<T>`:

```rust,ignore
pub enum CssPropertyValue<T> {
    Auto,
    None,
    Initial,
    Inherit,
    Revert,
    Unset,
    Exact(T),
}
```

Most properties accept the CSS-wide keywords. `inherit` walks to the parent's
resolved value. `initial` resets to the property's spec default. `unset`
behaves as `inherit` for inheritable properties and `initial` otherwise.
`revert` returns to the user-agent default. The parser preserves the
keyword and the cascade picks an explicit value at the latest moment.

## Inheritable properties

Some properties propagate from parent to child by default; others don't.
Inheritability is fixed by the property. The inheritable set follows CSS
conventions:

- Text: `color`, `font-family`, `font-size`, `font-weight`, `line-height`,
  `text-align`, `letter-spacing`, `word-spacing`.
- Cursor: `cursor`.
- Visibility: `visibility`.
- Custom: `hyphenation-language`.

Layout properties (`width`, `padding`, `flex-grow`, ...) and most visual
properties (`background`, `border`, ...) don't inherit. Write `inherit`
explicitly if you want one to.

## Dynamic properties (var(...))

A dynamic declaration is a CSS value swappable from Rust per frame.
Syntax in CSS: `var(--my_id, <default>)`. It compiles to
`DynamicCssProperty`:

```rust,ignore
pub struct DynamicCssProperty {
    pub dynamic_id: AzString,
    pub default_value: CssProperty,
}
```

Use them when you want to change a single value (an accent color, a
spacing unit) without re-parsing the stylesheet. The override path lives
on `Dom::with_css_property`.

## system: keywords

Anywhere a colour or font is expected, `system:<name>` resolves at cascade
time against the running OS and theme:

```rust,no_run
use azul::prelude::*;

let _ = Dom::create_div().with_css("
    background: system:control;
    color: system:control-text;
    border: 1px solid system:separator;
    font-family: system:body;
    @theme dark { background: system:control; }
");
```

The lookup re-evaluates per frame, so a theme switch (light to dark)
updates without re-parsing the stylesheet. The available names
(`system:control`, `system:accent`, `system:body`, `system:monospace`,
...) are catalogued in [System Themes](styling/themes.md). They compose
with `@theme` and `@os` the same way any other property would.

## Parsing CSS

`Css::from_string` returns the parsed stylesheet:

```rust,no_run
use azul::prelude::*;

let css = Css::from_string("
    color: rebeccapurple;
".into());
```

The parser is feature-gated behind `parser` (always enabled in the
default build).

## Where styles meet the DOM

The cascade runs once per layout pass, after your `LayoutCallback` returns.
Inputs, in priority order (low to high):

1. The user-agent stylesheet sets HTML defaults (`h1` font sizes,
   `<button>` padding, `<a>` color, ...).
2. Each `Css` attached to a subtree via `Dom::style(...)`, in `style()`
   push order.
3. Inline `with_css` rules on each node.
4. Programmatic `with_css_property` overrides (highest priority short of
   `!important`).

Internally the framework collects every CSS attachment from the recursive
`Dom` tree, merges the stylesheets in push order, and runs the cascade in
a single sweep. The output is a `StyledDom`: a flat, indexed view of the
cascaded properties. Subsequent frames only re-cascade the nodes whose
inputs actually changed.

The reason this matters even at the styling layer: every CSS string you
parse via `with_css` or `Css::from_string` is "free" in the sense that it
is one parse and one push onto a list. Selector matching and inheritance
happen once after you return.

See [The DOM](dom.md#component-level-stylesheets)
for the per-frame walkthrough, and [Layout](layout.md) for how the
cascaded properties feed the formatting algorithms.

Sub-pages cover the catalogue of properties, the platform integration,
and the icon and text-styling primitives:

- [CSS Properties Cheatsheet](styling/properties.md). Every property and
  the values it accepts.
- [System Themes](styling/themes.md). `system:*` colors and fonts,
  `@theme`, `@os`, and accessibility queries.
- [Text and Fonts](styling/text-and-fonts.md). `font-family`, weight,
  style, alignment, plus the `system:` font keywords.
- [Icon Packs](styling/icon-packs.md). Registering image and font icons
  under named packs.

---

### Doc Guide En Widgets (`doc/guide/en/widgets.md`)

---
slug: widgets
title: Built-in Widgets
language: en
canonical_slug: widgets
audience: external
maturity: wip
guide_order: 110
topic_only: false
short_desc: Built-in widgets and how to write your own
prerequisites: [styling, events, events/text-input]
tracked_files:
  - layout/src/widgets/button.rs
  - layout/src/widgets/check_box.rs
  - layout/src/widgets/color_input.rs
  - layout/src/widgets/drop_down.rs
  - layout/src/widgets/file_input.rs
  - layout/src/widgets/frame.rs
  - layout/src/widgets/label.rs
  - layout/src/widgets/mod.rs
  - layout/src/widgets/progressbar.rs
  - layout/src/widgets/titlebar.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:54:43Z
default-search-keys:
  - Dom
  - Button
  - TextInput
  - NumberInput
  - CheckBox
  - DropDown
  - FileInput
  - ColorInput
  - ProgressBar
  - Frame
  - TabHeader
  - TreeView
  - ListView
  - Ribbon
---

# Built-in Widgets

## Overview

*WIP.* Most leaf widgets (`Button`, `CheckBox`, `ColorInput`, `DropDown`, `FileInput`, `ProgressBar`, `Frame`, `Titlebar`) are usable today. Container widgets with richer state (see [Structural Widgets](widgets/structural.md)) have known gaps that are called out per widget.

A widget is a value with a `dom()` method that returns a `Dom` subtree. Widgets
carry per-platform default styles, store callbacks in their own state, and
convert into a `Dom` exactly once. Because the output is a plain `Dom`, you can
mix widgets and hand-rolled markup freely.

```rust,no_run
use azul::prelude::*;

let dom = Dom::create_div()
    .with_child(Button::create("OK".into()).dom());
```

This page covers the leaf widgets. For text and numeric input, and the larger
composite widgets, see:

- [Input Widgets](widgets/inputs.md): `TextInput`, `NumberInput`.
- [Structural Widgets](widgets/structural.md): `TabHeader`, `TreeView`,
  `ListView`, `Ribbon`.

## Common shape

Every widget follows the same builder pattern.

- Construct with `Widget::create(...)` (or `new`).
- Set callbacks with `with_on_*(data, fn)` or `set_on_*(...)`.
- Override styles with `with_*_style(css)` or `set_*_style(...)`.
- Convert to a DOM with `widget.dom()`.

Callbacks are `extern "C" fn(RefAny, CallbackInfo, ...) -> Update`. The `RefAny`
argument is the value you passed to `with_on_*` and is downcast back to your
application state inside the callback.

## Button

`Button` renders as a button node with a label child. `ButtonType` selects one
of eight variants for color and contrast.

```rust,ignore
pub enum ButtonType {
    Default, Primary, Secondary, Success,
    Danger,  Warning, Info,      Link,
}
```

```rust,no_run
use azul::prelude::*;

extern "C" fn on_click(_data: RefAny, _info: CallbackInfo) -> Update {
    Update::DoNothing
}

let dom = Button::create("Save".into())
    .with_button_type(ButtonType::Primary)
    .with_on_click(RefAny::new(()), on_click)
    .dom();
```

The button is focusable. Set `Button.image` to put an icon next to the label.

## CheckBox

`CheckBox` is a small box that toggles its filled state on click. The toggle
callback receives the post-click `CheckBoxState`.

```rust,no_run
use azul::prelude::*;

extern "C" fn on_toggle(_d: RefAny, _i: CallbackInfo, _s: CheckBoxState) -> Update {
    Update::RefreshDom
}

let dom = CheckBox::create(true)
    .with_on_toggle(RefAny::new(()), on_toggle)
    .dom();
```

## ColorInput

`ColorInput` is a colored swatch that fires a callback when clicked. Azul does
not ship a built-in color-picker dialog. Open one yourself in the callback (see
[File Dialogs](system/file-dialogs.md) for `ColorPickerDialog`).

```rust,no_run
use azul::prelude::*;

extern "C" fn on_change(_d: RefAny, _i: CallbackInfo, _s: ColorInputState) -> Update {
    Update::DoNothing
}

let dom = ColorInput::create(ColorU { r: 255, g: 128, b: 0, a: 255 })
    .with_on_value_change(RefAny::new(()), on_change)
    .dom();
```

## FileInput

`FileInput` renders as a button labelled either the placeholder text or the
basename of the currently selected path. Like `ColorInput`, it does not show a
native file dialog itself. Your callback owns that.

```rust,no_run
use azul::prelude::*;

extern "C" fn on_path(_d: RefAny, _i: CallbackInfo, _s: FileInputState) -> Update {
    Update::RefreshDom
}

let dom = FileInput::create(None.into())
    .with_default_text("Open project...".into())
    .with_on_path_change(RefAny::new(()), on_path)
    .dom();
```

## DropDown

`DropDown` shows the currently selected choice plus a dropdown indicator.
Receiving focus opens a native menu populated from `choices`. Selecting an item
invokes the callback with the chosen index.

```rust,no_run
use azul::prelude::*;

extern "C" fn on_pick(_d: RefAny, _i: CallbackInfo, _idx: usize) -> Update {
    Update::RefreshDom
}

let choices = StringVec::from_vec(vec!["Red".into(), "Green".into(), "Blue".into()]);
let dom = DropDown::create(choices)
    .with_on_choice_change(RefAny::new(()), on_pick)
    .dom();
```

The trigger is focusable. Tabbing to it and pressing Space opens the menu just
like a click.

## ProgressBar

`ProgressBar` shows a horizontal bar filled to a percentage. The completion is
clamped to `[0, 100]`. Supply your own `bar_background` and
`container_background` for custom palettes.

```rust,no_run
use azul::prelude::*;

let dom = ProgressBar::create(42.5).dom();
```

`set_height` overrides the bar height. The rendered bar scales with its
container.

`dom()` mounts the bar as a `VirtualView` node carrying a private dataset,
which is what makes the *live update* API work: from any callback,
`ProgressBar::update_progress(callback_info, node_id, percent)` moves the bar
**without a relayout** - the framework re-invokes only this node's
`VirtualView` callback and repaints just the bar. Resolve `node_id` by
minting a UUID marker (`Uuid::short()`) during layout and stamping it on the
widget - markers are excluded from node equality, so the fresh string never
churns the DOM diff:

```rust,no_run
use azul::prelude::*;

// In your layout(): mint, stamp, and keep the string in your state.
let marker = Uuid::short();
let bar = ProgressBar::create(0.0)
    .dom()
    .with_marker(Some(marker.clone()).into());

// In any callback (a timer tick, a network writeback, a pen-pressure event):
extern "C" fn on_tick(mut data: RefAny, mut info: CallbackInfo) -> Update {
    let marker = data.downcast_ref::<MyApp>().unwrap().meter_marker.clone();
    if let Some(node) = info.get_node_id_by_marker(marker).into_option() {
        ProgressBar::update_progress(info, node, 63.0);
    }
    Update::DoNothing // the queued VirtualView re-render repaints the bar
}
```

This is the reference implementation of the *inter-widget fast path* - the
pattern, when to use it, and when to prefer a plain `RefreshDom` are covered
in [the architecture chapter](architecture.md#inter-widget-communication).
The heavy path stays valid: store the percentage in your data model, return
`Update::RefreshDom`, and the rebuilt DOM carries the new value like any
other widget.

## Frame

`Frame` is a titled border container, analogous to an HTML fieldset or a Win32
group box: a thin border with the title text inset into the top edge.

```rust,no_run
use azul::prelude::*;

let dom = Frame::create(
    "Network".into(),
    Dom::create_div(),
).dom();
```

## Titlebar

`Titlebar` renders a custom window chrome bar. Use `Titlebar::dom` for a
title-only bar (the OS still draws the close, minimize, and maximize buttons).
Use `Titlebar::domWithButtons(...)` for a windowless mode where the app draws
its own controls.

Pair the buttoned form with `WindowDecorations::None` (see
[Windowing](system/windowing.md)) to draw the entire title bar yourself.

## Styling

Every widget exposes its inline default styles as a public field you can replace
before calling `dom()`. For one-off styling, prefer adding CSS to the produced
`Dom` via `Dom::with_css_property` or `Dom::with_css`.

```rust,no_run
use azul::prelude::*;

let dom = Button::create("Save".into()).dom();
```

---

### Doc Src Spec Exclude (`doc/src/spec/exclude.txt`)

# Spec paragraphs excluded from claude-exec processing.
# Format: feature:hash # CATEGORY: reason
# Grouped prompts have multiple hashes; each hash is listed separately.
#
# Categories:
#   METADATA       - W3C boilerplate, copyright, JS tooling, changelogs, abstracts
#   FLEX_GRID      - Flex/grid behavior handled by Taffy, not our solver
#   MISMATCHED     - Paragraph matched to wrong feature via keyword overlap
#   NON_NORMATIVE  - Notes, examples, editorial content with no impl requirement
#   SECURITY       - Security/privacy advisories, not layout requirements
#   HISTORICAL     - Legacy/quirks mode, CSS1 backward compat
#
# To override: use --exclude-file <path> with claude-exec

# === FLEX_GRID (34) ===
block-formatting-context:21ec9a # FLEX_GRID: flex container BFC behavior (Taffy)
block-formatting-context:440282 # FLEX_GRID: flex container BFC behavior (Taffy)
block-formatting-context:4bbd14 # FLEX_GRID: flex container BFC behavior (Taffy)
containing-block:bc85ca # FLEX_GRID: flex/grid containing block rules (Taffy)
containing-block:e78fdd # FLEX_GRID: flex/grid containing block rules (Taffy)
containing-block:f1e34a # FLEX_GRID: flex/grid containing block rules (Taffy)
containing-block:1bd3aa # FLEX_GRID: flex/grid containing block rules (Taffy)
display-property:1dcaab # FLEX_GRID: flex item display fixup (Taffy)
display-property:34c358 # FLEX_GRID: flex item display fixup (Taffy)
display-property:3dc2b2 # FLEX_GRID: flex item display fixup (Taffy)
display-property:400367 # FLEX_GRID: grid item display behavior (Taffy)
display-property:53ec28 # FLEX_GRID: flex item anonymous boxes (Taffy)
display-property:6d1a35 # FLEX_GRID: flex item display fixup (Taffy)
display-property:73e12c # FLEX_GRID: flex item display fixup (Taffy)
display-property:8b1c37 # FLEX_GRID: flex/grid item blockification (Taffy)
display-property:9f2f10 # FLEX_GRID: flex/grid item display fixup (Taffy)
display-property:acc3a8 # FLEX_GRID: flex/grid item display fixup (Taffy)
display-property:c3e8e3 # FLEX_GRID: flex/grid item display fixup (Taffy)
display-property:de75ba # FLEX_GRID: grid template area behavior (Taffy)
display-property:f24a3a # FLEX_GRID: flex container BFC behavior (Taffy)
floats:0ee40b # FLEX_GRID: floats in flex/grid containers (Taffy)
floats:e48b0f # FLEX_GRID: floats in flex/grid containers (Taffy)
height-calculation:34f03e # FLEX_GRID: flex/grid height calculation (Taffy)
height-calculation:fae9a1 # FLEX_GRID: flex/grid height calculation (Taffy)
intrinsic-sizing:47a5f2 # FLEX_GRID: flex/grid intrinsic sizing (Taffy)
intrinsic-sizing:c2f5fd # FLEX_GRID: flex/grid intrinsic sizing (Taffy)
margin-collapsing:78db70 # FLEX_GRID: margins in flex containers (Taffy)
min-max-sizing:09aca7 # FLEX_GRID: flex/grid min-max sizing (Taffy)
overflow:a25539 # FLEX_GRID: flex/grid overflow behavior (Taffy)
positioning:fd4b6f # FLEX_GRID: flex/grid static position (Taffy)
positioning:05212c # FLEX_GRID: flex/grid static position (Taffy)
table-layout:f66d5c # FLEX_GRID: grid/flex table interaction (Taffy)
width-calculation:49c825 # FLEX_GRID: flex/grid width calculation (Taffy)
width-calculation:bf2cb4 # FLEX_GRID: flex/grid width calculation (Taffy)

# === HISTORICAL (2) ===
box-model:dc65ce # HISTORICAL: CSS1 backward compat only
display-property:c8a95a # HISTORICAL: Legacy CSS2 quirks mode

# === METADATA (100) ===
block-formatting-context:22b37a # METADATA: Spec title/status/abstract
block-formatting-context:2d3d36 # METADATA: W3C conformance boilerplate
block-formatting-context:43dcca # METADATA: W3C copyright/legal
block-formatting-context:481b77 # METADATA: Editorial note
block-formatting-context:4dfe42 # METADATA: W3C copyright/legal
block-formatting-context:573a23 # METADATA: Spec changelog/revision
block-formatting-context:6a2b3a # METADATA: Spec metadata
block-formatting-context:77dd66 # METADATA: W3C conformance boilerplate
block-formatting-context:84ee16 # METADATA: Spec metadata
block-formatting-context:97bec1 # METADATA: Spec metadata
block-formatting-context:a817c8 # METADATA: Spec changelog/revision
block-formatting-context:a98713 # METADATA: Spec metadata
block-formatting-context:dc6656 # METADATA: W3C copyright/legal
block-formatting-context:e5c726 # METADATA: Spec title/status/abstract
block-formatting-context:b66a16 # METADATA: Spec title/status/abstract
block-formatting-context:afca25 # METADATA: Spec title/status/abstract
block-formatting-context:f7f612 # METADATA: Spec title/status/abstract
block-formatting-context:c8334a # METADATA: Spec title/status/abstract
block-formatting-context:d3744a # METADATA: Spec title/status/abstract
block-formatting-context:f92733 # METADATA: Spec title/status/abstract
box-model:206c07 # METADATA: Property index/JSON metadata
box-model:65e6c4 # METADATA: Spec metadata
box-model:b0e499 # METADATA: Editorial note
box-model:b8fd5b # METADATA: Spec metadata
box-model:4f423b # METADATA: Spec metadata
box-sizing:8a61b3 # METADATA: Spec metadata
containing-block:a7d527 # METADATA: Editorial note
containing-block:c44cd2 # METADATA: Spec changelog/revision
containing-block:75d635 # METADATA: Spec changelog/revision
containing-block:631e47 # METADATA: Spec changelog/revision
display-contents:6cc68c # METADATA: Spec title/status/abstract
display-contents:b6c26d # METADATA: Spec title/status/abstract
display-contents:cedb19 # METADATA: W3C copyright/legal
display-contents:d3be9a # METADATA: W3C conformance boilerplate
display-contents:ecd256 # METADATA: Spec metadata
display-property:297dde # METADATA: W3C copyright/legal
display-property:4cd7b8 # METADATA: Spec changelog/revision
display-property:d4ff4c # METADATA: Spec changelog/revision
display-property:232f73 # METADATA: Spec changelog/revision
display-property:522d08 # METADATA: Spec changelog/revision
display-property:c87e00 # METADATA: Spec changelog/revision
display-property:525355 # METADATA: W3C conformance boilerplate
display-property:590df0 # METADATA: Spec changelog/revision
display-property:29a047 # METADATA: Spec changelog/revision
display-property:98a6b8 # METADATA: Spec changelog/revision
display-property:8540ce # METADATA: Spec changelog/revision
display-property:a57217 # METADATA: Spec changelog/revision
display-property:757b3a # METADATA: Spec changelog/revision
display-property:ea5e83 # METADATA: Spec changelog/revision
display-property:b8272b # METADATA: Editorial note
display-property:c03a6b # METADATA: Spec metadata
display-property:c8598c # METADATA: W3C copyright/legal
display-property:d39fca # METADATA: W3C copyright/legal
display-property:d526c5 # METADATA: Spec title/status/abstract
display-property:da0df4 # METADATA: Editorial note
display-property:30a935 # METADATA: Editorial note
display-property:da3fc7 # METADATA: Editorial note
display-property:dc2132 # METADATA: Spec metadata
display-property:262f86 # METADATA: Spec metadata
display-property:e9a142 # METADATA: Spec title/status/abstract
display-property:f95610 # METADATA: Editorial note
display-property:fb134e # METADATA: Spec changelog/revision
display-property:9be88b # METADATA: Spec changelog/revision
display-property:ff9d60 # METADATA: W3C copyright/legal
display-property:ffec75 # METADATA: Spec changelog/revision
floats:213ea4 # METADATA: Spec changelog/revision
font-metrics:349ca3 # METADATA: Editorial note
font-metrics:790ce4 # METADATA: Editorial note
font-metrics:e530fe # METADATA: Editorial note
font-metrics:85ff7f # METADATA: Spec title/status/abstract
font-metrics:5ee4dc # METADATA: Spec title/status/abstract
font-metrics:bbe814 # METADATA: Editorial note
font-metrics:c55f9b # METADATA: JS tooling/boilerplate
font-metrics:dc0632 # METADATA: JS tooling/boilerplate
height-calculation:e7f6fb # METADATA: Spec changelog/revision
inline-formatting-context:101e51 # METADATA: Spec title/status/abstract
inline-formatting-context:17b92e # METADATA: Spec metadata
inline-formatting-context:b88638 # METADATA: Property index/JSON metadata
inline-formatting-context:c2e884 # METADATA: Spec metadata
inline-formatting-context:97af40 # METADATA: Spec metadata
inline-formatting-context:c8d8d9 # METADATA: Spec metadata
intrinsic-sizing:3ba351 # METADATA: Editorial note
line-breaking:155a96 # METADATA: JS tooling/boilerplate
line-breaking:c232b4 # METADATA: JS tooling/boilerplate
line-breaking:42df94 # METADATA: Spec title/status/abstract
line-breaking:5b0ee9 # METADATA: W3C copyright/legal
line-breaking:8980d0 # METADATA: W3C copyright/legal
line-breaking:565a68 # METADATA: W3C copyright/legal
line-breaking:de6065 # METADATA: Module scope/interaction
line-breaking:52ec28 # METADATA: Module scope/interaction
line-height:513681 # METADATA: Spec metadata
line-height:cc03df # METADATA: Spec metadata
line-height:e2253a # METADATA: Spec metadata
line-height:0078fa # METADATA: Spec metadata
line-height:09b54e # METADATA: Spec metadata
min-max-sizing:2f66a6 # METADATA: Spec title/status/abstract
min-max-sizing:567ee8 # METADATA: Spec title/status/abstract
overflow:1418bb # METADATA: Spec metadata
overflow:17d06b # METADATA: JS tooling/boilerplate
overflow:2376e4 # METADATA: W3C conformance boilerplate
overflow:2ffaa0 # METADATA: Editorial note
overflow:46027a # METADATA: Spec title/status/abstract
overflow:55ac3d # METADATA: Spec title/status/abstract
overflow:7e8036 # METADATA: Property index/JSON metadata
overflow:546eac # METADATA: Property index/JSON metadata
overflow:d1f602 # METADATA: Property index/JSON metadata
overflow:87489c # METADATA: Spec title/status/abstract
overflow:7ee3a3 # METADATA: Spec title/status/abstract
overflow:a76069 # METADATA: JS tooling/boilerplate
overflow:ab9999 # METADATA: Spec metadata
overflow:ba7484 # METADATA: Property index/JSON metadata
overflow:bd4b14 # METADATA: Spec feedback/filing
overflow:c1443f # METADATA: Spec feedback/filing
overflow:b5ac54 # METADATA: Spec feedback/filing
overflow:c8b3bd # METADATA: Spec title/status/abstract
overflow:da5dc5 # METADATA: Spec changelog/revision
overflow:c141d5 # METADATA: Spec changelog/revision
overflow:b731b1 # METADATA: Spec changelog/revision
overflow:ed23be # METADATA: Spec title/status/abstract
overflow:ee85d0 # METADATA: JS tooling/boilerplate
overflow:fd38ce # METADATA: JS tooling/boilerplate
overflow:da4bbc # METADATA: JS tooling/boilerplate
overflow:f41f10 # METADATA: Spec changelog/revision
overflow:fe1c1b # METADATA: Spec changelog/revision
overflow:a4c82a # METADATA: Spec changelog/revision
positioning:1e5c69 # METADATA: Spec changelog/revision
positioning:3ea25e # METADATA: Editorial note
positioning:460084 # METADATA: W3C conformance boilerplate
positioning:516137 # METADATA: Editorial note
positioning:523819 # METADATA: Spec metadata
positioning:b177ef # METADATA: W3C copyright/legal
positioning:b59128 # METADATA: Spec title/status/abstract
positioning:b9f3f2 # METADATA: W3C conformance boilerplate
positioning:e2d849 # METADATA: JS tooling/boilerplate
positioning:fbf11b # METADATA: Spec changelog/revision
positioning:63b3ff # METADATA: Spec changelog/revision
table-layout:277a05 # METADATA: W3C conformance boilerplate
table-layout:71e2a2 # METADATA: Spec title/status/abstract
text-alignment-spacing:27d8b5 # METADATA: Spec metadata
white-space-processing:c83dbd # METADATA: Property index/JSON metadata
white-space-processing:fef250 # METADATA: Property index/JSON metadata
white-space-processing:53e715 # METADATA: Property index/JSON metadata
width-calculation:313de5 # METADATA: Spec title/status/abstract
width-calculation:7b049f # METADATA: W3C copyright/legal
writing-modes:57b561 # METADATA: Module scope/interaction
writing-modes:5e1fcb # METADATA: Module scope/interaction
writing-modes:b1765b # METADATA: Module scope/interaction
writing-modes:c33b74 # METADATA: Module scope/interaction
writing-modes:607c36 # METADATA: Spec metadata
writing-modes:7db4b9 # METADATA: Spec title/status/abstract
writing-modes:cd3200 # METADATA: Module scope/interaction

# === MISMATCHED (56) ===
block-formatting-context:4b6625 # MISMATCHED: keyword matched wrong feature
block-formatting-context:655266 # MISMATCHED: keyword matched wrong feature
block-formatting-context:749dad # MISMATCHED: keyword matched wrong feature
block-formatting-context:7d7dda # MISMATCHED: keyword matched wrong feature
block-formatting-context:9d8db6 # MISMATCHED: keyword matched wrong feature
block-formatting-context:c17409 # MISMATCHED: SVG/MathML specific
block-formatting-context:f772ae # MISMATCHED: keyword matched wrong feature
box-model:7b9bf2 # MISMATCHED: text/font shaping, not layout
box-model:7fc84b # MISMATCHED: keyword matched wrong feature
box-model:b94cbd # MISMATCHED: text/font shaping, not layout
box-model:c6699f # MISMATCHED: text/font shaping, not layout
box-model:fd2b5e # MISMATCHED: text/font shaping, not layout
containing-block:03528c # MISMATCHED: keyword matched wrong feature
containing-block:0afddd # MISMATCHED: keyword matched wrong feature
containing-block:d63ca7 # MISMATCHED: keyword matched wrong feature
containing-block:dbef58 # MISMATCHED: keyword matched wrong feature
display-contents:20a28e # MISMATCHED: text/font shaping, not layout
display-contents:2f80e6 # MISMATCHED: SVG/MathML specific
display-contents:18e3f2 # MISMATCHED: SVG/MathML specific
display-contents:852a59 # MISMATCHED: SVG/MathML specific
display-contents:884e96 # MISMATCHED: keyword matched wrong feature
display-contents:bc936f # MISMATCHED: keyword matched wrong feature
display-contents:c03741 # MISMATCHED: SVG/MathML specific
display-contents:2b08f1 # MISMATCHED: SVG/MathML specific
display-contents:4a524e # MISMATCHED: SVG/MathML specific
display-contents:c8f6e5 # MISMATCHED: SVG/MathML specific
display-contents:19efc9 # MISMATCHED: SVG/MathML specific
display-contents:d1716b # MISMATCHED: keyword matched wrong feature
display-contents:e202d5 # MISMATCHED: SVG/MathML specific
display-contents:6bbdf4 # MISMATCHED: SVG/MathML specific
display-contents:514064 # MISMATCHED: SVG/MathML specific
display-contents:e539bb # MISMATCHED: SVG/MathML specific
display-property:2a8d62 # MISMATCHED: SVG/MathML specific
display-property:447f4e # MISMATCHED: SVG/MathML specific
display-property:a469a6 # MISMATCHED: SVG/MathML specific
display-property:37553c # MISMATCHED: text/font shaping, not layout
display-property:57a8ef # MISMATCHED: keyword matched wrong feature
display-property:626c86 # MISMATCHED: text/font shaping, not layout
display-property:15292f # MISMATCHED: text/font shaping, not layout
display-property:8e018d # MISMATCHED: text/font shaping, not layout
display-property:7a70a5 # MISMATCHED: keyword matched wrong feature
display-property:98fa8e # MISMATCHED: SVG/MathML specific
display-property:1f71ad # MISMATCHED: SVG/MathML specific
display-property:89dd7b # MISMATCHED: SVG/MathML specific
display-property:d67739 # MISMATCHED: keyword matched wrong feature
display-property:d88ebf # MISMATCHED: text/font shaping, not layout
font-metrics:f2cd75 # MISMATCHED: text/font shaping, not layout
font-metrics:e61649 # MISMATCHED: text/font shaping, not layout
font-metrics:95152b # MISMATCHED: text/font shaping, not layout
height-calculation:39155f # MISMATCHED: text/font shaping, not layout
height-calculation:ab6302 # MISMATCHED: text/font shaping, not layout
inline-formatting-context:0f6442 # MISMATCHED: SVG/MathML specific
inline-formatting-context:2b8c9c # MISMATCHED: keyword matched wrong feature
intrinsic-sizing:be7fe7 # MISMATCHED: keyword matched wrong feature
line-breaking:6684fe # MISMATCHED: text/font shaping, not layout
margin-collapsing:8a9215 # MISMATCHED: keyword matched wrong feature
margin-collapsing:a71cf1 # MISMATCHED: keyword matched wrong feature
min-max-sizing:5d4628 # MISMATCHED: keyword matched wrong feature
overflow:139949 # MISMATCHED: text/font shaping, not layout
overflow:342f47 # MISMATCHED: text/font shaping, not layout
overflow:38af76 # MISMATCHED: text/font shaping, not layout
overflow:fe839e # MISMATCHED: text/font shaping, not layout
overflow:3be607 # MISMATCHED: keyword matched wrong feature
overflow:a2907f # MISMATCHED: keyword matched wrong feature
positioning:128150 # MISMATCHED: keyword matched wrong feature
positioning:5c81da # MISMATCHED: keyword matched wrong feature
positioning:68112a # MISMATCHED: keyword matched wrong feature
positioning:6c7e53 # MISMATCHED: keyword matched wrong feature
positioning:6028da # MISMATCHED: keyword matched wrong feature
positioning:66782a # MISMATCHED: keyword matched wrong feature
text-alignment-spacing:19088c # MISMATCHED: text/font shaping, not layout
text-alignment-spacing:3ac3e8 # MISMATCHED: text/font shaping, not layout
white-space-processing:9d95c2 # MISMATCHED: keyword matched wrong feature
width-calculation:3ce756 # MISMATCHED: text/font shaping, not layout
width-calculation:cc5750 # MISMATCHED: keyword matched wrong feature
writing-modes:313575 # MISMATCHED: SVG/MathML specific

# === NON_NORMATIVE (81) ===
block-formatting-context:9f7b94 # NON_NORMATIVE: introductory prose only
block-formatting-context:ea9baf # NON_NORMATIVE: non-normative example
box-model:24e046 # NON_NORMATIVE: non-normative note
box-model:3aabdb # NON_NORMATIVE: security advisory for authors
box-model:4bcbe0 # NON_NORMATIVE: non-normative note
box-model:d44624 # NON_NORMATIVE: non-normative note
box-model:63e37a # NON_NORMATIVE: non-normative note
box-model:6b0fa0 # NON_NORMATIVE: non-normative note
box-model:82ba77 # NON_NORMATIVE: non-normative note
box-model:b8fd5b # NON_NORMATIVE: non-normative note
box-model:c0df26 # NON_NORMATIVE: introductory prose only
box-model:562dc0 # NON_NORMATIVE: introductory prose only
box-model:c44f5b # NON_NORMATIVE: non-normative example
box-model:dd2b04 # NON_NORMATIVE: non-normative note
containing-block:0649dc # NON_NORMATIVE: non-normative note
containing-block:12d549 # NON_NORMATIVE: non-normative note
containing-block:1d38ca # NON_NORMATIVE: non-normative example
containing-block:c583c4 # NON_NORMATIVE: non-normative note
containing-block:d2ba39 # NON_NORMATIVE: non-normative note
display-contents:3c7066 # NON_NORMATIVE: non-normative note
display-contents:3f4884 # NON_NORMATIVE: non-normative note
display-contents:4f9129 # NON_NORMATIVE: non-normative note
display-contents:61a7f1 # NON_NORMATIVE: non-normative note
display-contents:7ddfd5 # NON_NORMATIVE: introductory prose only
display-property:1f8179 # NON_NORMATIVE: non-normative note
display-property:593b40 # NON_NORMATIVE: non-normative note
display-property:98e0fb # NON_NORMATIVE: print-specific UA hint
display-property:a15ae9 # NON_NORMATIVE: browser interop note
display-property:b0f0c9 # NON_NORMATIVE: non-normative note
display-property:c147b0 # NON_NORMATIVE: non-normative note
display-property:cfe31d # NON_NORMATIVE: introductory prose only
display-property:e00f3c # NON_NORMATIVE: non-normative note
display-property:f23538 # NON_NORMATIVE: non-normative note
floats:03a43a # NON_NORMATIVE: non-normative note
floats:ca04c6 # NON_NORMATIVE: non-normative note
font-metrics:015c6b # NON_NORMATIVE: non-normative note
font-metrics:1d6ca9 # NON_NORMATIVE: non-normative note
font-metrics:992ff9 # NON_NORMATIVE: non-normative note
font-metrics:42624f # NON_NORMATIVE: non-normative note
font-metrics:34679d # NON_NORMATIVE: non-normative note
font-metrics:637595 # NON_NORMATIVE: non-normative note
font-metrics:bfd441 # NON_NORMATIVE: non-normative note
height-calculation:789703 # NON_NORMATIVE: non-normative note
height-calculation:ce2835 # NON_NORMATIVE: non-normative note
height-calculation:fe2f59 # NON_NORMATIVE: non-normative note
inline-block:3714ad # NON_NORMATIVE: non-normative note
inline-block:e4ccad # NON_NORMATIVE: non-normative example
inline-block:7aa0aa # NON_NORMATIVE: non-normative example
inline-formatting-context:0f6442 # NON_NORMATIVE: non-normative example
inline-formatting-context:670e50 # NON_NORMATIVE: non-normative example
inline-formatting-context:fe563c # NON_NORMATIVE: non-normative example
inline-formatting-context:32a9c3 # NON_NORMATIVE: print-specific UA hint
inline-formatting-context:7d8ab8 # NON_NORMATIVE: print-specific UA hint
inline-formatting-context:af591a # NON_NORMATIVE: print-specific UA hint
inline-formatting-context:ca71c0 # NON_NORMATIVE: non-normative note
line-breaking:341597 # NON_NORMATIVE: non-normative note
line-breaking:f428b0 # NON_NORMATIVE: non-normative note
margin-collapsing:4d5fa2 # NON_NORMATIVE: non-normative note
margin-collapsing:6eaf22 # NON_NORMATIVE: non-normative note
margin-collapsing:6706c1 # NON_NORMATIVE: non-normative note
overflow:1e301f # NON_NORMATIVE: print-specific UA hint
overflow:1f17dd # NON_NORMATIVE: non-normative note
overflow:1f388d # NON_NORMATIVE: non-normative example
overflow:25bb60 # NON_NORMATIVE: non-normative example
overflow:27d544 # NON_NORMATIVE: introductory prose only
overflow:49031c # NON_NORMATIVE: non-normative note
overflow:512709 # NON_NORMATIVE: non-normative example
overflow:629ba6 # NON_NORMATIVE: non-normative note
overflow:69b3d6 # NON_NORMATIVE: non-normative example
overflow:76f35a # NON_NORMATIVE: non-normative note
overflow:7d9ef0 # NON_NORMATIVE: print-specific UA hint
overflow:94d80c # NON_NORMATIVE: non-normative note
overflow:ba15dd # NON_NORMATIVE: non-normative note
overflow:bf3831 # NON_NORMATIVE: non-normative example
overflow:c1d791 # NON_NORMATIVE: non-normative note
overflow:c22866 # NON_NORMATIVE: non-normative example
overflow:cba678 # NON_NORMATIVE: non-normative example
overflow:e559ba # NON_NORMATIVE: print-specific UA hint
positioning:272bf3 # NON_NORMATIVE: non-normative note
positioning:448a3a # NON_NORMATIVE: non-normative example
positioning:55971e # NON_NORMATIVE: non-normative note
positioning:602a86 # NON_NORMATIVE: non-normative note
positioning:63dc56 # NON_NORMATIVE: non-normative note
positioning:930891 # NON_NORMATIVE: non-normative example
positioning:b780d3 # NON_NORMATIVE: non-normative example
positioning:9df3f9 # NON_NORMATIVE: non-normative example
positioning:9fae9b # NON_NORMATIVE: non-normative note
positioning:d39822 # NON_NORMATIVE: non-normative note
positioning:d58083 # NON_NORMATIVE: non-normative note
positioning:f4892f # NON_NORMATIVE: print-specific UA hint
positioning:f60387 # NON_NORMATIVE: non-normative note
positioning:ffb81c # NON_NORMATIVE: non-normative note
table-layout:6ccaf3 # NON_NORMATIVE: non-normative example
text-alignment-spacing:8924be # NON_NORMATIVE: non-normative example
text-alignment-spacing:cfd493 # NON_NORMATIVE: non-normative note
text-alignment-spacing:206406 # NON_NORMATIVE: non-normative note

# === SECURITY (11) ===
block-formatting-context:86d802 # SECURITY: security/privacy considerations
box-model:ef2d56 # SECURITY: security/privacy considerations
box-model:f08ad2 # SECURITY: security/privacy considerations
height-calculation:22ca67 # SECURITY: security/privacy considerations
overflow:729676 # SECURITY: security/privacy considerations
overflow:e2d58f # SECURITY: security/privacy considerations
overflow:f2f342 # SECURITY: security/privacy considerations
positioning:556aeb # SECURITY: security/privacy advisory
positioning:a7225d # SECURITY: security/privacy advisory
positioning:ba4d0b # SECURITY: security/privacy advisory
positioning:fb8117 # SECURITY: security/privacy advisory

# === AGENT RUN 2 — NOT APPLICABLE (22) ===
# Identified by claude-exec agents as genuinely not applicable
block-formatting-context:26b535 # NON_NORMATIVE: vertical typographic baseline selection rules (text/inline concern)
block-formatting-context:3093e2 # NON_NORMATIVE: introductory prose about vertical writing, no normative requirements
block-formatting-context:3be1e0 # NON_NORMATIVE: author advice to use sideways-lr for certain scripts
block-formatting-context:410d6c # NON_NORMATIVE: notes about Ogham/Old Turkic script limitations
block-formatting-context:4ea192 # NON_NORMATIVE: informational example about text-transform + text-combine-upright
block-formatting-context:4f2433 # NON_NORMATIVE: introductory prose about vertical text concepts
block-formatting-context:9d3d05 # NON_NORMATIVE: editorial terminology clarification
block-formatting-context:f2cd5e # MISMATCHED: obsolete SVG glyph-orientation-vertical property aliasing
containing-block:ebcd87 # MISMATCHED: top layer is a browser UA concept (fullscreen/dialog)
display-property:511e16 # NON_NORMATIVE: author-facing guidance about unicode-bidi usage
display-property:f887e9 # FLEX_GRID: orthogonal flow root sizing for flex containers (Taffy) and table cells
intrinsic-sizing:99acd7 # MISMATCHED: UA may clause about form control minimum sizes
overflow:55ce1f # NON_NORMATIVE: informative UX details for text-overflow + scroll interaction
overflow:ea10a3 # MISMATCHED: viewport propagation of scrollbar-gutter (browser UA concept)
width-calculation:75a9f2 # MISMATCHED: CSS multi-column layout not implemented
width-calculation:bc5962 # FLEX_GRID: flex-basis handled by Taffy
writing-modes:3deddb # NON_NORMATIVE: authoring recommendation to use dir attribute instead of CSS direction
writing-modes:6e3844 # MISMATCHED: SVG1.1 presentational attributes and non-normative baseline intro
writing-modes:7a9e7d # MISMATCHED: Unicode bidi text reordering handled by external libraries
writing-modes:8e7281 # MISMATCHED: bidi control code insertion handled by external libraries
writing-modes:f3dd0e # MISMATCHED: SVG1.1 obsolete writing-mode value mapping
writing-modes:f488c9 # MISMATCHED: page progression direction (CSS Paged Media not implemented)

---

### Webrender Core Doc CLIPPING AND POSITIONING (`webrender/core/doc/CLIPPING_AND_POSITIONING.md`)

# Original Design

To understand the current design for clipping and positioning (transformations
and scrolling) in WebRender it can be useful to have a little background about
the original design for these features. The most important thing to remember is
that originally clipping, scrolling regions, and transformations were
properties of stacking contexts and they were completely _hierarchical_. This
goes a long way toward representing the majority of CSS content on the web, but
fails when dealing with important edges cases and features including:
 1. Support for sticky positioned content
 2. Scrolling areas that include content that is ordered both above and below
    intersecting content from outside the scroll area.
 3. Items in the same scrolling root, clipped by different clips one or more of
    which are defined outside the scrolling root itself.
 4. Completely non-hierarchical clipping situations, such as when items are
    clipped by some clips in the hierarchy, but not others.

Design changes have been a step by step path from the original design to one
that can handle all CSS content.

# Current Design

All positioning and clipping is handled by the `SpatialTree`. The name is a
holdover from when this tree was a tree of `Layers` which handled both
positioning and clipping. Currently the `SpatialTree` holds:
 1. A hierarchical collection of `SpatialNodes`, with the final screen
    transformation of each node depending on the relative transformation of the
    node combined with the transformations of all of its ancestors. These nodes
    are responsible for positioning display list items and clips.
 2. A collection of `ClipNodes` which specify a rectangular clip and, optionally,
    a set of rounded rectangle clips and a masking image.
 3. A collection of `ClipChains`. Each `ClipChain` is a list of `ClipNode`
    elements. Every display list item has an assigned `ClipChain` which
    specifies what `ClipNodes` are applied to that item.

The `SpatialNode` of each clip applied to an item is completely independent of
the `SpatialNode` applied to the item itself.

One holdover from the previous design is that both `ClipNode` and `SpatialNodes`
have a parent node, which is either a `SpatialNode` or a `ClipNode`.  From this
node WebRender can determine both a parent `ClipNode` and a parent `SpatialNode`
by finding the first ancestor of that type. This is handled by the
`DisplayListFlattener`.

## `SpatialNode`
There are three types of `SpatialNodes`:
  1. Reference frames which are created when content needs to apply
     transformation or perspective properties to display list items. Reference
     frames establish a new coordinate system, so internally all coordinates on
     display list items are relative to the reference frame origin. Later
     any non-reference frame positioning nodes that display list items belong
     to can adjust this position relative to the reference frame origin.
  2. Scrolling nodes are used to define scrolling areas. These nodes have scroll
     offsets which are a 2D translation relative to ancestor nodes and, ultimately,
     the reference frame origin.
  3. Sticky frames are responsible for implementing position:sticky behavior.
     This is also an 2D translation.

`SpatialNodes` are defined as items in the display list. After scene building
each node is traversed hierarchically during the `SpatialTree::update()` step.
Once reference frame transforms and relative offsets are calculated, a to screen
space transformation can be calculated for each `SpatialNode`. This transformation
is added the `TransformPalette` and becomes directly available to WebRender shaders.

In addition to screen space transformation calculation, the `SpatialNode` tree
is divided up into _compatible coordinate systems_. These are coordinate systems
which differ only by 2D translations from their parent system. These compatible
coordinate systems may even cross reference frame boundaries. The goal here is
to allow the application clipping rectangles from different compatible
coordinate systems without generating mask images.

## `ClipNode`

Each clip node holds a clip rectangle along with an optional collection of
rounded clip rectangles and a mask image. The fact that `ClipNodes` all have a
clip rectangle is important because it means that all content clipped by a
clip node has a bounding rectangle, which can be converted into a bounding
screen space rectangle.  This rectangle is called the _outer rectangle_ of the
clip. `ClipNodes` may also have an _inner rectangle_, which is an area within
the boundaries of the _outer rectangle_ that is completely unclipped.

These rectangles are calculated during the `SpatialTree::update()` phase. In
addition, each `ClipNode` produces a template `ClipChainNode` used to build
the `ClipChains` which use that node.

## `ClipChains`

There are two ways that `ClipChains` are defined in WebRender. The first is
through using the API for manually specifying `ClipChains` via a parent
`ClipChain` and a list of `ClipNodes`. The second is through the hierarchy of a
`ClipNode` established by its parent node. Every `ClipNode` has a chain of
ancestor `SpatialNodes` and `ClipNodes`. The creation of a `ClipNode`
automatically defines a `ClipChain` for this hierarchy. This behavior is a
compatibility feature with the old completely hierarchical clipping architecture
and is still how Gecko and Servo create most of their `ClipChains`. These
hierarchical `ClipChains` are constructed during the `ClipNode::update()` step.

During `ClipChain` construction, WebRender tries to eliminate clips that will
not affect rendering, by looking at the combined _outer rectangle_ and _inner
rectangle_ of a `ClipChain` and the _outer rectangle_ and _inner rectangle_ of
any `ClipNode` appended to the chain. An example of the goal of this process is
to avoid having to render a mask for a large rounded rectangle when the rest of
the clip chain constrains the content to an area completely inside that
rectangle. Avoiding mask rasterization in this case and others has large
performance impacts on WebRender.

# Clipping and Positioning in the Display List

Each non-structural WebRender display list item has
 * A `SpatialId` of a `SpatialNode` for positioning
 * A `ClipId` of a `ClipNode` or a `ClipChain` for clipping
 * An item-specific rectangular clip rectangle

The positioning node determines how that item is positioned. It's assumed that
the positioning node and the item are children of the same reference frame. The
clipping node determines how that item is clipped. This should be fully
independent of how the node is positioned and items can be clipped by any
`ClipChain` regardless of the reference frame of their member clips. Finally,
the item-specific clipping rectangle is applied directly to the item and should
never result in the creation of a clip mask itself.

## Converting user-exposed `ClipId`/`SpatialId` to internal indices

WebRender must access `ClipNodes` and `SpatialNodes` quite a bit when building
scenes and frames, so it tries to convert `ClipId`/`SpatialId`, which are already
per-pipeline indices, to global scene-wide indices.  Internally this is a
conversion from `ClipId` into `ClipNodeIndex` or `ClipChainIndex`, and from
`SpatialId` into `SpatialNodeIndex`. In order to make this conversion cheaper, the
`DisplayListFlattner` assigns offsets for each pipeline and node type in the
scene-wide `SpatialTree`.

Nodes are added to their respective arrays sequentially as the display list is
processed during scene building. When encountering an iframe, the
`DisplayListFlattener` must start processing the nodes for that iframe's
pipeline, meaning that nodes are now being added out of order to the node arrays
of the `SpatialTree`. In this case, the `SpatialTree` fills in the gaps in
the node arrays with placeholder nodes.

# Hit Testing

Hit testing is the responsibility of the `HitTester` data structure. This
structure copies information necessary for hit testing from the
`SpatialTree`. This is done so that hit testing can still take place while a
new `SpatialTree` is under construction.

# Ideas for the Future
1. Expose the difference between `ClipId` and `ClipChainId` in the API.
2. Prevent having to duplicate the `SpatialTree` for hit testing.
3. Avoid having to create placeholder nodes in the `SpatialTree` while
   processing iframes.

---

### Webrender Core Doc Blob (`webrender/core/doc/blob.md`)

# Blob images

Blob image is fallback mechanism for webrender that Gecko uses to render primitives that aren't currently supported by webrender. The main idea is to provide webrender with a custom handler that can take arbitray drawing commands serialized as buffers of bytes (the blobs) and turn them into images that webrender internally will treat as regular images.

At the API level, blob images are treated as other images. They are resources created and associated with image keys, and are used in the display list with regular image display items. 


## Active area

In order to support scrolling very large content, blob images don't necessarily have a finite size. They can grow in any direction. At any time they do have an "active area", also called "visible area" which defines the portion that has to be rasterized. Typically this active area moves along large blob images depending on the scroll position.
The coordinate system of active area the *should* be the one of the blob's drawing commands (this is really up to the blob handler implementation to enforce that, Gecko does), and its scale should correspond to device pixels. The active area's coordinates can be negative.

As far as positioning goes, the active area maps to the image display item's bounds. In other words the content at the top-left corner of the active area will be rendered on screen at the position of the top-left corner of the display item's local rect.

In Gecko, the active area corresponds to the intersection of the fallback content's rect and the displayport.

The terms "visible area" and "visible rect" are used a lot in the blobs code, unfortunately they collide with frame building's visibility/culling terminology. They don't correspond to what is visible to the user, but rather what is in the displayport.


## Tiling

Blob images can be either tiled or non-tiled. Non-tiled blob images support invalid rects while tiled blob images track only validty at the tile level. In gecko all blobs are tiled with a tile size of 256x256.

Just like regular tiled images, blob image tiles along the border of the image are shrinked to fit the remaining size. The only difference is that the tiling pattern always starts at the top-left corner for regular images (smaller boundary tiles only along the right and bottom edges), while it can be aribtrarily positioned for blob images (smaller boundary tiles potentially on all sides).

The tiling logic is in webrender/src/image.rs.


## Async rasterization

Blobs are typically too slow to rasterize on the critical path. We try to avoid blocking frame building on blob image rasterization. In order to do that we rasterize blobs as part of scene building. Rather than rasterize tiles on demand from visibility informating, we rasterize the entire active area during scene building. This means we potentially process a lot more content than will be displayed if the user doesn't scroll through all of the visible area.

When the render backend receives a transaction, it looks for all new and update blob images, and generate blob rasterization requests for all tiles of the blob images that overlap their active area. The requests are bundled with an `AsyncBlobImageRasterizer` object in the transaction that is sent to the scene builder thread. The async rasterizer is created by the `BlobImageHandler` at each transaction. It is a snapshot of the state of the blobs as well as external information such as fonts, and does the actual rasterization.

While tiles are rasterized eagerly during scene building, their content is uploaded lazily to the texture cache depending on the result of the visibility pass during frame building.


## Late rasterization

In some case we run into a missing blob image during frame building and have to rasterize it synchronously. This happens when a rasterized tile is uploaded to the texture cache (at which point the CPU side is discarded), the texture cache entry expires and after scrolling back into view the tile is needed again.
We should really keep the rasterized blobs around just like we keep regular images in the cache. Hopefully this section will become obsolete eventually and we'll be able to remove late blob rasterization.

The information needed for async rasterization corresponds to the state of blobs before scene building while late rasterization needs the state of blobs after the last complete scene build. This means we have to be careful about which version we manipulate in the resource cache.

---

### Webrender Core Doc Swizzling (`webrender/core/doc/swizzling.md`)

> It'd be great to have some (in-tree) docs describing the process you've worked through here, the overall motivation, how it works on different GPUs / platforms etc. Perhaps as a follow up?

# Swizzling in WR

## Problem statement

The goal is to avoid the CPU conversion of data done by the driver on texture data uploads. It's slow and always done synchronously, hurting our "Renderer" thread CPU utilization.

Gecko produces all of the image data in BGRA. Switching "imagelib" to RGBA is possible, but modifying Skia to follow is less trivial.
OpenGL support for BGRA8 as an internal texture format is a complex story: it's officially supported in Angle and a fair share of Android devices, but it's not available on the desktop GL (and until recently wasn't available in the Android emulator). Unofficially, when textures are initialized with `glTexImage` (as opposed to `glTexStorage`) with RGBA internal format, the desktop GL drivers often prefer to store the data in BGRA8 format, actually.

The only way to avoid the CPU conversion is to provide the data in exactly the same format that the driver is using internally for a texture. In this case, the driver does a straght `memcpy` into its CPU-visible memory, which is the best we can hope for with OpenGL API.

## Solution: swizzling

https://phabricator.services.mozilla.com/D21965 is providing the solution to this problem. The main principles are:

  1. Use `glTexStorage` whenever it's available. Doing so gives us full control of the internal format, also allows to avoid allocating memory for mipmaps that we don't use.
  2. Make the shared texture cache format to be determined at the init time, based on the GL device capabilities. For Angle and OpenGL ES this is BGRA8, for desktop this is RGBA8 (since desktop GL doesn't support BGRA internal formats). WebRender is now able to tell Gecko, which color format it prefers the texture data to use.
  3. If the data comes in a format that is different from our best case, we pretend that the data is actually in our best case format, and associate the allocated cache entry with the `Swizzle`. That swizzle configuration changes the way shaders sample from a texture, adjusting for the fact the data was provided in a different format.
  4. The lifetime of a "swizzled" texture cache data is starting at the point the data is uploaded and ending at a point where any shader samples from this data. Any other operation on that data (copying or blitting) is not configurable by `Swizzle` and thus would produce incorrect results. To address this, the change enhances `cs_copy` shader to be used in place of blitting from the texture cache, where needed.
  5. Swizzling becomes a part of the batch key per texture. Mixing up draw calls with texture data that is differently swizzled then introduces the batch breaks. This is a downside for the swizzling approach in general, but it's not clear to what extent this would affect Gecko.

## Code paths

Windows/Angle and Android:
  - we use `glTexStorage` with BGRA8 internal format, no swizzling is needed in general case.

Windows (non-Angle), Mac, Linux:
  - if `glTexStorage` is available, we use it with RGBA8 internal format, swizzling everything on texture sampling.
  - otherwise, we use RGBA unsized format with `gTexImage` and expect the data to come in BGRA format, no swizzling is involved.

---

### Webrender Core Doc Text Rendering (`webrender/core/doc/text-rendering.md`)

# Text Rendering

This document describes the details of how WebRender renders text, particularly the blending stage of text rendering.
We will go into grayscale text blending, subpixel text blending, and "subpixel text with background color" blending.

### Prerequisites

The description below assumes you're familiar with regular rgba compositing, operator over,
and the concept of premultiplied alpha.

### Not covered in this document

We are going to treat the origin of the text mask as a black box.
We're also going to assume we can blend text in the device color space and will not go into the gamma correction and linear pre-blending that happens in some of the backends that produce the text masks.

## Grayscale Text Blending

Grayscale text blending is the simplest form of text blending. Our blending function has three inputs:

 - The text color, as a premultiplied rgba color.
 - The text mask, as a single-channel alpha texture.
 - The existing contents of the framebuffer that we're rendering to, the "destination". This is also a premultiplied rgba buffer.

Note: The word "grayscale" here does *not* mean that we can only draw gray text.
It means that the mask only has a single alpha value per pixel, so we can visualize
the mask in our minds as a grayscale image.

### Deriving the math

We want to mask our text color using the single-channel mask, and composite that to the destination.
This compositing step uses operator "over", just like regular compositing of rgba images.

I'll be using GLSL syntax to describe the blend equations, but please consider most of the code below pseudocode.

We can express the blending described above as the following blend equation:

```glsl
vec4 textblend(vec4 text_color, vec4 mask, vec4 dest) {
  return over(in(text_color, mask), dest);
}
```

with `over` being the blend function for (premultiplied) operator "over":

```glsl
vec4 over(vec4 src, vec4 dest) {
  return src + (1.0 - src.a) * dest;
}
```

and `in` being the blend function for (premultiplied) operator "in", i.e. the masking operator:

```glsl
vec4 in(vec4 src, vec4 mask) {
  return src * mask.a;
}
```

So the complete blending function is:

```glsl
result.r = text_color.r * mask.a + (1.0 - text_color.a * mask.a) * dest.r;
result.g = text_color.g * mask.a + (1.0 - text_color.a * mask.a) * dest.g;
result.b = text_color.b * mask.a + (1.0 - text_color.a * mask.a) * dest.b;
result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.a;
```

### Rendering this with OpenGL

In general, a fragment shader does not have access to the destination.
So the full blend equation needs to be expressed in a way that the shader only computes values that are independent of the destination,
and the parts of the equation that use the destination values need to be applied by the OpenGL blend pipeline itself.
The OpenGL blend pipeline can be tweaked using the functions `glBlendEquation` and `glBlendFunc`.

In our example, the fragment shader can output just `text_color * mask.a`:

```glsl
  oFragColor = text_color * mask.a;
```

and the OpenGL blend pipeline can be configured like so:

```rust
    pub fn set_blend_mode_premultiplied_alpha(&self) {
        self.gl.blend_func(gl::ONE, gl::ONE_MINUS_SRC_ALPHA);
        self.gl.blend_equation(gl::FUNC_ADD);
    }
```

This results in an overall blend equation of

```
result.r = 1 * oFragColor.r + (1 - oFragColor.a) * dest.r;
           ^                ^  ^^^^^^^^^^^^^^^^^
           |                |         |
           +--gl::ONE       |         +-- gl::ONE_MINUS_SRC_ALPHA
                            |
                            +-- gl::FUNC_ADD

         = 1 * (text_color.r * mask.a) + (1 - (text_color.a * mask.a)) * dest.r
         = text_color.r * mask.a + (1 - text_color.a * mask.a) * dest.r
```

which is exactly what we wanted.

### Differences to the actual WebRender code

There are two minor differences between the shader code above and the actual code in the text run shader in WebRender:

```glsl
oFragColor = text_color * mask.a;    // (shown above)
// vs.
oFragColor = vColor * mask * alpha;  // (actual webrender code)
```

`vColor` is set to the text color. The differences are:

 - WebRender multiplies with all components of `mask` instead of just with `mask.a`.
   However, our font rasterization code fills the rgb values of `mask` with the value of `mask.a`,
   so this is completely equivalent.
 - WebRender applies another alpha to the text. This is coming from the clip.
   You can think of this alpha to be a pre-adjustment of the text color for that pixel, or as an
   additional mask that gets applied to the mask.

## Subpixel Text Blending

Now that we have the blend equation for single-channel text blending, we can look at subpixel text blending.

The main difference between subpixel text blending and grayscale text blending is the fact that,
for subpixel text, the text mask contains a separate alpha value for each color component.

### Component alpha

Regular painting uses four values per pixel: three color values, and one alpha value. The alpha value applies to all components of the pixel equally.

Imagine for a second a world in which you have *three alpha values per pixel*, one for each color component.

 - Old world: Each pixel has four values: `color.r`, `color.g`, `color.b`, and `color.a`.
 - New world: Each pixel has *six* values: `color.r`, `color.a_r`, `color.g`, `color.a_g`, `color.b`, and `color.a_b`.

In such a world we can define a component-alpha-aware operator "over":

```glsl
vec6 over_comp(vec6 src, vec6 dest) {
  vec6 result;
  result.r = src.r + (1.0 - src.a_r) * dest.r;
  result.g = src.g + (1.0 - src.a_g) * dest.g;
  result.b = src.b + (1.0 - src.a_b) * dest.b;
  result.a_r = src.a_r + (1.0 - src.a_r) * dest.a_r;
  result.a_g = src.a_g + (1.0 - src.a_g) * dest.a_g;
  result.a_b = src.a_b + (1.0 - src.a_b) * dest.a_b;
  return result;
}
```

and a component-alpha-aware operator "in":

```glsl
vec6 in_comp(vec6 src, vec6 mask) {
  vec6 result;
  result.r = src.r * mask.a_r;
  result.g = src.g * mask.a_g;
  result.b = src.b * mask.a_b;
  result.a_r = src.a_r * mask.a_r;
  result.a_g = src.a_g * mask.a_g;
  result.a_b = src.a_b * mask.a_b;
  return result;
}
```

and even a component-alpha-aware version of `textblend`:

```glsl
vec6 textblend_comp(vec6 text_color, vec6 mask, vec6 dest) {
  return over_comp(in_comp(text_color, mask), dest);
}
```

This results in the following set of equations:

```glsl
result.r = text_color.r * mask.a_r + (1.0 - text_color.a_r * mask.a_r) * dest.r;
result.g = text_color.g * mask.a_g + (1.0 - text_color.a_g * mask.a_g) * dest.g;
result.b = text_color.b * mask.a_b + (1.0 - text_color.a_b * mask.a_b) * dest.b;
result.a_r = text_color.a_r * mask.a_r + (1.0 - text_color.a_r * mask.a_r) * dest.a_r;
result.a_g = text_color.a_g * mask.a_g + (1.0 - text_color.a_g * mask.a_g) * dest.a_g;
result.a_b = text_color.a_b * mask.a_b + (1.0 - text_color.a_b * mask.a_b) * dest.a_b;
```

### Back to the real world

If we want to transfer the component alpha blend equation into the real world, we need to make a few small changes:

 - Our text color only needs one alpha value.
   So we'll replace all instances of `text_color.a_r/g/b` with `text_color.a`.
 - We're currently not making use of the mask's `r`, `g` and `b` values, only of the `a_r`, `a_g` and `a_b` values.
   So in the real world, we can use the rgb channels of `mask` to store those component alphas and
   replace `mask.a_r/g/b` with `mask.r/g/b`.

These two changes give us:

```glsl
result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r;
result.g = text_color.g * mask.g + (1.0 - text_color.a * mask.g) * dest.g;
result.b = text_color.b * mask.b + (1.0 - text_color.a * mask.b) * dest.b;
result.a_r = text_color.a * mask.r + (1.0 - text_color.a * mask.r) * dest.a_r;
result.a_g = text_color.a * mask.g + (1.0 - text_color.a * mask.g) * dest.a_g;
result.a_b = text_color.a * mask.b + (1.0 - text_color.a * mask.b) * dest.a_b;
```

There's a third change we need to make:

 - We're rendering to a destination surface that only has one alpha channel instead of three.
   So `dest.a_r/g/b` and `result.a_r/g/b` will need to become `dest.a` and `result.a`.

This creates a problem: We're currently assigning different values to `result.a_r`, `result.a_g` and `result.a_b`.
Which of them should we use to compute `result.a`?

This question does not have an answer. One alpha value per pixel is simply not sufficient
to express the same information as three alpha values.

However, see what happens if the destination is already opaque:

We have `dest.a_r == 1`, `dest.a_g == 1`, and `dest.a_b == 1`.

```
result.a_r = text_color.a * mask.r + (1 - text_color.a * mask.r) * dest.a_r
           = text_color.a * mask.r + (1 - text_color.a * mask.r) * 1
           = text_color.a * mask.r + 1 - text_color.a * mask.r
           = 1
same for result.a_g and result.a_b
```

In other words, for opaque destinations, it doesn't matter what which channel of the mask we use when computing `result.a`, the result will always be completely opaque anyways. In WebRender we just pick `mask.g` (or rather,
have font rasterization set `mask.a` to the value of `mask.g`) because it's as good as any.

The takeaway here is: **Subpixel text blending is only supported for opaque destinations.** Attempting to render subpixel
text into partially transparent destinations will result in bad alpha values. Or rather, it will result in alpha values which
are not anticipated by the r, g, and b values in the same pixel, so that subsequent blend operations, which will mix r and a values
from the same pixel, will produce incorrect colors.

Here's the final subpixel blend function:

```glsl
vec4 subpixeltextblend(vec4 text_color, vec4 mask, vec4 dest) {
  vec4 result;
  result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r;
  result.g = text_color.g * mask.g + (1.0 - text_color.a * mask.g) * dest.g;
  result.b = text_color.b * mask.b + (1.0 - text_color.a * mask.b) * dest.b;
  result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.a;
  return result;
}
```

or for short:

```glsl
vec4 subpixeltextblend(vec4 text_color, vec4 mask, vec4 dest) {
  return text_color * mask + (1.0 - text_color.a * mask) * dest;
}
```

To recap, here's what we gained and lost by making the transition from the full-component-alpha world to the
regular rgba world: All colors and textures now only need four values to be represented, we still use a
component alpha mask, and the results are equivalent to the full-component-alpha result assuming that the
destination is opaque. We lost the ability to draw to partially transparent destinations.

### Making this work in OpenGL

We have the complete subpixel blend function.
Now we need to cut it into pieces and mix it with the OpenGL blend pipeline in such a way that
the fragment shader does not need to know about the destination.

Compare the equation for the red channel and the alpha channel between the two ways of text blending:

```
  single-channel alpha:
    result.r = text_color.r * mask.a + (1.0 - text_color.a * mask.a) * dest.r
    result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.r

  component alpha:
    result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r
    result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.r
```

Notably, in the single-channel alpha case, all three destination color channels are multiplied with the same thing:
`(1.0 - text_color.a * mask.a)`. This factor also happens to be "one minus `oFragColor.a`".
So we were able to take advantage of OpenGL's `ONE_MINUS_SRC_ALPHA` blend func.

In the component alpha case, we're not so lucky: Each destination color channel
is multiplied with a different factor. We can use `ONE_MINUS_SRC_COLOR` instead,
and output `text_color.a * mask` from our fragment shader.
But then there's still the problem that the first summand of the computation for `result.r` uses
`text_color.r * mask.r` and the second summand uses `text_color.a * mask.r`.

There are multiple ways to deal with this. They are:

 1. Making use of `glBlendColor` and the `GL_CONSTANT_COLOR` blend func.
 2. Using a two-pass method.
 3. Using "dual source blending".

Let's look at them in order.

#### 1. Subpixel text blending in OpenGL using `glBlendColor`

In this approach we return `text_color.a * mask` from the shader.
Then we set the blend color to `text_color / text_color.a` and use `GL_CONSTANT_COLOR` as the source blendfunc.
This results in the following blend equation:

```
result.r = (text_color.r / text_color.a) * oFragColor.r + (1 - oFragColor.r) * dest.r;
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                ^  ^^^^^^^^^^^^^^^^^
                         |                              |      |
                         +--gl::CONSTANT_COLOR          |      +-- gl::ONE_MINUS_SRC_COLOR
                                                        |
                                                        +-- gl::FUNC_ADD

         = (text_color.r / text_color.a) * (text_color.a * mask.r) + (1 - (text_color.a * mask.r)) * dest.r
         = text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r
```

At the very beginning of this document, we defined `text_color` as the *premultiplied* text color.
So instead of actually doing the calculation `text_color.r / text_color.a` when specifying the blend color,
we really just want to use the *unpremultiplied* text color in that place.
That's usually the representation we start with anyway.

#### 2. Two-pass subpixel blending in OpenGL

The `glBlendColor` method has the disadvantage that the text color is part of the OpenGL state.
So if we want to draw text with different colors, we have two use separate batches / draw calls
to draw the differently-colored parts of text.

Alternatively, we can use a two-pass method which avoids the need to use the `GL_CONSTANT_COLOR` blend func:

 - The first pass outputs `text_color.a * mask` from the fragment shader and
   uses `gl::ZERO, gl::ONE_MINUS_SRC_COLOR` as the glBlendFuncs. This achieves:

```
oFragColor = text_color.a * mask;

result_after_pass0.r = 0 * oFragColor.r + (1 - oFragColor.r) * dest.r
                     = (1 - text_color.a * mask.r) * dest.r

result_after_pass0.g = 0 * oFragColor.g + (1 - oFragColor.g) * dest.r
                     = (1 - text_color.a * mask.r) * dest.r

...
```

 - The second pass outputs `text_color * mask` from the fragment shader and uses
   `gl::ONE, gl::ONE` as the glBlendFuncs. This results in the correct overall blend equation.

```
oFragColor = text_color * mask;

result_after_pass1.r
 = 1 * oFragColor.r + 1 * result_after_pass0.r
 = text_color.r * mask.r + result_after_pass0.r
 = text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r
```

#### 3. Dual source subpixel blending in OpenGL

The third approach is similar to the second approach, but makes use of the [`ARB_blend_func_extended`](https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_blend_func_extended.txt) extension
in order to fold the two passes into one:
Instead of outputting the two different colors in two separate passes, we output them from the same pass,
as two separate fragment shader outputs.
Those outputs can then be treated as two different sources in the blend equation.

---

### Doc Guide En Animations Timers (`doc/guide/en/animations/timers.md`)

---
slug: animations/timers
title: Timers
language: en
canonical_slug: animations/timers
audience: external
maturity: wip
guide_order: 101
topic_only: false
short_desc: Timers, threads, and scheduled work
prerequisites: [hello-world, events]
tracked_files:
  - core/src/task.rs
  - layout/src/timer.rs
  - layout/src/callbacks.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T06:00:00Z
default-search-keys:
  - Timer
  - TimerId
  - TimerCallback
  - TimerCallbackInfo
  - TimerCallbackReturn
  - TerminateTimer
  - Instant
  - Duration
  - RefAny
  - CallbackInfo
  - Update
  - add_timer
  - remove_timer
---

# Timers

## Introduction

*WIP.* The Timer API is functional today; field names may shift before 1.0.

A `Timer` is a function that runs on the main UI thread on its own schedule, independently of input events. You install it from inside an event callback and tear it down the same way. The framework wakes the event loop to fire it; the callback receives a normal `CallbackInfo` plus a `TimerCallbackInfo` wrapper.

```rust,no_run
use azul::prelude::*;

extern "C" 
fn tick(_: RefAny, _: TimerCallbackInfo) -> TimerCallbackReturn {
    TimerCallbackReturn::continue_and_refresh_dom()
}

extern "C" 
fn on_click(mut data: RefAny, mut info: CallbackInfo) -> Update {

    let timer = Timer::create(
        data.clone(), tick, info.get_system_time_fn()
    ).with_interval(Duration::System(
        SystemTimeDiff::from_millis(16)
    ));
    
    info.add_timer(TimerId::unique(), timer);

    Update::DoNothing
}
```

## When to reach for a timer

Use a timer when something must happen on a clock, not in response to input:

- React to a click, hover, or key. Not a timer. Use an event filter (see [events](../events.md)).
- Re-paint every frame. Yes. Interval = 16 ms; return `TimerCallbackReturn::continue_and_refresh_dom()`.
- Run something once after a delay. Yes. `with_delay(d)`; terminate from the callback.
- Poll a long-running operation. Not a timer. Use a background thread (covered in [background-tasks](../data/background-tasks.md)).
- Animate a CSS property. Not yet. The animation runtime is a stub; see [animations](..md).

Timers run on the UI thread. Heavy work blocks input. For anything I/O-bound or CPU-heavy, spawn a thread.

## The signature

A timer callback has the same C-ABI shape as an event callback, but the second argument and return type differ.

```rust,ignore
pub type TimerCallbackType = extern "C" fn(
    RefAny,
    TimerCallbackInfo,
) -> TimerCallbackReturn;
```

The `RefAny` is whatever you handed to `Timer::create`. It's most often the same data you handed to your layout callback. `TimerCallbackInfo` is documented in the next section. The return value packs two enums:

```rust,ignore
pub struct TimerCallbackReturn {
    pub should_update: Update,
    pub should_terminate: TerminateTimer,
}
```

`Update` is the same enum event callbacks return: `DoNothing`, `RefreshDom`, `RefreshDomAllWindows`. `TerminateTimer` is `Continue` or `Terminate`. Four convenience constructors match the common combinations:

```rust,ignore
TimerCallbackReturn::continue_unchanged()        // keep ticking, no relayout
TimerCallbackReturn::continue_and_refresh_dom()  // keep ticking, re-run layout
TimerCallbackReturn::terminate_unchanged()       // stop, no relayout
TimerCallbackReturn::terminate_and_refresh_dom() // stop, re-run layout
```

## TimerCallbackInfo

The wrapper type gives the callback two things the event API doesn't: which call this is and when the frame started.

- `call_count: usize`. 0 on first invocation, monotonic from there. Useful for "after N ticks, do X".
- `frame_start: Instant`. Monotonic timestamp captured before the callback runs. Use it for animation interpolation rather than calling `Instant::now()` again.
- `is_about_to_finish: bool`. `true` only on the final invocation when a timeout is configured (see below).
- `node_id: OptionDomNodeId`. The node the timer was attached to, if any.
- `callback_info: CallbackInfo`. The full event-API surface.

`TimerCallbackInfo` re-exports the methods you'd reach for from `CallbackInfo` directly: `add_timer`, `remove_timer`, `add_thread`, `remove_thread`, `modify_window_state`, `scroll_to`, `update_all_image_callbacks`, `trigger_virtual_view_rerender`. Mutations are recorded and applied after the callback returns, exactly as in event callbacks.

## Scheduling a timer

`Timer` uses a builder. The constructor takes the data, the callback, and a system-time function (passed in from the event loop):

```rust,ignore
pub fn create<C: Into<TimerCallback>>(
    refany: RefAny,
    callback: C,
    get_system_time_fn: GetSystemTimeCallback,
) -> Self
```

The three modifier methods cover all common scheduling shapes:

```rust,ignore
impl Timer {
    pub fn with_delay(self, delay: Duration) -> Self;       // wait before first fire
    pub fn with_interval(self, interval: Duration) -> Self; // gap between fires
    pub fn with_timeout(self, timeout: Duration) -> Self;   // stop after this much elapsed
}
```

A timer with no interval set defaults to a short tick. A timer with no timeout runs until the callback returns `TerminateTimer::Terminate` or you call `remove_timer`.

```rust,no_run
use azul::prelude::*;

extern "C" 
fn tick(_: RefAny, _: TimerCallbackInfo) -> TimerCallbackReturn {
    TimerCallbackReturn::continue_and_refresh_dom()
}

extern "C" 
fn on_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
    
    // Run timer after 500ms delay from now, then every 16ms, 
    // for at most 5 seconds.

    let initial_delay = SystemTimeDiff::from_millis(500);
    let interval = SystemTimeDiff::from_millis(16);
    let timeout = SystemTimeDiff::from_secs(5);
    
    let timer = Timer::create(
        data.clone(), 
        tick, 
        info.get_system_time_fn()
    )
    .with_delay(Duration::System(initial_delay))
    .with_interval(Duration::System(interval))
    .with_timeout(Duration::System(timeout));

    info.add_timer(TimerId::unique(), timer);
    
    Update::DoNothing
}
```

## Stopping a timer

Three ways, in order of preference:

1. Return `TerminateTimer::Terminate` from the callback when its work is done. The normal teardown path.
2. Set `with_timeout` when the schedule is bounded. The runtime forces termination on the tick that crosses the deadline; the callback sees `is_about_to_finish == true` on its final call so it can flush state.
3. Call `info.remove_timer(timer_id)` from any other callback when an external event (a window close, a cancel button) needs to kill the timer.

Returning `Terminate` doesn't drop the `RefAny` immediately. The framework releases its clone after the callback returns.

## TimerId — the handle

`TimerId` is a wrapper used to look up and remove timers. The framework reserves the first few IDs for built-in timers (caret blink, scroll momentum, drag autoscroll, tooltip delay). Always create user IDs with `TimerId::unique()`:

```rust,ignore
let id = TimerId::unique();
```

Don't construct a literal `TimerId { id: 0..=4 }`. That collides with the framework's own timers.

## Duration and Instant

`Instant` and `Duration` have two variants each: `System` (wraps `std::time::Instant` / `Duration` on platforms with `std`) and `Tick` (a counter for embedded targets). Mixing variants panics. Every `Instant` you'll see comes from the framework's own time function, so all values you compose match.

The constants you'll use most:

```rust,ignore
use azul_core::task::{Duration, SystemTimeDiff};

Duration::System(SystemTimeDiff::from_millis(16))    // 60 fps tick
Duration::System(SystemTimeDiff::from_millis(500))   // 0.5 s
Duration::System(SystemTimeDiff::from_secs(5))       // 5 s
Duration::System(SystemTimeDiff::from_nanos(16_667_000)) // 60 fps, exact
```

`Instant::linear_interpolate(start, end) -> f32` is the single most useful method on `Instant`. Given the current time and a `(start, end)` pair, it returns a clamped 0.0..=1.0 fraction. It's the building block for the [animation runtime](..md) once it lands.

## Timers that re-render images, not the DOM

When a timer animates pixels (a video frame, a GL texture, a canvas), re-running the layout pass for every tick is wasteful. Two narrower triggers exist:

- `info.update_all_image_callbacks()` re-invokes every image callback in the tree without touching layout. Pair with `TimerCallbackReturn::continue_unchanged()`.
- `info.trigger_virtual_view_rerender(dom_id, node_id)` re-invokes a single virtual-view callback for lazy-rendered scroll regions.

Both are applied after the callback returns. Use them in preference to `RefreshDom` whenever the DOM structure isn't changing.

## Common patterns

### Run once after a delay

```rust,no_run
use azul::prelude::*;

extern "C" 
fn run_once(_: RefAny, _: TimerCallbackInfo) -> TimerCallbackReturn {
    // ... do the deferred work ...
    TimerCallbackReturn::terminate_and_refresh_dom()
}

extern "C" fn on_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
    
    let timer = Timer::create(
        data.clone(), 
        run_once, 
        info.get_system_time_fn()
    )
    .with_delay(Duration::System(SystemTimeDiff::from_millis(300)));
    
    info.add_timer(TimerId::unique(), timer);
    
    Update::DoNothing
}
```

### Tick at 60 fps until a flag flips

```rust,no_run
use azul::prelude::*;

struct State { running: bool }

extern "C" 
fn frame(data: RefAny, _: TimerCallbackInfo) -> TimerCallbackReturn {
    let state = data.downcast_ref::<State>().unwrap();
    if !state.running {
        TimerCallbackReturn::terminate_unchanged()
    } else {
        TimerCallbackReturn::continue_and_refresh_dom()
    }
}

extern "C" 
fn on_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
    
    let timer = Timer::create(
        data.clone(), 
        frame, 
        info.get_system_time_fn()
    )
    .with_interval(Duration::System(SystemTimeDiff::from_millis(16)));
    
    info.add_timer(TimerId::unique(), timer);

    Update::DoNothing
}
```

### Cancel from somewhere else

Stash the `TimerId` in your model when you create the timer; remove it from any callback that owns the model.

```rust,no_run
use azul::prelude::*;

struct State { spinner_timer: Option<TimerId> }

extern "C" 
fn on_cancel(data: RefAny, mut info: CallbackInfo) -> Update {
    let mut state = data.downcast_mut::<State>().unwrap();
    if let Some(id) = state.spinner_timer.take() {
        info.remove_timer(id);
    }
    Update::RefreshDom
}
```

## Limits

- Timers fire on the UI thread. A callback that takes 50 ms blocks input for 50 ms.
- Tick precision is bounded by the platform timer resolution and by competing input. Don't expect sub-millisecond accuracy.
- A timer's `RefAny` is held alive by the framework until the timer terminates. A timer that holds the only reference to your model keeps the model alive for the lifetime of the timer.
- The schedule is enforced per tick, not per millisecond. If a system stall bunches several ticks together, the callback fires once per tick. Compute deltas from `frame_start`, not from `call_count`.

---

### Doc Guide En Animations Zombie Animations (`doc/guide/en/animations/zombie-animations.md`)

---
slug: animations/zombie-animations
title: Zombie Animations
language: en
canonical_slug: animations/zombie-animations
audience: external
maturity: wip
guide_order: 102
topic_only: false
short_desc: Exit animations, the retained subtree they run on, and native per-frame animation functions
prerequisites: [animations, dom/reconciliation]
tracked_files:
  - core/src/resources.rs
  - core/src/animation.rs
  - layout/src/window.rs
  - layout/src/callbacks.rs
  - css/src/props/basic/animation.rs
last_generated_rev: e33ef81cc56d8a7dac357d6c0610ff5e26d02e26
generated_at: 2026-08-20T00:00:00Z
default-search-keys:
  - ZombieAnimCallback
  - ZombieAnimInfo
  - ZombieFrame
  - AnimationFunction
  - StyleAnimation
  - AnimationTiming
  - AnimationIterationCount
  - TimerCallbackInfo
  - RefAny
---

# Zombie Animations

An exit animation animates a node that no longer exists. By the time the
layout callback has returned the DOM without it, reconciliation has already
unmounted it and layout has given its space to its neighbours — there is
nothing left to move. `-azul-animation-out` is what stops that: the node's
declaration makes the window **retain** the previous frame's subtree, and the
exit animates against that retained copy. The retained copy is the zombie.

```css
.sidebar {
    -azul-animation-in:  slideInLeft 220ms spring;
    -azul-animation-out: slideOutLeft 180ms ease-in;
}
@keyframes slideOutLeft {
    from { transform: translateX(0); }
    to   { transform: translateX(-100%); opacity: 0; }
}
```

## Retention is opt-in

A node without `-azul-animation-out` disappears the frame it unmounts — no
zombie, no retained tree, no engine-default slide. Departure animations are
something you or a widget library declare; nothing happens by default, and
the common unmount keeps its incremental layout entry untouched.

Retention also needs a *resolvable* name and a rect: a name that matches
neither a `@keyframes` block nor an attached animation function contributes
nothing, and a node that never laid out has nothing to animate from. Both
cases unmount instantly.

## The zombie owns the frame it animates

When at least one exit resolves, the previous frame's layout result is
**moved** out of the window's live results into the zombie — not cloned, not
shared. Three consequences you can rely on:

- **Hit-testing skips it by construction.** Input walks the live layout
  results, and the zombie is no longer among them. Clicks land on the new
  tree; your callbacks never see the departing node.
- **Scroll offsets freeze.** The retained frame keeps the scroll positions it
  was showing, so a list that scrolls under a departing panel does not yank
  the panel's content with it.
- **It is freed exactly once**, when the track finishes.

An `infinite` exit is clamped to a single run — an exit that never ends would
never reap its zombie. `infinite` on `-azul-animation-in` is not clamped, and
is how a spinner is expressed.

## Clipping

By default an exit is clipped to the rect it was retained at, so a
translating zombie cannot paint over the neighbours that have already taken
its space. The `no-clip` keyword turns that off for a declaration:

```css
-azul-animation-out: flyAway 400ms ease-out no-clip;
```

A native animation function can override it per frame through
`ZombieFrame::clip_to_frozen_rect`.

## Remounting mid-exit

Toggle a panel off and on again before its exit finishes and the exit is
*caught* rather than restarted. The retained node is keyed by its
reconciliation identity, so the remounted node is recognised as the same
node: the zombie is dropped (no double image), and the live node travels home
from wherever the exit had carried it, with velocity preserved.

The direction it travels home along is `-azul-animation-in` if the new
cascade declares one. If it does not, the engine reverses the out-track over
the time the exit had already spent, so the return runs at the pace the
departure did.

The mirror case works too: a node that unmounts mid-*enter* starts its
out-track from wherever the in-track had carried it, instead of snapping to
its laid-out position first.

## Native animation functions

`@keyframes` covers declarative motion. When a frame has to be *computed* —
physics, a value read off the live DOM, motion that depends on how far the
last gesture threw the panel — attach a function to the node and name it from
CSS:

```rust,ignore
use azul::prelude::*;

extern "C" fn shrink_out(
    _data: &mut RefAny,
    _info: &mut TimerCallbackInfo,
    z: &ZombieAnimInfo,
) -> ZombieFrame {
    // `z.t` is RAW LINEAR progress; the engine does not pre-apply easing to
    // native functions. `evaluate` applies the curve the CSS asked for.
    let eased = z.timing.evaluate(z.t);
    ZombieFrame {
        translate_x: -z.rect.size.width * eased,
        translate_y: 0.0,
        opacity: 1.0 - eased,
        width: OptionF32::Some(z.rect.size.width * (1.0 - eased)),
        clip_to_frozen_rect: true,
    }
}

fn sidebar() -> Dom {
    Dom::create_div()
        .with_class("sidebar".into())
        .with_animation_callback(
            "shrinkOut".into(),
            ZombieAnimCallback { cb: shrink_out as usize },
            RefAny::new(()),
        )
}
```

```css
.sidebar { -azul-animation-out: shrinkOut 300ms ease-in-out; }
```

The function lives on the node's own `NodeData`, not in app-global state: a
widget ships its fly-out next to its own DOM, and a library can hand you a
component whose motion travels with it.

**Name resolution order:** stylesheet `@keyframes` first, attached functions
second. The web mechanism stays the only default name source — a `@keyframes
shrinkOut` block anywhere in the stylesheet shadows the attached
`"shrinkOut"`. There are no engine builtins; an unresolvable name animates
nothing.

### What the function receives

`ZombieAnimInfo` is the zombie-specific half:

| Field | What it is |
| --- | --- |
| `styled_dom`, `node_id` | The tree the animated node lives in — retained for an exit, live for an enter — and its index in that tree. Borrowed for the call: walk it, don't store it. |
| `rect`, `viewport`, `dpi_factor` | The retained rect for exits, the solved rect for enters; the viewport it was laid out in. |
| `t` | Raw linear progress, `0..=1`. Not eased. |
| `timing` | The timing the CSS declared (`ease`, `spring`, `cubic-bezier(…)`). Apply it with `timing.evaluate(t)` or substitute your own curve. |
| `velocity_x`, `velocity_y` | Speed entering this frame, logical px/s. Reversing with continuity means emitting frames that start at this speed. |

The other half is a full `TimerCallbackInfo` — the same thing a timer
callback gets. The live DOM, the change queue, node measurement and the
momentum API are all reachable from inside an animation frame.

### What it returns

`ZombieFrame` holds absolute values, not deltas:

- `translate_x` / `translate_y` — logical px about the node's origin.
- `opacity` — `0.0` skips the node entirely for that frame.
- `width` — absolute painted width; left-anchored narrowing for exits. `None`
  keeps the full width. Ignored for enters, where the live layout owns width.
- `clip_to_frozen_rect` — per-frame override of the CSS `no-clip` keyword.
  Ignored for enters.

## Cost

A zombie is a whole retained frame — its own layout result, its own text and
layout caches. Tracks that change `width` re-solve that retained tree per
frame; `LayoutWindow::zombie_relayouts` counts how often that happened, which
is the number to watch if an exit animation costs more than it should. Tracks
that only translate or fade never re-solve anything.

## Cross-references

- [Animations](../animations.md): the `animation` and `-azul-animation-*`
  properties, timing keywords, and `@keyframes`.
- [Reconciliation](../dom/reconciliation.md): the identity that lets a
  remount recognise its own zombie.
- [Timers](timers.md): `TimerCallbackInfo`, which every animation function
  receives in full.

---

### Doc Guide En Architecture Components (`doc/guide/en/architecture/components.md`)

---
slug: architecture/components
title: Components
language: en
canonical_slug: architecture/components
audience: external
maturity: mature
guide_order: 42
topic_only: false
short_desc: Reusable UI fragments - named functions of (args) -> Dom
prerequisites: [dom]
tracked_files:
  - core/src/dom.rs
  - core/src/xml.rs
  - core/src/resources.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:53:30Z
default-search-keys:
  - Dom
  - ComponentLibrary
  - ComponentDef
  - ComponentMap
  - ComponentDataModel
  - ComponentFieldType
  - ComponentDefaultValue
  - CompileTarget
  - AppConfig
  - RefAny
  - StyledDom
  - CallbackInfo
---

# Components

## Introduction

A component is a Rust function that returns `Dom`.

There's no component trait. There's no derive macro. There's no special syntax. You compose components by calling functions. You reuse them through normal module visibility. The only value the framework actually inspects is the `Dom` you return.

```rust,no_run
use azul::prelude::*;

fn card(title: &str, body: &str) -> Dom {
    Dom::create_div()
        .with_class("card".into())
        .with_child(Dom::create_h2_with_text(title))
        .with_child(Dom::create_p_with_text(body))
}

let _ = Dom::create_body()
    .with_child(card("First", "alpha"))
    .with_child(card("Second", "beta"));
```

That's the whole model. The rest of this page covers two layers built on top.

The first layer is state. How do you thread persistent data through a component, and how do nested components forward events back up to their caller? The second layer, covered later under *Component Packs*, is registration. When you want a component visible to the XML loader or the live debugger by name, you wrap it in a `ComponentDef` and put it in a `ComponentLibrary`.

## Pure parameters

For purely visual components, pass values as parameters and return the constructed `Dom`. No `RefAny` is needed because no data has to persist across frames.

```rust,no_run
use azul::prelude::*;

pub fn badge(text: &str, kind: BadgeKind) -> Dom {
    let class = match kind {
        BadgeKind::Info  => "badge badge-info",
        BadgeKind::Warn  => "badge badge-warn",
        BadgeKind::Error => "badge badge-error",
    };
    Dom::create_span_with_text(text).with_class(class.into())
}

pub enum BadgeKind { Info, Warn, Error }
```

The caller picks the variant. The component just produces nodes. Tests are easy: call the function, walk the returned `Dom`, assert on shape.

## Owning state

When a component has internal state (an expansion flag, a counter value, a current selection), wrap it in a struct and pass it as a `RefAny` to the component's callbacks.

The state lives wherever the caller put it. The component's `dom()` method takes a clone of the `RefAny` and threads it through to each callback that needs it.

```rust,ignore
use azul::prelude::*;

pub struct Counter { value: i64 }

impl Counter {
    pub fn dom(state: RefAny) -> Dom {
        let label = match state.downcast_ref::<Counter>() {
            Some(c) => format!("{}", c.value),
            None => return Dom::create_div(),
        };
        Dom::create_div()
            .with_child(Dom::create_span_with_text(label))
            .with_child(
                Dom::create_button_no_a11y("+1".into())
                    .with_callback(EventFilter::Hover(HoverEventFilter::MouseUp), state, increment)
            )
    }
}

extern "C" fn increment(mut data: RefAny, _info: CallbackInfo) -> Update {
    let mut c = match data.downcast_mut::<Counter>() { Some(c) => c, None => return Update::DoNothing };
    c.value += 1;
    Update::RefreshDom
}
```

The component doesn't own its state. The caller owns it. The component renders against it and wires callbacks back to it. That's what makes components composable. Anyone who can hand a `Counter` a `RefAny<Counter>` can build one.

For state that the *node* itself should carry (the typing buffer of a text input, a per-row checkbox, the scroll offset of a list), use `with_dataset(OptionRefAny::Some(refany))` instead. The node-attached dataset is reachable inside the callback through `info.get_dataset(info.get_hit_node())`. That's the pattern used by the built-in widgets. See [Built-in Widgets](../widgets.md).

## The backreference pattern

A component that wraps another component holds a `RefAny` to *its own* parent. When the inner component finishes its work, it follows that backreference to forward a higher-level event upward.

```
/* Detailed source-code truncated for AI context efficiency. */
```

The private callback `validate` only knows about `NumberInput`. It parses the text the user typed. It updates its own state. Then it follows the backreference to the application-level callback.

The application sees a clean signature: `(parent: RefAny, info, value: i64)`. There's no string handling on the application side. There's no awareness of the inner widget's internals.

## A worked example: AgeInput over NumberInput

The application wraps `NumberInput` once more, this time to enforce a domain rule.

```rust,ignore
use azul::prelude::*;

pub struct AgeInput { age: i64 }

extern "C" fn layout(mut data: RefAny, _: LayoutCallbackInfo) -> Dom {
    let age = match data.downcast_ref::<AgeInput>() { Some(a) => a.age, None => return Dom::create_body() };
    let mut input = NumberInput::new(age);
    input.set_on_change(data.clone(), on_age_changed);
    Dom::create_body().with_child(input.dom())
}

extern "C" fn on_age_changed(mut data: RefAny, _info: CallbackInfo, new_age: i64) -> Update {
    let mut a = match data.downcast_mut::<AgeInput>() { Some(a) => a, None => return Update::DoNothing };
    if new_age < 0 || new_age > 150 { return Update::DoNothing; }
    a.age = new_age;
    Update::RefreshDom
}
```

The chain `AgeInput -> NumberInput -> <input>` is a State Graph. Each layer holds one backreference, pointing at the layer above.

Events travel the chain in reverse. The `<input>` loses focus. `NumberInput::validate` runs. `AgeInput::on_age_changed` runs. Nothing leaks across layers, and nothing has to be threaded through layout.

This is the same pattern walked through in [Architecture](..md#building-a-state-graph), now in real Rust.

## Returning multiple roots

A `Dom` has a single root. To return a sequence of siblings, wrap them in a neutral container or collect into a `Dom`.

```rust,no_run
use azul::prelude::*;

pub fn breadcrumb(parts: &[&str]) -> Dom {
    parts.iter()
        .enumerate()
        .map(|(i, &p)| {
            if i == 0 {
                Dom::create_span_with_text(p)
            } else {
                Dom::create_div()
                    .with_class("crumb".into())
                    .with_child(Dom::create_span_with_text(" / "))
                    .with_child(Dom::create_span_with_text(p))
            }
        })
        .collect::<Dom>()
}
```

`collect::<Dom>` produces a `<div>` whose children are the iterator's items. There's no fragment type and no portal type. If you don't want the wrapper to affect layout, give it `display: contents`.

## Component-origin tracking

When a component's `dom()` returns, the framework can stamp the root nodes of its output with a component-origin record. That's the field the inspector populates with the qualified component id (like `"shadcn:card"`) and the JSON-serialised data model.

The origin tag has three uses. The live debugger uses it to display a Component Tree alongside the DOM Tree. The code-generation roundtrip uses it to recover the source invocation. And clicking a node to jump to the component that produced it relies on it.

The origin is set automatically when a component is registered through the component system, whether from XML or from the builder. Plain functions don't need to opt in.

# Component Packs

A *component pack* is a `ComponentLibrary` in the codebase. It's a named collection of `ComponentDef`s.

Packs are how the framework finds a component by name. `<card title="..."/>` in XML resolves through a pack. `shadcn:avatar` in the live debugger resolves through a pack. `builtin:div` resolves through a pack. They're also how a component invocation can roundtrip through the design-time tools and come back out as source code.

There's a second authoring surface in the same pipeline. A component can be declared in `.azul` markup with a typed argument list, registered into a `ComponentLibrary`, and called by name.

```xml
<component name="card" args="title: String, body: String">
    <div class="card">
        <h2>{title}</h2>
        <p>{body}</p>
    </div>
</component>

<app>
    <card title="First" body="alpha"/>
    <card title="Second" body="beta"/>
</app>
```

The runtime path is `Dom::create_from_parsed_xml`, introduced in [The DOM - Parsing from XHTML](../dom.md#parsing-from-xhtml). It walks the parsed XML, resolves each tag against the registered component libraries, and produces the corresponding `Dom`.

Whether a component is hand-written Rust or XML-defined, the value is the same. It's a function from arguments and a `RefAny` to a `Dom`. The rest of this section is about *registering* those functions so the framework can look them up by name.

## Why packs and not just functions

A plain Rust component (`fn card(title: &str, body: &str) -> Dom`) is visible to whoever can see the symbol. That's fine for compiled-in components: your own widgets, a third-party crate's widgets.

It isn't enough when you want more.

- You want to preview a component in the live debugger without recompiling.
- You want a typed data model the debugger can edit, populate, and pass back to the render function.
- You want code generation. The design-time tool emits the equivalent Rust, C, or Python source for a tree the user laid out interactively.
- You want to load components from XML by name. `<card .../>` has to resolve to *some* function.

A `ComponentDef` (in `core/src/xml.rs`) carries everything the runtime and the design-time tools need:

```rust,ignore
pub struct ComponentDef {
    pub id: ComponentId,                 // collection + name, e.g. shadcn:card
    pub display_name: AzString,
    pub description: AzString,
    pub css: AzString,                   // ships with the component
    pub source: ComponentSource,         // where it came from (built-in, JSON, ...)
    pub data_model: ComponentDataModel,  // typed prop list - defaults double as preview values
    pub render_fn: ComponentRenderFn,    // (&def, &data, &registry) -> StyledDom
    pub compile_fn: ComponentCompileFn,  // (&def, &target_lang, &data, indent) -> source
    pub render_fn_source: OptionString,
    pub compile_fn_source: OptionString,
}
```

A `ComponentLibrary` groups defs under a name, version, and description. It also carries `exportable` and `modifiable` flags. The live editor uses those flags to decide whether the user can edit a component in place.

## Registering a pack

`AppConfig::create()` always pre-registers the `"builtin"` library. That library has 112 HTML element components (plus three control-flow builtins: `if`, `for`, `map`), so `builtin:div`, `builtin:p`, `builtin:button`, and the rest are available out of the box. Anything else is registered on top.

There are two registration shapes.

```rust,ignore
use azul::prelude::*;

let mut config = AppConfig::create();

// 1. Register a component library under a vendor name. The register fn is
//    called once and returns every component the library provides.
config.add_component_library(
    AzString::from("mylib"),
    my_register_library_fn,     // RegisterComponentLibraryFnType
);

// 2. Register an entire pre-built library.
config.add_component_library(
    AzString::from("shadcn"),
    register_shadcn,            // extern "C" fn() -> ComponentLibrary
);
```

Both work the same way. The registration function runs immediately at the call site. The returned `ComponentDef` or `ComponentLibrary` is moved into `config.component_libraries`. The library then becomes visible to the XML parser, to the layout callback (through `CallbackInfo`), and to the debug server.

Why the function-pointer indirection instead of a direct `ComponentDef` parameter? It's so the C and Python bindings can register libraries through their own callback shapes. The C example below shows it.

### Built-in libraries use the same registration API

The 112 built-in HTML element components are themselves registered through `add_component_library` inside `AppConfig::create()`. The render functions are the same `Dom::create_<tag>()` constructors documented in [DOM](../dom.md). Your own packs follow the same shape.

### From C

The registration callback is a `repr(C)` function pointer, so a plain function pointer is enough on the C side.

```c
/* `AzRegisterComponentLibraryFnType` is `AzComponentLibrary (*)(void)`. */
extern AzComponentLibrary register_shadcn(void);

int main(void) {
    AzAppConfig config = AzAppConfig_create();
    AzAppConfig_addComponentLibrary(
        &config,
        /* `AzString_fromCStr` takes `const int8_t*`, so a string literal
           needs the cast to compile without -Wpointer-sign. */
        AzString_fromCStr((const int8_t*)"shadcn"),
        register_shadcn
    );
    /* ... */
    return 0;
}
```

`ComponentLibrary` exports no constructor - it is a `repr(C)` struct, so the
registration function fills its fields in directly and returns it. The fields
are the ones listed above. `examples/c/routing-components.c` is a working
version of this, compiled and run by CI.

### From Python

The Python binding wraps the function pointer in a trampoline. You pass a Python
callable; because `RegisterComponentLibraryFnType` takes no arguments, the binding
recovers the callable from the engine's invocation slot rather than from a data
parameter.

**Registering a library from Python is not usable yet.** The call side works:

```python
config = AppConfig.create()
config.add_component_library("shadcn", register_shadcn)   # accepted
```

but the callable has to *return* a `ComponentLibrary`, and the Python class exposes
only `name`, `version`, `description`, `exportable` and `modifiable`. It has no
constructor, and its `components` field is a `ComponentDefVec`, which the binding
does not surface as a property (no Vec-typed field is). So there is no way to build
the value the callback must return. Use the C, or Rust, shape above until that gap
is closed.

## Render: live preview

`ComponentDef::render_fn` has this signature:

```rust,ignore
fn(&ComponentDef, &ComponentDataModel, &ComponentMap) -> ResultStyledDomRenderDomError
```

The render function takes a *modified* `data_model`. The design-time tool overrides default values on each field, then hands the model back. It also takes the full `ComponentMap` so the function can recursively instantiate sub-components. A `<card>` containing an `<h2>` containing a `<span>` is one render call that recurses three levels through the same registry. The return is a fully cascaded `StyledDom` ready for layout.

That's what powers the live preview. The design-time tool reads the component's data model, lets the user edit each prop, calls the render function, and shows the result inline. No recompile. No trip through the disk.

## Instantiation: from XML to DOM

When the XML parser encounters `<card title="First" body="alpha"/>`, it resolves `card` against the registered `ComponentMap`.

1. Strip the namespace if present. `<shadcn:card .../>` becomes `("shadcn", "card")`. A bare `<card .../>` falls back to `"builtin"` and is resolved like any built-in tag.
2. Look up the corresponding `ComponentDef`.
3. Take the def's `data_model` and populate each field's default value from the XML attributes. Coercion is typed.
4. Call the def's `render_fn` with the populated data model and the component map.
5. Stamp every root node of the returned `StyledDom` with a component-origin record. The qualified component id is `"shadcn:card"` and the JSON-serialised data model is the populated one. That's what lets the debugger reconstruct the invocation later.

The `ComponentMap` is what `Dom::create_from_parsed_xml` consults under the hood. The `AppConfig`'s `component_libraries` field carries the registered libraries, which are folded into a `ComponentMap` at app-create time.

## Compile: code generation roundtrip

`ComponentDef::compile_fn` is the inverse of `render_fn`. Given the same data model, it emits the source that *would* call this component as a function in the target language.

```rust,ignore
fn(&ComponentDef, &CompileTarget, &ComponentDataModel, indent: usize)
    -> ResultStringCompileError
```

That's what closes the round-trip for the design-time tool.

```text
              ┌──────────────────┐                           ┌────────────┐
   user edits │ ComponentDataModel│ ─── render_fn  ──────────▶│  StyledDom │
              └──────────────────┘                           └────────────┘
                       ▲                                            │
                       │                                            │ inspector
                       └────────────────── compile_fn ◀─────────────┘ (data → source)
                                            │
                                            ▼
                          fn card(title: &str, body: &str) -> Dom { … }
```

A node clicked in the inspector carries the component-origin record (the qualified component id and its data model JSON). The inspector calls `compile_fn` with that data model plus a `CompileTarget` (`Rust`, `C`, `Cpp`, or `Python`) and gets back source. From there the source can be pasted into the user's project, or handed to the codegen path that lives next to `api.json`. The round-trip is closed.

## What the data model looks like

`ComponentDataModel` is a flat list of named fields. Each field has:

- a name, like `"title"`, `"body"`, `"on_click"`, or `"children"`
- a `ComponentFieldType` (`Bool`, `F32`, `Callback`, `EnumRef`, `OptionType`, and so on)
- a `ComponentDefaultValue`. That's the initial value the inspector shows, and it's the value `render_fn` reads when nothing has overridden it.

For non-trivial types like a struct of struct of enum, `data_models` and `enum_models` on the enclosing `ComponentLibrary` carry the type definitions. References between fields use the type's name. The inspector walks those references when it builds an editor for nested data.

That's what makes user-defined types editable in the inspector. A C callback like `fn(RefAny, CallbackInfo) -> Update` shows up as a `Callback` field. The inspector lets the user pick from a list of registered callbacks instead of asking them to write Rust into a text box.

---

### Doc Guide En Architecture Routing (`doc/guide/en/architecture/routing.md`)

---
slug: architecture/routing
title: Routing
language: en
canonical_slug: architecture/routing
audience: external
maturity: mature
guide_order: 41
topic_only: false
short_desc: URL patterns, route params, and per-route layout callbacks
prerequisites: [dom, events/callbacks]
tracked_files:
  - core/src/resources.rs
  - layout/src/callbacks.rs
default-search-keys:
  - Route
  - RouteVec
  - RouteMatch
  - AppConfig
  - LayoutCallbackInfo
  - CallbackInfo
---

# Routing

Routes map a URL pattern to a layout callback. The same registry
drives a desktop app's view-switching and a web build's HTTP
endpoints, so a "settings page" written once shows up at the
`/settings` URL on the web and as a separate top-level layout on
desktop.

## Registering routes

Register routes on the `AppConfig` before passing it to
`App::create`:

```rust,no_run
use azul::prelude::*;

extern "C" fn layout_home(_: &mut RefAny, _: LayoutCallbackInfo) -> Dom { /* ... */ todo!() }
extern "C" fn layout_user(_: &mut RefAny, _: LayoutCallbackInfo) -> Dom { /* ... */ todo!() }
extern "C" fn layout_settings(_: &mut RefAny, _: LayoutCallbackInfo) -> Dom { /* ... */ todo!() }

fn main() {
    let mut config = AppConfig::create();
    config.add_route("/".into(), layout_home.into());
    config.add_route("/user/:id".into(), layout_user.into());
    config.add_route("/settings".into(), layout_settings.into());

    let app = App::create(initial_data, config);
    app.run(WindowCreateOptions::new(layout_home));
}
```

Adding a route that already exists (same pattern) replaces the
previous registration. The first registered route — or the explicit
`"/"` if present — is the initial layout.

## Pattern syntax

Patterns are slash-separated segments. Each segment is either:

- A literal — matches that exact path component.
- A `:name` placeholder — matches any path component and captures
  it as a parameter named `name`.

| Pattern         | Path                  | Match                     |
|---|---|---|
| `"/"`           | `"/"`                 | yes; no params            |
| `"/about"`      | `"/about"`            | yes; no params            |
| `"/about"`      | `"/settings"`         | no                        |
| `"/user/:id"`   | `"/user/42"`          | yes; `id = "42"`          |
| `"/user/:id"`   | `"/user/42/edit"`     | no (segment count mismatch) |
| `"/post/:slug"` | `"/post/hello-world"` | yes; `slug = "hello-world"` |

Patterns are matched in registration order; the first match wins.

## Reading the active route

The `LayoutCallbackInfo` knows which route the callback is rendering:

```rust,ignore
extern "C" fn layout_user(_: &mut RefAny, info: LayoutCallbackInfo) -> Dom {
    let id = info.get_route_param("id".into());
    Dom::create_h1_with_text(format!("User #{}", id.as_str()).into())
}
```

`info.get_route_pattern()` is the pattern the framework matched -
`"/"` when the app configured no routes at all, so a callback that
branches on it always has one string to branch on.
`info.get_route_param(key)` reads one extracted parameter, empty when
there is none.

Inside an event `CallbackInfo`, the same pair reads the same state,
via
`info.get_route_pattern()` (the active pattern) and
`info.get_route_param(key)` (one param). The `set_route_param(key,
value)` helper modifies a param in place — useful for paginated
views that want to bump `?page=2` without a full route switch:

```rust,ignore
extern "C" fn next_page(data: RefAny, mut info: CallbackInfo) -> Update {
    let cur: u32 = info.get_route_param("page".into()).as_str()
        .parse().unwrap_or(1);
    info.set_route_param("page".into(), (cur + 1).to_string().into());
    Update::DoNothing  // set_route_param already triggers a refresh
}
```

On web, `set_route_param` calls `history.replaceState()` so the URL
in the address bar stays in sync without adding a history entry.

## Switching routes from a callback

`CallbackInfo::switch_route` is the imperative form — used when a
button or menu item should navigate elsewhere:

```rust,ignore
extern "C" fn open_settings(_: RefAny, mut info: CallbackInfo) -> Update {
    info.switch_route("/settings".into(), StringPairVec::new());
    Update::RefreshDom
}

extern "C" fn open_user(data: RefAny, mut info: CallbackInfo) -> Update {
    let id = match data.downcast_ref::<u64>() { Some(i) => *i, None => return Update::DoNothing };
    let params = vec![StringPair { key: "id".into(), value: id.to_string().into() }].into();
    info.switch_route("/user/:id".into(), params);
    Update::RefreshDom
}
```

The framework swaps the active layout callback on the next frame,
fires `RefreshDom`, and reconciles the new tree against the
previous one — focus, scroll, and dataset state migrate across
matched nodes the same way a `RefreshDom` from an in-place mutation
does.

On web, `switch_route` calls `history.pushState()` so the back
button works as users expect.

## A practical multi-route layout

A typical app keeps each top-level view in its own callback and
shares a model:

```rust,no_run
use azul::prelude::*;

struct AppModel {
    users: Vec<User>,
    current_filter: String,
}

extern "C" fn layout_home(data: &mut RefAny, info: LayoutCallbackInfo) -> Dom {
    let model = data.downcast_ref::<AppModel>().unwrap();
    Dom::create_body()
        .with_child(navbar(info.get_route_pattern()))
        .with_child(home_content(&model))
}

extern "C" fn layout_user(data: &mut RefAny, info: LayoutCallbackInfo) -> Dom {
    let model = data.downcast_ref::<AppModel>().unwrap();
    let id = info.get_route_param("id".into());
    let user = model.users.iter().find(|u| u.id == id.as_str());

    let body = match user {
        Some(u) => user_detail(u),
        None => not_found_page(id.as_str()),
    };
    Dom::create_body()
        .with_child(navbar(info.get_route_pattern()))
        .with_child(body)
}

extern "C" fn layout_settings(data: &mut RefAny, info: LayoutCallbackInfo) -> Dom {
    let model = data.downcast_ref::<AppModel>().unwrap();
    Dom::create_body()
        .with_child(navbar(info.get_route_pattern()))
        .with_child(settings_panel(&model))
}

fn main() {
    let mut config = AppConfig::create();
    config.add_route("/".into(), layout_home.into());
    config.add_route("/user/:id".into(), layout_user.into());
    config.add_route("/settings".into(), layout_settings.into());

    let app = App::create(RefAny::new(initial_model()), config);
    app.run(WindowCreateOptions::new(layout_home));
}
```

Pull the navbar into its own component (with `add_component_library`
or a regular function) so the active-link styling — typically a
`.is-active` class on the link whose `href` matches the current
route — only lives in one place.

## Web vs desktop

On a desktop build, the route is purely an in-memory selector for
which layout callback to run. There's no URL bar, no `history`
stack, no `window.location` — the route is application state.
`switch_route` updates that state and triggers a reconcile.

On a web build (compiled to WASM and served through azul's web
host), each registered route also maps to an HTTP endpoint on the
server side: a request to `/user/42` runs `layout_user` with the
extracted params and returns the rendered HTML, so the page is
SEO-readable on first load. `switch_route` then calls
`history.pushState()` for the in-page client-side transition;
`set_route_param` calls `history.replaceState()`. The same callback
code drives both the server-rendered first-load HTML and the
client-side updates.

See [Deploying to the web](../deploying/web.md) for the WASM-build pipeline,
the static asset layout, and how the web host serves routes.

---

### Doc Guide En Data Background Tasks (`doc/guide/en/data/background-tasks.md`)

---
slug: data/background-tasks
title: Background Tasks
language: en
canonical_slug: data/background-tasks
audience: external
maturity: wip
guide_order: 212
topic_only: false
short_desc: Running long jobs off the layout thread
prerequisites: [animations/timers]
tracked_files:
  - core/src/task.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - Thread
  - ThreadId
  - ThreadSendMsg
  - ThreadReceiveMsg
  - ThreadSender
  - ThreadReceiver
  - WriteBackCallback
  - Timer
  - RefAny
  - CallbackInfo
  - Update
  - Instant
  - Duration
---

# Background Tasks

## Introduction

`Thread` is azul's background-work primitive: an OS thread plus two typed channels back to the main thread, owned and ticked by the event loop. Use it for blocking I/O, long computations, and anything you can't finish inside a single frame budget. The framework polls each registered `Thread` every frame, drains messages, and runs your write-back callbacks on the main thread.

There's no embedded async runtime. The framework gives you threads and [timers](../animations/timers.md). If you want futures, you bring the runtime.

## When to use a thread vs. a timer

- Blocking I/O (file, network, DB) or long compute. Use `Thread`.
- Periodic main-thread work (animation, polling app state). Use `Timer`.
- Wait for a future without a runtime. Use `Thread` running a current-thread executor.

A `Timer` runs on the main thread and is bounded by your frame budget; see [timers](../animations/timers.md). A `Thread` runs in its own OS thread and reports back through a channel.

## Spawning a thread

The worker function signature is fixed:

```rust,ignore
pub type ThreadCallbackType =
    extern "C" fn(RefAny, ThreadSender, ThreadReceiver);
```

Build a `Thread` with `Thread::create` and hand it to the event loop via `CallbackInfo::add_thread`:

```rust,no_run
use azul::prelude::*;

extern "C" fn worker(
    initial: RefAny,
    mut sender: ThreadSender,
    mut recv: ThreadReceiver,
) {
    // blocking work goes here
}

extern "C" fn on_click(mut data: RefAny, mut event: CallbackInfo) -> Update {
    let init_data      = RefAny::new(/* per-thread input */ ());
    let writeback_data = data.clone();
    let thread = Thread::create(init_data, writeback_data, worker);
    event.add_thread(ThreadId::unique(), thread);
    Update::DoNothing
}
```

`Thread::create(thread_initialize_data, writeback_data, callback)` takes:

- `thread_initialize_data`. Moved into the worker. Available as the first `RefAny` argument of the worker function. Use this for inputs the thread needs but the main side doesn't.
- `writeback_data`. Kept on the main side; passed back to every `WriteBackCallback` invocation. This is the handle the callback uses to mutate application state in response to the thread's output.
- `callback`. The `extern "C" fn(RefAny, ThreadSender, ThreadReceiver)` that runs on the new thread.

`event.add_thread(ThreadId::unique(), thread)` hands the thread to the event loop. From this point the framework polls it every frame.

## Sending data back: WriteBackCallback

The thread can't touch main-thread state directly. To update application data, send a `ThreadReceiveMsg::WriteBack` message. The payload is a `RefAny` plus a callback that runs on the main thread:

```rust,ignore
pub type WriteBackCallbackType =
    extern "C" fn(RefAny, RefAny, CallbackInfo) -> Update;
```

Full example:

```rust,no_run
use azul::prelude::*;

struct Loaded { rows: Vec<u32> }
struct MyModel { rows: Vec<u32> }

extern "C" fn apply_loaded(
    mut app: RefAny,
    mut payload: RefAny,
    _info: CallbackInfo,
) -> Update {
    let mut model = match app.downcast_mut::<MyModel>() {
        Some(m) => m, None => return Update::DoNothing,
    };
    let mut loaded = match payload.downcast_mut::<Loaded>() {
        Some(p) => p, None => return Update::DoNothing,
    };
    model.rows.append(&mut loaded.rows);
    Update::RefreshDom
}

extern "C" fn worker(
    _initial: RefAny,
    mut sender: ThreadSender,
    mut _recv: ThreadReceiver,
) {
    let rows = blocking_query();
    let msg  = ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg {
        refany:   RefAny::new(Loaded { rows }),
        callback: WriteBackCallback {
            cb: apply_loaded,
            ctx: OptionRefAny::None,
        },
    });
    sender.send(msg);
}
# fn blocking_query() -> Vec<u32> { Vec::new() }
```

The first `RefAny` argument of `apply_loaded` is the `writeback_data` passed to `Thread::create`; the second is the payload the worker sent. Return `Update::RefreshDom` to trigger a re-layout. `Update::DoNothing` keeps the existing DOM.

`sender.send` returns `bool`. `true` if the message was queued, `false` if the channel is closed (the framework has already torn the thread down).

## Returning Update directly

For the no-payload case (a thread that just wants to tell the UI to refresh) send `ThreadReceiveMsg::Update`:

```rust,no_run
use azul::prelude::*;
fn _stub(mut sender: ThreadSender) {
  sender.send(ThreadReceiveMsg::Update(Update::RefreshDom));
}
```

The framework applies the `Update` value verbatim. No callback runs.

## Receiving messages from the main thread

`ThreadReceiver::recv()` is non-blocking and returns `OptionThreadSendMsg`. The main thread sends three kinds of message:

- `ThreadSendMsg::Tick`. One frame elapsed. An opportunity to check progress, send a chunk back.
- `ThreadSendMsg::TerminateThread`. The framework is dropping the thread; finish quickly.
- `ThreadSendMsg::Custom(RefAny)`. App-defined message.

`Tick` arrives automatically on each frame. `TerminateThread` is sent when the thread is removed via `CallbackInfo::remove_thread` or the owning `Thread` handle is dropped. `Custom` is only delivered if your code arranges for it.

## Cooperative termination

A long-running thread should poll for `TerminateThread` between work units:

```rust,no_run
use azul::prelude::*;

fn _stub(mut recv: ThreadReceiver, items: Vec<u32>) {
  for item in items {
    if let OptionThreadSendMsg::Some(ThreadSendMsg::TerminateThread) = recv.recv() {
        return;
    }
    process(item);
  }
}

fn process(_: u32) { }
```

If the worker doesn't check, it runs to completion regardless. The framework's destructor sends `TerminateThread` and then joins. A non-cooperative thread blocks teardown until its callback returns.

## Cancelling from the main thread

```rust,no_run
use azul::prelude::*;
struct MyModel { 
    thread_id: Option<ThreadId> 
}

extern "C" 
fn on_cancel(mut data: RefAny, mut event: CallbackInfo) -> Update {
    let mut m = match data.downcast_mut::<MyModel>() {
        Some(m) => m, None => return Update::DoNothing,
    };
    if let Some(id) = m.thread_id.take() {
        event.remove_thread(id);
    }
    Update::RefreshDom
}
```

`remove_thread` schedules the same `TerminateThread` + drop sequence the destructor runs.

## Waiting without blocking

The API has no blocking sleep. Blocking the UI thread is wrong on every
target, and the browser has no such primitive at all, so anything that has to
happen *later* is expressed as a timer:

```rust,ignore
let timer = Timer::create(data, on_tick, get_system_time_fn)
    .with_delay(Duration::from_millis(500))      // first run after 500 ms
    .with_interval(Duration::from_millis(1000)); // then once a second
info.add_timer(TimerId::unique(), timer);
```

A worker thread created with `Thread::create` runs on its own OS thread and
may pace itself with the platform's own sleep (`std::thread::sleep` in Rust,
`nanosleep` / `Sleep` in C) - that never blocks the UI.

## Instant and Duration

The thread API uses `Instant` and `Duration` from the framework rather than the std types directly so timing logic compiles on `no_std` targets. Both are two-variant enums:

```rust,ignore
pub enum Instant  { System(InstantPtr),    Tick(SystemTick) }
pub enum Duration { System(SystemTimeDiff), Tick(SystemTickDiff) }
```

On `std` targets, `Instant::System` wraps `std::time::Instant`. On embedded or WASM targets that lack a real-time clock the variant is `Instant::Tick(SystemTick)`, a frame counter you advance from your event loop. Mixing variants panics, so pick one per platform and stay consistent.

There's no `Duration::from_millis` shorthand. Build the `SystemTimeDiff` explicitly:

```rust,ignore
Duration::System(SystemTimeDiff::from_millis(250))
Duration::System(SystemTimeDiff::from_secs(2))
Duration::System(SystemTimeDiff::from_nanos(1_000))
```

`Instant` exposes:

- `Instant::now()`. Current time on the active variant.
- `duration_since(&earlier) -> Duration`. Panics if `earlier > self` or if the variants don't match.
- `linear_interpolate(start, end) -> f32`. Clamped 0.0–1.0, useful for animation progress.
- `add_duration(...) -> Self`. Additive offsets.

## Reserved thread IDs

`ThreadId` reserves the first few IDs for framework-internal use. `ThreadId::unique()` skips past them, so user code never collides. Don't construct a literal `ThreadId { id: 0..=4 }` in user code.

## What you can't do today

- Run `async fn` directly. The framework doesn't provide an executor. To use Tokio, futures-rs, or smol, spawn a `Thread`, build a `Runtime` inside it, and use `WriteBackCallback` to surface results.
- Stream raw socket data through the framework. Networking has its own page; see [networking](networking.md). Until the runtime side lands, use a `Thread` plus `std::net::TcpStream` for the same shape as the example above.
- Share `&mut` references between thread and main. Communication is `RefAny` payloads only. Lock-free shared state isn't part of the framework. Wrap a `Mutex` inside `RefAny` if you need it.

---

### Doc Guide En Data Database (`doc/guide/en/data/database.md`)

---
slug: data/database
title: Local-first Database
language: en
canonical_slug: data/database
audience: external
maturity: beta
guide_order: 210
short_desc: A local-first key/value + index store that syncs to a backup endpoint
prerequisites: [hello-world, data/background-tasks]
tracked_files:
  - core/src/db.rs
  - dll/src/desktop/extra/sqlite/mod.rs
last_generated_rev: 730697e830000000000000000000000000000000
generated_at: 2026-09-07T00:00:00Z
default-search-keys:
  - Db
  - DbConfig
  - DbValue
  - DbRows
  - DbKeyRange
  - DbScope
  - sync_now
---

# Local-first Database

## Introduction

`Db` is a local-first key/value + index store. The local store holds a
*working set* of your data (a file under the app's data directory on desktop,
IndexedDB in the browser); every write lands there immediately and is queued
in a row-level oplog; `sync_now` pushes the queued rows to an optional backup
endpoint and pulls the remote changes back. Being offline is a normal state
(`Queued`), never an error.

The surface is identical on every target and it is not SQL: desktop keeps a
pure-Rust SQLite engine as a hidden implementation detail and the browser
uses its own database, so no engine is ever shipped to the web. Reads are
*requests* that resume a callback (the browser's storage is asynchronous),
writes are fire-and-forget, and every result struct has a static
`downcast(result)` accessor. Like `Pdf` and `AudioSink` the handle is a
C-ABI value you keep in your own `State`; it is reference counted, so clones
share the store.

## Opening + reading

```
/* Detailed source-code truncated for AI context efficiency. */
```

The request functions and the result struct each one resumes with:

| request | result struct |
|---|---|
| `Db::open(config, data, on_open)` | `DbOpenResult { result: ResultDbDbError }` |
| `db.get(store, key, data, on_result)` | `DbValueResult { value: OptionDbValue, error }` |
| `db.iterate(store, range, limit, data, on_result)` | `DbRowsResult { rows: DbRows, error }` |
| `db.query_index(store, index, range, limit, data, on_result)` | `DbRowsResult` |
| `db.subscribe(scope, data, on_change)` | `DbChangeResult { store, key, value }`, repeatedly |
| `db.sync_now(scope, data, on_result)` | `DbSyncStatusResult { status: DbSyncStatus }` |

`db.set(store, key, value)` and `db.remove(store, key)` return `bool` and
never block; `db.sync_status()` is a synchronous snapshot.

## Values, keys and ranges

[`DbValue`] is `Null`, `Integer(i64)`, `Real(f64)`, `Text(AzString)` or
`Blob(U8Vec)`, with `is_null` / `as_integer` / `as_real` / `as_text`
accessors. Keys are any non-`Null` value and order the way SQLite orders
values: `Null` < numbers < text < blobs. A [`DbKeyRange`] bounds a scan:
`DbKeyRange::all()`, `between(lower, upper)`, `only(key)`, or the struct
itself with `lower_open` / `upper_open` for exclusive bounds.

Results come back as a [`DbRows`] grid with the two columns `key` and
`value`: `num_rows()`, `num_columns()` and `get(row, col)`.

## Schema and indexes

Stores are created on first write. To get secondary indexes and per-store
conflict policies, declare them up front:

```rust,ignore
use azul::db::{DbConflictPolicy, DbIndexSchema, DbSchema, DbStoreSchema};

let schema = DbSchema {
    stores: vec![DbStoreSchema {
        name: "notes".into(),
        indexes: vec![DbIndexSchema {
            name: "by_tag".into(),
            key_path: "tag".into(),   // a top-level field of a JSON object value
            unique: false,
        }].into(),
        conflict_policy: DbConflictPolicy::LastWriteWins,
    }].into(),
};
let config = DbConfig::create("notes").with_schema(schema);
```

Values are indexed when they are JSON objects: `key_path` names the field
whose value becomes the index key. `db.query_index("notes", "by_tag",
DbKeyRange::only(DbValue::Text("work".into())), 0, data, on_rows)` then
returns the matching rows in index-key order.

## Sync, scope and budget

Sync is explicit: `sync_now` pushes every dirty row, then refreshes the
given (or the configured) scope from the endpoint. The [`DbSyncStatus`] it
resumes with reports `state` (`Disconnected` without an endpoint, `Idle`,
`Pushing`, `Pulling`, `Queued` while offline, `Error`), how many operations
still wait to be pushed, when the last sync completed, how many bytes the
local store uses against its budget, and how much of the scope the working
set covers. `set_on_sync_status` delivers the same struct after every sync.

Local stores are size-constrained (browser quotas), so the portable model is
working-set replication rather than full copies:

- `with_scope(DbScope { collections })` limits what is pulled to a set of
  [`DbCollectionScope`]s (a store, optionally a key range or an index
  predicate). `subscribe(scope, ...)` extends the working set and reports
  every change inside it, local or pulled.
- `with_local_budget_bytes(n)` caps the local store. Under pressure *clean*
  rows are evicted oldest-first; dirty (unpushed) rows are never evicted
  before a successful push.
- Conflicts (a key changed locally and remotely since the last sync) follow
  the store's [`DbConflictPolicy`]: `LastWriteWins` (default),
  `ServerWins`, `ClientWins`, or `Merge` with a merge function installed via
  `set_on_conflict(store, data, on_merge)`. The merge function is a pure
  `fn(RefAny, DbConflict) -> DbValue` (return `Null` to delete) invoked
  while the sync is applied; it must not touch the UI.

The wire protocol is a row-level oplog over HTTPS (`POST <url>/push` with
the queued operations as JSON, `GET <url>/pull?db=..&since=<cursor>` for
the remote changes), so a thin server in front of any store can serve it;
the browser host speaks the same protocol through `fetch()`.

## Feature gating

The `Db` handle is always present, but the desktop engine is opt-in via the
`db-sqlite` Cargo feature (part of `build-dll`) and sync needs the `http`
feature. Without an engine `Db::open` resumes with
`DbErrorKind::NoEngine`; without `http`, `sync_now` reports `Error` naming
the missing feature. `PlatformCapability::sql()` and `::sync()` answer these
questions at runtime.

## See also

- [background-tasks](background-tasks.md) - timers, for driving `sync_now`
  periodically on desktop.
- [architecture](../architecture.md) - keeping the handle in your `State`.

---

### Doc Guide En Data Networking (`doc/guide/en/data/networking.md`)

---
slug: data/networking
title: Networking
language: en
canonical_slug: data/networking
audience: external
maturity: stub
guide_order: 211
topic_only: false
short_desc: HTTP from a callback
prerequisites: [data/background-tasks]
tracked_files:
  - core/src/task.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - HttpRequestConfig
  - HttpClient
  - ThreadPool
  - HttpResponse
  - Thread
  - ThreadSendMsg
  - ThreadReceiveMsg
  - WriteBackCallback
  - RefAny
  - U8Vec
  - http_get
  - download_bytes
  - is_url_reachable
  - IrohEndpoint
  - IrohConfig
  - IrohEvent
  - IrohLoadBalancer
---

# Networking

## Introduction

*Stub.* The framework ships a small blocking HTTP helper (`HttpRequestConfig`, `HttpResponse`) you can call from inside a [`Thread`](background-tasks.md). For raw sockets, async I/O, WebSockets, or anything else, do networking the same way you do any other blocking I/O: from a `Thread`.

## What's available

- `HttpRequestConfig`. A small blocking HTTP client. Configure timeouts, headers, max response size, and TLS verification, then call `http_get`, `download_bytes`, or `is_url_reachable`. The convenience constructors `http_get_default` and `download_bytes_default` skip configuration.
- `HttpResponse`. The result. Carries `status_code`, `body` (`U8Vec`), `headers`, `content_type`, `content_length`. Use `is_success`, `is_redirect`, `is_client_error`, `is_server_error`, `body_as_string` to inspect it.

- `IrohEndpoint`. Peer-to-peer QUIC connections between apps, for media frames and messages. See [Peer-to-peer connections](#peer-to-peer-connections).

The framework is intentionally runtime-agnostic. There's no built-in raw-socket type and no async runtime integration. HTTP is request / resume shaped (below), so ordinary fetches need no worker thread; anything heavier still belongs in one.

## Making a request

Every `HttpRequestConfig` call is a *request*: it returns a `RequestId`
immediately and resumes the callback you pass in with the answer. On desktop
the transfer runs synchronously inside the call and the callback runs right
after the requesting callback returns; in the browser it is a `fetch()` and
the callback runs on a later task. Either way the callback never runs
re-entrantly inside the callback that issued the request, and the same code
works on both.

```rust,ignore
extern "C" fn on_fetch_clicked(data: RefAny, _info: CallbackInfo) -> Update {
    let cfg = HttpRequestConfig::create()
        .with_timeout(10)
        .with_user_agent("my-app/1.0");
    let _request = cfg.http_get("https://example.org/api".into(), data, on_response);
    Update::DoNothing
}

extern "C" fn on_response(mut data: RefAny, _info: CallbackInfo, result: RefAny) -> Update {
    let Some(answer) = HttpGetResult::downcast(result).into_option() else {
        return Update::DoNothing;
    };
    match answer.result {
        ResultHttpResponseHttpError::Ok(resp) => {
            // mutate the model through `data` the usual way
            let _ = (&mut data, resp);
            Update::RefreshDom
        }
        ResultHttpResponseHttpError::Err(e) => {
            eprintln!("request failed: {e:?}");
            Update::DoNothing
        }
    }
}
```

The result structs are `HttpGetResult` (for `http_get`, `http_post` and
`http_request`), `HttpBytesResult` (`download_bytes`) and
`HttpReachableResult` (`is_url_reachable`); each has a static
`downcast(result)` accessor. There is no default-config shortcut: build a
config with `HttpRequestConfig::create()` and call the method on it.

A request may be issued from a worker thread as well; its resume still runs
on the main thread. CORS applies in the browser and cannot be escaped: a
target that does not send `Access-Control-Allow-Origin` fails with
`HttpError::Other` naming CORS.

## Reusing connections

A request opens a connection, uses it once and closes it. Many requests to the
same server each pay for a new connection and TLS handshake. To keep
connections open between requests, create an `HttpClient` once (in your app
state, not per request) and attach a clone to each config:

```rust,ignore
// once, e.g. when building the app state
let client = HttpClient::create(
    HttpClientConfig::create().with_max_idle_connections_per_host(8),
);

// per request
let cfg = HttpRequestConfig::create().with_client(client.clone());
```

All clones share one pool, which closes its connections when the last clone is
dropped. With a client, TLS verification follows the client's
`HttpClientConfig`; the request's own timeout, headers and size limit still
apply. In the browser the client changes nothing: the browser pools
connections itself.

A pooled connection skips the TCP and TLS handshake, but not the DNS lookup:
the host is resolved before the pool is asked for a connection. When many
requests go to the same few hosts, let the client remember the answer:

```rust,ignore
let client = HttpClient::create(HttpClientConfig::create().with_dns_cache_secs(300));
```

The default is `0`, one lookup per request. A host that changes its address
is reached again once its cached answer expires.

The same idea applies to threads. `ThreadPool::create(n)` starts `n` workers,
and `pool.create_thread(...)` returns an ordinary `Thread` whose body runs on
one of them, so many short jobs don't each start an OS thread.

## Modelling connection state

Use a plain enum on the application data side. A typical shape:

```rust,ignore
enum ConnectionStatus {
    Idle,
    Connecting { thread_id: ThreadId, started: Instant },
    Done       { response:  HttpResponse },
    Failed     { reason:    String },
}
```

Cancel by calling `event.remove_thread(thread_id)` from a click handler. The thread destructor sends `TerminateThread` and joins. If your worker checks `recv.recv()` between operations, cancellation is prompt.

## Using an async runtime

The framework doesn't host a runtime, but nothing prevents you from running one inside a `Thread`:

```rust,ignore
extern "C" fn tokio_worker(
    _initial:   RefAny,
    mut sender: ThreadSender,
    mut _recv:  ThreadReceiver,
) {
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("runtime");

    rt.block_on(async {
        // futures-based code here; pump results through `sender`
    });
}
```

A current-thread runtime keeps everything on the worker. Use a multi-threaded runtime if you need a worker pool, but spawn it once and reuse. Runtimes are expensive to construct.

## Peer-to-peer connections

`IrohEndpoint` (module `iroh`) connects two apps directly over QUIC, with hole punching and an optional relay, using [iroh](https://iroh.computer). An endpoint is identified by its public key. A *ticket* is the dialing string that carries the key and the endpoint's current addresses; hand it to the other side as a link or QR code.

```rust,ignore
let endpoint = IrohEndpoint::bind(
    IrohConfig::create("my-app/1").with_relay_mode(IrohRelayMode::Disabled),
);

extern "C" fn pump(mut data: RefAny, _info: TimerCallbackInfo) -> TimerCallbackReturn {
    let endpoint = endpoint_of(&mut data);
    while let Some(event) = endpoint.recv().into_option() {
        match event.kind {
            IrohEventKind::Ready => show_invite(&mut data, event.text),
            IrohEventKind::PeerConnected => remember_peer(&mut data, event.peer),
            IrohEventKind::Frame => draw_frame(&mut data, event.track, event.data),
            IrohEventKind::Message => handle_message(&mut data, event.data),
            IrohEventKind::PeerDisconnected | IrohEventKind::Error => report(&mut data, event.text),
        }
    }
    TimerCallbackReturn::continue_unchanged()
}
```

- `connect(ticket)` dials. The outcome arrives as a `PeerConnected` or `Error` event.
- `send_frame(peer, track, data)` and `broadcast_frame(track, data)` send each frame on its own QUIC stream. A frame that has not left yet is replaced by the next frame of the same track, so a slow link lowers the frame rate instead of adding latency. JPEG frames from `RawImage::encode_jpeg` are the simplest video format.
- `send_message(peer, data)` is reliable and ordered, for chat and control data.
- `peer_stats(peer)` reports whether the path is direct or relayed, the RTT, the congestion window and frame counters.
- Nothing arrives unless you poll `recv`, so drive it from a timer.
- For rooms, `IrohLoadBalancer` picks the peers that forward media for everyone (`backbone_size`, `select_backbone`), and `IrohTileRole::rendition_height` picks the resolution a video tile should request.

The engine needs the dll's `iroh` feature, which `build-dll` enables; `PlatformCapability::iroh()` reports whether it is compiled in. In the browser the handle exists but does not bind yet. `examples/azul-meet` opens two windows that exchange camera and screen frames this way.

## What this page doesn't cover

- TLS configuration beyond `disable_tls_cert_verification`. For custom TLS stacks, use `rustls`, `native-tls`, or a third-party HTTP client inside the worker.
- Mid-frame cancellation of in-flight DNS or TCP handshakes. `std::net` doesn't expose this. Use `socket2` or a third-party client if you need it.
- WebSockets, gRPC, HTTP/2. Any blocking client works inside a `Thread`.

---

### Doc Guide En Debugging E2e Testing (`doc/guide/en/debugging/e2e-testing.md`)

---
slug: debugging/e2e-testing
title: End-to-End Testing
language: en
canonical_slug: debugging/e2e-testing
audience: external
maturity: wip
guide_order: 202
topic_only: false
short_desc: Driving an Azul app from a script for tests
prerequisites: [debugging]
tracked_files:
  - core/src/debug.rs
  - dll/src/desktop/logging.rs
  - dll/src/desktop/shell2/common/debug_server.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T00:00:00Z
---

# End-to-End Testing

## Introduction

*WIP.* Assertion vocabulary, JSON keys, and the in-browser test designer all work today; field names may shift as the assertion engine grows.

A test is a JSON object with a name, optional setup, and an ordered list of steps. A step is one debug command (`click`, `text_input`, `resize`, …) or one assertion (`assert_text`, `assert_layout`, …). The runner drives a real `App` instance and evaluates each assertion against the live DOM. Two ways to run:

- `AZ_E2E=<file> ./my_app` boots your application headlessly and runs the JSON file as a single batch. Prints cargo-test-style output, exits `0` if every test passes, `1` otherwise.
- `POST /e2e/run` from a script or the in-browser inspector queues the same payload against a running app.

## A first test

```json
{
  "name": "Button click increments counter",
  "setup": {
    "window_width": 800,
    "window_height": 600,
    "dpi": 96
  },
  "steps": [
    { "op": "wait_frame" },
    { "op": "click", "selector": "button" },
    { "op": "wait_frame" },
    { "op": "assert_text", "selector": "p", "expected": "6" },
    { "op": "take_screenshot" }
  ]
}
```

Run it:

```bash
AZ_BACKEND=headless AZ_E2E=test.json ./my_app
```

Output mirrors `cargo test`:

```text
running 1 test
test "Button click increments counter" ... ok

test result: ok. 1 passed; 0 failed; finished in 0.21s
```

A test file is either one test object or an array of them.

## File and step shape

```json
{
  "name": "string",
  "description": "string?",
  "config": {
    "continue_on_failure": false,
    "delay_between_steps_ms": 0
  },
  "setup": {
    "window_width":  800,
    "window_height": 600,
    "dpi":           96,
    "app_state":    { "any": "json" }
  },
  "steps": [
    { "op": "...", "screenshot": false, "<params>": "..." }
  ]
}
```

`config.continue_on_failure` keeps running steps after the first failure (still reports the test as failed). `config.delay_between_steps_ms` inserts a sleep, useful for visually inspecting a test that runs against a visible window. `setup.app_state` puts each test into a known state without restarting the process.

`screenshot: true` on any step captures the rendered window after the step and embeds the base64 PNG in that step's result.

## Step operations

Every `op` accepted by `POST /` (covered in [Debugging](..md)) is a valid step. Plus the assertion ops below.

- `assert_text` (params: `selector`, `expected`). First-match text content equals `expected`.
- `assert_exists` (params: `selector`). At least one node matches.
- `assert_not_exists` (params: `selector`). No node matches.
- `assert_node_count` (params: `selector`, `expected` int). Exactly `expected` nodes match.
- `assert_layout` (params: `selector`, `property`, `expected`, `tolerance?`). `property` is `x`, `y`, `width`, or `height`. Default tolerance `0.5` px.
- `assert_css` (params: `selector`, `property`, `expected`). Computed CSS value equals `expected`.
- `assert_app_state` (params: `path`, `expected`). Dot-path against the JSON-serialised app data.
- `assert_scroll` (params: `selector`, `x?`, `y?`, `tolerance?`). Scroll position of the first match.
- `assert_screenshot` (params: `reference`, `threshold?`, `max_diff_ratio?`, `save_actual?`). Compares the current screenshot against a reference PNG. **A missing reference PNG is a FAILURE.** Run with `AZ_E2E_RECORD=1` to record a provisional baseline — the step still fails, so an unreviewed baseline can never gate green.

### Mounting a DOM from the test file

`mount` replaces the app's `layout_callback` output with an inline document, so a test does not depend on whatever DOM the host binary happens to build. Markup and CSS are given as an **array of lines** (a single string is also accepted):

```json
{ "op": "mount",
  "html": ["<div class=\"a\">", "  <p>hi</p>", "</div>"],
  "css":  [".a { width: 100px; background: blue; }"] }
```

The lines are joined with `\n`, the CSS is injected as a `<style>` element and the document is parsed by azul's XML→StyledDom parser. `unmount` restores the app's own DOM.

### Damage, frame-work and resource assertions

`snapshot_frame` (`as`), `snapshot_resources` (`as`), `snapshot_managers` (`as`) and `reset_frame_counters` are plain ops that record state for the assertions below. They read `LayoutWindow::frame_report` (paint/present damage + work counters).

Three more plain ops make the frame report directly observable:

- `tick_ms` (params: `ms`). Advances the engine clock by `ms` **without sleeping** and renders a frame. Animations, scroll momentum, scrollbar fade and cursor blink all read `Instant::now()`, which honours this offset — so an animation can be driven to completion and then asserted to converge (`assert_idle_stable`).
- `get_frame_report`. Returns the whole `FrameReport` as JSON: `paint_damage_kind`/`paint_damage_rects`/`paint_damage_area_ratio`, the same for `present_*`, `relayout_iterations`, `dom_regenerations`, `hit_depth_cap`, `terminal_result`, `frame_index`, `test_clock_offset_ms`.
- `capture_damage_png` (params: `path`, `which?` = `paint`|`present`, `crop?`). Writes the **partial screen update** as a PNG: the frame masked to the damage region (everything else transparent), optionally cropped to the damage bounding box. No damage ⇒ a 1×1 transparent PNG.

The `assert_damage*` ops look at the damage **accumulated since the last `reset_frame_counters`**, not at the last frame — between the step that changed something and the assertion, the engine may render further idle frames whose damage is `None`, and those would otherwise clobber it. Pass `"frame": "last"` to look at the most recent frame only. `assert_idle_stable` always looks at the last frame.

- `assert_damage` (params: `which?` = `paint`|`present`, `kind?` = `none`|`rects`|`full`, `min_rects?`, `max_rects?`, `max_area_ratio?`, `frame?`).
- `assert_changed` (params: `vs`, `min_damage_rects?`, `threshold?`). LIVENESS: damage must be non-empty **and** pixels must actually differ from the snapshot.
- `assert_damage_covers_changes` (params: `vs`, `threshold?`, `slack_px?`). SOUNDNESS: every changed pixel lies inside the damage rects (under-paint ⇒ stale screen).
- `assert_damage_incremental` (params: `max_area_ratio?`, default `0.5`). The repaint is a PATCH, not a full redraw.
- `assert_idle_stable` (params: `vs?`). No input ⇒ damage drained to `none` (and pixels identical to `vs`). The infinite-redraw detector.
- `assert_work_bounded` (params: `max_relayouts?`, `max_dom_regens?`, `allow_depth_cap?`). Measured since the last `reset_frame_counters`. Fails if `MAX_EVENT_RECURSION_DEPTH` was hit — an invalidation loop is now a red test rather than a `log_warn`.
- `assert_resource_counts` (params: `vs`, then one mode per counter: `"eq"`|`"le"`|`"ge"` or an exact number). Counters: `fonts`, `font_hash_map`, `font_id_map`, `font_families_map`, `images`, `image_key_map`, `parsed_fonts`, `font_hash_to_families`, `font_chain_cache`.

### Manager assertions: consistency, and non-interference

The engine keeps ~20 managers on `LayoutWindow` (scroll, focus, hover, gesture, text edit, undo, virtual view, GPU state, the capability managers …). Two different questions can be asked about them, and they need two different assertions.

**Is any manager's state internally wrong?** — `assert_manager_invariants` (params: `managers?`, `cross?`, `min_checked?`). It sweeps for dangling node keys (X10: no manager key may name a node that no longer exists) plus the pairwise invariants. Six of them (`X2 X3 X5 X6 X9 X10`) hold unconditionally and run by default. Four are opt-in because each is a statement *about an interaction* and hard-fails when the scenario never performed it:

- `X1` — after a `scroll_into_view`, the target is inside its container's visible rect according to `ScrollManager`'s own offset.
- `X4` — the `DragState` the public drag API hands out describes the same drag the engine is running.
- `X7` — focus was cleared, so no cursor scroll may still be owed (`cursor_needs_initialization` / `pending_contenteditable_focus`).
- `X8` — during a text-selection drag, the container that autoscrolled is the selection focus node's own scroll container.

Naming a manager the assertion cannot check, or an invariant nobody implemented, is a hard failure with the reason attached — never a silent pass. `min_checked` lets a scenario require that the sweep actually inspected N things, so "nothing was wrong" cannot be confused with "there was no state to inspect".

**Did this op move a manager it has no business touching?** — `snapshot_managers` (`as`) + `assert_only_managers_changed` (params: `vs`, `changed`, `min_populated?`). Record every manager's state, perform one op, and require the set that moved to be *exactly* `changed`. Write `"changed": []` for "this must not touch a single manager".

```json
{ "op": "snapshot_managers", "as": "scrolled_and_settled" },
{ "op": "key_down", "key": "tab" },
{ "op": "key_up",   "key": "tab" },
{ "op": "wait_frame" },
{ "op": "assert_only_managers_changed",
  "vs": "scrolled_and_settled", "changed": ["focus"], "min_populated": 2 }
```

A scroll that quietly cleared focus, or a Tab that reset a scroll offset, renders identically to one that did not: it is invisible in the DOM, in the pixels and in the resource counters. This is the only assertion that sees it.

The check is **two-sided**. A manager that moved without being listed is a leak; a manager listed in `changed` that did *not* move is a scenario that has stopped exercising what it claims to. Without the second half, `"changed": [every, manager]` would pass forever while asserting nothing. `min_populated` is the third guard: two empty snapshots always compare equal, so it requires that at least N managers were actually holding state.

Settle the frames (`wait_frame` + `wait`, and `tick_ms` past any running fade or animation) before **both** the snapshot and the assertion. Transient repaint flags are real manager state and are recorded, so comparing a settled window against a mid-flight one correctly reports them as moved.

Selector resolution accepts CSS selectors (`.btn`, `#counter`, `div > span`), explicit `node_id` integers, or a `text` substring match. Pick whichever is least brittle. `selector` is preferred because the inspector can build them by clicking nodes in the DOM tree.

### Mocking OS requests: dialogs, HTTP, files and devices

A *request* (`FileDialog::open_file`, `HttpRequestConfig::http_get`, `FilePath::read_bytes`, `Db::sync_now`, ...) asks the OS for something and resumes the app later through its resume callback. A scenario cannot click a native file picker or reach the network, so under `AZ_E2E` / `AZ_BACKEND=headless` every request first consults a **mock store** that the scenario fills with the `mock` op. A mocked answer resumes the app through the same pump and the same callback a real answer takes; the app code under test is unchanged.

- `mock` (params: `set` object). Arms the store and queues answers. Keys of `set`:
  - `reset: true` - empties every queue and the records. Put it first in every scenario.
  - `file_open: { "path": "..." }` or `null` - the next `FileDialog::open_file` resumes with that path, or as cancelled. Queued: one answer per call, in order.
  - `file_open_multi: { "paths": [...] }` - the next `FileDialog::open_multiple_files`.
  - `color_pick: { "r": 0, "g": 0, "b": 0 }` or `null` - the next `ColorPickerDialog::open`.
  - `save_file: { "path": "..." }` or `null` - the next `FileDialog::save_file`.
  - `save_bytes: { "accept": true }` - what `FileDialog::save_bytes` reports (default `true`). The bytes are recorded either way, for `assert_saved_file`.
  - `file_read: { "<path>": { "text": "..." } }` or `{ "b64": "..." }` per path - canned content for `FilePath::read_bytes` / `read_string`. Paths without an entry read the real file.
  - `http: { "<pattern>": { "status": 200, "text": "...", "content_type": "..." } }` or `{ "error": "..." }` per pattern - canned answers for `http_get`, `http_post`, `http_request`, `download_bytes`, `is_url_reachable` and the Db sync endpoints. A pattern is an exact URL, a prefix ending in `*`, or `*`; `b64` replaces `text` for binary bodies.
  - `audio_devices: { "outputs": [...], "inputs": [...] }` - `AudioDeviceList::enumerate`.
  - `video_decode: { "none": true }` - `decode_mp4_h264` resumes without a video.

A request that runs while the store is armed but has no queued answer never opens a native dialog and never touches the network: it resumes as cancelled (dialogs), unreachable (HTTP) or empty (devices), **and is recorded**. That keeps a scenario that forgot to mock something from hanging on a dialog nobody can click; three assertions turn the record into a verdict:

- `assert_no_unmocked_requests`. Every request since the last `reset` had a canned answer. End every scenario that mocks what it needs with it.
- `assert_unmocked_request` (params: `request`, a substring of the record such as `"FileDialog::open_file"` or `"http https://..."`). The opposite: proves that a request ran without an answer, for scenarios that test the cancelled path.
- `assert_saved_file` (params: `name` or `name_ends_with`, `mime?`, `min_len?`, `contains?`). An export recorded by `FileDialog::save_bytes` matches; the newest matching export wins.

```json
{ "op": "mock", "set": { "reset": true,
    "file_open": { "path": "/tmp/report" },
    "file_read": { "/tmp/report": { "text": "a,b\n1,2\n" } } } },
{ "op": "click", "text": "Open file" },
{ "op": "wait_frame" },
{ "op": "assert_text", "selector": "#file", "expected": "file: /tmp/report (8 bytes)" },
{ "op": "assert_no_unmocked_requests" }
```

Mocked answers are delivered by the pump that also runs timers, and the pump keeps going while a resume callback issues further requests, so one `wait_frame` after the click settles a whole chain such as open -> read. The `resume` example (`examples/rust/src/resume.rs`) has one button per request kind, and the specs in `tests/e2e/resume_*.json` drive all of them, including the "forgot to mock" case.

## Step results

Each step returns:

```json
{
  "step_index": 1,
  "op": "click",
  "status": "pass",
  "duration_ms": 12,
  "logs": ["[Input] click at (200, 300)", "..."],
  "screenshot": "data:image/png;base64,...",
  "response": { "...command-specific data..." },
  "error": null
}
```

A test rolls steps up:

```json
{
  "name": "Button click increments counter",
  "status": "pass",
  "duration_ms": 156,
  "step_count": 5,
  "steps_passed": 5,
  "steps_failed": 0,
  "steps": [ ... ],
  "final_screenshot": "data:image/png;base64,..."
}
```

`final_screenshot` is the screenshot from the last step that requested one, or `null`.

## Driving with curl

The debug server's `POST /` accepts a `run_e2e_tests` op that bypasses the file-based runner:

```bash
curl -s -X POST http://127.0.0.1:8765/ \
     -H 'Content-Type: application/json' \
     -d @tests.json | jq '.data.value.results'
```

Default request timeout is 30 s; tests that take longer must either pass `"timeout_secs": 600` in the request body or use `AZ_E2E=` (which uses a 600 s timeout internally). The Hello World shell driver in [Debugging](..md) shows the same wait-frame / click / read-back pattern that an E2E step performs internally. You can build a test scenario incrementally as a curl script, then crystallise it into a JSON file once it works.

## Continuation across relayout

Some steps (`resize`, `set_node_text`, `delete_node`) require a relayout pass to complete before the next step can read the resulting state. The runner detects these and yields back to the timer; the test resumes on the next tick. From the test author's perspective this is invisible: write `resize` followed by `assert_layout` and the runner handles the suspension.

This is why `AZ_E2E` requires the application to reach the event loop. The test cannot make progress while the timer is not running. With `AZ_BACKEND=headless` (or `AZUL_HEADLESS=1`) the event loop runs without an OS window, which is the standard CI configuration.

## CI integration

A typical workflow:

1. Build the application once: `cargo build --release`.
2. Run the test bundle headlessly: `AZ_BACKEND=headless AZ_E2E=tests/smoke.json ./target/release/my_app`.
3. Process exit code is the test verdict. Use it as the CI step's exit code.
4. For screenshot diffs, set `assert_screenshot` steps with a `reference` PNG path; commit the references to the repo and update them via a `BLESS=1` workflow when intentional UI changes land.

For per-PR feedback, every shell script in [`tests/e2e/`](https://github.com/fschutt/azul/tree/master/tests/e2e) is a `curl`-against-`AZ_DEBUG` driver that became a JSON test once it was stable.

The language-binding board ([`scripts/e2e_language_matrix.sh`](https://github.com/fschutt/azul/blob/master/scripts/e2e_language_matrix.sh)) runs the same counter scenario against every binding and prints a status table. On a `--gate-shipped` failure it now dumps the tail of each failed binding's build/run log **and** its `AZ_RECORD` trace, so CI logs explain the failure without a local repro.

## Language toolchain prerequisites

Each binding drives the prebuilt library (built with `--features build-dll,debug-server`, which compiles in the `AZ_E2E` runner) and needs its own toolchain to build and run the hello-world. Missing toolchains report `⊘ SKIP` (they do not gate); a present-but-broken toolchain reports `✗ FAILS`. The shipped tier:

| Binding | Toolchain | Notes / common gotchas |
|---|---|---|
| Rust   | `cargo`/`rustc` 1.91+ | The example links the prebuilt DLL with `--no-default-features --features link-dynamic` (not `link-static`, which rebuilds Azul without the `debug-server` E2E runner). On Windows the import lib must be reachable as both `azul.dll.lib` and `azul.lib`. |
| C      | `clang`/`gcc`; MinGW on Windows (links `azul.dll.lib`) | Include the generated `azul.h`. |
| C++    | `clang++`/`g++` with `-std=c++20` (also 03–23) | Include `azul20.hpp`. Wrapper types are move-only RAII; pass owning values with `std::move` — never copy a moved-from wrapper. |
| Python | CPython 3.10+ | Uses the `azul` extension module (a separate build), not FFI. |
| Node   | `node` + the `koffi` npm package | macOS strips `DYLD_*` from the hardened `node`, so `koffi` must find the lib by an explicit path or the lib must sit next to the script. |
| C#     | .NET SDK (`dotnet`) | P/Invoke; the native lib must be loadable next to the build output. |
| **Java**   | **JDK 17+** and **Maven (`mvn`)** + **JNA 5.14** | The build targets release 11, so a JDK older than 11 fails with `invalid target release: 11` — pin JDK 17 (e.g. `actions/setup-java@v4` with Temurin 17). On macOS the JVM needs `-XstartOnFirstThread`. |
| **Kotlin** | **JDK 17+**, `kotlinc` (or Gradle) + **JNA 5.14** | Same JDK requirement as Java; `kotlinc` compiles `Azul.kt` + `HelloWorld.kt` against the JNA jar. |
| **Ruby**   | `ruby` + the **`ffi` gem** | `require 'ffi'` fails with `LoadError` unless the gem is installed: `gem install ffi`. It is **not** preinstalled on CI runners. |
| **Lua**    | **LuaJIT** (the stock PUC `lua` has no FFI); full E2E runs on **arm64/macOS**, not x86-64 | LuaJIT's `ffi` cannot call a C function that passes an aggregate **by value** on some ABIs: on **x86-64 (SysV)**, `App.create(.., AppConfig)` raises `NYI: cannot call this C function (yet)`. This is a LuaJIT limitation, **not** a version issue (a freshly-built current LuaJIT 2.1 still NYIs) and **not** a binding bug — the same `azul.lua` runs on arm64/macOS, where the struct is passed differently. CI therefore reports lua as `⊘ SKIP` on x86-64 (a toolchain limit, which never gates). |
| Go     | `go` toolchain + a C compiler (cgo) | — |

`AZ_LOG` is on by default (see [Debugging](..md)), so a binding that builds but exits early will print the platform-layer trace on stderr, which the board captures.

## Recording a test from the inspector

The in-browser inspector at `http://localhost:<port>/` includes an E2E designer. Click "Record", interact with the running window normally, and the inspector captures each click, text input, scroll, and resize as a step. Click "Stop", review the steps, and either run them in place or export to JSON for `AZ_E2E`.

The recorder uses the same selector resolver as `assert_*`, so the captured steps are robust to layout shifts as long as your DOM has stable IDs and classes.

## Limitations

- A test runs against the *current* application instance — there is no per-test sandbox. Use `setup.app_state` to put the app in a known state before each test.
- `take_native_screenshot` returns the actual framebuffer of the running window. Pixel-identical comparison across platforms is unrealistic; use `assert_screenshot` with a `max_diff_ratio` tolerance, or pin the diff job to one platform in CI.
- The runner does not multi-thread tests. Each test runs to completion before the next starts. If you need parallel runs, spawn N processes on N ports.

---

### Doc Guide En Debugging Profiling (`doc/guide/en/debugging/profiling.md`)

---
slug: debugging/profiling
title: Profiling
language: en
canonical_slug: debugging/profiling
audience: external
maturity: wip
guide_order: 201
topic_only: false
short_desc: Tracking allocations and per-frame budgets
prerequisites: [debugging]
tracked_files:
  - core/src/debug.rs
  - dll/src/desktop/logging.rs
  - dll/src/desktop/shell2/common/debug_server.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T00:00:00Z
default-search-keys:
  - App
  - StyledDom
  - Dom
  - RefAny
---

# Profiling

## Introduction

*WIP.* JSONL field names may shift; the env-var surface (`AZ_PROFILE`, `AZ_PROFILE_OUT`, `AZ_E2E_TEST`) is stable.

The profiler is gated by a build feature in the layout crate and driven entirely by environment variables. Without the feature it compiles to inline no-ops; with the feature on but no `AZ_PROFILE` set, every probe checks one atomic and returns. There is no static cost to leaving probes in production builds.

```bash
AZ_PROFILE=cpu ./my_app                                            # per-phase CPU dump to stderr
AZ_PROFILE=memory ./my_app                                         # heap-breakdown dump to stderr
AZ_PROFILE=heap,jsonl AZ_PROFILE_OUT=/tmp/run.jsonl ./my_app       # phase boundaries to JSONL
AZ_PROFILE=heap,jsonl,detail AZ_PROFILE_OUT=/tmp/d.jsonl ./my_app  # add per-step probes
```

## AZ_PROFILE tokens

`AZ_PROFILE` is a comma-separated list. Tokens are independent flags, not modes; combine freely. Whitespace is trimmed, matching is case-insensitive, unknown tokens are ignored.

- `memory` (alias `mem`). Per-frame heap-breakdown dump (DOM caches, layout cache, text cache, RSS) to stderr.
- `cpu` (alias `perf`). Per-phase wall-clock timings (layout, style, cascade, paint, callbacks, etc.).
- `cascade` (alias `css`). Top-N CSS properties by cascade-walk count per frame.
- `heap`. Phase-boundary heap probes inside the layout pass. Silent without `jsonl`.
- `jsonl`. Formats `heap` probes as JSONL to `AZ_PROFILE_OUT`. Requires `heap`.
- `detail`. Opts in to fine-grained per-step probes. Layered on top of `heap`.

`AZ_PROFILE_OUT=<path>` names the JSONL destination. With `jsonl` set and `AZ_PROFILE_OUT` unset, writers silently skip — benchmarks stay clean.

## What each token costs you

- **`cpu`** wraps phase functions with a timestamp pair on entry/exit. Sub-microsecond per probe; safe to leave on in CI.
- **`memory`** computes per-cache summaries every frame. Non-trivial — typical 20–100 µs depending on DOM size. Use during investigation, not in steady-state runs.
- **`heap` + `jsonl`** queries the libc allocator at every named boundary. Roughly the cost of one syscall (~200 ns).

## What each report contains

`AZ_PROFILE=cpu` prints one block per frame to stderr, one line per phase span name with mean, p99, and call count over the frame. `AZ_PROFILE=memory` prints one block per frame with field-level byte counts pulled from each cache:

```text
StyledDom        node_count=4083  total=2.1 MiB
  node_hierarchy 4083 * 16  = 65 KiB
  node_data      4083 * 96  = 392 KiB
  styled_nodes   4083 * 24  = 98 KiB
  prop_cache     css_props=412 KiB  computed=78 KiB
  ...
LayoutCache      tree_hot=88 KiB  warm=412 KiB  cold=64 KiB
TextCache        logical=18 KiB  shaped_glyphs=240 KiB  shaped_clusters=87 KiB
RSS              78.2 MiB
```

## Heap vs RSS

The profiler exposes both numbers because they tell you different things:

- **Heap** — bytes the libc allocator currently holds. On macOS this is `mstats().bytes_used`. A growth here is a Rust-level retention: an `Arc` chain never dropped, a `Vec` never shrunk, a `Box<T>` forgotten. mmap'd regions (thread stacks, file-backed fonts, GL buffers) are not counted.
- **RSS** — kernel resident-set size, what `top` and Activity Monitor show. Includes shared library text, mmap'd fonts, GPU driver pages.
- **`phys_footprint`** (macOS only) — what Activity Monitor's "Physical footprint" shows; excludes shared text pages. More honest than RSS for short-lived processes.

A leak in your code shows up in both. A leak that shows up only in RSS but not in heap usually means an mmap'd resource (a font file, a texture) is being held longer than it needs to be.

## Phase-boundary probes

`AZ_PROFILE=heap,jsonl` writes one line of JSONL per phase boundary inside the layout pass, with a monotonic `call` id grouping all labels from a single layout pass:

```jsonl
{"ev":"phase","label":"start","heap":18874368,"call":42}
{"ev":"phase","label":"styled_dom_built","heap":19135488,"call":42}
{"ev":"phase","label":"layout_solved","heap":19921920,"call":42}
{"ev":"phase","label":"display_list_built","heap":19987456,"call":42}
{"ev":"phase","label":"end","heap":19135488,"call":42}
```

Heap delta between adjacent labels with the same `call` id is the bytes retained by that phase. If `start` and `end` differ, that frame leaked. Run a few hundred frames, group by label, and fit a linear trend per label — anything that climbs is a suspect.

## Hunting a leak with AZ_E2E_TEST

`AZ_E2E_TEST=<scenario.json>` (gated by a build feature) takes over `main()` to run a deterministic resize/tick scenario against a headless backend. It probes RSS at a configurable cadence, compares against caps, and exits 0/1.

```json
{
  "name": "calc-resize-leak",
  "warmup_ticks": 10,
  "steps": [
    { "action": "resize",      "width": 800, "height": 600 },
    { "action": "resize",      "width": 600, "height": 400 },
    { "action": "resize_full", "width": 800, "height": 600 },
    { "action": "tick" }
  ],
  "loop": { "iterations": 500 },
  "rss_probes": {
    "every_n_iterations": 50,
    "warmup_skip": 0,
    "assert_growth_mib_max": 5.0,
    "assert_absolute_mib_max": 80.0,
    "memory_breakdown": true
  },
  "output": {
    "jsonl_path":    "/tmp/calc.jsonl",
    "summary_path": "/tmp/calc.summary.jsonl"
  }
}
```

`assert_growth_mib_max` is the per-run delta between baseline (after warmup) and the final RSS sample; `assert_absolute_mib_max` caps total RSS at any point. Either breach exits the process with code `1`. With `memory_breakdown: true` each probe also emits a flat `mem` event covering every measurable byte across the layout caches. Feed the JSONL to a regression analyzer and let it pick the field whose slope grew.

```bash
cargo run --release --features e2e-test -- \
  AZ_E2E_TEST=calc.json
# → exit 0 (under cap) or 1 (growth/absolute breach)
# → JSONL events in /tmp/calc.jsonl
```

## A workflow for "why is this growing?"

1. Reproduce with `AZ_PROFILE=memory`. Watch the per-frame block. Identify which cache's bytes grow.
2. If heap grows but no individual cache does, the leak is outside the layout pipeline. Check user code in callbacks that retain `RefAny` clones.
3. Re-run with `AZ_PROFILE=heap,jsonl,detail AZ_PROFILE_OUT=/tmp/probes.jsonl`. Group by phase label. The phase whose heap delta grows over time is where the leak lives.
4. Build a deterministic `AZ_E2E_TEST` scenario that reproduces the trigger (a resize loop, a route change, …). Set `assert_growth_mib_max` slightly under the observed growth so the regression becomes a binary signal.
5. Commit the scenario JSON next to the test that runs it.

## Allocator selection

The layout crate has feature flags for alternate allocators (`allocator_mimalloc`, `allocator_jemalloc`). They release freed pages to the OS before each RSS sample, otherwise the allocator holds onto pages and inflates RSS. Pick an allocator that matches your production deployment so the numbers match what users see — RSS curves change shape with the allocator.

---

### Doc Guide En Deploying Mobile (`doc/guide/en/deploying/mobile.md`)

---
slug: deploying/mobile
title: Mobile (iOS & Android)
language: en
canonical_slug: deploying/mobile
audience: external
maturity: beta
guide_order: 231
topic_only: false
short_desc: Build, package, sign and install an Azul app as an .apk / .ipa — all Rust, from any OS, no Xcode or Android Studio
prerequisites: [hello-world]
tracked_files:
  - dll/build.rs
  - dll/Cargo.toml
  - dll/src/desktop/shell2/android/mod.rs
  - dll/src/desktop/shell2/run.rs
  - scripts/build-android.sh
  - scripts/build-ios.sh
  - scripts/mobile-check-all.sh
last_generated_rev: ae991d834f665b88609d19ecf25dc88ea50e98a0
generated_at: 2026-05-30T00:00:00Z
default-search-keys:
  - App
  - WindowCreateOptions
  - link-static
  - aarch64-linux-android
  - aarch64-apple-ios
---

# Mobile (iOS & Android)

Your Azul app **is** the mobile app. The same `App::create(...).run(...)` you
wrote for desktop ships as an `.apk` or `.ipa` — there is no Java/Kotlin or
Swift/Objective-C app layer to write, and you need **neither Xcode nor Android
Studio**. An `.apk` and an `.ipa`/`.app` are just ZIP archives with a known
layout, so you cross-compile the native library with the Rust compiler and pack
it with a few command-line tools. You can build for *both* platforms from Linux;
only the final iOS code-signing touches an Apple-specific tool, and even that has
a cross-platform option (see [iOS signing](#ios-code-signing-no-xcode)).

The two ready-made scripts —
[`scripts/build-android.sh`](https://github.com/fschutt/azul/blob/master/scripts/build-android.sh)
and [`scripts/build-ios.sh`](https://github.com/fschutt/azul/blob/master/scripts/build-ios.sh)
— do the whole thing (cross-compile → bundle → sign → optionally deploy) with no
IDE; CI runs exactly these to produce the release artifacts. The sections below
explain what they do so you can reproduce or adapt them. Every example in the
repo (AzulMaps, azul-paint, azul-meet, …) is packaged this way.

## Supported targets

| Target | Use |
|---|---|
| `aarch64-apple-ios` | iOS device (arm64) |
| `aarch64-apple-ios-sim` | iOS simulator on Apple silicon |
| `x86_64-apple-ios` | iOS simulator on Intel |
| `aarch64-linux-android` | Android device (arm64-v8a) |
| `x86_64-linux-android` | Android emulator |

`rustup target add <triple>` installs each. `bash scripts/mobile-check-all.sh`
runs `cargo check` across all five **plus** the two desktop cross targets
(`x86_64-unknown-linux-gnu`, `x86_64-pc-windows-gnu`) — seven in total, and it
needs no SDK for any of them (see [Local testing](#local-testing)).

## How an Azul app maps onto each platform

Everything — layout, rendering (CPU), callbacks, the
[realtime-media](../system/realtime-media.md) / sensor / gamepad / geolocation device APIs
— is identical to desktop. The one structural difference is the **entry point**:

- **iOS** keeps a normal `fn main()`. Your `main` calls `App::run(...)`, which on
  iOS hands control to `UIApplicationMain` and drives the UIKit run loop. So an
  iOS app is just your example compiled as a **binary** for an iOS target — the
  exact same source as desktop.
- **Android** has *no* `main()`: the OS loads your `.so` and calls
  `ANativeActivity_onCreate` (provided by the bundled
  [`android-activity`](https://crates.io/crates/android-activity) glue), which
  invokes `android_main` inside `libazul`. So an Android app is your example
  compiled as a **`cdylib`** with a tiny load-time shim. See
  [Android entry point](#android-entry-point).

## Two ways to ship libazul

Just like desktop, you don't rebuild the framework — you decide how your app
links against it:

- **Dynamic (drop-in prebuilt).** Ship the prebuilt `libazul` in the bundle and
  link your small app against it. On Android, put `libazul.so` in the APK under
  `lib/<abi>/` (e.g. `lib/arm64-v8a/`); on iOS, embed `libazul.dylib` in
  `MyApp.app/Frameworks/` and set the app binary's rpath to
  `@executable_path/Frameworks`. CI publishes a `libazul` per mobile target on
  the [release page](https://azul.rs/ui/release/$VERSION) for download. Your app can be C, Rust, or any
  binding — a C `hello-world.c` links `libazul` and calls `AzApp_create` /
  `AzApp_run` exactly as on desktop. On iOS the app binary **and** the embedded
  `libazul.dylib` must both be code-signed.
- **Static (single artifact).** Build with the `link-static` feature so your app
  and azul compile into one `.so` (Android) or binary (iOS). This is what the
  build scripts and every repo example use, and what the rest of this page shows.

A mobile `libazul` is per-ABI/arch (`aarch64` for devices, `x86_64` for the
emulator/simulator) — you bundle the slice(s) you target, not a single file.

## Minimal toolchain (only the stubs you need)

You do **not** need the full NDK or the iOS SDK. Both are mostly *link stubs*
(empty `.so` API stubs / `.tbd` text stubs) plus headers, and Rust already ships
its own linker (`rust-lld`). Azul renders on the **CPU** on mobile
(`gl_context_ptr = None`), so there are no OpenGL ES / Metal libraries to link
either. The entire system-library surface is:

| Platform | Links against | Notes |
|---|---|---|
| **Android** | `libandroid` (NativeActivity, `ANativeWindow`, `ALooper`), `liblog` (`__android_log_print`), and `libc` / `libm` / `libdl` | Tiny NDK *stub* `.so`s — extract just those five, no full NDK needed at link time. (Set via `cargo:rustc-link-lib=android,log` in `dll/build.rs`.) |
| **iOS** | `Foundation`, `UIKit`, `CoreGraphics`, `libSystem` — plus one framework per device API you use (`AVFoundation`, `CoreMotion`, `CoreLocation`, `GameController`) | `.tbd` text stubs in the iOS SDK; copy only the ones you reference. |

So a from-scratch minimal setup is:

1. `rustup target add <triple>` — brings the Rust `std` for the target.
2. A small **stub sysroot**: the five Android stub `.so`s (from the NDK's
   `platforms/android-<api>/.../usr/lib/`) **or** the iOS framework `.tbd` stubs
   (from the SDK's `System/Library/Frameworks/`). A few hundred KB, not the
   multi-GB toolchain.
3. **`rust-lld`** as the linker — no external `ld`, `clang`, or `xcrun`.

For **packaging** you then need only small CLI tools — `aapt2` / `zipalign` /
`apksigner` for Android (head-less `sdkmanager` install, no Studio), nothing for
iOS beyond `zip` and a signer. A pure-`NativeActivity` Android app needs **no
Java** (so no JDK / `d8`) — the custom `AzulActivity.java` is only for the
optional gesture bridge.

> The ready-made `build-android.sh` / `build-ios.sh` currently lean on a normal
> NDK / iOS-SDK install for convenience; the table above is the irreducible set
> if you want to assemble a minimal cross-toolchain (e.g. to build iOS apps on
> Linux). Extracting a minimal stub sysroot is a one-time step.

## Building the native library

Mobile builds use `link-static` with **no default features** (the desktop
windowing/renderer defaults pull in things mobile doesn't want):

```sh
# iOS device — produces the binary (its main() runs UIApplicationMain via App::run)
cargo build --release --target aarch64-apple-ios -p my-app \
    --no-default-features --features "std,logging,link-static,a11y"

# Android arm64 — produces the cdylib the APK ships
cargo build --release --target aarch64-linux-android -p my-app \
    --no-default-features --features "std,logging,link-static,a11y,android-activity"
```

From Rust, depend on `azul-dll` directly with those features; from C, use the
generated `azul.h` (`cargo run -r -p azul-doc -- codegen c`) and the same
`AzApp_create` / `AzApp_run` entry points every binding uses.

## Android

### Android entry point

Because there is no `main()`, run your setup from a **load-time constructor**.
`libazul` already provides `android_main` (via the android-activity glue);
`App::run` on Android just stashes the window options for it to read — and it
must run *before* `ANativeActivity_onCreate`, which is exactly what a
[`ctor`](https://crates.io/crates/ctor) gives you. Factor the setup into one
function and wire both entry points:

```rust
use azul::prelude::*;

pub fn start() {
    let data = RefAny::new(DataModel { counter: 0 });
    let app = App::create(data, AppConfig::create());
    // Android: run() stashes the window options + returns; desktop/iOS: blocks.
    app.run(WindowCreateOptions::create(my_layout));
}

// Desktop / iOS — main() runs and App::run drives UIApplicationMain on iOS.
#[cfg(not(target_os = "android"))]
fn main() { start(); }

// Android — fires at dlopen, before libazul's android_main reads the options.
#[cfg(target_os = "android")]
#[ctor::ctor]
fn azul_android_init() { start(); }
```

(`azul-maps` / `azul-paint` in the repo are set up exactly like this.) Your crate
must build as a `cdylib` for Android, and pulls the android-activity glue + `ctor`
only on Android:

```toml
[lib]
crate-type = ["cdylib", "rlib"]

[target.'cfg(target_os = "android")'.dependencies]
azul = { package = "azul-dll", version = "0.2", default-features = false, features = ["link-static", "android-activity"] }
ctor = "0.2"
```

That is the *entire* difference from a desktop app.

### Package the APK (no Android Studio)

You need **NDK 27**, **build-tools 34** (`aapt2`, `zipalign`, `apksigner`) and a
**JDK 17** — all installable head-less via `sdkmanager`, no IDE. An `.apk` is a
ZIP, assembled like this (what `build-android.sh` does):

```sh
# 1. cross-compile the cdylib (cargo-ndk sets the NDK linker for you).
#    NB: the API level is --platform (cargo-ndk forwards a bare -p to cargo as
#    --package), and it goes BEFORE the `build` subcommand.
cargo ndk -t arm64-v8a --platform 24 -o ./jniLibs build --release \
    -p my-app --no-default-features --features "std,logging,link-static,a11y,android-activity"

# 2. lay out the APK tree, compile the manifest, add the lib + Java glue
aapt2 link --manifest AndroidManifest.xml -I "$ANDROID_HOME/platforms/android-34/android.jar" -o base.apk
zip -r base.apk lib/arm64-v8a/libmy_app.so classes.dex   # classes.dex = dexed scripts/android/*.java

# 3. align + sign with a (debug) keystore — apksigner is a CLI tool
zipalign -f 4 base.apk aligned.apk
apksigner sign --ks debug.keystore --ks-pass pass:android aligned.apk
```

**Files to copy** into your project (Java glue + manifest template) live in
[`scripts/android/`](https://github.com/fschutt/azul/tree/master/scripts/android):
`AndroidManifest.xml` (sets `android.app.lib_name` to your lib), `AzulActivity.java`
(the `NativeActivity` subclass), `NativeGestureBridge.java`, `AzulFilePicker.java`.
The manifest's `lib_name` must match your `cdylib` name. Simplest path:
`bash scripts/build-android.sh aarch64-linux-android <AppName> <com.pkg>`.

Declare permissions in `AndroidManifest.xml` (`CAMERA`, `RECORD_AUDIO`,
`ACCESS_FINE_LOCATION`, `INTERNET`, …) and request the dangerous ones at runtime.

**Minimum Android 7.0 (API 24)** — the camera backend links the NDK Camera2 stubs
(API 24); AAudio (API 26) is loaded at runtime, so on 7.0/7.1 the app runs and
audio just reports unavailable.

## iOS

### Build + bundle the .app/.ipa (no Xcode project)

A `.app` is a directory; an `.ipa` is `Payload/<App>.app` zipped. You do **not**
need an Xcode project — just the iOS SDK (for the linker sysroot) and the
cross-linker. `build-ios.sh` does:

```sh
# 1. build your example as an iOS binary (main() runs UIApplicationMain via App::run)
cargo build --release --target aarch64-apple-ios -p my-app \
    --no-default-features --features "std,logging,link-static,a11y"

# 2. assemble the bundle: the executable + an Info.plist
mkdir -p MyApp.app
cp target/aarch64-apple-ios/release/my-app MyApp.app/MyApp
cp scripts/ios/Info.plist MyApp.app/Info.plist          # template to copy/edit

# 3. (device) sign, then zip into an .ipa
mkdir -p Payload && cp -r MyApp.app Payload/ && zip -r MyApp.ipa Payload
```

The `Info.plist` template + entitlements are standard iOS files.
Add the usage strings for the device APIs you use: `NSCameraUsageDescription`,
`NSMicrophoneUsageDescription`, `NSLocationWhenInUseUsageDescription`,
`NSMotionUsageDescription`.

> **Cross-compiling iOS from Linux:** install the iOS SDK sysroot (extractable
> from the Xcode toolchain, no GUI) and point Rust's linker at it. The simulator
> needs no signing and runs unsigned `.app`s directly.

### iOS code signing (no Xcode)

Code signing is the *only* Apple-specific step, and it does **not** require Xcode
or even a Mac:

- [`rcodesign`](https://github.com/indygreg/apple-platform-rs) (the Rust
  `apple-codesign` crate) signs `.app`/`.ipa` bundles **on Linux or Windows** and
  can submit to **Apple's notarization web service** (`rcodesign notary-submit`),
  a REST API — no `xcrun`/`notarytool` needed.
- You still need an Apple Developer ID certificate + provisioning profile (the
  paid program), but those are files, not tools.
- Simulator builds and personal-team device installs over a debug bridge need no
  signing at all.

## Installing & debugging the built app

You don't have to build anything to try the demos — every example is published
per-OS on the [release page](https://azul.rs/ui/release/$VERSION) (Demos section): a `.apk` for Android, a
device `.app` and a Simulator `.app` for iOS. To install a build (yours or a
downloaded one):

### Android (`.apk`)

The APKs are debug-signed, so they sideload directly.

```sh
# Over USB (enable Settings → Developer options → USB debugging first):
adb install azul-maps-android.apk
# Replace an existing install: adb install -r …; uninstall: adb uninstall com.azul.azul_maps
```

Or copy the `.apk` to the phone and tap it (allow "install unknown apps" for the
browser/file manager).

**Debug logs:** azul's platform layer logs through the `log` facade to logcat
(via `liblog`):

```sh
adb logcat -s azul:V '*:S'        # azul lines only
adb logcat | grep -E '\[camera\]|\[gamepad\]|\[sensors\]|\[cap\]'
```

A native crash prints a tombstone — `adb logcat` shows the `backtrace:` with the
faulting library; pull `/data/tombstones/` for the full dump.

### iOS Simulator (`.app`, no signing)

The Simulator slice (`<demo>-ios-sim.app.zip`) runs **unsigned** — easiest to try
on a Mac:

```sh
unzip azul-maps-ios-sim.app.zip
open -a Simulator                              # boot a simulator
xcrun simctl install booted azul-maps.app
xcrun simctl launch --console booted <bundle-id>   # --console streams stdout/stderr
```

### iOS device (`.app` → signed)

A physical iPhone needs the binary code-signed (a **free** Apple ID / personal
team works for a 7-day sideload). On a Mac:

```sh
codesign --force --sign "Apple Development: you@example.com (TEAMID)" \
  --entitlements entitlements.plist azul-maps.app
xcrun devicectl device install app --device <udid> azul-maps.app
```

No Mac? `rcodesign` signs an `.app`/`.ipa` from Linux/Windows with a Developer ID
`.p12`. **Device logs:** `xcrun devicectl device console --device <udid>`, or
Console.app filtered by the app name, or `idevicesyslog` (libimobiledevice).

## From Rust to a final .apk / .ipa — cross-platform

The whole pipeline is `cargo` + small CLI tools, no IDE, and the same on Linux or
macOS (only the final iOS *signing* prefers a Mac, and even that has the
`rcodesign` escape hatch):

| Step | Android | iOS |
|------|---------|-----|
| 1. Compile | `cargo ndk -t arm64-v8a --platform 24 build` (the cdylib NativeActivity loads) | `cargo build --target aarch64-apple-ios[-sim]` (the binary; `main()` runs `UIApplicationMain`) |
| 2. Bundle | `aapt2 link` + `zip` the `.so` + `classes.dex` → `.apk` | lay out `MyApp.app/` (binary + `Info.plist`); `.ipa` = `Payload/MyApp.app` zipped |
| 3. Sign | `zipalign` + `apksigner` (debug keystore is fine for sideloading) | `codesign` / `rcodesign` (device only; Simulator needs none) |
| 4. Install | `adb install` | `xcrun simctl install` (sim) / `devicectl` (device) |

`build-android.sh` and `build-ios.sh` run steps 1–4 end to end.

## Local testing

There are four rungs here, and each proves something the one below it cannot.
Start at the top — it is free and catches most of what actually breaks.

| Rung | Cost | Proves |
|---|---|---|
| 1. `cargo check` per target | seconds, **any OS**, no SDK | the platform code *compiles* |
| 2. Synthetic-event harness | seconds, no hardware | the event pipeline dispatches |
| 3. Emulator / simulator | one-time SDK install | the app *boots and draws* |
| 4. Real device | a cable | timing, sensors, the actual digitizer |

Everything below is also available as a command that probes before it acts:

```sh
cargo run -p azul-doc -- mobile doctor            # what is installed, what is missing
cargo run -p azul-doc -- mobile install android   # plan, confirm, install
cargo run -p azul-doc -- mobile check             # rung 1, every target
cargo run -p azul-doc -- mobile emulator          # rung 3, headless
cargo run -p azul-doc -- mobile run android examples/azul-writer
```

`mobile install` prints a plan and waits for a `y` before running anything:
each step reports whether it is already satisfied, exactly what would run, or
why a human has to do it (`--yes` to skip the prompt in CI, `--dry-run` to only
ever print). It is idempotent, so a re-run after a failed download resumes
instead of starting over.

### 1. Cross-compiling is cheaper than it looks

**`cargo check` does not link.** That is the whole trick: it needs the Rust
`std` for the target and nothing else — no NDK, no iOS SDK, no cross C
toolchain, no `JAVA_HOME`. One `rustup target add` per triple and every mobile
backend compiles on whatever machine you happen to have:

```sh
rustup target add aarch64-apple-ios aarch64-apple-ios-sim aarch64-linux-android
bash scripts/mobile-check-all.sh          # or, per target:
cargo check -p azul-dll --lib --target aarch64-linux-android
```

This matters more than it sounds. Platform shells are `#[cfg]`-gated, so
**nothing in `shell2/android/`, `shell2/ios/`, `shell2/windows/` or
`shell2/linux/` is compiled by a normal `cargo build` on your machine** — an
editor will happily show you green squiggles over code the compiler has never
seen. A `use super::foo` that resolves on one platform and not another, a
struct literal missing a field added last week, a helper that exists only in
the desktop build: all of it is invisible until you check the target.

Run it before you commit platform code, not after.

### 2. Without any device at all

The synthetic-event harness drives the full input → dispatch → callback path
with no hardware, including pen, touch, gestures, scroll phases, IME
composition and gamepad state — see
[e2e-testing](../debugging/e2e-testing.md). This is the right place to pin
*behaviour*; the rungs below are for proving the platform glue underneath it
is real.

### 3a. Android emulator — no Android Studio

Everything comes from Homebrew and the headless `sdkmanager` — no Android
Studio, no Gradle:

```sh
cargo run -p azul-doc -- mobile install android
cargo run -p azul-doc -- mobile emulator
```

That is the whole thing. What it does, if you would rather type it:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Note the activity: the manifest declares `com.azul.app.AzulActivity`, a
`NativeActivity` subclass that constructs the gesture and accessibility bridges
in `onCreate`. Launching `android.app.NativeActivity` instead fails with
*"Activity class does not exist"*.

To *build* the APK you additionally need the NDK
(`sdkmanager "ndk;27.0.12077973"`) — that is the one large download, and it is
required for linking, not for checking. `mobile install android --no-ndk` skips
it when you only want to run a prebuilt APK.

> **What an emulator cannot tell you.** Azul's Android input bridges
> (`NativeTextBridge` for soft-keyboard text/IME/insets, `AzulGamepad` for
> controllers) are JNI entry points, and JNI is a *callee*. If the Java side
> that calls them is not in your APK, the emulator boots, draws, and exercises
> none of those paths — you will see a working window and conclude the input
> layer works. Check the APK actually ships the Java glue before drawing that
> conclusion.

### 3b. iOS Simulator

The simulator needs **Xcode** — not an Xcode *project* (you never need one of
those, see [above](#build--bundle-the-appipa-no-xcode-project)), but the app
itself, because `simctl` and the iOS SDK ship inside it. Command Line Tools
alone are not enough: `xcode-select -p` pointing at
`/Library/Developer/CommandLineTools` gives you `MacOSX.sdk` and no
`iphonesimulator` sysroot, so iOS can be `cargo check`ed but **not linked**.

```sh
cargo run -p azul-doc -- mobile install ios   # checks the above, installs what it can
cargo run -p azul-doc -- mobile simulator     # boots one and prints its UDID
```

`mobile install ios` cannot install Xcode for you — it is an App Store download
tied to an Apple ID — so that step is reported as yours, with the exact commands
(including `xcodes install --latest` if you want a pinned version). Everything
around it, the Rust targets and the simulator runtime, it does install.

By hand:

```sh
xcode-select -p                                   # must NOT be CommandLineTools
xcrun --sdk iphonesimulator --show-sdk-path       # must resolve

open -a Simulator
xcrun simctl install booted azul-maps.app
xcrun simctl launch --console booted <bundle-id>
```

Budget the disk: since Xcode 14 the simulator **runtime is a separate
download** from Xcode itself, so it is roughly 10 GB + 7–10 GB, not one number.

### 3c. Headless simulator automation with baguette

[`baguette`](https://github.com/tddworks/baguette) drives booted simulators
from a CLI — no Simulator.app window, no Xcode UI — which makes the iOS
simulator scriptable in CI the way `adb` already makes Android scriptable.

```sh
brew install baguette        # needs Xcode 26 (⇒ macOS 15.6+), Apple silicon
```

It is not a `simctl` wrapper: it links `SimulatorKit` / `CoreSimulator`
directly, which is what buys real gesture dispatch rather than synthesized
taps. The commands that matter for testing an azul app:

```sh
UDID=$(baguette list --json | jq -r '.[0].udid')
baguette boot --udid $UDID

# Input injection — the real thing, not a synthetic event
baguette tap    --udid $UDID --x 120 --y 300 --width 393 --height 852
baguette swipe  --udid $UDID --startX 40 --startY 400 --endX 350 --endY 400 \
                --width 393 --height 852
baguette pinch  --udid $UDID --cx 196 --cy 400 --startSpread 40 --endSpread 220 \
                --width 393 --height 852
baguette type   --udid $UDID --text "hello"
baguette key    --udid $UDID --code ArrowLeft --modifiers shift

# Assertions
baguette describe-ui --udid $UDID --output tree.json    # accessibility tree
baguette screenshot  --udid $UDID --output shot.png
baguette clipboard get --udid $UDID
baguette logs --udid $UDID --style ndjson --bundle-id <id>
```

Three of those are worth calling out because they map onto things azul
otherwise has no way to check on a device:

* **`describe-ui`** dumps the accessibility tree as JSON. That is a *structural
  assertion target*: azul builds that tree itself (see
  [accessibility](../system/accessibility.md)), so a diff against a golden file
  catches a11y regressions that a screenshot cannot see.
* **`clipboard get` / `sync`** reads the simulator's `UIPasteboard`, which is
  exactly what azul's iOS clipboard transport writes to. A copy in the app and
  a `clipboard get` outside it is a true end-to-end check of the typed-payload
  path — flavors and all.
* **`tap` / `swipe` / `pinch` / `type`** enter through UIKit, so they exercise
  the real `touchesBegan:`, `UIPencilInteraction`, `UIKeyInput` and gesture
  recognizers — not azul's own synthetic-event injection. Rung 2 proves the
  dispatch logic; this proves the *platform glue feeding it*.

For a scripted loop, `baguette serve --port 8421` exposes the same surface over
HTTP + WebSocket (`POST /simulators/:udid/input`,
`GET /simulators/:udid/describe-ui.json`, `WS …/stream`, `WS …/logs`), which is
usually easier to drive from a test harness than shelling out per gesture.

### 3d. One command: build, boot, install, launch, assert

`mobile run` is rungs 3a/3b end to end. It resolves the crate, builds it,
boots a device if one is not already up, installs, launches, waits for the
first frame, screenshots, and reports whether the engine actually started:

```sh
cargo run -p azul-doc -- mobile run android examples/azul-writer
cargo run -p azul-doc -- mobile run ios AzWriter --windowed
```

It exits non-zero when nothing in the device log came from the Rust side, or
when the log contains a native crash — so it works as a CI gate, not just as a
convenience.

`--e2e <scenario.json>` additionally replays a scenario against the device,
through `adb shell input` on Android and through baguette on iOS:

```sh
cargo run -p azul-doc -- mobile run android examples/azul-writer \
    --e2e e2e/op-scroll-to-position.json
```

**This is deliberately a subset, and it tells you which subset.** The e2e op
vocabulary is engine-internal: `mount` installs a DOM, `assert_response`
inspects the debug dispatcher's last reply, `snapshot_frame` reaches into the
frame cache. None of that is expressible from outside the process. What a host
driver *can* replay is the input half — taps, swipes, keys, text, screenshots —
plus structural assertions against the platform accessibility tree.

So every op is classified, and the ones the driver cannot honour are counted
and named:

```
==> host replay: bug_scroll_offsets_hit_test
  11 of 19 ops executed on the device
  7 op(s) a host driver cannot express:
    - assert_node_count x1
    - assert_response x2
    - mount x1
    …
  verdict: INCOMPLETE
```

A replay that dropped ops is never reported green. That matters more than it
looks: a passing test from a harness that silently did nothing is a false
statement about the device, and it is exactly how a platform-glue bug survives
a "green" suite. Run the engine-internal half with `azul-doc e2e`, which drives
the same dispatcher in-process; run this half to prove the `touchesBegan:` /
`GestureDetector` / `InputConnection` path that the in-process harness bypasses.

### 4. Real devices

An emulator will not tell you about digitizer sampling rate, pen tilt, Force
Touch stages, real sensor noise or thermal throttling. For anything timing- or
hardware-shaped, the device is the only answer — see the install steps
[above](#installing--debugging-the-built-app).

## See also

- [Realtime Media and Devices](../system/realtime-media.md) — camera/mic/sensors/gamepad/
  geolocation on mobile, and the permissions you declare.
- [Deploying to the web](web.md) — the WASM target, by comparison.
- [hello-world](../hello-world.md) — the app you're shipping.

---

### Doc Guide En Deploying Observability (`doc/guide/en/deploying/observability.md`)

---
slug: deploying/observability
title: Observability
language: en
canonical_slug: deploying/observability
audience: external
maturity: wip
guide_order: 233
topic_only: false
short_desc: Consent tiers, OTLP metrics and logs, crash reports, and the collector behind them
prerequisites: [hello-world, deploying/signing-updates]
tracked_files:
  - layout/src/telemetry/mod.rs
  - layout/src/telemetry/config.rs
  - layout/src/telemetry/metrics.rs
  - layout/src/telemetry/queue.rs
  - layout/src/telemetry/crash_mail.rs
  - layout/src/probe.rs
  - layout/src/dialogs/telemetry_consent.rs
  - dll/src/desktop/app.rs
last_generated_rev: c7631154b571a2467f2fee67c4e67e72cb29be83
generated_at: 2026-08-20T00:00:00Z
default-search-keys:
  - CallbackInfo
  - SysDialogType
  - UpdateSettings
  - StringPairVec
  - AppConfig
---

# Observability

A shipped desktop app answers "did 1.4.2 regress startup RAM?" from support
tickets, badly. Azul ships an OTLP client for the question: an in-process
metric registry, a structured log buffer, a disk queue that survives an
offline backend, and an uploader.

Nothing leaves the machine unless **two** keys turn:

1. you compile the `telemetry` feature in and configure an endpoint, and
2. the user selects a consent tier at or above the data being sent.

The default tier is `off`. Linking the feature in collects nothing.

## Turning it on

```toml
[dependencies]
azul = { version = "$VERSION", features = ["telemetry"] }
```

The app's identity comes from the `UpdateSettings` block that the updater
already reads — `app_name` becomes the OTLP service name and keys the queue
directory, `current_version` becomes the `version` label:

```rust,ignore
let mut app_config = AppConfig::create();
app_config.updates.app_name = "myapp".into();
app_config.updates.current_version = env!("CARGO_PKG_VERSION").into();
// Where SysDialogType::ReportProblem and manual crash reports are mailed.
// Unset, the report dialog saves to disk instead.
app_config.report_problem = Some("support@myapp.example".into()).into();
let app = App::create(RefAny::new(data), app_config);
```

`App::create` then does the wiring itself: `telemetry::init`, the panic hook,
one `app_sessions_started_total` tick, and the background uploader thread.
Recording is lock-light and safe on the UI thread; uploads never happen on
it.

The destination is environment or config, not code — a packager can point a
build at their own collector without a rebuild:

```bash
# Local debugging loop (the Grafana stack from layout/examples/telemetry-grafana):
AZ_OBSERVE=1 ./myapp

# Production / custom collector - the granular variables:
AZ_TELEMETRY_ENDPOINT=https://otlp.example.com \
AZ_TELEMETRY_TOKEN=<ingest token> \
AZ_TELEMETRY=metrics ./myapp
```

With no endpoint configured the client warns once and uploads nothing, rather
than buffering forever.

## Consent tiers

| Tier | Allows |
| --- | --- |
| `off` | nothing. The default, always |
| `crashes` | crash and panic reports |
| `metrics` | the above plus anonymous metrics and logs |
| `full` | the above plus serialized app state on a crash |

Four layers set the tier, most specific first:

1. `AZ_TELEMETRY` — CI, corporate lockdown, tests
2. `.azul/telemetryconfig.json` beside the executable — the packager/admin
   pin. `{"tier": "off"}` here also suppresses the consent dialog
3. `{config_dir}/{app-id}/telemetry.json` — this app's user choice
4. `{config_dir}/azul/telemetry.json` — "remember for all azul apps"

They are human-readable JSON deliberately: the config is part of the
transparency story.

### Asking the user

```rust,ignore
info.invoke_system_dialog(SysDialogType::TelemetryConsent);
```

The dialog lists **every instrument the app can record** — each engine metric
with a plain-language sentence, plus any app-defined ones — with a checkbox
per metric above the four signal switches (crashes, logs, metrics, app state
on crash). Saving applies immediately at runtime and persists; the "remember
for all azul apps" box writes the machine-wide
`{config_dir}/azul/config.json` channel default instead of this app's
override.

A tier downgrade retires the `client_id` through a deletion-request ping, so
the backend learns which install asked to be forgotten.

## What the engine records

Metrics carry exactly four labels — `version`, `channel`, `os`, `arch` —
enforced by the label set being a struct rather than a map. The random
`client_id` that makes "crash-free *users*" and adoption dedup possible rides
on **log records only**, never on a metric.

| Metric | Kind | Answers |
| --- | --- | --- |
| `app_sessions_started_total` | counter | the denominator of every release-health ratio |
| `app_crashes_total`, `app_panics_total` | counter | release health, per version |
| `app_startup_seconds` | histogram | "is startup slower since the update?" |
| `app_startup_rss_bytes` | histogram | the rollout gate: did RAM regress? |
| `app_rss_bytes`, `app_heap_bytes` | gauge | live memory |
| `app_frame_seconds{scope}` | histogram | frame cost by how much work it caused |
| `app_timer_frame_seconds` | histogram | the clock animations ride |
| `app_frame_relayout_scope_total{scope}` | counter | `repaint` vs `relayout` vs `regenerate_dom` mix |
| `app_slow_frames_total{scope}` | counter | frames past the slow threshold (32 ms default) |
| `app_phase_seconds{phase}` | histogram | per-phase timings from `Probe::span` |
| `app_update_check_total{result}`, `app_update_apply_total{result}` | counter | updater outcomes |

Every slow frame also emits a `WARN` log record naming the scope, the
duration and the document size, and the session's first one carries a
one-shot system-info snapshot.

`app_phase_seconds` is armed automatically: `telemetry::init` calls
`enable_probe_bridge()` whenever the resolved consent tier ships metrics, so
a consenting run fills the phase histogram with no further switches. (It
used to require a separate `enable_probe_bridge()` call - the histogram was
silently empty otherwise, and an empty histogram is indistinguishable from a
fast one.) `AZ_PROFILE` is unrelated to telemetry: it arms the same recorder
for LOCAL consumers - agent/e2e debugging, `AZ_PROFILE=cpu` dumps,
cross-frame phase diffs - and unknown tokens now warn with the valid list
instead of parsing to nothing.

## Your own metrics

From any callback, in any language binding:

```rust,ignore
info.record_counter("documents_opened_total".into(), 1, StringPairVec::from_vec(vec![]));
info.record_histogram("export_seconds".into(), elapsed, labels);
info.record_gauge("open_tabs".into(), tabs as f64, labels);
```

Labels are sanitized and capped — six keys, 64-character values — and every
distinct combination counts against the global series ceiling. Names must be
fixed strings, never user-derived: a metric name built from a filename is an
unbounded series. All three are no-ops unless telemetry is compiled in and
consented to, so they are safe to leave in the code path.

## Crashes

The panic hook counts the panic, buffers the message, and writes the queue
**to disk**. Release builds are `panic = "abort"`: the process dies the moment
the hooks return, so nothing can be deferred to a background thread. With an
OTLP endpoint configured the hook therefore ships the queued crash record
**synchronously** (a 3-second budget; whatever fails stays queued for the next
launch). Without an endpoint it re-spawns the app as the crash-reporter dialog
(below). With telemetry off — below the `crashes` tier — the plain "unexpected
fatal error" message box is the only surface (`AppConfig.enable_visual_panic_hook`,
on by default; suppressed under `AZ_E2E`), and it yields whenever telemetry
owns the crash, so a user sees exactly one crash surface.

Each crash record carries `crash.message`, `crash.location` (`file:line`),
`crash.scope` (the live probe-span path, so you know what the app was doing),
`crash.backtrace`, the document size and the `sys.*` snapshot. Paths are
scrubbed: `$HOME` becomes `~`, and rustc and registry paths collapse.

When a crashed process re-spawns itself with `AZ_CRASH_DUMP=<dump.json>`,
that invocation *is* the crash reporter: azul shows the dump in a CPU-rendered
dialog instead of starting the app. This happens on every crash-tier panic
without an endpoint — no crash contact is needed to *see* the report; only
its **Send** needs one. `AppConfig.report_problem` (the support mailbox
`SysDialogType::ReportProblem` mails to) arms that contact automatically when
the `crash-mail` transport is compiled in. A dump still queued on a later
launch — the reporter was closed, or crashed with the app — re-opens the
reporter alongside the app when it can send.

### Reports the user starts

`SysDialogType::ReportProblem` is the path that does not wait for a crash: a
message box with an optional screenshot of the current window and an optional
system-info block. The screenshot is captured in-process at invoke time, so it
shows the situation the user is complaining about. Nothing leaves the machine
before they press Send, and the report goes to `AppConfig.report_problem` — or
to disk when that is unset.

### Without a backend

For a deployment with no collector at all — tier `crashes`, metrics off, a
support mailbox — the `crash-mail` feature drains the dumps into one email
over plain SMTP, the dump as a JSON attachment and the user's message as the
body. It is manual by design: the app calls it from its own "the app crashed
last time, send a report?" dialog on the next launch.

## Where the data goes

Payloads are OTLP/HTTP **JSON** to `<endpoint>/v1/metrics` and
`<endpoint>/v1/logs` with a bearer token. A flush reports what happened to
each ping:

- `uploaded` — the collector took it.
- `retained` — unreachable or 5xx. The ping stays on disk and goes out next
  flush. This is the offline path working.
- `dropped` — a permanent 4xx, almost always a wrong token. Retrying a
  rejected payload forever is a worse failure than losing it.

The collector position is where authentication, per-tier policy and scrubbing
belong; Grafana belongs behind an identity proxy.

**Plan for an aggregating ingest proxy.** Four labels and no per-user label
means two clients on the same build write to the *same* series, each sending
its own cumulative totals — at the storage layer those interleave and look
like a counter that resets. `rate()` and `increase()` absorb resets, which is
why the queries below use them, but with a real fleet the arithmetic stops
being exact unless the proxy converts per-client cumulative payloads into
server-side aggregates first.

### A stack you can run

`layout/examples/telemetry-grafana/` is a complete loop — collector →
VictoriaMetrics + Loki → a provisioned Grafana dashboard — with a demo app
doing real work:

```bash
cd layout/examples/telemetry-grafana && ./run-stack.sh up
```

Its README covers the dashboard panels, the offline and panic paths, and a
query cookbook. Nothing in it is simulated; every histogram observation is a
real duration.

## Queries that gate a rollout

```promql
# panics per session, by version — do not raise rollout_percent until the new
# version is at or below the old one
sum by (version) (increase(app_panics_total[1h]))
  / sum by (version) (increase(app_sessions_started_total[1h]))

# startup p95 by version
histogram_quantile(0.95,
  sum by (version, le) (increase(app_startup_seconds_bucket[$__range])))
```

```logql
# every crash record, and one install's whole story
{service_name="myapp"} | event_kind="crash"
{service_name="myapp"} | client_id="…"
```

`increase(…[$__range])` rather than a sliding `rate()`: startup is observed
once per run, so a five-minute window is empty whenever no process started in
it, and a quantile over an all-zero rate is `NaN`.

## Environment variables

| Variable | Effect |
| --- | --- |
| `AZ_OBSERVE` | ONE-VAR local loop: `1`/`local` = metrics tier + `http://127.0.0.1:4318` + the local stack's token + 5s flushes; `<url>` targets another collector. Granular vars below override pieces. |
| `AZ_TELEMETRY` | consent tier: `off`, `crashes`, `metrics`, `full` |
| `AZ_TELEMETRY_ENDPOINT` | OTLP base URL; signal paths are appended |
| `AZ_TELEMETRY_TOKEN` | bearer token for the ingest endpoint |
| `AZ_TELEMETRY_FLUSH_SECS` | uploader flush interval |
| `AZ_TELEMETRY_CHANNEL` | the `channel` label (default `default`) |
| `AZ_TELEMETRY_CLIENT_ID` | pins the client id, for test determinism |
| `AZ_CRASH_DUMP` | set on the re-spawn; makes this run the crash reporter |
| `AZ_PROFILE` | also enables `Probe` recording, hence `app_phase_seconds` |

An unknown `AZ_TELEMETRY` value is rejected rather than guessed, so a typo is
visible instead of silently collecting or silently not.

## Cross-references

- [Signed Updates](signing-updates.md): the `UpdateSettings` block this page
  reads its identity from, and the release channels the metrics compare.
- [Profiling](../debugging/profiling.md): the same `Probe` spans, read
  locally instead of shipped.
- [Debugging](../debugging.md): overlays and structured logging on the
  developer's own machine.

---

### Doc Guide En Deploying Signing Updates (`doc/guide/en/deploying/signing-updates.md`)

---
slug: deploying/signing-updates
title: Signed Updates
language: en
canonical_slug: deploying/signing-updates
audience: external
maturity: wip
guide_order: 232
topic_only: false
short_desc: The update manifest, the signing chain, and the release script
prerequisites: [hello-world]
tracked_files:
  - layout/src/updater.rs
  - layout/src/dialogs/update_version.rs
  - layout/examples/verify_update_manifest.rs
  - scripts/sign-release.sh
last_generated_rev: 87d8e2882b7e7f84645a106a79d2fd5beacf256e
generated_at: 2026-08-18T22:28:46Z
default-search-keys:
  - AppConfig
  - UpdateSettings
  - UpdateMode
  - CallbackInfo
  - SysDialogType
---

# Signed Updates

Azul apps can check for, download and install their own updates. This page
covers the release side: the manifest you publish, the signature chain that
makes it trustworthy, and the script that produces both.

If you only want the in-app side — `CallbackInfo::check_for_updates`, the
`SysDialogType::UpdateVersion` dialog, staged rollouts — those read
`AppConfig.updates` and need no release tooling beyond a manifest URL.

## The shape of it

Your app is built with an `UpdateSettings`:

```rust
config.updates.manifest_url =
    Some("https://downloads.example.com/updates.json".into()).into();
config.updates.current_version = env!("CARGO_PKG_VERSION").into();
config.updates.app_name = "myapp".into();
config.updates.root_public_key = "RWQ…".into();   // see below
```

and you publish a manifest at that URL:

```json
{
  "latest": {
    "version": "2.0.0",
    "download_url": "https://downloads.example.com/myapp-2.0.0.bin",
    "changelog_md": "https://downloads.example.com/CHANGELOG.md",
    "digest": "sha256:49dba348…",
    "signature": "untrusted comment: …\nRUThI3d5…\ntrusted comment: …\n…\n",
    "signing_key_statement": "azul-signing-key-v1|pubkey=RWThI3d5…|expires=1818627834|generation=1",
    "signing_key_statement_sig": "untrusted comment: …\n…\n",
    "release_date": "2026-08-18T12:00:00Z",
    "slow": { "10": "…", "50": "…" }
  }
}
```

`release_date` and `slow` drive [staged rollouts](#staged-rollout) and are
optional. Everything else about delivery — where you host it, whether it is
S3 or a static file — is up to you; the client only needs to be able to GET
those two URLs.

## You do not have to publish that manifest

`manifest_url` is one URL, but the updater does not insist on one format.
It fetches whatever is there and works out what it got, so the smallest
useful deployment is a text file and the largest is the manifest above.

| What is at the URL | What you get |
| --- | --- |
| The manifest above | Everything: rollout, changelog, signature chain |
| A flat object `{"version": "2.0.0", "url": "…"}` | Version + download; same optional fields, no `latest` wrapper |
| A GitHub release | Version, asset, changelog, digest, signatures — see below |
| An OCI registry (`oci://…`) | Version, layer blob, and a digest pin for free |
| A bare version number, e.g. a `VERSION` file | Notification only: "there is a 2.0.0", with no download |

The lenience is about SHAPE, never about verification. Whatever the source,
the digest and the signature chain are checked identically, and a body that
matches none of these shapes is an error — an HTML error page from a
misconfigured host must never look like "you are up to date".

### GitHub releases

Point `manifest_url` at the repository. All of these mean the same thing:

```text
github://owner/repo
https://github.com/owner/repo
https://github.com/owner/repo/releases/latest
https://api.github.com/repos/owner/repo/releases/latest
```

The updater then maps a release onto a `ReleaseInfo`:

* `tag_name` is the version, with a leading `v` stripped.
* The release **body** is the changelog, inline — no second request, and it
  still works if the release page is unreachable.
* `published_at` seeds the [rollout ladder](#staged-rollout), so staged
  rollout works with no extra fields at all.
* The **asset** is chosen by matching this build's OS and architecture
  against the asset names (`…-x86_64-unknown-linux-musl.tar.gz`,
  `…_windows_amd64.zip`, `…-aarch64-apple-darwin.tar.gz` all work). Pin it
  explicitly with `github://owner/repo?asset=myapp-{version}-linux.bin` —
  `*` globs are allowed. If nothing matches this platform the updater
  reports the new version but **refuses to nominate a download**: handing
  someone an arbitrary binary is worse than telling them to fetch it.
* The **digest** comes from the asset's own `digest` field where GitHub
  provides one, and otherwise from a sibling `myapp.bin.sha256` or
  `SHA256SUMS` asset, matched by filename.
* The **signature chain** rides as sibling assets:
  `myapp.bin.minisig`, `signing-key-statement.txt` and
  `signing-key-statement.txt.minisig`. Upload the files
  `scripts/sign-release.sh` produced alongside the artifact and the chain
  verifies exactly as it does from a manifest.

Draft and pre-release entries are skipped. An unauthenticated client makes
one or two API calls per check, well inside GitHub's rate limit for a
per-user application.

A GitHub release with no signature assets is *unsigned*: fine if your app
does not pin a `root_public_key`, and a hard refusal if it does. That is the
intended behaviour — an app that has been told to require signatures should
not quietly accept a release that has none.

### OCI registries

If you already push artifacts to a container registry, that is an update
source:

```text
oci://ghcr.io/owner/app:2.0.0
oci://registry.example.com:5000/team/app        # a port is not a tag
```

* The registry's **token dance** is handled: an unauthenticated manifest
  request gets a `401` with a `WWW-Authenticate` challenge, the client
  fetches a (usually anonymous) pull token and retries. The same token is
  carried into the artifact download, since a blob request needs it too.
* A **multi-arch index** is followed to this platform's manifest. OCI names
  platforms `darwin/amd64` where Rust says `macos/x86_64`; that mapping is
  done for you. An index with nothing for your platform selects nothing
  rather than something arbitrary.
* The version comes from the `org.opencontainers.image.version` annotation,
  or from the tag when the reference names one. A `:latest` with no version
  annotation is an **error** — there is no honest answer to "which version
  is this", and reporting "up to date" would be a lie.
  `org.opencontainers.image.created` seeds the rollout ladder.
* Select the artifact layer with `?asset=` matched against the layer's media
  type or its `org.opencontainers.image.title` annotation; the first layer
  is the default.

The nice property here is that **the layer digest is the pin**. A registry
already content-addresses its blobs, so an OCI release is digest-verified by
construction — there is no checksum file to publish, forget, or trust. The
minisign chain still applies on top if you pin a root key; put the `.minisig`
and the statement in the manifest's annotations or ship them as extra layers.

## Why there are two keys

A single signing key is a bad trade: it has to live wherever your CI signs
builds, and if it leaks your only remedy is shipping a new binary to every
user, because the key they trust is compiled into the app they already have.

So the client trusts a **root key** that signs nothing but *statements*
about which signing key is currently valid:

```
root key ──signs──> signing-key statement ──names──> signing key ──signs──> artifact
```

The statement is a single line with no trailing newline:

```
azul-signing-key-v1|pubkey=<base64>|expires=<unix>|generation=<n>
```

* The **root secret key** lives offline and signs a statement about once a
  year. Its *public* half is compiled into your app.
* The **signing key** lives on the build machine and signs artifacts.
* `generation` is a rotation counter. Clients remember the highest one they
  have accepted and refuse anything lower, so a retired key stays retired
  even if someone replays an old statement. Rotating is: mint a new signing
  key, publish a statement with `generation` one higher. No new binary.
* `expires` bounds the damage of a leak you never noticed.

A manifest cannot name its own signing key — only a root-signed statement
can. That is the whole point: an attacker who controls your download server
and your manifest still cannot make a client install anything, because they
cannot produce a statement the root key signed.

Leaving `root_public_key` empty disables the chain and leaves only the
`digest` pin, which protects against corruption and a swapped file but not
against someone who can rewrite the manifest.

## Signing a release

```bash
# First run: mint the two key pairs, then STOP so you can put the root
# secret key somewhere safe before it has ever signed anything.
scripts/sign-release.sh --keys ./release-keys

# Then, per release:
scripts/sign-release.sh \
    --keys ./release-keys \
    --artifact ./target/release/myapp \
    --version 2.0.0 \
    --url https://downloads.example.com/myapp-2.0.0.bin \
    --changelog https://downloads.example.com/CHANGELOG.md \
    --out manifest.json
```

The script needs a minisign implementation. Either works:

```bash
cargo install rsign2          # recommended
apt install minisign          # or: brew install minisign
```

Before printing anything the script **verifies its own output with azul's
real client-side code**. Do not remove that step. A signature can be
perfectly valid to the tool that made it and still be refused by the app:

* **Prehashing.** The client accepts only prehashed signatures (`ED`), not
  the legacy form (`Ed`). `rsign2` always prehashes; some `minisign` builds
  need `-H`, which the script passes.
* **The statement is signed as exact bytes.** `echo` appends a newline and
  the signature then covers a string that is not the one in the manifest.
  The script uses `printf '%s'`.

Neither mistake is visible by inspection, and both produce a release that
looks fine until every client rejects it.

## Verifying before you publish

The same check runs standalone:

```bash
cargo run -p azul-layout --features updater --example verify_update_manifest -- \
    manifest.json ./myapp-2.0.0.bin RWQ…rootpubkey
```

It prints the resolved signing key, the generation, when the statement
expires and whether the artifact matches — and exits non-zero if a client
would refuse the release.

`--selftest` mints a throwaway hierarchy and walks the whole chain, which is
a quick way to see the byte formats without touching real keys:

```bash
cargo run -p azul-layout --features updater --example verify_update_manifest -- --selftest
```

## Testing the whole path locally

Serve the manifest and the artifact from a directory and point a drill at
them. This exercises exactly what a client does — check, changelog fetch,
resumable download, digest, signature chain:

```bash
cd release-dir && python3 -m http.server 8731 &

cargo run -p azul-layout --features updater,telemetry \
    --example telemetry_grafana -- \
    --update-manifest http://127.0.0.1:8731/manifest.json \
    --version 1.0.0 \
    --update-root-key RWQ…rootpubkey
```

```text
update: install=UserWritable effective_mode=SelfUpdate
update: 1.0.0 -> 2.0.0 available (manual mode)
update: staged AND VERIFIED …/staging/myapp-2.0.0.bin (4096 bytes, cached=false)
        — signature chain OK, key generation now 1
```

The same drill takes a GitHub repository, which is the quickest way to see
source resolution working against something real:

```bash
cargo run -p azul-layout --features updater,telemetry \
    --example telemetry_grafana -- \
    --update-manifest github://BurntSushi/ripgrep --version 0.1.0
```

```text
update: 0.1.0 -> 15.2.0 available (manual mode)
update: downloaded …/ripgrep-15.2.0-x86_64-unknown-linux-musl.tar.gz (2265718 bytes)
```

Then break it on purpose, which is the half worth running:

* Replace the artifact on the server after signing → refused, `digest
  mismatch`.
* Replace it *and* recompute the digest in the manifest → refused, `artifact
  signature invalid`, and the staged file is deleted rather than left behind.

## Install kinds and what the client will actually do

Verification is necessary but not sufficient: azul also refuses to
self-update where self-updating is wrong. `InstallKind::detect()` recognises
package-managed installs (dpkg/rpm, Homebrew, Flatpak/Snap, the Windows
Store, macOS `/Applications` bundles installed by a manager) and clamps the
mode to notify-only, so the app tells the user to update through the
mechanism that owns the files instead of overwriting them behind its back.

The machine-wide config can clamp further: `updates.autoupdate: false` in
`{config_dir}/azul/config.json` turns self-updating off for every azul app
on the machine, and `maintenance_window` (an RRULE subset) confines
unattended staging to a time window the machine's owner chose.

## Staged rollout

`release_date` plus an optional `slow` map spread a release over days
instead of shipping it to everyone at once:

```json
"release_date": "2026-08-18T12:00:00Z",
"slow": { "10": "2026-08-19T12:00:00Z", "50": "2026-08-20T12:00:00Z" }
```

Each client draws a persistent cohort bucket (0-99) once and keeps it
forever, so it stays in the same cohort for every release. Auto-updaters
open stage by stage; notify-only installs stay silent until the rollout
reaches 100 %. Clients still inside the gate report
`app_update_check_total{result="staggered"}`, so the reach of a rollout is a
dashboard query rather than a guess.

Without a `slow` map a default ladder applies (1 day → 10 %, 2 → 30 %,
3 → 50 %, 4 → 100 %). `"slow": "off"` ships to everyone immediately.

## Key hygiene, briefly

* The root secret key never touches CI. If it leaks, an attacker can appoint
  their own signing key and there is no in-band recovery — that is why it
  signs once a year and lives offline.
* The signing key on the build machine is the one you expect to rotate.
  Practise the rotation *before* you need it: bump `--generation`, publish,
  confirm clients accept the new statement.
* Keep the statement's `expires` shorter than the interval at which you are
  confident you would notice a compromise.

---

### Doc Guide En Deploying Web (`doc/guide/en/deploying/web.md`)

---
slug: deploying/web
title: Web
language: en
canonical_slug: deploying/web
audience: external
maturity: wip
guide_order: 230
topic_only: false
short_desc: Run the same azul binary as a web app — how the WASM lift works and how to ship it
prerequisites: [hello-world, headless/rendering]
tracked_files:
  - dll/src/web/config.rs
  - dll/src/web/mod.rs
  - dll/src/web/html_render.rs
  - dll/src/web/transpiler_remill.rs
  - docker/Dockerfile
default-search-keys:
  - AZ_BACKEND
  - web-prelift
  - run_web
  - Route
---

# Web

**The same binary that opens a desktop window can serve itself as a web app** —
no separate WASM build, no JS rewrite, no second codebase. You set one
environment variable and azul replaces the platform window with an HTTP server:

```sh
AZ_BACKEND=web://127.0.0.1:8080 ./my-azul-app
# open http://127.0.0.1:8080
```

Your `layout` callback runs on the server and is serialized to HTML + CSS; your
*event* callbacks (`on_click`, …) run in the browser as WebAssembly that azul
produces automatically from your compiled binary. This page explains **how that
works**, then **how to ship it**.

## What "lifting" is (and when each step runs)

azul does not ship a hand-written WASM build of itself. Instead it **lifts** the
native machine code of the library into WebAssembly with an embedded
[remill](https://github.com/lifting-bits/remill)-based lifter. That is what lets
one set of Rust callbacks run both server-side (desktop) and client-side
(browser). Lifting is slow — minutes for the whole framework — so it matters
*when* it happens. There are four distinct moments:

| Stage | When | What happens |
|---|---|---|
| **1. Docker build** (optional, once per release) | You build the base image | The whole **framework** is lifted to WASM and written to an on-disk cache (`AZ_LIFT_CACHE_DIR`). Slow, but done **once** and baked into the image. |
| **2. Server startup** | `./my-app` starts with `AZ_BACKEND=web://…` | Loads the warm cache. With a baked cache there is **nothing left to lift** for the framework; without one, this is where the multi-minute lift lands. |
| **3. First HTTP request** | A browser hits `/` | Only code **not** already cached is lifted — in practice just **your own** callbacks (seconds). Served at `/az/cb/{name}.{hash}.wasm`. |
| **4. In the browser** | The page runs | The client just **fetches and runs** that WASM. No lifting ever happens in the browser. |

The cache key is a hash of each function's **machine bytes**, not the app — so
the same framework function always maps to the same entry regardless of which
app is running. That is what makes a *shared, pre-warmed* cache possible: lift
the framework once in CI, ship it, and every app reuses it.

> **Portability note.** The lift output is architecture-neutral WASM, so the
> framework can be lifted on one CPU and served on another. azul's CI lifts on
> **aarch64** (where lifting is validated today) and bakes the result into an
> **x86_64** image.

## Quick start — run a demo

Once the base image is published (`ghcr.io/fschutt/azul-web-base`, built by
`.github/workflows/dockery.yml` on every website release):

```sh
docker run --rm -p 8080:8080 ghcr.io/fschutt/azul-web-base:0.2.0
# open http://localhost:8080
```

(Use `podman run …` — the command is identical.)

## Deploy your own app

Build your binary as usual, then base your container on the pre-lifted image so
your `docker build` only lifts **your** callbacks, not the framework:

```dockerfile
FROM ghcr.io/fschutt/azul-web-base:0.2.0

# Your azul app binary.
COPY target/release/my-app /usr/local/bin/my-app

# Bind the web backend. allow_public=1 is REQUIRED to bind a non-loopback
# address — the default refuses 0.0.0.0 because the server ships no auth.
# Add ?auth_token=… (and a reverse proxy, below) before exposing it publicly.
ENV AZ_BACKEND="web://0.0.0.0:8080?allow_public=1"
EXPOSE 8080
CMD ["/usr/local/bin/my-app"]
```

**Match your app's azul version to the base-image tag.** The cache is keyed by
machine bytes, so a different `libazul` build hashes differently and the cache
misses — pin both to the same version.

### Warming the cache yourself (`web-prelift`)

The base image is warmed by running an azul app once with the **prelift**
backend, which runs the exact server startup (Stages 1–2 above) and then exits
*before* it starts serving:

```sh
AZ_BACKEND=web-prelift://127.0.0.1:0 ./my-app   # lifts, populates the cache, exits
```

`web-prelift://` takes the same URL and query syntax as `web://`. Point it at
`AZ_LIFT_CACHE_DIR` and you get a warm cache you can copy into any image. This is
exactly what the base-image build does; you only need it if you build your own
variant.

## `AZ_BACKEND` URL format

- `web://127.0.0.1:8080` — localhost only (the safe default).
- `web://0.0.0.0:3000?allow_public=1` — all interfaces (requires `allow_public=1`).
- `web://[::1]:8080` — IPv6 loopback.
- `web://0.0.0.0:8443?tls_cert=cert.pem&tls_key=key.pem` — built-in TLS.
- `web-prelift://127.0.0.1:0` — warm the lift cache and exit (see above).

Query options: `allow_public`, `tls_cert`/`tls_key`, `auth_token`, `max_body`,
`max_connections`. The scheme is case-insensitive; a malformed value falls back
to the desktop shell.

## What runs where

| Runs on the **server** | Runs in the **browser** |
|---|---|
| `layout` callback (builds the `Dom`) | Browser layout + paint |
| Full CSS cascade (theme/viewport/container/lang all resolved) | `:hover` / `:focus` / `:active` (plain CSS, no round-trip) |
| HTML serialization + image/font collection | Event callbacks (`on_click`) as lifted WASM |
| Route matching | Reads the hydration payload into shared WASM memory |

The cascade is fully resolved server-side, so only interactive pseudo-states
ship as CSS. Event callbacks execute client-side against a hydrated `RefAny` in
shared WASM linear memory; mutations are read back by the loader and applied to
the DOM.

> **Maturity (WIP).** `on_click` → counter works end-to-end for `hello-world`.
> Broader event wiring (`mousemove`/`keydown`/…), full DOM diff/patch, and
> arbitrary hydration payloads are in progress. Complex apps fall back to
> server-side `POST /az/exec/{node_id}` execution.

## Routes

Routes use `:param` segments, passed via `AppConfig.routes`; without any, the
root layout callback becomes `/`:

```rust,ignore
let routes = vec![
    Route { pattern: "/users/:id".into(),     layout_callback: user_page() },
    Route { pattern: "/products/:sku".into(), layout_callback: product_page() },
];
```

Matching is exact → parameterized → `/` fallback. Assets are served under
`/az/` with content-hashed, `immutable` URLs (`/az/cb/…wasm`, `/az/img/…`,
`/az/font/…`), so a rebuild invalidates browser caches automatically.

## Production

The built-in server is a standalone HTTP listener — **no auth by default, no
load balancing**. For anything public, front it with a reverse proxy and bind to
loopback:

```text
[client] ──HTTPS──▶ [Caddy / nginx] ──HTTP──▶ [azul app on 127.0.0.1:8080]
```

```text
my-app.example.com {
    reverse_proxy 127.0.0.1:8080
    encode gzip
}
```

- **Memory**: the server holds all pre-rendered HTML, images, and fonts in RAM —
  a multi-route app with large images grows quickly.
- **Concurrency**: one OS thread per connection — fine for tens of users; for
  more, run several processes behind the proxy with shared external state.
- **Browser support**: plain HTML5 + ES5; the WASM path needs
  `WebAssembly.instantiateStreaming` + `BigInt` (Safari 14+, Chrome 85+,
  Firefox 78+ — evergreen browsers since 2020).

## Related

- [Headless rendering](../headless/rendering.md) — the render pipeline without a window

---

### Doc Guide En Dom Datasets (`doc/guide/en/dom/datasets.md`)

---
slug: dom/datasets
title: Datasets
language: en
canonical_slug: dom/datasets
audience: external
maturity: mature
guide_order: 52
topic_only: false
short_desc: Attaching state to a node for navigation and per-instance state
prerequisites: [dom]
tracked_files:
  - core/src/dom.rs
  - core/src/callbacks.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:53:30Z
default-search-keys:
  - Dom
  - NodeData
  - RefAny
  - DomNodeId
  - CallbackInfo
  - with_dataset
  - get_dataset
  - get_first_child
  - get_last_child
  - get_next_sibling
  - get_previous_sibling
  - get_hit_node
---

# Datasets

## Introduction

A dataset is a `RefAny` attached to a single node. You attach it with
`Dom::with_dataset(OptionRefAny)`. The framework doesn't read the
contents. It stores the `RefAny` on the node and hands it back to your
callback through `info.get_dataset(node_id)`.

That's the whole mechanism. Everything on this page follows from it.

The dataset is the slot for state that exists only because a specific
node exists. The cursor inside one text input. The expanded flag on one
tree row. A marker that tells a shared callback which button fired.
When `layout()` returns a fresh tree, the old node's dataset is dropped
along with the node. The dataset's lifetime is the node's lifetime.

There are two main use cases.

The first is **marker structs**: zero-field types whose only job is to
identify the node so a shared callback can dispatch on type. The second
is **per-instance state**: a struct of fields the callback reads and
mutates on every interaction.

For widgets that own resources expensive to recreate (a video decoder,
a GL texture, a websocket), the dataset is paired with a merge callback
so the framework can carry the `RefAny` across reconciliation instead
of dropping and rebuilding it. That mechanism has its own page. See
[Merge Callbacks](merge-callbacks.md).

## Attaching a dataset

The signature is `Dom::with_dataset(OptionRefAny) -> Dom`. Pass
`OptionRefAny::Some(RefAny::new(my_value))` to attach. Pass
`OptionRefAny::None` to leave the slot empty.

```rust,no_run
use azul::prelude::*;

struct EditorState { 
    text: String, 
    cursor: usize 
}

let state = RefAny::new(EditorState { 
    text: "hello".into(), 
    cursor: 0 
});

let _ = Dom::create_input_no_a11y("text".into(), "editor".into(), "hello".into())
    .with_dataset(OptionRefAny::Some(state));
```

`NodeData::set_dataset` is the underlying setter if you build
`NodeData` directly.

## Reading a dataset in a callback

Inside a callback, `info.get_hit_node()` returns the `DomNodeId` of the
node the event landed on. `info.get_dataset(node_id)` returns the
`RefAny` attached to that node, or `None`.

```rust,no_run
use azul::prelude::*;

struct EditorState { 
    text: String, 
    cursor: usize 
}

extern "C" fn on_keydown(_unused: RefAny, mut info: CallbackInfo) -> Update {
    let hit = info.get_hit_node();
    let mut ds = match info.get_dataset(hit) {
        Some(d) => d,
        None => return Update::DoNothing,
    };
    let mut state = match ds.downcast_mut::<EditorState>() {
        Some(s) => s,
        None => return Update::DoNothing,
    };
    state.text.push_str("...");
    Update::RefreshDom
}
```

Borrow rules follow `RefAny`. A `downcast_ref` blocks a `downcast_mut`.
Drop the guard before calling anything that may borrow the same
dataset.

## Marker structs

A dataset doesn't have to carry data. A zero-field struct works as a
type-level tag. One callback handles many nodes. The dataset's *type*
tells the callback which node fired.

```rust,no_run
use azul::prelude::*;

struct SaveButtonMarker;
struct CancelButtonMarker;

fn dialog_buttons() -> Dom {
    Dom::create_div()
        .with_child(
            Dom::create_button_no_a11y("Save".into())
                .with_dataset(OptionRefAny::Some(RefAny::new(SaveButtonMarker)))
                .with_callback(EventFilter::Hover(HoverEventFilter::MouseUp),
                               RefAny::new(()), on_dialog_click))
        .with_child(
            Dom::create_button_no_a11y("Cancel".into())
                .with_dataset(OptionRefAny::Some(RefAny::new(CancelButtonMarker)))
                .with_callback(EventFilter::Hover(HoverEventFilter::MouseUp),
                               RefAny::new(()), on_dialog_click))
}

extern "C" 
fn on_dialog_click(_unused: RefAny, mut info: CallbackInfo) -> Update {
    let hit = info.get_hit_node();
    let mut ds = match info.get_dataset(hit) {
        Some(d) => d,
        None => return Update::DoNothing,
    };
    if ds.downcast_ref::<SaveButtonMarker>().is_some() {
        // Save path.
    } else if ds.downcast_ref::<CancelButtonMarker>().is_some() {
        // Cancel path.
    }
    Update::RefreshDom
}
```

Both buttons share `on_dialog_click`. Dispatch happens on the dataset
type. There's no string match on a class name. There's no second
hit-test pass.

A marker can also carry fields. That's how a single callback handles a
whole table.

```rust,no_run
use azul::prelude::*;

#[derive(Debug)]
struct RowMarker {
    row_id: u64,
    column: ColumnKind,
}

#[derive(Debug, Copy, Clone)]
enum ColumnKind { Name, Email, Avatar, DeleteButton }

fn row(row_id: u64, kind: ColumnKind, label: &str) -> Dom {
    Dom::create_td()
        .with_child(Dom::create_text_do_not_use_without_block_level_wrapper(label))
        .with_dataset(OptionRefAny::Some(RefAny::new(RowMarker { row_id, column: kind })))
        .with_callback(EventFilter::Hover(HoverEventFilter::MouseUp),
                       RefAny::new(()), on_cell_click)
}

extern "C" 
fn on_cell_click(_unused: RefAny, mut info: CallbackInfo) -> Update {
    let hit = info.get_hit_node();
    let mut ds = match info.get_dataset(hit) {
        Some(d) => d,
        None => return Update::DoNothing,
    };
    let marker = match ds.downcast_ref::<RowMarker>() {
        Some(m) => m,
        None => return Update::DoNothing,
    };
    let _ = marker.row_id;
    let _ = marker.column;
    Update::RefreshDom
}
```

`marker.row_id` and `marker.column` identify the cell directly. No tree
walk. No DOM query.

## Walking the tree from a hit node

`CallbackInfo` exposes a small set of navigators. Each takes a
`DomNodeId` and returns `Option<DomNodeId>`. They read the styled-DOM
hierarchy.

```rust,ignore
let hit = info.get_hit_node();

let parent = info.get_parent(hit);
let next   = info.get_next_sibling(hit);
let prev   = info.get_previous_sibling(hit);
let first  = info.get_first_child(hit);
let last   = info.get_last_child(hit);
```

`info.get_dataset(other_node_id)` works against any node, not just the
hit node. A click on a child cell can read the parent row's dataset
without any application-side mapping.

`info.get_focused_node()` returns the node that currently holds
keyboard focus, useful for keyboard-driven callbacks where there's no
mouse hit.

## Datasets are rebuilt every frame

Each `layout()` call returns a fresh `Dom`. Each `with_dataset` call
inside that `layout()` builds a fresh `RefAny`. The previous tree's
nodes (and their datasets) get dropped.

This has consequences.

Anything a callback writes into a dataset is gone after the next
`layout()` unless the callback also writes it back into the
application-level `RefAny`. The dataset is short-term scratch. The
application data is the system of record.

Marker structs are cheap. A zero-field marker has no destructor body.
The allocation cost is the price of attaching it.

Heavy resources don't belong in a dataset that gets rebuilt. Use a
merge callback to carry them across reconciliation. See
[Merge Callbacks](merge-callbacks.md).

There's also a related pattern called the **double update**. A text
input callback writes the new character to the application data and
also writes it directly into the node's dataset, so the input shows
the latest keystroke before the next `layout()` runs. That's covered
in [Text Input and Selection](../events/text-input.md).

## What CallbackInfo exposes for dataset work

The handful of methods that matter on this page:

- `info.get_hit_node()` returns the `DomNodeId` of the node the event
  landed on.
- `info.get_dataset(node_id)` returns the dataset `RefAny` attached to
  any node, or `None`.
- `info.get_focused_node()` returns the focused node id when there is
  one.
- `info.get_parent` / `get_next_sibling` / `get_previous_sibling` /
  `get_first_child` / `get_last_child` walk the styled-DOM hierarchy.
- `info.get_string_contents(node_id)` returns the node's resolved text
  content, useful for reading what was typed into an editable node.

Drop any active dataset borrow before calling another method that
might re-borrow the same `RefAny`.

---

### Doc Guide En Dom Merge Callbacks (`doc/guide/en/dom/merge-callbacks.md`)

---
slug: dom/merge-callbacks
title: Merge Callbacks
language: en
canonical_slug: dom/merge-callbacks
audience: external
maturity: mature
guide_order: 53
topic_only: false
short_desc: How widgets keep heavy resources across a layout rebuild
prerequisites: [dom, dom/datasets, dom/reconciliation]
tracked_files:
  - core/src/dom.rs
  - core/src/diff.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:53:30Z
default-search-keys:
  - Dom
  - RefAny
  - DatasetMergeCallback
  - DatasetMergeCallbackType
  - with_merge_callback
  - Update
---

# Merge Callbacks

## Introduction

A merge callback lets a widget keep heavy resources alive across a layout rebuild. The framework matches each new node against the previous frame's tree and, before dropping the old dataset, hands both the new and old `RefAny` to your merge function so you can move whatever you want to keep into the new one.

## The problem

A [dataset](datasets.md) is rebuilt every time `layout()` runs. For a marker struct that's free. For an FFmpeg encoder it's a disaster.

Imagine a video pane whose dataset holds an open encoder, a GL texture, and a frame counter. The user toggles a checkbox somewhere far up the tree. `layout()` runs again. The pane appears in the new tree with a fresh dataset value. The old dataset drops. The encoder closes. The texture is freed. The next frame reopens everything.

That isn't acceptable for resources you can't recreate cheaply.

## The fix

A merge callback is a function attached to a node that gets to claim resources from the previous frame's dataset before that dataset is dropped.

```rust,ignore
pub type DatasetMergeCallbackType =
    extern "C" fn(new_data: RefAny, old_data: RefAny) -> RefAny;
```

You receive the new dataset (built fresh by `layout()` this frame) and the old dataset (the one from the previous frame). You move whatever you want to keep from `old` into `new`. You return the merged value. The framework installs it on the new node. Whatever you didn't take drops with the old `RefAny`.

This is a `Reconcile()` in the Kubernetes sense. The new value is the desired state. The old value is the actual state. The callback brings the world into alignment.

```text
              layout()  →  fresh Dom (desired state)
                                  │
                                  ▼
                         ┌─────────────────┐
                         │   diff pass     │
                         └─────────────────┘
                                  │
                  matched node?   │  no  → drop old, install new
                                  │ yes
                                  ▼
                      ┌────────────────────────┐
                      │ merge_fn(new, old) → m │
                      └────────────────────────┘
                                  │
                                  ▼
                         install m on new node
```

## When it fires

The callback runs during reconciliation. The conditions are strict.

- The new node has a merge callback registered via `with_merge_callback(...)`.
- Both the old node and the new node have a dataset attached.
- The diff matched the two nodes (Stable or Moved). Created and Destroyed nodes don't qualify.

If any of those fails, nothing happens. The new dataset stays as `layout()` built it. The old one drops normally.

The matching rules belong to the diff. A node survives a reorder when it carries a stable identity - set an `AttributeType::Id` on it, e.g. with `with_id(...)`. Without one the diff matches structurally, by logical position, which only holds while the sibling order does. See [Reconciliation](reconciliation.md) for how that works.

## Worked example: a video encoder

Take a video pane backed by FFmpeg. The application data describes the desired pipeline. The widget owns the live encoder and a GL texture. The user can change the bitrate, toggle an overlay, or switch the source path.

Two structs:

```rust,ignore
use azul::prelude::*;

#[derive(Clone, PartialEq)]
pub struct VideoConfig {
    pub source_path: String,
    pub width: u32, pub height: u32,
    pub codec: String,
    pub bitrate_kbps: u32,
    pub overlay_text: Option<String>,
}

pub struct VideoPaneState {
    pub config: VideoConfig,
    pub encoder: Option<FfmpegEncoder>,
    pub gl_texture: Option<GlTexture>,
    pub last_frame_idx: u64,
}
# pub struct FfmpegEncoder; pub struct GlTexture;
# impl FfmpegEncoder {
#     pub fn open(_: &VideoConfig) -> Self { Self }
#     pub fn reconfigure(&mut self, _: &VideoConfig) {}
# }
```

`VideoConfig` is what the application stores. `VideoPaneState` is widget-owned. The application never sees the encoder.

The merge function does three things. It takes the encoder out of the old state. It checks whether the new config is compatible. If yes, it reconfigures and keeps the encoder; if no, it lets the old encoder drop.

```rust,ignore
extern "C" fn reconcile_video(new_data: RefAny, old_data: RefAny) -> RefAny {
    let new = new_data.clone();
    let mut new_state = match new.downcast_mut::<VideoPaneState>() {
        Some(s) => s,
        None => return new_data,
    };
    let old = old_data.clone();
    let mut old_state = match old.downcast_mut::<VideoPaneState>() {
        Some(s) => s,
        None => return new_data,
    };

    if let Some(mut enc) = old_state.encoder.take() {
        if encoder_compatible(&new_state.config, &old_state.config) {
            enc.reconfigure(&new_state.config);
            new_state.encoder = Some(enc);
        }
    }
    if let Some(tex) = old_state.gl_texture.take() {
        if texture_compatible(&new_state.config, &old_state.config) {
            new_state.gl_texture = Some(tex);
        }
    }
    new_state.last_frame_idx = old_state.last_frame_idx;

    drop(new_state);
    drop(old_state);
    new
}

fn encoder_compatible(a: &VideoConfig, b: &VideoConfig) -> bool {
    a.source_path == b.source_path && a.codec == b.codec
}
fn texture_compatible(a: &VideoConfig, b: &VideoConfig) -> bool {
    a.width == b.width && a.height == b.height
}
```

Building the widget is straightforward. Each frame `layout()` produces a `VideoPaneState` with the new config and empty resource slots. The merge callback fills the slots from the previous frame.

```rust,ignore
pub fn video_pane(config: VideoConfig) -> Dom {
    let key = stable_key_for(&config.source_path);
    let state = RefAny::new(VideoPaneState {
        config,
        encoder: None,
        gl_texture: None,
        last_frame_idx: 0,
    });
    Dom::create_div()
        .with_class("video-pane".into())
        .with_dataset(OptionRefAny::Some(state))
        .with_merge_callback(reconcile_video)
        // The reconciler matches nodes by identity, and an explicit id is the
        // stable half of it - give the node one when its position can move.
        .with_id(key)
}
# fn stable_key_for(_: &str) -> u64 { 0 }
```

What happens on a `RefreshDom`:

1. The parent re-renders.
2. `layout()` builds a fresh `VideoPaneState` with `encoder: None` and `gl_texture: None`.
3. The diff matches old and new by key.
4. `reconcile_video` runs. The encoder moves over if the source and codec are unchanged. The texture moves over if the resolution is unchanged.
5. The new node now owns the live encoder. The old `VideoPaneState` drops empty.

If the user navigates away and the pane disappears from the new tree, the diff classifies the old node as destroyed. No callback fires. The old `VideoPaneState` drops normally. `Drop` on `FfmpegEncoder` and `GlTexture` releases the resources.

## Designing a merge function

A few rules.

Default to the new value. If you can't downcast, return `new_data` as-is. The new node ends up with whatever `layout()` built. That matches the behaviour of having no merge callback at all.

Use `Option<T>` for resources you might want to move. `Option::take()` is what transfers ownership. Whatever you don't take drops with the old `RefAny`.

Compare configs before reusing a resource. The new dataset has the desired config. The old dataset has the actual config plus the resource that was built for it. If the configs disagree, the resource is stale. Drop it and let the widget rebuild on the next render.

The clones the framework hands you are shallow. Heavy fields stay in place until you call `take()`.

## When to use this versus app-state RefAny

Putting expensive resources on the application's data model works if the resource is a singleton. One main viewer on the home screen is fine.

Merge callbacks are the right tool when the widget instance defines the lifetime. A video pane inside a search-result row is the canonical case. The application data has search results. It doesn't have "the encoder for the row that used to be at index 4". The widget's presence in the tree is what should keep the encoder alive.

Merge callbacks are also right when there are many instances created dynamically. Without them, you'd maintain a `HashMap<WidgetId, VideoPaneState>` on the application side that mirrors the DOM. The dataset slot is that hashmap, indexed by the diff key, freed when the node disappears.

A useful split:

- Application data lives in a `RefAny` at app construction. `layout()` reads it to decide what to render.
- Widget-instance state lives on the widget's root node as a dataset. Add a merge callback if the state owns expensive resources.

---

### Doc Guide En Dom Reconciliation (`doc/guide/en/dom/reconciliation.md`)

---
slug: dom/reconciliation
title: Reconciliation
language: en
canonical_slug: dom/reconciliation
audience: external
maturity: mature
guide_order: 51
topic_only: false
short_desc: Diffing, restyle scope, and damage-rect repaint
prerequisites: [dom]
tracked_files:
  - core/src/diff.rs
  - core/src/styled_dom.rs
  - css/src/props/property.rs
  - layout/src/cpurender.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:53:30Z
default-search-keys:
  - Dom
  - Update
  - AttributeType
  - with_merge_callback
  - DatasetMergeCallback
  - EventFilter
---

# Reconciliation

## Introduction

Azul rebuilds the `Dom` from scratch on every `Update::RefreshDom`. The
tree returned by `layout()` is frozen. The next state change runs
`layout()` again and produces a new tree. The diff is what makes that
affordable.

The framework matches each new node against the most plausible old node,
classifies what changed, and picks the cheapest path forward. A hover
highlight on one button repaints one rectangle. Tree size doesn't matter.

## Matching new to old

Every node in the new tree is one of four things:

- **Stable**. Matched against an old node at the same logical position.
  Old node id maps to new node id. Merge callbacks run. Focus and scroll
  state migrate.
- **Moved**. Matched against an old node at a different parent or
  sibling position. Same migration treatment as Stable.
- **Created**. No match. A Mount event fires.
- **Destroyed**. The old tree had a node here, the new tree doesn't. An
  Unmount event fires. The dataset drops.

For trees with stable sibling order, structural matching works without
any help. The third `<li>` inside a `<div id="notes">` matches its
counterpart in the old tree. Its text content can change and it'll
still match.

For trees whose order can change (sortable lists, drag-and-drop,
virtualised scrolling), structural matching loses cursor position,
focus, and dataset state when items reorder. Those cases need a stable
identity. Set an `AttributeType::Id` on the node so the diff can match
across the reorder.

## What does and doesn't trigger work

Different changes cost different amounts of work. The framework
classifies each property change so it can pick the cheapest correct
path:

- **Repaint only**. Color, background, opacity, transform, caret
  styling, object-fit, clip path. No layout pass runs. The display
  list updates and the affected pixels re-rasterise.
- **Inline reshape**. Font-style flip, vertical-align change inside a
  paragraph, letter-spacing update. Only the paragraph's text reflows.
  Block-level siblings stay where they are.
- **Local resize**. Border-width tweak, scrollbar gutter, padding
  inside a text container. The node's size recomputes. The parent may
  reposition subsequent siblings. No recursive relayout.
- **Full subtree relayout**. Flips of `display`, `position`, `float`.
  Anything that changes the formatting context.

A `:hover` that only changes `background-color` lands in the repaint-
only bucket. A change to `display: flex` triggers a full subtree
relayout. The framework picks the right tier automatically.

Pseudo-class state changes (`:hover`, `:focus`, `:active`) go through
the same classification. A hover that flips just the background never
runs layout.

## Lazy paint

Even after restyle decides "repaint only", azul doesn't paint the whole
window. It compares the new display list against the previous one and
returns the rectangles that need re-rasterising. Everything outside
those rectangles keeps the previous frame's pixels.

For a hover-state change on a button, this is constant-time work. The
renderer rasterises maybe 100 by 30 pixels. The rest of the frame is a
straight blit from the previous one.

A grow-only window resize feeds a specialised producer. Just the new
right strip and bottom strip get rasterised. The existing pixels stay.

## Per-frame flow

1. `layout()` returns a fresh `Dom`.
2. The framework matches new nodes against old. Lifecycle events
   (Mount, Unmount, Update) fire.
3. Restyle promotes the changes to the smallest sufficient scope. If
   nothing layout-affecting changed, no layout pass runs.
4. The display list rebuilds for the parts that changed.
5. The renderer compares display lists and produces damage rects.
6. Only those rects get rasterised.

For a hover-state change on a button, steps 3 to 5 are constant-time.
Step 6 paints a single small rectangle.

## Merge callbacks across the diff

For Stable and Moved nodes, the merge callback (if registered) runs
during the diff. It receives the old and new `RefAny` values. Heavy
resources (a video decoder, a GL texture, the cursor inside a focused
input) can move from the old dataset to the new one before the old
tree drops. See [Merge Callbacks](merge-callbacks.md) for the protocol.

For Created nodes, the Mount lifecycle event fires (no merge runs).
For Destroyed nodes, the dataset drops via `Drop` in the natural way
and the Unmount event fires.

## Internals

The full algorithm covers matching tiers, the property-change bitmask,
the relayout-scope enum and damage-rect coalescing.

---

### Doc Guide En Dom Virtual Views (`doc/guide/en/dom/virtual-views.md`)

---
slug: dom/virtual-views
title: Virtual Views
language: en
canonical_slug: dom/virtual-views
audience: external
maturity: mature
guide_order: 54
topic_only: false
short_desc: A node that materialises lazily, for infinite lists and embedded sub-DOMs
prerequisites: [dom]
tracked_files:
  - core/src/dom.rs
  - core/src/callbacks.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:53:30Z
default-search-keys:
  - Dom
  - NodeType
  - VirtualViewCallback
  - VirtualViewCallbackInfo
  - VirtualViewCallbackReason
  - VirtualViewReturn
  - EdgeType
  - RefAny
  - LogicalPosition
---

# Virtual Views

## Introduction

A `VirtualView` is a single node. Its inner content comes from a separate
callback. That callback only runs when needed.

It's azul's iframe-equivalent. Use it when the inner content would be too
expensive to build every frame. Common cases:

- An infinite list.
- A 100,000-row table.
- An editor with heavy decoration layers.
- A panel that's logically independent from its surroundings.

The outer `Dom` containing the `VirtualView` is built inside `layout()` like
any other node. The inside isn't built until the framework calls the
callback.

```rust,no_run
use azul::prelude::*;

struct ListData { 
    items: Vec<String> 
}

extern "C" 
fn render_list(mut data: RefAny, info: VirtualViewCallbackInfo) -> VirtualViewReturn {
    let d = match data.downcast_ref::<ListData>() { 
        Some(d) => d, 
        None => return VirtualViewReturn::default() 
    };
    
    let dom: Dom = d.items.iter()
        .map(|s| Dom::create_li_with_text(s.clone()))
        .collect();
    
    let row_h = 24.0_f32;
    let total = LogicalSize::create(
        info.bounds.logical_size.width, 
        row_h * d.items.len() as f32
    );
    
    VirtualViewReturn::with_dom(
        dom, total, 
        LogicalPosition::zero(), total, 
        LogicalPosition::zero()
    )
}

let list_state = RefAny::new(ListData { 
    items: vec!["A".into(), "B".into()] 
});
let cb = VirtualViewCallback { 
    cb: render_list, 
    ctx: OptionRefAny::None 
};
let _ = Dom::create_virtual_view(list_state, cb);
```

## Two coordinate systems: rendered vs virtual

The key idea is that the rendered size and the virtual size are separate
numbers. That's what lets a scrollbar pretend to span 30,000 rows while
only 30 rows actually live in the DOM.

```
┌───────────────────────────────────┐
│         Virtual Scroll Frame      │  ← virtual_scroll_size
│                                   │    (infinite scroll: total logical height,
│  ┌─────────────────────────────┐  │     may be larger than actual content)
│  │       Scroll Frame          │  │  ← content_size (overflow_content_size)
│  │    (actual content area)    │  │    (total height of rendered children)
│  │                             │  │
│  │  ┌───────────────────────┐  │  │
│  │  │    Screen Bounds      │  │  │  ← scroll clip size (= container inner size)
│  │  │   (visible window)    │  │  │    This is what the user sees.
│  │  │                       │  │  │
│  │  │   ← scroll offset →   │  │  │    Scroll offset shifts which part
│  │  │                       │  │  │    of content_size is visible in
│  │  └───────────────────────┘  │  │    the clip rect.
│  │                             │  │
│  └─────────────────────────────┘  │
│                                   │
└───────────────────────────────────┘
```

`VirtualViewReturn` carries two pairs:

- `scroll_size` and `scroll_offset` describe the actual rendered content.
  This is the box of DOM you're handing back. `scroll_offset` is where it
  sits in virtual coordinates.

- `virtual_scroll_size` and `virtual_scroll_offset` describe what the view
  pretends to be. `virtual_scroll_size` is the size the scrollbar
  represents. `virtual_scroll_offset` is usually `LogicalPosition::zero()`.

If every row is materialised, the rendered values match the virtual values.
The abstraction collapses to a plain scrollable subtree.

For a virtualised slice, say rows 10..30 of a million-row table:

- `scroll_size` is `(width, 20 * row_height)`. That's the 20 actual rows.
- `scroll_offset` is `(0, 10 * row_height)`. The rendered rows start at
  y = 10 × row_height in virtual coordinates.
- `virtual_scroll_size` is `(width, 1_000_000 * row_height)`. The
  scrollbar represents the whole table.
- `virtual_scroll_offset` is `(0, 0)`.

The framework paints the scrollbar from `virtual_scroll_size`. It clips the
rendered DOM to a viewport at `scroll_offset` of size `scroll_size`. If the
user scrolls outside the rendered window, it re-invokes the callback.

## Why the callback was invoked

Each invocation carries a `VirtualViewCallbackReason`. The variants are
defined in `core/src/callbacks.rs`:

- `InitialRender`. The first time the `VirtualView` appears. You always
  return content here.

- `DomRecreated`. The parent tree was rebuilt from scratch. The cached
  subtree was thrown away. The callback has to rebuild.

- `BoundsExpanded`. The window grew. The view's bounds now exceed its
  previous `scroll_size`. Time to enlarge the rendered content. This fires
  once per expansion, not on every resize tick.

- `EdgeScrolled(EdgeType)`. The user scrolled within approximately
  200 px of one of the four edges of the rendered content. Time to
  lazy-load more rows. `EdgeType` is one of `Top`, `Bottom`, `Left`,
  `Right`. Fires once per edge approach. The flag clears once the scroll
  moves away.

- `ScrollBeyondContent`. A programmatic scroll (e.g. `set_scroll_position`)
  jumped the offset past the rendered `scroll_size`.

The callback does not fire on small resizes that stay inside the rendered
`scroll_size`. It does not fire when the window shrinks. It does not fire
on parent re-renders that don't recreate the parent DOM. That's the
optimisation.

## Returning OptionDom::None

Sometimes the existing DOM is fine. The user scrolled, but stayed inside
the already-rendered area. Or the parent re-rendered without invalidating
the subtree.

Set `dom: OptionDom::None` in the return value. The previous DOM stays in
place. Only the scroll bounds are updated.

```rust,ignore
fn render_table(data: &mut TableData, info: VirtualViewCallbackInfo) -> VirtualViewReturn {
    if data.already_rendered_area_covers(info.scroll_offset, info.bounds.logical_size) {
        return VirtualViewReturn {
            dom: OptionDom::None,                  // keep current DOM
            scroll_size:  data.current_scroll_size,
            scroll_offset: data.current_scroll_offset,
            virtual_scroll_size: data.virtual_size,
            virtual_scroll_offset: LogicalPosition::zero(),
        };
    }
    let new_dom = data.render_more_rows(info.scroll_offset, info.bounds.logical_size);
    /* ... */
    # VirtualViewReturn::default()
}
```

`VirtualViewReturn::keep_current(...)` is a shortcut for the same return
shape.

This is the cheapest re-invocation. The most expensive ones are
`InitialRender` and `DomRecreated`, since both rebuild from nothing.

## A virtualised table, end to end

```
/* Detailed source-code truncated for AI context efficiency. */
```

The user sees a scrollbar that represents all million rows. The DOM holds
about 25 row nodes. The callback runs only when the user scrolls near an
edge, the parent rebuilds, or the window grows. It doesn't run every
frame.

## Pairing with merge callbacks

The data parameter on a `VirtualView` is a `RefAny`. The framework keeps
it alive across re-renders of the parent DOM. The inner `Dom` returned by
the callback is rebuilt each time the callback fires.

If the inner content owns expensive resources (per-row decoders, GL
textures), pair each row's root with [`with_dataset(...)`](datasets.md)
and a [merge callback](merge-callbacks.md). That way the resources
transfer when the table is re-rendered.

The outer `RefAny` is also a good place to keep live state: what was
rendered last time, the cached fetcher, in-flight requests. The callback
can look at that state before deciding whether to return `OptionDom::None`.

---

### Doc Guide En Events Callbacks (`doc/guide/en/events/callbacks.md`)

---
slug: events/callbacks
title: Callbacks
language: en
canonical_slug: events/callbacks
audience: external
maturity: mature
guide_order: 61
topic_only: false
short_desc: What CallbackInfo exposes — state, DOM mutation, focus, async work
prerequisites: [dom]
tracked_files:
  - core/src/callbacks.rs
  - layout/src/callbacks.rs
default-search-keys:
  - CallbackInfo
  - RefAny
  - Update
  - EventFilter
  - FocusTarget
  - KeyboardState
  - MouseState
  - DomNodeId
---

# Callbacks

[DOM](../dom.md) showed how to attach a callback to a node. The other
side of the wire — what a callback receives, can read, can change,
and returns — is what this page documents.

## The callback signature

Every callback the framework invokes has the same C-compatible
signature:

```rust,ignore
extern "C" fn(data: RefAny, info: CallbackInfo) -> Update
```

- `data` is the `RefAny` you passed to `with_callback`. Downcast it
  to your concrete type to read or mutate application state.
- `info` is a borrowed view into the framework's frame state — the
  hit node, the current input state, the layout result, and a
  handful of dispatch helpers.
- `Update` tells the framework what to do next: nothing, re-run
  `layout()` for this window, or re-run for every window.

```rust,no_run
use azul::prelude::*;

struct Counter { value: i64 }

extern "C" fn on_click(mut data: RefAny, _info: CallbackInfo) -> Update {
    let mut c = match data.downcast_mut::<Counter>() {
        Some(c) => c,
        None => return Update::DoNothing,
    };
    c.value += 1;
    Update::RefreshDom
}
```

The `extern "C"` is mandatory. Callbacks are C function pointers,
which is what makes the FFI bindings (Python, JavaScript, C#, …) work
the same way.

## The Update return

`Update` has three values:

| Variant | Meaning |
|---|---|
| `DoNothing` | No re-layout, no re-render. Use this when the callback only mutates internal state that doesn't show up on screen yet. |
| `RefreshDom` | Re-run `layout()` for the window the event came from. The framework reconciles old vs new tree. |
| `RefreshDomAllWindows` | Re-run `layout()` for every open window. Use sparingly — pick this when the change touches global state that every window's layout reads. |

If a single event fans out to multiple callbacks (e.g. propagation
from child to parent), the framework takes the strongest `Update`
across all of them.

## Reading application state

`RefAny::downcast_ref::<T>()` and `downcast_mut::<T>()` recover the
typed payload. The downcast checks the type id, so passing the wrong
type returns `None` rather than reinterpreting memory.

```rust,ignore
extern "C" fn save(data: RefAny, _info: CallbackInfo) -> Update {
    let model = match data.downcast_ref::<AppModel>() {
        Some(m) => m,
        None => return Update::DoNothing,  // wrong RefAny type
    };
    write_to_disk(&*model);
    Update::DoNothing
}
```

For a callback that needs to mutate, use `downcast_mut`. The mutable
borrow lasts the callback body.

## Finding the right method

`CallbackInfo` has around 350 methods. They fall into groups, and most
of them are documented on a page of their own:

| Group | Page |
|---|---|
| Finding, walking and editing nodes; hit testing; geometry | [Node Tree & Hit Testing](node-tree.md) |
| Cursor position, seats, capture and lock, pen, touch, gestures | [Pointer, Pen & Touch](pointer.md) |
| Drag sources, drop targets, drag data, OS file drops | [Drag & Drop](drag-and-drop.md) |
| Scroll containers, offsets, programmatic scrolling | [Scrolling](scrolling.md) |
| Window state, monitors, transient windows, tooltips, screenshots, system capabilities | [Window & System](window-system.md) |
| Audio and video playback, now-playing, media keys | [Media Playback](media.md) |
| Locale-aware numbers, dates, plurals, collation | [Locale Formatting](formatting.md) |
| Typing, IME, undo, document sync | [Text Input](text-input.md) |
| Cursors, selection ranges, clipboard | [Text Selection](text-selection.md) |
| Route pattern and parameters | [Routing](../architecture/routing.md) |

What stays on this page is the part that is not a group: the signature,
the return value, reading state, propagation, focus, async work, and
the styling and cache queries.

## Identifying the node that fired

```rust,ignore
let hit: DomNodeId = info.get_hit_node();
let rect = info.get_node_rect(hit);
```

A `DomNodeId` is `(DomId, NodeId)`, and for a callback attached to
several nodes the dataset is how you tell instances apart. Addressing,
walking and mutating nodes is the subject of
[Node Tree & Hit Testing](node-tree.md).

## Reading input state

The framework hands you the input state as of the event, plus the
snapshot from the previous one, so transitions are a diff rather than
bookkeeping you keep yourself:

```rust,ignore
info.get_current_keyboard_state();    // modifiers, pressed scancodes, reported chars
info.get_previous_keyboard_state();   // the same, one event ago
info.get_key_modifiers();             // just the modifier set
info.get_key_locks();                 // caps / num / scroll
info.is_key_repeat();                 // held, not newly pressed
```

`get_physical_key()` is the layout-independent key - the *position* on
the keyboard - which is what a shortcut like "W A S D" must use so it
stays a square on an AZERTY keyboard.

The pointer equivalents (`get_current_mouse_state`, the three cursor
position spaces, pen and touch) are on
[Pointer, Pen & Touch](pointer.md).

## Mutating the DOM without rebuilding

You don't have to return `RefreshDom` for small changes. The
framework exposes targeted mutations on `info` that go through a
faster path:

- `info.change_node_text(node_id, text)` — replaces the text content of
  a node.
- `info.change_node_image(node_id, image_ref, ...)` — swaps an image.
- `info.set_css_property(node_id, prop)` / `override_node_css_properties(...)`
  — set or override a CSS declaration without re-running the cascade
  for the whole tree.
- `info.change_node_image_mask(node_id, mask)` — update a clip mask.

These produce a `CallbackChange` queued on the info; the framework
applies them between the callback returning and the next paint. The
restyle and damage-rect machinery covered in
[Reconciliation](../dom/reconciliation.md) keeps the work proportional
to what changed.

For structural edits (insert a child, delete a node), use
`insert_child_node` and `delete_node`. Larger changes — anything
beyond a handful of nodes — are usually clearer expressed as a fresh
`Dom` from `layout()` plus `Update::RefreshDom`.

## Focus

```rust,ignore
info.set_focus(FocusTarget::Node(node_id));
info.set_focus_to_path(dom_id, css_path);     // by selector
info.has_focus(node_id);
info.clear_focus();
```

Focus moves on the next frame, and the reconciler migrates it across a
`RefreshDom` for nodes that still match - so a focused text field
survives a rebuild without you re-focusing it.

`focus_next()`, `focus_previous()`, `focus_first()` and `focus_last()`
walk the focus order, which is what a custom key handler calls instead
of computing the next focusable node itself.

Every one of these has a `_for_seat` twin - `set_focus_for_seat()`,
`clear_focus_for_seat()` - because a window can have several
independent cursors; see [Pointer, Pen & Touch](pointer.md).
`is_dom_focused(dom_id)` asks the same question of a whole sub-DOM.

Accessibility rides on the same axis: `set_accessibility_state()` and
`set_accessibility_value()` update what assistive technology reports
for a node, and `perform_accessibility_action()` performs an action
the assistive layer requested.

Scrolling a node into view is `scroll_node_into_view(node)`, and
`set_cursor_visibility(false)` hides the pointer - see
[Scrolling](scrolling.md) and [Pointer, Pen & Touch](pointer.md).

For text inputs and contenteditable surfaces, the cursor and
selection helpers (`add_cursor`, `add_selection_range`,
`get_primary_selection`, …) are documented separately in
[Text Selection](text-selection.md).

## Stopping propagation

Events bubble from the hit node to the root by default. Two opt-outs:

- `info.stop_propagation()` — finish the current node's callbacks,
  then stop. Other callbacks attached to *this* node still run.
- `info.stop_immediate_propagation()` — stop right now. No further
  callbacks at this node, no parents.

`info.prevent_default()` is the third opt-out: it tells the framework
not to apply the built-in handling for the event (e.g. don't insert a
character on `KeyDown` after your callback handled it). Browsers use
the same name for the same idea.

Event filtering — `EventFilter::Hover(...)` vs `Focus(...)` vs
`Window(...)`, propagation order, NotEvent — is in
[Events and Input](..md).

`get_current_event_id()` identifies the event being dispatched, which is
how two callbacks on different nodes tell "the same click" from "two
clicks", and `get_last_input_sample()` is the raw sample it came from.

## Logging and metrics

A callback runs inside the framework's loop, so `println!` lands
wherever the host put stdout - which on a windowed app is often
nowhere. Log through the info instead, and it goes to the same sink as
the framework's own output:

```rust,ignore
info.log(AppLogLevel::Info, "export finished".into());
info.warn("texture atlas is full".into());
```

Three metric recorders sit alongside them - `record_counter()`,
`record_gauge()` and `record_histogram()`, each taking a name, a value
and a label set. They feed the observability pipeline, so an app can be
instrumented without a metrics crate of its own.

`get_ctx()` returns the host-provided context `RefAny`, when one was
installed.

## Async work: timers and threads

The callback runs on the UI thread. Any work it does blocks the next
frame. For anything slow, schedule it.

```rust,ignore
let timer = Timer::new(/* interval */ 100.ms, refany.clone(), tick);
info.add_timer(TimerId::unique(), timer);
```

`add_timer` registers a recurring callback the framework drives on
the main loop. The timer callback returns a
`TimerCallbackReturn { update, terminate }` that controls both
whether to re-run layout and whether the timer fires again.

For background work, `add_thread` spawns a worker thread tied to a
`RefAny`. The thread sends messages back to a `merge_callback` on the
main thread — the framework already understands cross-thread message
delivery, so you don't need a manual mutex. See
[Background Tasks](../data/background-tasks.md).

## Window control

```rust,ignore
info.create_window(WindowCreateOptions::create(layout_fn));
info.close_window();
info.modify_window_state(new_state);
info.begin_interactive_move();       // hand the drag to the OS
```

`begin_interactive_move()` is the one worth singling out: it tells the
window manager to take over a drag, so a custom title bar moves the
window with the platform's own snapping and animations instead of a
per-frame reposition. Monitors, transient windows, tooltips and
screenshots are on [Window & System](window-system.md).

Routing across pages is `switch_route(pattern, params)`, with
`get_route_pattern()` and `get_route_param()` to read the active route;
see [Routing](../architecture/routing.md).

## Image and font caches

```rust,ignore
info.add_image_to_cache("logo".into(), image_ref);
info.remove_image_from_cache("logo".into());
info.reload_system_fonts();
```

Cached images are addressable by name from any layout pass. Reloading
system fonts is the right thing to do after a font config change
(rare, but desktop environments do change font defaults at runtime).

## Styling and layout queries

`set_css_property()` and `override_css_property()` change one
declaration on one node without re-running the cascade for the tree.
Reading back is the computed value, after the cascade:

```rust,ignore
info.get_computed_width(node);
info.get_computed_height(node);
info.get_computed_css_property(node, CssPropertyType::Display);
```

`has_pending_relayout_change()` reports whether anything queued so far
this callback will force a relayout - worth checking before adding
more, in a handler that runs every frame.

`get_animation_momentum()` and `set_animation_momentum()` read and
inject the velocity a node is carrying, which is how a custom gesture
hands a fling over to the built-in animation.

`get_gl_context()` returns the GL context for a node that draws its own
content, and `query_pagination()` measures how a styled DOM would break
across pages. Node geometry - sizes, positions, hit-test bounds - is on
[Node Tree & Hit Testing](node-tree.md).

## Working with sub-DOMs

`info.trigger_virtual_view_rerender(dom_id, node_id)` re-runs the
virtual-view callback for one specific sub-DOM. Use it when the
virtual view's source data changed but the parent layout hasn't.

`info.update_image_callback(dom_id, node_id)` triggers a re-render
of an `ImageCallback`-backed node — the GPU canvas pattern documented
in [SVG and Canvas](../images/svg.md).

## Finding another node: markers

A callback sometimes needs to act on a node it did *not* fire on — a
canvas callback moving a meter in the header, a timer updating a
preview tile. The address for that jump is a **marker**: a string you
mint at `layout()` time (use `Uuid::short()` — collision-free with no
coordination), stamp on the target with `Dom::with_marker`, and keep
wherever the driving callback can see it. Resolve it back to a node
with `get_node_id_by_marker`:

```rust,no_run
use azul::prelude::*;

struct App { meter_marker: String } // filled with Uuid::short() in layout()

extern "C" fn on_pen_move(mut data: RefAny, mut info: CallbackInfo) -> Update {
    let marker = match data.downcast_ref::<App>() {
        Some(a) => a.meter_marker.clone(),
        None => return Update::DoNothing,
    };
    if let Some(node) = info.get_node_id_by_marker(marker).into_option() {
        // Drive the widget through ITS public API - its dataset stays private.
        ProgressBar::update_progress(info, node, 63.0);
    }
    Update::DoNothing // the widget repaints itself, no relayout needed
}
```

Markers are invisible to CSS matching and **excluded from node
equality**, so minting a fresh UUID every `layout()` never makes the
DOM diff consider a node "changed". The full pattern — when to prefer
it over `RefreshDom`, and how widgets like `ProgressBar` expose update
functions for it — is the architecture guide's
["inter-widget fast path"](../architecture.md#inter-widget-communication)
section.

## A complete example

A "delete row" button: each row's button callback carries a dataset
naming its row plus a backreference to the app (the
[backreference pattern](../architecture.md)), mutates the model, and
refreshes:

```rust,no_run
use azul::prelude::*;

struct App { rows: Vec<String> }

/// Attached as the RefAny of each row's delete-button callback.
struct DeleteRow { index: usize, app: RefAny }

extern "C" fn on_delete(mut data: RefAny, _info: CallbackInfo) -> Update {
    let (index, mut app_ref) = match data.downcast_ref::<DeleteRow>() {
        Some(d) => (d.index, d.app.clone()),
        None => return Update::DoNothing,
    };
    let mut app = match app_ref.downcast_mut::<App>() {
        Some(a) => a,
        None => return Update::DoNothing,
    };

    if index < app.rows.len() {
        app.rows.remove(index);
    }

    Update::RefreshDom
}
```

The pattern — a per-row dataset naming the row, a backreference to the
model, mutate, return `RefreshDom` — works for nearly every "this
widget acts on its container" interaction.

---

### Doc Guide En Events Drag And Drop (`doc/guide/en/events/drag-and-drop.md`)

---
slug: events/drag-and-drop
title: Drag & Drop
language: en
canonical_slug: events/drag-and-drop
audience: external
maturity: wip
guide_order: 67
topic_only: false
short_desc: Dragging nodes, MIME-keyed drag data, accepting drops, and files dragged in from the OS
prerequisites: [hello-world, events, events/callbacks]
tracked_files:
  - core/src/drag.rs
  - core/src/events.rs
  - layout/src/event_determination.rs
  - layout/src/callbacks.rs
default-search-keys:
  - CallbackInfo
  - DragData
  - DragEffect
  - DropEffect
  - DragState
  - accept_drop
  - set_drag_data
  - get_drag_data
  - get_dropped_files
---

# Drag & Drop

Drag and drop is two halves that never meet directly: a *source* that
says what is being dragged, and a *target* that says whether it will
take it. They communicate through a MIME-keyed payload, the same model
the W3C `DataTransfer` API uses.

## Introduction

The drag system covers selection drags, scrollbar thumbs, window moves,
window resizes, OS file drops and DOM-node drags. The framework handles
the first four itself. This page is about the last two, which are the
ones you write callbacks for.

*Dragging data out* of an azul window into another application is not
implemented on any platform. Everything else on this page works.

## Marking a node draggable

```rust,no_run
use azul::prelude::*;
let card = Dom::create_div()
    .with_attribute(AttributeType::Draggable(true))
    .with_css("padding: 12px; background: #fef;");
```

The gesture manager sees the attribute during hit testing and starts a
drag when the user begins one. Nothing else is needed to make a node
pick up.

## The event sequence

On the **source** node: `DragStart`, then `Drag` on every cursor move,
then `DragEnd` exactly once - on a successful drop, a rejected one, or a
cancel. `DragEnd` always fires, which makes it the only correct place to
undo optimistic UI.

On a **target** node: `DragEnter`, `DragOver` (throttled, repeated),
`DragLeave`, and `Drop` if the target accepted.

Each of these exists as `HoverEventFilter`, `FocusEventFilter` and
`WindowEventFilter`. Use the hover form for "this node is the drop
zone"; use the window form to observe drags anywhere in the window, for
example to highlight every valid target the moment a drag starts.

## Drag data

Set it on the source at `DragStart`, read it on the target at `Drop`:

```rust,ignore
// DragStart, on the source
info.set_drag_data("application/x-myapp-task".into(), payload_bytes);
info.set_drag_data("text/plain".into(), "Task 17".as_bytes().into());

// Drop, on the target
if let Some(bytes) = info.get_drag_data("application/x-myapp-task".into()).into_option() {
    // ...
}
```

Several MIME types per drag are allowed and the usual pattern is two:
your own structured type for drops inside the app, and `text/plain` so
the same drag means something to a foreign target.

`get_drag_types()` lists the types the current drag carries. It is
readable during `DragOver`, which is the point - a target decides
whether to accept *before* it is allowed to read the payload, exactly as
in the browser. `get_drag_data()` returns nothing until `Drop`.

## Accepting a drop

Targets opt in. A node that never calls `accept_drop()` is not a drop
target: the cursor shows the no-drop indicator and `Drop` never fires.

```rust,ignore
// DragOver, on the target
if info.get_drag_types().iter().any(|t| t.as_str() == "application/x-myapp-task") {
    info.accept_drop();
    info.set_drop_effect(DropEffect::Move);
}
```

`accept_drop()` is the equivalent of `event.preventDefault()` in a W3C
`dragover` handler, and the same trap applies: accepting only in
`DragEnter` is not enough, because a later `DragOver` that does not
accept revokes it.

## Effects

`DragEffect` is what the source *allows*, set at `DragStart`:
`Uninitialized`, `None`, `Copy`, `CopyLink`, `CopyMove`, `Link`,
`LinkMove`, `Move`, `All`.

`DropEffect` is what the target *chose*: `None`, `Copy`, `Link`, `Move`.

The chosen effect must be in the set the source allows, or the drop is
rejected. The effect also drives the cursor, so a target that means
"copy" but says `Move` will lie to the user.

## Styling during a drag

Two pseudo-classes, so drag feedback does not need callbacks:

```css
.card:dragging  { opacity: 0.4; }
.dropzone:drag-over { outline: 2px solid #38f; }
```

## Files dragged in from the OS

File drops arrive on the same pipeline, with their own events.
`HoverEventFilter::DroppedFile` fires when the drop lands, and the
hovered-file methods report what is over the window before it does:

```rust,no_run
use azul::prelude::*;

extern "C" fn on_dropped_file(_: RefAny, info: CallbackInfo) -> Update {
    for path in info.get_dropped_files().iter() {
        // ... open each file ...
    }
    Update::RefreshDom
}
```

`get_dropped_files()` is the full list and `get_dropped_file()` the
first of them; `get_hovered_files()` and `get_hovered_file()` are the
same pair while the drag is still in flight, which is what a drop zone
uses to preview "3 files" before the user lets go.
`is_file_drag_active()` distinguishes an OS file drag from an in-app
node drag, which matters because both are "a drag" to everything else.

## More methods

**Querying the drag** - `is_drag_active`, `is_dragging`,
`is_node_drag_active`, `is_file_drag_active`, `get_drag_state`,
`get_dragged_node`. `get_drag_state()` returns the whole picture at once
- drag type, source node, current drop target, and the file path for a
file drag - if you would rather read one struct than call four
predicates.

**Movement** - `get_drag_delta`, `get_drag_delta_screen`,
`get_drag_delta_screen_incremental`. The first is relative to the drag's
start in window coordinates, the second the same in desktop
coordinates, and the incremental form is the delta *since the previous
event* - which is the one a window-move or a resize handle wants, since
it never accumulates rounding error.

**Data and acceptance** - `set_drag_data`, `get_drag_data`,
`get_drag_types`, `accept_drop`, `set_drop_effect`.

**Files** - `get_dragged_file`, `get_dropped_file`, `get_dropped_files`,
`get_hovered_file`, `get_hovered_files`.

## Cross-references

- [Pointer, Pen & Touch](pointer.md) - pointer capture, which a
  hand-rolled drag needs and a `draggable` node does not.
- [Scrolling](scrolling.md) - drag auto-scroll runs on the scroll timer.

---

### Doc Guide En Events Formatting (`doc/guide/en/events/formatting.md`)

---
slug: events/formatting
title: Locale Formatting
language: en
canonical_slug: events/formatting
audience: external
maturity: wip
guide_order: 70
topic_only: false
short_desc: Locale-aware numbers, dates, lists, plurals and collation from inside a callback
prerequisites: [hello-world, events, events/callbacks]
tracked_files:
  - layout/src/callbacks.rs
  - core/src/icu.rs
default-search-keys:
  - CallbackInfo
  - IcuResult
  - PluralCategory
  - FormatLength
  - format_integer
  - format_date
  - pluralize
  - sort_strings
---

# Locale Formatting

Numbers, dates, lists and plurals differ per locale in ways no format
string survives. These methods do it properly, from inside a callback,
without pulling in an i18n crate of your own.

## Introduction

Every method takes the locale as its first argument - a BCP-47 tag such
as `"en-US"` or `"de-AT"` - rather than reading a global. That is
deliberate: an app that shows one user's document to another, or
formats a report for a locale that is not the UI's, needs both at once.

The date and time functions return `IcuResult` because a locale can be
unknown or a date invalid; the number functions return a `String`
directly.

## Numbers

```rust,ignore
let n = info.format_integer("de-DE".into(), 1_234_567);      // "1.234.567"
let x = info.format_decimal("de-DE".into(), 1234, 2);        // "12,34"
```

`format_decimal()` takes the value as an integer plus a decimal-place
count rather than an `f64`, which keeps money exact: 12.34 is
`(1234, 2)` and never becomes 12.339999999999999.

## Dates and times

```rust,ignore
let d = info.format_date("fr-FR".into(), date, FormatLength::Long);
let t = info.format_time("fr-FR".into(), time, /* include_seconds */ false);
let dt = info.format_datetime("fr-FR".into(), datetime, FormatLength::Short);
```

`FormatLength` picks how much the locale spells out, from numeric-short
to fully written. Let it decide field order - month-day versus day-month
is a locale property, not a formatting choice.

`get_current_time()` returns the current `Instant` through the same
clock the framework uses, which is the one to read if you want a
timestamp that agrees with the animation and timer subsystems.

## Plurals

English has two plural forms. Arabic has six, Polish three with
different rules, and "if n == 1" is wrong in most of the world:

```rust,ignore
let s = info.pluralize(
    locale, count,
    "no files".into(), "one file".into(), "two files".into(),
    "{} files".into(), "{} files".into(), "{} files".into(),
);
```

You supply every CLDR category - zero, one, two, few, many, other - and
the locale selects. For English only `one` and `other` are ever chosen,
so filling the rest with the same string costs nothing and makes the
call correct in Polish too. `get_plural_category()` returns the chosen
category if you would rather branch yourself.

## Lists

```rust,ignore
let s = info.format_list(locale, items, ListType::And);
```

"A, B and C" in English, "A, B et C" in French, and the serial comma
handled per locale. `ListType` also covers *or* and unit lists.

## Sorting and comparing

Alphabetical order is not code-point order: `ä` sorts with `a` in
German and after `z` in Swedish.

```rust,ignore
let sorted = info.sort_strings(locale, names);
let ord = info.compare_strings(locale, a, b);   // -1, 0, 1
let same = info.strings_equal(locale, a, b);
```

`strings_equal()` is locale-aware equality, not byte equality, so it
matches strings that differ only in how they are composed - which is
the difference between a search box that finds `café` typed two ways
and one that does not.

## More methods

**Numbers** - `format_integer`, `format_decimal`.

**Dates and times** - `format_date`, `format_time`, `format_datetime`,
`get_current_time`.

**Plurals and lists** - `pluralize`, `get_plural_category`,
`format_list`.

**Collation** - `sort_strings`, `compare_strings`, `strings_equal`.

**Other** - `get_icu_localizer` returns the underlying localizer handle
for code that wants to hold one rather than pass a locale each time;
`get_string_contents(node_id)` reads a node's localized string content.

---

### Doc Guide En Events Media (`doc/guide/en/events/media.md`)

---
slug: events/media
title: Media Playback
language: en
canonical_slug: events/media
audience: external
maturity: wip
guide_order: 69
topic_only: false
short_desc: Driving audio and video nodes, now-playing metadata, OS media keys and system audio
prerequisites: [hello-world, events, events/callbacks]
tracked_files:
  - layout/src/callbacks.rs
  - core/src/audio.rs
  - core/src/dom.rs
default-search-keys:
  - CallbackInfo
  - PlaybackState
  - NowPlayingInfo
  - MediaControlRequest
  - media_play
  - media_seek
  - set_now_playing
  - get_media_position
---

# Media Playback

`Dom::create_audio()` and `Dom::create_video()` make a media node. This
page is the control surface: a callback drives the node, reads its
state, and tells the OS what is playing.

## Introduction

A media node holds its own playback state. Every method here addresses
one by `DomNodeId`, so a page with three videos is three independent
players and no global "current track" exists unless you make one.
`create_audio()` and `create_video()` take accessibility info, because
a player nobody can announce is a player a screen reader user cannot
operate.

## Transport

```rust,ignore
info.media_play(node);
info.media_pause(node);
info.media_toggle(node);                 // whichever is the opposite
info.media_seek(node, 42.0);             // seconds, absolute
```

Reading back:

```rust,ignore
let playing  = info.is_media_playing(node);
let at       = info.get_media_position(node);   // seconds
let total    = info.get_media_duration(node);
let state    = info.get_media_state(node);      // OptionPlaybackState
```

`is_media_playing()` is the boolean shortcut; `get_media_state()` is
the full state - stopped, playing, paused, buffering, ended - which is
what a progress bar needs to distinguish "stalled" from "paused".

## Volume

```rust,ignore
info.media_set_volume(node, 0.5);   // 0.0 ..= 1.0
info.media_set_muted(node, true);
```

Mute is not volume zero, and the pair is kept separate deliberately:
unmuting restores the volume the user had set, so a mute button must
use `media_set_muted()` rather than saving and restoring a level
itself. `get_media_volume()` and `is_media_muted()` read both back.

## Driving a custom source

If you decode media yourself and push frames into the node, three
methods let you own the clock:

```rust,ignore
info.media_set_duration(node, total_seconds);  // once, when known
info.media_advance(node, dt);                  // per frame
info.media_report_error(node);                 // decode failed
```

`media_advance(node, dt)` moves the node's clock forward by a delta
rather than seeking to an absolute point, which is what keeps it in
step with a timer-driven decode loop. `media_report_error()` moves the
node into its error state so the UI and accessibility layer both see
that playback failed, instead of the node sitting silently at a
position that never advances.

## Telling the OS what is playing

```rust,ignore
info.set_now_playing(now_playing);
```

This populates the platform's media panel - macOS Now Playing, Windows
SMTC, MPRIS on Linux - with title, artist, artwork and duration. Set it
whenever the track changes, and it is what makes the lock screen and
the media keys show your app at all.

The keys then come back as requests:

```rust,ignore
if let Some(req) = info.get_media_control_request().into_option() {
    // play / pause / next / previous / seek, from a headset button
    // or the keyboard's media keys
}
```

These are *requests*, not commands: the OS is asking, and your app
decides. Ignoring one is legitimate - a request to skip when there is
no next track should do nothing rather than stop playback.

## System audio

`is_system_audio_active()` reports whether something else on the
machine is playing, and `get_system_audio_change()` fires when that
changes - a call starting, another app taking the output device. The
usual response is to duck or pause.

`set_system_audio_takeover(true)` declares that your app is now the
primary audio application, which is the other side of the same
negotiation.

## More methods

**Transport** - `media_play`, `media_pause`, `media_toggle`,
`media_seek`, `media_advance`.

**State** - `is_media_playing`, `get_media_state`,
`get_media_position`, `get_media_duration`, `media_set_duration`,
`media_report_error`.

**Volume** - `media_set_volume`, `get_media_volume`, `media_set_muted`,
`is_media_muted`.

**OS integration** - `set_now_playing`, `get_media_control_request`,
`is_system_audio_active`, `get_system_audio_change`,
`set_system_audio_takeover`.

---

### Doc Guide En Events Node Tree (`doc/guide/en/events/node-tree.md`)

---
slug: events/node-tree
title: Node Tree & Hit Testing
language: en
canonical_slug: events/node-tree
audience: external
maturity: wip
guide_order: 65
topic_only: false
short_desc: Addressing nodes, walking the tree, geometry, and changing a node without a rebuild
prerequisites: [hello-world, events, events/callbacks]
tracked_files:
  - layout/src/callbacks.rs
  - layout/src/hit_test.rs
  - core/src/dom.rs
  - core/src/styled_dom.rs
default-search-keys:
  - CallbackInfo
  - DomNodeId
  - NodeHierarchyItemId
  - get_hit_node
  - get_node_rect
  - get_node_id_by_marker
  - change_node_text
  - override_node_css_properties
---

# Node Tree & Hit Testing

A callback fires with a whole DOM behind it. This page is about reaching
into that tree: finding the node that fired, walking to its relatives,
asking where it is on screen, and changing it without rebuilding
anything.

## Introduction

Everything here hangs off `CallbackInfo`. Nothing on this page rebuilds
the DOM - these are reads against the tree `layout()` already produced,
plus a handful of targeted writes that skip `layout()` entirely. If you
want a new tree, return `Update::RefreshDom` instead and let `layout()`
run.

## Addressing a node

There are two spellings, and which one a method takes is not a matter of
taste - it tells you which layer the method belongs to.

**`DomNodeId`** is the pair `(DomId, NodeId)` and is what callbacks hand
you. `get_hit_node()` returns one. Most read methods take one:

```rust,ignore
let hit: DomNodeId = info.get_hit_node();
let rect = info.get_node_rect(hit);
```

**A split `dom_id` + `node_id`** pair appears on the methods that reach
the node *hierarchy* directly, and those return a
`NodeHierarchyItemId` rather than an `OptionDomNodeId`:

```rust,ignore
let parent = info.get_parent_node(dom_id, node_id);   // NodeHierarchyItemId
let parent = info.get_parent(some_dom_node_id);       // OptionDomNodeId
```

Both reach the same node. The `DomNodeId` form is the one to reach for
in application code; the split form exists for callers that already hold
the two halves and want to avoid rebuilding the pair. A
`NodeHierarchyItemId` is not an option type - it carries its own "no
such node" state, so check it before use.

Most apps have a single root DOM, `DomId::ROOT_ID`. Sub-DOMs come from
`IFrame` nodes and virtual views; `get_dom_ids()` lists them all.

## Which node fired

```rust,ignore
let hit = info.get_hit_node();                  // the node the event landed on
let deepest = info.get_deepest_hovered_node();  // innermost node under the cursor
let hovered = info.get_hovered_nodes();         // every node under the cursor
```

`get_hit_node()` is the node the event was dispatched to, which is not
always the innermost one under the pointer: an event that bubbles is
dispatched to an ancestor while the pointer is over a child.
`get_deepest_hovered_node()` answers the other question.

For a callback attached to many nodes - one "submit" shared by several
forms - the dataset is how you tell instances apart:

```rust,ignore
let me = match info.get_dataset(info.get_hit_node()) {
    Some(d) => d,
    None => return Update::DoNothing,
};
```

To reach a node that did *not* fire, give it an address in `layout()`
and resolve it here. A marker is the cheapest one, because it is
invisible to CSS and does not affect node equality, so minting a fresh
one every frame costs no `Mount` / `Unmount` churn:

```rust,ignore
// layout()
let marker = Uuid::short();
let bar = ProgressBar::create(0.0).dom().with_marker(Some(marker.clone()).into());

// callback
if let Some(node) = info.get_node_id_by_marker(marker).into_option() { /* ... */ }
```

`get_node_id_by_id_attribute(dom_id, id)` does the same for a node
carrying a real `id`.

## Walking the tree

`get_parent`, `get_first_child`, `get_last_child`, `get_next_sibling` and
`get_previous_sibling` take and return `DomNodeId`s.
`get_all_children_nodes(dom_id, node_id)` returns the whole child list at
once, and `get_children_count(dom_id, node_id)` its length.

`get_node_child_index_path(ancestor, node)` gives the child indices from
one node down to another - a stable path you can store and re-resolve
after a rebuild, which raw ids do not survive.

## Geometry

```rust,ignore
let rect  = info.get_node_rect(node);              // position + size, laid out
let pos   = info.get_node_position(node);
let size  = info.get_node_size(node);
let bounds = info.get_node_hit_test_bounds(node);  // what hit testing uses
```

`get_node_rect` is the laid-out box. `get_node_hit_test_bounds` is the
area that actually receives pointer events, which differs wherever a node
was given a larger or smaller hit area than its visual box.
`get_hit_node_rect()` and `get_hit_node_layout_rect()` are shorthands for
the node that fired.

To measure a tree you have *not* mounted, `measure_dom(dom, available)`
lays it out against a bound and returns the size it would take;
`measure_dom_shrink_to_fit(dom, bound)` gives the smallest box that still
fits its content. Both are the right way to size a popup before showing
it.

## Reading content and attributes

`get_node_text_content(node)`, `get_node_text_length(node)`,
`get_node_tag_name(node)`, `get_node_classes(node)`, `get_node_id(node)`
and `get_node_attribute(node, name)` read what the node is. They answer
from the styled DOM, so they see what `layout()` produced, including
anything a previous callback changed in place.

## Changing a node without rebuilding

These write straight into the current tree. No `layout()` call, no diff -
which is why they are the fast path for high-frequency updates such as a
drag or a pressure curve:

```rust,ignore
info.change_node_text(node, "42".into());
info.override_node_css_properties(dom_id, node_id, props);
info.change_node_image(dom_id, node_id, image, UpdateImageType::Background);
```

The cost is that the change is invisible to your data model: the next
`RefreshDom` rebuilds from `layout()` and the override is gone unless the
model produces it too. Use them for transient visual state, and the model
for anything that must survive a rebuild.

`insert_child_node` and `delete_node` change the shape of the tree the
same way. They are deliberately narrow - structural edits are the diff's
job, and doing many of them by hand will lose to a single `RefreshDom`.

## More methods

Everything else in this group, by what it answers.

**Addressing and identity** - `get_dom_ids`, `get_dom_subtree`,
`get_styled_dom_clone`, `get_node_id_by_id_attribute`,
`get_node_id_by_marker`, `get_node_child_index_path`.

**Relatives, split-pair form** - `get_parent_node`,
`get_first_child_node`, `get_last_child_node`, `get_next_sibling_node`,
`get_previous_sibling_node`, `get_all_children_nodes`,
`get_children_count`.

**Hit testing and hover** - `get_hit_node`, `get_hit_node_rect`,
`get_hit_node_layout_rect`, `get_deepest_hovered_node`,
`get_hovered_nodes`, `get_hovered_nodes_frames_ago`,
`get_cursor_relative_to_node`, `request_hit_test_update`.
`get_hovered_nodes_frames_ago(n)` is how a gesture looks back at what the
pointer was over before the current frame.

**Focus** - `is_node_focused`, `is_node_focused_for_seat`,
`get_focused_node`, `get_focused_node_for_seat`, `set_focus_to_node`. The
`_for_seat` forms take a seat id, because one window can have several
independent cursors; see [Pointer, Pen & Touch](pointer.md).

**Scrolling, per node** - `find_scroll_parent`, `get_scroll_node_info`,
`get_scroll_offset_for_node`, `scroll_node_into_view`. See
[Scrolling](scrolling.md).

**Text and selection, per node** - `get_node_cursor_position`,
`get_node_selection_ranges`, `node_has_selection`. See
[Text Selection](text-selection.md).

**Mutation** - `change_node_text`, `change_node_css_properties`,
`override_node_css_properties`, `change_node_image`,
`change_node_image_mask`, `set_node_ids_and_classes`, `insert_child_node`,
`delete_node`.

**Other** - `measure_dom`, `measure_dom_shrink_to_fit`,
`take_screenshot_of_node`, `open_menu_for_node`, `open_menu_for_hit_node`,
`is_node_drag_active`, `get_dragged_node`, `get_dataset`.

---

### Doc Guide En Events Pointer (`doc/guide/en/events/pointer.md`)

---
slug: events/pointer
title: Pointer, Pen & Touch
language: en
canonical_slug: events/pointer
audience: external
maturity: wip
guide_order: 66
topic_only: false
short_desc: Cursor state, pointer seats, capture and lock, pen pressure and tilt, touch coalescing, and gestures
prerequisites: [hello-world, events, events/callbacks]
tracked_files:
  - layout/src/callbacks.rs
  - layout/src/managers/gesture.rs
  - core/src/events.rs
  - core/src/window.rs
default-search-keys:
  - CallbackInfo
  - MouseState
  - PointerSource
  - get_cursor_position
  - capture_pointer
  - set_pointer_lock
  - get_pen_pressure
  - get_coalesced_touches
---

# Pointer, Pen & Touch

A mouse, a finger and a pen all arrive as the same events. This page is
about the state behind them: where the pointer is, which device it is,
how to keep events flowing to one node, and what the extra axes of a pen
or a touch screen give you.

## Introduction

Pointer events are dispatched the usual way - see
[Callbacks](callbacks.md) for how a handler is attached and
[Node Tree & Hit Testing](node-tree.md) for which node receives them.
What you get *inside* the handler is a snapshot of the pointer, and the
methods here read it. Nothing on this page rebuilds the DOM.

## Where the pointer is

Three coordinate spaces, three methods, and picking the wrong one is the
most common source of "my drag is offset by the title bar":

```rust,ignore
let in_window   = info.get_cursor_position();            // window-relative, logical px
let in_viewport = info.get_cursor_relative_to_viewport(); // scrolled content
let on_screen   = info.get_cursor_position_screen();      // desktop coordinates
```

All three are options, because there may be no pointer at all - a
keyboard-only session, or a touch device with nothing touching it.

For a position relative to a specific node - the usual thing a canvas or
a slider wants - use `get_cursor_relative_to_node(node)` rather than
subtracting rectangles yourself.

## Buttons and history

`get_current_mouse_state()` is the whole button and position state as of
this event. `get_previous_mouse_state()` is the same thing one event
ago, which is what you diff to find out *what changed*: a callback fires
on both press and release, and the pair tells you which.

```rust,ignore
let now = info.get_current_mouse_state();
let before = info.get_previous_mouse_state();
```

`was_double_clicked()` answers the question directly instead of making
you time clicks yourself.

## Seats: a window can have several cursors

A *seat* is one independent cursor with its own buttons and focus. Most
systems have exactly one, so most code can ignore this - but multi-seat
Wayland, multi-touch and pen-plus-finger input all produce more than one
at a time, and a handler that assumes one cursor will mix them up.

```rust,ignore
let seat = info.get_pointer_device_id();
let state = info.get_pointer_seat_state(seat);
```

A seat is not a device. Two devices driving the same cursor are one
seat; one touch screen reporting three fingers is three. Anything that
tracks a gesture across events must key its state by seat id, or a
second finger will overwrite the first one's start position.

The focus methods have `_for_seat` variants for the same reason - see
[Node Tree & Hit Testing](node-tree.md).

## Capture and lock

**Capture** routes every subsequent pointer event to one node, even once
the pointer leaves it. This is what makes a drag survive a fast mouse
movement out of the handle:

```rust,ignore
info.capture_pointer(handle);      // on press
// ... events keep arriving at `handle` ...
info.release_pointer_capture();    // on release
```

Always release. A capture that is never released makes the rest of the
window unclickable, and nothing times it out for you.

**Lock** is different: `set_pointer_lock(true)` hides the cursor and
stops reporting absolute positions, delivering relative motion instead.
That motion comes from `get_raw_mouse_motion()`, which is unaccelerated
and unclamped - the input a first-person camera or an infinite-drag
number field wants. Absolute position methods stop being useful while
locked, by design.

## Which device produced this event

```rust,ignore
match info.get_pointer_source() {
    PointerSource::Mouse => { /* ... */ }
    PointerSource::Touch => { /* ... */ }
    PointerSource::Pen   => { /* ... */ }
    _ => {}
}
```

Worth branching on more often than people do: a 44-px hit target and a
long-press menu are right for touch and wrong for a mouse, and the same
callback serves both.

## Pen

A pen is a pointer with extra axes, all of them optional because not
every tablet reports every one:

```rust,ignore
let pressure = info.get_pen_pressure();        // 0.0 ..= 1.0
let tilt     = info.get_pen_tilt();            // two angles
let distance = info.get_pen_hover_distance();  // above the surface, not touching
```

`is_pen_in_contact()` separates hovering from drawing, `is_pen_eraser()`
reports the inverted end of the stylus, and
`is_pen_barrel_button_pressed()` the side button.
`get_pen_tool_kind()` is the coarser question - pen, eraser, airbrush,
lens - and `get_pen_state()` returns the whole set at once if you would
rather read one struct than six options.

Treat every one of these as absent-by-default. A drawing app that
requires pressure will draw nothing on hardware that does not report it;
fall back to a constant.

## Touch: coalescing and prediction

A touch screen samples far faster than the display refreshes, so a
single frame's event carries more than one sample:

```rust,ignore
for point in info.get_coalesced_touches().iter() { /* every real sample */ }
for point in info.get_predicted_touches().iter() { /* extrapolated, not yet real */ }
```

Draw the coalesced points to get a smooth stroke instead of a polyline
between frames. Draw the predicted ones to hide input latency - but
render them as provisional and replace them when the real samples
arrive, because the prediction is a guess and will sometimes overshoot a
sharp corner.

## Gestures

The recognised gestures are read, not subscribed to. Each returns an
option that is `Some` only on the frame the gesture is active:

```rust,ignore
if let Some(pinch) = info.get_pinch().into_option() { /* scale */ }
if let Some(rot) = info.get_rotation().into_option() { /* angle */ }
if let Some(dir) = info.get_swipe_direction().into_option() { /* ... */ }
if let Some(press) = info.get_long_press().into_option() { /* ... */ }
```

`has_sufficient_history_for_gestures()` guards the case where a gesture
begins before enough samples exist to measure it - checking it stops a
pinch from reporting a wild first scale factor.
`settle_scroll_gesture()` ends an in-flight scroll gesture early, which
is what you call when a pinch takes over from a pan.

## More methods

Everything else in this group, by what it answers.

**Position and state** - `get_cursor_position`,
`get_cursor_position_screen`, `get_cursor_relative_to_viewport`,
`get_current_mouse_state`, `get_previous_mouse_state`,
`get_pointer_seat_state`, `get_pointer_device_id`, `get_pointer_source`,
`was_double_clicked`, `get_raw_mouse_motion`.

**Capture and lock** - `capture_pointer`, `release_pointer_capture`,
`set_pointer_lock`.

**Pen and tablet** - `get_pen_pressure`, `get_pen_tilt`,
`get_pen_hover_distance`, `get_pen_state`, `get_pen_tool_kind`,
`is_pen_in_contact`, `is_pen_eraser`, `is_pen_barrel_button_pressed`,
`get_proximity`, `get_tablet_devices`, `get_tablet_pad`,
`get_dial_state`. `get_tablet_devices()` enumerates connected tablets;
`get_tablet_pad()` and `get_dial_state()` read the buttons, rings and
dials on the tablet body rather than the stylus.

**Touch** - `get_coalesced_touches`, `get_predicted_touches`.

**Gestures** - `get_pinch`, `get_rotation`, `get_swipe_direction`,
`get_long_press`, `has_sufficient_history_for_gestures`,
`settle_scroll_gesture`, `inject_native_gesture`.
`inject_native_gesture()` feeds a synthetic gesture into the same
pipeline, which is how automated tests drive one.

**Menus and feedback** - `open_menu`, `open_menu_at`,
`invoke_system_dialog`, `play_haptic`, `play_haptic_request`.
`open_menu()` places a context menu at the pointer and `open_menu_at()`
at a position you choose; `play_haptic()` takes a pattern and a target,
`play_haptic_request()` a prepared request.

See also [Scrolling](scrolling.md) for wheel and momentum, and
[Node Tree & Hit Testing](node-tree.md) for resolving which node a
pointer is over.

---

### Doc Guide En Events Scrolling (`doc/guide/en/events/scrolling.md`)

---
slug: events/scrolling
title: Scrolling
language: en
canonical_slug: events/scrolling
audience: external
maturity: wip
guide_order: 64
topic_only: false
short_desc: Scroll containers, scroll events, programmatic scrolling and momentum
prerequisites: [hello-world, events]
tracked_files:
  - core/src/events.rs
  - layout/src/hit_test.rs
  - layout/src/callbacks.rs
  - layout/src/managers/scroll_state.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T06:00:00Z
default-search-keys:
  - CallbackInfo
  - EventFilter
  - HoverEventFilter
  - WindowEventFilter
  - scroll_to
---

# Scrolling

## Introduction

*WIP.* Scrolling is functional: scroll containers, wheel and trackpad
input, programmatic scrolling and momentum all work. Drag and drop moved
to its own page, [Drag & Drop](drag-and-drop.md), because the two only
share the input pipeline.

Scrolling is a gesture: the framework tracks the pointer across several
events, decides which gesture is in progress, and emits high-level
events you handle the same way as any other event filter from
[events](..md).

## Making a node scrollable

CSS does the work. Set `overflow` to `scroll`, `auto`, or `hidden` on a node whose content exceeds its box, and the framework gives it scrollbars and wires the wheel/trackpad/touchpad events to it.

```rust,no_run
use azul::prelude::*;
let scroll_box = Dom::create_div()
    .with_css("width: 400px; height: 200px; overflow-y: scroll;");
```

- `visible` (default). Children draw outside the box. No clip, no scrollbar, no `Scroll` events.
- `hidden`. Children clip to the box. No scrollbar. No scrolling.
- `scroll`. Children clip. Scrollbar always visible, even if not needed. Wheel scrolls.
- `auto`. Children clip. Scrollbar visible only when content overflows. Wheel scrolls.

`overflow-x` and `overflow-y` set the axes independently. The shorthand `overflow: scroll` sets both.

The container's scroll-clip size is its inner box (border-box minus borders, padding, and scrollbar track). The content size is the total extent of its children. Scrolling shifts which slice of the content sits inside the clip.

### What you don't get

- **No viewport-level scrolling.** The window itself doesn't scroll. Make `<body>` or a descendant the scroll container.
- **No automatic scroll-into-view on focus.** Call `info.scroll_to(...)` from a focus callback if you want this.

## Reading scroll events

`HoverEventFilter::Scroll` fires for wheel/trackpad/touch scroll over a scrollable node. `ScrollStart` and `ScrollEnd` bracket a contiguous gesture so you can debounce work.

```rust,no_run
use azul::prelude::*;

extern "C" fn on_scroll(_: RefAny, _: CallbackInfo) -> Update {
    Update::DoNothing
}

fn build(data: RefAny) -> Dom {
    let mut node = Dom::create_div();
    node.add_callback(
        EventFilter::Hover(HoverEventFilter::Scroll),
        data,
        on_scroll,
    );
    node
}
```

Inside the callback, query the current scroll state through `CallbackInfo`:

```rust,ignore
impl CallbackInfo {
    pub fn get_scroll_offset(&self) -> Option<LogicalPosition>;
    pub fn get_scroll_offset_for_node(&self, dom_id: DomId, node_id: NodeId)
        -> Option<LogicalPosition>;
    pub fn get_scroll_state(&self, /* ... */) -> Option<ScrollState>;
    pub fn get_scroll_delta(&self) -> LogicalPosition;
    pub fn had_scroll_activity(&self) -> bool;
}
```

`get_scroll_offset()` returns the offset for the hit node, which is convenient inside a `Scroll` callback. `ScrollState::scroll_position` is the live position.

## Scrolling programmatically

`CallbackInfo::scroll_to` queues a scroll that the runtime applies after the callback returns:

```rust,ignore
impl CallbackInfo {
    pub fn scroll_to(&mut self, /* ... */);
}
```

The position is in the scroll container's content space, so `(0, 0)` scrolls to the top-left of the content.

## Hit-test order

Front-to-back. The topmost element under the cursor is hit first; deeper nodes are tried only if the topmost doesn't claim the event. A scroll routes to the topmost scrollable ancestor of the hit node, not to whichever scroll container the cursor happens to sit inside. This matches browser behaviour: a button's `cursor: pointer` overrides the container's `cursor: text`, and a `<select>` swallows wheel events that would otherwise scroll the page.

## Smooth and momentum scrolling

The framework animates between scroll positions when `scroll_to` is called. Trackpad and wheel deltas are accumulated frame-to-frame; momentum drives the inertia phase after a fling. You don't manage the timer.

CSS `-azul-overflow-scrolling: touch` enables momentum on a node. `overscroll-behavior: contain` prevents scroll chaining to the parent.

## More methods

**Reading** - `get_scroll_offset` is the offset of the scroll node the
event belongs to; `get_scroll_state(dom_id, node_id)` and
`get_scroll_delta(dom_id, node_id)` are the full state and the change
since the last event for one node. `get_scroll_offset_for_node` and
`get_scroll_node_info` answer the same per node; see
[Node Tree & Hit Testing](node-tree.md).

**Scrolling** - `scroll_to(dom_id, node_id, position)` jumps,
`scroll_to_animated(dom_id, node_id, target)` eases, and
`scroll_to_unclamped` allows a position past the content's end - the
overscroll region, which is what a rubber-band animation drives.
`scroll_node_into_view(node)` and `scroll_active_cursor_into_view()`
scroll the minimum needed to reveal a node or the text caret.

**Finding the container** - `find_scroll_target(dom_id, node_id)`
resolves which scroll container a wheel event over this node would
actually move, following the same topmost-scrollable-ancestor rule the
hit test uses. `find_scroll_parent` is its `DomNodeId` form.

**Platform** - `get_natural_scroll()` reports the system's scroll
direction preference, which you need only if you are synthesising
scroll deltas yourself; the framework already applies it to real input.
`has_system_natural_scroll()` says whether the platform reported a
preference at all - see [Window & System](window-system.md).

## Cross-references

- [`events`](..md): the event filter system this page builds on.
- [`timers`](../animations/timers.md): scrolling momentum and drag auto-scroll run on reserved timers.
- [Drag & Drop](drag-and-drop.md): the drag half of this page, moved out.

---

### Doc Guide En Events Text Input (`doc/guide/en/events/text-input.md`)

---
slug: events/text-input
title: Text Input
language: en
canonical_slug: events/text-input
audience: external
maturity: wip
guide_order: 62
topic_only: false
short_desc: Editable text, IME and composition, cursor motion, undo, validation and document sync
prerequisites: [events]
tracked_files:
  - core/src/hit_test.rs
  - core/src/selection.rs
  - layout/src/widgets/text_input.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - TextInput
  - TextInputState
  - TextInputStateWrapper
  - TextInputValid
  - TextInputSelection
  - TextInputSelectionRange
  - OnTextInputReturn
  - PendingTextEdit
  - Dom
  - CallbackInfo
  - Update
  - DatasetMergeCallback
---

# Text Input

## Introduction

*WIP.* The text-input runtime is wired but several pieces are still missing — macOS IME / CJK support is incomplete and APIs may change.

There are two ways to make text editable:

- The [`TextInput`](#the-textinput-widget) widget: a single-line input field with platform-native styling, placeholder, focus border, and blinking cursor.
- The [`contenteditable`](#the-contenteditable-flag) flag on any DOM node: for code-editor lines, multi-line text areas, or rich-text spans.

Both share the same event plumbing. A focused, editable node receives `Focus(TextInput)` and `Focus(VirtualKeyDown)` events. IME composition is handled by the platform shell; from your callback's point of view, you receive the produced character once composition commits.

## The TextInput widget

`TextInput::create()` returns a default-styled widget; `dom()` consumes it into a `Dom`.

```rust,no_run
use azul::prelude::*;

let input = TextInput::create()
    .with_placeholder("Your name".into())
    .with_text("Felix".into())
    .dom();
```

The produced subtree is a focusable container with a placeholder text node, a label text node holding the buffer, and a cursor.

### Wiring callbacks

Three optional callbacks fire in addition to the default key/text handlers:

- `with_on_text_input(data, cb)`: fires for every accepted character. Signature: `extern "C" fn(RefAny, CallbackInfo, TextInputState) -> OnTextInputReturn`.
- `with_on_virtual_key_down(data, cb)`: fires for non-text keys (arrows, backspace). Same signature.
- `with_on_focus_lost(data, cb)`: fires when focus moves elsewhere. Signature: `extern "C" fn(RefAny, CallbackInfo, TextInputState) -> Update`.

```rust,no_run
use azul::prelude::*;

struct Form { name: String }

extern "C" fn on_input(
    mut data: RefAny,
    _info: CallbackInfo,
    new_state: TextInputState,
) -> OnTextInputReturn {
    if let Some(mut form) = data.downcast_mut::<Form>() {
        form.name = new_state.get_text();
    }
    OnTextInputReturn { update: Update::DoNothing, valid: TextInputValid::Yes }
}

# let form = RefAny::new(Form { name: String::new() });
let dom = azul::widgets::TextInput::create()
    .with_on_text_input(form, on_input)
    .dom();
```

`OnTextInputReturn::valid` is the gate that lets you reject a character (e.g. "only digits"). Returning `TextInputValid::No` rolls back the edit before mutating the DOM. `OnTextInputReturn::update` follows the usual `Update` semantics from [Events](..md).

### TextInputState

`TextInputState` carries:

- `text: U32Vec` (characters as `u32`, FFI-friendly).
- `placeholder: OptionString`.
- `max_len: usize`.
- `cursor_pos: usize`.
- `selection: TextInputSelection`.

`TextInputState::get_text()` reconstructs a normal Rust string from the buffer.

## The contenteditable flag

`NodeData::set_contenteditable(true)` (or `Dom::with_contenteditable(true)` for builders) marks any node as an editable region:

```rust,no_run
use azul::prelude::*;

let line = Dom::create_div()
    .with_contenteditable(true)
    .with_tab_index(TabIndex::Auto);
```

Once the node has focus and the contenteditable bit is set, every printable key press the OS produces is delivered to that node. The platform shell records the edit, the framework computes the changeset, and the renderer consumes it.

### Edits avoid a full re-layout

Text edits run through an incremental display-list path that bypasses the user's `layout_callback`. The motivation: if every keystroke triggered a full DOM rebuild, the layout callback would return a fresh DOM with the original text and overwrite the edit.

The framework distinguishes three levels of post-event work:

- Redraw only: scroll offsets, GPU transforms. Layout callback doesn't run.
- Display-list update: text edits, incremental relayout. Layout callback doesn't run.
- Full regeneration: `Update::RefreshDom`, focus changes that move the DOM. Layout callback runs.

Returning `Update::RefreshDom` from a text-input callback forces the third path. Do this only when the edit changes something the layout callback needs to see, such as adding a new sibling node or hiding a section.

## The double-update pattern

Because the layout callback is bypassed during a text edit, your application model and the live DOM can drift out of sync if you only write to one of them. The double-update pattern keeps both in sync:

1. Inside `on_text_input`, write the new text to your `RefAny` model so a future re-layout reads the right value.
2. Update the node's dataset with `Dom::with_dataset` (set during layout) and a `DatasetMergeCallback` so the in-place display list patch reflects the edit.

The widget's internal callbacks already do this for you. If you write a custom contenteditable that maintains its own buffer, mirror both.

## Reading edits inside a callback

`CallbackInfo::get_text_changeset()` returns the current `PendingTextEdit`:

```rust,no_run
use azul::prelude::*;

extern "C" fn on_key(_data: RefAny, info: CallbackInfo) -> Update {
    if let Some(edit) = info.get_text_changeset() {
        let _inserted: &str = edit.inserted_text.as_str();
    }
    Update::DoNothing
}
```

`CallbackInfo::change_node_text(node_id, text)` is the corresponding write side. It replaces the text content of a node and queues an incremental display-list update.

## Default actions

These keystrokes are handled by the framework after every callback returns, unless a callback called `info.prevent_default()`:

- Backspace: delete the grapheme before the cursor.
- Delete: delete the grapheme after the cursor.
- Left/Right arrow: move the cursor by one grapheme.
- Home/End: move the cursor to line start/end.
- Ctrl+Home / Ctrl+End: move the cursor to document start/end.
- Ctrl+A: select all (scoped to the focused contenteditable).
- Escape: collapse selection.

Suppress with `info.prevent_default()` to override. The rest of the callback chain still runs (W3C semantics; see [Events](..md#default-actions)).

## IME and composition

Chinese, Japanese and Korean input - and dead keys, and emoji pickers -
go through an *input method*: the user types several keystrokes that
build up a provisional string, and only the commit produces real text.

```rust,ignore
if let Some(pre) = info.get_composition_text().into_option() {
    // provisional text: draw it underlined, do not put it in your model
}
let caret = info.get_composition_cursor();   // caret WITHIN the composition
```

Composition text is not committed text. An editor that writes it into
its buffer will duplicate every character once the commit arrives.
Draw it, and wait. `is_composing()` is the boolean form, and it is also
the guard a key handler needs: Enter during a composition selects a
candidate and must not submit the form.

On mobile, `request_soft_keyboard(true)` raises the on-screen keyboard
and `get_keyboard_inset()` reports how much of the window it now
covers - which is what you pad the layout with so the field being
edited is not behind the keyboard.

## Moving the cursor programmatically

Every motion the arrow keys perform has a method, each taking whether
to extend the selection - which is what the Shift modifier means:

```rust,ignore
info.move_cursor_left(node, /* extend_selection */ false);
info.move_cursor_to_line_end(node, true);      // shift+End
info.move_cursor_to_document_start(node, false);
info.move_cursor(dom_id, node_id, cursor);     // to an exact position
```

`delete_backward(node)` and `delete_forward(node)` are Backspace and
Delete, and `insert_text(dom_id, node_id, text)` types a string in.
All of them respect grapheme boundaries, so a family emoji deletes as
one character rather than leaving half a sequence behind.

## Dry runs: the inspect_ family

Each of those mutations has an `inspect_` twin that computes the result
and changes nothing:

```rust,ignore
let would_be = info.inspect_move_cursor_right(node);   // OptionTextCursor
let deletion = info.inspect_backspace(node);           // OptionDeleteResult
```

This is how you answer "would this do anything?" before doing it -
greying out a Delete button when the cursor is at the end, previewing
what a paste would replace with `inspect_paste_target_range()`, or
validating an edit before applying it. Nothing in an `inspect_` call
touches the document, so it is safe to call every frame.

## Undo

There are two stacks, and they are separate on purpose:

```rust,ignore
info.undo_app_state();        // your model's own undo
info.undo_structural_edit();  // the framework's text/DOM edit history
```

Text edits go on the structural stack automatically.
`commit_undo_snapshot()` closes the current undo group, which is how
you decide what one Ctrl+Z reverts: typing a word is usually one
snapshot, not eight.

`can_undo(node)` and `can_redo(node)` drive the toolbar buttons, and
`get_undo_text(node)` and `get_redo_text(node)` give the label - "Undo
typing" rather than a bare "Undo". `inspect_undo_operation()` and
`inspect_redo_operation()` return the operation itself if you want to
show more than a label.

## Validation

`get_validity_state()` reports the focused field's validity and
`get_validity_state_of(node)` any other node's - required-but-empty,
too long, failing a pattern. Reading it is how a submit button stays
disabled without your own parallel validation pass.

## Cursor blink

`start_cursor_blink_timer()` and `stop_cursor_blink_timer()` control
the caret's blink, and `reset_cursor_blink()` restarts the cycle at
"visible". Call the reset on every edit and cursor move: a caret that
keeps blinking on its own schedule while you type disappears exactly
when you are looking for it.

## Collaborative editing

A document that several people edit needs edits recorded, shipped and
acknowledged, and the framework keeps that bookkeeping:

```rust,ignore
info.record_document_edit(changeset);              // local edit -> the log
let pending = info.get_unsynced_text_edits();      // what the peer has not seen
info.mark_text_revision_synced(revision);          // peer confirmed up to here
```

`get_document_text_revision()` is the monotonic revision counter,
`get_document_edit_clone()` a snapshot of the pending changeset, and
`get_document_caret()` the caret as a document position rather than a
node-local one - which is the form that survives being sent to another
machine.

Applying a remote edit is acknowledged with
`mark_document_edit_applied(id)`, or
`mark_document_edit_applied_with_inverse(id, inverse)` when you also
supply the inverse operation, so the edit remains undoable.

Other users' cursors are painted through the selection-owner API; see
[Text Selection](text-selection.md).

## Where it goes wrong

- **Edit lost after `RefreshDom`.** The layout callback rebuilt the DOM with stale text. Either keep the contenteditable subtree out of the rebuild, or apply the double-update pattern so the rebuild reads from the same model the edit wrote to.
- **`max_len` is not enforced.** Add a length check in your `on_text_input` and return `TextInputValid::No` when the buffer is full.
- **First click positions the cursor at the start.** The cursor is initialised at end-of-text on focus; the first click can race with focus acquisition. Subsequent clicks behave normally.
- **`TextInputSelection` / `TextInputSelectionRange` are not yet wired through the default callbacks.** Multi-node selection across nodes goes through the cross-DOM selection model; see [Text Selection](text-selection.md).

## More methods

**Editing** - `insert_text`, `delete_backward`, `delete_forward`,
`create_text_input`, `get_text_changeset`, `set_text_changeset`.
`create_text_input(text)` creates an editable text surface
imperatively.

**Cursor motion** - `move_cursor`, `move_cursor_left`,
`move_cursor_right`, `move_cursor_up`, `move_cursor_down`,
`move_cursor_to_line_start`, `move_cursor_to_line_end`,
`move_cursor_to_document_start`, `move_cursor_to_document_end`.

**Dry runs** - `inspect_move_cursor_left`, `inspect_move_cursor_right`,
`inspect_move_cursor_up`, `inspect_move_cursor_down`,
`inspect_move_cursor_to_line_start`, `inspect_move_cursor_to_line_end`,
`inspect_move_cursor_to_document_start`,
`inspect_move_cursor_to_document_end`, `inspect_backspace`,
`inspect_delete`, `inspect_delete_changeset`,
`inspect_select_all_changeset`, `inspect_paste_target_range`,
`inspect_copy_changeset`, `inspect_cut_changeset`.

**Undo** - `undo_app_state`, `redo_app_state`, `undo_structural_edit`,
`redo_structural_edit`, `commit_undo_snapshot`, `can_undo`, `can_redo`,
`get_undo_text`, `get_redo_text`, `inspect_undo_operation`,
`inspect_redo_operation`.

**IME and soft keyboard** - `get_composition_text`,
`get_composition_cursor`, `is_composing`, `request_soft_keyboard`,
`get_keyboard_inset`.

**Caret** - `start_cursor_blink_timer`, `stop_cursor_blink_timer`,
`reset_cursor_blink`.

**Validation** - `get_validity_state`, `get_validity_state_of`.

**Document sync** - `record_document_edit`, `get_document_edit_clone`,
`get_unsynced_text_edits`, `get_document_text_revision`,
`mark_text_revision_synced`, `mark_document_edit_applied`,
`mark_document_edit_applied_with_inverse`, `get_document_caret`.

---

### Doc Guide En Events Text Selection (`doc/guide/en/events/text-selection.md`)

---
slug: events/text-selection
title: Text Selection
language: en
canonical_slug: events/text-selection
audience: external
maturity: wip
guide_order: 63
topic_only: false
short_desc: Selection ranges, multiple cursors, remote selections and the rich clipboard
prerequisites: [events, events/text-input]
tracked_files:
  - core/src/hit_test.rs
  - core/src/selection.rs
  - layout/src/widgets/text_input.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - CallbackInfo
  - Selection
  - SelectionState
  - SelectionRange
  - SelectionVec
  - SelectionRangeVec
  - SelectionId
  - GraphemeClusterId
  - CursorAffinity
  - TextCursor
  - DomNodeId
---

# Text Selection

## Introduction

*WIP.* Selection rendering and the per-frame management code are wired but the high-level mouse-driven flow is partially implemented. Cross-node selection works in the data model but is not yet rendered for every layout case; APIs may change.

Selection in Azul follows the W3C Selection API model: a directed range with an **anchor** (where the user pressed) and a **focus** (where the user is now). The range can span any subtree of the DOM, not just the inside of a single text input.

## Positions: TextCursor

A position in editable text is a `TextCursor`:

```rust,ignore
use azul::prelude::*;
let cursor = TextCursor {
    cluster_id: GraphemeClusterId { source_run: 0, start_byte_in_run: 5 },
    affinity: CursorAffinity::Leading,
};
```

`GraphemeClusterId` is a stable, logical pointer into the original inline content. It survives Bidi reordering and line breaking. `CursorAffinity` disambiguates the two visual positions a single logical index can have.

- `Leading`. In LTR text, the left edge of the cluster. In RTL text, the right edge.
- `Trailing`. In LTR text, the right edge of the cluster. In RTL text, the left edge.

The pair `(cluster, Leading)` and `(previous-cluster, Trailing)` describe the same visual point, but only one is correct after a line wrap or a Bidi run boundary.

## Ranges: SelectionRange and Selection

```rust,ignore
use azul::prelude::*;
fn build(start: TextCursor, end: TextCursor) {
    let range = SelectionRange { start, end };
    let sel: Selection = Selection::Range(range);          // highlighted
    let caret: Selection = Selection::Cursor(start);       // blinking caret
}
```

A `Selection` is either `Cursor` (collapsed, a blinking caret) or `Range` (highlighted, a selection rectangle). Direction is implicit: `start` may be logically after `end` if the user dragged backwards.

## DOM order, not visual order

Selection always follows DOM tree order, even when the visual layout reverses it (`flex-direction: row-reverse`, `direction: rtl`). Dragging visually left-to-right across a `row-reverse` flex container still selects "the second sibling, then the first sibling" because that's the source order. The highlight rectangles are computed from visual positions, but the contents of the selection follow the tree.

This matches browser behaviour.

## Reading the current selection

Every read is scoped to a DOM, because a window can have several - a
main document and an iframe each have their own selection:

```rust,ignore
let any     = info.has_any_selection();               // anywhere in the window
let here    = info.has_selection(dom_id);
let state   = info.get_selection(dom_id);             // OptionSelectionState
let ranges  = info.get_selection_ranges(dom_id);
let n       = info.get_selection_count(dom_id);
```

`SelectionState` carries the node and the active selections on it.
Per-node questions - `node_has_selection(node)` and
`get_node_selection_ranges(node)` - are on
[Node Tree & Hit Testing](node-tree.md).

To respond to selection changes, register a callback on
`Hover(MouseUp)` or on `FocusEventFilter::FocusReceived` and read the
selection from it:

```rust,no_run
use azul::prelude::*;

extern "C" fn on_select(_data: RefAny, info: CallbackInfo) -> Update {
    if let Some(state) = info.get_selection(DomId::ROOT_ID).into_option() {
        let _ranges = &state.selections;
        // ... update UI ...
    }
    Update::DoNothing
}
```

## Multiple cursors

The model is multi-cursor from the ground up; a single caret is just
the case where there is one.

```rust,ignore
let id = info.add_cursor(dom_id, node_id, cursor);              // a caret
let id = info.add_selection_range(dom_id, node_id, range);      // a range
info.remove_selection_by_id(id);
```

Both return a `SelectionId` that stays stable across edits, which is
what lets you remove or update one cursor out of many later.

`get_primary_cursor(dom_id)` is the main caret - the one that scrolls
into view and that typing follows - and `get_primary_selection(dom_id)`
the selection belonging to it. `get_multi_cursor_selections(dom_id)`
returns every cursor with its id, which is what a "Select all
occurrences" command produces and what your rendering must then handle.

`set_selection(dom_id, node_id, selection)` replaces the whole
selection state at once, which is the right call when you are setting a
selection rather than adding to one.

`process_text_selection_click(position, time_ms)` feeds a click into
the selection state machine, and the timestamp is why: it is what turns
two clicks into a word selection and three into a line.

## Other users' selections

A collaborative editor shows where everyone else is. Remote selections
are owned, coloured and drawn by the framework rather than by your DOM:

```rust,ignore
info.set_selection_owner_color(owner, ColorU::from_str("#e2725b"));
info.set_remote_selections(owner, selections);
// on disconnect:
info.clear_remote_selections(owner);
info.clear_selection_owner_color(owner);
```

Each `SelectionOwner` is one remote participant. Because the framework
paints them, they behave like real selections - they reflow with the
text, survive edits and do not need a parallel overlay of absolutely
positioned rectangles.

`get_document_selection()` returns the selection in document
coordinates, as spans, which is the form to send over the wire: node
ids mean nothing on the other machine.

## Copy, cut, paste

The clipboard carries `ClipboardContent`, not a string - rich text,
HTML and images travel alongside the plain-text fallback, in both
directions:

```rust,ignore
let content = info.get_clipboard_content();        // OptionClipboardContent
info.set_clipboard_content(content);
```

To customise what Copy and Cut produce, set the content for the target
node before the default action runs:

```rust,ignore
info.set_copy_content(node, content);
info.set_cut_content(node, content);
```

The default Ctrl+C / Ctrl+X / Ctrl+V keystrokes copy the current
selection, cut it, or paste at the caret. `inspect_copy_changeset()`,
`inspect_cut_changeset()` and `inspect_paste_target_range()` compute
what each would do without doing it - see the `inspect_` family in
[Text Input](text-input.md).

## Painting the highlight

The painter renders selection highlights as rectangles behind the text. Selection updates flow through the same incremental display-list path as text edits (see [Text Input](text-input.md)), so extending a selection doesn't run the layout callback.

CSS `selection-background-color` and `selection-color` style the highlight:

```css
::selection {
    background-color: #b3d4fc;
    color: #000;
}
```

## Known limitations

- **Selection clears between drag frames** in some configurations. The legacy per-frame `clear_selection()` call hasn't been fully removed from the mouse-drag path; on affected platforms the highlight flickers during a drag.
- **Cross-node rendering is incomplete.** The data model is correct but the painter currently renders only the anchor's container in some layouts.
- **No primary-selection clipboard on Linux/X11.** Middle-click paste between Azul and other apps doesn't work yet.
- **No RTL-aware direction handling.** `direction: rtl` isn't yet considered when ordering the visual highlight rectangles for the first/last line.
- **No vertical writing mode.** `writing-mode: vertical-*` isn't respected by the selection axis.

## More methods

**Reading** - `has_any_selection`, `has_selection`, `get_selection`,
`get_selection_ranges`, `get_selection_count`,
`get_document_selection`.

**Cursors and ranges** - `add_cursor`, `add_selection_range`,
`set_selection`, `set_select_all_range`, `remove_selection_by_id`,
`get_primary_cursor`, `get_primary_selection`,
`get_multi_cursor_selections`, `process_text_selection_click`.
`set_select_all_range(target, range)` defines what Ctrl+A selects on a
node, for a surface where "everything" is narrower than the node's
whole content.

**Remote selections** - `set_remote_selections`,
`clear_remote_selections`, `set_selection_owner_color`,
`clear_selection_owner_color`.

**Clipboard** - `get_clipboard_content`, `set_clipboard_content`,
`set_copy_content`, `set_cut_content`.

---

### Doc Guide En Events Window System (`doc/guide/en/events/window-system.md`)

---
slug: events/window-system
title: Window & System
language: en
canonical_slug: events/window-system
audience: external
maturity: wip
guide_order: 68
topic_only: false
short_desc: Window state, monitors, transient windows, tooltips, screenshots and system capabilities
prerequisites: [hello-world, events, events/callbacks]
tracked_files:
  - layout/src/callbacks.rs
  - core/src/window.rs
  - core/src/gamepad.rs
  - core/src/keyring.rs
default-search-keys:
  - CallbackInfo
  - FullWindowState
  - WindowCreateOptions
  - Monitor
  - Capability
  - modify_window_state
  - create_window
  - get_monitors
  - get_permission_status
---

# Window & System

Everything a callback can ask about the world outside its own DOM: the
window it lives in, the monitor that window is on, and the machine's
capabilities - from safe-area insets to the keyring.

## Introduction

These methods are all on `CallbackInfo`. The window ones read and write
a state the shell owns; the system ones are thin wrappers over platform
services, and most of them can fail or be unavailable, so they return
options or results. Assume nothing is present.

## Reading and changing the window

The window is one struct, read whole and written whole:

```rust,ignore
let mut state = info.get_current_window_state();
state.title = "Untitled - edited".into();
info.modify_window_state(state);
```

`get_previous_window_state()` is the same struct one event ago, which
is how you detect *what changed*: a resize callback that wants the
delta, or a handler that should only act when the window actually
became focused rather than on every event while it is focused.

`get_current_window_flags()` and `get_previous_window_flags()` are the
cheaper pair for the boolean subset - maximised, minimised, fullscreen,
focused - when a whole `FullWindowState` is more than the question
needs.

`queue_window_state_sequence()` applies several states in order on
successive frames. That exists because some transitions are illegal as
a single jump on some platforms - leaving fullscreen *and* changing size
in one step, say - and the sequence lets the shell perform them the way
the platform requires.

## More windows

```rust,ignore
let mut opts = WindowCreateOptions::create(layout_settings);
opts.window_state.title = "Settings".into();
info.create_window(opts);
```

`create_window()` opens a real OS window sharing the same `RefAny` data
model; `close_window()` closes the one the callback is running in.
`get_current_window_handle()` hands back the raw platform handle for
interop with a native library that needs one.

## Transient windows

A popup, a dropdown, a tear-off panel: nodes in your DOM that the shell
can promote to real OS windows, so they escape the parent window's
bounds without you building a second DOM.

```rust,ignore
info.set_transient_window_open(node, true);   // show it
info.set_transient_window_torn(node, true);   // user dragged it off
```

`get_transient_window_zone(node)` resolves which transient zone a node
belongs to, which is what a handler inside a popup uses to find the
popup it is in.

## Monitors

```rust,ignore
let here = info.get_current_monitor();   // the one this window is on
let all  = info.get_monitors();          // every attached monitor
```

Each `Monitor` carries its position, size and scale factor. Place a new
window with these rather than assuming an origin, because on a
multi-monitor desktop the coordinate origin is not where you think it
is - and a secondary monitor can sit at negative coordinates.

`get_safe_area_insets()` is the mobile counterpart: the notch, the home
indicator and the rounded corners, as insets to pad your root by.

## Tooltips

```rust,ignore
info.show_tooltip("Delete permanently".into());
info.hide_tooltip();
```

Native tooltips, drawn by the platform, so they can leave the window.
`show_tooltip_at()` places one at a position of your choosing instead of
at the pointer.

## Screenshots

`take_screenshot(dom_id)` renders a DOM through azul's own pipeline and
returns PNG bytes, with `take_screenshot_base64()` for the same thing as
text. The `native` pair - `take_native_screenshot_bytes()` and
`take_native_screenshot_base64()` - asks the *compositor* for what is
actually on screen instead.

The distinction matters for testing. Only the native form proves a frame
reached the display; azul's own renderer can happily produce a correct
image for a window that never painted.

## Permissions and capabilities

Anything privacy-sensitive is gated:

```rust,ignore
match info.get_permission_status(Capability::Location) {
    PermissionState::Granted => { /* ... */ }
    _ => { /* ask, or degrade */ }
}
```

Check before use rather than calling and handling the failure -
platforms differ in whether a denied call is silent, and a check is the
only portable answer.

## Secrets and biometrics

The keyring stores a secret in the platform's own credential store -
Keychain, Credential Manager, Secret Service - never in your files:

```rust,ignore
info.keyring_store("api-token".into(), secret, /* require_biometry */ true);
info.keyring_get("api-token".into());
```

Both are asynchronous. `keyring_get()` starts a retrieval; the value
arrives at a later callback, where `get_keyring_result()` reads it. The
same shape applies to biometrics: `request_biometric_auth()` starts the
prompt and `get_biometric_result()` reads the outcome later, with
`get_biometric_kind()` reporting whether this machine does fingerprint,
face, or nothing at all.

## More methods

**Window** - `get_current_window_state`, `get_previous_window_state`,
`get_current_window_flags`, `get_previous_window_flags`,
`modify_window_state`, `queue_window_state_sequence`, `create_window`,
`close_window`, `get_current_window_handle`, `set_icon`.
`set_icon(node, spec)` sets the window icon from a node's rendered
content.

**Transient windows** - `set_transient_window_open`,
`set_transient_window_torn`, `get_transient_window_zone`.

**Monitors and layout environment** - `get_current_monitor`,
`get_monitors`, `get_safe_area_insets`.

**Tooltips** - `show_tooltip`, `show_tooltip_at`, `hide_tooltip`.

**Screenshots and colour** - `take_screenshot`, `take_screenshot_base64`,
`take_native_screenshot_bytes`, `take_native_screenshot_base64`,
`pick_screen_color`, `get_picked_screen_color`. The colour pair is the
eyedropper: `pick_screen_color()` starts the pick and
`get_picked_screen_color()` reads what the user landed on.

**System appearance and behaviour** - `get_system_style`,
`reload_system_fonts`, `get_system_natural_scroll`,
`has_system_natural_scroll`, `get_system_time_fn`.
`get_system_style()` is the platform's theme - light or dark, accent
colour, font sizes - and `has_system_natural_scroll()` tells you whether
the platform reported a preference at all before
`get_system_natural_scroll()` gives you its value.

**Devices** - `get_gamepad_state`, `get_primary_gamepad`,
`get_hid_devices`, `get_hid_reports`, `get_sensor_reading`,
`get_location_fix`. Sensors and location are permission-gated;
`get_sensor_reading(kind)` covers accelerometer, gyroscope and the rest
behind one call.

**Secrets** - `keyring_store`, `keyring_get`, `keyring_delete`,
`get_keyring_result`, `request_biometric_auth`, `get_biometric_result`,
`get_biometric_kind`.

**Remote selections** - `set_remote_selections`,
`clear_remote_selections`. These render other users' cursors and
selections in a collaborative document; see
[Text Selection](text-selection.md).

**Updates** - `check_for_updates`, which runs the check on a thread and
returns its `ThreadId`.

---

### Doc Guide En Headless Pdf (`doc/guide/en/headless/pdf.md`)

---
slug: headless/pdf
title: Generating PDFs
language: en
canonical_slug: headless/pdf
audience: external
maturity: beta
guide_order: 221
topic_only: false
short_desc: Render a styled Dom to PDF bytes - headless, no window, no file I/O
prerequisites: [hello-world, styling]
tracked_files:
  - dll/src/desktop/extra/pdf/mod.rs
  - examples/azul-doc/src/main.rs
last_generated_rev: bf54ec9cbc7c1d3b7f6a2e6c0a3c5e8a9d0b1c2d
generated_at: 2026-05-21T00:00:00Z
default-search-keys:
  - Pdf
  - from_dom
  - dom_to_pdf
  - U8Vec
  - write_json
  - read_json
---

# Generating PDFs

## Introduction

Azul can render a styled `Dom` to a PDF the same way it lays out a window -
headless, through the layout engine - and hand you the PDF as bytes. This is
the "HTML-to-PDF" use case: you build your document as a `Dom` with CSS
(headings, tables, images, page breaks), call one function, and get a
`U8Vec`. It is completely **decoupled from the window** - no `App`, no event
loop - and does **no file I/O**: you receive the bytes and save / upload /
stream them yourself. (This mirrors the printpdf-WASM API: `dom -> PDF pages`.)

## The API

`Pdf` is a tiny stateless handle (`azul::misc::Pdf`):

```rust
use azul::misc::Pdf;

// `doc` is any styled Dom. Page size is in pixels at 96 DPI:
//   A4     = 794 x 1123    Letter = 816 x 1056
let bytes = Pdf::create().from_dom(doc, 794.0, 1123.0);   // -> U8Vec

// You own the bytes - persist them however you like:
std::fs::write("out.pdf", bytes.as_slice()).unwrap();
```

`Pdf::from_dom(dom, page_width_px, page_height_px) -> U8Vec` does the same thing
in one call, on a `Pdf` you already have.

Content taller than one page is **paginated automatically**: the headless
layout splits the document across as many pages as it needs, each
`page_width_px` x `page_height_px`.

## A complete example

`examples/azul-writer` is the P5 goal app. Its export callback builds the document
`Dom` and exports it - no window involved in the conversion:

```rust
fn doc_page() -> Dom {
    Dom::create_body()
        .with_child(Dom::create_text_do_not_use_without_block_level_wrapper("Invoice #42").with_css("font-size: 32px;"))
        .with_child(/* ... tables, paragraphs, images ... */)
}

extern "C" fn on_export(mut data: RefAny, _info: CallbackInfo) -> Update {
    if let Some(s) = data.downcast_ref::<DocState>() {
        let bytes = Pdf::create().from_dom(doc_page(), 794.0, 1123.0); // A4 @ 96 DPI
        let _ = std::fs::write(&s.export_path, bytes.as_slice());
    }
    Update::DoNothing
}
```

Because the conversion is headless, you can also generate PDFs from a server, a
CLI, or a background `Thread` - anywhere you can build a `Dom`.

## The JSON model

For programmatic / advanced PDF construction you can go through the printpdf
JSON representation: `pdf.write_json(json) -> U8Vec` serializes a document
model to PDF bytes, and `pdf.read_json(bytes) -> Json` parses an existing PDF
back into the model. `from_dom` is the high-level path; `write_json` is the
escape hatch when you need to place content the layout engine wouldn't.

## Feature gating

The `Pdf` handle is always present (it codegen-exposes with no feature gating),
but the `printpdf` engine behind it is opt-in via the `pdf` Cargo feature.
Without the feature, `from_dom` / `write_json` return empty bytes - so a build
that doesn't need PDF export doesn't pay for the dependency.

## See also

- [styling](../styling.md) - the CSS your document `Dom` uses.
- [headless-rendering](rendering.md) - the same layout engine, no window.
- [hello-world](../hello-world.md) - building a `Dom`.

---

### Doc Guide En Headless Rendering (`doc/guide/en/headless/rendering.md`)

---
slug: headless/rendering
title: Headless Rendering
language: en
canonical_slug: headless/rendering
audience: external
maturity: wip
guide_order: 220
topic_only: false
short_desc: Running the pipeline without a window
prerequisites: [dom]
tracked_files:
  - layout/src/cpurender.rs
  - dll/src/desktop/native_screenshot.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - App
  - AppConfig
  - CallbackInfo
  - take_screenshot
  - take_native_screenshot
---

# Headless Rendering

## Introduction

*WIP.* Wayland and XCB native screenshot support is incomplete.

The same binary that opens a desktop window can run without one. There is no display server, no OpenGL context, no visible window. Set an environment variable, the framework swaps in a CPU-only backend, and the rest of the pipeline (layout, callbacks, timers, re-renders) runs unchanged. Output is captured through the HTTP debug API.

```bash
AZ_BACKEND=headless ./my_app
```

This is the standard configuration for screenshot diffing in CI, smoke tests, and pre-rendering content for caching.

```azul-render screenshot=headless-hello width=400 height=200 subtitle="A free-standing render — no window, no display server"
<body style="background: white; font-family: sans-serif;">
  <p style="font-size: 28px; padding: 24px; color: #1d4f8b;">Hello from headless</p>
</body>
```

## Selecting the backend

Two environment variables, set before the process starts:

- `AZ_BACKEND=headless`. Forces the headless backend even when a display is available.
- `AZUL_HEADLESS=1`. Legacy alias for the same.
- `AZ_DEBUG=<port>`. Starts the HTTP debug server (event injection plus screenshot capture).

```bash
AZ_BACKEND=headless ./my_azul_app
AZ_BACKEND=headless AZ_DEBUG=8765 ./my_azul_app
```

A binary built for desktop runs unchanged in a server-side container with these flags set.

## Capturing screenshots over HTTP

When the process boots with `AZ_DEBUG=<port>`, the debug server (covered in [Debugging](../debugging.md)) exposes two screenshot ops:

- `take_screenshot`. Returns a CPU-rasterised PNG of the current DOM, no window decorations.
- `take_native_screenshot`. Returns the current framebuffer with whatever the OS is drawing.

In headless mode there is no OS framebuffer, so prefer `take_screenshot`. Both return a base64 data URI in the `data.value` field of the response envelope:

```bash
AZ_BACKEND=headless AZ_DEBUG=8765 ./my_app &
sleep 0.2
curl -s -X POST http://127.0.0.1:8765/ \
  -d '{"op":"take_screenshot"}' \
  | jq -r '.data.value' \
  | sed 's|^data:image/png;base64,||' \
  | base64 -d > screenshot.png
```

This is the pattern `azul-doc autoreview autodoc-screenshots` uses to render `azul-render` fences in the documentation.

## Driving a headless app

Drive interactions through the same HTTP API documented in [Debugging](../debugging.md). A headless run blocks on a wait condition just like a normal window blocks on `WaitMessage()` / `XNextEvent()` / `NSEvent`, so an idle process uses zero CPU. Inject events, query state, capture pixels:

```bash
post() { curl -s -X POST "http://127.0.0.1:8765/" -d "$1"; }

post '{"op":"wait_frame"}'
post '{"op":"resize","width":1024,"height":768}'
post '{"op":"click","selector":".increment-btn"}'
post '{"op":"wait_frame"}'
post '{"op":"take_screenshot"}' | jq -r '.data.value' > out.b64
```

For repeatable scenarios crystallised into a JSON file, see [End-to-End Testing](../debugging/e2e-testing.md).

## Determinism and CI

CPU rendering is consistent in concept across platforms, but pixel-exact output differs at sub-pixel positioning, antialiasing, and font hinting boundaries. For CI screenshot diffing:

- Use `assert_screenshot` with a `max_diff_ratio` tolerance rather than byte-equality.
- Pin the harness to one platform (Linux is the cheapest in CI) and treat baselines from other platforms as informational.
- Bundle a font (DejaVu, Noto) with your test assets and reference it explicitly via `AppConfig.bundled_fonts`. Relying on the host's fontconfig produces different glyph metrics on different machines.

## Platform notes

- macOS, Windows, X11: native screenshot capture works.
- Wayland: the compositor does not expose other windows' contents to applications. Use the headless backend rather than trying to capture a visible window.

The reftest harness in `layout/tests/` is the reference implementation of these patterns.

---

### Doc Guide En Headless Xml (`doc/guide/en/headless/xml.md`)

---
slug: headless/xml
title: XML Parsing
language: en
canonical_slug: headless/xml
audience: external
maturity: wip
guide_order: 222
topic_only: false
short_desc: Loading a Dom from an XML string
prerequisites: []
tracked_files:
  - core/src/xml.rs
  - layout/src/xml/mod.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T00:00:00Z
default-search-keys:
  - Xml
  - XmlNode
  - XmlNodeChild
  - XmlError
  - DomXmlParseError
  - Dom
  - Css
  - ComponentMap
  - ComponentId
---

# XML Parsing

## Introduction

*WIP.* Public surface is stable. The component-instantiation pipeline (`<my-widget>` style custom tags via `ComponentMap`) is mid-migration; treat names as load-bearing but signatures as moving targets.

Azul ships an XML/HTML5-lite parser that runs without a window, a renderer, or any GPU dependency. Use it as a tooling library: validate `.azul` files in CI, scrape resource URLs from a static site, build a DOM for headless rendering, or feed XHTML into custom layout code.

The parser is exposed in two layers:

- Tree (`Xml::from_str`) returns an `Xml` value with a `root: Vec<XmlNodeChild>`. A generic XML tree to walk, mutate, or scan.
- Direct-to-DOM (`Dom::create_from_parsed_xml`) consumes an `XmlNode` and returns a `Dom` ready for layout.

Both layers handle the same HTML5-lite quirks: BOM stripping, `<?xml?>` and `<!DOCTYPE>` skipping, void elements (`<br>`, `<img>`, `<meta>`), entity decoding, and `<style>` text extraction.

## Parsing into an XML tree

`Xml::from_str` consumes a string and returns an `Xml` whose `root` is the document's child list. The parser tolerates multiple root nodes so you can paste a fragment without an enclosing element.

```rust,no_run
use azul::xml::{Xml, XmlNodeChild};

let src = r#"
    <article id="p1" class="post">
      <h1>Hello</h1>
      <p>First &amp; only paragraph.</p>
    </article>
"#;

let doc = Xml::from_str(src.into()).unwrap();
assert_eq!(doc.root.as_ref().len(), 1);
match &doc.root.as_ref()[0] {
    XmlNodeChild::Element(node) => {
        assert_eq!(node.node_type.as_str(), "article");
        assert_eq!(node.children.as_ref().len(), 2); // <h1>, <p>
    }
    _ => unreachable!(),
}
```

`XmlNode` carries the tag name (`node_type`), an attribute map (`StringPairVec`), and an ordered list of `XmlNodeChild`. A child is either another `Element` or a `Text` leaf. The parser decodes the standard XML entities (`&lt;`, `&gt;`, `&amp;`, `&apos;`, `&quot;`, `&nbsp;`, plus numeric and hex character references) on the way in. You read decoded UTF-8 strings.

```rust,no_run
use azul::xml::{Xml, XmlNodeChild};

let xml = "<a href=\"/foo?x=1&amp;y=2\">link</a>";
let doc = Xml::from_str(xml.into()).unwrap();
if let XmlNodeChild::Element(a) = &doc.root.as_ref()[0] {
    let href = a.attributes.as_ref().iter()
        .find(|p| p.key.as_str() == "href")
        .map(|p| p.value.as_str())
        .unwrap();
    assert_eq!(href, "/foo?x=1&y=2");
}
```

## Parsing into a Dom

`Dom::create_from_parsed_xml` takes one `XmlNode` and returns a `Dom`. Pull the root element from your `Xml` first:

```rust,ignore
use azul::xml::{Xml, XmlNodeChild};
use azul::dom::Dom;

let doc = Xml::from_str(html.into()).unwrap();
let root = doc.root.as_ref().iter().find_map(|c| match c {
    XmlNodeChild::Element(node) => Some(node.clone()),
    _ => None,
}).unwrap();

let dom: Dom = Dom::create_from_parsed_xml(root);
```

Errors during DOM construction surface as `DomXmlParseError`: `Xml`, `MalformedHierarchy`, `MultipleHtmlRootNodes`, `MultipleBodyNodes`, `NoHtmlNode`, `NoBodyInHtml`, `Component`, `Css`, `RenderDom`.

## HTML5-lite quirks

The tokenizer is strict XML, but the tree-builder layer adds enough HTML5 leniency that most real-world fragments parse without an `xmlns` declaration or strict closing rules.

- Void elements auto-close: `area`, `base`, `br`, `col`, `embed`, `hr`, `img`, `input`, `link`, `meta`, `param`, `source`, `track`, `wbr`. So `<br>` is the same as `<br/>`.
- Auto-close on conflict: `<li>` before `<li>`, `<p>` before block-level, `<td>`/`<tr>`/`<th>` siblings, `<dt>`/`<dd>`, `<option>`/`<optgroup>`. So `<p>a<p>b</p>` parses as two siblings.
- Mismatched close tags walk the tag stack to find the matching opener and pop everything between. `<a><b></a>` closes both.
- BOM, `<?xml?>`, `<!DOCTYPE …>`, and leading `` are skipped before tokenization.

## Errors

Parse errors carry line and column positions. The top-level enum is `XmlError`. It wraps tokenizer errors, structural errors (`MalformedHierarchy`, `UnexpectedCloseTag`, `NoRootNode`), and a few resource-limit variants (`NodesLimitReached`, `AttributesLimitReached`).

```rust,no_run
use azul::xml::{Xml, XmlError};

let bad = "<a><b></a>"; // mismatched close
match Xml::from_str(bad.into()) {
    Ok(_) => {} // tolerated by HTML5-lite walking
    Err(XmlError::MalformedHierarchy(e)) => {
        eprintln!("expected </{}>, got </{}>", e.expected, e.got);
    }
    Err(other) => eprintln!("{:?}", other),
}
```

`MalformedHierarchyError` carries `expected` and `got`. `UnexpectedCloseTagError` carries `expected`, `actual`, and `pos`.

## Scanning external resources

`Xml::scan_external_resources` walks the parsed document and returns every URL it would need to fetch to render the page: `<img src>`, `<link href>`, `<script src>`, `<video|audio|source src>`, `<a href>` when the URL has a resource-like extension, plus `url(…)` and `@import` inside `<style>` blocks and inline `style=` attributes.

```rust,no_run
use azul::xml::{Xml, ExternalResourceKind};

let html = r#"
    <html>
      <head>
        <link rel="stylesheet" href="/site.css">
        <style>@import "/print.css";</style>
      </head>
      <body>
        <img src="/banner.png">
        <a href="/manual.pdf">manual</a>
      </body>
    </html>
"#;

let doc = Xml::from_str(html.into()).unwrap();
let resources = doc.scan_external_resources();

for r in resources.as_ref() {
    println!("{:?} {} (from <{} {}>)",
        r.kind, r.url.as_str(),
        r.source_element.as_str(),
        r.source_attribute.as_str());
}
```

`ExternalResource` carries `kind`, `mime_type`, `source_attribute`, `source_element`, and `url`. `ExternalResourceKind` is one of `Audio`, `Font`, `Icon`, `Image`, `Script`, `Stylesheet`, `Unknown`, `Video`. Use it to bundle assets, prefetch fonts, or audit a third-party fragment before letting your renderer load anything from the network.

## Components

> WIP. The component layer (`ComponentMap`, `ComponentId`) is the entry point for custom tags like `<my-card title="hi">…</my-card>`. The migration to it is partial.

Each `ComponentId` is a `(collection, name)` pair so you can scope sets: `builtin:div`, `myproject:my-card`. The parser doesn't instantiate components on its own. For the standalone library use cases on this page (parsing, validation, resource scraping), the component layer can be ignored: call `Xml::from_str` or `Dom::create_from_parsed_xml` directly and you get raw HTML semantics with no custom expansion.

## When to choose which entry point

- Need a generic XML tree to walk, mutate, or serialize. Use `Xml::from_str`.
- Need to render the document or feed it to layout. Use `Xml::from_str` then `Dom::create_from_parsed_xml` on the root.
- Need to scan a document's outbound URLs. Use `Xml::from_str` then `scan_external_resources`.

---

### Doc Guide En Hello World C (`doc/guide/en/hello-world/c.md`)

---
slug: hello-world/c
title: Hello World [C]
language: en
canonical_slug: hello-world/c
audience: external
maturity: mature
guide_order: 12
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - core/src/callbacks.rs
  - core/src/lib.rs
  - dll/src/lib.rs
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Css
  - RefAny
  - WindowCreateOptions
  - LayoutCallbackInfo
  - CallbackInfo
  - Update
---

# Hello World [C]

## Introduction

C is the "native" surface of Azul: every other binding (Rust, C++, Python, ...) ultimately 
calls into the same `azul.h` symbols, so anything that works in C works everywhere. The C 
header exposes the exact same architecture as every other binding, however C is notably the 
only language where you have to call the destructors manually.

### Installation

Ideally, install the precompiled dynamic C library using your package manager:

```sh
# macOS
brew tap fschutt/azul https://azul.rs/ui/brew.git
brew install fschutt/azul/azul

# Debian / Ubuntu
# (This adds the azul.rs apt repository to your sources.list, so you can receive updates via apt update)
echo "deb [trusted=yes] https://azul.rs/ui/apt stable main" | sudo tee /etc/apt/sources.list.d/azul.list
sudo apt update && sudo apt install azul

# Fedora / RHEL / openSUSE
sudo dnf config-manager --add-repo https://azul.rs/ui/rpm/azul.repo && sudo dnf install azul

# Arch Linux
echo '
[azul]
SigLevel = Optional TrustAll
Server = https://azul.rs/ui/arch/$arch' | sudo tee -a /etc/pacman.conf
sudo pacman -Sy azul

# Alpine Linux
echo "https://azul.rs/ui/alpine" | sudo tee -a /etc/apk/repositories
sudo apk add --allow-untrusted azul

# Windows
choco install libazul --source https://azul.rs/ui/nuget/index.json
# or
scoop bucket add azul https://azul.rs/ui/scoop.git && scoop install azul
```

Alternatively, download it next to your project from the [release page](https://azul.rs/ui/release/$VERSION):

```sh
# Linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so
# macOS M1+
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
# macOS Intel
curl -O https://azul.rs/ui/release/$VERSION/libazul.x86_64.dylib
# Windows
curl -O https://azul.rs/ui/release/$VERSION/azul.dll
curl -O https://azul.rs/ui/release/$VERSION/azul.dll.lib
```

Additionally, you need the C header:

```sh
curl -O https://azul.rs/ui/release/$VERSION/azul.h
```

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL gets built in `target/release/libazul.{so,dylib}` 
(or `azul.dll`). The C header is generated by `azul-doc codegen all` (alongside all other language bindings) 
and ends up at `target/codegen/azul.h`. Copy both somewhere your C compiler can find.

## Simple "Counter" Example

The simplest example to showcase Azul's model is only about ~60 lines long:

```
/* Detailed source-code truncated for AI context efficiency. */
```

### `AZ_REFLECT`

In difference to other bindings, we need to generate the upcast and downcast functions 
with the `AZ_REFLECT(...)` macro. It uses the compiler-generated type ID to safely check 
the downcasted type. The macro requires you to pass in the destructor function, which 
will run once the last instance of a `RefAny` is deleted. 

Alternatively, you can also use the `AZ_REFLECT_JSON` macro, which allows you to add 
JSON support for your class - this may in some cases be necessary, i.e. for introspection 
during debugging:

```c
typedef struct { uint32_t counter; } MyDataModel;
void MyDataModel_destructor(void* m) { }

// Macro has to be used before using the *Ref structs later on
AZ_REFLECT_JSON(MyDataModel, MyDataModel_destructor,
                MyDataModel_toJson, MyDataModel_fromJson);

AzJson MyDataModel_toJson(AzRefAny refany) {
    MyDataModelRef ref = MyDataModelRef_create(&refany);
    if (!MyDataModel_downcastRef(&refany, &ref)) {
        MyDataModelRef_delete(&ref);
        return AzJson_null();              // serialize failed -> null
    }
    int64_t counter = (int64_t)ref.ptr->counter;
    MyDataModelRef_delete(&ref);           // release borrow before building Json

    AzJsonKeyValue kv = AzJsonKeyValue_create(str("counter"), AzJson_int(counter));
    return AzJson_object(AzJsonKeyValueVec_fromItem(kv));   // { "counter": 5 }
}

AzResultRefAnyString MyDataModel_fromJson(AzJson json) {
    AzOptionJson field = AzJson_getKey(&json, str("counter"));
    if (field.None.tag == AzOptionJson_Tag_None) {
        return AzResultRefAnyString_err(str("Expected object with 'counter'"));
    }
    AzOptionI64 counter_opt = AzJson_asInt(&field.Some.payload);
    if (counter_opt.None.tag == AzOptionI64_Tag_None) {
        return AzResultRefAnyString_err(str("'counter' is not an integer"));
    }
    MyDataModel model = { .counter = (uint32_t)counter_opt.Some.payload };
    return AzResultRefAnyString_ok(MyDataModel_upcast(model));
}
```

### Further Notes

Every `_create` reference (like `MyDataModelRef_create`) must be paired with a `_delete` before 
returning - on the success *and* the failure path, since a missing `_delete` leaves the `RefAny` 
borrowed and the next downcast fails. On the API page, the classes that have `_delete` functions 
are marked with a "has destructor" label; all other types are "trivially copyable", e.g. `LogicalPosition` 
having only two floating point numbers for x and y - therefore not needing a `_delete` function. 

In the example we build the UI with `Dom` constructors like `AzDom_createPWithText` and `AzDom_createBody`. 
The styling is done via `AzDom_setCss`, which internally parses the string once and caches it. Note that 
in difference to the web, you can add a full CSS stylesheet here, including rules for `:hover { }`, `:focus { }`, 
`@media ... { }`. Alternatively, you can use `AzDom_addCssProperty` with the `AzCssProperty_*` constructors 
to avoid the parsing or define your properties as compile-time constants. 

Strings are passed across the FFI as length-prefixed UTF-8 buffers: for compile-time literals 
`AzString_fromConstStr` is zero-allocation and just takes a pointer to `.rodata` bytes - and 
therefore never deallocates the string. Meanwhile, runtime-built strings such as the `snprintf`'d 
counter need `AzString_copyFromBytes` so the buffer can outlive the `layout()` function returning. 

Finally, `AzRefAny_clone` just bumps the (atomic, thread-safe) reference count instead of doing a 
deep copy. That should be everything that is "C specific" about the API - everything else is the 
same for every binding.

## Build and run

If you installed `libazul` via a Linux package manager (`apt`, `rpm`, `pacman`, `apk`), 
the header and shared library live in standard system locations, so the compiler finds them automatically:

```sh
# Linux (apt / rpm / pacman / apk)
cc hello-world.c -lazul -o hello-world
```

If you used Homebrew on macOS, or a package manager on Windows, you need to point the compiler to the installation directory:

```sh
# macOS (Homebrew)
cc -I"$(brew --prefix)/include" -L"$(brew --prefix)/lib" hello-world.c -lazul -o hello-world

# Windows (Chocolatey) - in Developer Command Prompt
cl /I%AZ_LINK_PATH% hello-world.c %AZ_LINK_PATH%\azul.dll.lib /Fe:hello-world.exe

# Windows (Scoop) - in Developer Command Prompt
cl /I%USERPROFILE%\scoop\apps\azul\current hello-world.c %USERPROFILE%\scoop\apps\azul\current\azul.dll.lib /Fe:hello-world.exe
```

Note the `%AZ_LINK_PATH%`, which is set by the Chocolatey installation script automatically. 
If you downloaded the header and DLL manually (or built from source), you must point the compiler 
at them explicitly. `-I` / `-L` add include and link search paths; `-Wl,-rpath` tells the dynamic 
loader where to find `libazul.{so,dylib}` at runtime, so that you do not have to set 
`LD_LIBRARY_PATH` (Linux) or `DYLD_LIBRARY_PATH` (macOS) on every run. On Windows, `azul.dll` 
has to sit next to the resulting `.exe` at run time.

```sh
# Linux (manual download)
cc hello-world.c -I. -L. -lazul -ldl -lpthread -lm -Wl,-rpath,'$ORIGIN' -o hello-world

# macOS (manual download)
cc hello-world.c -I. -L. -lazul -Wl,-rpath,@executable_path -o hello-world

# Windows (manual download) - in Developer Command Prompt
cl /I. hello-world.c azul.dll.lib /Fe:hello-world.exe
```

After running the `hello-world` binary, you should see the window pictured on the 
[hello-world landing page](../hello-world.md). Click the button: the counter should increment, 
the layout callback then re-runs, and the new value renders.

1. `AzApp_run` opens a native window and runs the layout callback once with your `AzRefAny`.
2. The returned `AzDom` is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set up in the `AzDom`. 
   On click, the framework borrows your `AzRefAny` mutably, runs `on_click`, observes the `AzUpdate_RefreshDom` 
   return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's `AzDom` and the current one, and only 
   re-updates and re-paints the counter, not the entire window.

Congratulations if you made it this far - now you've already mastered 80% of the framework. 
As you might have guessed, more complex UI and styling are only composing more Dom objects 
together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or 
explore what [methods the `AzDom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Cpp (`doc/guide/en/hello-world/cpp.md`)

---
slug: hello-world/cpp
title: Hello World [C++]
language: en
canonical_slug: hello-world/cpp
audience: external
maturity: mature
guide_order: 13
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - core/src/callbacks.rs
  - core/src/lib.rs
  - dll/src/lib.rs
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Css
  - RefAny
  - WindowCreateOptions
  - LayoutCallbackInfo
  - CallbackInfo
  - Update
---

# Hello World [C++]

## Introduction

In order to use Azul from C++, you first need to install the native compiled `libazul` library
as well as the C bindings. The C++ bindings are then "extensions" of the C API, which add 
RAII types, builder methods and integration with the C++ standard container types on top.

Since C++ has shifted significantly between standards, there is one wrapper header per 
standard rather than a single "C++ header". Pick the one that matches what you compile with,
see below for the C++ features each header supports.

### Installation

Same as the [C installation](c.md#installation): Ideally, install the precompiled dynamic C 
library using your package manager (the C++ headers `azul03.hpp` … `azul23.hpp` are installed 
next to `azul.h`):

```sh
# macOS
brew tap fschutt/azul https://azul.rs/ui/brew.git
brew install fschutt/azul/azul

# Debian / Ubuntu
# (This adds the azul.rs apt repository to your sources.list, so you can receive updates via apt update)
echo "deb [trusted=yes] https://azul.rs/ui/apt stable main" | sudo tee /etc/apt/sources.list.d/azul.list
sudo apt update && sudo apt install azul

# Fedora / RHEL / openSUSE
sudo dnf config-manager --add-repo https://azul.rs/ui/rpm/azul.repo && sudo dnf install azul

# Arch Linux
echo '
[azul]
SigLevel = Optional TrustAll
Server = https://azul.rs/ui/arch/$arch' | sudo tee -a /etc/pacman.conf
sudo pacman -Sy azul

# Alpine Linux
echo "https://azul.rs/ui/alpine" | sudo tee -a /etc/apk/repositories
sudo apk add --allow-untrusted azul

# Windows
choco install libazul --source https://azul.rs/ui/nuget/index.json
# or
scoop bucket add azul https://azul.rs/ui/scoop.git && scoop install azul
```

Alternatively, download it next to your project from the [release page](https://azul.rs/ui/release/$VERSION). 
Every `azul<NN>.hpp` does `#include "azul.h"`, so you always need the C header as well:

```sh
# Linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so
curl -O https://azul.rs/ui/release/$VERSION/azul.h
curl -O https://azul.rs/ui/release/$VERSION/azul17.hpp   # or azul03/11/14/20/23.hpp

# macOS M1+
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
curl -O https://azul.rs/ui/release/$VERSION/azul.h
curl -O https://azul.rs/ui/release/$VERSION/azul17.hpp

# macOS Intel (saved as libazul.dylib, the name -lazul and the loader look for)
curl -o libazul.dylib https://azul.rs/ui/release/$VERSION/libazul.x86_64.dylib
curl -O https://azul.rs/ui/release/$VERSION/azul.h
curl -O https://azul.rs/ui/release/$VERSION/azul17.hpp

# Windows
curl.exe -O https://azul.rs/ui/release/$VERSION/azul.dll
curl.exe -O https://azul.rs/ui/release/$VERSION/azul.dll.lib
curl.exe -O https://azul.rs/ui/release/$VERSION/azul.h
curl.exe -O https://azul.rs/ui/release/$VERSION/azul17.hpp
```

The C++ bundle holds everything the compiler needs except the library itself - `azul.h`, 
all six `azul<NN>.hpp`, the `azul.cppm` module and one `hello-world-cpp<NN>.cpp` per standard - 
in one download:

```sh
curl -LO https://azul.rs/ui/release/$VERSION/azul-cpp-$VERSION.tar.gz
tar xzf azul-cpp-$VERSION.tar.gz
```

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL gets built in `target/release/libazul.{so,dylib}` 
(or `azul.dll`). The headers are generated by `azul-doc codegen all` (alongside all other language bindings) 
and end up at `target/codegen/`: `azul.h`, the six `azul<NN>.hpp` wrappers and `azul.cppm`, the C++20+ 
module interface unit. Copy the wrapper for your standard plus the DLL somewhere your compiler can find.

## Simple "Counter" Example

The simplest example to showcase Azul's model is only about ~45 lines long in C++17:

```cpp
#include "azul17.hpp"
#include <string>
#include <string_view>

using namespace azul;
using namespace std::string_view_literals;

struct MyDataModel {
    uint32_t counter;
};

ffi::Update on_click(ffi::RefAny data, ffi::CallbackInfo info);

ffi::Dom layout(ffi::RefAny data, ffi::LayoutCallbackInfo info) {
    RefAny data_wrapper(data);
    auto* d = data_wrapper.downcast_ref<MyDataModel>();
    if (!d) return Dom::create_body();

    Dom label = Dom::create_p_with_text(String(std::to_string(d->counter)))
        .with_css("font-size: 32px; margin: 0;"sv);

    Dom button = Button::create("Increase counter"sv)
        .with_button_type(ButtonType::Primary)
        .with_on_click(data_wrapper.clone(), on_click)
        .dom();

    return Dom::create_body()
        .with_child(std::move(label))
        .with_child(std::move(button));
}

ffi::Update on_click(ffi::RefAny data, ffi::CallbackInfo info) {
    RefAny data_wrapper(data);
    auto* d = data_wrapper.downcast_mut<MyDataModel>();
    if (!d) return Update::DoNothing;
    d->counter += 1;
    return Update::RefreshDom;
}

int main() {
    MyDataModel model = { 5 };
    RefAny data = RefAny::create(std::move(model));

    WindowCreateOptions window = WindowCreateOptions::create(layout);
    App app = App::create(std::move(data), AppConfig::create());
    app.run(std::move(window));
    return 0;
}
```

## Reflection

Your data model is stored type-erased in a `RefAny`, which you can instantiate with: 
`RefAny::create<T>(model)` and downcast with `refany.downcast_ref<T>()` and `refany.downcast_mut<T>()`. 
The compiler creates a unique runtime type ID per `T` - the destructor is generated along with it, 
so `~T()` runs when the last `RefAny` referencing your struct drops.

In `azul03.hpp` the same three functions come from a macro instead: `AZ_REFLECT(MyDataModel)` emits `MyDataModel_upcast` / `_downcast_ref` / `_downcast_mut`. 

To additionally make the `RefAny` serializable to JSON, the `AZ_REFLECT_JSON` macro additionally 
takes two serialization hooks - useful for introspection during debugging:

```cpp
ffi::Json MyDataModel_toJson(ffi::RefAny refany) {
    // Adopt the raw ffi::RefAny into the C++ wrapper
    RefAny data(refany);
    
    auto* d = data.downcast_ref<MyDataModel>();
    if (!d) return Json::null().release();
    // Use C++ azul::Json and azul::JsonKeyValue constructors
    auto kv = JsonKeyValue::create("counter"sv, Json::int_(d->counter));
    
    // .release() transfers ownership of the C++ wrapper back to the C ABI
    return Json::object(JsonKeyValueVec::from_item(std::move(kv))).release();
}

ffi::ResultRefAnyString MyDataModel_fromJson(ffi::Json ffi_json) {
    // Adopt the raw ffi::Json into the C++ wrapper
    Json json(ffi_json);

    // azul::Json uses std::optional-like wrappers for returns
    auto field = json.get_key("counter"sv);
    if (!field.has_value()) {
        return ResultRefAnyString::err("Expected object with 'counter'"sv).release();
    }

    auto counter_opt = field.value().as_int();
    if (!counter_opt.has_value()) {
        return ResultRefAnyString::err("'counter' is not an integer"sv).release();
    }

    // Wrap our model in a RefAny, and pack it into a success Result
    MyDataModel model;
    model.counter = static_cast<uint32_t>(counter_opt.value());
    return ResultRefAnyString::ok(RefAny::create(model)).release();
}

// NOTE: no destructor like in the C macro here, 
// because C++ calls ~MyDataModel() automatically
AZ_REFLECT_JSON(MyDataModel, MyDataModel_toJson, MyDataModel_fromJson);
```

`AZ_REFLECT_JSON(StructName, toJson, fromJson)` works with every header from `azul03.hpp` to 
`azul23.hpp`. `RefAny::create<T>(model)` on its own creates a `RefAny` without JSON hooks, so use 
the macro whenever the model should be serializable.

## Per-standard notes

### C++03

`azul03.hpp` has no support for move semantics and no `noexcept`. Returning non-copyable RAII objects 
by value works through the Colvin-Gibbons trick: each non-copyable class has a nested `Proxy` struct, 
the object converts to it when returned (releasing ownership) and the receiver constructs from it 
(acquiring ownership) - direct copy construction transfers ownership, like `std::auto_ptr`. 

The consequence is that these objects can **not** be stored safely in C++03 STL containers. 
Reflection goes through the `AZ_REFLECT(StructName)` / `AZ_REFLECT_JSON(StructName, toJson, fromJson)` 
macros shown above, and the header pulls in the C headers (`string.h`, `stdint.h`) rather than 
their `<c*>` variants.

### C++11

`azul11.hpp` adds real move semantics, `noexcept` everywhere, `enum class` and `std::function`. 
The `AZ_REFLECT` macro is replaced by the template members on `RefAny`: `RefAny::create<T>(model)` 
(with `T` deduced from the argument), `refany.downcast_ref<T>()`, `refany.downcast_mut<T>()` and 
`RefAny::type_id<T>()`.

### C++14

Same as C++11, plus the variable-template shorthand `RefAny::type_id_v<T>` for `RefAny::type_id<T>()` 
and `auto`-return functions. `RefAny::type_id_v` can be checked at runtime but not `static_assert`-ed.

### C++17

The standard this guide is written against. `azul17.hpp` adds:

- `std::string_view` sibling overloads on every method that takes a `String`, so `"foo"sv` 
  flows in without a `String(...)` wrapping step;
- `[[nodiscard]]` on factory and constructor methods;
- `Option<T>::toStdOptional() -> std::optional<Inner>` plus the matching implicit conversion.
  - If the payload has a wrapper class, `Inner` *is* that wrapper (`std::optional<String>`, not `std::optional<AzString>`)
  - For non-copy payloads the conversion is `&&`-qualified (consuming), so ownership moves into the `std::optional` exactly once;
- Structured bindings on every `ResultXxx` wrapper: `auto [ok, err] = std::move(result);` works 
  without per-class hooks.

### C++20

Everything from C++17, plus:

- `azul::ReflectableModel` concept, which constrains the `RefAny::create` / `downcast_ref` / 
  `downcast_mut` / `type_id` template members, so feeding a non-reflectable type produces a 
  readable requires-clause error instead of a wall of template-instantiation noise;
- `Vec<T>::toSpan() -> std::span<T>` for zero-copy access;
- A sibling `azul.cppm` module interface unit for C++20+ `import azul;` support: with a 
  modules-aware toolchain you can `import azul;` instead of `#include "azul20.hpp"` (build 
  commands below).

### C++23

Everything from C++20, plus:

- `Result<Ok, Err>::toStdExpected() && -> std::expected<Ok, Err>` and the matching implicit 
  conversion, so a method returning a `ResultXxx` wrapper can be assigned straight into a 
  `std::expected<Ok, Err>` and chained monadically with `.and_then` / `.or_else`. `Ok` / `Err` 
  are the wrapper classes where one exists (`std::expected<Url, UrlParseError>`, not 
  `std::expected<AzUrl, AzUrlParseError>`), and the expected owns them;
- deducing-`this` builder methods: every `with_*` is emitted as 
  `template<class Self> auto with_xxx(this Self&& self, …)`, so the same body works 
  on l-values and r-values without separate `const&` / `&&` overloads. This needs a compiler 
  that supports it (e.g. Clang 18+, GCC 14+); older compilers get the classic overloads 
  with the same behaviour.

The full set of examples lives under `examples/cpp/cpp<NN>/` in the repository; each standard's 
`hello-world.cpp` exercises that standard's own features.

### Further Notes

Callback signatures use the raw C types (`ffi::RefAny` aka `AzRefAny`, 
`ffi::CallbackInfo` aka `AzCallbackInfo`, `ffi::Update` aka `AzUpdate`), 
because a function pointer has to match the C signature exactly. 

In the function body itself you can use the C++ types, but you **have to** convert
the raw `ffi::` type into the C++ type first, so that C++ can automatically 
call destructors:

```cpp
ffi::Update on_click(ffi::RefAny data, ffi::CallbackInfo info) {
    azul::RefAny data_wrapper(data); // Adopt into C++ RAII!
    // ... now use data_wrapper
}
```

This guarantees that the destructor calls `AzRefAny_delete` on drop, so that
the reference count is properly decreased again. If you skip this, you may leak memory.

When returning from the callback, the opposite conversion (from the wrapper C++ 
type back to the raw C type) happens via a `move`. For temporary values (r-values), 
this is automatic:

```cpp
return Dom::create_body()
```

However, for named local variables (l-values), you must explicitly tell C++ 
to give up ownership: 

```cpp
return std::move(my_dom_node);
```

The `.with_*` builders consume `*this` and return a new value, so chain them inline; the sibling 
`set_*` methods (e.g. `Button::set_on_click`) mutate in place instead. This is effectively the same 
behaviour as in Rust.

Simple enums are emitted as namespaces of value constants with the Rust spelling 
(`Update::RefreshDom`, `ButtonType::Primary`) whose type is the raw C enum, so they 
pass through arguments and C function-pointer returns without casts. The callback 
typedefs get non-prefixed aliases (`azul::CallbackType`, `azul::LayoutCallbackType`, …) 
for storing callbacks in your own code.

The rest behaves as in every binding: the UI is built from `Dom` constructors like 
`Dom::create_p_with_text`, `with_css` parses the string once and caches it - and in difference to the 
web you can pass a full stylesheet, including `:hover { }`, `:focus { }` and `@media ... { }` rules - 
and `RefAny::clone` just bumps the (atomic, thread-safe) reference count instead of deep-copying.

## Build and run

If you installed `libazul` via a Linux package manager (`apt`, `rpm`, `pacman`, `apk`), the 
header and shared library live in standard system locations, so the compiler finds them automatically:

```sh
# Linux (apt / rpm / pacman / apk)
g++ -std=c++17 hello-world.cpp -lazul -o hello-world
```

If you used Homebrew on macOS, or a package manager on Windows, you need to point 
the compiler to the installation directory:

```sh
# macOS (Homebrew)
c++ -std=c++17 $(pkg-config --cflags --libs azul) hello-world.cpp -o hello-world

# Windows (Chocolatey) - in Developer Command Prompt
cl /std:c++17 /EHsc /I%AZ_LINK_PATH% hello-world.cpp %AZ_LINK_PATH%\azul.dll.lib /Fe:hello-world.exe

# Windows (Scoop) - in Developer Command Prompt
cl /std:c++17 /EHsc /I%USERPROFILE%\scoop\apps\azul\current hello-world.cpp %USERPROFILE%\scoop\apps\azul\current\azul.dll.lib /Fe:hello-world.exe
```

Note the `%AZ_LINK_PATH%`, which is set by the Chocolatey installation script automatically. 
If you downloaded the headers and DLL manually (or built from source), you must point the compiler 
at them explicitly. 

`-I` / `-L` add include and link search paths; `-Wl,-rpath` tells the dynamic 
loader where to find `libazul.{so,dylib}` at runtime, so that you do not have to set 
`LD_LIBRARY_PATH` (Linux) or `DYLD_LIBRARY_PATH` (macOS) on every run. On Windows, `azul.dll` 
has to sit next to the resulting `.exe` at run time.

```sh
# Linux (manual download)
g++ -std=c++17 hello-world.cpp -I. -L. -lazul -ldl -lpthread -lm -Wl,-rpath,'$ORIGIN' -o hello-world

# macOS (manual download)
c++ -std=c++17 hello-world.cpp -I. -L. -lazul -Wl,-rpath,@executable_path -o hello-world

# Windows (manual download) - in Developer Command Prompt
cl /std:c++17 /EHsc /I. hello-world.cpp azul.dll.lib /Fe:hello-world.exe

# C++03 (Linux) - same DLL, different wrapper header (azul03.hpp)
g++ -std=c++03 hello-world.cpp -I. -L. -lazul -ldl -lpthread -lm -Wl,-rpath,'$ORIGIN' -o hello-world

# C++20+ with modules (Clang 16+): build the module from the sibling azul.cppm once,
# then replace the #include with `import azul;` in your source files
clang++ -std=c++20 --precompile azul.cppm -o azul.pcm
clang++ -std=c++20 -fmodule-file=azul=azul.pcm hello-world.cpp azul.pcm -L. -lazul -o hello-world
# (the header comment of azul.cppm lists the GCC and MSVC equivalents)
```

After running the `hello-world` binary, you should see the window pictured on the 
[hello-world landing page](../hello-world.md). Click the button: the counter should increment, 
the layout callback then re-runs, and the new value renders.

1. `app.run(std::move(window))` opens a native window and runs the layout callback once with your `RefAny`.
2. The returned `Dom` is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set up in the `Dom`. On click, the framework borrows your `RefAny` mutably, runs `on_click`, observes the `Update::RefreshDom` return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's `Dom` and the current one, and only re-updates and re-paints the counter, not the entire window.

Congratulations if you made it this far - now you've already mastered 80% of the framework. 
As you might have guessed, more complex UI and styling are only composing more Dom objects 
together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or 
explore what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Crystal (`doc/guide/en/hello-world/crystal.md`)

---
slug: hello-world/crystal
title: Hello World [Crystal]
language: en
canonical_slug: hello-world/crystal
audience: external
maturity: mature
guide_order: 29
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/crystal/hello-world.cr
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-19T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - RefAny
  - WindowCreateOptions
  - Update
---

# Hello World [Crystal]

## Introduction

In order to use `libazul.so` from Crystal, you need the native library and
the generated API bindings which wrap the C API in ordinary Crystal classes.

## Installation

First, you need to install Crystal: `brew install crystal` on macOS, or install it 
from [crystal-lang.org](https://crystal-lang.org/install/).

Dependencies in Crystal are managed with `shards`, and Azul publishes the
generated binding as a shard in a git repository - the same way the Homebrew
tap and the Scoop bucket are served. `shards install` clones it into `lib/`,
which is where `require "azul"` looks.

Create a directory for your project and download the manifest and the
example:

```sh
mkdir hello-world && cd hello-world
curl -O https://azul.rs/ui/release/$VERSION/shard.yml
curl -O https://azul.rs/ui/release/$VERSION/hello-world.cr
```

The manifest declares `azul` as a git dependency on
`https://azul.rs/ui/crystal.git`. The shard is the *binding* only - the native
library is a separate download, because it is platform-specific:

```sh
# macOS
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
# Linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so
# Windows
curl -O https://azul.rs/ui/release/$VERSION/azul.dll
curl -O https://azul.rs/ui/release/$VERSION/azul.dll.lib
```

Now `shards install` clones the shard into `lib/azul/`, and `crystal build`
compiles against it:

```sh
shards install

# macOS
crystal build hello-world.cr --link-flags "-L$PWD -framework Foundation -framework AppKit -framework OpenGL -framework CoreGraphics -framework CoreText"
# Linux
crystal build hello-world.cr --link-flags "-L$PWD"
# Windows
crystal build hello-world.cr --link-flags "azul.dll.lib"

DYLD_LIBRARY_PATH=. ./hello-world   # macOS
LD_LIBRARY_PATH=. ./hello-world     # Linux
```

The library path must be absolute (`-L$PWD`, not `-L.`): Crystal runs
the linker from its own cache directory, so a relative `-L.` points
there and the link fails. The binary embeds no rpath, which
is why the run step needs `LD_LIBRARY_PATH=.` / `DYLD_LIBRARY_PATH=.`.

### Manual Compilation

If you prefer a single-file script without a `shard.yml`, you can manually download 
the single-file wrapper to a `lib/` folder:

```sh
mkdir -p lib && curl -o lib/azul.cr https://azul.rs/ui/release/$VERSION/lib/azul.cr
# Then compile directly using `crystal build`
```

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL lands in
`target/release/libazul.{so,dylib}` (or `azul.dll`). The bindings end up in
the `target/codegen/` folder: `azul.cr` is the one-file binding, and 
`target/codegen/crystal/` is the same code as a shard (`shard.yml` + `src/`). 
Copy that directory to `lib/azul/` and `require "azul"` picks it up the same way.

## Simple "Counter" Example

```crystal
require "azul"

class Counter
  property count : Int32

  def initialize(@count = 5)
  end
end

def on_click(counter : Counter, info : Azul::CallbackInfo) : Azul::Update
  counter.count += 1
  Azul::Update::RefreshDom
end

def layout(counter : Counter, info : Azul::LayoutCallbackInfo) : Azul::Dom
  label = Azul::Dom.p_with_text(counter.count.to_s)
    .with_css("font-size: 32px; margin: 0;")

  button = Azul::Button.new("Increase counter")
    .with_button_type(:primary)
    .with_on_click(counter, ->on_click(Counter, Azul::CallbackInfo))

  Azul::Dom.body
    .with_child(label)
    .with_child(button.dom)
end

window = Azul::WindowCreateOptions.new(->layout(Counter, Azul::LayoutCallbackInfo))
window.window_state.title = "Hello World"
window.window_state.size.dimensions.width = 400
window.window_state.size.dimensions.height = 300

app = Azul::App.new(Counter.new, Azul::AppConfig.new)
app.run(window)
```

## Binding Internals

Internally, Azul wraps the object returned by `Counter.new` in a `Azul::Handles`
table and only stores that numeric ID, then uses it in the automatic downcasting
(before invoking the `def layout` with it). This is relatively standard practice 
for GC languages, so that the garbage collector doesn't delete the object while 
it's still needed for the next `layout()` call.

Additionally, callbacks are methods that take the model as its own type, such as
`->on_click(Counter, Azul::CallbackInfo)` - this way we know exactly what we need to
downcast to again when invoking the callback.

Handing the same `counter` to several callbacks shares one object: a `RefAny` clone 
is another reference to it, not a copy. `layout` gets its `Counter` the same way.

The bindings allow you to use native Crystal symbols as enum arguments
(`with_button_type(:primary)`) and strings convert to `AzString` automatically.

## Build and run

 From the directory containing your `shard.yml` and the native library, 
 build and run your application:

```sh
# Linux
crystal build hello-world.cr --link-flags "-L$PWD"
LD_LIBRARY_PATH=. ./hello-world

# macOS
crystal build hello-world.cr --link-flags "-L$PWD -framework Foundation -framework AppKit -framework OpenGL -framework CoreGraphics -framework CoreText"
DYLD_LIBRARY_PATH=. ./hello-world

# Windows (PowerShell)
crystal build hello-world.cr --link-flags "$PWD\azul.dll.lib"
.\hello-world.exe
```

On macOS the five `-framework` flags are required. Add `--release` for 
an optimized build; the first build of `azul.cr` takes a few seconds.

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `app.run` opens a native window and runs `layout` once with your `Counter`.
2. The returned `Dom` is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter 
   set up in the `Dom`. On click, the framework borrows your model mutably, runs `on_click`, 
   observes the `Azul::Update::RefreshDom` return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's `Dom` and the current one, 
   and only re-updates and re-paints the counter, not the entire window.

Congratulations - once you've got the hello-world example running, you've already mastered 
80% of the framework. As you might have guessed, more complex UI and styling are only composing 
more Dom objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or explore 
what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Csharp (`doc/guide/en/hello-world/csharp.md`)

---
slug: hello-world/csharp
title: Hello World [C#]
language: en
canonical_slug: hello-world/csharp
audience: external
maturity: mature
guide_order: 15
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/csharp/hello-world.cs
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - WindowCreateOptions
  - Update
---

# Hello World [C#]

## Introduction

In order to use C# (.NET 8+) with the prebuilt `libazul` library, you need the rendered 
generated `Azul.cs` binding glue, which talks to `libazul` over P/Invoke. Internally, 
the bindings handle C# specific things such as making sure the objects are alive long 
enough and that the garbage collector doesn't accidentally move them. However, in 
regular C#, you'll never come across this, so this is just documented here for completeness.

## Installation

Azul isn't (yet) on nuget.org, but you can install the `libazul` package by using 
the `azul.rs` mirror. It will pick the correct binary artifact for Linux/macOS/Windows 
and store it under `runtimes/<rid>/native`. The package ID is `Azul.Net`, the assembly 
and namespace are still `Azul`:

```sh
dotnet nuget add source https://azul.rs/ui/nuget/index.json --name azul
dotnet add package Azul.Net --version $VERSION
```

Alternatively, you can install the native library and binding by hand:

1. Download the native library from the [release page](https://azul.rs/ui/release/$VERSION) 
   and keep it next to your binary (or on the loader path):

   ```sh
   # macOS (Apple Silicon; Intel: libazul.x86_64.dylib)
   wget -O libazul.dylib https://azul.rs/ui/release/$VERSION/libazul.dylib
   # Linux
   wget -O libazul.so    https://azul.rs/ui/release/$VERSION/libazul.so
   # Windows (PowerShell)
   Invoke-WebRequest -Uri "https://azul.rs/ui/release/$VERSION/azul.dll" -OutFile "azul.dll"
   ```

2. Add the generated `Azul.cs` bindings to your project:

   ```sh
   # Azul.cs ships with the GitHub release
   curl -L -O https://github.com/fschutt/azul/releases/download/$VERSION/Azul.cs
   # optional project scaffold:
   wget https://azul.rs/ui/release/$VERSION/Azul.csproj
   ```

The generated `Azul.cs` installs a `DllImportResolver` which automatically 
discovers the native library in the current working directory when running 
`dotnet run`.

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL is built in 
`target/release/libazul.{so,dylib}` (or `azul.dll`). The header/bindings 
are previously generated by `azul-doc codegen all` and end up at 
`target/codegen/`. Copy both somewhere `dotnet` can find them.

## Simple "Counter" Example

```csharp
using System;
using Azul;

namespace HelloWorld
{
    public sealed class MyDataModel
    {
        public uint Counter;
        public MyDataModel(uint counter) { Counter = counter; }
    }

    public static class Program
    {
        private static readonly MyDataModel _model = new MyDataModel(5);

        private static Update OnClick(MyDataModel m, CallbackInfo info)
        {
            m.Counter += 1;
            return Update.RefreshDom;
        }

        private static Dom Layout(MyDataModel m, LayoutCallbackInfo info)
        {
            var label = Dom.CreatePWithText(m.Counter.ToString())
                .WithCss("font-size: 32px; margin: 0;");
            var buttonDom = Button.Create("Increase counter")
                .WithButtonType(ButtonType.Primary)
                .OnClick(m, OnClick)
                .Dom();
            return Dom.CreateBody()
                .WithChild(label)
                .WithChild(buttonDom);
        }

        public static int Main(string[] args)
        {
            using var app = App.Create(_model, AppConfig.Create());
            app.Run(WindowCreateOptions.Create<MyDataModel>(Layout));
            return 0;
        }
    }
}
```

While the bindings use things like `IntPtr` and a `HostInvoker` under the hood, 
these internals are abstracted away in the bindings, so the above example reads 
as "native" C#. 

Note that `WithCss("...")` accepts any CSS string, including inline 
`:hover { }`, `@media ... { }` and `@os(...)` queries. Callbacks are 
strongly-typed delegates:

- A layout callback is `Func<MyDataModel, LayoutCallbackInfo, Dom>`
- A click handler is `Func<MyDataModel, CallbackInfo, Update>`
- Same for other callbacks and their arguments / return types

The generated `Azul.cs` bindings automatically handle wrapping your data model 
into a type-erased `RefAny` handle and, crucially, perform the safe runtime downcast 
(as `T`) before invoking your callback. 

If the type mismatches, the wrapper catches it and returns `Update.DoNothing` (or an empty `Dom`), 
meaning there is no `IntPtr` ceremony or manual upcasting / downcasting code needed. However, this
should never be possible since the bindings also enforce proper type matching in the `.OnClick` 
registration of the callback. 

Using `var app` disposes deterministically: `Dispose()` calls the C-side `_delete()`, 
so the native memory is released when the `App` goes out of scope.

## Build and run

```sh
# All platforms - copy the native library into the project root,
# and the generated DllImportResolver finds it there
dotnet run
```

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the 
new value renders.

1. `app.Run(...)` opens a native window and runs the layout callback once with your data model.
2. The returned `Dom` is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set up in the `Dom`. On click, the framework borrows your data model mutably, runs the click callback, observes the `Update.RefreshDom` return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's `Dom` and the current one, and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 
80% of the framework. As you might have guessed, more complex UI and styling are only composing 
more `Dom` objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or explore 
what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World D (`doc/guide/en/hello-world/d.md`)

---
slug: hello-world/d
title: Hello World [D]
language: en
canonical_slug: hello-world/d
audience: external
maturity: mature
guide_order: 28
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/d/hello-world.d
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-19T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - RefAny
  - WindowCreateOptions
  - Update
---

# Hello World [D]

## Introduction

In order to use `libazul` from D (`dmd` 2.113+ or `ldc2` 1.43+), you need the native 
library and the `azul.d` module, which wraps the underlying C API. 

## Installation

The preferred way to use Azul in D is via the `dub` package manager. Azul 
provides a pre-configured `dub` package containing the API wrappers split 
into parallel-compilable modules (`source/azul/*.d`), which compiles 
significantly faster than the single-file module.

First, create a new `dub` project and download the native `libazul` engine 
for your operating system into the project root:

```sh
mkdir hello-world && cd hello-world
dub init -n .

# macOS
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
# Linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so
# Windows
curl -O https://azul.rs/ui/release/$VERSION/azul.dll
curl -O https://azul.rs/ui/release/$VERSION/azul.dll.lib
```

Next, download and extract the Azul `dub` package:

```sh
curl -LO https://azul.rs/ui/release/$VERSION/azul-d-$VERSION.tar.gz
mkdir azul-d && tar xzf azul-d-$VERSION.tar.gz -C azul-d
```

Update your app's `dub.json` to link the local `azul-d` package and the native library:

```json
{
    "name": "hello-world",
    "dependencies": {
        "azul": { "path": "./azul-d" }
    },
    "lflags-posix": ["-L."],
    "lflags-osx": [
      "-framework", "Foundation", 
      "-framework", "AppKit", 
      "-framework", "OpenGL", 
      "-framework", "CoreGraphics", 
      "-framework", "CoreText"
    ],
    "lflags-windows": ["+azul.dll.lib"]
}
```

Now, simply copy the "Counter" example below into your `source/app.d` file and run the project:

```sh
# macOS / Linux
LD_LIBRARY_PATH=. DYLD_LIBRARY_PATH=. dub run

# Windows
dub run
```

### Manual Compilation

If you prefer to compile manually without a package manager, you 
can download the single-file wrapper (`azul.d`) and compile everything directly:

```sh
curl -O https://azul.rs/ui/release/$VERSION/azul.d
curl -O https://azul.rs/ui/release/$VERSION/hello-world.d

# macOS
ldc2 hello-world.d azul.d -L-L. -L-lazul \
  -L-framework -LFoundation -L-framework -LAppKit -L-framework -LOpenGL \
  -L-framework -LCoreGraphics -L-framework -LCoreText -of=hello-world
DYLD_LIBRARY_PATH=. ./hello-world

# Linux
dmd hello-world.d azul.d -L-L. -L-lazul -of=hello-world
LD_LIBRARY_PATH=. ./hello-world

# Windows
dmd hello-world.d azul.d -L/LIBPATH:. azul.dll.lib -of=hello-world.exe
hello-world.exe
```

Note on the weird `-L-L. -L-lazul`: 

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL lands in 
`target/release/libazul.{so,dylib}` (or `azul.dll`). The bindings are generated 
in `target/codegen/`: `azul.d` is the one-file module, and `target/codegen/d/` 
is the same code as a dub package (one module per API area - which compiles faster 
in parallel).

## Simple "Counter" Example

```d
module hello_world;

import azul;
import std.conv : to;

final class Counter
{
    int count = 5;
}

Update onClick(Counter counter, CallbackInfo info)
{
    counter.count += 1;
    return Update.refreshDom;
}

Dom layout(Counter counter, LayoutCallbackInfo info)
{
    auto label = Dom.pWithText(counter.count.to!string)
        .withCss("font-size: 32px; margin: 0;");

    auto button = Button("Increase counter")
        .withButtonType(ButtonType.primary)
        .withOnClick(counter, &onClick);

    return Dom.body()
        .withChild(label)
        .withChild(button.dom());
}

void main()
{
    auto window = WindowCreateOptions(&layout);
    window.windowState.title = "Hello World";
    window.windowState.size.dimensions.width = 400;
    window.windowState.size.dimensions.height = 300;

    auto app = App(new Counter, AppConfig());
    app.run(window);
}
```

## Internals

Internally, the `new Counter` is tracked via a `GC.addRoot`, so that the
GC doesn't free the object while the layout callback still needs it. Internally, 
the D object is wrapped in an opaque `RefAny`

`onClick` automatically downcasts said `RefAny` again automatically and
returns `Update.doNothing` if the downcast fails (with a log message).
The API auto-converts strings to the expected Rust structs.

## Build and run

If you used the `dub` installation, running your application is as simple as:

```sh
# macOS / Linux
LD_LIBRARY_PATH=. DYLD_LIBRARY_PATH=. dub run

# Windows
dub run
```

If you compiled manually, run the resulting executable ensuring the native 
library is in the library path.

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `app.run` opens a native window and runs `layout` once with your `Counter`.
2. The returned `Dom` is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set 
   up in the `Dom`. On a click event, the framework borrows your model mutably, runs `onClick`, 
   observes the `Update.refreshDom` return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's `Dom` and the current one, 
   and only re-updates and re-paints the counter, not the entire window.

Congratulations - once you've got the hello-world example running, you've already mastered 80% 
of the framework. As you might have guessed, more complex UI and styling are only composing more 
Dom objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or explore 
what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Fortran (`doc/guide/en/hello-world/fortran.md`)

---
slug: hello-world/fortran
title: Hello World [Fortran]
language: en
canonical_slug: hello-world/fortran
audience: external
maturity: mature
guide_order: 26
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/fortran/hello_world.f90
  - doc/src/codegen/v2/lang_fortran/makefile.rs
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - RefAny
  - WindowCreateOptions
  - Update
---

# Hello World [Fortran]

## Introduction

To use `libazul` from Fortran, you need the prebuilt native library and the generated
Fortran binding: a set of modules that talk to the library through `iso_c_binding`,
behind one `azul` module. The binding is tested with gfortran and needs a Fortran 2003
compiler.

Every API class is a derived type with type-bound procedures (`dom_t`, `button_t`,
`app_t`), strings are plain `character`, enum values are integer constants
(`ButtonType_Primary`, `Update_RefreshDom`) and callbacks are ordinary module functions.
Your data model is any type you define: the binding keeps it and hands it back to each
callback as `class(*)`, and `select type` turns it back into your type.

## Installation

You need gfortran and GNU make (macOS: `brew install gcc`, Debian / Ubuntu:
`sudo apt install gfortran make`, Windows: the MinGW-w64 gfortran from MSYS2).

The release bundle `azul-fortran-$VERSION.tar.gz` contains the generated modules,
a `Makefile` and the counter example. Unpack it next to the native library:

```sh
mkdir hello-world && cd hello-world
curl -LO https://azul.rs/ui/release/$VERSION/azul-fortran-$VERSION.tar.gz
tar xzf azul-fortran-$VERSION.tar.gz

# macOS
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
# Linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so
# Windows
curl -O https://azul.rs/ui/release/$VERSION/azul.dll

make -j8
./hello_world
```

The first `make` compiles the binding, which takes about two minutes with `-j8`. After
that, `make` only recompiles your own source. The executable looks for the library in its
own directory, so you can run it from anywhere. If the library lives somewhere else, for
example installed with Homebrew, pass its directory: `make LIBDIR="$(brew --prefix)/lib"`.

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL lands in
`target/release/libazul.{so,dylib}` (or `azul.dll`). The Fortran binding is
generated into `target/codegen/fortran/`, together with its `Makefile`. Copy both
next to your program.

## Simple "Counter" Example

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Notes

In the example, `use azul, only: ...` imports only the names you use. A bare `use azul` 
also works, but then gfortran loads the whole binding for your file, which takes tens of 
seconds instead of one.
   
`app_create(model_t(counter=5), ...)` copies your model into the binding. Each
callback receives that copy as `class(*), intent(inout) :: model`, and changes made
inside `type is (model_t)` are kept: `class(*)` is "any type", the model as
libazul holds it, and `select type` is the checked downcast back to yours.

A model of any other type is a programming error, so `class default` reports it through
`info%log` - which reaches the application's log sink, not just the terminal -
and returns the callback's no-op result (`Update_DoNothing`).

`call button%with_on_click(model, on_click)` binds the model the layout callback is
running with, not a copy, so the click changes the same counter.

`layout` and `on_click` are ordinary module functions. Their dummy arguments must
match the binding's interfaces (`class(*), intent(inout)` for the model, `intent(inout)` 
for `info`); a mismatch is a compile error.

Methods like `with_css` or `with_child` change the object in place and are called
with `call`. `button%dom()` turns the button into a `dom_t`. Arguments are passed 
by-value and "moved" into whatever theyre used in, so after `call body%with_child(label)` 
you shouldn't pass `label` on again or `%delete()` it. The API prevents double-frees 
internally by making the n+1th `free()`. a no-op

A callback that returns an invalid result, such as an `integer` that is not an
`Update` value or a `dom_t` that was never assigned, does not reach the engine.
Similar to downcasting errors, the bindings log the problem and uses the default 
result (`Update_DoNothing`) instead:

```
[azul][error] azul: ButtonOnClickCallback expected an Update (0 to 2), got 7
```

### Class Constructors

In modern Fortran, you can often use the type name as a constructor 
(e.g., `button_t('Increase counter')` or `app_t(model, config)`). Under the hood, 
this routes to the explicit constructor function (like `button_create(...)`). You 
can use either spelling freely.

However, some types like `dom_t` have dozens of constructors with identical argument 
signatures (e.g., `create_body()` and `create_div()` both take zero arguments). 
Because Fortran cannot resolve overloaded interfaces with identical signatures, `dom_t` 
does not have a generic constructor. For these types, you must call the explicit creation 
functions by name, such as `dom_create_body()` or `dom_create_p_with_text(text)`.

## Build and run

```sh
make
./hello_world
```

You should see the window pictured on the [hello-world landing page](../hello-world.md).
Click the button: the counter should increment, the layout callback then re-runs, and the
new value renders.

1. `app%run(...)` opens a native window and runs the layout callback once with your model.
2. The returned `dom_t` is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set up
   in the DOM. On click, the framework borrows your data model mutably, runs the click callback,
   observes the `Update_RefreshDom` return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one,
   and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 80%
of the framework. As you might have guessed, more complex UI and styling are only composing
more `dom_t` objects together and working with the various event filters.

You can now start reading about the [architecture patterns](../architecture.md) or
explore what [methods the `Dom` has to offer](../dom.md).

See you in the next tutorial!

---

### Doc Guide En Hello World Go (`doc/guide/en/hello-world/go.md`)

---
slug: hello-world/go
title: Hello World [Go]
language: en
canonical_slug: hello-world/go
audience: external
maturity: mature
guide_order: 17
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/go/main.go
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-18T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
---

# Hello world [Go]

In order to use the `libazul` library from Go (1.21+), you will need to install the
Go bindings from `azul.rs/ui/go`, which provide a fully idiomatic wrapper over the C API.
Internally, they use `ebitengine/purego` to dynamically load the shared library and 
handle callback trampolines at runtime.

Because this relies on `purego`, you do not need a C compiler to build or cross-compile your app, 
making single-binary deployments and cross-platform builds trivial.

## Installation

The easiest way to get started is to create a new module and fetch the bindings using `go get`:

```sh
mkdir hello-world && cd hello-world
go mod init hello-world
go get azul.rs/ui/go
```

Then download the pre-compiled native library for your platform into the same folder:

```sh
# macOS
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
# Linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so
# Windows
curl -O https://azul.rs/ui/release/$VERSION/azul.dll
```

## Simple "Counter" Example

```go
package main

import (
	"fmt"

	azul "azul.rs/ui/go"
)

type counterModel struct {
	Counter int
}

func onClick(model *counterModel, _ *azul.CallbackInfo) azul.Update {
	model.Counter++
	return azul.Update_RefreshDom
}

func layout(model *counterModel, _ *azul.LayoutCallbackInfo) *azul.Dom {
	body := azul.DomCreateBody()
	label := azul.DomCreatePWithText(azul.Str(fmt.Sprintf("%d", model.Counter)))

	button := azul.ButtonCreate(azul.Str("Increase counter"))
	button.OnClick(model, azul.Bind(onClick))

	body.SetCss(azul.Str("p { font-size: 32px; margin: 0; }"))
	body.AddChild(label)
	body.AddChild(button.Dom())
	
	return body
}

func main() {
	// Dynamically load the embedded/downloaded shared library
	if err := azul.LoadLibrary(""); err != nil {
		panic(err)
	}

	data := &counterModel{Counter: 5}
	window := azul.WindowCreateOptionsCreate(azul.Bind(layout))
	app := azul.AppCreate(data, azul.AppConfigCreate())
	app.Run(window)
}
```

There are certain Go-specific things this example shows:

1. Everything is a flat `azul` namespace (otherwise, Go would have tons of problems with recursive imports).
2. Use `azul.Bind` to create the callbacks and `azul.Str` to create strings. These are the only 
   "special" functions that don't exist in the API bindings (otherwise you'd have to manage pointers manually).
3. `azul.LoadLibrary(path)` is required at startup in order to initialize the library - if `path` is an empty string,
   the function will look for the `.dll` in the current working directory. The idea is that you can `//go:embed` the `.dll` itself and then unpack it to a certain path before initializing for a "single binary" deployment.
4. The callbacks will auto-upcast and auto-downcast from `any` to `RefAny` to reduce manual boilerplate. If the
   downcast fails, it will log an error and return whatever the default is (i.e. `Update_DoNothing` or 
   `azul.DomBody()`), to keep the app running, yet also log the error:

  ```
  azul.Bind: type assertion failed, expected *main.profileModel, got *main.counterModel
  ```

See the [Observability Guide](../deploying/observability.md) for how to monitor live applications with Prometheus 
and Grafana for live errors (which would show you the `azul.Bind` errors) - additionally, you can write 
unit tests with coverage to make sure no downcast errors happen and that `stdout` is clean of warnings.

### Build and run

```sh
go run .
```

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the 
new value renders.

1. `app.Run(...)` opens a native window and runs the layout callback once with your data model.
2. The returned `Dom` is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set up in the `Dom`. On click, the framework borrows your data model mutably, runs the click callback, observes the `Update.RefreshDom` return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's `Dom` and the current one, and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 
80% of the framework. As you might have guessed, more complex UI and styling are only composing 
more `Dom` objects together and working with the various event filters. 
### Cross-compilation

Cross-compilation is completely native and frictionless. Because the bindings are 100% pure Go, you don't need a C cross-compiler (like `mingw-w64`) or any special environment variables.

For example, to compile a Windows executable from a Linux or macOS host, simply use `GOOS=windows`:

```sh
# Fetch the Go package
mkdir hello-world && cd hello-world
go mod init hello-world
go get azul.rs/ui/go

# Fetch the Windows DLL target
curl -O https://azul.rs/ui/release/$VERSION/azul.dll

# Cross compile to Windows NATIVELY (no mingw required!)
export GOOS=windows
export GOARCH=amd64
go build -o hello-world.exe .
```

Congratulations! Once you've got the hello-world example running, you've already mastered 80% of the framework. You can now start reading about the [architecture patterns](../architecture.md) or explore what [methods the `Dom` has to offer](../dom.md).

---

### Doc Guide En Hello World Haskell (`doc/guide/en/hello-world/haskell.md`)

---
slug: hello-world/haskell
title: Hello World [Haskell]
language: en
canonical_slug: hello-world/haskell
audience: external
maturity: mature
guide_order: 27
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/haskell/HelloWorld.hs
  - examples/haskell/azul-example.cabal
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - RefAny
  - WindowCreateOptions
  - Update
---

# Hello World [Haskell]

## Introduction

To use `libazul` from Haskell, you need the native prebuilt library and the generated `azul`
cabal package, which calls the C API through GHC's foreign function interface. The bindings 
convert between your data model and the `RefAny` which the framework stores, and passes structs 
through small C shims that cabal compiles with it. The package is tested with GHC 9.6 and 9.14.

## Installation

You need GHC and cabal, for example from [GHCup](https://www.haskell.org/ghcup/) or with
`brew install ghc cabal-install`. On Windows, use the MSYS2 shell that GHCup installs.
The release bundle contains the example (`HelloWorld.hs`, `azul-example.cabal`,
`cabal.project`) and the generated `azul` package in `azul-haskell/`.

```sh
mkdir hello-world && cd hello-world

# macOS
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
# Linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so
# Windows 
curl -O https://azul.rs/ui/release/$VERSION/azul.dll

curl -LO https://azul.rs/ui/release/$VERSION/azul-haskell-$VERSION.tar.gz
tar xzf azul-haskell-$VERSION.tar.gz
cabal run --extra-lib-dirs=$PWD hello-world
```

`--extra-lib-dirs` tells the linker where the native library is. 
It must be an absolute path, and it must stay the same between runs: cabal
treats it as part of the build configuration and rebuilds the whole package 
when it changes.

The first run compiles the generated package (about a thousand small modules 
and the C shims) in parallel, which takes about 2-3 minutes on an 8-core machine. 
Later runs only compile your own code.

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL lands in
`target/release/libazul.{so,dylib}` (or `azul.dll`), the cabal package 
in `target/codegen/haskell/`. The package also needs the C header in 
its `cbits/` directory:

```sh
cp -R target/codegen/haskell my_app/azul-haskell
cp target/codegen/azul.h my_app/azul-haskell/cbits/
cp examples/haskell/{HelloWorld.hs,azul-example.cabal,cabal.project} my_app/
cp target/release/libazul.dylib my_app/
```

## Simple "Counter" Example

```haskell
module Main where

import Azul
import qualified Azul.App as App
import qualified Azul.AppConfig as AppConfig
import qualified Azul.Button as Button
import qualified Azul.Dom as Dom
import qualified Azul.WindowCreateOptions as WindowCreateOptions

newtype DataModel = DataModel { counter :: Int }

onClick :: DataModel -> CallbackInfo -> (DataModel, Update)
onClick model _ = (model { counter = counter model + 1 }, Update_RefreshDom)

layout :: DataModel -> LayoutCallbackInfo -> IO Dom
layout model _ = do
  label <- Dom.createPWithText (show (counter model)) 
    >>= Dom.withCss "font-size: 32px; margin: 0;"

  button <- Button.create "Increase counter"
    >>= Button.withButtonType ButtonType_Primary
    >>= Button.onClick onClick
    >>= Button.dom

  Dom.createBody 
    >>= Dom.withChild label 
    >>= Dom.withChild button

main :: IO ()
main = do
  window <- WindowCreateOptions.create layout
  AppConfig.create 
    >>= App.create (DataModel 5) 
    >>= App.run window
```

## Notes

Here, `import Azul` brings the types into scope. Each class's constructors and methods 
live in its own module, imported qualified: `Button.create`, `Dom.withChild`, like 
`Map.insert` from `Data.Map`.

`onClick` is a pure function: it gets the current model and returns the new model together
with an `Update`. The package stores the new model; `Update_RefreshDom` runs `layout` again.
A handler that needs `IO` has the type `DataModel -> CallbackInfo -> IO (DataModel, Update)`.

`Button.onClick` attaches the handler to the model of the running callback, here the model
`layout` was called with. `App.create` takes the initial model, any Haskell value.

If a handler expects another model type, the package logs
`azul: ButtonOnClickCallback expected a model of type OtherModel, got DataModel` and doesn't
call it. An exception inside a callback is caught and logged the same way and the app keeps
running.

Every function takes its receiver last, so builder calls chain with `>>=`. Arguments passed
by value are moved into the library: after `Dom.withChild label`, `label` cannot be used again.
Using it anyway raises an `AzulError` instead of crashing.

## Build and run

From the directory containing `cabal.project` and the native library:

```sh
export LD_LIBRARY_PATH=. # Linux only

cabal run --extra-lib-dirs=$PWD hello-world
```

You should see the window pictured on the [hello-world landing page](../hello-world.md).
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `App.run` opens a native window and runs `layout` once with your model.
2. The returned DOM is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set up
   in the DOM. On click, the framework runs `onClick` with the current model, stores the model
   it returns, observes the refresh return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one,
   and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 80%
of the framework. As you might have guessed, more complex UI and styling are only composing
more Dom objects together and working with the various event filters.

You can now start reading about the [architecture patterns](../architecture.md) or
explore what [methods the `Dom` has to offer](../dom.md).

See you in the next tutorial!

---

### Doc Guide En Hello World Java (`doc/guide/en/hello-world/java.md`)

---
slug: hello-world/java
title: Hello World [Java]
language: en
canonical_slug: hello-world/java
audience: external
maturity: mature
guide_order: 16
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/java/HelloWorld.java
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - WindowCreateOptions
  - Update
---

# Hello World [Java]

## Introduction

In order to use Azul from Java, you need both the prebuilt `libazul` library and the bindings, 
which talk to the library via JNA. Azul is tested against JDK 17+ and JNA 5.14+. Azul is not (yet) 
on Maven Central, but azul.rs hosts a maven2 repository, serving the `rs.azul:azul` artifact. 

```sh
curl -O https://azul.rs/ui/release/$VERSION/pom.xml
curl -O https://azul.rs/ui/release/$VERSION/HelloWorld.java
mvn -q package
 # macOS: java -XstartOnFirstThread -jar ...
java -jar target/hello-world-1.0.0.jar
```

The jar bundles the compiled `com.azul` binding and `libazul` for Linux x86-64, macOS arm64 and Windows 
x64 as JNA resources:

```xml
<repositories>
  <repository>
    <id>azul-rs</id>
    <url>https://azul.rs/ui/maven</url>
  </repository>
</repositories>
<dependencies>
  <dependency>
    <groupId>rs.azul</groupId>
    <artifactId>azul</artifactId>
    <version>$VERSION</version>
  </dependency>
</dependencies>
```

Alternatively, compile the generated sources yourself:

1. Download the native library from the
   [release page](https://azul.rs/ui/release/$VERSION) (`libazul.dylib`
   / `libazul.so` / `azul.dll`) and keep it in your working directory or pass
   `-Djna.library.path=.`.
2. Download the Java bundle and build with the
   sources as a second source root (the pom's `from-codegen` profile):

   ```sh
   curl -LO https://azul.rs/ui/release/$VERSION/azul-java-$VERSION.tar.gz
   tar xzf azul-java-$VERSION.tar.gz
   mvn -q package -Dazul.codegen.dir=azul-java
   java -Djna.library.path=. -jar target/hello-world-1.0.0.jar
   ```

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL lands in `target/release/libazul.{so,dylib}` 
(or `azul.dll`). The header/bindings are previously generated by `azul-doc codegen all` and end 
up in `target/codegen/`. Copy both somewhere `java` can find them.

## Simple "Counter" Example

```java
package com.azul;

class Counter {
    public int count = 5;
}

public class HelloWorld {
    public static void main(String[] args) {
        try (App app = App.create(new Counter(), HelloWorld::layout)) {
            WindowCreateOptions options = WindowCreateOptions.create();
            app.run(options);
        }
    }

    public static Dom layout(Counter data, LayoutCallbackInfo info) {
        String countStr = String.format("%d", data.count);
        Dom label = Dom.createPWithText(countStr);
                
        Button btn = Button.create("Increase counter")
            .withOnClick(data, HelloWorld::onClick);

        return Dom.createBody()
            .withChild(label)
            .withChild(btn.dom());
    }

    public static Update onClick(Counter data, CallbackInfo info) {
        data.count++;
        return Update.RefreshDom; 
    }
}
```

## Build and run

Each release ships with an example `pom.xml`, so you can use it directly as your 
project pom.xml. It bundles JNA into the output jar via
`maven-shade-plugin` and sets `Main-Class: com.azul.HelloWorld` in the
manifest, so a plain `java -jar` works with no manual classpath:

```sh
mvn package
# macOS — -XstartOnFirstThread is REQUIRED so libazul's NSApplication loop
# pumps on the JVM main thread.
java -XstartOnFirstThread -Djna.library.path=. -jar target/hello-world-1.0.0.jar
```

On Linux/Windows you can drop `-XstartOnFirstThread`. `-Djna.library.path=.` points
JNA at the directory holding `libazul.dylib` / `libazul.so` / `azul.dll`.

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `app.run(...)` opens a native window and runs the layout callback once with your data model.
2. The returned DOM is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter 
   set up in the DOM. On receiving a click event, the framework borrows your data model 
   mutably, runs the click callback, observes the `Update.RefreshDom` return, and re-invokes 
   the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one, 
   and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 80% 
of the framework. As you might have guessed, more complex UI and styling are only composing more 
Dom objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or explore 
what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Kotlin (`doc/guide/en/hello-world/kotlin.md`)

---
slug: hello-world/kotlin
title: Hello World [Kotlin]
language: en
canonical_slug: hello-world/kotlin
audience: external
maturity: mature
guide_order: 17
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/kotlin/HelloWorld.kt
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - WindowCreateOptions
  - Update
---

# Hello World [Kotlin]

## Introduction

To use Azul from Kotlin, you need to install both the native library and the Kotlin bindings. 
Internally, the bindings rely on the same JNA calls as the [Java](./java.md) and provide *idiomatic*
bindings to Kotlin, so you don't have to manually worry about GC lifetimes, upcasting and downcasting
or handling `Pointer` arithmetic.

## Installation

For a quick demo, you can use the pre-made `HelloWorld.kt` and `build.gradle.kts` from
the release page and build with Gradle:

```sh
curl -O https://azul.rs/ui/release/$VERSION/build.gradle.kts
curl -O https://azul.rs/ui/release/$VERSION/settings.gradle.kts
curl -O https://azul.rs/ui/release/$VERSION/HelloWorld.kt
gradle build
# macOS: java -XstartOnFirstThread -jar ...
java -jar build/libs/hello-world-1.0.0.jar
```

Kotlin has its own artifact (`rs.azul:azul-kotlin`, the compiled `Azul.kt`) in the same
self-hosted maven repository as the Java jar; add it in the `build.gradle.kts`:

```kotlin
repositories {
    mavenCentral()
    maven {
        url = uri("https://azul.rs/ui/maven")
    }
}

dependencies {
    implementation("rs.azul:azul-kotlin:$VERSION")
    // JNA is a transitive dependency, but you can pin it
    implementation("net.java.dev.jna:jna:5.14.0")
}
```

Alternatively, you can compile from the generated `Azul.kt`:

1. Download the native library from the
   [release page](https://azul.rs/ui/release/$VERSION) (`libazul.dylib`
   / `libazul.so` / `azul.dll`) and keep it in your working directory.
2. Download the `azul-kotlin-$VERSION.tar.gz` bundle - and add
   the generated Kotlin files to your source set.
3. Build and run.

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL gets built in 
`target/release/libazul.{so,dylib}` (or `azul.dll`). The Kotlin bindings 
are generated by the previous `codegen all` step in `target/codegen/kotlin/`.
Copy both somewhere `java` can find them.

## Simple "Counter" Example

```kotlin
package com.azul

class Counter {
    var count: Int = 5
}

fun layout(data: Counter, info: LayoutCallbackInfo): Dom {
    val countStr = "${data.count}"
    val label = Dom.createPWithText(countStr)
    val btn = Button.create("Increase counter")
        .withOnClick(data, ::onClick)
    
    return Dom.createBody()
        .withChild(label)
        .withChild(btn.dom())
}

fun onClick(data: Counter, info: CallbackInfo): Update {
    data.count++
    return Update.RefreshDom
}

fun main(args: Array<String>) {
    App.create(Counter(), ::layout).use { app ->
        val options = WindowCreateOptions.create()
        app.run(options)
    }
}
```

The example works identically to Java: the `App.create` generic parameters 
properly set the type information for the layout callback, and `withOnClick` 
expects a callback receiving that specific typed `data` model. Internally, 
a `AzulHostInvoker` abstraction handles registering the class and 
hides the JNA `Pointer` conversion logic behind the scenes.

Using `app.use { ... }` ensures that `.close()` is called 
when the app exits, properly cleaning up the native C memory.

## Build and run

```sh
gradle build
# macOS — -XstartOnFirstThread is required so libazul's
# NSApplication loop starts on the JVM main thread
java -XstartOnFirstThread -Djna.library.path=. -jar build/libs/hello-world-1.0.0.jar
```

On Linux/Windows drop `-XstartOnFirstThread`. 

`-Djna.library.path=.` points JNA at the directory 
holding `libazul.dylib` / `libazul.so` / `azul.dll`.

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `app.run(...)` opens a native window and runs the layout callback once with your data model.
2. The returned DOM is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set up in the DOM. 
   On receiving a click event, the framework borrows your data model mutably, runs the click callback, 
   observes the `Update.RefreshDom` return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one, 
   and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 80% of 
the framework. As you might have guessed, more complex UI and styling are only composing more Dom 
objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or explore 
what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Lua (`doc/guide/en/hello-world/lua.md`)

---
slug: hello-world/lua
title: Hello World [Lua]
language: en
canonical_slug: hello-world/lua
audience: external
maturity: mature
guide_order: 18
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/lua/hello-world.lua
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - WindowCreateOptions
  - Update
---

# Hello World [Lua]

## Introduction

In order to use Azul from Lua, you need the native prebuilt `libazul` library and the 
pre-rendered Lua bindings, which handle the `ffi` abstraction for you. Azul's Lua bindings 
rely on the `ffi` module. 

## Installation

The easiest way to get this is by using LuaJIT 2.1+, which includes the `ffi` module by default,
so you can skip to the next section.

If you are instead using PUC Lua (5.1+), install the `cffi-lua` module first with 
`luarocks install cffi-lua`. It builds from source, so it needs Meson, Ninja and a C++ compiler 
(via `xcode-select --install` or similar). The same `azul.lua` then picks up `cffi` automatically 
when LuaJIT's `ffi` is not there.

```sh
# LuaJIT
brew install luajit

# or vanilla Lua + cffi-lua
brew install lua luarocks meson
luarocks install cffi-lua
```

### Bindings

The easiest way is to just put the `azul.lua` next to your script or on `LUA_PATH`:

```sh
wget https://azul.rs/ui/release/$VERSION/azul.lua
```

Alternatively, you can install the Lua bindings via LuaRocks using the provided `.rockspec`:

```sh
# Via LuaRocks (downloads azul.lua automatically)
luarocks install https://azul.rs/ui/release/$VERSION/azul-$VERSION-1.rockspec
```

Be aware that LuaRocks installs for the Lua it was set up for (usually PUCL).

In either case, you still need to download the native `libazul` library from 
the [release page](https://azul.rs/ui/release/$VERSION) (`libazul.dylib` / `libazul.so` / `azul.dll`) 
and place it where `ffi.load` can find it: the working directory, or in `DYLD_LIBRARY_PATH` (macOS) / `LD_LIBRARY_PATH` (Linux). On macOS, a Homebrew-installed 
`libazul` in `$(brew --prefix)/lib` is not searched by default, so point `DYLD_LIBRARY_PATH` there:

```sh
export DYLD_LIBRARY_PATH="$(brew --prefix)/lib"
```

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL gets built in 
`target/release/libazul.{so,dylib}` (or `azul.dll`). The header/bindings are 
previously generated by `azul-doc codegen all` and end up in the `target/codegen/` folder. 
Copy both somewhere Lua can find them.

## Simple "Counter" Example

```lua
local azul = require('azul')

local model = { counter = 5 }

local function on_click(data, _info)
    data.counter = data.counter + 1
    return azul.Update.RefreshDom
end

local function layout(data, _info)
    local label = azul.Dom.create_p_with_text(tostring(data.counter))
        :with_css('font-size: 32px; margin: 0;')

    local button_dom = azul.Button.create('Increase counter')
        :set_button_type(azul.ButtonType.Primary)
        :with_on_click(data, on_click)
        :dom()

    return azul.Dom.create_body()
        :add_child(label)
        :add_child(button_dom)
end

local window = azul.WindowCreateOptions.create(layout):with({
    window_state = {
        title = 'Hello World',
        size = { dimensions = { width = 400.0, height = 300.0 } },
    },
})

local app = azul.App.create(model, azul.AppConfig.create())
app:run(window)
```

The Lua `App.create` and `with_on_click` wrappers automatically inject a 
trampoline that securely passes your plain Lua table (`model`) back into your callbacks.

There are two builder flavours, which compose freely: `add_*` / `set_*` mutate in place 
and return `self`, whereas `with_*` consume `self` and return the new value. Plain Lua 
strings flow through the auto-string conversion, so `'Increase counter'` and `tostring(data.counter)` 
can be passed directly.

## Build and run
```sh
# macOS LuaJIT, with libazul.dylib next to the script
luajit hello-world.lua
# macOS LuaJIT, with libazul installed via Homebrew
DYLD_LIBRARY_PATH="$(brew --prefix)/lib" luajit hello-world.lua
# Linux
LD_LIBRARY_PATH=. luajit hello-world.lua

# PUCL, with vanilla Lua + cffi-lua
lua hello-world.lua
```

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `app:run(window)` opens a native window and runs the layout callback once with your data model.
2. The returned DOM is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set up in 
   the DOM. On receiving a click event, the framework borrows your data model mutably, runs the 
   click callback, observes the refresh return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one, 
   and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 80% of 
the framework. As you might have guessed, more complex UI and styling are only composing more Dom 
objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or explore 
what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Node (`doc/guide/en/hello-world/node.md`)

---
slug: hello-world/node
title: Hello World [Node.js]
language: en
canonical_slug: hello-world/node
audience: external
maturity: mature
guide_order: 20
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/node/hello-world.js
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - WindowCreateOptions
  - Update
---

# Hello World [Node.js]

## Introduction

In order to talk with the `libazul` library from a JS runtime, you need the 
pre-rendered JS bindings, which use [koffi](https://koffi.dev/) when run with Node.js (16+), 
`bun:ffi` on Bun and `Deno.dlopen` on Deno. All three run the same `azul.js`, so 
that you can write ordinary JS without worrying about C callbacks or garbage 
collection lifetimes.

## Installation

The `azul` package is shipped from its own index at azul.rs. The quickest install 
is the hosted tarball - it bundles `libazul` for Linux/macOS/Windows and pulls 
in `koffi` automatically:

```sh
npm install https://azul.rs/ui/npm/azul-$VERSION.tgz
```

With that installed you only need to `require('azul')` in your code. 

If you'd rather like to do it manually:

```sh
npm install koffi
# macOS
wget -O libazul.dylib https://azul.rs/ui/release/$VERSION/libazul.dylib
# Linux
wget -O libazul.so    https://azul.rs/ui/release/$VERSION/libazul.so
# Windows (PowerShell)
Invoke-WebRequest -Uri https://azul.rs/ui/release/$VERSION/azul.dll -OutFile azul.dll
```

Then drop the generated `azul.js` binding next to your script:

```sh
wget https://azul.rs/ui/release/$VERSION/azul.js
```

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL gets built in `target/release/libazul.{so,dylib}` 
(or `azul.dll`). The header/bindings are previously generated by `azul-doc codegen all` step and end up in
`target/codegen/`. Copy both somewhere Node / Deno / Bun can find them.

## Simple "Counter" Example

```javascript
'use strict';

// Use require('./azul.js') if downloaded manually
const azul = require('azul');
const model = { counter: 5 };

function onClick(data, _info) {
    data.counter += 1;
    return azul.Update.RefreshDom;
}

function layout(data, _info) {
    const label = azul.Dom.createPWithText(String(data.counter))
        .withCss('font-size: 32px; margin: 0;');

    const button = azul.Button.create('Increase counter')
        .setButtonType(azul.ButtonType.Primary)
        .withOnClick(data, onClick)
        .dom();

    return azul.Dom.createBody()
        .addChild(label)
        .addChild(button);
}

const window = azul.WindowCreateOptions.create(layout).with({
    windowState: {
        title: 'Hello World',
        size: { dimensions: { width: 400.0, height: 300.0 } }
    },
});

azul.App.create(model, azul.AppConfig.create()).run(window);
```

Notice that you can pass plain JavaScript objects directly to `azul.App.create` 
and `.withOnClick`; the generated bindings automatically handle tracking and 
unpacking the C handles for you. 

Callbacks are plain functions `(data, info) => ...` returning `azul.Update.*` 
(or an `azul.Dom` for layout). Enums and helpers sit at the top level of the
module (`azul.Update.RefreshDom`, `azul.ButtonType.Primary`).

## Build and run

```sh
node hello-world.js
bun hello-world.js
# Deno needs the permissions, and a package.json marking the CommonJS module:
printf '{"type":"commonjs"}\n' > package.json
deno run --allow-ffi --allow-read --allow-env hello-world.js
```

On startup, `azul.js` looks for the native library 
(`libazul.dylib` / `libazul.so` / `azul.dll`) in this order:

1. `$AZ_LIB` — explicit path to the library file (overrides everything),
2. the directory containing `azul.js` itself,
3. `$AZ_LIB_DIR` — directory containing the library,
4. the current working directory,
5. the system loader search path (`DYLD_LIBRARY_PATH` / `LD_LIBRARY_PATH` / `PATH`).

With the download steps above (library and `azul.js` in the same directory) 
it is found automatically - no environment variables needed.

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `app.run(window)` opens a native window and runs the layout callback once with your data model.
2. The returned DOM is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter 
   set up in the DOM. On receiving a click event, the framework borrows your data model 
   mutably, runs the click callback, observes the refresh return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one, 
   and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 80% of 
the framework. As you might have guessed, more complex UI and styling are only composing more Dom 
objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or explore 
what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Ocaml (`doc/guide/en/hello-world/ocaml.md`)

---
slug: hello-world/ocaml
title: Hello World [OCaml]
language: en
canonical_slug: hello-world/ocaml
audience: external
maturity: mature
guide_order: 21
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/ocaml/hello_world.ml
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - WindowCreateOptions
  - Update
---

# Hello World [OCaml]

## Introduction

To use `libazul` from OCaml (4.14+), you need both the native prebuilt library as well as the
pre-rendered OCaml bindings, which use [`ctypes-foreign`](https://github.com/yallop/ocaml-ctypes) 
under the hood. The bindings abstract away the need to interface with the C types directly.

## Installation

The Azul OCaml bindings are distributed directly via `azul.rs`. The release bundle 
is a complete, ready-to-build Dune project containing the generated `azul` library, 
the `azul.opam` package definition, and the `hello_world.ml` counter example.

To get started, download the bundle, use `opam` to install its dependencies natively, and fetch the `libazul` binary for your platform:

```sh
mkdir -p my_app && cd my_app
curl -LO https://azul.rs/ui/release/$VERSION/azul-ocaml-$VERSION.tar.gz
tar xzf azul-ocaml-$VERSION.tar.gz

# Automatically installs ctypes, ctypes-foreign, and dune
opam install . --deps-only -y

# macOS
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
# Linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so
# Windows
curl -O https://azul.rs/ui/release/$VERSION/azul.dll

# macOS: DYLD_LIBRARY_PATH=.
LD_LIBRARY_PATH=. dune exec ./hello_world.exe 
```

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL lands in 
`target/release/libazul.{so,dylib}` (or `azul.dll`). 
The header/bindings are previously generated by `azul-doc codegen all` and 
end up in `target/codegen/`. Copy both somewhere `dune` can find them.

## Simple "Counter" Example

Now you are ready to run the first "Hello World" example:

```ocaml
type my_data_model = { mutable counter : int }

(* Converts between my_data_model and Azul.RefAny.t in both directions *)
let model : my_data_model Azul.RefAny.key = Azul.RefAny.key "my_data_model"

let on_click (m : my_data_model) (_info : Azul.CallbackInfo.t) : Azul.Update.t =
  m.counter <- m.counter + 1;
  Azul.Update.RefreshDom

let layout (m : my_data_model) (_info : Azul.LayoutCallbackInfo.t) : Azul.Dom.t =
  let label =
    Azul.Dom.p ~css:"font-size: 32px; margin: 0;" (Int.to_string m.counter)
  in
  let button =
    Azul.Button.create "Increase counter"
      ~button_type:Azul.ButtonType.Primary
      ~on_click:(Azul.RefAny.bind model m on_click)
  in
  Azul.Dom.body ~children:[ label; button ]

let () =
  let data = Azul.RefAny.upcast model { counter = 5 } in
  let window = Azul.WindowCreateOptions.create ~layout:(Azul.RefAny.lift model layout) () in
  let app_config = Azul.AppConfig.create () in
  let app = Azul.App.create ~data ~app_config () in

  Azul.App.run app window
```

## Build and run

```sh
dune exec ./hello_world.exe
# or, after `dune build`:
#   macOS:  DYLD_LIBRARY_PATH=. ./_build/default/hello_world.exe
#   linux:  LD_LIBRARY_PATH=. ./_build/default/hello_world.exe
```

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `Azul.App.run` opens a native window and runs the layout callback once with your model.
2. The returned DOM is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set 
   up in the DOM. On click, the framework borrows your data model mutably, runs the click 
   callback, observes the refresh return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one, 
   and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 80% 
of the framework. As you might have guessed, more complex UI and styling are only composing 
more Dom objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or 
explore what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Pascal (`doc/guide/en/hello-world/pascal.md`)

---
slug: hello-world/pascal
title: Hello World [Pascal]
language: en
canonical_slug: hello-world/pascal
audience: external
maturity: mature
guide_order: 24
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/pascal/hello-world.pas
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - RefAny
  - WindowCreateOptions
  - Update
---

# Hello World [Pascal]

## Introduction

To use `libazul` from Free Pascal (3.2.0+), you need the native prebuilt library and the generated
`Azul` bindings (`azul.pas`). The bindings wrap the C API in classes: you write a model class,
plain functions as callbacks, and pass ordinary Pascal strings. For older compilers, see the 
[Supported Compilers](#supported-compilers) section.

## Installation

You need the Free Pascal Compiler (`brew install fpc` / `apt install fp-compiler`), 
the `azul.pas` unit and the native library from the release page.

macOS:

```sh
# macOS
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
# Linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so
# Windows
curl -O https://azul.rs/ui/release/$VERSION/azul.dll

curl -O https://azul.rs/ui/release/$VERSION/azul.pas
curl -O https://azul.rs/ui/release/$VERSION/hello-world.pas
fpc -Fl. hello-world.pas
DYLD_LIBRARY_PATH=. ./hello-world # hello-world.exe on Windows
```


The bindings link the library by name, so `-Fl.` (the library search path) 
is the only flag you need. You will find the precompiled `.so` and `.dylib` 
files for various different architectures on the release page, download and 
rename it to link properly.

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL lands in
`target/release/libazul.{so,dylib}` (or `azul.dll`), the unit in 
`target/codegen/azul.pas`. Copy both next to your program.

## Simple "Counter" Example

```pascal
program HelloWorld;

{$mode delphi}

uses
  SysUtils, Azul;

type
  TMyModel = class
    Counter: Integer;
  end;

function OnIncrease(Model: TMyModel; Info: TAzCallbackInfo): TAzUpdate;
begin
  Model.Counter := Model.Counter + 1;
  Result := azRefreshDom;
end;

function Layout(Model: TMyModel; Info: TAzLayoutCallbackInfo): TDom;
var
  LabelDom: TDom;
  Btn: TButton;
begin
  LabelDom := TDom.P(IntToStr(Model.Counter))
    .WithCss('font-size: 32px; margin: 0;');

  Btn := TButton.Create('Increase counter')
    .SetButtonType(azPrimary)
    .OnClick<TMyModel>(OnIncrease);

  Result := TDom.Body.AddChild(LabelDom).AddChild(Btn.Dom);
end;

var
  Model: TMyModel;
  App: TAzApp<TMyModel>;
begin
  Model := TMyModel.Create;
  Model.Counter := 5;

  App := TAzApp<TMyModel>.Create(Model, Layout);
  App.Window.Title := 'Hello World';
  App.Window.Width := 400;
  App.Window.Height := 300;
  App.Run;

  App.Free;
end.
```

`TAzApp<TMyModel>` takes ownership of your model: `App.Free` frees the app and then the
model, so do not free the model yourself. Callbacks are plain functions that receive your 
model and the callback info. 

The bindings automatically downcast the `RefAny` model to the type expected by the callbacks 
first argument and logs an error (returning `DoNothing`) if the downcast fails. An exception 
raised inside a callback is caught and logged the same way, so that the app keeps running, 
even if a callback fails.

## Supported Compilers

The example uses `{$mode delphi}`. In `{$mode objfpc}{$H+}`, generics
need `specialize` and function arguments need `@`:

```pascal
type
  TMyApp = specialize TAzApp<TMyModel>;

  Btn := TButton.Create('Increase counter')
    .SetButtonType(azPrimary)
    .specialize OnClick<TMyModel>(@OnIncrease);

  App := TMyApp.Create(Model, @Layout);
```

The `azul.pas` bindings are written for Free Pascal and does not compile with Embarcadero Delphi. 
It masks floating-point exceptions when it loads, because the library relies on IEEE-754 NaN and
infinity values. If you enable FPU exceptions in your own code, mask them again before
calling into Azul.

FPC 3.0.x has no generic methods. Pass a typed callback object instead of `OnClick<TMyModel>`:

```pascal
    .OnClick(TAzButtonOnClickCallbackTypedWrapper<TMyModel>.Create(OnIncrease));
```

FPC 3.3.1 adds anonymous functions, so you *can* use inline callbacks, 
however this is not recommended, because the general goal in Azul 
is to separate the callback code from the UI code as much as possible
without any implicit captured variables.

```pascal
    .OnClick<TMyModel>(
      function(Model: TMyModel; Info: TAzCallbackInfo): TAzUpdate
      begin
        Model.Counter := Model.Counter + 1;
        Result := azRefreshDom;
      end);
```

## Build and run

From the directory containing `azul.pas`, `hello-world.pas` and the native library:

```sh
fpc -Fl. hello-world.pas

# macOS
DYLD_LIBRARY_PATH=. ./hello-world
# Linux
LD_LIBRARY_PATH=. ./hello-world
# Windows
hello-world.exe
```

You should see the window pictured on the [hello-world landing page](../hello-world.md).
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `App.Run` opens a native window and runs `Layout` once with your model.
2. The returned DOM is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set up
   in the DOM. On receiving a click event, the framework borrows your data model mutably, runs the
   click callback, observes the `RefreshDom` return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one,
   and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 80%
of the framework. As you might have guessed, more complex UI and styling are only composing
more Dom objects together and working with the various event filters.

You can now start reading about the [architecture patterns](../architecture.md) or
explore what [methods the `Dom` has to offer](../dom.md).

See you in the next tutorial!

---

### Doc Guide En Hello World Python (`doc/guide/en/hello-world/python.md`)

---
slug: hello-world/python
title: Hello World [Python]
language: en
canonical_slug: hello-world/python
audience: external
maturity: mature
guide_order: 14
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - core/src/callbacks.rs
  - core/src/lib.rs
  - dll/src/lib.rs
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Css
  - RefAny
  - WindowCreateOptions
  - LayoutCallbackInfo
  - CallbackInfo
  - Update
---

# Hello World [Python]

## Introduction

For Python, Azul offers a custom python extension (using the `pyo3` binding library), 
so you can write idiomatic Python and the binding takes care of the rest. The extension 
is packaged as a `.whl`, so you can easily install it with PyPI or `uv`.

Azul is not (yet) on the public pypi.org index, so you can either use the self-hosted 
pip index on azul.rs, or a manual download.

### Self-hosted pip index

Pointing `--index-url` at the self-hosted PEP 503 index on azul.rs makes pip 
(or uv, poetry, pdm) resolve `azul` from azul.rs instead of pypi.org:

```sh
pip install azul --index-url https://azul.rs/ui
```

### Manual download

If you want to install the Python extension manually, download it for your platform from the
[GitHub release](https://github.com/fschutt/azul/releases/tag/$VERSION) and put it
next to your script, so Python can find it:

```sh
# macOS
curl -L -o azul.so https://github.com/fschutt/azul/releases/download/$VERSION/azul.so
# linux
curl -L -o azul.so https://github.com/fschutt/azul/releases/download/$VERSION/azul.cpython.so
# windows
curl.exe -L -O https://github.com/fschutt/azul/releases/download/$VERSION/azul.pyd
```

The abi3 wheel targets Python 3.10+ - make sure you have a recent version. If there is 
no prebuilt module for your platform or architecture, see "[Building the extension](#building-the-extension)" 
below for the manual route.

## Building the extension

You can build the Azul Python extension manually with:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
cargo build -p azul-dll --release \
    --no-default-features --features python-extension
```

The resulting library is `target/release/libazul.{so,dylib}` (`azul.dll` on Windows). 
Python imports it as `azul`, so you need to rename or symlink it:

```sh
# macOS
cp target/release/libazul.dylib target/release/azul.so
# Linux
cp target/release/libazul.so target/release/azul.so
# Windows
copy target\release\azul.dll target\release\azul.pyd
```

Then either run Python from the directory containing the file, or prepend 
that path to `sys.path`:

```python
import sys, os
sys.path.insert(0,
    os.path.join(os.path.dirname(__file__), 'target', 'release'))
import azul
```

## Simple "Counter" Example

```python
from azul import *

class DataModel:
    def __init__(self, counter):
        self.counter = counter

def layout(data, info):
    label = (Dom.create_p_with_text(str(data.counter))
             .with_css("font-size: 32px; margin: 0;"))

    button = (Button.create("Increase counter")
              .with_on_click(data, on_click)
              .dom()
              .with_css("flex-grow: 1;"))

    return (Dom.create_body()
            .with_child(label)
            .with_child(button))

def on_click(data, info):
    data.counter += 1
    return Update.RefreshDom

if __name__ == "__main__":
    model = DataModel(5)
    window = WindowCreateOptions.create(layout)
    app = App.create(model, AppConfig.create())
    app.run(window)
```

The Python extension allows you to natively work with Python functions without 
worrying about lifetime or reference count handling. Function callbacks and 
conversions between `PyObject` and Azul's `RefAny` are transparently handled 
for you. The binding wraps your `DataModel` instance and hands the same instance 
back to every callback, which you mutate in place.

In the Python extension, there are no submodules, so you can import `azul.App` 
instead of `azul.app.App`. Enum variants are plain class attributes, so returning 
`Update.RefreshDom` takes no parentheses. 

Styles are CSS strings, where `with_css("...")` also accepts `:hover { }`, 
`@media ... { }` and `@os(...)` queries inline. Builder methods such as 
`.with_css(...)` and `.with_child(...)` consume `self` and return a new 
`Dom`, so you can keep chaining the result.

The `info` argument, which this example ignores, carries read-only access to the 
system font cache, image cache, GL context, window size, routing and localization 
dictionaries into the `layout` function, plus the mutation helpers (DOM navigation, 
CSS overrides without rebuilding, computed-layout queries) into `on_click`. 

You can customize the window by changing the fields of the `WindowCreateOptions` 
(also see the API search box on the API documentation page).

## Run it

```sh
python3 hello-world.py
```

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `app.run(window)` opens a native window and runs the layout callback once with your `DataModel` instance.
2. The returned DOM is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event 
   filter set up in the DOM. On a click event, the framework takes your data model 
   instance, runs the click callback on it, observes the `Update.RefreshDom` return 
   and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one 
   and only re-updates and re-paints the counter, not the entire window.

Congratulations - once you've got the hello-world example running, you've already mastered 80% 
of the framework. As you might have guessed, more complex UI and styling are only composing more 
Dom objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or explore 
what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Ruby (`doc/guide/en/hello-world/ruby.md`)

---
slug: hello-world/ruby
title: Hello World [Ruby]
language: en
canonical_slug: hello-world/ruby
audience: external
maturity: mature
guide_order: 19
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/ruby/hello-world.rb
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - WindowCreateOptions
  - Update
---

# Hello World [Ruby]

## Introduction

In order to use `libazul` from Ruby (2.6+), you need the native library, the 
[`ffi`](https://github.com/ffi/ffi) gem and the rendered Ruby bindings, which
allow you to write idiomatic Ruby code without worrying about C function calls
and GC lifetimes.

The name `azul` on rubygems.org belongs to an unrelated project, so azul ships
from its own source at azul.rs. The gem bundles `libazul` for Linux/macOS/Windows 
and pulls in `ffi` automatically:

```sh
## required because of --clear-sources in the next cmd
gem install ffi
## --clear-sources required, otherwise gem pulls from rubygems.org
gem install azul --clear-sources --source https://azul.rs/ui/gems
```

With the gem installed you just `require 'azul'` (no `-I.`). 
To wire it up by hand instead:

```sh
gem install --user-install ffi -v 1.15.5
# download the native library - macOS
wget -O libazul.dylib https://azul.rs/ui/release/$VERSION/libazul.dylib
# Linux
wget -O libazul.so    https://azul.rs/ui/release/$VERSION/libazul.so
# Windows (PowerShell)
Invoke-WebRequest -Uri https://azul.rs/ui/release/$VERSION/azul.dll -OutFile azul.dll
```

Then drop the generated `azul.rb` binding next to your script and run with `-I.`:

```sh
wget https://azul.rs/ui/release/$VERSION/azul.rb
```

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated .rs C-API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL lands in `target/release/libazul.{so,dylib}` 
(or `azul.dll`). The header/bindings are previously generated by `azul-doc codegen all` and 
end up at `target/codegen/`. Copy both somewhere `ruby` can find them.

## Simple "Counter" Example

```ruby
require 'azul'

class MyDataModel
  attr_accessor :counter
  def initialize(counter); @counter = counter; end
end

model = MyDataModel.new(5)
data  = Azul::RefAny.wrap(model)

on_click = lambda do |m, _info|
  next Azul::Update::DoNothing if m.nil?
  m.counter += 1
  Azul::Update::RefreshDom
end

layout = lambda do |m, _info|
  next Azul::Dom.create_body if m.nil?

  label = Azul::Dom.create_p_with_text(m.counter.to_s)
    .with_css('font-size: 32px; margin: 0;')

  button = Azul::Button.create('Increase counter')
    .with_button_type(Azul::ButtonType::Primary)
    .on_click(m, on_click)

  Azul::Dom.create_body
    .with_child(label)
    .with_child(button.dom)
end

window = Azul::WindowCreateOptions.create_with_layout(layout).with(
  window_state: {
    title: 'Hello World',
    size: { dimensions: { width: 400.0, height: 300.0 } },
  },
)

app = Azul::App.create(data, Azul::AppConfig.create)
app.run(window)
```

Notice that `Azul::RefAny.wrap` wraps any Ruby object into a handle which prevents it from being garbage collected by Ruby while C holds a reference to it. The generated bindings automatically unwrap this handle before calling your lambdas, handing the original Ruby object back to your callbacks (returning `nil` if there is a mismatch, so guard with `next ... if m.nil?`). 

Callbacks are lambdas with the signature `|obj, info|`, and you yield a value out of them 
with `next` rather than `return`. The smart builders hide all the FFI plumbing: `WindowCreateOptions.create_with_layout(layout)` and `Button.create(...).on_click(data, on_click)` automatically handle callback registration for you. Furthermore, `.with(...)` allows you 
to recursively assign nested fields while auto-converting standard Ruby `String`s to `AzString`. 

Styling takes plain CSS strings through `Dom#with_css('font-size: 32px;')`, with no manual property 
construction, and the generated types have Ruby helpers as well (`Azul::String#to_s`, `Option#to_opt`, `Result#unwrap`, `Vec#to_a`). 

If the library cannot be found at run time, put `libazul.dylib` / `libazul.so` next 
to `azul.rb` or point `AZ_LIB_DIR` at its directory - note that `DYLD_LIBRARY_PATH` 
does not work with the macOS system Ruby.

## Build and run

```sh
ruby -I. hello-world.rb
```

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `app.run(window)` opens a native window and ran the layout callback once with your data model.
2. The returned DOM is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set 
   up in the DOM. On click, the framework borrows your data model mutably, runs the click callback, 
   observes the refresh return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one, and 
   only re-updates and re-paints the counter, not the entire window.

Congratulations - once you've got the hello-world example running, you've already mastered 80% of 
the framework. As you might have guessed, more complex UI and styling are only composing more Dom 
objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or explore 
what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Rust (`doc/guide/en/hello-world/rust.md`)

---
slug: hello-world/rust
title: Hello World [Rust]
language: en
canonical_slug: hello-world/rust
audience: external
maturity: mature
guide_order: 11
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - core/src/callbacks.rs
  - core/src/lib.rs
  - dll/src/lib.rs
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - Dom
  - Css
  - WindowCreateOptions
  - LayoutCallbackInfo
  - CallbackInfo
  - Update
  - RefreshDom
---

# Hello World [Rust]

## Introduction

Azul is a GUI library written in Rust itself. Therefore, it might seem strange for newcomers 
to see the first step being "please download a precompiled `.dll` / `.so` file". However, in 
practice there are very significant benefits once you get over this initial hurdle, that massively 
outweigh the small disadvantage of having to download one `.dll` file:

- Fast recompilation times: only one dependency (the API) instead of hundreds from crates.io
- The library can be compiled as optimized layout code (hot path) while your UI binary can be unoptimized callback code (slow path)
- `/target` directory now only uses a couple MiB instead of GiB of space
- DLLs can integrate with the OS-native package managers such as `apt`, `yum` or `brew` for self-updates
- Multiple Azul applications don't duplicate the library code: one update and all applications are patched
- Faster CI builds: no more recompilation of hundreds of crates

### As a native library

Install it with your package manager (every channel is self-hosted on azul.rs):

```sh
# macOS
brew tap fschutt/azul https://azul.rs/ui/brew.git
brew install fschutt/azul/azul

# Debian / Ubuntu
# (This adds the azul.rs apt repository to your sources.list, so you can receive updates via apt update)
echo "deb [trusted=yes] https://azul.rs/ui/apt stable main" | sudo tee /etc/apt/sources.list.d/azul.list
sudo apt update && sudo apt install azul

# Fedora / RHEL / openSUSE
sudo dnf config-manager --add-repo https://azul.rs/ui/rpm/azul.repo && sudo dnf install azul

# Arch Linux
echo '
[azul]
SigLevel = Optional TrustAll
Server = https://azul.rs/ui/arch/$arch' | sudo tee -a /etc/pacman.conf
sudo pacman -Sy azul

# Alpine Linux
echo "https://azul.rs/ui/alpine" | sudo tee -a /etc/apk/repositories
sudo apk add --allow-untrusted azul

# Windows
choco install libazul --source https://azul.rs/ui/nuget/index.json
# or
scoop bucket add azul https://azul.rs/ui/scoop.git && scoop install azul
```

…or download it next to your project from the
[release page](https://azul.rs/ui/release/$VERSION):

```sh
# Linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so

# macOS M1+
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
# macOS Intel
curl -O https://azul.rs/ui/release/$VERSION/libazul.x86_64.dylib

# Windows
curl.exe -O https://azul.rs/ui/release/$VERSION/azul.dll
curl.exe -O https://azul.rs/ui/release/$VERSION/azul.dll.lib
```

In order for Rust to know where the downloaded dll lives, you need to set the `AZ_LINK_PATH`
variable to the path of the dll. Azul's `build.rs` system is relatively smart and will figure 
out where the other files are that it needs. 

If you want to end up with a "single-binary" build, download the `.a` file and set that as 
your `AZ_LINK_PATH`. 

```sh
export AZ_LINK_PATH=/my/path/to/libazul.so
```

If `AZ_LINK_PATH` is not set, it will try to find the system-installed 
library (i.e. the one installed by apt or brew above) and link against it. So if you installed 
`libazul` via brew or similar, then you don't need to set the environment variable at all - 
only if you used the curl method.

### Installing the API bindings

Now that you have prepared the precompiled library, you still need the generated API bindings. 
Currently, Azul is not on crates.io, but the bindings are available from azul.rs, so you can 
register azul.rs as a "registry" in cargo in your project like this:

```toml
# .cargo/config.toml
[registries]
azul = { index = "sparse+https://azul.rs/ui/cargo/" }
```

```sh
cargo new hello-azul && cd hello-azul
cargo add azul --registry azul
```

Alternatively, add this to your dependencies manually:

```toml
[dependencies]
azul = { version = "$VERSION", registry = "azul" }
```

### Building from source

If you want to build the Azul DLL from source or link it as a "Rust crate" instead 
of using the precompiled .a library, you need to build the azul-dll project (under /dll), 
with `--features build-dll`. 

In order to do this however, you first need to actually generate the bindings again, 
so that the crate even compiles. The bindings are generated from the `api.json` in 
the root folder using the `azul-doc` binary.

```sh
git clone --depth 1 --branch $VERSION https://github.com/fschutt/azul
cargo run --release --manifest-path azul/Cargo.toml -p azul-doc -- codegen all
cargo build --release -p azul-dll --rename azul --path azul/dll --features build-dll
```

## Simple "Counter" Example

The simplest example to showcase Azul's model is only about ~30 lines long:

```rust
use azul::{prelude::*, widgets::Button};

struct DataModel {
    counter: usize,
}

extern "C" 
fn my_layout_func(mut data: RefAny, _: LayoutCallbackInfo) -> Dom {
    let counter = match data.downcast_ref::<DataModel>() {
        Some(d) => format!("{}", d.counter),
        None => return Dom::create_body(),
    };

    let label = Dom::create_p_with_text(counter.as_str())
        .with_css("font-size: 32px; margin: 0;");

    let mut button = Button::create("Increase counter");
    button.set_on_click(data.clone(), my_on_click);
    let button = button.dom().with_css("flex-grow: 1;");

    Dom::create_body().with_child(label).with_child(button)
}

extern "C" 
fn my_on_click(mut data: RefAny, _: CallbackInfo) -> Update {
    let mut data = match data.downcast_mut::<DataModel>() {
        Some(s) => s,
        None => return Update::DoNothing,
    };

    data.counter += 1;

    Update::RefreshDom
}

fn main() {
    let data = DataModel { counter: 5 };
    let config = AppConfig::create();
    let app = App::create(RefAny::new(data), config);
    let window = WindowCreateOptions::create(my_layout_func);
    app.run(window);
}
```

Notice that `extern "C"` is used because every callback crosses the FFI boundary. 
You use `downcast_ref` (or `downcast_mut`) to recover your concrete struct from 
the type-erased `RefAny`. 

Building the UI is done using primitive node constructors like `Dom::create_p_with_text` 
and `Dom::create_body`, and styled with `with_css("...")`. Finally, `data.clone()` 
just bumps the reference count instead of doing a deep copy.

## Build and run

```sh
cargo run --release
```

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the 
new value renders.

1. `App::run` opens a native window and runs the layout callback once with your `RefAny`.
2. The returned `Dom` is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event 
   filter set up in the `Dom`. On click, the framework borrows your `RefAny` mutably, 
   runs `my_on_click`, observes the `Update::RefreshDom` return, and re-invokes the 
   layout callback.
4. The framework determines the diff between the previous frame's `Dom` and the 
   current one, and only re-updates and re-paints the counter, not the entire window.

Congratulations - once you've got the hello-world example running, you've already 
mastered 80% of the framework. As you might have guessed, more complex UI and styling 
are only composing more Dom objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or 
explore what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Scala (`doc/guide/en/hello-world/scala.md`)

---
slug: hello-world/scala
title: Hello World [Scala]
language: en
canonical_slug: hello-world/scala
audience: external
maturity: mature
guide_order: 25
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/scala/HelloWorld.scala
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - WindowCreateOptions
  - Update
---

# Hello World [Scala]

## Introduction

To use Azul from Scala, you need the native library and the [Java](./java.md) bindings: Scala calls
the generated `com.azul` classes directly, there is no separate Scala binding. Scala functions
convert to the typed Java callback interfaces, so the program looks like the Java and Kotlin
examples. The example compiles with Scala 3 and Scala 2.13 and runs on JDK 17+.

## Installation

Azul is tested against JDK 17+ and JNA 5.14+. Azul is not (yet) on Maven Central, 
but azul.rs hosts a maven2 repository, serving the `rs.azul:azul` artifact. 

If you haven't already, install the Scala Build Tool. You can use 
[SDKMAN!](https://sdkman.io/) (`sdk install sbt`), Homebrew (`brew install sbt`), 
or follow the [official instructions](https://www.scala-sbt.org/download.html).

Then, for a quick start, download the pre-configured `build.sbt` and `HelloWorld.scala`
and run `sbt assembly`, where `sbt` handles mixed Java / Scala builds transparently: 

```sh
curl -O https://azul.rs/ui/release/$VERSION/build.sbt
curl -O https://azul.rs/ui/release/$VERSION/HelloWorld.scala
sbt assembly
 # macOS: java -XstartOnFirstThread -jar ...
java -jar target/scala-2.13/hello-world-assembly-1.0.0.jar
```

The preconfigured `build.sbt` contains the following lines that are 
necessary to add the `azul` dependency to your project (shown here for copy-pasting):

```scala
resolvers += "azul-rs" at "https://azul.rs/ui/maven"

libraryDependencies += "rs.azul" % "azul" % "$VERSION"
```

### Manual Installation

For a manual install, you can build the Java bindings from source instead of using the pre-built 
`.jar` - first, download the native engine (`libazul.dylib` / `libazul.so` / `azul.dll`) from the 
[release page](https://azul.rs/ui/release/$VERSION) and place it in your working directory (or 
use `-Djna.library.path=<path>`).

Then, download the `azul-scala-$VERSION.tar.gz` bundle from the same release page. 
This archive contains `HelloWorld.scala`, a pre-configured `build.sbt`, and all the raw `.java` 
binding files.

Finally, extract the bundle and compile. Because `sbt` natively handles mixed Scala/Java projects, 
running `sbt assembly` will automatically compile the Java bindings alongside your Scala code into 
JVM `.class` files (under `target/scala-2.13/classes/`), and package them into an executable jar:

```sh
curl -LO https://azul.rs/ui/release/$VERSION/azul-scala-$VERSION.tar.gz
tar xzf azul-scala-$VERSION.tar.gz
sbt assembly
java -Djna.library.path=. -jar target/scala-2.13/hello-world-assembly-1.0.0.jar
```

### Without sbt

If you don't want to use `sbt`, you can manually compile the Java bindings using `javac` and 
run the project using [Scala CLI](https://scala-cli.virtuslab.org/) (which Scala 3.5+ installs as `scala`):

```sh
curl -LO https://azul.rs/ui/release/$VERSION/azul-scala-$VERSION.tar.gz
tar xzf azul-scala-$VERSION.tar.gz

# Download JNA (required to compile the Java bindings)
curl -L -o jna.jar https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar
# Compile the raw .java bindings into a `classes/` directory
javac -cp jna.jar -d classes azul-java/*.java
# Run the Scala app against the compiled Java classes
scala-cli run HelloWorld.scala --extra-jars classes --extra-jars jna.jar --java-opt -Djna.library.path=.
```

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
# generate the bindings from api.json (required)
cargo run -p azul-doc --release -- codegen all
# build the actual DLL with the now-generated API bindings
cargo build -p azul-dll --release --features build-dll
```

Notice the required `--features build-dll`. The DLL lands in `target/release/libazul.{so,dylib}` 
(or `azul.dll`). The Java bindings that Scala uses are generated by the previous `codegen all` 
step and end up in `target/codegen/java/`. Copy both somewhere your Scala project can find them.

## Simple "Counter" Example

```scala
package com.azul

import scala.util.Using

class Counter {
  var count: Int = 5
}

object HelloWorld {
  def main(args: Array[String]): Unit =
    Using.resource(App.create(new Counter, layout(_, _))) { app =>
      val options = WindowCreateOptions.create()
      app.run(options)
    }

  def layout(data: Counter, info: LayoutCallbackInfo): Dom = {
    val countStr = data.count.toString
    val btn = Button.create("Increase counter")
      .withOnClick(data, onClick(_, _))

    Dom.createBody()
      .withChild(Dom.createPWithText(countStr))
      .withChild(btn.dom())
  }

  def onClick(data: Counter, info: CallbackInfo): Update = {
    data.count += 1
    Update.RefreshDom
  }
}
```

`App.create` takes the model type from `new Counter`, so `layout` receives a
`Counter` directly. `onClick` does the same. 

If a callback throws, the binding logs the error and the app keeps running and
`Using.resource` closes the `App` when `run` returns, which releases the native memory.

### Scala 2 vs 3

In Scala, a method (defined with `def`) is not actually an object, but 
APIs like `App.create` or `withOnClick` expect to receive a "function object"
(specifically, a Java SAM interface). 

```scala
// Scala 2 requires the placeholder annotations to know 
// how many parameters this will take
App.create(new Counter, layout(_, _))

Button.create("Increase").withOnClick(data, onClick(_, _))
```

The Scala 3 compiler is smarter, it automatically handles the conversion behind the scenes:

```scala
// In Scala 3, "automatic eta expansion":
App.create(new Counter, layout)

Button.create("Increase").withOnClick(data, onClick)
```

The code above uses the `(_, _)` syntax so that the example compiles for both Scala 2 
and Scala 3 users.

## Build and run

Each release ships with an example `build.sbt`, so you can use it directly as your 
project configuration. It bundles JNA into the output jar via `sbt-assembly` and 
sets the main class, so a plain `java -jar` works with no manual classpath:

```sh
sbt assembly
# macOS — -XstartOnFirstThread is REQUIRED so libazul's NSApplication loop
# pumps on the JVM main thread.
java -XstartOnFirstThread -Djna.library.path=. -jar target/scala-2.13/hello-world-assembly-1.0.0.jar
```

On Linux/Windows you can drop `-XstartOnFirstThread`. `-Djna.library.path=.` points
JNA at the directory holding `libazul.dylib` / `libazul.so` / `azul.dll`.

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `app.run(...)` opens a native window and runs the layout callback once with your data model.
2. The returned DOM is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set up in the DOM. 
   On receiving a click event, the framework borrows your data model mutably, runs the click callback, 
   observes the `Update.RefreshDom` return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one, 
   and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 80% of 
the framework. As you might have guessed, more complex UI and styling are only composing more Dom 
objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or explore 
what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Swift (`doc/guide/en/hello-world/swift.md`)

---
slug: hello-world/swift
title: Hello World [Swift]
language: en
canonical_slug: hello-world/swift
audience: external
maturity: mature
guide_order: 30
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/swift/hello-world.swift
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-19T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
  - RefAny
  - WindowCreateOptions
  - Update
---

# Hello World [Swift]

In order to use `libazul` from Swift (5.10+ on Linux / macOS, 6+ on Windows), you need 
the native library and the generated API bindings.

## Installation

Download the release bundle, which contains the Swift bindings (`Azul/`), the C 
interop map (`module.modulemap`), and the example script (`hello-world.swift`).

```sh
# macOS
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
# Linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so
# Windows
curl -O https://azul.rs/ui/release/$VERSION/azul.dll
curl -O https://azul.rs/ui/release/$VERSION/azul.dll.lib

curl -LO https://azul.rs/ui/release/$VERSION/azul-swift-$VERSION.tar.gz
tar xzf azul-swift-$VERSION.tar.gz
```

### Building from source

Only needed if you want to track `master` or patch the library locally:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
cargo run -p azul-doc --release -- codegen all
cargo build -p azul-dll --release --features build-dll
```

The DLL lands in `target/release/libazul.{so,dylib}` (or `azul.dll`), 
the bindings end up in `target/codegen/swift/` (generated by 
`azul-doc codegen all`).

## Simple "Counter" Example

```swift
import Azul

final class Counter {
    var count = 5
}

func onClick(_ counter: Counter, _ info: CallbackInfo) -> Update {
    counter.count += 1
    return .refreshDom
}

func layout(_ counter: Counter, _ info: LayoutCallbackInfo) -> Dom {
    let label = Dom.pWithText(String(counter.count))
        .withCss("font-size: 32px; margin: 0;")

    let button = Button("Increase counter")
        .withButtonType(.primary)
        .withOnClick(counter, onClick: onClick)

    return Dom.body()
        .withChild(label)
        .withChild(button.dom())
}

let window = WindowCreateOptions(layout)
window.windowState.title = "Hello World"
window.windowState.size.dimensions.width = 400
window.windowState.size.dimensions.height = 300

let app = App(Counter(), AppConfig())
app.run(window)
```

## Binding Internals

Callbacks mutate the model in place, the binding retains the object 
automatically for as long as the framework references it. Callbacks
are plain functions, they take the specific model type (`Counter`) 
directly. The framework handles type-erasure and downcasting behind the scenes. 

Enums use leading-dot syntax (`.primary`, `.refreshDom`) and Swift `String`s 
converts natively.

## Build and run

Compile the `Azul` module first, then compile your application against it. 

Note: The module library is named `AzulSwift` to prevent filename 
collisions with `libazul.so/dylib` on case-insensitive filesystems.

Linux:

```sh
# compile
swiftc -emit-library \
       -emit-module \ 
       -module-name Azul \
       -parse-as-library -j8 \
       -I. Azul/*.swift \
       -L. -lazul \
       -o libAzulSwift.so

# link
swiftc -I. hello-world.swift \
       -L. -lAzulSwift -lazul \
       -o hello-world

# run
LD_LIBRARY_PATH=. ./hello-world
```

macOS:

```sh
# compile framework
swiftc -emit-library \
       -emit-module  \
       -module-name Azul \
       -parse-as-library -j8 \
       -I. Azul/*.swift  \
       -L. -lazul \
       -o libAzulSwift.dylib

# link
swiftc -I. hello-world.swift -L. \
       -lAzulSwift -lazul \
       -framework Foundation \
       -framework AppKit \
       -framework OpenGL \
       -framework CoreGraphics \
       -framework CoreText \
       -o hello-world

# run
DYLD_LIBRARY_PATH=. ./hello-world
```

Windows (use the Git Bash, ideally):

```sh
# compile
swiftc -emit-library \
       -emit-module \
       -module-name Azul \
       -parse-as-library -j8 \
       -I. Azul/*.swift azul.dll.lib \
       -o AzulSwift.dll

# link
swiftc -I. hello-world.swift \
           AzulSwift.lib azul.dll.lib \
       -o hello-world.exe

./hello-world.exe
```

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `app.run(...)` opens a native window and runs the layout callback once with your data model.
2. The returned DOM is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event filter set up in the DOM. 
   On receiving a click event, the framework borrows your data model mutably, runs the click callback, 
   observes the `.refreshDom` return, and re-invokes the layout callback.
4. The framework determines the diff between the previous frame's DOM and the current one, 
   and only re-updates and re-paints the counter, not the entire window.

Congratulations! Once you've got the hello-world example running, you've already mastered 80% of 
the framework. As you might have guessed, more complex UI and styling are only composing more Dom 
objects together and working with the various event filters. 

You can now start reading about the [architecture patterns](../architecture.md) or explore 
what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Hello World Zig (`doc/guide/en/hello-world/zig.md`)

---
slug: hello-world/zig
title: Hello World [Zig]
language: en
canonical_slug: hello-world/zig
audience: external
maturity: mature
guide_order: 22
topic_only: false
prerequisites: [hello-world]
tracked_files:
  - api.json
  - examples/zig/hello-world.zig
last_generated_rev: 2660b0c45c9ea401ad6777a203f468755167e62e
generated_at: 2026-09-16T00:00:00Z
default-search-keys:
  - App
  - AppConfig
  - Dom
  - Button
---

# Hello world [Zig]

To use `libazul` from Zig (0.16+), you will need:

- The `libazul` native library from the release page
- The `azul.zig` wrapper, which contains all the structs, functions and Zig wrappers, 
  so that you don't need to interact with the C API directly
  and hides the "C" layer beneath
- A minimal `build.zig` and the `hello-world.zig` for your project

```sh
curl -LO https://azul.rs/ui/release/$VERSION/azul-zig-$VERSION.tar.gz
tar xzf azul-zig-$VERSION.tar.gz # azul.zig, build.zig, hello-world.zig

# linux
curl -O https://azul.rs/ui/release/$VERSION/libazul.so
LD_LIBRARY_PATH=. zig build run
# macos
curl -O https://azul.rs/ui/release/$VERSION/libazul.dylib
DYLD_LIBRARY_PATH=. zig build run
# windows
curl -O https://azul.rs/ui/release/$VERSION/azul.dll
zig build run
```

The `LD_LIBRARY_PATH=.` / `DYLD_LIBRARY_PATH=.` prefix is necessary
because the downloaded `build.zig` links `libazul` from the current
directory but does not embed an rpath - the dynamic loader has to be
told where the library lives at run time.

## Simple "Counter" Example

```zig
const std = @import("std");
const azul = @import("azul.zig");

const MyDataModel = struct {
    counter: u32,
};

const MyModelRef = azul.ReflectModel(MyDataModel).Ref;

fn onClick(model: MyModelRef, _: azul.CallbackInfo) azul.Update {
    const m = model.get();
    m.counter += 1;
    return .RefreshDom;
}

fn layout(model: MyModelRef, _: azul.LayoutCallbackInfo) azul.Dom {
    const m = model.get();

    var buf: [16]u8 = undefined;
    const slice = std.fmt.bufPrint(&buf, "{d}", .{m.counter}) catch return azul.Dom.createBody();
    
    var label = azul.Dom.createPWithText(slice);
    label.setCss("font-size: 32px; margin: 0;");

    var button = azul.Button.create("Increase counter");
    button.setButtonType(azul.C.AzButtonType_Primary);
    
    button.setOnClick(model.clone(), onClick);
    
    var body = azul.Dom.createBody();
    body.addChild(label.inner);
    body.addChild(button.dom().inner);
    return body;
}

pub fn main(init: std.process.Init) !void {
    _ = init; 
    
    const data = MyDataModel{ .counter = 5 };

    var window = azul.WindowCreateOptions.create(layout);
    
    window.inner.window_state.title = azul.C.AzString_fromUtf8("Hello World".ptr, 11);
    window.inner.window_state.size.dimensions.width = 400.0;
    window.inner.window_state.size.dimensions.height = 300.0;

    var app = azul.App.create(data, azul.AppConfig.create());
    app.run(window.inner);
}
```

## Binding Internals

The snippet above uses advanced Zig features to erase boilerplate:

1. `comptime` Type Reflection (`azul.ReflectModel`): By passing the state struct 
   into the `comptime` wrapper, the bindings generate a smart-pointer `Ref` type for you.
2. Native Slice Integration: You can pass Zig string slices directly to wrapper functions 
   like `Dom.createPWithText("string")`. The codegen leverages `anytype` behind the 
   scenes to do zero-cost conversions to C FFI boundaries.
3. Pure Zig Callbacks: Instead of wrestling with raw `AzRefAny` pointers and downcasting 
   inside every callback, `setOnClick` uses `comptime` introspection to generate C-ABI 
   shims for your functions, letting you write perfectly typed callbacks with pure Zig 
   inputs and outputs. This way, you can return `.RefreshDom` or wrapper structs directly.

## Build and run

```sh
# macOS
DYLD_LIBRARY_PATH=. zig build run
# Linux
LD_LIBRARY_PATH=. zig build run
# Windows
zig build run
```

If you prefer a single explicit command without `build.zig`, this is 
the invocation the end-to-end harness uses:

```sh
# Linux
zig build-exe hello-world.zig -lc -lazul -L. -rpath . -femit-bin=hello-world
LD_LIBRARY_PATH=. ./hello-world

# macOS
zig build-exe hello-world.zig -lc -lazul -L. -rpath . \
  -framework Foundation -framework AppKit -framework OpenGL \
  -framework CoreGraphics -framework CoreText -femit-bin=hello-world
DYLD_LIBRARY_PATH=. ./hello-world
```

You should see the window pictured on the [hello-world landing page](../hello-world.md). 
Click the button: the counter should increment, the layout callback then re-runs, and the new value renders.

1. `app.run` opens a native window and runs the layout callback once with your `data`.
2. The returned `Dom` is styled, laid out, and rendered.
3. The framework then continuously queries whether anything matches the event 
   filter set up in the `Dom`. On click, the framework borrows your `RefAny` mutably, 
   runs `onClick`, observes the `Update.RefreshDom` return, and re-invokes the 
   layout callback.
4. The framework determines the diff between the previous frame's `Dom` and the 
   current one, and only re-updates and re-paints the counter, not the entire window.

Congratulations - once you've got the hello-world example running, you've already mastered 
80% of the framework. As you might have guessed, more complex UI and styling are only 
composing more Dom objects together and working with the various event filters. 

To make this more streamlined, you can now start reading about the 
[architecture patterns](../architecture.md) or explore what [methods the `Dom` has to offer](../dom.md). 

See you in the next tutorial!

---

### Doc Guide En Images Canvas Gl (`doc/guide/en/images/canvas-gl.md`)

---
slug: images/canvas-gl
title: GL Canvas
language: en
canonical_slug: images/canvas-gl
audience: external
maturity: wip
guide_order: 92
topic_only: false
short_desc: Embedding an OpenGL canvas inside a Dom node
prerequisites: [images]
tracked_files:
  - core/src/svg.rs
  - core/src/svg_path_parser.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - ImageRef
  - Texture
  - GlContextPtr
  - RenderImageCallback
  - RenderImageCallbackInfo
  - HidpiAdjustedBounds
  - CallbackInfo
  - RefAny
  - Update
---

# GL Canvas

## Introduction

*WIP.* The GL callback ABI is stable; what you can call on the GL context is the OpenGL 3.2-core subset exposed by `GlContextPtr`.

A render-image callback gives you a fresh GL context and the laid-out bounding box of a DOM node, and asks you to return an `ImageRef` for that node. Use it whenever the pixel content depends on size or animation: an OpenGL scene, a tessellated SVG that needs custom transforms, a chart sized to fit its container.

## The callback

```rust,ignore
pub type RenderImageCallbackType =
    extern "C" fn(RefAny, RenderImageCallbackInfo) -> ImageRef;
```

Wrap a function pointer in `RenderImageCallback::create(...)` and convert it to the FFI-compatible form with `.to_core()`:

```rust,no_run
use azul::prelude::*;

extern "C" 
fn render(_data: RefAny, _info: RenderImageCallbackInfo) -> ImageRef {
    // ... build texture ...
    panic!("returned a Texture-backed ImageRef")
}

fn build(state: RefAny) -> Dom {
    Dom::create_image(ImageRef::callback(
        RenderImageCallback::create(render).to_core(),
        state,
    ))
}
```

`RenderImageCallbackInfo` exposes:

- `get_bounds()`. Returns `HidpiAdjustedBounds` with logical and physical sizes.
- `get_gl_context()`. Returns `OptionGlContextPtr`. `None` on backends without GL.
- `get_callback_node_id()`. The node ID this callback is attached to.
- `get_ctx()`. The FFI context, used by Python and C bindings.

## Allocating a texture

`Texture::allocate_rgba8` allocates an RGBA8 texture sized to a `PhysicalSizeU32` and clears it to a background color:

```rust,no_run
use azul::prelude::*;

fn body(info: &mut RenderImageCallbackInfo) -> Option<Texture> {
    let gl = info.get_gl_context().into_option()?;
    let size = info.get_bounds().get_physical_size();
    let mut texture = Texture::allocate_rgba8(
        gl, size, ColorU::white(),
    );
    texture.clear();
    Some(texture)
}
```

Sizing matches the post-layout physical pixel box, so the texture and the on-screen draw area are 1:1. The renderer doesn't rescale.

## Drawing tessellated SVG into a texture

`Texture::draw_tesselated_svg_gpu_node` takes a GPU mesh, a target size, a fill color, and an optional list of transforms. Transforms are `StyleTransform`s — the same ones the CSS layer uses — so percentage translations resolve against the texture size:

```rust,no_run
use azul::prelude::*;

fn run(
    texture: &mut Texture, 
    mesh: TessellatedGPUSvgNode,
    size: PhysicalSizeU32, 
    deg: f32
) {
    texture.draw_tesselated_svg_gpu_node(
        mesh,
        size,
        ColorU::from_str("#cc00cc"),
        vec![
            StyleTransform::Translate(StyleTransformTranslate2D {
                x: PixelValue::percent(50.0),
                y: PixelValue::percent(50.0),
            }),
            StyleTransform::Rotate(AngleValue::deg(deg)),
        ],
    );
}
```

To anti-alias the result, call `texture.apply_fxaa()` before returning. It's a single-pass post-process suitable for vector content.

## Returning the texture

A texture becomes an `ImageRef` via `ImageRef::gl_texture(texture)`. Refs are reference-counted, so returning a clone after every callback is fine:

```rust,no_run
use azul::prelude::*;

fn done(t: Texture) -> ImageRef {
  ImageRef::gl_texture(t)
}
```

If something fails (no GL context, missing data, GPU error), return a `null_image` 
of the requested size. The renderer treats it as transparent and reserves space in the layout.

## When the callback runs

A render-image callback runs:

1. Once on first display of the DOM node, after layout has assigned the node a size.
2. On every frame the DOM is re-built, if the node is still in the tree. To drive animation without rebuilding, return `Update::RefreshDom` from a [timer](../animations/timers.md) callback so the framework reissues the render.
3. Never if the node never enters the tree, or if the render-image feature is disabled (e.g. headless mode without a GL context).

The renderer doesn't memoize results. The callback is responsible for caching its own state in the `RefAny` it received.

## A complete loop

The end-to-end pattern: tessellate SVG geometry once, upload to GPU buffers in a startup callback, redraw on every animation tick. The shape of the code is:

```rust,no_run
use azul::prelude::*;

struct AppState {
    rotation_deg: f32,
    fill_buffer: Option<TessellatedGPUSvgNode>,
}

extern "C" 
fn render(mut data: RefAny, mut info: RenderImageCallbackInfo) -> ImageRef {
    let size = info.get_bounds().get_physical_size();
    let invalid = ImageRef::null_image(
        size.width as usize, size.height as usize,
        RawImageFormat::RGBA8, U8VecRef::from(&[][..]),
    );
    let result = (|| {
        let mut state = data.downcast_mut::<AppState>()?;
        let gl = info.get_gl_context().into_option()?;
        let buffer = state.fill_buffer.as_ref()?.clone();
        let mut texture = Texture::allocate_rgba8(gl, size, ColorU::white());
        texture.clear();
        texture.draw_tesselated_svg_gpu_node(
            buffer, size, ColorU::from_str("#0080ff"),
            vec![],
        );
        texture.apply_fxaa();
        Some(ImageRef::gl_texture(texture))
    })();
    result.unwrap_or(invalid)
}
```

The full working example lives in `examples/rust/src/opengl.rs`. It parses GeoJSON, tessellates the polygons once, and rotates the result on a timer.

## Lifetime and cleanup

`Texture` is reference-counted and frees its GL texture when the last clone drops. Returning a fresh `ImageRef::gl_texture(...)` from each callback invocation is the normal pattern: the previous frame's texture is dropped automatically by the framework once the new one replaces it.

`GlContextPtr` is also reference-counted. Cloning it in your `RefAny` state is safe and avoids re-fetching it via `info.get_gl_context()` on hot paths.

---

### Doc Guide En Images Clip Masks (`doc/guide/en/images/clip-masks.md`)

---
slug: images/clip-masks
title: Clip Masks
language: en
canonical_slug: images/clip-masks
audience: external
maturity: wip
guide_order: 94
topic_only: false
short_desc: Clipping a node to an image's alpha, and to an SVG path
prerequisites: [hello-world, images]
tracked_files:
  - core/src/dom.rs
  - layout/src/solver3/display_list.rs
default-search-keys:
  - ImageMask
  - with_clip_mask
  - change_node_image_mask
  - SvgNodeData
---

# Clip Masks

A clip mask decides, per pixel, how much of a node is drawn. Anything
that produces an image can be a mask - including an image callback,
which is what makes a mask animatable.

## Introduction

Masking happens at paint time, not at layout time. A masked node
occupies exactly the same box it would otherwise; the mask only
changes which of its pixels reach the screen. That is why masking
never reflows anything, and why a mask can change every frame cheaply.

Azul has two kinds: a raster mask built from an image, and a vector
clip built from SVG geometry.

## An image as a mask

```rust,no_run
use azul::prelude::*;

let mask = ImageMask {
    image: mask_image,                       // alpha is what counts
    rect: LogicalRect::new(
        LogicalPosition::new(0.0, 0.0),
        LogicalSize::new(200.0, 200.0),
    ),
    repeat: false,
};

let avatar = Dom::create_image(photo).with_clip_mask(mask);
```

The mask image's alpha channel is the mask: opaque pixels show the
node, transparent ones hide it, and partial alpha gives a soft edge -
which is how you get an anti-aliased circular avatar without a
rounded-corner approximation.

`rect` is **element-local**. Its origin is measured from the node's own
paint box, so `(0, 0)` is the node's top-left corner and the mask moves
with the node.

Coverage outside `rect` is **zero**: a mask smaller than the node hides
everything the mask does not reach, rather than leaving it painted. A
mask larger than the node is cropped by the node's box.

`repeat` tiles the mask across the node from `rect`'s origin, which is
how you build a texture, a hatch or a scanline effect from a small
image. The tiling is resolved when the display list is built, at the
mask image's own resolution, so a tile stays crisp on a HiDPI display.
Two cases fall back to drawing the mask once: a mask whose pixels are
not on the CPU (a GL texture, an image callback), and a tile so small
relative to the node that the tile sheet would exceed 4096x4096 - a 1px
tile over a full-screen element is millions of tiles, and allocating
that silently would be worse than not repeating.

## An SVG path as a mask

A node carrying SVG path geometry is clipped to that path instead. The
path is rasterised against the node's box, in the coordinate system of
the nearest `<svg>` ancestor's `viewBox` - so geometry authored at 16
units inside a `viewBox="0 0 16 16"` fills the node rather than a
sixteenth of it.

This path needs the `cpurender` feature. A build without it prints one
warning and draws the node unclipped, which is worth knowing before
concluding that a clip "does not work".

## Changing the mask from a callback

```rust,ignore
info.change_node_image_mask(dom_id, node_id, new_mask);
```

Like the other in-place mutations, this writes into the current
display list: no `layout()`, no diff, no reflow. Call it as often as
you like.

That makes the obvious animations cheap. A wipe transition is a mask
whose `rect` grows each tick; a spotlight is a radial-gradient mask
whose origin follows the cursor; a progress ring is a mask swapped for
the next frame of a pre-rendered sequence. Drive any of them from a
[timer](../animations/timers.md) and return `Update::DoNothing`.

The other way to animate a mask is not to change it at all: build the
mask from `ImageRef::callback()` and let the mask *redraw itself*, then
call `update_image_callback()` when its input changes. See
[Image Callbacks](image-callbacks.md).

## Cross-references

- [Image Callbacks](image-callbacks.md) - nodes that draw themselves.
- [Images](../images.md) - `ImageRef` construction.
- [SVG](svg.md) - the geometry model the vector clip uses.

---

### Doc Guide En Images Image Callbacks (`doc/guide/en/images/image-callbacks.md`)

---
slug: images/image-callbacks
title: Image Callbacks
language: en
canonical_slug: images/image-callbacks
audience: external
maturity: wip
guide_order: 93
topic_only: false
short_desc: Nodes that draw themselves - CPU or GPU - and how to make one repaint
prerequisites: [hello-world, images, events/callbacks]
tracked_files:
  - core/src/resources.rs
  - core/src/callbacks.rs
  - layout/src/callbacks.rs
  - examples/azul-paint/src/lib.rs
default-search-keys:
  - ImageRef
  - RenderImageCallback
  - RenderImageCallbackInfo
  - update_image_callback
  - update_all_image_callbacks
  - get_bounds
---

# Image Callbacks

An image node does not have to hold pixels. `ImageRef::callback()`
makes a node that produces its content on demand, once layout knows how
big it is - a paint canvas, a chart, a map tile, a video frame.

## Introduction

This is the same mechanism a GL canvas uses, but it is not GL-specific:
the callback may return a raw CPU image just as well as a texture, and
most custom drawing in azul does. [GL Canvas](canvas-gl.md) covers the
texture-and-FXAA path specifically; this page is the general shape.

## The callback

```rust,no_run
use azul::prelude::*;

extern "C" fn render(mut data: RefAny, mut info: RenderImageCallbackInfo) -> ImageRef {
    let size = info.get_bounds().get_logical_size();
    let (w, h) = (size.width.max(1.0) as u32, size.height.max(1.0) as u32);
    // ... draw into a buffer of w x h ...
    ImageRef::null_image(w as usize, h as usize,
                         RawImageFormat::RGBA8, U8VecRef::from(&[][..]))
}

fn canvas(state: RefAny) -> Dom {
    Dom::create_image(ImageRef::callback(
        RenderImageCallback::create(render).to_core(),
        state,
    ))
}
```

The `RefAny` is the callback's own state, separate from the app model,
and it is where a canvas keeps its pixel buffer between frames.

`RenderImageCallbackInfo` is deliberately small. `get_bounds()` is the
laid-out box - the only reliable source of the size, since CSS decides
it. `get_callback_node_id()` is the node being rendered.
`get_gl_context()` is the GL context, present only when a GPU backend
is running, so a callback that wants to work everywhere needs a CPU
path too. `log()` is how the callback reports trouble: it runs inside
the paint pass, where a panic would take the frame with it.

Return an `ImageRef` of *some* size in every path. Returning a
zero-sized or invalid image makes the node draw nothing with no
diagnostic; a `null_image` of the right dimensions at least shows the
node exists.

## Making it repaint

The callback is not re-run every frame. It runs when the node is
mounted, when its size changes, and when you ask:

```rust,ignore
info.update_image_callback(dom_id, node_id);   // this node
info.update_all_image_callbacks();             // every image callback
```

The single-node form is the one to use. The blanket form re-renders
every image callback in the window, which is right after a device
change - a GL context loss, a DPI change - and wasteful otherwise.

`update_image_callback()` takes a `NodeId`, not a `DomNodeId`, and
node-finding methods return a `NodeHierarchyItemId`, whose raw value is
offset by one - zero means "no node". Convert deliberately:

```rust,ignore
if let Some(node) = info.get_node_id_by_marker(marker).into_option() {
    let raw = node.node.into_raw();
    if raw != 0 {
        info.update_image_callback(node.dom, NodeId { inner: raw });
    }
}
```

That marker indirection is how a callback on a *different* node - a
toolbar button, a pointer handler on an overlay - reaches the canvas.
See [Node Tree & Hit Testing](../events/node-tree.md).

## Redrawing only when something changed

A pointer-move handler fires far more often than the canvas needs to be
rasterised. The pattern the paint example uses is a revision counter in
the callback's own state:

```rust,ignore
struct CanvasCache {
    model: RefAny,     // the document being drawn
    image: Option<ImageRef>,
    rendered_rev: u64, // which revision `image` was rendered from
}
```

The render callback compares the model's current revision with
`rendered_rev`, returns the cached `ImageRef` when they match, and
rasterises only when they differ. With that in place, calling
`update_image_callback()` on every pointer move is cheap: most calls
hit the cache, and the ones that do not had real work to do.

## Animation

For continuous animation, drive the repaint from a
[timer](../animations/timers.md) that calls `update_image_callback()`
on each tick, and return `Update::DoNothing` - the node repaints
without a relayout and without running your `layout()` function.

Return `Update::RefreshDom` only when the *structure* changed. An
animation that rebuilds the DOM every frame will be smooth on your
machine and not on anyone else's.

## Swapping an image without a rebuild

For a node whose image is data rather than a drawing, there is no need
for a callback at all:

```rust,ignore
info.change_node_image(dom_id, node_id, new_image, UpdateImageType::Content);
```

`UpdateImageType::Content` replaces the node's image content;
`UpdateImageType::Background` replaces its CSS background image. Both
write into the current display list and skip `layout()` entirely, which
makes them the right call for a frame-by-frame swap - a video player
pushing decoded frames, a sprite animation, a live preview.

The named image cache is the other half of this:

```rust,ignore
info.add_image_to_cache("logo".into(), image_ref);
info.remove_image_from_cache("logo".into());
```

A cached image is addressable by name from any later layout pass, so a
loader thread can decode an image, put it in the cache, and let the
next `layout()` reference it without threading an `ImageRef` through
the model.

`ImageRef` is intern-keyed and reference counted: handing the same
clone to two consecutive `Dom`s reuses one upload, and the GPU memory
is freed when the last clone drops. To change pixels you build a new
`ImageRef` - which is why a swap costs an upload and a redraw through a
callback does not.

## Cross-references

- [GL Canvas](canvas-gl.md) - the GPU texture path in detail.
- [Clip Masks](clip-masks.md) - masking a node with an image.
- [Images](../images.md) - `ImageRef` construction and sizing.

---

### Doc Guide En Images Svg (`doc/guide/en/images/svg.md`)

---
slug: images/svg
title: SVG
language: en
canonical_slug: images/svg
audience: external
maturity: wip
guide_order: 91
topic_only: false
short_desc: Parsing and rendering SVG documents
prerequisites: [images]
tracked_files:
  - core/src/svg.rs
  - core/src/svg_path_parser.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - Svg
  - SvgPath
  - SvgRect
  - SvgMultiPolygon
  - SvgFillStyle
  - SvgStrokeStyle
  - SvgDashPattern
  - SvgFillRule
  - SvgLineJoin
  - TessellatedSvgNode
  - RawImage
  - Dom
---

# SVG

## Introduction

Scalable Vector Graphics (svg) support in azul is built around a small, copy-friendly 
geometry model. You construct paths in memory, tessellate them once, and either upload 
the result to a GPU buffer or rasterize it via the CPU SVG path. Higher-level XML-driven 
SVG (image embedding, gradients, full document parsing) goes through the `Svg` helper.

## Geometry types

```rust,ignore
pub enum SvgPathElement {
    Line(SvgLine),
    QuadraticCurve(SvgQuadraticCurve),
    CubicCurve(SvgCubicCurve),
}

pub struct SvgPath { pub items: SvgPathElementVec }
pub struct SvgMultiPolygon { pub rings: SvgPathVec }
```

An `SvgPath` is one open or closed contour built from line and Bézier segments. Several 
paths combine into an `SvgMultiPolygon`. Interior holes are represented by reversing the 
winding order of a ring. The fill rule defaults to `Winding`. Switch to `EvenOdd` via 
`SvgFillStyle.fill_rule` when interior overlaps need to cancel.

Construct a path with `SvgPath::create(items)` and a multi-polygon with `SvgMultiPolygon::create(rings)`. Inspect with `SvgPath::is_closed`, `get_start`, `get_end`, `get_bounds`. Modify with `SvgPath::close`, `reverse`, `join_with`.

`SvgRect` covers analytic rectangles with optional corner radii (`x`, `y`, `width`, `height`, `radius_top_left`, `radius_top_right`, `radius_bottom_left`, `radius_bottom_right`). Use `SvgRect::expand`, `get_center` for layout helpers.

## Closed and open paths

`SvgPath::is_closed()` returns `true` when the last segment's end equals the first segment's start. `close()` appends an explicit line segment if the path isn't already closed (matching the SVG `Z` command). Tessellation treats open paths as strokes only. A fill on an open path produces an empty triangle list.

## Tessellation

A polygon becomes triangles via `tessellate_fill` and `tessellate_stroke` on `SvgMultiPolygon`. Each returns a `TessellatedSvgNode`:

```rust,ignore
pub struct TessellatedSvgNode {
    pub vertices: SvgVertexVec,  // (x, y) pairs
    pub indices: U32Vec,          // triangle list
}
```

`SvgFillStyle` controls fill behavior:

- `fill_rule: SvgFillRule`. `Winding` (default) or `EvenOdd`.
- `tolerance: f32`. Curve subdivision tolerance in CSS pixels.
- `anti_alias: bool`, `high_quality_aa: bool`.
- `line_join: SvgLineJoin`, `miter_limit: f32`.
- `transform`. An optional pre-tessellation transform.

```rust,no_run
use azul::svg::{SvgFillStyle, SvgMultiPolygon};

fn polygon() -> SvgMultiPolygon { panic!() }

fn main() {
    let mut style = SvgFillStyle::default();
    style.tolerance = 0.5;
    let mesh = polygon().tessellate_fill(style);   
}
```

Stroke options live in `SvgStrokeStyle`:

- `line_width`. Stroke width in user units.
- `start_cap` / `end_cap`. `SvgLineCap::Butt`, `SvgLineCap::Square`, `SvgLineCap::Round`.
- `line_join`. `SvgLineJoin::Miter`, `MiterClip`, `Round`, `Bevel`.
- `miter_limit`. Ratio of miter length to stroke width.
- `dash_pattern`. Optional `SvgDashPattern` for dashed strokes.

## Drawing on the GPU

Once tessellated, a node can be uploaded once and drawn many times by wrapping it in a `TessellatedGPUSvgNode`:

```rust,no_run
use azul::svg::*;
use azul::gl::GlContextPtr;

fn ctx() -> GlContextPtr { panic!() }
fn mesh() -> TessellatedSvgNode { panic!() }

let gpu_mesh = TessellatedGPUSvgNode::create(&mesh(), ctx());
```

Per-frame drawing happens inside an image-rendering callback (see 
[Canvas and GL Textures](canvas-gl.md)) by calling `Texture::draw_tesselated_svg_gpu_node`, 
optionally with a transform list to translate, rotate, or scale the geometry.

## XML-level SVG

`Svg` is a handle to a parsed SVG document. The constructors `Svg::from_string` and `Svg::from_bytes` read a full SVG document with `SvgParseOptions`:

- `dpi`. Default `96.0`.
- `default_font_family`.
- `font_size`.
- `shape_rendering`, `text_rendering`, `image_rendering`.
- `fontdb`.
- `keep_named_groups`, `languages`, `relative_image_path`.

The result is rendered to a `RawImage` via `SvgRenderOptions`, which lets you fix the output size (`SvgFitTo::Width`, `SvgFitTo::Height`, `SvgFitTo::Zoom`, `SvgFitTo::Original`) or use the document's intrinsic viewport, plus `target_size`, `background_color`, and `transform`. Wrap the returned image in `ImageRef::new_rawimage` and insert it like any other raster image.

## Combining many polygons

`TessellatedSvgNode::from_nodes` flattens a slice of meshes into a single vertex/index buffer. This matters for performance when drawing thousands of tiny polygons (for example, the `opengl` example tessellates thousands of map polygons and uploads exactly one fill mesh and one stroke mesh).

## Rendering SVG headlessly

The whole SVG-to-pixels pipeline runs without a window. There are two
shapes of "headless SVG":

**Inline SVG inside an XHTML document.** The XML parser recognises
`<svg>` tags inside `<body>` and turns them into vector nodes on the
returned `Dom`. Build the Dom with `Dom::create_from_parsed_xml`,
launch the binary with `AZ_BACKEND=headless`, and the framework
rasterises into an in-memory framebuffer instead of opening a
window. Pair with `AZ_DEBUG=<port>` and a `take_screenshot`
request returns a base64 PNG.

```sh
AZ_BACKEND=headless AZ_DEBUG=8765 ./my_svg_renderer &
curl -s -X POST http://localhost:8765/ \
    -d '{"type":"take_screenshot"}' \
  | jq -r '.data.value' | base64 -d > out.png
```

This is the path covered in [Headless Rendering](../headless/rendering.md).
The DOM-side example is in [Document Object Model](../dom.md#parsing-from-svg).

**Standalone SVG file.** When you have an `.svg` on disk and want a
raster out, parse with `Svg::from_string` (or `Svg::from_bytes`) and
let the renderer produce a `RawImage` directly. This skips the Dom
entirely:

```rust,no_run
use azul::prelude::*;

let svg_bytes = std::fs::read("logo.svg").unwrap();
let svg = Svg::from_bytes(svg_bytes.into(), SvgParseOptions::default()).unwrap();
let raw = svg.render(SvgRenderOptions::default()).unwrap();
let image = ImageRef::create_rawimage(raw);
// `image` is now embeddable as `Dom::create_image(image)`, or you
// can write the raw RGBA buffer to a PNG with the `image` crate.
```

Both paths share the SVG renderer described above. The choice is just
about whether you want the SVG rendered next to other Dom content
(use the inline form) or as an isolated raster image (use the
standalone form).

---

### Doc Guide En Layout Blocks (`doc/guide/en/layout/blocks.md`)

---
slug: layout/blocks
title: Simple Layout
language: en
canonical_slug: layout/blocks
audience: external
maturity: mature
guide_order: 81
topic_only: false
short_desc: Explains block formatting, sizing, positioning, the box model and how to handle overflowing content
prerequisites: [layout]
tracked_files:
  - css/src/props/layout/display.rs
  - css/src/props/layout/dimensions.rs
  - css/src/props/layout/position.rs
  - css/src/props/layout/spacing.rs
  - css/src/props/layout/overflow.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:49:28Z
default-search-keys:
  - Dom
  - Css
  - CssProperty
  - PixelValue
  - StyledDom
---

# Simple Layout

## Overview

Every element in a `Dom` starts at `display: block`. A block element takes
the full width of its parent, stacks below the previous sibling, and is
sized by the box-model properties (`width`, `height`, `padding`, `border`,
`margin`). Block layout runs by default. Flexbox and grid are opt-in.

## display: block and inline-block

The `display` property:

- `block` (default). Full-width box. Stacks vertically with siblings.
- `inline-block`. Flows with text but accepts `width` and `height`.
- `flex`. One-axis container. See [Flexbox](flex.md).
- `grid`. Two-axis container. See [Grid](grid.md).
- `none`. Removes from layout. The element stays in the DOM.

`inline` (without `-block`) is covered on
[Inline and inline-block](inline.md). The table values (`table`,
`table-row`, `table-cell`, `list-item`, `flow-root`) are honoured but rare
in app code.

```html
<div style='display: block; padding: 16px; background: #e0e7ff;'>first</div>
<div style='display: block; padding: 16px; background: #ddd6fe;'>second</div>
```

## Sizing: width, height, and min- / max- constraints

`width` and `height` accept:

- `auto`. The default. Sizes from content (block) or stretches to container.
- `<length>`. Pixels (`px`), points (`pt`), em (`em`), root em (`rem`),
  viewport (`vw` / `vh`), or `<percentage>`.
- `min-content`, `max-content`, `fit-content(<length>)`. Content-driven.
- `calc(<expr>)`. Arithmetic over the above. See [`calc()`](#calc) below.

`min-width`, `min-height`, `max-width`, and `max-height` take a single
pixel value. Defaults: `min-*` is `0px`, `max-*` is unconstrained.

```css
.card { width: 320px; min-height: 100px; max-width: 100%; }
.fluid { width: calc(50% - 20px); }
```

### box-sizing

- `content-box` (default). `width` and `height` apply to content only.
- `border-box`. `width` and `height` include padding and border.

`box-sizing: border-box` is what you usually want for forms and grids.
The `content-box` default matches the original CSS 1 behaviour.

### calc()

```css
.sidebar { width: calc(33.333% - 10px); }
.gutter  { width: calc((100% - 40px) / 3); }
```

Operators: `+`, `-`, `*`, `/`. Negative numeric literals are recognised
when they follow an operator or `(`. Otherwise `-` is a subtraction
operator.

## margin and padding

Padding insets the content within the box; margin separates the box from its
siblings:

```css
.card { padding: 12px 16px; margin: 0 auto; }
.tight { padding-top: 4px; padding-inline-start: 8px; }
```

The shorthand expands the standard CSS way. One value sets all four
sides. Two values set vertical and horizontal. Three values set top,
horizontal, bottom. Four values set top, right, bottom, left.
`padding-inline-start` and `padding-inline-end` follow `writing-mode` and
`direction`.

Margins on `position: static` block elements collapse vertically the way
CSS specifies. Margins inside `display: flex` and `display: grid`
containers do not collapse. Use `gap` instead.

## position

The `position` property:

- `static` (default). Normal flow. Offsets are ignored.
- `relative`. Normal flow, then shifted by `top`, `right`, `bottom`, `left`.
- `absolute`. Offsets, relative to the nearest positioned ancestor.
- `fixed`. Offsets, relative to the window.
- `sticky`. Flow until offsets would be violated, then pinned to the scroll container.

"Positioned ancestor" means any ancestor whose `position` is not `static`.

```html
<div style='position: relative;'>
   <div style='position: absolute; top: 8px; right: 8px;'>badge</div>
   container content
 </div>
```

### Offsets: top, right, bottom, left

Each offset is a `<length>` or `<percentage>`. `auto` (the default) means
"no constraint" and lets the solver compute it.

- `<length>` is a fixed pixel distance.
- `<percentage>` resolves against the containing block's width
  (`left`/`right`) or height (`top`/`bottom`).

For `position: absolute`:

- Setting `top` and `bottom` stretches the element vertically.
- Setting `left` and `right` stretches it horizontally.
- Combined with `width: auto` / `height: auto`, this fills a positioned
  ancestor exactly.

```html
<div style='position: relative; height: 200px;'>
   <div style='position: absolute; top: 0; bottom: 0; right: 0; width: 60px;'>
     full-height sidebar
   </div>
 </div>
```

### position: sticky

A sticky element behaves like `relative` until the user scrolls past the
offsets, at which point it pins to the scroll container's edge:

```html
<div style='overflow: auto; height: 300px;'>
   <h2 style='position: sticky; top: 0; background: white;'>section A</h2>
   <p>...long content...</p>
   <h2 style='position: sticky; top: 0; background: white;'>section B</h2>
   <p>...long content...</p>
 </div>
```

The nearest scroll container is whatever ancestor has `overflow: auto |
scroll | hidden | clip`. If the document body is the scroll container, the
sticky element pins to the window.

## z-index

`z-index` accepts:

- `auto` (default). Same stacking level as parent.
- `<integer>`. Explicit stacking order. Positive values stack above,
  negative below.

`z-index` only takes effect on positioned elements (`position` is not
`static`). A positioned element with `z-index: <integer>` creates a new
*stacking context*. Its descendants then stack relative to it, not the
document root.

## overflow

`overflow` controls clipping and scrolling. Apply `overflow-x` and
`overflow-y` independently, or set both with the shorthand `overflow`.

- `visible` (default). No clip, no scroll.
- `hidden`. Clips. Programmatic scrolling only. No scrollbar.
- `clip`. Clips. No scrolling.
- `scroll`. Clips and scrolls. Scrollbar always shown.
- `auto`. Clips and scrolls. Scrollbar shown when content overflows.

### clip vs hidden

`clip` is a stricter form of `hidden`:

- Both clip overflowing content.
- `hidden` permits programmatic scrolling. `clip` does not. `clip` makes
  the element a "non-scroll container".
- `overflow-clip-margin` extends the clip region outside the box. It has
  no effect on `hidden`.

### Computed-value coupling

CSS Overflow 3 § 3.1 specifies that `visible` or `clip` on one axis
becomes `auto` or `hidden` if the other axis is scrollable:

```text
overflow-x: visible, overflow-y: scroll  →  computed: auto / scroll
overflow-x: clip,    overflow-y: hidden  →  computed: hidden / hidden
overflow-x: visible, overflow-y: visible →  unchanged
```

### scrollbar-gutter

`scrollbar-gutter` reserves space for the scrollbar so layout doesn't
shift when content starts overflowing.

- `auto` (default). Gutter only when the scrollbar is shown.
- `stable`. Gutter always reserved on the scrollbar's edge.
- `stable both-edges`. Gutter on both edges for visual symmetry.

```css
.pane { overflow-y: auto; scrollbar-gutter: stable; }
```

### overflow-clip-margin

`overflow-clip-margin` extends the clip region outside the box. It only
applies when `overflow: clip` is set.

Syntax: `<visual-box> || <length>`. The box defaults to `padding-box`. The
length defaults to `0px`.

```css
.badge { overflow: clip; overflow-clip-margin: padding-box 4px; }
```

### The legacy clip: rect(...) property

`clip: rect(...)` is the deprecated CSS 2.1 property used with
`position: absolute`. Each edge is either `auto` (the corresponding box
edge) or a `<length>`:

```css
.overlay { position: absolute; clip: rect(0, 100px, 50px, 0); }
```

Prefer `clip-path` for new code.

## gap, row-gap, column-gap

`gap` adds space between flex and grid items without a margin on each
child:

```css
.grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
.row  { display: flex; column-gap: 8px; row-gap: 4px; }
```

The shorthand `gap: <row> <column>` and the longhands `row-gap` and
`column-gap` all take a single `<length>`. `gap` does nothing on plain
block children. It's flex and grid only.

## Recipes

### Modal overlay

```azul-render screenshot=pos-modal width=480 height=300 subtitle="Modal centred over a fixed-position backdrop"
<body style="font-family: sans-serif;">
  <div style="position: relative; height: 280px; background: #f3f4f6;">
    <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4);"></div>
    <div style="position: absolute; top: 50%; left: 50%; width: 240px; height: 120px; margin-left: -120px; margin-top: -60px; background: white; padding: 16px;">
      <strong>Modal title</strong>
      <p>Modal body content.</p>
    </div>
  </div>
</body>
```

### Pinned header

```azul-render screenshot=pos-sticky width=400 height=300 subtitle="position: sticky pins the header within a scroll container"
<body style="font-family: sans-serif;">
  <div style="overflow: auto; height: 280px; border: 1px solid #ddd;">
    <h2 style="position: sticky; top: 0; background: #fef3c7; margin: 0; padding: 8px;">Sticky header</h2>
    <div style="height: 100px; padding: 8px;">row 1</div>
    <div style="height: 100px; padding: 8px; background: #f3f4f6;">row 2</div>
    <div style="height: 100px; padding: 8px;">row 3</div>
  </div>
</body>
```

### Layered badge

```azul-render screenshot=pos-badge width=320 height=160 subtitle="Absolute-positioned badge on a relatively-positioned card"
<body style="font-family: sans-serif;">
  <div style="position: relative; width: 240px; height: 120px; background: #ddd6fe; padding: 16px; margin: 16px;">
    card body
    <div style="position: absolute; top: -8px; right: -8px; background: #ef4444; color: white; padding: 4px 8px; border-radius: 4px;">NEW</div>
  </div>
</body>
```

### Default block flow

```azul-render screenshot=layout-block width=480 height=200 subtitle="Default block flow. Each element on its own line"
<body style="font-family: sans-serif;">
  <div style="background: #e0e7ff; padding: 8px;">first</div>
  <div style="background: #ddd6fe; padding: 8px;">second</div>
  <div style="background: #c4b5fd; padding: 8px;">third</div>
</body>
```

## Default values at a glance

- `display` defaults to `block`.
- `width` and `height` default to `auto`.
- `box-sizing` defaults to `content-box`.
- `min-width` and `min-height` default to `0px`.
- `max-width` and `max-height` default to unconstrained.
- `margin-*` and `padding-*` default to `0px`.
- `position` defaults to `static`.
- `top`, `right`, `bottom`, and `left` default to `auto`.
- `z-index` defaults to `auto`.
- `overflow-x` and `overflow-y` default to `visible`.
- `scrollbar-gutter` defaults to `auto`.
- `overflow-clip-margin` defaults to `padding-box 0px`.

---

### Doc Guide En Layout Flex (`doc/guide/en/layout/flex.md`)

---
slug: layout/flex
title: Flexbox
language: en
canonical_slug: layout/flex
audience: external
maturity: mature
guide_order: 83
topic_only: false
short_desc: One-axis container layout with grow/shrink/basis
prerequisites: [layout]
tracked_files:
  - css/src/props/layout/flex.rs
  - css/src/props/layout/spacing.rs
last_generated_rev: 4a4eb85b8c8943cc13bff93b797f0b795c23069c
generated_at: 2026-05-02T05:49:28Z
default-search-keys:
  - Dom
  - Css
  - CssProperty
  - StyledDom
---

# Flexbox

## Overview

`display: flex` and `display: inline-flex` lay out children along one axis and align them on the other. The axis is set by `flex-direction`. The remaining properties decide how free space is distributed and where items align.

```html
<div style='display: flex; gap: 8px; padding: 8px;'>
   <div>one</div>
   <div>two</div>
   <div>three</div>
 </div>
```

## Container properties

These apply to the element with `display: flex` or `display: inline-flex`.

### flex-direction

* `row` (default). Main axis is horizontal. Items run first to last.
* `row-reverse`. Main axis is horizontal. Items run last to first.
* `column`. Main axis is vertical. Items run first to last.
* `column-reverse`. Main axis is vertical. Items run last to first.

`row` makes the inline axis the main axis. `column` makes the block axis the main axis.

### flex-wrap

* `nowrap` (default). Forces a single line for all children - items shrink to fit.
* `wrap`. Overflowing children wrap to a new line.
* `wrap-reverse`. Wraps in reverse cross-axis order.

```html
<div style='display: flex; flex-wrap: wrap; gap: 8px;'>
   <div style='width: 120px;'>one</div>
   <div style='width: 120px;'>two</div>
   <div style='width: 120px;'>three</div>
 </div>
```

### justify-content: main-axis alignment

`justify-content` distributes free space along the main axis.

* `start` (default). Items pack to the absolute start of the axis.
* `end`. Items pack to the absolute end of the axis.
* `flex-start`. Items pack to the flex-relative start. 
  * Under `row-reverse` or `column-reverse`, this equals `end`.
* `flex-end`. Items pack to the flex-relative end. 
  * Under `row-reverse` or `column-reverse`, this equals `start`.
* `center`. Free space splits equally on both ends.
* `space-between`. Space between items, none at ends.
* `space-around`. Half-space at ends, full between.
* `space-evenly`. Equal space everywhere.

`start` and `flex-start` are distinct variants and do not behave the same in reversed directions, i.e. in RTL contexts. The same applies to `end` and `flex-end`.

### align-items: cross-axis alignment for every line

* `stretch` (default). Fills the cross axis.
* `start` / `flex-start`. Aligns to cross-start.
* `end` / `flex-end`. Aligns to cross-end.
* `center`. Centres on cross axis.
* `baseline`. Aligns text baselines.

Unlike `justify-content`, the `align-items` property aliases `start` to `flex-start` and `end` to `flex-end`.

### align-content: cross-axis alignment between lines

Takes effect when `flex-wrap: wrap` produces multiple lines. `align-content` accepts:

* `stretch` (default).
* `center`.
* `start` / `flex-start`.
* `end` / `flex-end`.
* `space-between`.
* `space-around`.

## Item properties

These apply to children of a flex container.

### flex-grow and flex-shrink

`flex-grow` is a non-negative number, default `0`. `flex-shrink` is its mirror, default `1`.

When the container has free space on the main axis, items split it in proportion to `flex-grow`. When the container overflows, items shrink in proportion to `flex-shrink`.

```html
<div style='display: flex;'>
   <div style='flex-grow: 1;'>fills available</div>
   <div style='flex-grow: 2;'>fills twice as much</div>
   <div>auto-sized</div>
 </div>
```

### flex-basis

* `auto` (default). Uses `width` or `height`.
* `<length>`. Fixed size before grow/shrink applies.

`flex-basis` defines the initial size of the item before space distribution. It accepts `px`, percentages, `em`, `rem`, and `pt`. Setting a non-`auto` basis completely overrides the item's `width` property.

### flex shorthand

The `flex` property combines `flex-grow`, `flex-shrink`, and `flex-basis`.

* `flex: none` sets grow to `0`, shrink to `0`, and basis to `auto`.
* `flex: <number>` sets grow. Shrink defaults to `1` and basis becomes `0px`.
* `flex: <basis>` sets basis.
* `flex: <grow> <shrink>` sets grow and shrink. Basis becomes `0px`.
* `flex: <grow> <basis>` sets grow and basis. Shrink defaults to `1`.
* `flex: <grow> <shrink> <basis>` sets all three.

Setting `flex: 1` computes to `flex-basis: 0px`. This overrides the default `auto` basis and removes the element's natural width from the distribution calculation.

### align-self: override align-items for one item

`align-self` overrides the container's `align-items` for a specific item. It accepts:

* `auto` (default). Inherits the container's `align-items`.
* `stretch`, `center`, `start` / `flex-start`, `end` / `flex-end`, `baseline`.

```html
<div style='display: flex; align-items: stretch;'>
   <div>stretches</div>
   <div style='align-self: center;'>centred only</div>
 </div>
```

## Recipes

### Sidebar + content

```azul-render screenshot=flex-sidebar width=480 height=200 subtitle="Fixed sidebar with growing content area"
<body style="font-family: sans-serif;">
  <div style="display: flex; height: 180px; gap: 8px;">
    <div style="width: 120px; background: #e0e7ff; padding: 8px;">sidebar</div>
    <div style="flex-grow: 1; background: #f5f3ff; padding: 8px;">content fills the rest</div>
  </div>
</body>
```

### Equal columns

```azul-render screenshot=flex-equal width=480 height=160 subtitle="Three equal columns via flex-grow: 1"
<body style="font-family: sans-serif;">
  <div style="display: flex; gap: 8px; padding: 8px;">
    <div style="flex-grow: 1; background: #fce7f3; padding: 8px;">A</div>
    <div style="flex-grow: 1; background: #fbcfe8; padding: 8px;">B</div>
    <div style="flex-grow: 1; background: #f9a8d4; padding: 8px;">C</div>
  </div>
</body>
```

### Centred content

```azul-render screenshot=flex-center width=400 height=200 subtitle="Both axes centred"
<body style="font-family: sans-serif;">
  <div style="display: flex; justify-content: center; align-items: center; height: 180px; background: #ecfeff;">
    <div style="background: #a5f3fc; padding: 16px;">centred</div>
  </div>
</body>
```

### Wrapping cards

```azul-render screenshot=flex-wrap width=480 height=240 subtitle="Cards wrap onto multiple lines as the container narrows"
<body style="font-family: sans-serif;">
  <div style="display: flex; flex-wrap: wrap; gap: 12px; padding: 8px;">
    <div style="width: 140px; background: #fef3c7; padding: 12px;">card 1</div>
    <div style="width: 140px; background: #fde68a; padding: 12px;">card 2</div>
    <div style="width: 140px; background: #fcd34d; padding: 12px;">card 3</div>
    <div style="width: 140px; background: #fbbf24; padding: 12px;">card 4</div>
  </div>
</body>
```

## Default values at a glance

* `flex-direction` defaults to `row`.
* `flex-wrap` defaults to `nowrap`.
* `justify-content` defaults to `start`.
* `align-items` defaults to `stretch`.
* `align-content` defaults to `stretch`.
* `flex-grow` defaults to `0`.
* `flex-shrink` defaults to `1`.
* `flex-basis` defaults to `auto`.
* `align-self` defaults to `auto`.
* `gap` defaults to `0`.

---

### Doc Guide En Layout Grid (`doc/guide/en/layout/grid.md`)

---
slug: layout/grid
title: Grid
language: en
canonical_slug: layout/grid
audience: external
maturity: mature
guide_order: 84
topic_only: false
short_desc: Two-axis container layout with tracks and areas
prerequisites: [layout]
tracked_files:
  - css/src/props/layout/grid.rs
  - css/src/props/layout/spacing.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:49:28Z
default-search-keys:
  - Dom
  - Css
  - CssProperty
  - StyledDom
---

# Grid

## Overview

`display: grid` lays out children on a two-dimensional grid of explicit columns
and rows. Tracks (`grid-template-columns` / `grid-template-rows`) decide where
the lines go; placement properties (`grid-column`, `grid-row`) decide which
cells each child occupies.

```html
<div style='display: grid;
            grid-template-columns: 200px 1fr 200px;
            grid-template-rows: auto 1fr auto;
            gap: 12px;
            height: 400px;'>
   <header style='grid-column: 1 / 4;'>header</header>
   <nav>nav</nav>
   <main>main</main>
   <aside>aside</aside>
   <footer style='grid-column: 1 / 4;'>footer</footer>
 </div>
```

## Track sizing

`grid-template-columns` and `grid-template-rows` accept a space-separated
list of track sizes:

- `<length>` (`100px`, `2em`, `30%`). Fixed size.
- `Nfr` (`1fr`, `0.5fr`, `2.5fr`). Fraction of remaining space.
  Fractional values are supported.
- `auto`. Sizes from content.
- `min-content`. Smallest size that doesn't overflow content.
- `max-content`. Sized to fit unwrapped content.
- `minmax(<min>, <max>)`. Clamps the track to a range.
- `fit-content(<length>)`. Equivalent to
  `min(max-content, max(min-content, <length>))`.

```html
<div style='display: grid;
            grid-template-columns: 100px 1fr minmax(200px, 2fr) auto;'>
   ...
 </div>
```

### repeat()

`repeat(N, <track-list>)` expands inline. The track list may contain
multiple tracks, which all repeat together:

- `repeat(3, 1fr)` expands to `1fr 1fr 1fr`.
- `repeat(2, 100px 1fr)` expands to `100px 1fr 100px 1fr`.
- `100px repeat(2, 1fr) auto` expands to `100px 1fr 1fr auto`.

The repeat count is capped at 10,000 to prevent runaway expansion.

## grid-template-areas

Names regions of the grid using quoted row strings. Each cell is a token; `.`
means "no area".

```html
<div style='display: grid;
            grid-template-columns: 200px 1fr 200px;
            grid-template-rows: 80px 1fr 60px;
            grid-template-areas:
              "header header header"
              "nav    main   aside"
              "footer footer footer";
            gap: 8px;'>
   <header style='grid-area: header;'>...</header>
   <nav    style='grid-area: nav;'>...</nav>
   <main   style='grid-area: main;'>...</main>
   <aside  style='grid-area: aside;'>...</aside>
   <footer style='grid-area: footer;'>...</footer>
 </div>
```

Every row string must have the same number of cells, and a named area
must form a rectangle.

## Item placement: grid-column / grid-row

`grid-column` and `grid-row` take `<start> / <end>`:

- `auto`. Auto-place.
- `2`. Start at line 2, span 1 cell.
- `2 / 4`. From line 2 up to (not including) line 4.
- `2 / span 3`. Start at line 2, span 3 tracks.
- `span 2`. Span 2 tracks, position auto.
- `header / footer`. Named lines.
- `-1`. Last line, counts from the end.

```html
<div style='grid-column: 1 / -1;'>full-width header</div>
<div style='grid-row: 2; grid-column: 2 / span 2;'>main content</div>
```

`grid-area: <name>` is the shorthand when you've defined `grid-template-areas`.

## Auto-placement

Children without explicit placement flow into the next free cell.
`grid-auto-flow` controls the direction:

- `row` (default). Fill rows left-to-right.
- `column`. Fill columns top-to-bottom.
- `row dense`. Backfill earlier holes.
- `column dense`. Backfill earlier holes, column-first.

`grid-auto-rows` and `grid-auto-columns` size implicitly-created tracks.
For example, if you've defined three columns but place a child on column
5, columns 4 and 5 are sized by `grid-auto-columns`.

## gap, row-gap, column-gap

`gap: <row> <column>`. The longhands take a single `<length>`. Same as
flexbox.

```html
<div style='display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px 8px;'>
   ...
 </div>
```

## Alignment

Grid uses two pairs of axes (rows by columns) and reuses the flex names:

- **Row (cross) axis.** Container uses `align-items`. Items override with `align-self`.
- **Column (main) axis.** Container uses `justify-items`. Items override with `justify-self`.
- **Line spacing within tracks (cross).** Container uses `align-content`. No item override.
- **Line spacing within tracks (main).** Container uses `justify-content`. No item override.

`justify-items` accepts `stretch` (default), `start`, `end`, `center`.
`justify-self` accepts `auto` (default, inherits `justify-items`),
`stretch`, `start`, `end`, `center`.

## Recipes

### Holy grail layout

```azul-render screenshot=grid-holy-grail width=480 height=320 subtitle="Header / nav / main / aside / footer with grid-template-areas"
<body style="font-family: sans-serif;">
  <div style="display: grid;
              grid-template-columns: 100px 1fr 100px;
              grid-template-rows: 60px 1fr 50px;
              grid-template-areas: 'h h h' 'n m a' 'f f f';
              gap: 4px;
              height: 300px; padding: 8px;">
    <div style="grid-area: h; background: #818cf8; padding: 8px;">header</div>
    <div style="grid-area: n; background: #c4b5fd; padding: 8px;">nav</div>
    <div style="grid-area: m; background: #ddd6fe; padding: 8px;">main</div>
    <div style="grid-area: a; background: #c4b5fd; padding: 8px;">aside</div>
    <div style="grid-area: f; background: #818cf8; padding: 8px;">footer</div>
  </div>
</body>
```

### Card grid

```azul-render screenshot=grid-cards width=480 height=240 subtitle="Auto-flowing cards in a 3-column grid"
<body style="font-family: sans-serif;">
  <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; padding: 8px;">
    <div style="background: #fef3c7; padding: 16px;">card 1</div>
    <div style="background: #fde68a; padding: 16px;">card 2</div>
    <div style="background: #fcd34d; padding: 16px;">card 3</div>
    <div style="background: #fef3c7; padding: 16px;">card 4</div>
    <div style="background: #fde68a; padding: 16px;">card 5</div>
    <div style="background: #fcd34d; padding: 16px;">card 6</div>
  </div>
</body>
```

### Responsive minmax

```azul-render screenshot=grid-minmax width=480 height=200 subtitle="Tracks that clamp to a range with minmax()"
<body style="font-family: sans-serif;">
  <div style="display: grid; grid-template-columns: minmax(120px, 1fr) 2fr minmax(80px, 200px); gap: 8px; padding: 8px;">
    <div style="background: #a7f3d0; padding: 12px;">120 → 1fr</div>
    <div style="background: #6ee7b7; padding: 12px;">2fr</div>
    <div style="background: #34d399; padding: 12px;">80 → 200</div>
  </div>
</body>
```

## Default values at a glance

- `grid-template-columns` defaults to `none`.
- `grid-template-rows` defaults to `none`.
- `grid-template-areas` defaults to `none`.
- `grid-auto-flow` defaults to `row`.
- `grid-auto-columns` defaults to `auto`.
- `grid-auto-rows` defaults to `auto`.
- `grid-column` and `grid-row` default to `auto / auto`.
- `justify-items` defaults to `stretch`.
- `justify-self` defaults to `auto`.
- `align-items` defaults to `stretch`.
- `align-self` defaults to `auto`.
- `gap` defaults to `0`.

---

### Doc Guide En Layout Inline (`doc/guide/en/layout/inline.md`)

---
slug: layout/inline
title: Inline Layout
language: en
canonical_slug: layout/inline
audience: external
maturity: mature
guide_order: 82
topic_only: false
short_desc: Text flow, word breaks, writing modes, multi-column
prerequisites: [layout/blocks]
tracked_files:
  - css/src/props/layout/text.rs
  - css/src/props/layout/wrapping.rs
  - css/src/props/layout/fragmentation.rs
  - css/src/props/layout/column.rs
  - css/src/props/layout/display.rs
  - css/src/props/basic/font.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:49:28Z
default-search-keys:
  - Dom
  - Css
  - CssProperty
  - StyledDom
---

# Inline Layout

## Overview

Inline content flows from left to right (or right to left, in RTL scripts)
inside a block. The browser-style line box wraps when it runs out of room
and pushes the next chunk down. Block elements stack vertically; inline
elements stack horizontally and wrap.

`display: inline` participates in line layout. It gets no explicit width
or height, paints between text runs, and breaks across lines if it has to.
`display: inline-block` *does* accept `width` and `height` (and so behaves
like a small block), but otherwise still rides along inline content.

```html
<p>
  Inline:
  <span style='display: inline; background: #fef3c7;'>span</span> wraps with text.
  Inline-block:
  <span style='display: inline-block; width: 80px; background: #c4b5fd;'>fixed-width</span>
  is sized like a block but flows inline.
</p>
```

## Choosing between inline and inline-block

- `inline`. Doesn't accept `width`/`height`. Breaks across lines. Use for
  text emphasis, anchors, icons inside prose.
- `inline-block`. Accepts `width`/`height`. Doesn't break across lines.
  Use for tags, chips, fixed-size inline buttons.

`inline-flex` and `inline-grid` are flex and grid containers that
participate inline. Their content lays out as flex or grid, but the box
itself rides the surrounding line.

## How text becomes lines

You don't drive text layout directly. Set CSS properties on the container.
The engine shapes glyphs, picks line breaks, reorders bidirectional runs,
and positions output at layout time.

## Wrapping: white-space, word-break, overflow-wrap

`white-space` controls whitespace collapsing and wrapping:

- `normal` (default). Collapses whitespace. Wraps.
- `pre`. Preserves whitespace. No wrapping except at explicit breaks.
- `nowrap`. Collapses whitespace. No wrapping.
- `pre-wrap`. Preserves whitespace. Wraps.
- `pre-line`. Collapses whitespace but preserves newlines. Wraps.
- `break-spaces`. Preserves whitespace. Breaks at every space.

`word-break` decides what to do with sequences that have no soft breaks
(URLs, code, CJK):

- `normal` (default).
- `break-all`. Allows break between any two characters.
- `keep-all`. Forbids break inside CJK.
- `break-word`. Deprecated alias.

`overflow-wrap` accepts `normal` (only at allowed break points),
`anywhere` (break anywhere to prevent overflow), and `break-word`.

`hyphens: none | manual | auto` enables soft-hyphen breaking. `auto`
requires the layout crate's hyphenation resources for the active
language.

```css
.code { white-space: pre; tab-size: 4em; }
.tag  { white-space: nowrap; }
.body { hyphens: auto; word-break: normal; overflow-wrap: anywhere; }
```

## Spacing and metrics

The properties that modulate inline flow:

- `line-height`. `<percentage>` or `<length>`. Default `120%`.
  Baseline-to-baseline distance.
- `letter-spacing`. `<length>`. Default `0px`. Added between every glyph.
- `word-spacing`. `<length>`. Default `0px`. Added between words.
- `tab-size`. `<length>`. Default `8em`. Width of a tab character.
- `vertical-align`. Default `baseline`. How inline-blocks line up against
  the line box.

```css
.body { line-height: 150%; letter-spacing: 0.02em; }
pre   { tab-size: 4em; }
```

`vertical-align` accepts `baseline`, `top`, `middle`, `bottom`, `sub`,
`super`, `text-top`, `text-bottom`, a `<percentage>`, or a `<length>`.
The line box is sized by the tallest inline-block on the line, so mixing
`vertical-align: top` and `vertical-align: middle` produces the same line
height. Only the glyph placement within that line changes.

## Direction and writing modes

For bidirectional text, set `direction` on the block container:

- `ltr` (default). Left-to-right.
- `rtl`. Right-to-left.

`unicode-bidi` sets the bidi-override embedding for an inline element.
Most apps don't touch it. The bidi pass handles mixed text correctly
without help.

`writing-mode` swaps the block and inline axes. Inline text then flows
down (or up) instead of across:

- `horizontal-tb` (default). Latin scripts.
- `vertical-rl`. CJK vertical.
- `vertical-lr`. Mongolian.

Properties that follow the inline axis (`padding-inline-start`,
`padding-inline-end`, `margin-inline-*`, `gap`'s row/column meaning)
re-orient when the writing mode is vertical.

```html
<div style='writing-mode: vertical-rl; height: 200px;'>
  Vertical CJK-style flow, top-to-bottom right-to-left.
</div>
```

## Multi-column

`column-count`, `column-width`, and the `column-rule-*` longhands flow
block content into multiple columns:

```css
article { column-count: 2; column-gap: 24px; column-rule: 1px solid #ccc; }
```

- `column-count`. Integer or `auto`.
- `column-width`. `<length>` or `auto`.
- `column-gap`. `<length>`.
- `column-rule`. Shorthand for `width style color`.
- `column-span`. `none` or `all` (span every column).
- `column-fill`. `balance` (default, even split) or `auto` (fill columns
  one by one).

## Pagination and fragmentation

For print and paginated views, fragmentation properties decide where
breaks may occur:

- `page-break-before` / `page-break-after`. `auto`, `always`, `avoid`,
  `left`, `right`.
- `break-inside`. `auto`, `avoid`, `avoid-page`, `avoid-column`.
- `widows`, `orphans`. Minimum lines kept together.
- `box-decoration-break`. `slice` or `clone`.

These have no visible effect inside a window. They fire when the layout
is rendered to PDF.

## Floats and clear

`clear: left | right | both | none` interacts only with floats. It's
honoured on block elements next to `float` siblings. Floats are rare in
modern UI code. Flexbox and grid handle the same problems with fewer
surprises.

## Recipes

### Wrapping prose

```azul-render screenshot=inline-prose width=480 height=200 subtitle="Default inline flow with hyphens: auto"
<body style="font-family: serif; padding: 16px;">
  <p style="line-height: 150%; hyphens: auto; text-align: justify;">
    Inline text wraps, justifies, and hyphenates across the full
    paragraph rather than line by line.
  </p>
</body>
```

### Inline tags / chips

```azul-render screenshot=inline-tags width=480 height=140 subtitle="display: inline-block tags inside flowing text"
<body style="font-family: sans-serif; padding: 16px;">
  <p style="line-height: 1.8;">
    A paragraph with
    <span style="display: inline-block; padding: 2px 8px; background: #fef3c7; border-radius: 12px;">tag</span>
    and another
    <span style="display: inline-block; padding: 2px 8px; background: #ddd6fe; border-radius: 12px;">chip</span>
    placed inline with the prose.
  </p>
</body>
```

### Two-column article

```azul-render screenshot=inline-multicol width=520 height=200 subtitle="column-count: 2 with column-gap and column-rule"
<body style="font-family: serif; padding: 16px;">
  <article style="column-count: 2; column-gap: 24px; column-rule: 1px solid #ccc;">
    Long-form prose that flows naturally across two balanced columns.
    column-fill: balance is the default. Content splits evenly between
    columns instead of filling the first one before moving on.
  </article>
</body>
```

## Default values at a glance

- `display` defaults to `block`. Override to `inline` or `inline-block` per element.
- `white-space` defaults to `normal`.
- `word-break` defaults to `normal`.
- `overflow-wrap` defaults to `normal`.
- `hyphens` defaults to `manual`.
- `direction` defaults to `ltr`.
- `writing-mode` defaults to `horizontal-tb`.
- `line-height` defaults to `120%`.
- `letter-spacing` defaults to `0px`.
- `word-spacing` defaults to `0px`.
- `tab-size` defaults to `8em`.
- `vertical-align` defaults to `baseline`.
- `column-count` defaults to `auto`.
- `column-fill` defaults to `balance`.
- `column-rule` defaults to `medium none currentColor`.
- `widows` and `orphans` default to `2`.

---

### Doc Guide En Styling Icon Packs (`doc/guide/en/styling/icon-packs.md`)

---
slug: styling/icon-packs
title: Icon Packs
language: en
canonical_slug: styling/icon-packs
audience: external
maturity: wip
guide_order: 74
topic_only: false
short_desc: Register icons and use them with `Dom::create_icon` or `<icon>`
prerequisites: [styling]
tracked_files:
  - core/src/icon.rs
  - layout/src/icon.rs
  - core/src/dom.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - IconProviderHandle
  - IconResolverCallbackType
  - IconStyleOptions
  - AppConfig
  - SystemStyle
  - Dom
  - StyledDom
---

# Icon Packs

## Introduction

*WIP.* Image and font icons resolve through the default resolver on all platforms; SVG and animated icons run through the same callback path but are not yet covered by built-in helpers.

An icon pack is a named bag of icons that the framework looks up by name
when it sees a `Dom::create_icon("home")` node (or an `<icon>` element).
Registration happens once on `AppConfig.icon_provider`; the lookup runs
before every layout pass and resolves the name to a `StyledDom` subtree
(typically an `<img>` or a glyph in an icon font).

```rust,ignore
use azul::prelude::*;
let mut config = AppConfig::create(/* ... */);

// 1. Register a font-icon pack pointing at Material Icons.
let material = /* a FontRef built once at startup */;
config.icon_provider.register_font_icon(
    "material".into(),
    "home".into(),
    material.clone(),
    "\u{e88a}".into(),
);
config.icon_provider.register_font_icon(
    "material".into(),
    "settings".into(),
    material,
    "\u{e8b8}".into(),
);

// 2. Register an image-icon pack for app-specific icons.
config.icon_provider.register_image_icon(
    "app".into(),
    "logo".into(),
    image_ref,
);

// 3. Use it in a Dom.
let dom = Dom::create_div().with_children(vec![
    Dom::create_icon("home".into()),
    Dom::create_icon("logo".into()),
].into());
```

## How lookup works

Icons are stored on `IconProviderHandle` as a nested map of pack to
icon-name to data. Lookup walks the packs in registration order and takes
the first match. Pack names mostly exist for namespacing and bulk
unregistration, not for selection.

Methods on `IconProviderHandle`:

- `register_icon(pack, name, data)`. Adds or overwrites an icon with arbitrary data.
- `register_font_icon(pack, name, font, char)`. Adds a font-glyph icon.
- `register_image_icon(pack, name, image)`. Adds an image icon.
- `unregister_icon(pack, name)`. Removes a single icon.
- `unregister_pack(pack)`. Removes every icon in a pack.
- `set_resolver(callback)`. Replaces the resolver for the whole provider.

Icon names are case-insensitive: registering `"Home"` and looking up
`"home"` resolve to the same entry.

## The resolver callback

The resolver turns a registered icon plus the original `<icon>` node into
a `StyledDom`. The signature is `IconResolverCallbackType`:

```rust,ignore
extern "C" fn(
    icon_data: OptionRefAny,         // the data you registered, or None
    original_icon_dom: &StyledDom,   // the <icon> node with its inline styles
    system_style: &SystemStyle,      // current theme, accent, accessibility flags
) -> StyledDom;
```

The default resolver handles font icons (registered via
`register_font_icon`) and image icons (via `register_image_icon`) out of
the box. For anything else (SVG, animated, vector) write your own
resolver and pass it to `IconProviderHandle::with_resolver(my_callback)`
or `IconProviderHandle::set_resolver(my_callback)`. The callback sees
`SystemStyle`, so you can produce a different DOM for dark mode, a
high-contrast variant, or a reduced-motion fallback.

## System-style integration

The default resolver copies a curated subset of CSS properties from the
original `<icon>` node onto the resolved DOM and filters based on
`SystemStyle`. The `IconStyleOptions` type controls per-icon behaviour
through three fields:

- `inherit_text_color` makes the icon adopt the cascaded `color`.
- `prefer_grayscale` drops explicit colour and adds a grayscale filter to
  image icons.
- `tint_color` overrides the icon's fill with a single colour.

The cascade still runs as normal. A `with_css("color: red;")` on the
`<icon>` node beats the system style.

## Naming conventions

A pack is identified by its name string. The framework reserves no names,
but the convention is:

- `app`: your application's first-party icons.
- `material`, `phosphor`, `lucide`, ...: third-party icon fonts.
- `os`: anything you load from a platform icon theme (Windows shell
  imageres.dll, macOS NSImage, GNOME `icon-theme.cache`).

When two packs ship the same icon name, the *first registered* wins.
Register your `app` pack last if you want app icons to override
third-party ones.

## Recipes

### Toolbar with mixed packs

```azul-render screenshot=icons-toolbar width=400 height=120 subtitle="A toolbar mixing app and material icons"
<body style="font-family: sans-serif; padding: 16px;">
  <div style="display: flex; gap: 12px; padding: 8px; background: #f3f4f6;">
    <span style="display: inline-block; width: 24px; height: 24px; background: #1976d2;"></span>
    <span style="display: inline-block; width: 24px; height: 24px; background: #1976d2;"></span>
    <span style="display: inline-block; width: 24px; height: 24px; background: #1976d2;"></span>
  </div>
</body>
```

### Themed icon button

```rust,no_run
use azul::prelude::*;
let _ = Dom::create_button("Settings", SmallAriaInfo::label("Settings"))
    .with_children(vec![Dom::create_icon("settings".into())].into())
    .with_css("
        display: inline-flex; gap: 6px; padding: 6px 12px;
        background: system:button-face;
        color: system:button-text;
        @theme dark { color: system:accent; }
    ");
```

The `@theme dark` rule changes the cascaded `color`, which the default
font-icon resolver picks up when `inherit_text_color` is set.

## Disabling and overriding

A few escape hatches:

- `IconProviderHandle::set_resolver(custom)` swaps the whole resolver
  for one provider.
- `IconProviderHandle::unregister_pack("material".into())` removes every
  icon in a pack. Useful for "skin packs" you load and unload at runtime.

End-user ricing of icons (replacing `material/home` with a user-chosen
SVG without recompiling) is on the road map alongside the existing
`AZ_RICING` CSS hook described in
[System Themes](themes.md#how-user-theming-layers-with-component-css).

---

### Doc Guide En Styling Properties (`doc/guide/en/styling/properties.md`)

---
slug: styling/properties
title: CSS Properties
language: en
canonical_slug: styling/properties
audience: external
maturity: mature
guide_order: 71
topic_only: false
short_desc: Reference of every CSS property azul recognises
prerequisites: [styling]
tracked_files:
  - css/src/props/basic/angle.rs
  - css/src/props/basic/color.rs
  - css/src/props/basic/direction.rs
  - css/src/props/basic/font.rs
  - css/src/props/basic/length.rs
  - css/src/props/basic/pixel.rs
  - css/src/props/basic/time.rs
  - css/src/props/style/azul_exclusion.rs
  - css/src/props/style/background.rs
  - css/src/props/style/border.rs
  - css/src/props/style/border_radius.rs
  - css/src/props/style/box_shadow.rs
  - css/src/props/style/content.rs
  - css/src/props/style/effects.rs
  - css/src/props/style/filter.rs
  - css/src/props/style/lists.rs
  - css/src/props/style/scrollbar.rs
  - css/src/props/style/selection.rs
  - css/src/props/style/text.rs
  - css/src/props/style/transform.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - Css
  - CssProperty
  - StyleBoxShadow
  - StyleFilterVec
  - StyleCursor
  - StyleMixBlendMode
  - BorderStyle
  - ColorU
  - PixelValue
  - PercentageValue
  - AngleValue
  - CssDuration
  - FloatValue
---

# CSS Properties

## Overview

The properties azul recognises, grouped by what they style. Each entry lists
the syntax and the Rust type the parser produces.

## Value primitives

- Length (px/em/rem/...) accepts `12px`, `1.5em`, or `100%`. Type `PixelValue`.
- Length (no percent) accepts `4px` or `2em`. Type `PixelValueNoPercent`.
- Percentage accepts `50%`. Type `PercentageValue`.
- Angle accepts `90deg`, `0.5turn`, `1.57rad`, or `100grad`. Type `AngleValue`.
- Time accepts `200ms`, `0.3s`, or `5t`. Type `CssDuration`. `t` is a **tick**
  (one frame at the nominal 60Hz rate): it expresses a duration without assuming
  a real clock, so `caret-animation-duration: 5t` flips on exactly the 5th frame
  on any machine. `t` rather than `fr`, because `fr` is CSS grid's flex unit.
- Color accepts `#fff`, `#1a2b3c`, `rgb(...)`, `rgba(...)`, `hsl(...)`, `hsla(...)`, named colors, and `currentcolor`. Type `ColorU`.
- System color accepts `system:accent`, `system:text`, `system:background`, and so on. Type `ColorOrSystem`.
- System font accepts `system:ui`, `system:monospace`, `system:title`, and so on. Type `StyleFontFamily::SystemType`.
- Float accepts `1.5` or `0`. Type `FloatValue`.

Length units: `px`, `em`, `rem`, `pt`, `pc`, `cm`, `mm`, `in`, `vw`, `vh`,
`vmin`, `vmax`, `%`, `ex`, `ch`. Angle units: `deg`, `rad`, `grad`,
`turn`, `%`.

Bare `0` is accepted for any length without a unit. Bare numbers without
a unit are interpreted as degrees for angle properties.

## CSS-wide keywords

Every property accepts `auto`, `none`, `initial`, `inherit`, `unset`, and
`revert` in addition to its typed value (`CssPropertyValue<T>`). Some
properties only accept a subset (`border-style` ignores `auto`,
`font-family` rejects `initial`, etc.) but the parser is permissive and
the cascade handles the rest.

## Box model

- `width`, `height`, `min-width`, `min-height`, `max-width`, `max-height` accept a length, `auto`, `min-content`, `max-content`, or `fit-content`. Types `LayoutWidth`, `LayoutHeight`, and so on.
- `padding`, `padding-top`, `padding-right`, `padding-bottom`, `padding-left` accept a length. Type `LayoutPadding*`.
- `margin`, `margin-top`, `margin-right`, `margin-bottom`, `margin-left` accept a length or `auto`. Type `LayoutMargin*`.
- `box-sizing` accepts `content-box` or `border-box`. Type `LayoutBoxSizing`.
- `display` accepts `block`, `inline`, `inline-block`, `flex`, `inline-flex`, `grid`, `inline-grid`, `none`, or `inherit`. Type `LayoutDisplay`.
- `position` accepts `static`, `relative`, `absolute`, `fixed`, or `sticky`. Type `LayoutPosition`.
- `top`, `right`, `bottom`, `left` accept a length or `auto`. Types `LayoutTop` and so on.
- `overflow`, `overflow-x`, `overflow-y` accept `visible`, `hidden`, `scroll`, `auto`, or `clip`. Type `LayoutOverflow`.
- `z-index` accepts an integer. Type `LayoutZIndex`.

`width: auto` and `height: auto` defer to the layout algorithm.
`min-content`, `max-content`, and `fit-content` derive from intrinsic
sizing.

## Background

`background` is a shorthand for `background-color` and `background-image`.
Multiple comma-separated layers stack from front to back.

```css
background: linear-gradient(to right, #1976d2, #42a5f5);
background: radial-gradient(circle at 30% 30%, #fff, #999);
background: conic-gradient(from 0deg, red, yellow, green, blue, red);
background: url('chrome.png'), #f0f0f0;
```

- `background` (shorthand) accepts a color, gradient, image, or multiple layers. Type `StyleBackgroundContentVec`.
- `background-color` accepts a color. Type `ColorU`.
- `background-image` accepts `linear-gradient(...)`, `radial-gradient(...)`, `conic-gradient(...)`, `url(...)`, or `none`. Type `StyleBackgroundContent`.
- `background-position` accepts a length pair or `top`/`bottom`/`left`/`right`/`center`. Type `StyleBackgroundPosition`.
- `background-size` accepts a length pair, `cover`, `contain`, or `auto`. Type `StyleBackgroundSize`.
- `background-repeat` accepts `repeat`, `repeat-x`, `repeat-y`, `no-repeat`, `round`, or `space`. Type `StyleBackgroundRepeat`.

Gradient direction accepts angles (`90deg`), `to <side>` syntax
(`to top right`), or corner directions.

```azul-render screenshot=styling-gradients width=480 height=200 subtitle="Linear, radial, and conic backgrounds"
<html>
<head><style>
body { font-family: sans-serif; padding: 16px; background: white; display: flex; gap: 12px; }
.cell { width: 140px; height: 140px; border-radius: 8px; }
.lin { background: linear-gradient(135deg, #1976d2, #42a5f5); }
.rad { background: radial-gradient(circle at 30% 30%, #fff, #777); }
.con { background: conic-gradient(from 0deg, red, yellow, green, blue, red); }
</style></head>
<body><div class="cell lin"></div><div class="cell rad"></div><div class="cell con"></div></body>
</html>
```

## Border

- `border` (shorthand) accepts width, style, and color. Sets all four sides.
- `border-width`, `border-{top,right,bottom,left}-width` accept `thin`, `medium`, `thick`, or a length. Type `StyleBorder*Width`.
- `border-style`, `border-{top,right,bottom,left}-style` accept `none`, `solid`, `double`, `dotted`, `dashed`, `hidden`, `groove`, `ridge`, `inset`, or `outset`. Type `BorderStyle`.
- `border-color`, `border-{top,right,bottom,left}-color` accept a color. Type `StyleBorder*Color`.
- `border-radius`, `border-{top-left,top-right,bottom-left,bottom-right}-radius` accept a length. Type `StyleBorder*Radius`.

`border` shorthand expands to all four sides. `border-top` etc. expand to
the three properties of one side.

## Box shadow and filter

- `box-shadow` accepts `<x> <y> [blur] [spread] [color] [inset]`. Type `StyleBoxShadow`.
- `text-shadow` accepts `<x> <y> [blur] [color]`. Type `StyleBoxShadow`.
- `filter` accepts `blur(<len>)`, `brightness(<%>)`, `contrast(<%>)`, `grayscale(<%>)`, `hue-rotate(<angle>)`, `invert(<%>)`, `opacity(<%>)`, `saturate(<%>)`, `sepia(<%>)`, and `drop-shadow(...)`. Type `StyleFilterVec`.
- `backdrop-filter` accepts the same values as `filter`. Type `StyleFilterVec`.
- `mix-blend-mode` accepts `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, `exclusion`, `hue`, `saturation`, `color`, or `luminosity`. Type `StyleMixBlendMode`.

`box-shadow` accepts multiple comma-separated shadows. The first one is on
top.

## Text

```css
font-family: 'Inter', sans-serif;
font-size: 16px;
font-weight: 600;
line-height: 1.5;
color: #222;
text-align: justify;
letter-spacing: 0.02em;
text-decoration: underline;
```

- `color` accepts a color. Type `StyleTextColor`.
- `font-family` accepts a comma-separated list of names, a generic family (`sans-serif`, `serif`, `monospace`, `cursive`, `fantasy`), or `system:ui`. Type `StyleFontFamilyVec`.
- `font-size` accepts a length or percentage. Type `StyleFontSize`.
- `font-weight` accepts `100`-`900`, `normal`, `bold`, `bolder`, or `lighter`. Type `StyleFontWeight`.
- `font-style` accepts `normal`, `italic`, or `oblique`. Type `StyleFontStyle`.
- `line-height` accepts a unitless number, length, or percentage. Type `StyleLineHeight`.
- `letter-spacing` accepts a length. Type `StyleLetterSpacing`.
- `word-spacing` accepts a length. Type `StyleWordSpacing`.
- `tab-size` accepts an integer or length. Type `StyleTabSize`.
- `text-align` accepts `left`, `right`, `center`, `justify`, `start`, or `end`. Type `StyleTextAlign`.
- `text-decoration` accepts `none`, `underline`, `overline`, `line-through`, or combinations. Type `StyleTextDecoration`.
- `text-transform` accepts `none`, `uppercase`, `lowercase`, or `capitalize`. Parses, type not exposed.
- `text-orientation` accepts `mixed`, `upright`, or `sideways`. Type `StyleTextOrientation`.
- `direction` accepts `ltr` or `rtl`. Type `StyleDirection`.
- `white-space` accepts `normal`, `nowrap`, `pre`, `pre-wrap`, or `pre-line`. Type `StyleWhiteSpace`.
- `word-break` accepts `normal`, `break-all`, or `keep-all`. Type `StyleWordBreak`.
- `overflow-wrap` accepts `normal`, `break-word`, or `anywhere`. Type `StyleOverflowWrap`.
- `hyphens` accepts `none`, `manual`, or `auto`. Type `StyleHyphens`.
- `hyphenation-language` (Azul) accepts a BCP 47 tag. Type `StyleHyphenationLanguage`.

System font keywords resolve to platform defaults (`SF Pro`, `Segoe UI`,
`Cantarell`, ...) through `SystemFontType`. See
[System Themes](themes.md).

## Transforms and 3D

- `transform` accepts `translate(<len>, <len>)`, `translateX/Y/Z`, `scale(<n>, <n>)`, `scaleX/Y/Z`, `rotate(<angle>)`, `rotateX/Y/Z`, `skew(<angle>, <angle>)`, `matrix(...)`, `matrix3d(...)`, and `perspective(<len>)`. Type `StyleTransformVec`.
- `transform-origin` accepts a length pair. Type `StyleTransformOrigin`. Default `50% 50%`.
- `transform-style` accepts `flat` or `preserve-3d`. Parses, type not exposed.
- `perspective` accepts a length or `none`. Parses, type not exposed.
- `perspective-origin` accepts a length pair. Type `StylePerspectiveOrigin`.
- `backface-visibility` accepts `visible` or `hidden`. Type `StyleBackfaceVisibility`.

Transforms compose as in CSS: the functions are multiplied left to right,
so they apply to the element right to left. `transform: translate(10px, 0)
rotate(45deg) scale(1.2)` first scales, then rotates, then translates — and
the whole chain pivots about `transform-origin` (`50% 50%`, the element's
centre, by default; percentages resolve against the element's own box).
The common 3D idiom `perspective(800px) rotateX(30deg)` therefore works as
in a browser: the rotation happens first and the perspective projects the
tilted plane.

## Flexbox and grid

The flexbox and grid syntax is standard CSS. The full reference will live
in [Layout](../layout.md). Quick lookup:

- `flex` (shorthand) accepts `<grow> <shrink> <basis>`. Shorthand for the four flex properties.
- `flex-direction` accepts `row`, `row-reverse`, `column`, or `column-reverse`. Type `LayoutFlexDirection`.
- `flex-wrap` accepts `nowrap`, `wrap`, or `wrap-reverse`. Type `LayoutFlexWrap`.
- `flex-grow`, `flex-shrink` accept a float. Types `LayoutFlexGrow` and `LayoutFlexShrink`.
- `flex-basis` accepts a length, `auto`, or `content`. Type `LayoutFlexBasis`.
- `align-items`, `align-self`, `align-content` accept `start`, `end`, `center`, `stretch`, `baseline`, `flex-start`, `flex-end`, `space-between`, `space-around`, or `space-evenly`. Types `LayoutAlignItems`, `LayoutAlignSelf`, `LayoutAlignContent`.
- `justify-content`, `justify-items`, `justify-self` accept the same values as align. Types `LayoutJustifyContent`, `LayoutJustifyItems`, `LayoutJustifySelf`.
- `gap`, `row-gap`, `column-gap` accept a length pair. Type `LayoutGap`.
- `grid-template-rows`, `grid-template-columns` accept a track list with `fr`, `auto`, or length. Grid track types.
- `grid-row`, `grid-column` accept `<start> / <end>`. Grid placement types.
- `grid-area` accepts a shorthand or named area. Grid placement types.

`fr` units use a 100x internal multiplier. See the layout guide for
intrinsic-sizing rules.

## Effects

- `opacity` accepts `0.0`-`1.0` or a percentage. Type `StyleOpacity`.
- `visibility` accepts `visible`, `hidden`, or `collapse`. Type `StyleVisibility`.
- `cursor` accepts `default`, `pointer`, `text`, `move`, `crosshair`, `wait`, `help`, `grab`, `grabbing`, `not-allowed`, `progress`, `cell`, `col-resize`, `row-resize`, `e-resize`, `n-resize`, `s-resize`, `w-resize`, `se-resize`, and so on. Type `StyleCursor`.
- `object-fit` accepts `fill`, `contain`, `cover`, `none`, or `scale-down`. Type `StyleObjectFit`.
- `object-position` accepts a length pair. Type `StyleObjectPosition`.
- `aspect-ratio` accepts `<n>` or `<n> / <m>`. Type `StyleAspectRatio`.
- `pointer-events` accepts `auto` or `none`. Parses, type not exposed.

`StyleCursor` covers every common system cursor on each platform.

## Lists

- `list-style-type` accepts `none`, `disc`, `circle`, `square`, `decimal`, `decimal-leading-zero`, `lower-roman`, `upper-roman`, `lower-alpha`, `upper-alpha`, `lower-greek`, or `upper-greek`. Type `StyleListStyleType`.
- `list-style-position` accepts `inside` or `outside`. Type `StyleListStylePosition`.

## Scrolling

- `scroll-behavior` accepts `auto` or `smooth`. Parses, type not exposed.
- `overscroll-behavior`, `overscroll-behavior-x`, `overscroll-behavior-y` accept `auto`, `contain`, or `none`. Parses, type not exposed.
- `scrollbar-width` accepts `auto`, `thin`, `none`, or a length. Type `LayoutScrollbarWidth`.
- `scrollbar-color` accepts `<thumb> <track>`. Type `StyleScrollbarColor`.
- `scrollbar-track-color` accepts a color. Parses, type not exposed.
- `scrollbar-thumb-color` accepts a color. Parses, type not exposed.

## Selection appearance (Azul)

- `selection-background-color` accepts a color. Type `SelectionBackgroundColor`.
- `selection-color` accepts a color. Type `SelectionColor`.
- `selection-radius` accepts a length. Type `SelectionRadius`.

These accept the OS-native default automatically (see
[System Themes](themes.md)).

## Generated content

- `content` accepts a string, `none`, `normal`, or a keyword. Type `Content`.
- `counter-reset` accepts a name list. Type `CounterReset`.
- `counter-increment` accepts a name list. Type `CounterIncrement`.
- `string-set` accepts a name plus content. Type `StringSet`.

The runtime support for `::before` and `::after` generated content is
in-progress. The parser accepts the syntax today but the layout engine
doesn't yet emit nodes from it.

## Animation and transitions

The animation runtime isn't yet wired up. See the
[Animations](../animations.md) page. These properties parse and
round-trip but their typed views are still internal:

- `transition` shorthand: property duration timing-function delay.
- `transition-property`: property name list, `all`, `none`.
- `transition-duration`, `transition-delay`: time values.
- `transition-timing-function`: `ease`, `linear`, `ease-in`, `ease-out`,
  `ease-in-out`, `cubic-bezier(...)`.
- `animation` shorthand: name, duration, timing-function, delay,
  iteration-count, direction, fill-mode, play-state.
- The individual `animation-*` longhands: each accepts the matching value
  list.

## Azul-specific properties

- `exclusion-margin`, `exclusion-margin-top`, `exclusion-margin-bottom`, `exclusion-margin-left`, `exclusion-margin-right` accept a length. Type `StyleExclusionMargin`.
- `hyphenation-language` accepts a BCP 47 tag. Type `StyleHyphenationLanguage`.
- `shape-inside`, `shape-outside`, `clip-path` accept `inset(...)`, `circle(...)`, `ellipse(...)`, `polygon(...)`, `path(...)`, `url(...)`, or `none`. Parses, type not exposed.

`exclusion-margin` is azul's CSS Exclusions mode. It lets text flow around
floated or positioned elements with a configurable inset.

---

### Doc Guide En Styling Text And Fonts (`doc/guide/en/styling/text-and-fonts.md`)

---
slug: styling/text-and-fonts
title: Styling Text
language: en
canonical_slug: styling/text-and-fonts
audience: external
maturity: wip
guide_order: 73
topic_only: false
short_desc: Font family, size, weight, alignment, decoration, and the system font keywords
prerequisites: [styling]
tracked_files:
  - css/src/props/basic/font.rs
  - css/src/props/layout/text.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:49:28Z
default-search-keys:
  - FontRef
  - StyleFontFamily
  - StyleFontSize
  - StyleFontWeight
  - StyleFontStyle
  - PixelValue
  - LayoutTextJustify
  - Direction
---

# Styling Text

## Overview

*WIP.* Font fallback and the text shaper are stable; some advanced typography (`font-feature-settings`, variable axes) is not yet wired through from CSS to the layout engine. The properties below all parse and apply.

Text rendering is driven by four font properties (`font-family`,
`font-size`, `font-weight`, `font-style`) and modulated by alignment,
justification, and line metrics.

## font-family

`StyleFontFamily` is one of:

- `System(name)`. CSS like `"Arial"` or `Times New Roman`. Resolves to a face matching the family name.
- `SystemType(SystemFontType)`. CSS like `system:ui` or `system:monospace:bold`. Resolves to a platform UI font.
- `File(url)`. CSS like `url(/fonts/Inter.ttf)`. Resolves to a font file loaded from the URL.
- `Ref(FontRef)`. Not addressable from CSS. Resolves to a pre-loaded font handle.

The property takes a comma-separated *fallback list*. Each entry is tried
in order; the first one that resolves to a face containing the requested
glyph wins.

```css
body { font-family: "Inter", system:ui, sans-serif; }
code { font-family: system:monospace, "SF Mono", Menlo, monospace; }
```

### system: fonts

The `system:<role>[:<variant>]` prefix selects a platform UI font without
hard-coding a family name. It resolves to the OS's preferred face for
that role:

- `system:ui`. The default UI font.
- `system:ui:bold`. The UI bold variant.
- `system:monospace`, `system:monospace:bold`, `system:monospace:italic`. Platform monospace.
- `system:title`, `system:title:bold`. Larger UI text.
- `system:menu`. Menu and menu-item label font.
- `system:small`. Small-print UI font.
- `system:serif`, `system:serif:bold`. Platform serif.

If the role isn't recognised, the parser keeps the literal string as a
`System` family (so `system:invalid` becomes the family named
`"system:invalid"`).

### FontRef and pre-loaded fonts

`FontRef` is a reference-counted handle that points at parsed font data.
You won't construct `FontRef` from CSS, but you'll see it on the Rust side
when binding a font once and using it across multiple DOMs.

## font-size

`StyleFontSize` wraps a `PixelValue`. The default is `12pt`. Pick whatever
your design system needs:

```css
h1 { font-size: 28px; }
p  { font-size: 1em; }
small { font-size: 80%; }
```

`em` is relative to the parent's `font-size`. `rem` is relative to the
root. `%` resolves the same way as `em`.

## font-weight

`StyleFontWeight`:

- `lighter`. Lighter than parent.
- `100` ... `300`. Maps to `W100` ... `W300`.
- `normal`, `400`. Maps to `Normal`.
- `500`, `600`. Maps to `W500`, `W600`.
- `bold`, `700`. Maps to `Bold`.
- `800`, `900`. Maps to `W800`, `W900`.
- `bolder`. Heavier than parent.

The numeric scale is the OpenType weight class. The parser maps standard
numbers to enum variants. `450` and other in-between numbers are *not*
accepted; use the named variant for the closest weight.

## font-style

`StyleFontStyle`:

```rust,ignore
StyleFontStyle::Normal   // upright (default)
StyleFontStyle::Italic   // italic face
StyleFontStyle::Oblique  // oblique (synthesised slant if no italic face)
```

## text-align

Horizontal alignment of inline content within its line box:

- `start` (default). Left in LTR, right in RTL.
- `end`. Right in LTR, left in RTL.
- `left` / `right`. Absolute, ignoring text direction.
- `center`. Centred.
- `justify`. Spread to fill the line. See `text-justify` below.

## text-justify

`LayoutTextJustify` refines what `text-align: justify` does:

- `auto` (default). UA picks the appropriate algorithm for the script.
- `none`. No justification (`text-align: justify` is treated as `start`).
- `inter-word`. Distributes whitespace only between words.
- `inter-character`. Distributes whitespace between every character (CJK-friendly).
- `distribute`. Legacy alias of `inter-character`.

The legacy `distribute` value computes to `inter-character` per the spec.

## Line metrics

- `line-height` accepts `<percentage>`. Default `120%`. Multiplier applied to `font-size`.
- `letter-spacing` accepts `<length>`. Default `0px`. Added between every glyph.
- `word-spacing` accepts `<length>`. Default `0px`. Added between words.
- `tab-size` accepts `<length>`. Default `8em`. Width of a tab character.

```css
.body { line-height: 150%; letter-spacing: 0.02em; }
pre   { tab-size: 4em; }
```

## Wrapping and breaks

`white-space` controls collapsing and wrapping:

- `normal` (default). Collapses whitespace and wraps.
- `pre`. Preserves whitespace. No wrap (only at explicit breaks).
- `nowrap`. Collapses whitespace. No wrap.
- `pre-wrap`. Preserves whitespace and wraps.
- `pre-line`. Collapses whitespace but preserves newlines. Wraps.
- `break-spaces`. Preserves whitespace and breaks at every space. Wraps.

`word-break` and `overflow-wrap` decide what to do with long unbreakable
sequences (URLs, code, CJK):

```rust,ignore
StyleWordBreak::Normal      // default
StyleWordBreak::BreakAll    // allow break between any two characters
StyleWordBreak::KeepAll     // forbid break inside CJK
StyleWordBreak::BreakWord   // deprecated alias

StyleOverflowWrap::Normal   // only at allowed break points
StyleOverflowWrap::Anywhere // break anywhere to prevent overflow
StyleOverflowWrap::BreakWord
```

`hyphens: none | manual | auto` enables soft-hyphen breaking. `auto`
requires the layout crate's hyphenation resources for the active language.

## Direction and writing mode

For bidirectional text:

```rust,ignore
StyleDirection::Ltr   // left-to-right (default)
StyleDirection::Rtl   // right-to-left

StyleUnicodeBidi      // bidi-override embedding for the inline element
```

For vertical scripts, set `writing-mode` on the block container. See
[Inline and Text Flow](../layout/inline.md#direction-and-writing-modes)
for the values.

## Text decoration and selection

- `text-decoration` accepts `none`, `underline`, `overline`, or `line-through`.
- `user-select` accepts `auto`, `text`, `none`, or `all`.
- `vertical-align` accepts `baseline`, `top`, `middle`, `bottom`, `sub`, `super`, `text-top`, `text-bottom`, `<percentage>`, or `<length>`.

`user-select: none` is what you want on buttons and icon glyphs to prevent
double-click text selection from overlapping the click.

## Recipes

### Heading and body

```azul-render screenshot=text-heading width=480 height=200 subtitle="A heading and paragraph using system fonts"
<body style="font-family: system:ui, sans-serif; padding: 16px;">
  <h1 style="font-size: 24px; font-weight: bold; margin: 0 0 8px 0;">Heading</h1>
  <p style="font-size: 14px; line-height: 150%; margin: 0;">
    Body text rendered at 14px with 150% line-height. Long enough to wrap on
    a 480px-wide line.
  </p>
</body>
```

### Justified paragraph

```azul-render screenshot=text-justify width=400 height=200 subtitle="text-align: justify with default text-justify: auto"
<body style="font-family: serif; padding: 16px;">
  <p style="text-align: justify; line-height: 140%; font-size: 14px;">
    Justified text spreads each line to fill the box. The last line keeps its
    natural alignment because text-align-last defaults to auto.
  </p>
</body>
```

### Mixed weights

```azul-render screenshot=text-weights width=400 height=200 subtitle="lighter / normal / bold weights side by side"
<body style="font-family: system:ui; padding: 16px; font-size: 16px;">
  <p style="font-weight: 300;">Light weight (300)</p>
  <p style="font-weight: normal;">Normal weight (400)</p>
  <p style="font-weight: 600;">Semi-bold (600)</p>
  <p style="font-weight: bold;">Bold (700)</p>
</body>
```

## Default values at a glance

- `font-family`. Default `serif` (platform default).
- `font-size`. Default `12pt`.
- `font-weight`. Default `normal` (400).
- `font-style`. Default `normal`.
- `line-height`. Default `120%`.
- `letter-spacing`. Default `0px`.
- `word-spacing`. Default `0px`.
- `tab-size`. Default `8em`.
- `text-align`. Default `start`.
- `text-justify`. Default `auto`.
- `white-space`. Default `normal`.
- `word-break`. Default `normal`.
- `overflow-wrap`. Default `normal`.
- `hyphens`. Default `manual`.
- `direction`. Default `ltr`.
- `text-decoration`. Default `none`.
- `user-select`. Default `auto`.
- `vertical-align`. Default `baseline`.

---

### Doc Guide En Styling Themes (`doc/guide/en/styling/themes.md`)

---
slug: styling/themes
title: System Themes
language: en
canonical_slug: styling/themes
audience: external
maturity: wip
guide_order: 72
topic_only: false
short_desc: System colors, `@theme`, `@os`, and accessibility queries
prerequisites: [styling]
tracked_files:
  - css/src/system.rs
  - css/src/dynamic_selector.rs
  - css/src/props/basic/color.rs
  - css/src/props/basic/font.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - SystemStyle
  - SystemColors
  - SystemFontType
  - ThemeCondition
  - AccessibilitySettings
  - Css
  - ColorU
---

# System Themes

## Overview

*WIP.* Discovery (theme, accent, fonts, accessibility) is wired up across all desktop platforms. The user-facing CSS hooks (`system:*` colors, `system:*` fonts, `@theme dark`) work today. Some discovered values still arrive via CLI wrappers; the FFI-direct paths and ricing overrides are still being stabilized.

A native-feeling app reads its colors and fonts from the host OS.
Azul exposes those values through three CSS hooks:

- `system:<color>` for colors that follow the user's accent and theme.
- `system:<font>` for the platform's UI, monospace, or serif fonts.
- `@theme dark { ... }` and `@os <name>` for conditional rules that
  re-evaluate per frame when the user toggles dark mode or moves to a
  different desktop environment.

```css
background: system:window-background;
color: system:text;
font-family: system:ui;
border: 1px solid system:accent;
@theme dark {
    background: #1c1c1e;
    color: #f0f0f0;
}
```

```azul-render screenshot=themes-light slideshow=themes-toggle width=400 height=180 subtitle="Light theme"
<html>
<head><style>
body { font-family: sans-serif; padding: 20px; background: #fafafa; }
.card { background: white; color: #222; border: 1px solid #1976d2; padding: 16px; border-radius: 6px; }
</style></head>
<body><div class="card">Adaptive surface, light theme</div></body>
</html>
```

```azul-render screenshot=themes-dark slideshow=themes-toggle width=400 height=180 subtitle="Dark theme, OS-supplied palette"
<html>
<head><style>
body { font-family: sans-serif; padding: 20px; background: #0d0d0f; }
.card { background: #1c1c1e; color: #f0f0f0; border: 1px solid #0a84ff; padding: 16px; border-radius: 6px; }
</style></head>
<body><div class="card">Adaptive surface, dark theme</div></body>
</html>
```

## System colors

Use a `system:<name>` keyword wherever a color is accepted. The framework
resolves it at frame time using the user's current settings:

- `system:text`. Primary text color.
- `system:background`. Content background.
- `system:accent`. The user's accent color (Windows, macOS, GNOME).
- `system:accent-text`. Readable text on an accent fill.
- `system:button-face`. Button or control background.
- `system:button-text`. Button or control text.
- `system:window-background`. Window chrome background.
- `system:selection-background`. Selected-text background.
- `system:selection-text`. Selected-text foreground.

The resolver picks the user's current value if the OS reported one, and
falls back to a standard color otherwise. The full collection (link,
separator, grid, sidebar, and inactive-window variants) is on
`SystemColors`.

```rust,no_run
use azul::prelude::*;
let _ = Dom::create_button("Save", SmallAriaInfo::label("Save")).with_css("
    background: system:button-face;
    color: system:button-text;
    border: 1px solid system:accent;
    padding: 6px 14px;
    :hover { background: system:accent; color: system:accent-text; }
");
```

## System fonts

`system:<role>` keywords pick the right face on each platform.
`SystemFontType` enumerates the roles:

- `system:ui`. macOS: SF Pro Text. Windows: Segoe UI Variable. Linux: Cantarell.
- `system:ui:bold`. macOS: SF Pro Text Bold. Windows: Segoe UI Bold. Linux: Cantarell Bold.
- `system:monospace`. macOS: SF Mono or Menlo. Windows: Cascadia Mono or Consolas. Linux: Ubuntu Mono or DejaVu Sans Mono.
- `system:monospace:bold`. macOS: Menlo Bold. Windows: Cascadia Mono Bold. Linux: Ubuntu Mono Bold.
- `system:monospace:italic`. macOS: Menlo Italic. Windows: Cascadia Mono Italic. Linux: Ubuntu Mono Italic.
- `system:title`. macOS: SF Pro Display. Windows: Segoe UI Variable Display. Linux: Cantarell.
- `system:title:bold`. macOS: SF Pro Display Bold. Windows: Segoe UI Variable Display Bold. Linux: Cantarell Bold.
- `system:menu`. macOS: SF Pro Text. Windows: Segoe UI. Linux: Cantarell.
- `system:small`. macOS: SF Pro Text 11pt. Windows: Segoe UI 9pt. Linux: Cantarell 9pt.
- `system:serif`. macOS: New York. Windows: Cambria. Linux: DejaVu Serif.
- `system:serif:bold`. macOS: Georgia Bold. Windows: Cambria Bold. Linux: DejaVu Serif Bold.

The framework walks a fallback chain at font resolution and falls through
to the `sans-serif`, `monospace`, or `serif` generics if none match.

```css
font-family: system:ui;
font-size: 14px;
```

## @theme adaptation

`@theme <variant> { ... }` blocks evaluate per frame. The variant matches
the system's current preference (light or dark) and updates the moment the
user toggles their OS-wide setting (no DOM rebuild required):

```css
background: white;
color: #1a1a1a;
@theme dark {
    background: #1c1c1e;
    color: #f0f0f0;
}
@theme custom-high-contrast {
    background: black;
    color: yellow;
}
```

The variants follow `ThemeCondition`:

- `@theme light`: system reports light theme.
- `@theme dark`: system reports dark theme.
- `@theme <name>`: a custom string treated as user-defined. The system
  resolver doesn't emit it on its own.

For typical apps, define the base style for light mode and override
selected properties under `@theme dark`. Combine with `@os` for
platform-flavoured dark mode (a Mac-style sheen vs. a Windows-style flat
fill).

## @os

A single rule covers OS family, version, and Linux desktop environment.
The grammar is `@os(<family>[:<de>] [<op> <version>])`. Each clause is
optional; `op` is `>=`, `<=`, or `=`.

OS families:

- `windows`. Windows desktop.
- `macos`. macOS.
- `ios`. iOS.
- `apple`. macOS or iOS.
- `linux`. Any Linux desktop.
- `android`. Android.
- `web`. WASM target.
- `any`. Always matches.

Family-only rules also accept the bare-identifier form: `@os linux { … }`
is the same as `@os(linux) { … }`.

```css
/* family only */
@os(linux)               { font-family: 'Cantarell'; }
@os(windows)             { font-family: 'Segoe UI'; }

/* family + version — codename or bare number both work */
@os(windows >= 11)       { font-family: 'Segoe UI Variable Text'; }
@os(macos >= big-sur)    { font-family: '.SF NS'; }
@os(linux >= 6)          { /* kernel 6.0+ */ }

/* Linux desktop environment */
@os(linux:gnome)         { font-family: 'Cantarell'; }
@os(linux:kde)           { font-family: 'Noto Sans'; }

/* family + DE + DE version */
@os(linux:gnome > 40)    { padding-inline-start: 16px; }
```

Comparisons across OS families always evaluate to false.
`@os(macos >= sonoma)` on Windows is just inert, not a parse error.

Desktop-environment versions only match when the runtime knows the DE's
version number; until detection is wired up for a given DE, the
`@os(linux:de > N)` form will not match.

Version synonyms are accepted permissively: bare numbers (`11`),
prefixed forms (`win-11`, `win11`, `windows-11`), and codenames where
they exist (`big-sur`, `monterey`, `sonoma`) all map to the same
underlying version. Linux accepts `5`, `5.4`, and `5.4.10`.

## Accessibility queries

These map to the OS's accessibility settings. They live on
`AccessibilitySettings` and re-evaluate per frame.

```css
transition: background 200ms ease;
@media (prefers-reduced-motion) {
    transition: none;
}
@media (prefers-contrast) {
    background: black;
    color: white;
    border: 2px solid white;
}
```

- `@media (prefers-reduced-motion)`. Source: macOS `AXReduceMotion`, Windows `SPI_GETCLIENTAREAANIMATION`, Linux `enable-animations`.
- `@media (prefers-contrast)`. Source: macOS `AXIncreaseContrast`, Windows `SPI_GETHIGHCONTRAST`, Linux `high-contrast`.

Honour `prefers-reduced-motion` for any non-essential animation.

## @media viewport queries

Standard CSS:

```css
padding: 24px;
font-size: 16px;
@media (max-width: 640px) {
    padding: 12px;
    font-size: 14px;
}
@media (orientation: portrait) {
    flex-direction: column;
}
```

The viewport size comes from the current window. On a multi-window app
each window has its own viewport, evaluated independently.

## @lang(<bcp47>)

Match the system locale. Prefix matching: `@lang(de)` matches `de`,
`de-DE`, `de-AT`. Useful for locale-specific quotes, hyphenation, and
typographic conventions:

```css
quotes: '\u{201C}' '\u{201D}';
@lang(de) { quotes: '\u{201E}' '\u{201C}'; }
@lang(fr) { quotes: '\u{00AB} ' ' \u{00BB}'; }
```

The active locale field is `SystemStyle.language` (BCP 47, e.g.,
`"en-US"`).

## Reading discovered values from Rust

The full snapshot is `SystemStyle`. Every field ends up populating the
dynamic selectors and the `system:*` resolver. In Rust code you generally
don't need to touch it. Stick with `system:*` and `@theme` or `@os` in
your CSS. Those expressions stay ergonomic and re-evaluate automatically.

## How user theming layers with component CSS

The cascade has three layers, from outermost to innermost:

1. **System discovery** (the `system:*` keywords and `@theme dark`
   condition). Resolved per frame from the running OS, so a theme
   toggle takes effect on the next paint without a re-layout.
2. **End-user ricing** — the optional CSS file the user dropped into
   `~/.config/azul/styles/<app>.css` (Linux/macOS) or
   `%APPDATA%\azul\styles\<app>.css` (Windows). Loaded at app startup
   when the `io` feature is on.
3. **Application CSS** — every component-level `Css` attached via
   `Dom::style(...)` on a subtree root, plus inline rules attached
   via `Dom::with_css(...)` on individual nodes. CSS lives on the
   tree the layout callback returns; there is no global stylesheet
   passed to `App::create`.

Components don't fight user theming because their selectors target
component-internal classes (`.shadcn-card`, `.my-row`) while user
theming targets the `system:*` color and font hooks. As long as a
component reads its colors from `system:*` instead of hard-coding
hex values, the user's `~/.config/azul/styles/<app>.css` can repaint
the component without the component's source changing.

A few escape hatches when the discovery isn't enough:

- **Inline override on a node**: `Dom::with_css_property(...)` wins
  the cascade for that node.
- **Subtree override via component CSS**: stack a second `Css` via
  `Dom::style(css)`. Later rule blocks win at equal
  `(priority, specificity)`. See [DOM › Component-level
  stylesheets](../dom.md#component-level-stylesheets).

## Controlling end-user customization

Azul has a single env var for the entire end-user-customization
layer: `AZ_RICING`.

Unset (the default) means the framework loads the user CSS file at
`~/.config/azul/styles/<app>.css` if it exists, and on Linux runs the
standard detection chain (`KDE > GNOME > riced-desktop > defaults`).
This is the right behavior for a normal install on a normal
desktop.

`AZ_RICING=off` (aliases: `disabled`, `none`, `0`) skips both the
user CSS file and the riced-desktop sources. Pick this for a kiosk
build, a CI runner, or any install that must not pick up local
theme customization. The cascade still runs `system:*` resolution
and `@theme` conditions — disabling ricing only stops the
*user-supplied* layer; the OS-supplied palette is still honored.

`AZ_RICING=force` (aliases: `prefer`, `aggressive`, `1`) reorders the
Linux detection chain so riced-desktop sources (Hyprland config,
pywal cache, i3/sway) win over the GNOME and KDE paths. Use this
when `XDG_CURRENT_DESKTOP` still reports `gnome` but the actual
session is a tiling WM with a custom palette. The user CSS file
still loads in this mode.

There is no env var that forces dark/light mode. The platform's own
facilities (macOS *General > Appearance*, Windows *Personalization >
Colors > Choose your mode*, GNOME *Settings > Appearance*) drive the
`prefers-color-scheme` discovery and azul re-evaluates `@theme` on
the next frame.

## Previewing on a different platform

Themes vary not just by light/dark but by OS conventions: a button on
iOS doesn't look like a button on Windows 11, and `system:*` colors
resolve differently on each. The `main()` function can override the
detected environment between `AppConfig::create()` and `App::create()`,
forcing the cascade to evaluate as if the app were running on a
different platform:

```rust,no_run
use azul::prelude::*;

fn main() {
    let mut config = AppConfig::create();

    // Pretend this app is running on iOS, dark theme, with a French
    // locale, on a 360x780 viewport. Useful for screenshot diffing,
    // designer review, or "does my app look OK on Android?" checks.
    config.mock_css_environment = OptionCssMockEnvironment::Some(
        CssMockEnvironment {
            os: OptionOsCondition::Some(OsCondition::Ios),
            theme: OptionThemeCondition::Some(ThemeCondition::Dark),
            language: OptionString::Some("fr-FR".into()),
            viewport_width: OptionF32::Some(360.0),
            viewport_height: OptionF32::Some(780.0),
            ..Default::default()
        }
    );

    // Or start from the detected style and override what you want to pin:
    // let mut style = SystemStyle::detect();
    // style.metrics.corner_radius = OptionF32::Some(0.0);
    // config.system_style = style;

    let app = App::create(initial_data, config);
    app.run(WindowCreateOptions::new(layout));
}
```

Every field on `CssMockEnvironment` is optional — the ones not set
fall back to auto-detected values. Combined with a `SystemStyle`
whose fields you set yourself (`SystemStyle::detect()` is the
starting point the FFI exposes; the curated platform presets in
`css::system::defaults` are Rust-internal),
this gives a single binary the ability to render its UI as if it
were running on any supported target — useful for screenshot
testing, designer review, and "what would my app look like, pixel by
pixel, on iOS?" sanity checks.

The override is read at app startup; it doesn't change the actual
runtime windowing backend (`AZ_BACKEND` does that, and that env var
isn't a theming concern — it picks `x11` vs `wayland` etc. for the
real OS the app is actually running on). For the full list of `AZ_*`
runtime env vars (debug server, profiling, layout tracing, headless
rendering), see [Debugging](../debugging.md).

---

### Doc Guide En System Accessibility (`doc/guide/en/system/accessibility.md`)

---
slug: system/accessibility
title: Accessibility
language: en
canonical_slug: system/accessibility
audience: external
maturity: wip
guide_order: 125
topic_only: false
short_desc: Screen reader integration and ARIA roles
prerequisites: [dom]
tracked_files:
  - core/src/a11y.rs
  - layout/src/managers/a11y.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T12:00:00Z
default-search-keys:
  - AccessibilityInfo
  - SmallAriaInfo
  - ProgressAriaInfo
  - MeterAriaInfo
  - DialogAriaInfo
  - AccessibilityRole
  - AccessibilityAction
  - AccessibilityState
  - Dom
  - NodeType
---

# Accessibility

## Introduction

Azul exposes accessibility metadata to assistive technologies (screen readers,
switch navigation, voice control). The way it works is that you attach an 
`AccessibilityInfo` to a node, and the framework publishes it on the next 
layout pass.

```rust,no_run
use azul::prelude::*;
let save = Dom::create_button(
    "Save",
    SmallAriaInfo::label("Save the document".into())
        .with_role(AccessibilityRole::PushButton),
);
```

There are two entry points. `SmallAriaInfo` covers the common case (label,
role, description). `AccessibilityInfo` carries the full record (states,
actions, accelerators, live regions, label/describe-by relationships).

## Adding a label, role, and description with SmallAriaInfo

`SmallAriaInfo` is a builder over three optional fields:

```rust,ignore
pub struct SmallAriaInfo {
    pub label: OptionString,
    pub role: OptionAccessibilityRole,
    pub description: OptionString,
}
```

Construct with `SmallAriaInfo::label(text)` and chain `with_role` and
`with_description`:

```rust,no_run
use azul::prelude::*;
let aria = SmallAriaInfo::label("Submit form".into())
    .with_role(AccessibilityRole::PushButton)
    .with_description("Sends the form to the server.".into());
```

`label` is the accessible name screen readers announce. `role` overrides the
role that would otherwise be inferred from the HTML tag. `description` is read
after the label as supplementary context. Use it sparingly. It does not
replace a missing label.

## Constructors that require accessibility info

The accessible variants of the `Dom::create_*` helpers take a `SmallAriaInfo`
parameter. Each interactive element comes in two forms.

- `Dom::create_button(text, aria)`, with the escape hatch `create_button_no_a11y(text)`.
- `Dom::create_a(href, text, aria)`, with the escape hatch `create_a_no_a11y(href, text)`.
- `Dom::create_input(ty, name, label, aria)`, with the escape hatch
  `create_input_no_a11y(ty, name, label)`.
- `Dom::create_textarea(name, label, aria)`, with the escape hatch
  `create_textarea_no_a11y(name, label)`.
- `Dom::create_select(name, label, aria)`, with the escape hatch
  `create_select_no_a11y(name, label)`.
- `Dom::create_table(caption, aria)`, with the escape hatch `create_table_no_a11y()`.
- `Dom::create_label(for_id, text, aria)`, with the escape hatch
  `create_label_no_a11y(for_id, text)`.

The `_no_a11y` variants are deliberately verbose. Use them only when the
accessible name comes from somewhere else, for example an icon-only link
whose name comes from a sibling image's alt text.

For elements you build with `Dom::create_div().with_child(...)`, attach a label
through `Dom::with_accessibility_info(AccessibilityInfo)`.

## Going beyond labels: AccessibilityInfo

`AccessibilityInfo` is the full record:

```rust,ignore
pub struct AccessibilityInfo {
    pub accessibility_name: OptionString,
    pub accessibility_value: OptionString,
    pub description: OptionString,
    pub accelerator: OptionVirtualKeyCodeCombo,
    pub default_action: OptionString,
    pub states: AccessibilityStateVec,
    pub supported_actions: AccessibilityActionVec,
    pub labelled_by: OptionDomNodeId,
    pub described_by: OptionDomNodeId,
    pub role: AccessibilityRole,
    pub is_live_region: bool,
}
```

Set this when you need any of: dynamic states, custom action lists, keyboard
accelerators, label-by/describe-by relationships, or live regions.

## AccessibilityRole: what kind of element this is

`AccessibilityRole` is an enum over the standard role set. Most HTML tags are
mapped automatically. You set `role` only when the visual element does not
match its tag, for example a div styled as a tab strip:

```rust,no_run
use azul::prelude::*;
let tab = SmallAriaInfo::label("Settings tab".into())
    .with_role(AccessibilityRole::PageTab);
```

Roles commonly used in app code:

- `PushButton` for a clickable button.
- `Link` for a hyperlink.
- `CheckButton` for a checkbox.
- `RadioButton` for a radio button.
- `ComboBox` for a dropdown with text input.
- `DropList` for a dropdown without text input.
- `Slider` for a slider.
- `SpinButton` for a numeric stepper.
- `ProgressBar` for a progress indicator.
- `MenuItem` for a menu item.
- `PageTab` and `PageTabList` for an individual tab and its strip.
- `Outline` and `OutlineItem` for a tree view and its nodes.
- `List` and `ListItem` for a list and its items.
- `Table`, `Row`, and `Cell` for a data grid.
- `Dialog` for a modal dialog.
- `Alert` for a non-modal notification.
- `Tooltip` for a hover tooltip.

`Unknown` is the default fallback and tells the platform to infer from the
tag. `Nothing` explicitly hides the element from assistive tech.

## AccessibilityState: dynamic state

States live on `AccessibilityInfo.states`. Push them whenever the visual state
changes. A toggled checkbox flips `CheckedFalse` to `CheckedTrue`. A busy
panel adds `Busy` until loading finishes.

- `Unavailable`: disabled or grayed out.
- `Selected`: item is selected (independent of focus).
- `Focused`: has keyboard focus (managed automatically).
- `CheckedTrue` and `CheckedFalse`: binary toggle.
- `Readonly`: not editable.
- `Default`: the default action button in a dialog.
- `Expanded` and `Collapsed`: disclosure state.
- `Busy`: element is unresponsive while working.
- `Offscreen`: scrolled out of view.
- `Focusable`: can receive focus.
- `Selectable` and `Multiselectable`: container supports selection.
- `Linked` and `Traversed`: hyperlink, possibly visited.
- `Protected`: sensitive content (passwords).

State changes only become visible to assistive tech after the next layout pass
rebuilds the a11y tree. Return `Update::RefreshDom` from your callback when
you mutate states so the new tree is shipped.

## AccessibilityAction: what assistive tech can do

Actions populate `AccessibilityInfo.supported_actions`. The platform routes
incoming requests back to your DOM node:

```rust,ignore
pub enum AccessibilityAction {
    Default, Focus, Blur,
    Collapse, Expand,
    ScrollIntoView,
    Increment, Decrement,
    ShowContextMenu, HideTooltip, ShowTooltip,
    ScrollUp, ScrollDown, ScrollLeft, ScrollRight,
    ReplaceSelectedText(AzString),
    ScrollToPoint(LogicalPosition),
    SetScrollOffset(LogicalPosition),
    SetTextSelection(TextSelectionStartEnd),
    SetSequentialFocusNavigationStartingPoint,
    SetValue(AzString),
    SetNumericValue(FloatValue),
    CustomAction(i32),
}
```

For most elements you don't need to list actions explicitly. The framework
adds `Focus` for focusable nodes, `Click` for nodes with activation behavior,
and the text-editing actions (`SetTextSelection`, `ReplaceSelectedText`,
`SetValue`) for inputs and contenteditable regions. List actions in
`supported_actions` only when you implement custom behavior, for example
`Increment` and `Decrement` on a div-based slider.

## Live regions

Set `AccessibilityInfo.is_live_region = true` on an element whose content
changes while the user is doing something else. Screen readers announce
live-region updates without requiring focus. Typical uses are chat windows,
toast notifications, autosave indicators, and timers.

## Headings

Headings use the standard h1 through h6 tags. The framework maps them to the
accessibility tree's heading role with the level set, so screen-reader heading
navigation works without per-element configuration. Use real heading tags
rather than styled divs.

## Focus and text editing

The focus manager (covered in `events`) tracks the focused node. The a11y
tree's focus follows that value so assistive tech moves with the keyboard.

For contenteditable, input, and textarea elements:

- The current text is exposed as the node's value, not its label, so screen
  readers can announce edits without re-reading the field name.
- The text-editing actions (`SetTextSelection`, `ReplaceSelectedText`,
  `SetValue`) are added automatically.
- When the focused element has an active selection or cursor, the tree
  reports character lengths and anchor/focus positions so the screen reader
  can announce the cursor position or selection length.

## Action requests are events

When assistive tech invokes an action, the framework synthesizes the matching
native event:

- `Default`, `Focus`, `Blur` become focus and click events to your callbacks.
- Scroll actions adjust the scroll offset of the matching scroll frame.
- `SetValue`, `ReplaceSelectedText`, and `SetTextSelection` become text input
  events delivered to the focused contenteditable.
- `CustomAction(i32)` fires whatever event filter your DOM declares for the
  default action. Set `AccessibilityInfo.default_action` to a human-readable
  description so the screen reader can announce it.

You don't write separate code paths for assistive tech. The same callback that
handles a mouse click handles `AccessibilityAction::Default`.

## Enabling the feature

Accessibility is gated behind the `a11y` Cargo feature. With the feature off,
the a11y manager is a no-op. You can still set `SmallAriaInfo` on your DOM
and the data is preserved, but no a11y tree is published.

```toml
# Cargo.toml
[dependencies]
azul = { version = "*", features = ["a11y"] }
```

---

### Doc Guide En System Clipboard (`doc/guide/en/system/clipboard.md`)

---
slug: system/clipboard
title: Clipboard
language: en
canonical_slug: system/clipboard
audience: external
maturity: wip
guide_order: 122
topic_only: false
short_desc: Reading and writing the system clipboard
prerequisites: [events]
tracked_files:
  - layout/src/managers/clipboard.rs
  - layout/src/managers/selection.rs
  - layout/src/callbacks.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T00:00:00Z
default-search-keys:
  - ClipboardContent
  - CallbackInfo
  - get_clipboard_content
  - set_clipboard_content
  - set_cut_content
  - set_copy_content
  - inspect_copy_changeset
  - inspect_cut_changeset
---

# Clipboard

## Introduction

*WIP.* Copy, cut, and paste of plain text work on every supported platform. Styled-text round-trip and HTML clipboard formats are partially implemented; the public API is stable.

Clipboard access happens from inside callbacks via `CallbackInfo` helper
methods. When the user presses paste, the platform reads the system clipboard
first, stages the content, then runs your paste callback so you can inspect or
override what gets inserted. Copy and cut go the other way: your callback can
queue a `ClipboardContent`, and the platform commits it to the OS clipboard
once the callback batch completes.

```rust,no_run
use azul::prelude::*;

extern "C" 
fn on_paste(_data: RefAny, info: CallbackInfo) -> Update {
    if let Some(content) = info.get_clipboard_content() {
        println!("about to paste: {:?}", content.plain_text.as_str());
    }
    Update::DoNothing
}
```

## Data model

`ClipboardContent` is the unit of currency for every clipboard operation. It
carries plain text plus an ordered list of styled runs.

```rust,ignore
pub struct ClipboardContent {
    pub plain_text: AzString,
    pub styled_runs: StyledTextRunVec,
}

pub struct StyledTextRun {
    pub text:        AzString,
    pub font_family: OptionString,
    pub font_size_px: f32,
    pub color:       ColorU,
    pub is_bold:     bool,
    pub is_italic:   bool,
}
```

Today only `plain_text` is written to the OS clipboard. Build the styled runs
now if you want to be ready for the rich-text path.

## Reading the clipboard during paste

`CallbackInfo::get_clipboard_content` returns the staged paste content while a
paste callback is running. Outside of paste it returns `None`.

```rust,no_run
use azul::prelude::*;

extern "C" 
fn block_long_pastes(_data: RefAny, info: CallbackInfo) -> Update {
    let Some(content) = info.get_clipboard_content() else {
        return Update::DoNothing;
    };

    if content.plain_text.as_str().len() > 10_000 {
        return Update::DoNothing;
    }

    Update::RefreshDom
}
```

## Overriding what gets copied

`CallbackInfo::set_clipboard_content` queues a `ClipboardContent` for the
current hit node, so a Copy callback can transform the selected text before
the OS clipboard receives it.

```rust,no_run
use azul::prelude::*;

extern "C" 
fn rewrite_copy(_data: RefAny, info: CallbackInfo) -> Update {
    info.set_clipboard_content(ClipboardContent {
        plain_text: "[copied via my-app]".into(),
        styled_runs: StyledTextRunVec::from_vec(vec![]),
    });
    Update::DoNothing
}
```

For the cut path, `CallbackInfo::set_cut_content` mirrors the copy helper.
Both helpers also exist in their explicit-target form,
`CallbackInfo::set_copy_content(target, content)` and
`CallbackInfo::set_cut_content(target, content)`, when you need to override
clipboard content for a node other than the one that fired the callback.

## Inspecting before commit

`CallbackInfo::inspect_copy_changeset(target)` and
`CallbackInfo::inspect_cut_changeset(target)` peek at whatever has been
queued so far for a given node, before the platform pushes it to the OS. Use
them when one callback wants to react to a change another callback already
queued (for example, to log the final text or to veto the operation by
overwriting it with empty content).

## Common pitfalls

- Calling `get_clipboard_content` outside a paste callback returns `None`. The
  manager only stages content while a paste is in flight.
- Set methods are queued, not synchronous. The platform writes to the OS
  clipboard once the callback batch completes. Don't assume the OS clipboard
  reflects your change inside the same callback.
- Styled runs are accepted but not yet committed. Only `plain_text` reaches
  another application today.

---

### Doc Guide En System Device Input (`doc/guide/en/system/device-input.md`)

---
slug: system/device-input
title: Device Input
language: en
canonical_slug: system/device-input
audience: external
maturity: beta
guide_order: 123
topic_only: false
short_desc: React to motion sensors, gamepads, and location as events - no polling
prerequisites: [events, events/callbacks]
tracked_files:
  - core/src/events.rs
  - core/src/sensors.rs
  - core/src/gamepad.rs
  - layout/src/managers/sensors.rs
  - layout/src/managers/gamepad.rs
  - layout/src/managers/geolocation.rs
last_generated_rev: 754b7f00e088960c14db598f64fa200dacc28bf1
generated_at: 2026-05-21T00:00:00Z
default-search-keys:
  - SensorChanged
  - GamepadInput
  - get_sensor_reading
  - get_primary_gamepad
  - get_gamepad_state
  - get_location_fix
  - create_geolocation_probe
  - get_safe_area_insets
  - SafeAreaInsets
  - WindowEventFilter
---

# Device Input

## Introduction

Motion sensors (accelerometer / gyroscope / magnetometer), gamepads, and
geolocation are **event-driven**: you attach a callback to a window-level event
filter and the framework invokes it when a new sample arrives. You do **not**
poll on a [Timer](../animations/timers.md). Inside the callback you read the current value
through an accessor on [`CallbackInfo`](../events/callbacks.md). This is the same
"react to an event, then read the detail" shape as a key press or a mouse move.

This avoids the busy-loop a poll would cause and keeps the device's sample rate
in charge of how often your code runs.

## Motion sensors

Attach a callback to `WindowEventFilter::SensorChanged`; it fires when any
sensor reading advances. Read the value with `CallbackInfo::get_sensor_reading`:

```rust
let dom = Dom::create_body().with_callback(
    EventFilter::Window(WindowEventFilter::SensorChanged),
    state.clone(),
    on_sensor,
);

extern "C" fn on_sensor(mut data: RefAny, info: CallbackInfo) -> Update {
    if let OptionSensorReading::Some(r) = info.get_sensor_reading(SensorKind::Accelerometer) {
        if let Some(mut s) = data.downcast_mut::<MyState>() {
            s.tilt_x = r.x;  // m/s^2 on each axis; r.magnitude() for total
        }
    }
    Update::RefreshDom
}
```

There is also a node-level `HoverEventFilter::SensorChanged` mirror, for the
"redraw this node when a reading changes" pattern. Identical consecutive
readings do not re-fire (an idle sensor streaming the same value is silent).

## Gamepad

Attach to `WindowEventFilter::GamepadInput`; read state with
`CallbackInfo::get_primary_gamepad` (the first connected pad - the common
single-controller case) or `get_gamepad_state(id)`:

```rust
extern "C" fn on_gamepad(mut data: RefAny, info: CallbackInfo) -> Update {
    if let OptionGamepadState::Some(pad) = info.get_primary_gamepad() {
        let jump = pad.is_pressed(GamepadButton::South);   // A / Cross
        let move_x = pad.left_stick_x;                     // [-1, 1]
        // ... drive your game/menu state
    }
    Update::RefreshDom
}
```

`GamepadState` carries `connected`, a `buttons` bitset (read via `is_pressed`),
two sticks, and the triggers (`left_z` / `right_z`). A disconnect keeps the slot
with `connected = false` so you can observe it.

## Geolocation

Geolocation follows the WebAuthn-style permission model: mounting a
`Dom::create_geolocation_probe(...)` node *is* the permission request. Once a
backend delivers a fix you read it with `CallbackInfo::get_location_fix`:

```rust
// Mounting the probe asks the OS for location.
container.with_child(Dom::create_geolocation_probe(GeolocationProbeConfig {
    high_accuracy: true,
    background: false,
    max_accuracy_m: 0.0,
    min_interval_ms: 0,
}));

// Read the latest fix from any callback:
if let OptionLocationFix::Some(fix) = info.get_location_fix() {
    let (lat, lon) = (fix.latitude_deg, fix.longitude_deg);
}
```

`LocationFix` carries latitude/longitude, accuracy, altitude (+ accuracy),
heading, speed, and a timestamp. See the AzulMaps example for a live readout.

## Pen / stylus

Pen input (Apple Pencil, S-Pen, Wacom) is **state-based**, like sensors: react
to ordinary pointer events (MouseDown / MouseMove / MouseUp) and read the pen
detail with `CallbackInfo::get_pen_state` (or `get_pen_pressure` /
`get_pen_tilt`). `PenState` carries position, pressure, tilt, the contact +
eraser flags, the barrel button, and the extended axes (tangential pressure,
barrel roll). The platform backend populates it through the gesture manager;
`examples/azul-paint` uses exactly this to draw pressure-modulated,
barrel-roll-oriented strokes.

```rust
extern "C" fn on_pointer_move(mut data: RefAny, info: CallbackInfo) -> Update {
    if let Some(pen) = info.get_pen_state().into_option() {
        let width = 2.0 + pen.pressure * 10.0; // light touch -> thin line
        // ... draw with `width`, oriented by pen.barrel_roll, erase if is_eraser
    }
    Update::RefreshDom
}
```

(The `PenDown`/`Move`/`Up` event *filters* exist but are not the path apps use -
pointer-event + `get_pen_state` is.)

## Safe-area insets (notches)

On displays with a notch, rounded corners, or system bars (a MacBook notch, a
phone, a tablet) part of the window is obscured. Read the insets - the margins
to keep interactive content clear of - with `CallbackInfo::get_safe_area_insets`:

```rust
extern "C" fn on_event(data: RefAny, info: CallbackInfo) -> Update {
    let safe = info.get_safe_area_insets();          // css SafeAreaInsets
    if let Some(top) = safe.top.into_option() {
        // ... pad the top bar by `top` so it clears the notch
    }
    Update::DoNothing
}
```

`SafeAreaInsets` carries `top` / `right` / `bottom` / `left`, each an
`OptionPixelValue` (`None` where there is no inset). It's the same
`azul_css::SafeAreaInsets` the styling system holds, so the values are shared
with layout. The platform shell populates them - macOS reads the notch via
`NSView.safeAreaInsets`; iOS (`UIView.safeAreaInsets`) and Android
(`WindowInsets`) are being wired. Zero on displays with no inset.

## Configurability

- **Sensors**: the manager keeps one reading per `SensorKind`; request rate /
  which sensors are platform-backend concerns (CoreMotion / Android
  `SensorManager`).
- **Gamepad**: deadzone + rumble are platform-backend features; the cross-
  platform surface gives you raw stick/trigger values and the button bitset.
- **Geolocation**: `GeolocationProbeConfig` configures accuracy, background
  delivery, max accuracy, and minimum interval at mount time.

## Testing without hardware

Every path above is exercised synthetically (no device) by
`layout/tests/synthetic_events.rs`: it injects readings/states/fixes through the
same `push_sensor_reading` / `push_gamepad_state` / `push_location_fix` channels
the real platform backends use, then asserts the manager folds them and the
`EventProvider` yields `SensorChanged` / `GamepadInput`. See
[e2e-testing](../debugging/e2e-testing.md). The pen path is exercised via
`GestureAndDragManager::update_pen_state` -> `get_pen_state`.

## See also

- [events](../events.md) - the event-filter + dispatch model.
- [callbacks](../events/callbacks.md) - `CallbackInfo` accessors + `RefAny`.
- [Realtime Media and Devices](realtime-media.md) - camera/mic capture.

---

### Doc Guide En System File Dialogs (`doc/guide/en/system/file-dialogs.md`)

---
slug: system/file-dialogs
title: File Dialogs
language: en
canonical_slug: system/file-dialogs
audience: external
maturity: wip
guide_order: 121
topic_only: false
short_desc: Native open/save dialogs and folder pickers
prerequisites: [events]
tracked_files:
  - layout/src/desktop/dialogs.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T00:00:00Z
default-search-keys:
  - FileDialog
  - MsgBox
  - MsgBoxIcon
  - ColorPickerDialog
  - FileTypeList
  - OkCancel
  - YesNo
  - OptionColorU
  - OptionString
---

# File Dialogs

## Introduction

Azul ships native message boxes, file pickers, folder pickers, and a color
chooser. Each call shows the platform's native dialog chrome.

Every picker is a *request*: it returns a `RequestId` immediately and
resumes a callback you pass in once the user has answered. The same code
runs on desktop (where the dialog is modal and the callback runs right after
the requesting callback returns), on mobile (the OS picker answers through a
delegate) and in the browser (the picker is asynchronous and gated on a user
gesture). Only `MsgBox` stays synchronous, because `alert()` / `confirm()`
are.

```rust,no_run
use azul::prelude::*;

extern "C" fn on_open_clicked(data: RefAny, _info: CallbackInfo) -> Update {
    let _request = FileDialog::open_file(
        "Open a file".into(),
        OptionString::None,
        OptionFileTypeList::None,
        data,            // handed back to `on_file_picked` untouched
        on_file_picked,  // the resume callback
    );
    Update::DoNothing
}

extern "C" fn on_file_picked(_data: RefAny, _info: CallbackInfo, result: RefAny) -> Update {
    let Some(picked) = FileOpenResult::downcast(result).into_option() else {
        return Update::DoNothing;
    };
    match picked.path.into_option() {
        Some(p) => MsgBox::info(format!("You picked {}", p.as_str()).into()),
        None    => MsgBox::info("Cancelled".into()),
    };
    Update::DoNothing
}
```

The resume callback always has the same shape,
`fn(data: RefAny, info: CallbackInfo, result: RefAny) -> Update`: `data` is
what you passed to the request, `result` is the operation's result struct
type-erased into a `RefAny`, and every result struct has one static
`downcast(result)` accessor to get the typed value back.

## Types

- `MsgBox`: message boxes (`ok`, `ok_cancel`, `yes_no`, `info`).
- `FileDialog`: open, save, multi-select file pickers, and the folder
  picker.
- `ColorPickerDialog`: RGB color picker, returns `OptionColorU`.

Supporting enums:

- `MsgBoxIcon` with variants `Info`, `Warning`, `Error`, `Question`. Used by
  every `MsgBox` call.
- `OkCancel` with variants `Ok`, `Cancel`. Used by `MsgBox::ok_cancel` for the
  default-highlighted button and the return value.
- `YesNo` with variants `Yes`, `No`. Used by `MsgBox::yes_no` the same way.
- `FileTypeList` with `document_types: StringVec` and `document_descriptor`,
  used to pass `FileDialog` filters.

## Chaining requests

A resume callback is an ordinary callback, so it can issue the next request.
Reading a picked file is the common two-step chain: the picker resumes with
a path, `FilePath::read_bytes` resumes with the bytes.

```rust,no_run
use azul::prelude::*;

extern "C" fn on_open_clicked(data: RefAny, _info: CallbackInfo) -> Update {
    let filter = FileTypeList {
        document_types: StringVec::from_vec(vec![
            "png".into(),
            "jpg".into(),
            "jpeg".into(),
        ]),
        document_descriptor: "Image files".into(),
    };
    let _request = FileDialog::open_file(
        "Pick an image".into(),
        OptionString::None,
        OptionFileTypeList::Some(filter),
        data,
        on_image_picked,
    );
    Update::DoNothing
}

extern "C" fn on_image_picked(data: RefAny, _info: CallbackInfo, result: RefAny) -> Update {
    let Some(picked) = FileOpenResult::downcast(result).into_option() else {
        return Update::DoNothing;
    };
    let Some(path) = picked.path.into_option() else {
        return Update::DoNothing; // cancelled
    };
    let _request = path.read_bytes(data, on_image_bytes);
    Update::DoNothing
}

extern "C" fn on_image_bytes(_data: RefAny, _info: CallbackInfo, result: RefAny) -> Update {
    let Some(read) = FileReadBytesResult::downcast(result).into_option() else {
        return Update::DoNothing;
    };
    match read.result {
        ResultU8VecFileError::Ok(_bytes) => Update::RefreshDom,
        ResultU8VecFileError::Err(_e)    => Update::DoNothing,
    }
}
```

`OptionString::None` for `default_path` lets the OS pick a sensible starting
directory. Pass `OptionString::Some(...)` to override. In the browser a
picker only opens from a user gesture: a request issued from a timer resolves
with `path: None`.

## File pickers

The picker requests on `FileDialog`, and the result struct each one resumes
with:

```rust,ignore
fn open_file(
    title: AzString,
    default_path: OptionString,
    filter_list: OptionFileTypeList,
    data: RefAny,
    on_result: ResumeCallbackType,
) -> RequestId;                         // FileOpenResult { path: OptionFilePath }

fn open_directory(
    title: AzString,
    default_path: OptionString,
    data: RefAny,
    on_result: ResumeCallbackType,
) -> RequestId;                         // FileOpenResult (path = the directory)

fn open_multiple_files(
    title: AzString,
    default_path: OptionString,
    filter_list: OptionFileTypeList,
    data: RefAny,
    on_result: ResumeCallbackType,
) -> RequestId;                         // FileOpenMultiResult { paths: FilePathVec }

fn save_file(
    title: AzString,
    suggested_name: AzString,
    data: RefAny,
    on_result: ResumeCallbackType,
) -> RequestId;                         // SaveTargetResult { target: OptionSaveTarget }

fn save_bytes(
    suggested_name: AzString,
    mime: AzString,
    bytes: U8Vec,
) -> bool;                              // fire-and-forget export
```

Cancelling resolves with `path: None` / an empty `paths` / `target: None`.
Filters are extension-only: pass bare extensions like `"png"`, `"jpg"` (no
leading dot, no glob), plus a human-readable label. `open_directory` and
`save_file` ignore filters.

`save_file` answers with a *write target*, not a path: on desktop it is a
real path (`SaveTarget::as_path` is `Some`), in Chromium a File System
Access handle, and in Firefox / Safari a `Download` sentinel whose `as_path`
is `None`. Use it when the app has to write the same file again later.
An app that only exports a blob of bytes (a PDF, an image) should call
`save_bytes` instead: a native save dialog on desktop, a download in the
browser, and no path ever reaches the app.

## Message boxes

```rust,no_run
use azul::prelude::*;

MsgBox::ok("Saved".into(), "File written.".into(), MsgBoxIcon::Info);

let proceed = MsgBox::ok_cancel(
    "Confirm".into(),
    "Overwrite existing file?".into(),
    MsgBoxIcon::Warning,
    OkCancel::Cancel,
);
if proceed == OkCancel::Ok {
    // ...
}

let answer = MsgBox::yes_no(
    "Quit".into(),
    "Discard unsaved changes?".into(),
    MsgBoxIcon::Question,
    YesNo::No,
);

MsgBox::info("All done.".into());
```

## Color picker

```rust,no_run
use azul::prelude::*;

extern "C" fn on_pick_color(data: RefAny, _info: CallbackInfo) -> Update {
    let initial = OptionColorU::Some(ColorU { r: 37, g: 99, b: 235, a: 255 });
    let _request = ColorPickerDialog::open("Pick a color".into(), initial, data, on_color_picked);
    Update::DoNothing
}

extern "C" fn on_color_picked(_data: RefAny, _info: CallbackInfo, result: RefAny) -> Update {
    let Some(picked) = ColorPickResult::downcast(result).into_option() else {
        return Update::DoNothing;
    };
    match picked.color.into_option() {
        Some(c) => println!("rgb({}, {}, {})", c.r, c.g, c.b),
        None    => println!("cancelled"),
    }
    Update::DoNothing
}
```

The picker returns RGB only; alpha is forced to opaque. Pass
`OptionColorU::None` to start at black. In the browser `<input type=color>`
has no cancel event everywhere, so closing it without a change resolves as
`None`.

## Limitations

- All dialog calls block the calling thread until dismissed. Inside a callback
  this stops event delivery to every window in your app. For long-running
  operations triggered after the dialog closes, spawn a background thread.
- Filter format is extension-only.
- Pass one `FileTypeList` per call. If you need multiple labelled groups,
  present them in a custom in-window picker instead.

---

### Doc Guide En System Realtime Media (`doc/guide/en/system/realtime-media.md`)

---
slug: system/realtime-media
title: Realtime Media
language: en
canonical_slug: system/realtime-media
audience: external
maturity: beta
guide_order: 124
topic_only: false
short_desc: Camera/mic capture, audio playback, and streaming A/V frames to a peer (the azul-meet pattern)
prerequisites: [events/callbacks, data/background-tasks]
tracked_files:
  - layout/src/widgets/capture_common.rs
  - layout/src/widgets/microphone.rs
  - core/src/audio.rs
  - core/src/video.rs
  - dll/src/desktop/extra/audio/mod.rs
  - examples/azul-meet/src/main.rs
last_generated_rev: 754b7f00e088960c14db598f64fa200dacc28bf1
generated_at: 2026-05-21T00:00:00Z
default-search-keys:
  - MicrophoneWidget
  - AudioSink
  - OnAudioFrameCallback
  - AudioFrame
  - VideoFrame
  - CameraWidget
  - VideoEncoder
  - VideoDecoder
  - backend_name
---

# Realtime Media

## Introduction

Azul exposes camera / screen / microphone **capture** and audio **playback** as
ordinary widgets and handles - no globals, no manager singletons. Each capture
source is a "dumb widget" that owns a background worker and hands you each frame
through a callback hook; playback is a handle you keep in your own application
`State`. Tying them together is the `azul-meet` example (a loopback audio call):
capture -> hook -> serialize -> [transport] -> deserialize -> playback.

> **Transport is your choice.** Capture and playback are transport-agnostic: a
> hook hands you decoded frames and `AudioSink` plays frames you hand it, so what
> carries the bytes between peers is entirely up to you. A first-class,
> browser-and-native peer-to-peer transport (the **AzMeet** conferencing layer)
> is being designed separately; until it lands, serialize frames yourself and
> send them over whatever transport your app already uses.

The architecture follows the framework's backreference dependency-injection
pattern (see [architecture](../architecture.md)): a widget takes a `RefAny` (a
reference to your data) plus a callback, and invokes the callback with the
captured frame so you can store, process, or send it. You never reach into a
global; the data flows back to *your* state.

## Capturing video frames (camera / screen / video)

The `CameraWidget`, `ScreenCaptureWidget`, and video-playback widget share one
hook: `set_on_frame` / `with_on_frame`, invoked once per decoded frame with a
[`VideoFrame`] (`{ width, height, bytes }`, RGBA). Mount the widget anywhere in
your DOM; the capture lives as long as the node is mounted.

```rust
let camera = CameraWidget::create(CameraConfig::default())
    .with_on_frame(state.clone(), on_video_frame)
    .dom();

extern "C" fn on_video_frame(mut data: RefAny, _info: CallbackInfo, frame: VideoFrame) -> Update {
    if let Some(mut s) = data.downcast_mut::<MyState>() {
        // frame.bytes is RGBA, frame.width x frame.height. Save it, run it
        // through an effect, or encode + send it (see "Streaming frames to a peer").
        s.last_frame_bytes = frame.bytes.len();
    }
    Update::RefreshDom
}
```

The widget renders a GPU-texture preview itself; your hook is purely a data tap.

### One capture, many sizes (consumers)

The preview tile, a remote participant and a recorder rarely want the same
size. Register each as a **consumer**: the device is opened ONCE at the
smallest size that covers everyone (the tile's own device-pixel size, which
the widget learns from layout, plus every consumer), and every captured frame
is cut to each consumer's size off the main thread — so the camera never
captures more than the largest consumer can use, and nothing is sent bigger
than it was asked for.

```rust
// "Client Bob wants 500x200; the local preview is 100x200": the camera is
// opened at 500x200 and each frame is sampled twice, from one capture.
let camera = CameraWidget::create(CameraConfig::default())
    .with_consumer(FrameConsumer::create(BOB, 500, 200))
    .with_on_consumer_frame(state.clone(), on_consumer_frame)
    .dom();

extern "C" fn on_consumer_frame(mut data: RefAny, _info: CallbackInfo, cut: ConsumerFrame) -> Update {
    // cut.consumer.id tells you who this is for; cut.frame is RGBA at
    // cut.consumer.width x cut.consumer.height.
    if cut.consumer.id == BOB {
        send_to_bob(&cut.frame);
    }
    Update::DoNothing
}
```

`on_frame` still receives the frame as captured (at the configured size or
larger) when you set it; with consumers alone the full-size frame never
leaves the capture thread. The same applies to `ScreenCaptureWidget`, whose
request also honours `config.source` (display index / window id),
`config.fps`, and leaves your own windows out of a shared desktop so a share
that shows the meeting window does not loop.

On macOS the per-consumer cut runs through Accelerate/vImage; elsewhere the
portable scaler (`image_scale`) does the same work. Both are pure functions of
the frame, so the cuts can run in parallel later without an API change.

## Capturing audio (microphone)

`MicrophoneWidget` is the audio twin of the capture widgets - same shape, no GL.
It mounts an invisible node, starts a capture thread on mount, and calls your
`on_frame` hook with each [`AudioFrame`] (`{ sample_rate, channels, samples }`,
interleaved `f32`).

```rust
let mic = MicrophoneWidget::create(AudioConfig { sample_rate: 48_000, channels: 1 })
    .with_on_frame(state.clone(), on_audio_frame)
    .dom();

extern "C" fn on_audio_frame(mut data: RefAny, _info: CallbackInfo, frame: AudioFrame) -> Update {
    if let Some(mut s) = data.downcast_mut::<MyState>() {
        s.captured += frame.frame_count();
    }
    Update::RefreshDom
}
```

## Playing audio (`AudioSink`)

Playback is a handle, not a widget - you usually play audio you *received*, not
audio bound to a node. `AudioSink` follows the same C-ABI handle convention as
`Db` / `Pdf`: open it, keep it in your `State`, feed it frames, drop it to stop.

```rust
let sink = AudioSink::open(AudioConfig { sample_rate: 48_000, channels: 1 });
// ... later, for each frame you want to hear:
sink.play(frame);            // queues the samples to the output
// sink.is_open(), sink.frames_played(), sink.close()
```

## Streaming frames to a peer

Capture hands you a decoded frame; playback takes a frame. The only thing between
two peers is **your serialization + a transport of your choice**. A frame becomes
bytes, the bytes travel, and the far side turns them back into a frame:

```rust
// on_frame: capture -> serialize -> send over your transport
let bytes = frame_to_bytes(&frame);
s.transport.send(s.peer.clone(), bytes);

// recv (Timer tick or worker): receive -> deserialize -> play
while let Some(bytes) = s.transport.poll_recv() {
    if let Some(frame) = bytes_to_frame(bytes.as_ref()) {
        s.sink.play(frame);
    }
}
```

`frame_to_bytes` / `bytes_to_frame` are yours (a length-prefixed struct, or the
encoded codec bytes from `VideoEncoder` below). For a full keyframe that exceeds
the network MTU you chunk it into sequenced messages and reassemble on the far
side; a few-KB audio frame fits in a single message.

### The transport seam

`s.transport` above is deliberately abstract. The realtime-media APIs stop at the
**serialize/deserialize seam** so you can drop in whatever moves bytes between
peers — and the trade-offs there (raw datagrams vs. congestion-controlled QUIC,
direct peer-to-peer vs. relayed, native-only vs. also-in-the-browser) are exactly
what the **AzMeet** conferencing transport is being designed to standardize.
Until that ships as a first-class API, wire the seam to your own transport.

## Putting it together: the azul-meet pattern

`examples/azul-meet` wires the full loop as a **loopback** call (it sends to
itself, so the whole round-trip runs on one machine, no network required):

1. A `MicrophoneWidget` captures audio; its `on_frame` serializes the
   `AudioFrame` and sends it to the peer.
2. A recv `Timer` drains the transport, deserializes each message back into an
   `AudioFrame`, and `AudioSink::play`s it.

A real two-party call is the same code with `peer` set to the remote endpoint.
See `examples/azul-meet/src/main.rs` for the complete app (serialization +
Timer + State).

## What is on-device

The widget/handle surfaces above are cross-platform and always present. The
actual hardware backends are platform-specific and only run on a real device:

- **Capture** (camera, screen, microphone): AVFoundation / ScreenCaptureKit /
  AVAudioEngine on Apple, Camera2 / MediaProjection / AAudio on Android. The
  current desktop builds use stand-in workers (a test pattern / test tone) so
  the API + plumbing are exercisable without hardware.
- **Audio output** (`AudioSink`): rodio/cpal on desktop, AVAudioEngine / AAudio
  on mobile.
- **Video encode/decode** (`VideoEncoder` / `VideoDecoder`), submit + poll:
  `VideoEncoder::open(w, h, h265, bitrate_kbps)` -> `encode(VideoFrame, force_keyframe)
  -> bool` (accepted) then drain `recv_packet() -> Option<U8Vec>`;
  `VideoDecoder::open(h265)` -> `decode(bytes) -> bool` then drain
  `recv_frame() -> Option<VideoFrame>`. Hardware and browser (WebCodecs)
  codecs are output-callback shaped, so one submitted frame can yield zero
  or several packets, possibly later - poll from a timer.
  `VideoEncoder::backend_name()` reports the platform-native codec the build
  selects: **gpu-video** (Vulkan Video) on Linux/Windows desktop, **VideoToolbox**
  on Apple (Vulkan Video can't build there - no MoltenVK video), **MediaCodec**
  on Android. The handles + the selection are exposed cross-platform; the codec
  FFI itself is the on-device part. Use these at the `azul-meet`
  serialize/deserialize seam (your transport carries the encoded bytes).

## Testing without hardware

The synthetic-event harness (`layout/tests/synthetic_events.rs`) injects
sensor / gamepad / geolocation / audio / video events through the same channels
a real device uses, so you can exercise the capture + event paths in CI. See
[e2e-testing](../debugging/e2e-testing.md).

## See also

- [callbacks](../events/callbacks.md) - the hook + `RefAny` mechanism.
- [background-tasks](../data/background-tasks.md) - the `Thread` that drives capture.
- [timers](../animations/timers.md) - polling your transport for received frames each frame.
- [Mobile](../deploying/mobile.md) - shipping this on iOS / Android.

---

### Doc Guide En System Windowing (`doc/guide/en/system/windowing.md`)

---
slug: system/windowing
title: Windows & Menus
language: en
canonical_slug: system/windowing
audience: external
maturity: mature
guide_order: 120
topic_only: false
short_desc: Windows, menus, decorations, and per-window state
prerequisites: [hello-world, events]
tracked_files:
  - core/src/window.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:54:43Z
default-search-keys:
  - WindowCreateOptions
  - WindowFlags
  - FullWindowState
  - WindowDecorations
  - WindowFrame
  - WindowType
  - WindowBackgroundMaterial
  - Monitor
  - Menu
  - MenuItem
  - StringMenuItem
  - CallbackInfo
  - Titlebar
---

# Windows & Menus

## Overview

A window is created by passing a `WindowCreateOptions` to `App::run` (the first window) or to `CallbackInfo::create_window` (subsequent windows). The framework manages a window's lifecycle, decorations, menus, and per-window state; your code describes what should happen via the layout callback and event handlers.

```rust,no_run
use azul::prelude::*;

struct App;

extern "C" fn layout(_: RefAny, _: LayoutCallbackInfo) -> Dom {
    Dom::create_body()
}

fn main() {
    let app = App::create(RefAny::new(App), AppConfig::create());
    app.run(WindowCreateOptions::create(layout));
}
```

## Configuring the first window

`WindowCreateOptions::create(layout_callback)` returns a struct with sensible defaults: a 640×480 window, light theme, smooth scrolling, native menus on Windows/macOS. Tweak via the public fields:

```rust,no_run
use azul::prelude::*;

extern "C" fn layout(_: RefAny, _: LayoutCallbackInfo) -> Dom {
    Dom::create_body()
}

let mut win = WindowCreateOptions::create(layout);
win.window_state.title = "My App".into();
win.window_state.size.dimensions = LogicalSize::new(1024.0, 768.0);
win.window_state.flags.is_resizable = true;
win.size_to_content = false;
```

`WindowCreateOptions` public fields:

- `window_state: FullWindowState` — initial state (size, title, flags, ...).
- `create_callback: OptionCallback` — optional fn called once after window opens.
- `renderer: OptionRendererOptions` — VSync, sRGB, hardware accel.
- `theme: OptionWindowTheme` — light/dark override.
- `size_to_content: bool` — resize to fit first layout (default false).
- `hot_reload: bool` — CSS hot-reload on file change.

`FullWindowState` carries title, size, flags, position, theme, and the layout callback. The framework keeps it in sync with the OS as the user resizes and moves the window.

## Window flags

`WindowFlags` groups boolean window properties. The defaults match a normal application window:

- `frame: WindowFrame` — `Normal`, `Minimized`, `Maximized`, or `Fullscreen`.
- `decorations: WindowDecorations` — controls the title bar (see below).
- `background_material: WindowBackgroundMaterial` — compositor blur/transparency.
- `window_type: WindowType` — `Normal`, `Menu`, `Tooltip`, or `Dialog`.
- `is_visible`, `is_resizable`, `has_focus` — standard toggles.
- `is_always_on_top`, `smooth_scroll_enabled`, `autotab_enabled`.
- `use_native_menus`, `use_native_context_menus` — default to true on Windows/macOS, false on Linux.
- `is_top_level`, `prevent_system_sleep`.
- `fullscreen_mode: FullScreenMode` — animation for fullscreen transitions.
- `has_decorations` — opt-in for client-side decorations.
- `close_requested` — set by the OS, cleared by your callback to veto a close.

To toggle a flag at runtime, modify the window state from inside a callback:

```rust,no_run
use azul::prelude::*;

extern "C" fn toggle_fullscreen(_: RefAny, mut info: CallbackInfo) -> Update {
    let mut state = info.get_current_window_state().clone();
    state.flags.frame = match state.flags.frame {
        WindowFrame::Fullscreen => WindowFrame::Normal,
        _ => WindowFrame::Fullscreen,
    };
    info.modify_window_state(state);
    Update::DoNothing
}
```

## Decorations and CSD

`WindowDecorations` controls the title bar:

- `Normal` — full native title bar with controls.
- `NoTitle` — native frame, but no title text. App must draw its own.
- `NoTitleAutoInject` — same as `NoTitle`, but the framework prepends a styled title bar to your DOM.
- `NoControls` — title bar present, controls (min/max/close) hidden.
- `None` — borderless. Combine with `has_decorations: true` for full client-side decorations.

Client-side decorations are mandatory on Wayland (no native protocol), opt-in on Windows/macOS/X11. With `decorations: WindowDecorations::None` and `has_decorations: true`, the framework lets you draw the title bar yourself; mark a node with the class `__azul-native-titlebar` to make dragging it move the window.

`WindowDecorations::NoTitleAutoInject` is the easy path: the framework injects a default title bar that respects the system style and handles drag automatically.

`WindowFrame` controls the frame state. Setting `frame` and calling `modify_window_state` performs the corresponding OS-level transition. On macOS, the speed of the fullscreen transition is controlled by `WindowFlags.fullscreen_mode` (`SlowFullScreen` plays the system animation; `FastFullScreen` is instant).

## Background materials

`WindowBackgroundMaterial` selects the platform compositor's blur/transparency effect:

- `Opaque`. Default on both macOS and Windows.
- `Transparent`. Translucent with no blur on both platforms.
- `Sidebar`. Sidebar material on macOS, light acrylic on Windows.
- `Menu`. Menu material on macOS, acrylic on Windows.
- `HUD`. HUD material on macOS, dark acrylic on Windows.
- `Titlebar`. Titlebar material on macOS, mica on Windows.
- `MicaAlt`. Equivalent to `Titlebar` on macOS, mica alt on Windows.

X11 and Wayland ignore this field. To use a transparent material, also set the layout body's CSS `background-color` to a translucent value.

## Multiple windows

From inside any callback, push a new `WindowCreateOptions`:

```rust,no_run
use azul::prelude::*;

extern "C" fn child_layout(_: RefAny, _: LayoutCallbackInfo) -> Dom {
    Dom::create_body()
}

extern "C" fn open_settings(mut data: RefAny, mut info: CallbackInfo) -> Update {
    let mut win = WindowCreateOptions::create(child_layout);
    win.window_state.title = "Settings".into();
    win.window_state.size.dimensions = LogicalSize::new(400.0, 300.0);
    info.create_window(win);
    Update::DoNothing
}
```

The new window opens after the callback returns. The two windows share the same `RefAny` if you pass it to both layout callbacks, or each window can carry its own data.

The application stays alive as long as at least one window is open; closing the last window ends the event loop and `App::run` returns.

To close the current window from a callback:

```rust,ignore
info.close_window();
```

To intercept the close button, set `FullWindowState.close_callback` on the state. Returning `Update::DoNothing` and clearing `flags.close_requested` keeps the window open:

```rust,no_run
use azul::prelude::*;

extern "C" fn on_close(_: RefAny, mut info: CallbackInfo) -> Update {
    let mut state = info.get_current_window_state().clone();
    state.flags.close_requested = false;       // veto the close
    info.modify_window_state(state);
    Update::DoNothing
}
```

## Menus

`Menu` is a tree of `MenuItem`s, used for both menu bars and context menus:

```rust,no_run
use azul::prelude::*;

struct State;

extern "C" fn on_open(_: RefAny, _: CallbackInfo) -> Update {
    Update::DoNothing
}

extern "C" fn on_quit(_: RefAny, _: CallbackInfo) -> Update {
    Update::DoNothing
}

let data: RefAny = RefAny::new(State);

let mut open_item = StringMenuItem::create("Open…".into());
open_item.callback = Some(CoreMenuCallback { callback: on_open.into(), refany: data.clone() }).into();

let mut quit_item = StringMenuItem::create("Quit".into());
quit_item.callback = Some(CoreMenuCallback { callback: on_quit.into(), refany: data }).into();

let file_menu = Menu::create(vec![
    MenuItem::String(open_item),
    MenuItem::Separator,
    MenuItem::String(quit_item),
].into());
```

`MenuItem` has three variants: `String(StringMenuItem)`, `Separator`, and `BreakLine` (for horizontal layouts).

`StringMenuItem` is built via `StringMenuItem::create(label)`, then populated through public fields:

- `accelerator: OptionVirtualKeyCodeCombo`
- `callback: OptionCoreMenuCallback`
- `children: MenuItemVec`
- `icon: OptionMenuItemIcon`
- `menu_item_state: MenuItemState` — `Normal`, `Disabled`, or `Greyed`.

```rust,ignore
let mut item = StringMenuItem::create("Bold".into());
item.accelerator = Some(VirtualKeyCodeCombo {
    keys: vec![VirtualKeyCode::LControl, VirtualKeyCode::B].into()
}).into();
item.menu_item_state = MenuItemState::Greyed;
item.icon = Some(MenuItemIcon::Checkbox(true)).into();
```

### Menu bars

Attach a `Menu` to the body of your DOM:

```rust,no_run
use azul::prelude::*;

fn build_menu() -> Menu {
    Menu::create(Vec::<MenuItem>::new().into())
}

let mut body = Dom::create_body().with_menu_bar(build_menu());
```

The framework uses native menus where supported (Windows HMENU, macOS NSMenu) when `WindowFlags.use_native_menus` is true; otherwise it renders a borderless azul popup window.

### Context menus

Attach a context menu to any node:

```rust,no_run
use azul::prelude::*;

fn build_menu() -> Menu {
    Menu::create(Vec::<MenuItem>::new().into())
}

let mut node = Dom::create_div().with_context_menu(build_menu());
```

The default trigger is right-click; change `Menu.context_mouse_btn` (`ContextMenuMouseButton::Right | Left | Middle`) for other triggers.

To open a menu programmatically (e.g. from a hamburger button click):

```rust,no_run
use azul::prelude::*;

fn build_menu() -> Menu {
    Menu::create(Vec::<MenuItem>::new().into())
}

extern "C" fn on_click(_: RefAny, mut info: CallbackInfo) -> Update {
    info.open_menu(build_menu());                                 // at cursor
    info.open_menu_at(build_menu(), LogicalPosition { x: 100.0, y: 50.0 });
    Update::DoNothing
}
```

`MenuPopupPosition` controls placement relative to the cursor or the clicked element (`BottomLeftOfCursor`, `TopOfHitRect`, `AutoCursor`, …). Set via `Menu::with_popup_position(...)` or `Menu::set_popup_position(...)`.

## Window types

`WindowType` tells the OS what kind of window this is, which affects taskbar/dock visibility, focus behaviour, and whether the window auto-closes on focus loss:

- `Normal` — standard application window.
- `Menu` — always-on-top, frameless, auto-closes on focus loss.
- `Tooltip` — always-on-top, no input.
- `Dialog` — blocks parent window (modal).

Set `window_state.flags.window_type` before passing to `create_window`.

## Tooltips

`CallbackInfo` exposes:

```rust,ignore
info.show_tooltip("Save the file".into());                            // at cursor
info.show_tooltip_at("...".into(), LogicalPosition { x: 100., y: 0. });
info.hide_tooltip();
```

Hover-triggered tooltips are automatic for any node with `aria-label`, `alt`, or `title` attributes. The framework starts a timer and shows the tooltip when it fires.

## Window state in callbacks

Read from `CallbackInfo`:

```rust,ignore
info.get_current_window_state()    // &FullWindowState
info.get_current_window_flags()    // WindowFlags
info.get_current_keyboard_state()  // KeyboardState
info.get_current_mouse_state()     // MouseState
```

Write via `info.modify_window_state(new_state)`. The framework diffs against the previous state and applies changes (moving the window, resizing, toggling flags) after the callback returns.

## Monitors

`Monitor` describes a connected display. Window-DPI changes (`WindowEventFilter::DpiChanged`) and monitor changes (`WindowEventFilter::MonitorChanged`) fire when the user drags a window to another display.

Monitor-connected and monitor-disconnected events are application-scoped (`ApplicationEventFilter::MonitorConnected` / `MonitorDisconnected`).

## Window icons and taskbar

`WindowIcon` and `TaskBarIcon` are RGBA byte buffers. Set them on the platform-specific options:

```rust,ignore
win.window_state.platform_specific_options.windows_options.window_icon =
    Some(WindowIcon::Small(SmallWindowIconBytes { ... })).into();
```

On X11, set `LinuxWindowOptions.window_icon`. macOS uses the icon from the `.app` bundle (no per-window override).

## Platform-specific options

`PlatformSpecificOptions` groups the four platforms:

- `windows_options: WindowsWindowOptions` — parent HWND, redirection bitmap, taskbar icon.
- `linux_options: LinuxWindowOptions` — X11/Wayland app ID, WM_CLASS, theme.
- `mac_options: MacWindowOptions` — reserved.
- `wasm_options: WasmWindowOptions` — reserved.

Linux options carry X11-specific hints (`x11_window_types`, `x11_wm_classes`, `x11_resize_increments`) and Wayland-specific identifiers (`wayland_app_id`, `wayland_theme`). Most apps don't need these — the defaults work.

## Common errors

- **Window opens off-screen** — `window_state.position` was set to a `WindowPosition::Initialized(...)` outside any monitor's bounds. Set to `Uninitialized` to let the OS place it.
- **Title bar disappears** — `decorations: WindowDecorations::None` without `has_decorations: true` produces a borderless window with no way to drag. Either set `has_decorations: true` (and draw your own title bar) or use `Normal` / `NoTitleAutoInject`.
- **Native menus on Linux** — `use_native_menus` defaults to false on Linux because Linux has no universal menu protocol. The framework renders a borderless azul popup instead.
- **Close button does nothing** — you registered a close callback that returned `Update::DoNothing` but didn't clear `flags.close_requested`. Clear it explicitly to veto the close.

---

### Doc Guide En Widgets Inputs (`doc/guide/en/widgets/inputs.md`)

---
slug: widgets/inputs
title: Input Widgets
language: en
canonical_slug: widgets/inputs
audience: external
maturity: wip
guide_order: 111
topic_only: false
short_desc: Text fields, checkboxes, radios, sliders, dropdowns
prerequisites: [widgets, events/text-input, events/text-selection]
tracked_files:
  - layout/src/widgets/text_input.rs
  - layout/src/widgets/number_input.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:54:43Z
default-search-keys:
  - TextInput
  - TextInputState
  - TextInputStateWrapper
  - TextInputValid
  - TextInputSelection
  - TextInputSelectionRange
  - OnTextInputReturn
  - NumberInput
  - NumberInputState
  - Button
  - ColorInput
  - Update
---

# Input Widgets

## Overview

*WIP.* Text input is functional with cursor, placeholder, and focus callbacks; the widget surface described here is stable.

`TextInput` and `NumberInput` are the two ways to collect typed user input. Both
render as a single-line box, both raise the same lifecycle events, and
`NumberInput` is implemented as a thin wrapper that adds numeric validation on
top of `TextInput`.

## TextInput

`TextInput` is the one-line text field. State and callbacks live together in
the widget value:

```rust,ignore
pub struct TextInput {
    pub text_input_state: TextInputStateWrapper,
    pub placeholder_style: CssPropertyWithConditionsVec,
    pub container_style:   CssPropertyWithConditionsVec,
    pub label_style:       CssPropertyWithConditionsVec,
}

pub struct TextInputState {
    pub text:        U32Vec,
    pub placeholder: OptionString,
    pub max_len:     usize,
    pub selection:   OptionTextInputSelection,
    pub cursor_pos:  usize,
}
```

The text buffer is stored as `U32Vec` (Unicode scalar values cast to `u32`) so
cursor positions are character-based and survive multi-byte UTF-8 edits without
re-indexing.

### Building one

```rust,no_run
use azul::prelude::*;

let dom = TextInput::create()
    .with_text("hello".into())
    .with_placeholder("type something...".into())
    .dom();
```

`create()` returns a `TextInput` with platform defaults. `with_text` replaces
the buffer; `set_text` is its mutable counterpart.

### Three callbacks

`TextInput` exposes three callbacks.

- `on_text_input` fires when a printable character is typed. It returns
  `OnTextInputReturn { update, valid }`.
- `on_virtual_key_down` fires when a non-printable key (arrows, Enter, Esc) is
  pressed. It returns `OnTextInputReturn { update, valid }`.
- `on_focus_lost` fires when focus moves away. It returns `Update`.

`OnTextInputReturn { valid: TextInputValid::No }` rejects the just-typed
character. The framework rolls the buffer back to the pre-edit state and
re-renders. `Yes` keeps the edit.

```rust,no_run
use azul::prelude::*;

extern "C" fn validate(
    _data: RefAny, _info: CallbackInfo, state: TextInputState,
) -> OnTextInputReturn {
    let valid = if state.get_text().chars().all(|c| c.is_alphanumeric()) {
        TextInputValid::Yes
    } else {
        TextInputValid::No
    };
    OnTextInputReturn { update: Update::DoNothing, valid }
}

let dom = TextInput::create()
    .with_on_text_input(RefAny::new(()), validate)
    .dom();
```

By default, the framework writes the new character into the buffer before
`on_focus_lost` and `on_virtual_key_down` fire. Set
`update_text_input_before_calling_focus_lost_fn = false` if your callback needs
to compare against the old buffer.

### Cursor and selection

`cursor_pos` is the character index. `selection` is `OptionTextInputSelection`,
which is either `All` or a `FromTo` range carrying `dir_from` and `dir_to`.

### Styling

The default style is a white box with a thin border that turns blue on focus.
Three slots accept CSS replacements.

- `container_style` targets the outer wrapper (border, padding, background).
- `label_style` targets the actual text node.
- `placeholder_style` targets the placeholder text shown when empty.

## NumberInput

`NumberInput` wraps `TextInput` and adds `f32` validation. Construction takes
the initial value:

```rust,no_run
use azul::prelude::*;

let dom = NumberInput::create(42.0).dom();
```

### State

```rust,ignore
pub struct NumberInputState {
    pub previous: f32,
    pub number:   f32,
    pub min:      f32,
    pub max:      f32,
}
```

`previous` is set to the prior `number` whenever a successful edit commits, so
the value-change callback can compute deltas without storing its own history.

> **Default min:** the constructor leaves `min` at `0.0`. Set it to a negative
> value if your input must accept negative numbers.

### Callbacks

`NumberInput` exposes its own value-change and focus-lost hooks, plus the
underlying `TextInput`'s text-input and virtual-key-down hooks.

- `with_on_value_change(data, fn)` fires after a typed string parses
  successfully as `f32` and lies within `[min, max]`.
- `with_on_focus_lost(data, fn)` fires when the input loses focus, regardless
  of validity.
- `with_on_text_input(data, fn)` is forwarded to the underlying `TextInput`.
- `with_on_virtual_key_down(data, fn)` is forwarded to the underlying
  `TextInput`.

```rust,no_run
use azul::prelude::*;

extern "C" fn on_change(
    _d: RefAny, 
    _i: CallbackInfo, 
    state: NumberInputState,
) -> Update {
    let _ = state.number - state.previous;
    Update::DoNothing
}

let dom = NumberInput::create(50.0)
    .with_on_value_change(RefAny::new(()), on_change)
    .dom();
```

Invalid input is rejected by the underlying `TextInput`'s `on_text_input`
callback returning `TextInputValid::No`, so the buffer never enters an
unparseable state.

## Picking between them

- For free-form text, names, or search, use `TextInput`.
- For numbers with a known range, use `NumberInput`.
- For typed URLs, emails, or regex-validated input, use `TextInput` with a
  custom `on_text_input` returning `TextInputValid::No` for invalid characters.
- For a date or time, build one out of three `NumberInput`s; there's no
  date/time widget yet.

---

### Doc Guide En Widgets Maps (`doc/guide/en/widgets/maps.md`)

---
slug: widgets/maps
title: Maps
language: en
canonical_slug: widgets/maps
audience: external
maturity: beta
guide_order: 113
topic_only: false
short_desc: A slippy map widget with pan/zoom, tap-to-pin hooks, and lat/lon projection
prerequisites: [widgets, events/callbacks]
tracked_files:
  - layout/src/widgets/map.rs
  - examples/azul-maps/src/main.rs
last_generated_rev: 47525d4250000000000000000000000000000000
generated_at: 2026-05-21T00:00:00Z
default-search-keys:
  - MapWidget
  - MapViewport
  - MapLatLon
  - MapTileLayer
  - latlon_at_px
  - px_at_latlon
  - on_pin_tap
  - on_viewport_changed
  - on_mount
  - MapSetup
---

# Maps

## Introduction

`MapWidget` is a slippy-map widget: a Web-Mercator tile grid you can pan and
zoom, with hooks for tap-to-pin and viewport changes, and projection helpers to
place your own markers. It is the AzulMaps (P3) goal app. Like the other
widgets it owns its state in a `RefAny` dataset that survives relayout via a
merge callback - you just `create` it, configure it, and call `.dom()`.

## Creating a map

```rust
use azul::widgets::{MapWidget, MapViewport, MapTileLayer};

let map = MapWidget::create(MapTileLayer::default())
    .with_viewport(MapViewport {
        centre_lat_deg: 37.7749,   // San Francisco
        centre_lon_deg: -122.4194,
        zoom: 11.0,
        bearing_deg: 0.0,
        pitch_deg: 0.0,
    })
    .dom();
```

`MapTileLayer` configures the tile source (and attribution). The widget computes
the visible XYZ tile grid from the viewport and renders one GPU-translated tile
div per tile. **Pan (drag) and pinch-zoom are handled by the widget itself** -
no wiring needed.

## Hooks: observing taps and the viewport

Two backreference-DI hooks (see [architecture](../architecture.md)) let your app
react without globals - the callback receives the data and writes back to *your*
state:

- `with_on_pin_tap(data, cb)` - fires when the user taps the map (a press +
  release with no drag), with the tapped [`MapLatLon`].
- `with_on_viewport_changed(data, cb)` - fires on pan/zoom, with the new
  [`MapViewport`] (persist it, sync a minimap, etc.).

```rust
use azul::dom::MapPinTapCallback;
use azul::option::OptionRefAny;

let map = MapWidget::create(layer)
    .with_viewport(viewport)
    .with_on_pin_tap(
        state.clone(),
        MapPinTapCallback { cb: on_pin_tap, callable: OptionRefAny::None },
    )
    .dom();

extern "C" fn on_pin_tap(mut data: RefAny, _info: CallbackInfo, coord: MapLatLon) -> Update {
    if let Some(mut s) = data.downcast_mut::<MyState>() {
        s.pins.push((coord.lat_deg, coord.lon_deg));   // drop a pin
    }
    Update::RefreshDom
}
```

The widget does the tap-vs-drag detection and the projection for you, so you
just store the lat/lon.

## Projection: placing your own markers

To draw markers/overlays on top of the map, convert between lat/lon and screen
pixels with the two static helpers (small-angle Web Mercator):

```rust
// lat/lon -> screen position (place a pin div here):
let p = MapWidget::px_at_latlon(viewport, MapLatLon { lat_deg, lon_deg }, container_size);

// screen position -> lat/lon (inverse):
let coord = MapWidget::latlon_at_px(viewport, px, container_size);
```

`container_size` is the map node's pixel size (e.g. from
`CallbackInfo::get_hit_node_rect().size`). Re-projecting each layout makes your
pins track the map as it pans and zooms. `examples/azul-maps` uses exactly this
to render dropped pins + a coordinate readout.

## Sharing connections and threads

By default each tile opens its own connection and downloads on its own thread.
That is fine for a map shown now and then. An app that keeps a map open, or
shows several, can give the maps a shared `HttpClient` (open connections to the
tile server are reused instead of a TLS handshake per tile) and a shared
`ThreadPool` (a fixed number of download threads).

That is configuration, so it does not belong in `layout()`. Create the pools
once with your app state, and hand them to the map in its `on_mount` hook. The
hook runs when the map enters the tree, receives the map's current `MapSetup`
and returns the one to use:

```rust
use azul::dom::MapMountCallback;
use azul::http::{HttpClient, HttpClientConfig};
use azul::task::ThreadPool;
use azul::widgets::MapSetup;

struct MyState {
    viewport: MapViewport,
    tiles: HttpClient,     // HttpClient::create(HttpClientConfig::create())
    workers: ThreadPool,   // ThreadPool::create(4)
}

// in layout(): UI only
let map = MapWidget::create(layer)
    .with_viewport(viewport)
    .with_on_mount(
        state.clone(),
        MapMountCallback { cb: on_map_mount, callable: OptionRefAny::None },
    )
    .dom();

extern "C" fn on_map_mount(mut data: RefAny, _info: CallbackInfo, setup: MapSetup) -> MapSetup {
    let Some(s) = data.downcast_ref::<MyState>() else { return setup };
    setup
        .with_http_client(s.tiles.clone())
        .with_thread_pool(s.workers.clone())
        .with_max_in_flight(8)   // at most 8 tiles downloading at once
}
```

The map keeps its setup through every relayout (the merge callback carries it)
and drops its clones when it leaves the tree. If the map is mounted again, the
hook runs again with the setup it returned last time. `examples/c/async.c`
shows the same flow in C.

## Geolocation

To show "you are here", drop a `Dom::create_geolocation_probe(...)` into the map
subtree and read the fix with `CallbackInfo::get_location_fix` - see
[Device Input](../system/device-input.md#geolocation).

## Notes

- Tile fetch + MVT decode + MapCSS-to-SVG are behind the `map-tiles` Cargo
  feature; with it off, the grid + viewport bookkeeping still work (the demo
  shows the projection math without live tiles).
- The tile cache lives in the widget's dataset `RefAny` and is carried across
  relayout by a `DatasetMergeCallback`, so panning doesn't refetch everything.

## See also

- [widgets](..md) - the widget model.
- [callbacks](../events/callbacks.md) - the hook + `RefAny` mechanism.
- [Device Input](../system/device-input.md) - geolocation for the "you are here" dot.

---

### Doc Guide En Widgets Structural (`doc/guide/en/widgets/structural.md`)

---
slug: widgets/structural
title: Structural Widgets
language: en
canonical_slug: widgets/structural
audience: external
maturity: wip
guide_order: 112
topic_only: false
short_desc: Panels, splitters, tab views, list views, tree views
prerequisites: [widgets]
tracked_files:
  - layout/src/widgets/list_view.rs
  - layout/src/widgets/ribbon.rs
  - layout/src/widgets/tabs.rs
  - layout/src/widgets/tree_view.rs
last_generated_rev: 7ecd570e4c0c3584e5107e770058c16cb59fa6e7
generated_at: 2026-05-02T05:54:43Z
default-search-keys:
  - TabHeader
  - TabHeaderState
  - ListView
  - TreeView
  - TreeViewNode
  - Ribbon
  - RibbonSection
  - RefAny
  - Update
---

# Structural Widgets

## Overview

*WIP.* These widgets render today, but several have known gaps — `ListView` does not yet wire its column or row callbacks into the DOM, so treat their APIs as provisional. `TabHeader` and `TreeView` are usable as documented.

The structural widgets are containers. They take other widgets or DOMs as
content and lay them out in tabbed, hierarchical, or tabular forms.

## Tabs

`TabHeader` is the clickable bar of labels. Compose it with your own content
DOM in a column flexbox so the header stays a fixed height and the content
fills the rest.

```rust,no_run
use azul::prelude::*;
extern "C" 
fn on_tab(
    _d: RefAny, 
    _i: CallbackInfo, 
    _s: TabHeaderState,
) -> Update {
    Update::RefreshDom
}

let labels = StringVec::from_vec(vec![
    "General".into(), 
    "Advanced".into()
]);
let dom = Dom::create_div()
    .with_child(
        TabHeader::create(labels)
            .with_active_tab(0)
            .with_on_click(RefAny::new(()), on_tab)
            .dom()
    )
    .with_child(Dom::create_text_do_not_use_without_block_level_wrapper("Settings go here."));
```

`TabHeader` keeps `active_tab: usize` and emits `TabHeaderState { active_tab }`
on click. The active state is driven externally; the widget does not toggle
itself. Your callback rebuilds the DOM with a new `active_tab`.

## TreeView

`TreeView` renders a hierarchy where each node has a label, an optional list
of children, and per-node expanded/selected flags.

```rust,ignore
pub struct TreeViewNode {
    pub label:       AzString,
    pub children:    TreeViewNodeVec,
    pub is_expanded: bool,
    pub is_selected: bool,
}
```

Build the data with the chained `with_*` builders and convert in one call.

```rust,no_run
use azul::prelude::*;
extern "C" 
fn on_click(_d: RefAny, _i: CallbackInfo, _idx: usize) -> Update {
    Update::RefreshDom
}

let root = TreeViewNode::create("Project".into())
    .with_expanded(true)
    .with_child(TreeViewNode::create("src".into()).with_expanded(true)
        .with_child(TreeViewNode::create("main.rs".into()))
        .with_child(TreeViewNode::create("lib.rs".into())))
    .with_child(TreeViewNode::create("Cargo.toml".into()));

let dom = TreeView::create(root)
    .with_on_node_click(RefAny::new(()), on_click)
    .dom();
```

The `usize` argument to the click callback is the depth-first preorder index
of the clicked node, not a node ID. Map it back to your data by walking the
tree in the same order.

`is_expanded` and `is_selected` are read-only outputs of the widget. To toggle,
store the tree in your `RefAny`, mutate it inside the callback, and return
`Update::RefreshDom`.

## ListView

`ListView` renders a tabular list with column headers and rows. The data
shape:

```rust,ignore
pub struct ListView {
    pub columns:        StringVec,
    pub rows:           ListViewRowVec,
    pub sorted_by:      OptionUsize,
    pub scroll_offset:  PixelValueNoPercent,
    pub content_height: OptionPixelValueNoPercent,
    pub column_context_menu: OptionMenu,
    pub on_lazy_load_scroll: OptionListViewOnLazyLoadScroll,
    pub on_column_click:     OptionListViewOnColumnClick,
    pub on_row_click:        OptionListViewOnRowClick,
}
```

The widget supports lazy-loading. Pass an explicit `content_height` to size the
scroll region for unloaded rows, and use `on_lazy_load_scroll` to fetch more
data as the user scrolls.

> **Known limitation.** The current `dom()` does not yet attach
> `on_column_click`, `on_row_click`, `on_lazy_load_scroll`, or
> `column_context_menu` to event filters in the produced DOM. Configure them
> now to keep your call sites stable, but plan to handle clicks via external
> event filters until the widget wires them through.

## Ribbon

`Ribbon` is an Office-style ribbon: a tab bar where each tab contains one or
more titled `RibbonGroup`s. Groups separate themselves with vertical
dividers and stack their title below the content.

```rust,no_run
use azul::prelude::*;

let home = RibbonTab::create("Home".into())
    .with_group(RibbonGroup::create("Clipboard".into()).with_item(clipboard_item()))
    .with_group(RibbonGroup::create("Font".into()).with_item(font_item()));

let view = RibbonTab::create("View".into())
    .with_group(RibbonGroup::create("Zoom".into()).with_item(zoom_item()));

let dom = Ribbon::create(RibbonTabVec::from_vec(vec![home, view]))
    .dom();
```

`set_active_tab(idx)` clamps to the last valid index. The
`with_on_tab_click(data, fn)` callback receives the clicked tab index. Like
tabs, the widget is externally driven; rebuild your DOM with the new active
tab to switch.

## Picking the right container

- For flat tabs of independent panels, use `TabHeader` plus a content div.
- For ribbon-style tabs with named subsections, use `Ribbon`.
- For a hierarchical sidebar, use `TreeView`.
- For a tabular data grid (columns + rows), use `ListView`.

For ad-hoc layouts, plain `Dom::create_div` with flex CSS is almost always
shorter than wiring a widget. Reach for these widgets when you want the
platform-native look or the widget's specific event surface.

---

--- METRICS ---
- Files Extracted: 96
- Estimated Token Budget: ~222958 tokens
- Canonical Reference: https://codewiki.google/github.com/fschutt/azul
