bevy_easy_ui 0.1.0

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

bevy_easy_ui

CI docs.rs Crates.io

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

Version compatibility

bevy_easy_ui bevy_ui_text_input bevy
0.1.0 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("Click me!"),
        TextFont { font_size: 24.0, ..default() },
        TextColor(Color::WHITE),
    ));
});

…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("Click me!")
            .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.0"
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("Click me!")
                        .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
button_with_observers Buttons with Pointer<Over> / Pointer<Out> / Pointer<Click> observers (hover, 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)
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)
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
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 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, EasyLabelStyle, EasyTextWidgetStyle, EasySpanStyle, EasyImageStyle, EasyViewportStyle.


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 (text color), with_font_family, with_font_size, with_font_weight, with_smoothing, with_features, with_justify, with_linebreak, with_line_height, with_text_shadow / with_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:

  • EasyCheckbox — wraps bevy_ui_widgets::Checkbox + Checkable + Checked
  • EasySlider — wraps bevy_ui_widgets::Slider + SliderValue + SliderRange (+ optional SliderStep / SliderPrecision)
  • EasyRadioButton + EasyRadioGroup — wraps bevy_ui_widgets::RadioButton + RadioGroup
  • 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.

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

This is a 0.1.0 release — the API works and is covered by the eight 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.


License

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