Giter VIP home page Giter VIP logo

bevy_lunex's Introduction

image

Caution

This branch is not released yet and is still WIP.

Blazingly fast path based retained layout engine for Bevy entities, built around vanilla Bevy ECS. This library is intended to replace the existing bevy_ui crate, but nothing is stopping you from using them both at the same time.

It uses a combination of Bevy's built-in hierarchy and its own custom hierarchy to give you the freedom of control without the bloat or borrow checker limitations usually faced when creating UI.

It gives you the ability to make your own custom UI using regular ECS like every other part of your app.

TLDR: It positions your entities as HTML objects for you, so you can slap custom rendering or images on them.

Showcase

image

^ A recreation of Cyberpunk UI in Bevy. (Source code here).

Description

Note

This library is EXPERIMENTAL. I do not guarantee consistent updates. I'm developing it for my own personal use, so if I judge it has outlived its use case, I will stop developing this project.

Bevy_Lunex is built on a simple concept: to use Bevy's ECS as the foundation for UI layout and interaction, allowing developers to manage UI elements as they would any other entities in their game or application as opposed to bevy_ui.

  • Path-Based Hierarchy: Inspired by file system paths, this approach allows for intuitive structuring and nesting of UI elements. It's designed to make the relationship between components clear and manageable, using a syntax familiar to most developers, while also avoiding the safety restrictions Rust enforces (as they don't help but instead obstruct for UI).

  • Retained Layout Engine: Unlike immediate mode GUI systems, Bevy_Lunex uses a retained layout engine. This means the layout is calculated and stored, reducing the need for constant recalculations and offering potential performance benefits, especially for static or infrequently updated UIs.

  • Built on top of ECS: Since it's built with ECS, you can extend or customize the behavior of your UI by simply adding or modifying components. The scripting is also done by regular systems you are familiar with.

  • 2D & 3D UI: One of the features of Bevy_Lunex is its support for both 2D and 3D UI elements, leveraging Bevy's Transform component. This support opens up a wide range of possibilities for developers looking to integrate UI elements seamlessly into both flat and spatial environments.

Workflow

First, we need to define a component, that we will use to mark all entities that will belong to our ui system.

#[derive(Component, Default)]
pub struct MyUiSystem;

Then we need to add UiPlugin with our marker component. The NoData generics are used if you need to store some data inside the nodes.

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(UiPlugin::<NoData, NoData, MyUiSystem>::new())
        .run();
}

By marking any camera with MyUiSystem, it will pipe its size into our future UI system entity.

commands.spawn((
    MyUiSystem,
    Camera2dBundle {
        transform: Transform::from_xyz(0.0, 0.0, 1000.0),
        ..default()
    }
));

Now we should create our entity with the UI system. The base componets are UiTree + Dimension + Transform. The UiTreeBundle already contains these components. The newly introduced Dimension component is used as the source size for the UI system. We also need to add the MovableByCamera component so our entity will receive updates from camera. The last step is adding our MyUiSystem type as a generic.

commands.spawn((
    UiTreeBundle::<NoData, NoData, MyUiSystem> {
        tree: UiTree::new("MyUiSystem"),
        ..default()
    },
    MovableByCamera,
)).with_children(|ui| {
    // Here we will spawn our UI in the next code block ...
});

Now, any entity with MyUiSystem + UiLayout + UiLink spawned as a child of the UiTree will be managed as a UI entity. If it has a Transform component, it will get aligned based on the UiLayout calculations taking place in the parent UiTree. If it has a Dimension component then its size will also get updated by the UiTree output. This allows you to create your own systems reacting to changes in Dimension and Transform components.

You can add a UiImage2dBundle to the entity to add images to your widgets. Or you can add another UiTree as a child, which will use the computed size output in Dimension component instead of a Camera piping the size to it.

ui.spawn((
    MyUiSystem,
    UiLink::path("Root"),
    UiLayout::Window::FULL.pos(Ab(20.0)).size(Rl(100.0) - Ab(40.0)).pack(),
));

ui.spawn((
    MyUiSystem,
    UiLink::path("Root/Rectangle"),
    UiLayout::Solid::new().size(Ab((1920.0, 1080.0))).pack(),
    UiImage2dBundle::from(assets.load("background.png")),
));

UiLink is what is used to define the the custom hierarchy. It uses / as the separator. If any of the names don't internally exist inside the parent UiTree, it will create them.

As you can see in the terminal (If you have added a UiDebugPlugin), the final structure looks like this:

> MyUiSystem == Window [pos: (x: 0, y: 0) size: (x: 100%, y: 100%)]
    |-> Root == Window [pos: (x: 20, y: 20) size: (x: -40 + 100%, y: -40 + 100%)]
    |    |-> Rectangle == Solid [size: (x: 1920, y: 1080) align_x: 0 align_y: 0]

Quite simple, isn't it? Best part is that by relying on components only, you are potentially able to hot-reload UI or even stream UI over the network. The downside is that by relying on strings to link entities, we are giving up some safety that Rust provides. But I am all for using the right tools for the right task. By putting away some safety, we can skip the bothersome bloat that would otherwise be required for such application.

