Code Monkey home page Code Monkey logo

bevy_lunex's Introduction

image

Blazingly fast path based 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.

  • Any aspect ratio: Lunex is designed to support ALL window sizes out of the box without deforming. The built in layout types react nicely and intuitively to aspect ratio changes.

  • Optimized: Unlike immediate mode GUI systems, Bevy_Lunex is 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.

  • ECS focused: Since it's built with ECS, you can extend or customize the behavior of your UI by simply adding or modifying components. The interactivity is done by regular systems and events.

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

  • Custom cursor: You can style your cursor with any image you want! Lunex also provides easy drop-in components for mouse interactivity.

image

image

Try out the live WASM demo on Itch.io or GitHub Pages (Limited performance & stutter due to running on a single thread). For best experience compile the project natively. You can find source code here.

Syntax Example

This is an example of a clickable Button created from scratch using predefined components. As you can see, ECS modularity is the focus here. The library will also greatly benefit from upcoming BSN (Bevy Scene Notation) addition that Cart is working on.

commands.spawn((

	// #=== UI DEFINITION ===#

	// This specifies the name and hierarchy of the node
	UiLink::<MainUi>::path("Menu/Button"),

	// Here you can define the layout using the provided units (per state like Base, Hover, Selected, etc.)
	UiLayout::window().pos(Rl((50.0, 50.0))).size((Rh(45.0), Rl(60.0))).pack::<Base>(),


	// #=== CUSTOMIZATION ===#

	// Give it a background image
	UiImage2dBundle { texture: assets.load("images/button.png"), ..default() },

	// Make the background image resizable
	ImageScaleMode::Sliced(TextureSlicer { border: BorderRect::square(32.0), ..default() }),

	// This is required to control our hover animation
	UiAnimator::<Hover>::new().forward_speed(5.0).backward_speed(1.0),

	// This will set the base color to red
	UiColor<Base>::new(Color::RED),

	// This will set hover color to yellow
	UiColor<Hover>::new(Color::YELLOW),


	// #=== INTERACTIVITY ===#

	// This is required for hit detection (make it clickable)
	PickableBundle::default(),

	// This will change cursor icon on mouse hover
	OnHoverSetCursor::new(CursorIcon::Pointer),

	// If we click on this, it will emmit UiClick event we can listen to
	UiClickEmitter::SELF,
));

Documentation

There is a Lunex book for detailed explanations about the concepts used in Lunex. You can find it here: Bevy Lunex book.

For production ready example/template check out Bevypunk source code.

Versions

Bevy Bevy Lunex
0.14.0 0.2.0 - 0.2.3
0.13.2 0.1.0
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

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 badcodecat 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  avatar  avatar

bevy_lunex's Issues

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.

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

web/wasm support?

Does lunex support web/wasm? Is bevy punk on web already I can see?

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

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

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.

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();

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

FillPortion/Flex Unit [feature request]

Proposal

FillPortion units are a way to specify a portion of the total available space that an element should take up.
For example, if you have a container with a width 100px and you have 3 children with widths of 1 fill portion, 2 fill portions, and 1 fill portion, the children will take up 25px, 50px, and 25px respectively.
Right now there is support for relative units and absolute units, but no way to specify a portion of the available space.
This would be a useful feature for creating flexible layouts.

Out in the wild

To better explain the proposal, here are some examples of how this works in other libraries:
In the rust GUI library iced, there is a FillPortion unit that works exactly as described above:

Column::new()
	.push(Space::with_width(Length::FillPortion(1))) // Will take up 1/3 of the available space
	.push(Space::with_width(Length::FillPortion(2))) // Will take up 2/3 of the available space

In HTML/CSS, you can achieve a similar effect using flexbox:

<div style="display: flex; width: 100px;">
	<div style="flex: 1;"> 
		<!-- Will take up 1/3 of the available space -->
	</div>
	<div style="flex: 2;"> 
		<!-- Will take up 2/3 of the available space -->
	</div>
</div>

(note that the above HTML/CSS example can be somewhat replicated using bevy's own UI system)

How it could look in bevy_lunex

A new unit could be added, perhaps called Fp for FillPortion to match the naming of other units in bevy_lunex.

// A FillPortion of 3
let fill_portion = Fp(3);
// A FillPortion of 1
let fill_portion = Fp(1);

Considerations

In order for this to work, there are a few things that need to be considered:

  • The layout functions would need to be "aware" of other elements with FillPortion units in order to calculate the correct size for each element.
  • How FillPortion units interact with other units

Advanced navigation

First of all, thanks for the lovely crate ๐Ÿซถ

I haven't seen anything about UI navigation with keyboard or controller input. I don't think it's supported in bevypunk either.

So I have a few questions:

  • Is it supported in any form?
  • If not, is it planned to be supported?
  • If the answer is no to either, can it be worked around?

I find this pretty critical, especially for handheld gaming consoles like Steam Deck. There is also the fact that pretty much all major games support UI navigation with keyboard. In the end, it would be great to have it in bevy_lunex as well!

(btw, https://github.com/nicopap/ui-navigation is a good example of what this issue is about)

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

Split up large files into smaller modules

Proposal

While looking through the codebase, I noticed that there are quite a few large files (a thousand lines or more).
I propose splitting these files into smaller modules to make the codebase more readable, maintainable, and easier to navigate for newcomers (like me in this case!).

Example

As an example, layout.rs in lunex_engine defines the Layout enum, but it also defines Boundary, Window, Solid, and Div structs for use in the Layout enum.
In this case, I think it would be beneficial to move the layout modes (Boundary, Window, Solid, and Div) into their own files, either in the same directory or in a subdirectory.

Here's how the file structure could look after moving the layout modes into a subdirectory:

๐Ÿ“ layout
โ”œโ”€ ๐Ÿ“œ layout.rs
โ””โ”€ ๐Ÿ“ layout_modes
	โ”œโ”€ ๐Ÿ“œ boundary.rs
	โ”œโ”€ ๐Ÿ“œ window.rs
	โ”œโ”€ ๐Ÿ“œ solid.rs
	โ””โ”€ ๐Ÿ“œ div.rs

Here is a sample of the corresponding change in layout.rs:

mod layout_modes;

pub enum Layout {
    layout_modes::boundary::Boundary,
    layout_modes::window::Window,
    layout_modes::solid::Solid,
    layout_modes::div::Div,
}

impl Layout {
    ...
}

...

Considerations

Of course this is just an aesthetic change, the proposal doesn't offer any technical improvements.
If this is a change that you would like to see, I would be happy to try and implement it (to the best of my ability, erring on the side of caution as I am not familiar with the codebase).

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.