Skip to content

nv-dev-labs/bevy_easy_ui

Repository files navigation

Bevy Easy UI

CI docs.rs Crates.io License

A declarative, fluent builder-pattern abstraction layer on top of Bevy's UI system and bevy_ui_text_input

⚠️ This crate is in early development. The API is stable for now but bevy_easy_ui is still evolving as bevy is evolving too, so expect some breaking changes in the future.

Version compatibility

bevy_easy_ui bevy_ui_text_input bevy
0.1.x 0.7.0 0.18.1

What is it

bevy_easy_ui turns this:

commands.spawn((
    Button,
    Node { width: px(200.0), height: px(80.0), ..default() },
    BorderColor::all(Color::WHITE),
    BackgroundColor(Color::BLACK),
))
.with_children(|parent| {
    parent.spawn((
        Text::new("Hello, Bevy!"),
        TextFont { font_size: 24.0, ..default() },
        TextColor(Color::WHITE),
        Label,
    ));
});

…into this:

EasyButton::new()
    .with_border_color(WHITE.into())
    .with_border(px(2.0), px(10.0))
    .with_width(px(200.0))
    .with_height(px(80.0))
    .with_background_color(BLACK.into())
    .with_child(
        EasyLabel::new("Hello, Bevy!")
            .with_color(WHITE.into())
            .with_font_size(24.0),
    )
    .spawn(&mut commands);

Every setter is chainable, type-checked, and the trait system prevents misusing a container as a non-container (or vice versa).


Quick start

# Cargo.toml
[dependencies]
bevy = "0.18.1"
bevy_easy_ui = "0.1.1"
use bevy::prelude::*;
use bevy_easy_ui::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);

    EasyVerticalLayout::new()
        .with_z_index(1)
        .with_width(percent(100.0))
        .with_height(percent(100.0))
        .with_justify_content(JustifyContent::Center)
        .with_align_items(AlignItems::Center)
        .with_child(
            EasyButton::new()
                .with_z_index(2)
                .with_border_color(WHITE.into())
                .with_border(px(2.0), px(10.0))
                .with_background_color(BLACK.into())
                .with_child(
                    EasyLabel::new("Hello, Bevy!")
                        .with_z_index(3)
                        .with_color(WHITE.into())
                        .with_font_size(24.0),
                ),
        )
        .spawn(&mut commands);
}

Run with cargo run — a centered dark button with a white border and white text appears.


Examples

Each example is a standalone cargo run --example NAME showcasing a specific widget or pattern.

Example What it shows
hello Minimal setup: a centered button with a label with hover and click feedback
image_button Icon button built from EasyButton + EasyImage as a child
rounded_image EasyImage with various border_radius values (sharp, small, full, asymmetric)
checkbox EasyCheckbox wired to a ValueChange<bool> observer that recolors its background
slider EasySlider + EasySliderThumb with a ValueChange<f32> observer that updates the thumb position
scroll Scrollable EasyVerticalLayout and EasyHorizontalLayout using Overflow::scroll_y() / scroll_x() + the ScrollPlugin mouse-wheel observer
viewport EasyViewport rendering a live camera output into a UI node
rich_text EasyRichText with per-EasySpan colors, sizes, and justify
text_input EasyTextInput (re-export of bevy_ui_text_input)
radio EasyRadioGroup + EasyRadioButton with a ValueChange<bool> observer that toggles the Checked state and recolors the radio buttons
cargo run --example hello
cargo run --example rich_text

Widgets

The crate ships a set of Easy* builders, each wrapping the matching Bevy component(s) with a fluent API.

Widget Bevy base Kind Purpose
EasyVerticalLayout Node + FlexDirection::Column container Flex column layout
EasyHorizontalLayout Node + FlexDirection::Row container Flex row layout
EasyButton Button + Node container Clickable button (accepts children + observers)
EasyRichText Text + TextSpan children container Multi-style text
EasySlider Slider + Node container Usable slider
EasyRadioGroup RadioGroup + Node container Group of radio buttons (only one can be checked)
EasyRadioButton RadioButton + Node non-container Individual radio button
EasySliderThumb SliderThumb + Node non-container Slider thumb for Slider
EasyCheckbox Checkbox + Checkable + Node non-container Checkable box
EasyLabel Text + Node + Label non-container Text marked as a label
EasyText Text + Node + TextFont + TextColor non-container Styled text
EasySpan TextSpan non-container Inline span used inside EasyRichText
EasyImage ImageNode + Node non-container Image (rect, color, flip, mode, atlas)
EasyTextInput bevy_ui_text_input::TextInputNode non-container Re-export of bevy_ui_text_input
EasyViewport Node + ViewportNode non-container UI node displaying a Camera render target

