Logo

Blazingly fast retained layout engine for Bevy entities, built around vanilla Bevy ECS. It gives you the ability to make your own custom UI using regular ECS like every other part of your app.

important

This book is made for version 0.3.X of Bevy_Lunex

note

This crate is being maintained by a university student. Don't expect updates during the semester.

warning

This crate is opinionated and thus you must decide if it is a good fit for what you want to achieve.

This is mainly because Lunex provides you with only capability to position entities, leaving everything else in your hands. The current version also lacks any kind of flexbox-like layout.

Good fit πŸ‘

  • Worldspace 3D UI
  • Spritebased 2D UI
  • Custom rendering hook
  • Very customizable
  • Low-level interactivity

Not so good πŸ‘Ž

  • Development speed & iteration
  • Using prebuilt input components
  • Making desktop application UI

Installation & Setup

Adding Bevy_Lunex to your project is straightforward, just like any other Rust crate.

Add the following to your Cargo.toml:

[dependencies]
  bevy_lunex = { version = "*" }

Alternatively, you can use the latest bleeding edge version from the Git repository:

[dependencies]
  bevy_lunex = { git = "https://github.com/bytestring-net/bevy_lunex" }

Project Setup

You have to add the UiLunexPlugin to your application.

fn main() -> AppExit {
    App::new()
        // Add necessary plugins
        .add_plugins((DefaultPlugins, UiLunexPlugin))
        .run()
}

Next you have to spawn your camera. Your main camera must have the UiSourceCamera::<N> component, with N being a constant from 0..3 range.

note

The purpose of this is that if you are creating a splitscreen game, you can have up to 4 cameras. This component tells the UI which camera's viewport size to use as the root node size.

tip

If you need more indexes, you can add UiLunexIndexPlugin::<N> for said index manually.

fn spawn_camera(mut commands: Commands) {
    // Spawn the camera
    commands.spawn((

        // This camera will become the source for all UI paired to index 0.
        Camera2d, UiSourceCamera::<0>,
        
        // Ui nodes start at 0 and move + on the Z axis with each depth layer.
        // This will ensure you will see up to 1000 nested children.
        Transform::from_translation(Vec3::Z * 1000.0),
        
        // Explained in # Chapters/Debug-Tooling section of the book
        RenderLayers::from_layers(&[0, 1]),
    ));
}

Debug Tooling

Sometimes it is hard to know why your UI is behaving unexpectedly. To help you with debugging, Lunex offers additional tooling that should make your life a little bit easier.

To enable it, you have to add UiLunexDebugPlugin::<R_2D, R_3D> to your application. The generics are constants used for RenderLayers inside the debug plugin.

  • R_2D: Specifies which render layer should 2D gizmos use.
  • R_3D: Specifies which render layer should 3D gizmos use.

If you don't use RenderLayers for any other purpose, then you can add the plugin with these values:

UiLunexDebugPlugin::<1, 2>

This also means that you have to add a properly configured RenderLayers component to your cameras if you want to see these outlines.

  • For Camera2d:

    RenderLayers::from_layers(&[0, 1])
  • For Camera3d:

    RenderLayers::from_layers(&[0, 2])

This will draw gizmo outlines around all UI nodes, allowing you to see their positions and sizes.

Debug

Additionally, it will print the layouts to the terminal whenever a change is detected.

β–Ά 11v1 β‡’ [w: 1920, h: 1080]
  β”œβ”€ Background β‡’ [w: 1920, h: 1080, d: 1] ➜ Solid
  └─ 13v1 β‡’ [w: 595, h: 1080, d: 1] ➜ Solid
  ┆  β”œβ”€ Panel β‡’ [w: 624, h: 1134, d: 2] ➜ Window
  ┆  β”œβ”€ 15v1 β‡’ [w: 624, h: 216, d: 2] ➜ Window
  ┆  β”‚  └─ Logo β‡’ [w: 624, h: 192, d: 3] ➜ Solid
  ┆  └─ 17v1 β‡’ [w: 327, h: 367, d: 2] ➜ Window
  ┆  ┆  β”œβ”€ New Game β‡’ [w: 327, h: 51, d: 3] ➜ Window
  ┆  ┆  β”‚  └─ 23v1 β‡’ [w: 327, h: 51, d: 4] ➜ Window
  ┆  ┆  β”‚  ┆  β”œβ”€ 24v1 β‡’ [w: 113, h: 31, d: 5] ➜ Window
  ┆  ┆  β”‚  ┆  └─ 25v1 β‡’ [w: 22, h: 31, d: 5] ➜ Window
  ┆  ┆  β”œβ”€ Settings β‡’ [w: 327, h: 51, d: 3] ➜ Window
  ┆  ┆  β”‚  └─ 31v1 β‡’ [w: 327, h: 51, d: 4] ➜ Window
  ┆  ┆  β”‚  ┆  β”œβ”€ 32v1 β‡’ [w: 98, h: 31, d: 5] ➜ Window
  ┆  ┆  β”‚  ┆  └─ 33v1 β‡’ [w: 22, h: 31, d: 5] ➜ Window
  ┆  ┆  └─ Quit Game β‡’ [w: 327, h: 51, d: 3] ➜ Window
  ┆  ┆  ┆  └─ 43v1 β‡’ [w: 327, h: 51, d: 4] ➜ Window
  ┆  ┆  ┆  ┆  β”œβ”€ 44v1 β‡’ [w: 111, h: 31, d: 5] ➜ Window
  ┆  ┆  ┆  ┆  └─ 45v1 β‡’ [w: 22, h: 31, d: 5] ➜ Window

Quick start

Now that we have everything setup, let's create some quick UI.

First, spawn a UiLayoutRoot. This is where our UI will start. You can specify the size of the UI viewport with Dimension component, but for 2D we don't want that. Instead we add UiFetchFromCamera::<N> with N being the index of our camera's UiSourceCamera::<N> component.

This will ensure that the Camera -> Dimension -> UiLayout pipeline will always be up to date.

// Create UI
commands.spawn((
    // Initialize the UI root for 2D
    UiLayoutRoot::new_2d(),

    // Make the UI synchronized with camera viewport size
    UiFetchFromCamera::<0>,

)).with_children(|ui| {

    // ... Here we will spawn our UI

});

And now inside the with_children closure we will spawn a red rectange node. This rectangle will be position exactly in the middle of our screen and with width 200px and height 50px.

ui.spawn((
    // You can name the entity
    Name::new("My Rectangle"),

    // Specify the position and size of the button
    UiLayout::window()
        .anchor(Anchor::Center) // Put the origin at the center
        .pos(Rl((50.0, 50.0)))  // Set the position to 50%
        .size((200.0, 50.0))    // Set the size to [200.0, 50.0]
        .pack(),

    // Color the sprite with red color
    UiColor::from(Color::srgb(1.0, 0.0, 0.0)),

    // Attach sprite to the node
    Sprite::from_image(asset_server.load("images/button.png")),

    // When hovered, it will request the cursor icon to be changed
    OnHoverSetCursor::new(SystemCursorIcon::Pointer),

// Interactivity is done through observers, you can query anything here
)).observe(|_: Trigger<Pointer<Click>>, mut exit: EventWriter<AppExit>| {
    
    // Close the app on click
    exit.send(AppExit::Success);
});

And thats it! You can of course do much more with the crate. Continue reading to learn on how to spawn text nodes, enable animations and much more!

Base Units

Lunex features 9 different UI units, which are used as arguments for UiValue<T>. The T is expected to be f32, Vec2, Vec3 or Vec4. They are used in layout functions where impl Into<UiValue<T>> is specified as argument.

  • Ab - Stands for absolute, usually Ab(1) = 1px
  • Rl - Stands for relative, it means Rl(1.0) == 1%
  • Rw - Stands for relative width, it means Rw(1.0) == 1%w, but when used in height field, it will use width as source
  • Rh - Stands for relative height, it means Rh(1.0) == 1%h, but when used in width field, it will use height as source
  • Em - Stands for size of symbol M, it means Em(1.0) == 1em, so size 16px if font size is 16px
  • Vp - Stands for viewport, it means Vp(1.0) == 1v% of the UiTree original size
  • Vw - Stands for viewport width, it means Vw(1.0) == 1v%w of the UiTree original size, but when used in height field, it will use width as source
  • Vh - Stands for viewport height, it means Vh(1.0) == 1v%h of the UiTree original size, but when used in width field, it will use height as source

Basic Operations

All unit types implement basic mathematical operations:

let a: Ab<f32> = Ab(4.0) + Ab(6.0); // -> 10px
let b: Ab<f32> = Ab(4.0) * 2.0;     // -> 8px

You can also combine different unit types:

let a: UiValue<f32> = Ab(4.0) + Rl(6.0); // -> 4px + 6%

If a unit is unspecified, the f32 value is considered to be in Ab unit:

let a: Ab<f32> = 5.0.into(); // -> 5px

Vector Definitions

You can easily define vectors using these units:

let a: UiValue<Vec2> = Ab(10.0).into();             // -> [10px, 10px]
let b: UiValue<Vec2> = Ab((10.0, 15.0)).into();     // -> [10px, 15px]
let c: UiValue<Vec2> = (Ab(10.0), Rl(5.0)).into();  // -> [10px, 5%]

Works for larger vectors like Vec3 and Vec4 the same.

tip

If you put them as arguments to impl Into<UiValue<T>>, you don't have to call .into().

Layouts

There are multiple layouts that you can utilize to achieve the structure you are aiming for.

Boundary

Defined by point1 and point2, it is not influenced by UI flow and is absolutely positioned.

  • pos1 - Position of the top-left corner
  • pos2 - Position of the bottom-right corner

This will make a node start at 20% and end at 80% on both axis from the parent node.

UiLayout::boundary()
    .pos1(Rl(20.0))
    .pos2(Rl(80.0))
    .pack()

Window

Defined by position and size, it is not influenced by UI flow and is absolutely positioned.

  • pos - Position of the node
  • anchor - The origin point relative to the rest of the node
  • size - Size of the node

This will make a node centered at x: 53%, y: 15% and with size width: 60% and height: 65%.

UiLayout::window()
    .pos(Rl((53.0, 15.0)))
    .anchor(Anchor::Center)
    .size(Rl((60.0, 65.0)))
    .pack()

Solid

Defined by size only, it will scale to fit the parenting node. It is not influenced by UI flow.

  • size - Aspect ratio, it doesn't matter if it is (10, 10) or (100, 100)
  • align_x - Horizontal alignment, -1.0 to 1.0 with 0.0 as default
  • align_y - Vertical alignment, -1.0 to 1.0 with 0.0 as default
  • scaling - If the container should fit inside parent or fill the parent

tip

This layout is ideal for images, because it preserves aspect ratio under all costs.

Here we will set aspect ratio to the size of our imaginary texture (881.0, 1600.0) in pixels. Then we can align it horizontally.

UiLayout::solid()
    .size((881.0, 1600.0))
    .align_x(-0.74)
    .pack(),

Contributors

This documentation is maintained by

Help

For issues related to the library, please create a ticket on GitHub.

You can also reach out to me on the Bevy Discord, where you can use the Bevy Lunex thread.

note

For specific questions, feel free to send me a direct message on Discord: @idedary.

Just make sure you are in the Bevy discord or otherwise I will ignore you.