Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Retained UI

Live Demos: Retained UI | Widget Gallery

Nightshade provides a retained UI system built on ECS entities. Each widget is an entity with layout, styling, and interaction components. UI state persists between frames — you build the tree once and react to events.

Building a UI

Use UiTreeBuilder to construct UI hierarchies:

#![allow(unused)]
fn main() {
fn initialize(&mut self, world: &mut World) {
    let camera = spawn_ortho_camera(world, Vec2::new(0.0, 0.0));
    world.resources.active_camera = Some(camera);

    let mut tree = UiTreeBuilder::new(world);
    tree.build_ui(tree.root_entity(), |ui| {
        ui.label("Hello, Nightshade!");
        ui.button("Click me");
        ui.row(|ui| {
            ui.label("Left");
            ui.button("Right");
        });
    });
    self.root = Some(tree.finish());
}
}

Reacting to Events

Check widget events each frame:

#![allow(unused)]
fn main() {
fn run_systems(&mut self, world: &mut World) {
    if world.ui_button_clicked(self.my_button) {
        self.counter += 1;
    }

    if world.ui_slider_changed(self.volume_slider) {
        let value = world.ui_slider_value(self.volume_slider);
        self.volume = value;
    }
}
}

Or iterate all events:

#![allow(unused)]
fn main() {
fn run_systems(&mut self, world: &mut World) {
    for event in world.ui_events().to_vec() {
        match event {
            UiEvent::ButtonClicked(entity) => { /* handle */ }
            UiEvent::SliderChanged { entity, value } => { /* handle */ }
            _ => {}
        }
    }
}
}

Layout Types

Flow Layout (Default)

Vertical or horizontal stacking:

#![allow(unused)]
fn main() {
tree.build_ui(root, |ui| {
    ui.column(|ui| {
        ui.label("Item 1");
        ui.label("Item 2");
        ui.label("Item 3");
    });

    ui.row(|ui| {
        ui.button("Cancel");
        ui.button("OK");
    });
});
}

Docked Panels

#![allow(unused)]
fn main() {
tree.build_ui(root, |ui| {
    ui.docked_panel_left("Tools", 250.0, |ui| {
        ui.label("Tool options");
    });

    ui.docked_panel_right("Inspector", 300.0, |ui| {
        ui.label("Properties");
    });

    ui.docked_panel_bottom("Console", 150.0, |ui| {
        ui.label("Output");
    });
});
}

Floating Panels

#![allow(unused)]
fn main() {
tree.build_ui(root, |ui| {
    ui.floating_panel(
        "Settings",
        Rect { x: 100.0, y: 100.0, width: 300.0, height: 400.0 },
        |ui| {
            ui.label("Window content");
        },
    );
});
}

Widgets

Labels and Text

#![allow(unused)]
fn main() {
ui.label("Simple text");
ui.label_colored("Error!", Vec4::new(1.0, 0.2, 0.2, 1.0));
ui.heading("Section Title");
ui.separator();
ui.spacing(10.0);
}

Buttons

#![allow(unused)]
fn main() {
let btn = ui.button("Start Game");
let colored = ui.button_colored("Delete", Vec4::new(0.8, 0.2, 0.2, 1.0));
}

Sliders and Drag Values

#![allow(unused)]
fn main() {
let slider = ui.slider(0.0, 100.0, 50.0);
let drag = ui.drag_value(1.0, 0.0, 10.0, 0.1, 2);
}

Toggles and Checkboxes

#![allow(unused)]
fn main() {
let toggle = ui.toggle(false);
let checkbox = ui.checkbox("Fullscreen", false);
}

Text Input

#![allow(unused)]
fn main() {
let input = ui.text_input("Enter name...");
}
#![allow(unused)]
fn main() {
let dropdown = ui.dropdown(vec!["Low".into(), "Medium".into(), "High".into()], 1);
let menu = ui.menu("File", vec!["New".into(), "Open".into(), "Save".into()]);
}

Color Picker

#![allow(unused)]
fn main() {
let picker = ui.color_picker(Vec4::new(1.0, 0.5, 0.0, 1.0));
}

Scroll Areas

