Skip to content

Avon662/pane_ui

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pane_ui

Pane UI Logo

License wgpu Crates.io

A immediate-mode UI library for wgpu. UI is defined in RON files and rendered each frame via wgpu. Supports hot reload of layouts, styles, and shaders.


Quick Start

[dependencies]
pane_ui = "0.1.0"

Create assets/menu.ron:

(
    hot_reload: true,
    start_root: "main",
    default_style: Some("plain"),
    style_dirs: ["styles"],
    roots: [(
        name: "main",
        buttons: [
            ( id: "btn_ok",   x: -110.0, y: 0.0, width: 100.0, height: 40.0, text: "OK",     on_press: Custom("ok") ),
            ( id: "btn_quit", x:   10.0, y: 0.0, width: 100.0, height: 40.0, text: "Quit",   on_press: Quit ),
        ],
    )],
)
fn main() {
    pane::run("assets/menu.ron");
}

For a callback on each action:

pane::run_with("assets/menu.ron", |ui, action| {
    if let pane::PaneAction::Custom(ref tag) = action {
        println!("pressed: {tag}");
    }
});

Examples in the demos folder

Integration Modes

Standalone

Pane owns the window and event loop.

pane::run("assets/menu.ron");
// or with a callback:
pane::run_with("assets/menu.ron", |ui, action| { ... });

Overlay

Renders into a caller-owned wgpu surface. Your render loop stays intact.

// At startup
let mut ui = pane::overlay("assets/hud.ron", &device, &queue, format, None);

// In your event loop
ui.handle_event(&event, pw, ph);

// After your own draw calls
let actions = ui.draw(&mut encoder, &view, pw, ph);
for action in actions { ... }

Pass Some(gilrs) to share an existing gamepad context, or None to let pane create its own.

Headless

No GPU, no window. Useful for testing or driving UI logic server-side.

let mut menu = pane::headless("assets/menu.ron");
let actions = menu.update(0.016);   // advance by dt, returns PaneActions
menu.press("btn_ok");               // requires headless_accessible: true in the RON
let root = menu.active_root();

Coordinate System

Origin (0, 0) is the center of the screen. Height is always 1080 units regardless of resolution; width scales with aspect ratio.

          y = -540
             ┬
             │  ← negative Y is up
(-960) ──── 0,0 ──── (+960)   (16:9)
             │  ← positive Y is down
             ┴
          y = +540

A widget at x: 0, y: 0 is centered on every screen and resolution.


RON File Reference

Root file

(
    hot_reload: bool,               // watch files and reload on change
    headless_accessible: bool,      // allow PaneHeadless::press()
    background: Option<String>,     // background image path
    default_style: Option<String>,  // fallback style for widgets with no explicit style
    start_root: String,             // which root to show first
    shader_dirs: Vec<String>,       // directories to scan for .wgsl files
    style_dirs:  Vec<String>,       // directories to scan for .ron styles
    roots: Vec<Root>,
)

Root

(
    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>,
)

Common widget fields

Every interactive widget shares id, x, y, width, height, style, tooltip, and disabled.

Button on_press values:

on_press: Custom("my_tag")        // emits PaneAction::Custom("my_tag")
on_press: SwitchRoot("settings")  // switches active root (pane handles it automatically)
on_press: Quit                    // emits PaneAction::Quit
on_press: Print                   // prints to stdout

Widgets

Widget Description
Button Clickable, with text, tooltip, and disabled state
Toggle On/off switch with separate styles per state
Slider Draggable range input with optional step size
TextBox Single-line text input with placeholder and max length
Dropdown Collapsible option selector
RadioGroup Mutually exclusive option set
ScrollList Scrollable list; children are auto-positioned
ScrollPane Scrollable container for mixed content; children are auto-positioned
Bar Edge-anchored panel (top/bottom/left/right); children are auto-positioned
Popout Spring-animated panel that slides between open/closed
Tab Multi-page container; pages switch via SwitchTabPage press actions
Label Non-interactive text
Divider Solid rectangle for visual separation
Image Static image or animated GIF
ProgressBar Read-only fill indicator (0.0–1.0)
Actor Animated element with cursor-reactive behaviors

