A simple, fast, and modular UI library for Bevy, combining immediate mode ergonomics with Bevy ECS-powered retained UI.
- Develop complex UI as simple Rust code.
- UI visuals, styling is fully customizable.
- Extend immediate mode with custom extensions / capabilities.
👉 Web Demo 👈
- Immediate mode entity hierarchy management
Build interactive entity hierarchies with a clean API. - Fully compatible with Bevy
Heavy lifting is done by Bevy ECS andbevy_uiretained mode UI. - Supports Bevy Scene Notation (BSN)
Add scenes on entities by calling.on_spawn_apply_scene,.on_spawn_queue_apply_scene. - Custom extension support
Add custom capabilities like.clicked(),.selected(true),.hovered(). Extension use integrated with rust type system for autocompletion and compile time check support. - Inbuilt support for UI use case
Contains extensions that implement necessary logic for constructing UI. - Reusable widgets
Implement widgets using functional or bevy native style. - Hot-patching support
- Supported out of the box. See hotpatching.
- Alternative: hot_lib_reloader.
- Fast
Only visits each entity once per tick and does minimal amount of changes. Heavy lifting is done by Bevy's retained UI. - Parallelizable
Minimal data access requirements allow systems to run in parallel with other systems without exclusive world access. - Simple
Define UI in straightforward functions, free from macro/observer/trigger boilerplate. - Modular
Develop your UI by writing UI in small composable parts. Extend functionality with modular extensions. - Integration-friendly
Works with other libraries (e.g., reloadable CSS style with bevy_flair).
| bevy_immediate | bevy | MSRV |
|---|---|---|
| 0.9 | 0.20 | 1.97.1 |
| 0.8 | 0.19 | 1.95 |
| 0.7 | 0.19 | 1.95 |
| 0.6 | 0.18.1 | 1.89 |
| 0.5 | 0.18 | 1.89 |
| 0.4 | 0.17 | 1.88 |
| 0.3 | 0.17 | 1.88 |
| 0.2 | 0.17 | 1.88 |
| 0.1 | 0.16 | 1.85 |
To use add bevy_immediate to your project dependencies in Cargo.toml file.
See CHANGELOG for changes between versions.
Examples can be viewed: (cargo run --example demo).
- Hello world - Minimal usage example
- Power user - Customized API for complex use cases
- egui-style API - egui style API for your UI implementation.
- Plain UI - Create your UI as a single system
- Bevy inbuilt widgets:
- Widgets - Showcases how to use widgets from bevy
- Feathers gallery - Replicates Bevy's
feathers_galleryexample using immediate mode - Scrollarea - Showcases how to create reusable scrollareas
- Text edit - Showcases text edit integration using Bevy's
EditableText. - Reusable widget implementation
- Functional widget - Implement widgets as plain functions
- Native widget - Implement native Bevy-like widgets
- Widget use - Use functional and native widgets together
- Main menu example - Build a simple main menu with selectable buttons
- Floating elements
- Tooltips - Add tooltips.
- Anchored UI - Build drop down menus, comboboxes, popups that utilize anchored floating UI.
- Floating windows - Resizable, draggable floating windows.
- Extensions
- Extension implementation - Write your own capabilities (e.g.
.clicked()or.selected(...)) - Using extensions - Use a custom predefined set of extensions
- Extension implementation - Write your own capabilities (e.g.
- Style - Contains UI styling implementation for examples
- Hot-Patching example - Modify UI during program execution: See Hotpatching section.
Examples are located in ./examples/
Using bevy_feathers and bevy_ui_widgets.
// Checkbox using bsn
ui.ch()
.on_spawn_apply_scene(|| {
bsn! { @FeathersCheckbox { @caption: {bsn!{ Text("Checkbox") }} } }
})
.checked(&mut checkbox_value);
// Checkbox using mixed style
ui.ch()
.on_spawn_apply_scene(|| bsn! { @FeathersCheckbox })
.add(|ui| {
ui.ch().text("Checkbox");
})
.checked(&mut checkbox_value);
// Toggle switch
ui.ch()
.on_spawn_apply_scene(|| bsn! {@FeathersToggleSwitch})
.interactions_disabled(state.disabled) // Control whether interactions are enabled
.checked(&mut toggle_value);
// Button that counts clicks
let mut button = ui.ch()
.on_spawn_apply_scene(|| { bsn! { @FeathersButton } });
.add(|ui| {
ui.ch().text(format!("Clicked: {}", count));
});
if button.activated() {
count += 1;
}Here's a more advanced example where user has added their own API.
pub struct PowerUserExamplePlugin;
impl bevy_app::Plugin for PowerUserExamplePlugin {
fn build(&self, app: &mut bevy_app::App) {
// Initialize plugin with your widget root component
app.add_plugins(BevyImmediateAttachPlugin::<CapsUi, PowerUserExampleRoot>::new());
app.insert_resource(ShowHidden { show: false });
}
}
#[derive(Resource)]
struct ShowHidden {
show: bool,
}
#[derive(Component)]
pub struct PowerUserExampleRoot;
#[derive(SystemParam)]
pub struct Params<'w> {
show_hidden: ResMut<'w, ShowHidden>,
}
impl ImmediateAttach<CapsUi> for PowerUserExampleRoot {
type Params = Params<'static>;
fn construct(ui: &mut Imm<CapsUi>, params: &mut Params) {
ui.ch().my_title("Bevy power user example");
ui.ch()
.my_subtitle("Use helper functions to simplify and reuse code!");
ui.ch().my_subtitle("Show collapsible element");
ui.ch().my_row_container().add(|ui| {
for (text, state) in [("No", false), ("Yes", true)] {
let mut button = ui
.ch_id(("choice", state))
.my_button()
.selected(params.show_hidden.show == state)
.add(|ui| {
ui.ch().my_text(text);
});
if button.clicked() {
params.show_hidden.show = state;
}
}
});
if params.show_hidden.show {
ui.ch_id("yes_no").my_container_with_background().add(|ui| {
ui.ch().my_text("Lorem Ipsum!");
});
}
ui.ch().my_text("It is really simple!");
}
}ui.column(|ui| {
ui.label("Egui-style API");
ui.row(|ui| {
ui.column(|ui| {
for i in 0..5 {
let mut ui = ui.with_add_id_pref(i);
ui.label(&format!("Item {i}"));
}
});
ui.column(|ui| {
for i in 0..5 {
let mut ui = ui.with_add_id_pref(i);
if ui.button(&format!("Button {i}")).clicked() {
params.click_count += 1;
}
}
});
});
});You can add new capabilities with just a few lines of code.
Here’s how .selected(...) is implemented.
/// Implements capability to mark entities as selectable.
pub struct CapabilityUiSelectable;
impl ImmCapability for CapabilityUiSelectable {
fn build<Cap: CapSet>(app: &mut bevy_app::App, cap_req: &mut crate::ImmCapAccessRequests<Cap>) {
cap_req.request_component_write::<Selectable>(app.world_mut());
}
}
/// Marks component as being selectable
#[derive(bevy_ecs::component::Component)]
pub struct Selectable {
/// Is selectable component selected
pub selected: bool,
}
/// Implements methods to set entity selectable
pub trait ImmUiSelectable {
/// Insert [`Selected`] component with given boolean value
///
/// Useful for styling purposes
fn selected(self, selected: bool) -> Self;
}
impl<Cap> ImmUiSelectable for ImmEntity<'_, '_, '_, Cap>
where
Cap: ImplCap<CapabilityUiSelectable>,
{
fn selected(mut self, selected: bool) -> Self {
if let Ok(Some(mut comp)) = self.cap_get_component_mut::<Selectable>() {
if comp.selected != selected {
comp.selected = selected;
}
return self;
}
self.entity_commands().insert(Selectable { selected });
self
}
}New child entities can be created with .ch, .ch_id, .tch, .ch_with_manual_id and related helpers.
For child entity creation that could appear, disappear, or that are created inside a loop: a unique id must be provided.
Provided id is combined with the parent id. Ids must be unique between siblings in the same frame.
| API | When to use |
|---|---|
.tch() |
Stable UI at a fixed call site: id comes from #[track_caller] (file, line, column) plus parent scope. If you call .tch() from a shared helper, add #[track_caller] on that helper so the id matches the real call site. |
.ch_id(...) / lch!(ui, …) |
Explicit or line/column-based ids; loops need a per-iteration suffix (see below). |
.ch() |
Sequential auto ids only when sibling count is fixed and entities never appear/disappear between frames. |
.with_add_id_pref(...) |
Useful in loops (and similar dynamic scopes) when using .ch() or .tch() so each iteration gets its own id prefix. |
If two siblings reuse the same id in one frame, they map to the same entity and UI will misbehave. Use with_add_id_pref or explicit ids in loops.
Examples:
ui.tch(); // Id is automatically assigned based on code location (file, line, column)
for idx in 0..count {
// Inside loops still unique idx must be provided to uniquely identify UI entity
let mut ui = ui.with_add_id_pref(idx);
ui.tch();
}
// Helper methods that use tch needs to use #[track_caller] annotation
#[track_caller]
fn helper(ui: &ImmEntity<Caps>) {
ui.tch();
}
helper(ui);
helper(ui);
// ################################################################
ui.ch(); // Uses internal counter for id generation;
// Should not be used for appearing/disappearing entities
// (such entities may be misidentified between frames).
ui.ch_id("my_id"); // Child entity created with unique id by combining parent scope id and given argument.
ui.ch_id(lid!()); // The same as above, but given argument is code location (line, column)
lch!(ui); // The same as ui.ch_id(lid!())
for idx in 0..count {
ui.ch_id(("my_loop", idx)); // Inside loops you need to provide additional unique id source
ui.ch_id(lid!(idx));
lch!(ui, idx);
// Or use `with_add_id_pref` to provide unique id source to all following calls in scope
let mut ui = ui.with_add_id_pref(idx);
ui.tch();
ui.ch();
ui.ch_id("my_id"); // Child entity created with unique id by combining parent scope id and given argument.
ui.ch_id(lid!()); // The same as above, but given argument is code location (line, column)
lch!(ui); // The same as ui.ch_id(lid!())
}
lid! / lch! use the macro’s line and column for id data. Inside a loop, the line/column is the same every iteration. Use with_add_id_pref, ch_id, or lch!(ui, idx) so each item gets a distinct id.
Follow: Instructions & Limitations
Launch examples with:
BEVY_ASSET_ROOT="." dx serve --hot-patch --features "bevy_immediate/hotpatching" --features "bevy/hotpatching" --example demo
Make sure that you enable hotpatching feature bevy_immediate and bevy crates so that UI is recreated upon hotpatch.
Try to modify and save ./examples/hot_patching.rs or any other example and see changes in the live demo.
Make sure that you assign unique id using ch_id for ui nodes that
can appear, disappear.
- Queries: Add
Without<ImmMarker<Caps>>to your query filter. - Resources: Avoid direct conflicts, or use .ctx() / .ctx_mut() APIs to access resources used by capabilities.
Contributions are welcome!
- Add your improvements to examples
- Suggest or implement new capabilities useful for UI creation
Publish your own crate that is built using bevy_immediate!
- Originally created for Settletopia
- Inspired by egui_taffy.
- Initial idea discussion
-
Easier definition of new capability sets
- Tried transitive capability implementation (works only inside one crate)
- Tried transitive trait implementation (works only inside one crate)
- Tried TupleList approach (conflicting trait implementations)
- ???
-
Create reusable logic for:
- Bevy ui widgets
- Bevy scroll areas
- Tooltips
- Popups
- Draggable, resizable windows (like
egui::Window)
