bevy_lunex

repository·main·Indexed 21 days ago

https://github.com/bytestring-net/bevy_lunex

A high-performance retained UI layout engine for Bevy ECS. It provides a component-per-functionality approach to building UIs, featuring state-based layouts (e.g., base and hover states), a flexible layout system (Boundary, Window, and Solid types), and a comprehensive cursor management system including software cursors with gamepad support via the CursorPlugin.

Tokens
17K
Snippets
56
Records
66
Agent score
69%

What's inside bevy_lunex

  1. Overview of Bevy Lunex

    main

    Bevy Lunex is a high-performance retained layout engine designed for Bevy entities. Unlike traditional UI frameworks, it is built directly around vanilla Bevy ECS, allowing you to create custom UI using regular ECS patterns just like any other part of your application.

    Key Characteristics

    • Positioning Focus: Lunex primarily provides the capability to position entities. It does not currently include flexbox-like layout capabilities.
    • ECS-Native: Everything is handled via Bevy ECS, making it highly customizable and suitable for low-level interactivity.

    Use Case Suitability

    Recommended for:

    • Worldspace 3D UI
    • Sprite-based 2D UI
    • Custom rendering hooks
    • Scenarios requiring high customizability and low-level control

    Not recommended for:

    • Rapid development and high iteration speed
    • Using prebuilt input components
    • Building standard desktop application UIs
  2. How Text2d scaling and layout works

    main

    Lunex automates the scaling of 2D text to fit UI bounds through the following lifecycle:

    1. Text Computation: Lunex waits for Bevy to compute the text bounds (glyph size, font size, etc.).
    2. Boundary Assignment: Once computed, Lunex takes these values and applies them to the UiLayout::boundary::size property, scaled by the value provided in UiTextSize.
    3. Layout Computation: The UI layout is computed for the current frame.
    4. Transform Scaling: Lunex scales the entity's Transform so that the text fits precisely within the computed node bounds.
  3. How interactivity works with Observers

    main

    Interactivity in bevy_lunex is implemented using Observers. An observer is a one-shot system that executes only when a specific event is triggered on a specific entity.

    To use interactivity, you define an observer that takes a Trigger<E> (where E is the event type) and attach it to a spawned entity. This creates a local observer that listens for that event on that specific entity.

    // Example of attaching a local observer to an entity
    ui.spawn((/* components */))
        .observe(|trigger: Trigger<Pointer<Click>>, /* other resources */| {
            // Logic executed when the entity is clicked
        });
  4. Understand Lunex layout terminal output

    main

    When UiLunexDebugPlugin is active, it prints a tree representation of your UI layout to the terminal whenever changes occur. The output format follows this pattern:

    [ID] ⇒ [w: Width, h: Height, d: Depth] ➜ [Type]

    • ID: The unique identifier of the node (e.g., 11v1).
    • Dimensions: Width (w) and Height (h).
    • Depth: The nesting depth (d).
    • Type: The UI element type (e.g., Solid, Window).
    ▶ 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
  5. Understand Lunex UI Base Units

    main

    Lunex uses 9 distinct UI unit types as arguments for UiValue<T>, where T is typically f32, Vec2, Vec3, or Vec4. These units are used in layout functions that accept impl Into<UiValue<T>>.

    Available Units

    UnitNameMeaning / Behavior
    AbAbsoluteAb(1) = 1px
    RlRelativeRl(1.0) = 1%
    RwRelative WidthRw(1.0) = 1%w. If used in a height field, it uses width as the source.
    RhRelative HeightRh(1.0) = 1%h. If used in a width field, it uses height as the source.
    EmEmEm(1.0) = 1em (e.g., 16px if font size is 16px)
    VpViewportVp(1.0) = 1v% of the UiTree original size
    VwViewport WidthVw(1.0) = 1v%w of the UiTree original size. If used in a height field, it uses width as the source.
    VhViewport HeightVh(1.0) = 1v%h of the UiTree original size. If used in a width field, it uses height as the source.
  6. Configure UI Source Cameras

    main

    For the UI to render correctly, your main camera must be tagged with the UiSourceCamera::<N> component, where N is an index in the range 0..3.

    This component tells the UI which camera's viewport size to use as the root node size. This architecture supports up to 4 cameras, which is useful for split-screen games.

    If you require more than the default indices, you can manually add UiLunexIndexPlugin::<N> for the specific index needed.

    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]),
        ));
    }
  7. Configure RenderLayers for Lunex debug gizmos

    main

    The UiLunexDebugPlugin<R_2D, R_3D> uses two generic constants to determine which RenderLayers the debug gizmos should be drawn on:

    • R_2D: The render layer for 2D gizmos.
    • R_3D: The render layer for 3D gizmos.

    To see the debug outlines, your cameras must include these layers in their RenderLayers component.

    If you use UiLunexDebugPlugin::<1, 2>, configure your cameras as follows:

    // For Camera2d
    // Must include layer 0 and the 2D debug layer (e.g., 1)
    RenderLayers::from_layers(&[0, 1])
    
    // For Camera3d
    // Must include layer 0 and the 3D debug layer (e.g., 2)
    RenderLayers::from_layers(&[0, 2])
  8. Setup and use Text 3D in Bevy Lunex

    main

    Text rendering in 3D is provided via the bevy_rich_text3d crate, which bevy_lunex re-exports. If you have disabled default features, you must enable the text3d feature to use it.

    To render 3D text, you must spawn an entity with a specific combination of components. Unlike 2D text, Text3d requires a Mesh3d and a MeshMaterial3d to function.

    Required Components

    • Text3d: Specifies the actual text content.
    • UiLayout: Specifies position and anchor (size is ignored).
    • UiTextSize: Specifies the height of the text in proportion to the parent node.
    • Mesh3d: Must include an empty Mesh3d::default() component.
    • MeshMaterial3d: Requires a material. It is recommended to use a StandardMaterial with unlit: true and alpha_mode: AlphaMode::Blend using the TextAtlas::DEFAULT_IMAGE texture.

    Important Constraints

    • Camera Requirement: Text3d can ONLY be rendered with a Camera3d.
    • Font Loading: Unlike standard Bevy text, you do not provide a font handle in Text3dStyling. Instead, you must load fonts into a fontdb using the LoadFonts resource, then reference the font by its name string.
    ui.spawn((
        Name::new("Panel"),
        // Set the layout of this mesh
        UiLayout::window().pos(Rl(50.0)).anchor(Anchor::Center).pack(),
        // This controls the height of the text, so 10% of the parent's node height
        UiTextSize::from(Rh(10.0)),
        // Set the text value
        Text3d::new("Hello 3D UI!"),
        // Style the 3D text
        Text3dStyling {
            size: 64.0,
            color: Srgba::new(1., 1., 1., 1.),
            align: TextAlign::Center,
            font: Arc::from("Rajdhani"),
            weight: Weight::BOLD,
            ..Default::default()
        },
        // Provide a material to this mesh
        MeshMaterial3d(materials.add(
            StandardMaterial {
                base_color_texture: Some(TextAtlas::DEFAULT_IMAGE),
                alpha_mode: AlphaMode::Blend,
                unlit: true,
                ..Default::default()
            }
        )),
        // Requires an empty mesh
        Mesh3d::default(),
    ));
  9. Enable Lunex debug tooling

    main

    To debug UI behavior, you can enable the UiLunexDebugPlugin. This plugin provides two main features:

    1. Gizmo Outlines: Draws outlines around all UI nodes to visualize their positions and sizes.
    2. Layout Logging: Prints the UI layout hierarchy to the terminal whenever a change is detected.

    To enable gizmo outlines, you must add the plugin to your Bevy application and configure the RenderLayers on your cameras to match the layers specified in the plugin's generic parameters.

    // Example: Adding the plugin with default render layers
    app.add_plugins(UiLunexDebugPlugin::<1, 2>);
  10. Initialize a 2D UI root with camera synchronization

    main

    To start a 2D UI, spawn a UiLayoutRoot using UiLayoutRoot::new_2d(). To ensure the UI viewport size stays synchronized with your camera, add the UiFetchFromCamera::<N> component, where N is the index of your camera's UiSourceCamera::<N> component. This maintains the Camera -> Dimension -> UiLayout pipeline automatically.

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