Bar, ScrollList, and ScrollPane auto-position their children — you don't set coordinates on items inside them.

Actor

An Actor is an animated element that reacts to cursor events via a trigger/action behavior system.

actors: [(
    id: "cursor_glow",
    origin_x: 0.0, origin_y: 0.0,
    width: 120.0, height: 120.0,
    gif: Some("glow"),
    behaviours: [
        (trigger: Always,      action: MoveTo(x: 0.0, y: 0.0, speed: 3.0)),
        (trigger: OnHoverSelf, action: FollowCursor(speed: 8.0, trail: 0.3)),
        (trigger: OnPressSelf, action: SwapGif(texture: "glow_bright", shader: "textured")),
    ],
)]

Triggers: Always · OnHoverSelf · OnPressSelf · OnClickSelf · OnClickAnywhere

Actions: FollowCursor { speed, trail } · MoveTo { x, y, speed } · SwapGif { texture, shader }

Actors can also be controlled from code:

ui.actor_follow_cursor("cursor_glow", 8.0, 0.3);
ui.actor_move_to("cursor_glow", 0.0, 0.0, 3.0);
ui.actor_reset("cursor_glow");          // restore RON behaviors
ui.actor_set_pos("cursor_glow", x, y);  // teleport, no spring

Actions

PaneOverlay::draw and PaneHeadless::update return Vec<PaneAction> each frame. run_with passes them one at a time to the callback.

use pane::PaneAction;

match action {
    PaneAction::Custom(tag)              => { ... }
    PaneAction::Slider(id, value)        => { ... }  // value: f32
    PaneAction::Toggle(id, checked)      => { ... }  // checked: bool
    PaneAction::TextChanged(id, text)    => { ... }  // every keystroke
    PaneAction::TextSubmitted(id, text)  => { ... }  // Enter pressed
    PaneAction::Dropdown(id, idx, label) => { ... }
    PaneAction::Radio(id, idx, label)    => { ... }
    PaneAction::SwitchRoot(name)         => { ... }  // pane switches automatically
    PaneAction::Quit                     => { ... }
}

Runtime API

All three handles (StandaloneHandle, PaneOverlay, PaneHeadless) share the same core API.

Reading widget state

if let Some((item, state)) = ui.read("volume_slider") {
    println!("hovered={} grabbed={}", state.hovered, state.grabbed);
}

WidgetState fields (all bool):

Field Meaning
hovered Cursor is over the widget
pressed Being held down
focused Has keyboard/gamepad focus
disabled Non-interactive
checked Toggle is on
grabbed Slider thumb is being dragged
open Dropdown or popout is expanded

Writing widget state

Pushes a value into a widget without emitting a PaneAction. Use this to sync UI to app state.

use pane::WriteValue;

ui.write("volume",   &WriteValue::Slider(0.8));
ui.write("mute",     &WriteValue::Toggle(true));
ui.write("username", &WriteValue::Text("Alice".into()));
ui.write("quality",  &WriteValue::Selected(2));   // Dropdown or RadioGroup index
Variant Applies to Behaviour
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.

Default style

let current = ui.default_style();           // Option<&str>
ui.set_default_style("retro");              // returns false if name not registered

Affects all widgets that don't have an explicit style set in the RON. Widgets with an explicit style are unaffected.

Toasts

ui.push_toast("Saved!", 2.0, 0.0, -400.0, 300.0, 60.0);
//             msg      secs  x    y       w      h

Creating and destroying widgets at runtime

use pane::ButtonBuilder;

ui.create("main", ButtonBuilder::new("btn_dynamic")
    .pos(-100.0, 200.0)
    .size(200.0, 50.0)
    .text("Click me")
    .on_press("dynamic_action"));   // returns false if id exists or root is missing

ui.destroy("btn_dynamic");          // returns false if not found

To create an empty root in code:

ui.create_root("generated");
ui.create("generated", ButtonBuilder::new("btn_ok").pos(0.0, 0.0).size(200.0, 50.0).text("OK").on_press("ok"));

Available builders: ButtonBuilder · ToggleBuilder · SliderBuilder · TextBoxBuilder · DropdownBuilder · RadioGroupBuilder

To use a specific style, resolve its id first:

let s = ui.style_id("frosted_glass");
let builder = ButtonBuilder::new("btn").pos(0.0, 0.0).size(200.0, 50.0).text("Hi").on_press("hi");
let builder = if let Some(s) = s { builder.style(s) } else { builder };
ui.create("main", builder);

Styling

Built-in styles

Style Description
frosted_glass Frosted, translucent glass
glass_pill Pill-shaped glass
plain Flat solid colors
emboss Raised surface with shadow
sharp_outline Bordered, minimal
retro Pixelated arcade aesthetic

Set default_style in the root RON to apply a style to all widgets that don't specify one explicitly.

Custom styles

Drop a .ron file in your style directory and reference it by filename (no extension):

// 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: (
        color: (r: 0.08, g: 0.08, b: 0.18, a: 0.98),
        border_color: (r: 0.4, g: 0.9, b: 1.0, a: 1.0),
        shadow_size: 16.0,
    ),
    pressed: (
        color: (r: 0.02, g: 0.02, b: 0.08, a: 1.0),
        scale: 0.95,
    ),
    disabled: (
        color: (r: 0.05, g: 0.05, b: 0.08, a: 0.4),
        border_color: (r: 0.2, g: 0.2, b: 0.3, a: 0.3),
    ),
)

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 — 0.95 on pressed compresses the widget physically; values above 1.0 overshoot.

Custom shaders

Drop .wgsl files in your shader directory and reference them by filename (no extension) in a style or widget:

// styles/neon.ron
hovered: (
    shader: "liquid_glass",
    ...
),

Built-in shaders: flat · frosted_glass · liquid_glass · retro · outline · emboss · textured


Hot Reload

Set hot_reload: true in the root RON. While the app is running:

File saved What updates
menu.ron Layout, widget config, root structure
styles/*.ron Visual design for all widgets using that style
shaders/*.wgsl GPU shader recompiles and swaps live

Requires the dev feature for style hot reload:

pane_ui = { version = "0.1.0", features = ["dev"] }

Controller Support

All widgets support controller navigation with no configuration:

Input Action
Left stick / D-pad Move focus to nearest widget in that direction
South (A / Cross) Confirm / activate
East (B / Circle) Cancel / close
Left / Right Adjust slider; navigate dropdown/radio
Up / Down Scroll list; navigate options

In overlay mode, pass a gilrs::Event to ui.handle_gamepad_event(event), or let pane manage its own gamepad context.


GIF Support

Assign a gif to an Image or Actor. Three playback modes:

idle:    ( texture: "spinner", gif_mode: Loop ),      // plays forever
pressed: ( texture: "burst",   gif_mode: Once ),      // plays once, holds last frame
//                             gif_mode: OnceHide     // plays once, then hides widget

Platform Support

Platform Status
Linux (Wayland) Tested — primary development platform (Fedora)
Linux (X11) Untested - Supported via winit
Windows Untested - Supported
macOS Untested - Supported
Web (WASM) Not supported — winit event loop incompatible

Building & Running

cargo build                  # build library + binary
cargo run                    # run the built-in demo
PANE_DEBUG=1 cargo run       # run demo with debug overlay
cargo test                   # run tests
cargo test <test_name>       # run a single test
cargo doc --open             # build and browse API docs
cargo build --features dev   # enable hot-reload for styles

Dependencies

Crate Version Role
wgpu 29.0 GPU rendering
winit 0.30 Window & event loop
glyphon 0.11 Text rendering
gilrs 0.11 Controller input
ron 0.12 Config format
serde 1.0 Deserialization
image 0.25 Image & GIF loading

License

Licensed under the Apache License, Version 2.0.

About

A Rust Native Data Driven Ui

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors