Crate bevy_immediate

Crate bevy_immediate 

Source
Expand description

§bevy_immediate: Immediate Mode UI for Bevy

bevy_version Latest version Documentation License

A simple, fast, and modular UI library for Bevy, combining immediate mode ergonomics with Bevy ECS-powered retained UI.

  • Write complex UI logic as simple Rust code.
  • No macros, observers, triggers, events, signals.
  • Focus on your UI, not the boilerplate.

§Features

  • Immediate mode entity hierarchy management
    Build interactive entity hierarchies with a clean API.
  • Custom extension support
    Add custom capabilities like .clicked(), .selected(true), .hovered(). Extension use integrated with rust type system for IDE and compile 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.
  • 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
    Extend the API with your own small capabilities and traits that encapsulate complex logic.
  • Integration-friendly
    Works with other libraries (e.g., reloadable CSS style with bevy_flair).
  • Hot reloading support
    Can be added via hot_lib_reloader.

⚠️ Note: This library is under active development. Expect some breaking changes, but they will be minimized.

§Version compatibility

bevy_immediatebevyMSRV
0.10.161.85

To use add bevy_immediate to your project dependencies in Cargo.toml file.

See CHANGELOG for changes between versions.

§Examples

Check out ./examples/ (cargo run --example demo).

image

Example with code reuse and interactive elements:


pub struct MenuExamplePlugin;

impl bevy_app::Plugin for MenuExamplePlugin {
    fn build(&self, app: &mut bevy_app::App) {
        app.insert_resource(CurrentExample::WidgetUse);

        app.add_plugins(BevyImmediateAttachPlugin::<CapsUi, MenuUiRoot>::new());
    }
}

#[derive(Component)]
pub struct MenuUiRoot;

#[derive(SystemParam)]
pub struct Params<'w> {
    current_example: ResMut<'w, CurrentExample>,
    debug_options: ResMut<'w, UiDebugOptions>,
}

impl ImmediateAttach<CapsUi> for MenuUiRoot {
    type Params = Params<'static>;

    fn construct(ui: &mut Imm<CapsUi>, params: &mut Params) {
        ui.ch()
            .on_spawn_insert(|| Node {
                flex_direction: FlexDirection::Column,
                align_items: bevy_ui::AlignItems::Stretch,
                ..fill_parent_node()
            })
            .add(|ui| {
                ui.ch()
                    .on_spawn_insert(styles::title_text_style)
                    .on_spawn_text("Demo");
                ui.ch()
                    .on_spawn_insert(styles::text_style)
                    .on_spawn_text("bevy_immediate");

                ui.ch().on_spawn_insert(|| Node {
                    height: Val::Px(10.),
                    ..default()
                });

                for (example, title) in MENU_VARIANTS {
                    let mut button = ui
                        .ch()
                        .on_spawn_insert(styles::button_bundle)
                        .selected(example == *params.current_example)
                        .add(|ui| {
                            ui.ch()
                                .on_spawn_insert(styles::text_style)
                                .on_spawn_text(title);
                        });

                    if button.clicked() {
                        *params.current_example = example;
                    }
                }

                ui.ch().on_spawn_insert(|| Node {
                    flex_grow: 1.,
                    ..default()
                });

                let mut button = ui
                    .ch()
                    .on_spawn_insert(button_bundle)
                    .selected(params.debug_options.enabled)
                    .add(|ui| {
                        ui.ch().on_spawn_insert(text_style).text("Debug");
                    });
                if button.clicked() {
                    params.debug_options.enabled = !params.debug_options.enabled;
                }
            });
    }
}

pub const MENU_VARIANTS: [(CurrentExample, &str); 4] = [
    (CurrentExample::HelloWorld, "Hello World"),
    (CurrentExample::WidgetUse, "Widget usage"),
    (CurrentExample::ExtensionUse, "Extension usage"),
    (CurrentExample::PowerUser, "Power user"),
];

#[derive(Resource, Hash, Clone, Copy, PartialEq, Eq)]
pub enum CurrentExample {
    WidgetUse,
    HelloWorld,
    ExtensionUse,
    PowerUser,
}

§Power user example

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!");
    }
}

§Extend functionality by implementing new capability

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 {
                return self;
            }
            comp.selected = selected;
            return self;
        }

        self.entity_commands().insert(Selectable { selected });
        self
    }
}

§FAQ

§UI nodes are changing order and not correctly laid out

Make sure that you assign unique id using ch_id for ui nodes that can appear, disappear.

§How do I avoid collisions with resources or queries in my systems?

  • 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.

§Contributing

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!

§Inspiration

§Future work

  • 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)

    Therefore currently to define capability set users need to list all used capabilities.

  • Improve examples

    • Add Example for hot reloading
  • Create reusable logic for:

    • Scroll areas
    • Tooltips
    • Windows (like egui::Window)

Re-exports§

pub use immediate::*;

Modules§

attach
Logic to provide immediate mode api to attach entity tree to already existing entity
immediate
Base logic to provide immediate mode api
ui
Extensions to provide ergonomic functionality for working with creating UI (bevy_ui) in immediate mode
utils
Utility types to simplify implementation

Macros§

impl_capability_set
Implements list of capabilities for given type

Structs§

ImmCapAccessRequests
Tracks what kind of query accesses capability has requested
ImmCapAccessRequestsResource
Stores requested capabilities for given immediate mode request
ImmCapQueryParam
You can retrieve components that were requested by capabilities. See FilteredEntityMut

Traits§

CapSet
Type implement support for set of ImmCapability
ImmCapability
Marks types that are used to implement immediate mode capabilities
ImplCap
Trait that marks that CapSet implements given capability implementation