Containers (layouts, button, rich_text, viewport) implement the Container trait and expose:

  • .with_child(impl Into<EasyElement>) — adds a child
  • .with_observer(impl IntoObserverSystem) — attaches a Bevy observer
  • .spawn(&mut Commands) — finalizes and spawns the tree

Non-containers (label, text, image, text_input) implement WithObservers and only expose .with_observer(...) and .spawn(...).


Reusable styles with with_style

Every widget also exposes a with_style(style: <Widget>Style) setter that swaps the whole style bundle at once — Node + EasyBoxStyle + EasyStackStyle (+ EasyTextStyle for text widgets). It's the same shape as a Bevy bundle, but assembled ahead of time.

Use it when you have a few pre-defined looks (e.g. a theme) you want to apply as a unit, without chaining ten with_* calls every time:

use bevy::prelude::*;
use bevy_easy_ui::prelude::*;

fn primary_button_style() -> EasyButtonStyle {
    EasyButtonStyle {
        node: Node {
            width: px(200.0),
            height: px(64.0),
            padding: UiRect::all(px(12.0)),
            ..default()
        },
        box_style: EasyBoxStyle {
            background_color: BackgroundColor(BLUE.into()),
            border_color: BorderColor::all(WHITE.into()),
            ..default()
        },
        stack_style: EasyStackStyle {
            z_index: ZIndex(2),
            ..default()
        },
    }
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);

    EasyVerticalLayout::new()
        .with_width(percent(100.0))
        .with_height(percent(100.0))
        .with_justify_content(JustifyContent::Center)
        .with_align_items(AlignItems::Center)
        .with_child(
            EasyButton::new()
                .with_style(primary_button_style())
                .with_child(
                    EasyLabel::new("Click me!")
                        .with_color(WHITE.into())
                        .with_font_size(24.0),
                ),
        )
        .spawn(&mut commands);
}

The available style types are:

  • EasyButtonStyle
  • EasyVerticalLayoutStyle
  • EasyHorizontalLayoutStyle
  • EasyRichTextStyle
  • EasyCheckboxStyle
  • EasyLabelStyle
  • EasyTextInputStyle
  • EasySpanStyle
  • EasyImageStyle
  • EasyViewportStyle
  • EasySliderThumbStyle
  • EasySliderStyle
  • EasyRadioGroupStyle
  • EasyRadioButtonStyle

⚠️ They'll probably be refactored in the future as most of them have the same structure.


The traits

Four extension traits add the builder setters on top of Bevy's components. They are implemented for every widget that owns the matching Bevy component, so the setters are always available.

EasyNodeNode properties

  • Size (with_width, with_height, with_min_*, with_max_*),
  • Position (with_position, with_top, etc.),
  • Alignment (with_align_items, with_justify_content, …),
  • Spacing (with_margin, with_padding, with_row_gap, with_column_gap),
  • Borders (with_border, with_border_radius; with_border_color is in EasyBoxStyleExt),
  • Flex (with_flex_direction, with_flex_wrap, with_flex_grow, with_flex_shrink, with_flex_basis),
  • Grid (with_grid_template_*, with_grid_auto_*, with_grid_row, with_grid_column),
  • Overflow (with_overflow, with_scrollbar_width, with_overflow_clip_margin),
  • Display (with_display, with_box_sizing, with_aspect_ratio).

EasyBoxStyleExt — background, border, shadow

  • with_background_color
  • with_border_color
  • with_box_shadow
  • with_border_gradient
  • with_background_gradient
  • with_outline

EasyStackStyleExt — z-index

  • with_z_index,
  • with_global_z_index

EasyTextStyleExt — text-specific

  • with_color
  • with_font_family
  • with_font_size
  • with_font_weight
  • with_smoothing
  • with_features
  • with_justify
  • with_linebreak
  • with_line_height
  • with_text_shadow

EasyImageNodeImageNode properties

  • with_image
  • with_image_color
  • with_texture_atlas
  • with_flip_x
  • with_flip_y
  • with_rect
  • with_image_mode

Scrollable containers

The crate ships a tiny ScrollPlugin that turns the mouse wheel into a Scroll event you can attach to any Overflow::scroll_*() node:

use bevy::prelude::*;
use bevy_easy_ui::prelude::*;

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, ScrollPlugin))
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);

    EasyVerticalLayout::new()
        .with_width(px(320.0))
        .with_height(px(300.0))
        .with_overflow(Overflow::scroll_y())
        .with_padding(px(10.0))
        .with_child(EasyLabel::new("Top of the list"))
        // … more children …
        .spawn(&mut commands);
}

Hold Ctrl while scrolling to scroll horizontally instead.


Colors

The prelude re-exports bevy::color::palettes::css::* (WHITE, BLACK, BLUE, DARK_GRAY, LIGHT_BLUE, …) so every color literal slots directly into a with_*_color(...into()) call:

EasyButton::new()
    .with_background_color(BLUE.into())
    .with_border_color(WHITE.into())

If you need a custom color, build it with bevy::color::Color::srgba(...) and pass it through .into().


Contribution

Open an issue or a PR if you have suggestions, questions, or want to add a new widget or feature.

Roadmap

The following widgets are planned but not yet wrapped as Easy* builders. They will be implemented by following the same pattern as the existing widgets, on top of the corresponding headless types in bevy_ui_widgets:

  • MenuPopup + MenuItem — wraps bevy_ui_widgets::Menu + MenuItem
  • EasyScrollbar — wraps bevy_ui_widgets::Scrollbar + CoreScrollbarThumb

If you'd like to take one of these, the integration checklist below explains the wiring once the widget compiles.

⚠️ bevy_ui_widgets is still in active development, so the builder APIs can change as the underlying components evolve.

Adding a new widget

The crate follows a consistent pattern across all widgets — pick whichever existing widget is closest to what you want to build, then copy it:

  1. Bundle#[derive(Bundle)] pub struct EasyXxx { ... pub node: Node, pub box_style: EasyBoxStyle, pub stack_style: EasyStackStyle }. The bundle is the raw Bevy components grouped together.
  2. Containerpub struct EasyXxxContainer { bundle, children: Vec<EasyElement>, observers: Vec<Observer> }. Holds the bundle plus any children/observers queued during building.
  3. BuilderEasyXxx::new() -> EasyXxxContainer and EasyXxx::default_bundle() -> Self. new() always returns the container, never the bundle, so setters stay chainable.
  4. Accessor implsEasyNode, EasyBoxStyleExt, EasyStackStyleExt (and EasyTextStyleExt for text widgets). They expose the with_* setters.
  5. Container / WithObservers impl — picks the right trait: Container if the widget can have children, WithObservers for non-containers.
  6. Style — a pub struct EasyXxxStyle { node, box_style, stack_style } with with_style(...) on the builder, so users can swap the whole look at once.

Integration checklist

Once the widget compiles, wire it into the rest of the crate so users find it under one import:

  • Add a pub mod line in src/widgets/containers/mod.rs (or src/widgets/mod.rs for non-container widgets).
  • Add a variant EasyXxxContainer(EasyXxxContainer) in src/core/element.rs and a matching From<EasyXxxContainer> for EasyElement impl.
  • Re-export the bundle, container, and style with pub use ...::*; in src/prelude.rs.
  • Add a cargo run --example xxx example in examples/ and reference it in the Examples table of this README.

Filing issues

For bug reports, include the Bevy version, the crate version, a minimal repro, and what you expected vs. what you got. For feature requests, sketch the API you'd like to call — EasyXxx::new().with_*(...).with_child(...).spawn(&mut commands) is the shape we aim for.


Known limitations

0.1.x releases — the API works and is covered by the ten examples, but it is still a young library with rough edges. Things will move, names will change, and some patterns may not be fully fleshed out yet. Contributions and bug reports are very welcome, and feedback from early users is the fastest way to make the next version better.

If you hit something unexpected, please open an issue — even small reports help prioritize what to harden next.

Attaching custom components to a widget

Builder methods like .with_child(...) and .with_observer(...) consume the builder, so a custom Component cannot be chained in. Spawn the builder first to get its Entity, then use commands.entity(id).insert(...); re-parenting a spawned widget works the same way with add_children.

Questions to answer in future iterations:

  • Should we set all widgets as a container or only those that can have children? The latter is more type-safe and enforces a kind of convention, but the former is more flexible and let the user decide. What do you think?

  • ...


License

Dual-licensed under MIT or Apache-2.0 at your option.

About

A declarative, fluent builder-pattern abstraction layer on top of Bevy's UI system and bevy_ui_text_input

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages