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..."); }
Dropdowns and Menus
#![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:
| Unit | Description |
|---|---|
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); }
Modal Dialogs
#![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.