Nodes & Units

There are multiple nodes in UiLayout.

  • Window - Defined by point and size, it is not influenced by UI context and is absolutely positioned.
  • Solid - Defined by size only, it will scale to fit the parenting node. It is not influenced by UI context.
  • Div - Defined by padding & margin. Dictates the UI context. It uses styleform paradigm, very similar to HTML.

Warning

Div is not finished, it's WIP, please refrain from using it.

This library comes with several UI units. They are:

  • 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
  • Sp - Stands for remaining space, it's used as proportional ratio between margins, to replace alignment and justification. Only used by Div
  • 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

Warning

Sp is not finished, it's WIP, please refrain from using it.

Versions

Bevy Bevy Lunex
0.13.2 0.1.0 - latest
0.12.1 0.0.10 - 0.0.11
0.12.0 0.0.7 - 0.0.9
0.11.2 0.0.1 - 0.0.6

Warning

Any version below 0.0.X is experimental and is not intended for practical use.

Contributing

Any contribution submitted by you will be dual licensed as mentioned below, without any additional terms or conditions. If you have the need to discuss this, please contact me.

Licensing

Released under both APACHE and MIT licenses. Pick one that suits you the most!

bevy_lunex's People

Contributors

aecsocket avatar idedary avatar ukoehb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

bevy_lunex's Issues

Efficient cursor mapping

The cyberpunk example has this code, where you iterate over all widgets to detect which ones are underneath the cursor. I'd like to propose a more efficient approach.

New rules:

  1. Only widgets at the same level in a tree are sorted. A child widget of one branch is always sorted higher than a child widget of another branch if the first branch is sorted above the second branch. (this may already be a rule idk)
  2. The bounding box of a widget always automatically expands/shrinks to encompass all child widgets.

Now, to identify which widget is under the cursor, you find the highest-sorted widget at the base of the tree that contains the cursor. Then the highest sorted child widget of that widget under the cursor, and so on until you reach the terminal widget under the cursor. If that widget does not 'interact' with the cursor state (is non-blocking [e.g. invisible], has no registered handlers for the cursor state [e.g. an invisible list widget may respond to mouse wheel events but wants clicks and hovers to fall through], and does not have any rules propagating the cursor state to parent widgets [this may be redundant if you need a handler to propagate to parents]), then backtrack to the next candidate. Continue back-tracking until you find a viable candidate to consume the cursor state/event.

Re: 'propagating cursor interactions to parent widgets'
Since only the most-viable candidate (the top-most widget under the cursor) is selected for cursor interactions, if you want multiple widgets to react to a cursor (e.g. a stack of widgets that are affected by cursor hover or click), then we need a way to propagate cursor interactions to associated widgets. I think you could have 'interaction bundles' or maybe 'interaction channels' or maybe just entity relations that propagate cursor events as in bevy_event_listener.

To round off this proposal, you could register event handlers to each widget and implement an event handling algorithm to do all of the above automatically.

General feedback

Comments and suggestions I had while reading/reviewing/using the crate.

  • Rename: is_within() -> contains_position() (or just contains()).
  • Rename (examples): system -> ui.
  • Optimize: cursor_update() move gets outside loop.
  • Why is UiTree a component and not a resource if you can only have one of them?
  • The noun-first naming pattern is hard to read. For example: position_get() would be much better as get_position().
  • Instead of using pack(), let Widget::create() take impl Into<LayoutPackage>, then call .into() on it.
  • In WindowLayout, relative is in units of % while _relative are normalized (inconsistent).
  • In ImageElementBundle, the image_dimensions field is NOT intuitive. Increasing the numbers reduces the image size? Changing the ratio does nothing?

Further thoughts I will drop in the comments of this issue. Overall I am excited to continue using this crate and to see it grow.

can't use multiple cameras

Using multiple cameras is quite common, for example with a HUD or minimap.
Currently, this line panics when multiple cameras exist:
https://github.com/bytestring-net/bevy-lunex/blob/672d977d793bd215f46bcbc44a8277b3c413d81f/crates/bevy_lunex_ui/src/code/cursor.rs#L64

Perhaps tag the expected camera with a LunexCamera component and add that to the query.
So instead of this query:

cameras: Query<(&Camera, &Transform), Without<Cursor>>

you'd have this:

cameras: Query<(&Camera, &Transform, &LunexCamera), Without<Cursor>>

Request for clarity on 1.0 release plans

(Usually there are communities i can go to, but there doesnt seem to be one so sorry if this is the wrong place for this)

I'd like to start contributing, and got really excited about this project, but it seems I picked a weird time. Id like a window into the plans and potential features/differences from the current system to 1.0.
If its not all concrete yet, and you'd rather not. Then a peek into whats currently being thought about/lightly worked on

(i dug around and found blueprint_ui but it didnt help my confusion, is this all just frozen until further notice?)

Widget building ergonomics

I think widget builders would improve ergonomics substantially. It doesn't seem like element builders would be quite as useful though.

Before:

let slider_bar = lunex::Widget::create(
        &mut ui,
        root.end("slider_bar"),
        lunex::RelativeLayout::new()
            .with_rel_1(Vec2 { x: slider_x_left, y: 49.0 })
            .with_rel_2(Vec2 { x: slider_x_right, y: 51.0 })
    ).unwrap();

After:

let slider_bar = lunex::RelativeWidgetBuilder::new()
    .with_rel_1(Vec2 { x: slider_x_left, y: 49.0 })
    .with_rel_2(Vec2 { x: slider_x_right, y: 51.0 })
    .build(&mut ui, root.end("slider_bar"))
    .unwrap();

Hot reloading thoughts

Here are some preliminary thoughts about how to implement hot-reloading.

  1. Add extra inner id to widgets.
  • Widget paths point to widgets with the highest current inner ids (among widgets with the same name).
  • Widgets that don't have the highest current inner id are garbage collected.
    • When rebuilding a UI branch, increment the inner id of the branch root widget and all its descendents.
  1. Track which UI branches use specific styles.
  • If a style changes in-file, rebuild the UI branches that use that style.
  1. Track which function calls generate which UI branches.
  • When the file-tracker identifies that a hot-reload-marked function has changed, rebuild the UI branches that were generated by that function (i.e. call that function again).

Text Input

Hello! New to the bevy ecosystem, and this is the first UI library I've seen that has more than a simple draw a button UI example as their most complex sample. So, thank you! The Cyberpunk recreating is really well done.

However, something I notice missing from most UI libraries for Bevy is any form of text input. Do you have an example of a simple single field text input being used, and then its contents referenced anywhere? I'm trying to wrap my head around how I'm supposed to be setting up the object hierarchy and wiring it all up using this library

DSL thoughts

I have been thinking a bit about a DSL syntax for bevy_lunex. This syntax could easily be translated to pure-rust without too much additional verbosity, but I wanted to see if I could figure out a nice DSL.

Here is an example:

#[derive(LnxStyle)]
struct PlainTextStyle
{
    font      : &'static str,
    font_size : usize,
    color     : Color,
}

/// Deriving `LnxParams` transforms the text fields into lnx 'param methods' (see the example).
#[derive(LnxParams, Default)]
struct PlainTextParams
{
    justify: Justification,
    width: Option<f32>,
    height: Option<f32>,
    depth: f32,
}

impl Into<TextParams> for PlainTextParams
{
    fn into(self) -> TextParams
    {
        let params = match self.justify
        {
            Justification::Center => TextParams::center(),
            _ => unimplemented!()
        }
        params.set_width(self.width);
        params.set_height(self.height);
        params.depth(depth);
    }
}


#[lnx_prefab]
fn plain_text(lnx: &mut LnxContext, text: &str, params: PlainTextParams)
{
    let style = lnx.get_style::<PlainTextStyle>().unwrap();
    let text_style = TextStyle {
            font      : lnx.asset_server().load(style.font),
            font_size : style.font_size,
            color     : style.color,
        };

    let text_params: TextParams = params
        .into()
        .with_style(&text_style);
    lnx.commands().spawn(TextElementBundle::new(lnx.widget().clone(), text_params, text)); 
}

#[LnxStyleBundle]
struct BaseStyle
{
    plain: PlainTextStyle,
}

impl Default for BaseStyle
{
    //define the style
}

#[LnxStyleBundle]
struct DisplayStyle
{
    plain: PlainTextStyle,
}

impl Default for PlainTextStyle
{
    //define the style
}

let ui =
lnx!{
    // import rust bindings or lnx scripts
    use ui::prefabs::{plain_text, PlainTextParams};
    use ui::prefabs::text_params::Center;

    // registers style handles that can be referenced in the widget tree
    // - style bundles are default-initialize, unpacked, and stored in Arcs; copies are minimized in the widget tree
    styles[
        base_style: BaseStyle
        display_style: DisplayStyle
    ]

    // [r] creates a relative widget and implicitly sets it in the lnx context widget stack
    // - when entering a {} block, a new empty widget stack entry is added, then popped when leaving the block
    // - when a new widget is created within a block, the previous entry in the stack is over-written (but the other widget
    //   handle remains valid until leaving the block where it is created)
    [r] root: rx(0, 100) ry(0, 100)
    {
        // sets the current style in the lnx context style stack
        // - all previous styles in the stack will be hidden
        [style] set(base_style)

        // this nameless widget is implicitly a child of `root`
        [r] _: rx(20, 80), ry(42.5, 57.5)
        {
            // layers `display_style` on top of the previous style in the style stack
            // - the parent style will be used whenever the child style cannot satisfy a prefab's requirements
            [style] add(display_style)

            // style and parent widget are implicitly passed to the prefab
            // - we use 'param methods' to define parameters in the prefab
            // - if there is a param method name conflict, you must resolve the conflict with the names of the 
            //   target structs
            [inject] plain_text("Hello, World!", justify(Center), height(100), PlainTextParams::width(100))
        }
    }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.