#![allow(unused)]
fn main() {
ui.scroll_area(Vec2::new(300.0, 200.0), |ui| {
    for index in 0..50 {
        ui.label(&format!("Item {index}"));
    }
});
}

Collapsing Headers

#![allow(unused)]
fn main() {
ui.collapsing_header("Advanced Settings", true, |ui| {
    ui.checkbox("Debug mode", false);
    ui.slider(0.0, 1.0, 0.5);
});
}

Tab Bars

#![allow(unused)]
fn main() {
let tabs = ui.tab_bar(vec!["General".into(), "Audio".into(), "Video".into()], 0);
}

Tree Views

#![allow(unused)]
fn main() {
ui.tree_view(false, |ui| {
    ui.tree_node(tree_entity, root_entity, "Root", 0, None);
});
}

Bidirectional Binding

Bind widget values directly to variables:

#![allow(unused)]
fn main() {
fn run_systems(&mut self, world: &mut World) {
    world.ui_bind_slider(self.volume_slider, &mut self.volume);
    world.ui_bind_toggle(self.fullscreen_toggle, &mut self.fullscreen);
    world.ui_bind_text_input(self.name_input, &mut self.player_name);
    world.ui_bind_dropdown(self.quality_dropdown, &mut self.quality_index);
}
}

Reactive Properties

Track changes efficiently with UiProperty:

#![allow(unused)]
fn main() {
struct MyEditor {
    rotation: UiProperty<f32>,
    slider: Entity,
}

fn run_systems(&mut self, world: &mut World) {
    world.ui_bind_reactive_slider(self.slider, &mut self.rotation);

    if self.rotation.take_dirty() {
        apply_rotation(world, *self.rotation);
    }
}
}

Composite Widgets

Create reusable widget groups:

#![allow(unused)]
fn main() {
struct Vec3Editor {
    x_drag: Entity,
    y_drag: Entity,
    z_drag: Entity,
}

impl CompositeWidget for Vec3Editor {
    type Value = Vec3;

    fn build(tree: &mut UiTreeBuilder) -> Self {
        let x = tree.add_drag_value(0.0, -1000.0, 1000.0, 0.1, 2);
        let y = tree.add_drag_value(0.0, -1000.0, 1000.0, 0.1, 2);
        let z = tree.add_drag_value(0.0, -1000.0, 1000.0, 0.1, 2);
        Self { x_drag: x, y_drag: y, z_drag: z }
    }

    fn value(&self, world: &World) -> Vec3 {
        Vec3::new(
            world.ui_drag_value(self.x_drag),
            world.ui_drag_value(self.y_drag),
            world.ui_drag_value(self.z_drag),
        )
    }
}
}

Flexible Units

Position and size values support multiple unit types that can be combined:

UnitDescription
Ab(value)Absolute pixels
Rl(value)Relative to parent (0–100%)
Rw(value)Relative to parent width
Rh(value)Relative to parent height
Em(value)Font-size multiples
Vp(value)Viewport percentage
Vw(value)Viewport width percentage
Vh(value)Viewport height percentage

Units combine with +:

#![allow(unused)]
fn main() {
Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 32.0))
}

Toast Notifications

#![allow(unused)]
fn main() {
world.ui_show_toast("File saved", 3.0, ToastSeverity::Success);
world.ui_show_toast("Connection lost", 5.0, ToastSeverity::Error);
}
#![allow(unused)]
fn main() {
let dialog = ui.confirm_dialog("Delete?", "This cannot be undone.");

fn run_systems(&mut self, world: &mut World) {
    if let Some(confirmed) = world.ui_modal_result(self.dialog) {
        if confirmed {
            self.delete_item(world);
        }
    }
}
}

Theming

Widgets use theme colors that adapt to the active theme:

#![allow(unused)]
fn main() {
node.with_theme_color::<UiBase>(ThemeColor::Background)
    .with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
    .with_theme_color::<UiPressed>(ThemeColor::BackgroundActive)
    .with_theme_border_color(ThemeColor::Border)
}

Use egui for complex debug tools and editor interfaces. Use the retained UI for in-game menus, HUD elements, and game UI that benefits from persistent state and ECS integration.