# pane_ui — API Reference
## Integration Modes
| `run` / `run_with` | yes | yes | Pane is the whole app |
| `overlay` / `PaneOverlay` | no | yes | Compositing onto your own renderer |
| `headless` / `PaneHeadless` | no | no | Tests, servers, CI |
---
### Standalone — `run` / `run_with`
Pane creates the window and event loop. Never returns.
```rust
pane::run("assets/menu.ron");
pane::run_with("assets/menu.ron", |ui: &mut StandaloneHandle, action| {
if let pane::PaneAction::Custom(ref tag) = action {
if tag == "save" {
ui.push_toast("Saved!", 2.0, 0.0, -400.0, 300.0, 60.0);
}
}
});
```
**`StandaloneHandle` methods** (available inside the `run_with` callback):
| `read(id)` | `Option<(&UiItem, WidgetState)>` | Query widget state by id |
| `write(id, &WriteValue)` | — | Set a widget value without emitting a `PaneAction` |
| `create(root, builder)` | `bool` | Add a widget to a named root at runtime |
| `destroy(id)` | `bool` | Remove a widget by id from any root |
| `create_root(name)` | — | Create an empty root |
| `style_id(name)` | `Option<StyleId>` | Resolve a style name to its id |
| `default_style()` | `Option<&str>` | Name of the current default style |
| `set_default_style(name)` | `bool` | Switch the default style by name |
| `push_toast(msg, secs, x, y, w, h)` | — | Spawn a transient toast notification |
---
### Overlay — `overlay` / `PaneOverlay`
You keep your wgpu device, queue, and event loop. Pane composites on top of your frame.
```rust
let mut ui = pane::overlay("assets/hud.ron", &device, &queue, format, None);
// In your winit event loop:
ui.handle_event(&event, window_width as f32, window_height as f32);
// In your render pass, after your own draw commands:
for action in ui.draw(&mut encoder, &view, pw, ph) {
match action {
pane::PaneAction::Custom(tag) => { /* ... */ }
pane::PaneAction::Slider(id, val) => { /* ... */ }
pane::PaneAction::Quit => std::process::exit(0),
_ => {}
}
}
```
Pass your existing `gilrs::Gilrs` to share it, or `None` to let pane create its own:
```rust
let ui = pane::overlay("assets/hud.ron", &device, &queue, format, Some(gilrs));
```
**`PaneOverlay` methods:**
| `handle_event(&WindowEvent, pw, ph)` | — | Feed a winit event into pane's input handler |
| `handle_gamepad_event(gilrs::Event)` | — | Feed a gamepad event manually |
| `disable_auto_gamepad()` | — | Stop pane from polling its internal gilrs instance |
| `draw(&mut CommandEncoder, &TextureView, pw, ph)` | `Vec<PaneAction>` | Tick + render; returns all actions this frame |
| `read(id)` | `Option<(&UiItem, WidgetState)>` | Query widget state by id |
| `write(id, &WriteValue)` | — | Set a widget value without emitting a `PaneAction` |
| `create(root, builder)` | `bool` | Add a widget to a named root at runtime |
| `destroy(id)` | `bool` | Remove a widget by id from any root |
| `create_root(name)` | — | Create an empty root |
| `style_id(name)` | `Option<StyleId>` | Resolve a style name to its id |
| `default_style()` | `Option<&str>` | Name of the current default style |
| `set_default_style(name)` | `bool` | Switch the default style by name |
| `push_toast(msg, secs, x, y, w, h)` | — | Spawn a transient toast notification |
| `set_debug(bool)` | — | Print every internal message to stdout |
| `actor_move_to(id, x, y, speed)` | — | Send an actor to a fixed position |
| `actor_follow_cursor(id, speed, trail)` | — | Make an actor chase the cursor |
| `actor_reset(id)` | — | Clear programmatic override; restore RON behaviours |
| `actor_set_pos(id, x, y)` | — | Teleport an actor instantly with no spring |
---
### Headless — `headless` / `PaneHeadless`
No GPU, no window. Widget logic and state run normally; draw calls are skipped.
```rust
let mut menu = pane::headless("assets/menu.ron");
menu.press("play_button"); // simulate a click
let actions = menu.update(1.0 / 60.0); // advance by dt seconds
```
**`PaneHeadless` methods:**
| `update(dt: f32)` | `Vec<PaneAction>` | Advance simulation by `dt` seconds |
| `press(id)` | — | Simulate a button press (requires `headless_accessible: true` in RON) |
| `active_root()` | `Option<&str>` | Name of the currently active root |
| `read(id)` | `Option<(&UiItem, WidgetState)>` | Query widget state by id |
| `write(id, &WriteValue)` | — | Set a widget value without emitting a `PaneAction` |
| `create(root, builder)` | `bool` | Add a widget to a named root at runtime |
| `destroy(id)` | `bool` | Remove a widget by id from any root |
| `create_root(name)` | — | Create an empty root |
| `style_id(name)` | `Option<StyleId>` | Resolve a style name to its id |
| `default_style()` | `Option<&str>` | Name of the current default style |
| `set_default_style(name)` | `bool` | Switch the default style by name |
| `push_toast(msg, secs, x, y, w, h)` | — | Spawn a toast (tracked but not rendered) |
| `set_debug(bool)` | — | Print every internal message to stdout |
| `actor_move_to(id, x, y, speed)` | — | Override actor movement |
| `actor_follow_cursor(id, speed, trail)` | — | Override actor to follow cursor |
| `actor_reset(id)` | — | Clear programmatic override |
| `actor_set_pos(id, x, y)` | — | Teleport actor instantly |
---
## `PaneAction`
Returned as `Vec<PaneAction>` from `draw` and `update` each frame. Every variant includes the widget's `id` string from the RON file.
```rust
match action {
PaneAction::Custom(tag) => { /* button with Custom("tag") on_press */ }
PaneAction::Slider(id, value) => { /* value is in min..=max */ }
PaneAction::Toggle(id, checked) => { }
PaneAction::Dropdown(id, index, label) => { }
PaneAction::Radio(id, index, label) => { }
PaneAction::TextChanged(id, text) => { /* every keystroke; text is the full string */ }
PaneAction::TextSubmitted(id, text) => { /* Enter pressed */ }
PaneAction::SwitchRoot(name) => { /* pane has already switched */ }
PaneAction::Quit => { }
}
```
---
## `WriteValue`
Passed to `write(id, &WriteValue)` to update widget state without emitting a `PaneAction`.
| `Slider(f32)` | Slider, ProgressBar | Clamped to `min..=max` |
| `Toggle(bool)` | Toggle | Sets checked state |
| `Text(String)` | TextBox | Truncated to `max_len` if set |
| `Selected(usize)` | Dropdown, RadioGroup | Clamped to valid range |
Unknown id or mismatched widget type logs a warning and does nothing.
---
## `WidgetState`
Returned alongside `&UiItem` from `read()`. All fields default to `false`.
```rust
pub struct WidgetState {
pub hovered: bool, // cursor is over the widget
pub pressed: bool, // widget is being held down
pub focused: bool, // has keyboard/gamepad nav focus
pub disabled: bool, // non-interactive
pub checked: bool, // toggle is on
pub grabbed: bool, // slider thumb is being dragged
pub open: bool, // dropdown or popout is expanded
}
```
Not every flag is relevant to every widget type.
---
## Runtime Widget Builders
Use these with `create(root, builder)` to add widgets at runtime.
```rust
use pane::ButtonBuilder;
let style = ui.style_id("frosted_glass");
let builder = ButtonBuilder::new("btn_dynamic")
.pos(-100.0, 200.0)
.size(200.0, 50.0)
.text("Click me")
.on_press("dynamic_action");
let builder = if let Some(s) = style { builder.style(s) } else { builder };
ui.create("main", builder);
ui.destroy("btn_dynamic");
```
Available builders and their chaining methods:
**`ButtonBuilder`** — `.pos(x, y)` · `.size(w, h)` · `.text(s)` · `.style(StyleId)` · `.tooltip(s)` · `.on_press(tag)` · `.disabled(bool)`
**`ToggleBuilder`** — `.pos(x, y)` · `.size(w, h)` · `.text(s)` · `.style_off(StyleId)` · `.style_on(StyleId)` · `.checked(bool)` · `.on_change(tag)`
**`SliderBuilder`** — `.pos(x, y)` · `.size(w, h)` · `.range(min, max)` · `.value(v)` · `.step(s)` · `.style_track(StyleId)` · `.style_thumb(StyleId)` · `.on_change(tag)`
**`TextBoxBuilder`** — `.pos(x, y)` · `.size(w, h)` · `.text(s)` · `.placeholder(s)` · `.max_len(n)` · `.style(StyleId)`
**`DropdownBuilder`** — `.pos(x, y)` · `.size(w, h)` · `.options(iter)` · `.selected(i)` · `.style(StyleId)` · `.on_change(tag)`
**`RadioGroupBuilder`** — `.pos(x, y)` · `.size(w, h)` · `.options(iter)` · `.selected(i)` · `.gap(f)` · `.style_idle(StyleId)` · `.style_selected(StyleId)` · `.on_change(tag)`
---
## RON File Format
### Root file
```ron
(
hot_reload: bool, // watch for file changes at runtime
headless_accessible: bool, // enable PaneHeadless::press()
background: Option<String>, // background image path
clear_color: Option<Color>, // fallback clear color
default_style: Option<String>, // fallback style for widgets with no explicit style
start_root: String, // which root is active on launch
shader_dirs: Vec<String>, // directories to scan for .wgsl files
style_dirs: Vec<String>, // directories to scan for .ron styles
roots: Vec<Root>,
)
```
### Root
```ron
(
name: String,
buttons: Vec<Button>,
toggles: Vec<Toggle>,
sliders: Vec<Slider>,
text_boxes: Vec<TextBox>,
dropdowns: Vec<Dropdown>,
radio_groups: Vec<RadioGroup>,
scroll_lists: Vec<ScrollList>,
scroll_panes: Vec<ScrollPane>,
bars: Vec<Bar>,
popouts: Vec<Popout>,
tabs: Vec<Tab>,
labels: Vec<FreeLabel>,
dividers: Vec<Divider>,
images: Vec<Image>,
progress_bars: Vec<ProgressBar>,
actors: Vec<Actor>,
)
```
---
## Widgets
### Button
```ron
(
id: "btn_ok",
x: 0.0, y: 0.0,
width: 200.0, height: 60.0,
text: "OK",
on_press: Custom("ok"), // see Press Actions
style: "frosted_glass", // optional; falls back to default_style
tooltip: "Confirm", // optional
nav_default: false, // if true, gets gamepad focus when entering the root
disabled: false,
)
```
Emits: `PaneAction::Custom(tag)` · `PaneAction::SwitchRoot(name)` · `PaneAction::Quit`
---
### Toggle
```ron
(
id: "mute",
x: 0.0, y: 0.0,
width: 240.0, height: 50.0,
text: "Mute audio",
checked: false,
style_off: "plain",
style_on: "glass_pill",
on_change: Custom("mute_changed"),
tooltip: "Toggle sound", // optional
)
```
Emits: `PaneAction::Toggle(id, checked)`
---
### Slider
```ron
(
id: "volume",
x: 0.0, y: 0.0,
width: 300.0, height: 40.0,
min: 0.0, max: 1.0, value: 0.8,
step: 0.05, // optional snap interval
style_track: "plain",
style_thumb: "glass_pill",
on_change: Custom("volume_changed"),
tooltip: "Master volume", // optional
)
```
Emits: `PaneAction::Slider(id, value)`
---
### TextBox
```ron
(
id: "player_name",
x: 0.0, y: 0.0,
width: 320.0, height: 50.0,
hint: "Enter name…",
max_len: 24, // optional character limit
style: "plain",
style_focus: "glass_pill", // optional; used while focused
on_change: Custom("name_key"),
on_submit: Custom("name_submit"),
password: false,
font_size: 20.0, // optional override
tooltip: "Your name", // optional
)
```
Emits: `PaneAction::TextChanged(id, text)` on each keystroke · `PaneAction::TextSubmitted(id, text)` on Enter
---
### Dropdown
```ron
(
id: "quality",
x: 0.0, y: 0.0,
width: 200.0, height: 50.0,
options: ["Low", "Medium", "High", "Ultra"],
selected: 2,
style: "plain",
style_list: "frosted_glass", // optional; style for the expanded list panel
style_item: "plain", // optional; style for each option button
on_change: Custom("quality_changed"),
tooltip: "Render quality", // optional
)
```
Emits: `PaneAction::Dropdown(id, index, label)`
---
### RadioGroup
```ron
(
id: "difficulty",
x: 0.0, y: 0.0,
width: 400.0, height: 50.0,
options: ["Easy", "Normal", "Hard"],
selected: 1,
gap: 8.0,
style_idle: "plain",
style_selected: "glass_pill",
on_change: Custom("diff_changed"),
tooltip: "Difficulty", // optional
)
```
Emits: `PaneAction::Radio(id, index, label)`
---
### ScrollList
A scrollable list of buttons clipped to its bounds. Children are auto-positioned.
```ron
(
id: "file_list",
x: 0.0, y: 0.0,
width: 300.0, height: 400.0,
pad_top: 8.0, pad_bottom: 8.0,
pad_left: 0.0, pad_right: 0.0,
gap: 4.0,
style: "frosted_glass", // optional background
horizontal: false,
full_span: false,
items: [
( id: "item_1", height: 60.0, text: "Item 1", on_press: Custom("select_1"), style: "plain" ),
( id: "item_2", height: 60.0, text: "Item 2", on_press: Custom("select_2"), style: "plain" ),
],
)
```
Item fields: `id` · `height` · `width` (horizontal only) · `text` · `tooltip` · `style` · `on_press`
---
### ScrollPane
A freeform scrollable container. Children can be any widget type and are auto-positioned along the scroll axis.
```ron
(
id: "settings_pane",
x: 0.0, y: 0.0,
width: 500.0, height: 600.0,
pad_top: 16.0, pad_bottom: 16.0,
pad_left: 16.0, pad_right: 16.0,
gap: 8.0,
style: "frosted_glass", // optional
horizontal: false,
manual: false, // if true, children use their own x/y instead of auto-layout
items: [
Button(( id: "btn", width: 200.0, height: 50.0, text: "OK", on_press: Custom("ok") )),
Slider(( id: "vol", width: 300.0, height: 40.0, min: 0.0, max: 1.0, value: 0.5, on_change: Custom("vol") )),
// Button | Toggle | Slider | TextBox | Label | Divider | Image | ProgressBar | ScrollPane | ScrollList | Tab
],
)
```
---
### Bar
An edge-anchored panel. Children are auto-positioned along the bar's axis.
```ron
(
id: "top_bar",
edge: Top, // Top | Bottom | Left | Right | Free
thickness: 60.0, // height for Top/Bottom, width for Left/Right
pad: 12.0, // padding before first and after last item
gap: 8.0,
style: "frosted_glass", // optional
items: [
Button(( id: "home", height: 40.0, text: "Home", on_press: SwitchRoot("main"), style: "plain" )),
Spacer, // fills remaining space
Label(( id: "title", text: "My App", size: 22.0 )),
],
)
```
Item types: `Button(ListButtonDef)` · `Label` · `Spacer` · `ScrollList`
`Free` edge: set `x`, `y`, `width`, `height` manually instead of `thickness`.
---
### Popout
A panel that spring-animates between an open and closed position when its toggle button is pressed.
```ron
(
id: "sidebar",
closed_x: -700.0, closed_y: 0.0,
open_x: -300.0, open_y: 0.0,
width: 400.0, height: 800.0,
toggle_id: "sidebar_btn",
style: "frosted_glass", // optional
edge: Left, // Left | Right | Top | Bottom — used for nav direction
shadow: true,
horizontal: false,
gap: 8.0,
full_span: false,
home_toggles: false, // if true, focus returns to toggle_id when panel closes
items: [
Button(( id: "opt1", width: 300.0, height: 50.0, text: "Option 1", on_press: Custom("opt1") )),
],
)
```
---
### Tab
A multi-page container with labelled tabs. Children are auto-positioned within each page.
```ron
(
id: "settings_tabs",
x: 0.0, y: 0.0,
width: 600.0, height: 500.0,
pad_top: 8.0, pad_bottom: 8.0,
pad_left: 8.0, pad_right: 8.0,
gap: 8.0,
style: "frosted_glass", // optional
pages: [
(
label: "Audio",
items: [ Slider(( id: "volume", width: 300.0, height: 40.0, min: 0.0, max: 1.0, value: 0.8, on_change: Custom("vol") )) ],
),
(
label: "Video",
items: [ Dropdown(( id: "quality", width: 200.0, height: 50.0, options: ["Low", "High"], on_change: Custom("quality") )) ],
),
],
)
```
Switch pages from a button: `on_press: SwitchTabPage { tab_id: "settings_tabs", page: 1 }`
Page item types: `Button` · `Toggle` · `Slider` · `TextBox` · `Label` · `Divider` · `Image` · `ProgressBar` · `ScrollPane` · `ScrollList` · `Tab`
---
### Label
```ron
(
id: "title",
x: 0.0, y: -400.0,
text: "Main Menu",
size: 48.0,
color: (r: 1.0, g: 1.0, b: 1.0, a: 0.9), // optional
width: 0.0, // optional max wrap width; 0 = unlimited
)
```
---
### Divider
A solid filled rectangle. Use for separators, backgrounds, or decorative shapes.
```ron
(
id: "sep",
x: 0.0, y: 100.0,
width: 400.0, height: 2.0,
style: "plain",
full_span: false, // if true, stretches to full screen width
)
```
---
### Image
```ron
(
id: "logo",
x: 0.0, y: -300.0,
width: 256.0, height: 128.0,
path: "assets/logo.png",
gif_mode: Loop, // optional: Loop | Once | OnceHide (GIFs only)
)
```
---
### ProgressBar
```ron
(
id: "loading",
x: 0.0, y: 200.0,
width: 300.0, height: 20.0,
value: 0.75, // 0.0..=1.0
style_track: "plain",
style_fill: "glass_pill",
)
```
Update at runtime: `ui.write("loading", &WriteValue::Slider(0.9))`
---
### Actor
An animated element that can follow the cursor or move to programmatic targets.
```ron
(
id: "cursor_glow",
x: 0.0, y: 0.0,
width: 64.0, height: 64.0,
gif: "assets/glow.gif", // base looping animation (optional)
style: "plain", // optional fallback style
z_front: false, // if true, draws on top of all other widgets
return_on_end: true,
behaviours: [
( trigger: Always, action: FollowCursor(speed: 12.0, trail: 0.08) ),
( trigger: OnHoverSelf, action: SwapGif(path: "assets/glow_hover.gif") ),
( trigger: OnClickAnywhere, action: MoveTo(x: 0.0, y: 0.0, speed: 20.0) ),
],
)
```
**Triggers:**
| `Always` | Every frame |
| `OnHoverSelf` | Cursor is inside the actor's bounds |
| `OnPressSelf` | Mouse button held over the actor |
| `OnClickSelf` | Mouse button released over the actor |
| `OnClickAnywhere` | Mouse button released anywhere |
**Actions:**
| `FollowCursor(speed, trail)` | Lerp toward cursor; `trail` adds lag (0.0 = instant) |
| `MoveTo(x, y, speed)` | Lerp toward a fixed grid position |
| `SwapGif(path)` | Show a different gif while the trigger is active |
---
### Toast
A transient notification that fades out automatically.
From code:
```rust
ui.push_toast("Settings saved", 2.0, 0.0, -400.0, 300.0, 60.0);
// message secs x y width height
```
From a button's `on_press`:
```ron
on_press: Toast(
message: "Saved!",
duration: 2.0,
x: 0.0, y: -400.0,
width: 300.0, height: 60.0,
)
```
---
## Press Actions
Used in `on_press` fields of Button and list buttons.
| `Custom("tag")` | Emits `PaneAction::Custom("tag")` |
| `SwitchRoot("name")` | Changes the active root; emits `PaneAction::SwitchRoot("name")` |
| `SwitchTabPage { tab_id: "id", page: 0 }` | Jumps a Tab widget to a specific page |
| `Quit` | Exits in standalone mode; emits `PaneAction::Quit` in overlay/headless |
| `Print` | Prints to stdout |
| `Toast { message, duration, x, y, width, height }` | Spawns a toast notification |
---
## Styling
### Built-in styles
| `plain` | Flat solid color |
| `frosted_glass` | Translucent frosted glass |
| `glass_pill` | Pill-shaped glass |
| `retro` | Pixelated arcade aesthetic |
| `emboss` | Raised surface with shadow |
| `sharp_outline` | Minimalist outline |
### Custom styles
Drop a `.ron` file in a directory listed under `style_dirs`. Reference by filename without extension.
```ron
// styles/neon.ron
(
shader: "flat",
idle: (
shape: RoundedRectangle,
color: (r: 0.05, g: 0.05, b: 0.12, a: 0.95),
border_width: 1.5,
border_color: (r: 0.2, g: 0.8, b: 1.0, a: 1.0),
shadow_size: 8.0,
shadow_color: (r: 0.2, g: 0.8, b: 1.0, a: 0.4),
),
hovered: ( ... ),
pressed: ( scale: 0.95, ... ),
disabled: ( ... ),
)
```
Each visual state (`idle`, `hovered`, `pressed`, `disabled`) supports:
`shape` · `color` · `corner_radius` · `border_width` · `border_color` · `shadow_size` · `shadow_color` · `scale` · `shader` · `texture` · `gif_mode` · `text_color` · `font_size` · `text_align` · `font` · `bold` · `italic`
`scale` is spring-animated.
### Built-in shaders
`flat` · `frosted_glass` · `liquid_glass` · `retro` · `outline` · `emboss` · `textured`
Custom shaders: drop a `.wgsl` file in a directory listed under `shader_dirs`. Reference by filename without extension.
---
## Coordinate System
- **Origin `(0.0, 0.0)`** — center of screen
- **Height** — always 1080 units regardless of resolution
- **Width** — scales with aspect ratio (`grid_width = (px_width / px_height) * 1080`)
- **Y axis** — positive is downward (top ≈ −540, bottom ≈ +540)
- **X axis** — positive is right (edges depend on aspect ratio; ±960 at 16:9)
---
## Hot Reload
Set `hot_reload: true` in the root `.ron`. While the app is running, saving any watched file updates the UI live:
| `menu.ron` | Layout, widget config, root structure |
| `styles/*.ron` | Visual design |
| `shaders/*.wgsl` | GPU shader recompiles and swaps live |
The `dev` feature is required for runtime style hot-reload:
```toml
pane_ui = { version = "0.1.0", features = ["dev"] }
```
---
## Gamepad Navigation
All widgets respond to controller input with no configuration.
| D-pad / left stick | Move focus to nearest widget in that direction |
| South (A / Cross) | Confirm — activates focused widget |
| East (B / Circle) | Cancel — closes open popouts |
| Left / Right on Slider | Adjust value |
| Scroll in ScrollList / ScrollPane | Keeps focused item visible |
---
## Debugging
```bash
PANE_DEBUG=1 cargo run # standalone
```
```rust
ui.set_debug(true); // overlay or headless
```
Prints every internal `UiMsg` to stdout — widget ticks, root switches, reload events.