diff --git a/.cargo/config.toml b/.cargo/config.toml
index 411a543..c0ec372 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,6 +1,12 @@
[target.x86_64-unknown-linux-gnu]
-linker = "clang"
-rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]
+ linker = "clang"
+ rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold", "-Zshare-generics=y"]
-[build]
-rustflags = ["-Z", "threads=8"]
\ No newline at end of file
+[unstable]
+ codegen-backend = true
+
+[profile.dev]
+ codegen-backend = "cranelift"
+
+[profile.dev.package."*"]
+ codegen-backend = "llvm"
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..be065ff
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+*.ase filter=lfs diff=lfs merge=lfs -text
+*.aseprite filter=lfs diff=lfs merge=lfs -text
+*.svg filter=lfs diff=lfs merge=lfs -text
+*.kra filter=lfs diff=lfs merge=lfs -text
+*.png filter=lfs diff=lfs merge=lfs -text
+*.jpeg filter=lfs diff=lfs merge=lfs -text
+*.jpg filter=lfs diff=lfs merge=lfs -text
+*.webp filter=lfs diff=lfs merge=lfs -text
+*.mp4 filter=lfs diff=lfs merge=lfs -text
+*.ogg filter=lfs diff=lfs merge=lfs -text
+*.glb filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 29ac559..03b5e57 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -8,9 +8,9 @@ jobs:
deploy:
runs-on: ubuntu-latest
permissions:
- contents: write # To push a branch
- pages: write # To push to a GitHub Pages site
- id-token: write # To update the deployment status
+ contents: write
+ pages: write
+ id-token: write
steps:
- uses: actions/checkout@v4
with:
@@ -22,6 +22,14 @@ jobs:
mkdir mdbook
curl -sSL $url | tar -xz --directory=./mdbook
echo `pwd`/mdbook >> $GITHUB_PATH
+ - name: Install mdbook-alerts plugin
+ run: |
+ tag=$(curl 'https://github.com/lambdalisue/rs-mdbook-alerts/releases/latest' | jq -r '.tag_name')
+ url="https://github.com/lambdalisue/rs-mdbook-alerts/releases/download/${tag}/mdbook-alerts-x86_64-unknown-linux-gnu"
+ mkdir -p mdbook-alerts
+ curl -sSL $url -o mdbook-alerts/mdbook-alerts
+ chmod +x mdbook-alerts/mdbook-alerts
+ echo `pwd`/mdbook-alerts >> $GITHUB_PATH
- name: Build Book
run: |
cd docs
@@ -31,7 +39,6 @@ jobs:
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
- # Upload entire repository
path: 'docs/book'
- name: Deploy to GitHub Pages
id: deployment
diff --git a/.vscode/keybindings.json b/.vscode/keybindings.json
deleted file mode 100644
index 29d00e8..0000000
--- a/.vscode/keybindings.json
+++ /dev/null
@@ -1,21 +0,0 @@
-// Place this in your user keybinds
-[
- {
- "key": "f5",
- "command": "workbench.action.tasks.runTask",
- "args": "rust: cargo run 1",
- "when": "editorLangId == 'rust'"
- },
- {
- "key": "f6",
- "command": "workbench.action.tasks.runTask",
- "args": "rust: cargo run 2",
- "when": "editorLangId == 'rust'"
- },
- {
- "key": "f7",
- "command": "workbench.action.tasks.runTask",
- "args": "rust: cargo run 3",
- "when": "editorLangId == 'rust'"
- }
-]
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index c165e3f..ae7ef99 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -4,32 +4,32 @@
{
"type": "cargo",
"command": "run",
- "args": ["--bin", "minimal"],
+ "args": ["--bin", "sprite2d"],
"problemMatcher": [
"$rustc"
],
"group": "build",
- "label": "rust: cargo run 1"
+ "label": "Run 1"
},
{
"type": "cargo",
"command": "run",
- "args": ["--bin", "worldspace"],
+ "args": ["--bin", "sprite3d"],
"problemMatcher": [
"$rustc"
],
"group": "build",
- "label": "rust: cargo run 2"
+ "label": "Run 2"
},
{
"type": "cargo",
"command": "run",
- "args": ["--bin", "worldspace_text"],
+ "args": ["--bin", "mesh2d"],
"problemMatcher": [
"$rustc"
],
"group": "build",
- "label": "rust: cargo run 3"
+ "label": "Run 3"
}
]
}
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index 21d312b..078d580 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,45 +1,74 @@
+#======================#
+#=== WORKSPACE INFO ===#
+
[workspace]
resolver = "2"
- members = [
- "crates/*",
- "examples/*",
- ]
- exclude = [
- "promo/*",
- "examples/*",
- "docs/*",
- ".gitignore",
- ".vscode",
- ]
+ members = ["crate", "examples/*"]
+ exclude = [".github", ".vscode", "docs/*", "promo/*", ".gitattributes", ".gitignore"]
+
+[workspace.package]
+ authors = ["IDEDARY"]
+ version = "0.3.0"
+ edition = "2021"
+ license = "MIT OR Apache-2.0"
+ repository = "https://github.com/bytestring-net/bevy-lunex"
+ keywords = ["ui", "layout", "bevy", "lunex", "bevy-lunex"]
+ categories = ["gui", "mathematics", "game-development"]
+
+#========================#
+#=== PROFILE SETTINGS ===#
[profile.dev]
+ debug = 0
+ strip = "debuginfo"
opt-level = 1
-[workspace.package]
- authors = ["IDEDARY"]
- version = "0.2.3"
- edition = "2021"
- license = "MIT OR Apache-2.0"
- repository = "https://github.com/bytestring-net/bevy-lunex"
- keywords = ["ui", "layout", "bevy", "lunex", "bevy-lunex"]
- categories = ["gui", "mathematics", "game-development"]
+[profile.dev.package."*"]
+ opt-level = 3
+
+[profile.release]
+ opt-level = 3
+ panic = 'abort'
+ debug = 0
+ strip = true
+ lto = "thin"
+ codegen-units = 1
+
+#===============================#
+#=== DEPENDENCIES & FEATURES ===#
[workspace.dependencies]
- bevy_lunex = { path = "crates/bevy_lunex", version = "0.2.3" }
- lunex_engine = { path = "crates/lunex_engine", version = "0.2.3" }
+ # LIBRARY CRATES
+ bevy_lunex = { path = "crate" }
- colored = { version = "^2.1" }
- indexmap = { version = "^2.1" }
- thiserror = { version = "^1.0" }
+ #===========================#
+ #=== GAME ENGINE SOURCE === #
- bevy = { version = "^0.14", default-features = false, features = [
+ # GAME ENGINE
+ bevy = { version = "^0.15", default-features = false, features = [
"bevy_pbr",
"bevy_sprite",
"bevy_text",
- "multi_threaded",
"bevy_gizmos",
+ "bevy_picking",
+ "bevy_ui",
+ "bevy_window",
+ "bevy_winit",
+ "custom_cursor",
+
+ # Required for doctests
+ "x11",
] }
- bevy_kira_audio = { version = "^0.20" }
- bevy_mod_picking = { version = "^0.20", default-features = false, features = ["selection", "backend_raycast"] }
\ No newline at end of file
+ #===============================#
+ #=== GAME ENGINE EXTENSIONS === #
+
+ # AUDIO
+ # bevy_kira_audio = { version = "^0.22.0" }
+
+ #===========================#
+ #=== RUST MISCELLANEOUS === #
+
+ radsort = { version = "^0.1.1" }
+ colored = { version = "^3.0.0" }
diff --git a/LICENSE-MIT b/LICENSE-MIT
index 2318e67..c189670 100644
--- a/LICENSE-MIT
+++ b/LICENSE-MIT
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2023 Bytestring
+Copyright (c) 2025 Bytestring
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 13abdcb..fdf4e02 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-
+
@@ -10,7 +10,7 @@
#
-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.
+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.
* **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.
@@ -20,82 +20,103 @@ Blazingly fast ***path based*** retained ***layout engine*** for Bevy entities,
* **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.
-
##

-
-
-> *Try out the live WASM demo on [`Itch.io`](https://idedary.itch.io/bevypunk) or [`GitHub Pages`](https://idedary.github.io/Bevypunk/) (Limited performance & stutter due to running on a single thread). For best experience compile the project natively. You can find [source code here](https://github.com/IDEDARY/Bevypunk).*
+> *Try out the live WASM demo on [`Itch.io`](https://idedary.itch.io/bevypunk) (Limited performance & stutter due to running on a single thread). For best experience compile the project natively.*
## 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.
+This is an example of a clickable Button created from scratch using provided components.
+Thanks to ECS, the syntax is highly modular with strong emphasis on components-per-functionality.
+As you can see, it is no different from vanilla Bevy ECS.
```rust
+// Create UI
commands.spawn((
-
- // #=== UI DEFINITION ===#
-
- // This specifies the name and hierarchy of the node
- UiLink::
::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:: (),
-
-
- // #=== 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::::new().forward_speed(5.0).backward_speed(1.0),
-
- // This will set the base color to red
- UiColor ::new(Color::RED),
-
- // This will set hover color to yellow
- UiColor::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,
-));
+ // Initialize the UI root for 2D
+ UiLayoutRoot::new_2d(),
+ // Make the UI synchronized with camera viewport size
+ UiFetchFromCamera::<0>,
+)).with_children(|ui| {
+
+ // Spawn a button in the middle of the screen
+ ui.spawn((
+ Name::new("My Button"),
+ // Specify the position and size of the button
+ UiLayout::window().pos(Rl((50.0, 50.0))).size((200.0, 50.0)).pack(),
+ // When hovered, it will request the cursor icon to be changed
+ OnHoverSetCursor::new(SystemCursorIcon::Pointer),
+ )).with_children(|ui| {
+
+ // Spawn a child node but with a background
+ ui.spawn((
+ // You can define layouts for multiple states
+ UiLayout::new(vec![
+ // The default state, just fill the parent
+ (UiBase::id(), UiLayout::window().full()),
+ // The hover state, grow to 105% of the parent from center
+ (UiHover::id(), UiLayout::window().anchor(Anchor::Center).size(Rl(105.0)))
+ ]),
+ // Enable the hover state and give it some properties
+ UiHover::new().forward_speed(20.0).backward_speed(4.0),
+ // You can specify colors for multiple states
+ UiColor::new(vec![
+ (UiBase::id(), Color::BEVYPUNK_RED.with_alpha(0.15)),
+ (UiHover::id(), Color::BEVYPUNK_YELLOW.with_alpha(1.2))
+ ]),
+ // You can attach any form of rendering to the node, be it sprite, mesh or something custom
+ Sprite {
+ image: asset_server.load("images/button.png"),
+ // Here we enable sprite slicing
+ image_mode: SpriteImageMode::Sliced(TextureSlicer { border: BorderRect::square(32.0), ..default() }),
+ ..default()
+ },
+ // Make sure it does not cover the bounding zone of parent
+ PickingBehavior::IGNORE,
+ )).with_children(|ui| {
+
+ // Spawn a text child node
+ ui.spawn((
+ // For text we always use window layout to position it. The size is computed at runtime from text bounds
+ UiLayout::window().pos((Rh(40.0), Rl(50.0))).anchor(Anchor::CenterLeft).pack(),
+ UiColor::new(vec![
+ (UiBase::id(), Color::BEVYPUNK_RED),
+ (UiHover::id(), Color::BEVYPUNK_YELLOW.with_alpha(1.2))
+ ]),
+ UiHover::new().forward_speed(20.0).backward_speed(4.0),
+ // Here we specify the text height proportional to the parent node
+ UiTextSize::from(Rh(60.0)),
+ // You can attach text like this
+ Text2d::new("Click me!"),
+ TextFont {
+ font: asset_server.load("fonts/semibold.ttf"),
+ font_size: 64.0,
+ ..default()
+ },
+ // Make sure it does not cover the bounding zone of parent
+ PickingBehavior::IGNORE,
+ ));
+ });
+ })
+ // Utility observers that enable the hover state on trigger
+ .observe(hover_set::, true>)
+ .observe(hover_set::, false>)
+ // Interactivity is done through observers, you can query anything here
+ .observe(|_: Trigger>| {
+ // ... Do something on click
+ });
+});
```
## Documentation
-There is a Lunex book for detailed explanations about the concepts used in Lunex. You can find it here: [`Bevy Lunex book`](https://bytestring-net.github.io/bevy_lunex/).
-
-For production ready example/template check out [`Bevypunk source code`](https://github.com/IDEDARY/Bevypunk).
-
-## Versions
+- The Lunex Book: [`Bevy Lunex book`](https://bytestring-net.github.io/bevy_lunex/).
-| Bevy | Bevy Lunex |
-|--------|-----------------|
-| ^ 0.14 | 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 |
+- Highly documented source code on Docs.rs: [`Docs.rs`](https://docs.rs/bevy_lunex/latest/bevy_lunex/).
-> ***Any version below 0.0.X is EXPERIMENTAL and is not intended for practical use***
+- Highly documented production-ready example: [`Bevypunk example`](https://github.com/IDEDARY/Bevypunk).
## Contributing
diff --git a/crate/Cargo.toml b/crate/Cargo.toml
new file mode 100644
index 0000000..2a100b7
--- /dev/null
+++ b/crate/Cargo.toml
@@ -0,0 +1,27 @@
+#====================#
+#=== PACKAGE INFO ===#
+
+[package]
+ name = "bevy_lunex"
+ authors.workspace = true
+ version.workspace = true
+ edition.workspace = true
+ license.workspace = true
+ repository.workspace = true
+ keywords.workspace = true
+ categories.workspace = true
+
+#===============================#
+#=== DEPENDENCIES & FEATURES ===#
+
+[dependencies]
+
+ # GAME ENGINE
+ bevy = { workspace = true }
+
+ # AUDIO
+ # bevy_kira_audio = { workspace = true }
+
+ # RUST MISCELLANEOUS
+ radsort = { workspace = true }
+ colored = { workspace = true }
diff --git a/crate/src/cursor.rs b/crate/src/cursor.rs
new file mode 100644
index 0000000..cb04787
--- /dev/null
+++ b/crate/src/cursor.rs
@@ -0,0 +1,555 @@
+use crate::*;
+use bevy::{input::{gamepad::GamepadButtonChangedEvent, mouse::MouseButtonInput, ButtonState}, picking::{pointer::{Location, PointerAction, PointerId, PointerInput, PointerLocation, PressDirection}, PickSet}, render::camera::{NormalizedRenderTarget, RenderTarget}, utils::HashMap, window::{PrimaryWindow, SystemCursorIcon, WindowRef}, winit::cursor::CursorIcon};
+
+
+// #=========================#
+// #=== CURSOR ICON QUEUE ===#
+
+#[derive(Resource, Reflect, Clone, PartialEq, Debug, Default)]
+pub struct CursorIconQueue {
+ pointers: HashMap
+}
+impl CursorIconQueue {
+ /// A method to request a new cursor icon. Works only if priority is higher than already set priority this tick.
+ pub fn request_cursor(&mut self, pointer: PointerId, window: Entity, requestee: Entity, request: SystemCursorIcon, priority: usize) {
+ if let Some(data) = self.pointers.get_mut(&pointer) {
+ data.window = window;
+ data.queue.insert(requestee, (request, priority));
+ } else {
+ let mut queue = HashMap::new();
+ queue.insert(requestee, (request, priority));
+ self.pointers.insert(pointer, CursorQueueData { window, queue });
+ }
+ }
+ /// A method to cancel existing cursor in the queue stack
+ pub fn cancel_cursor(&mut self, pointer: PointerId, requestee: &Entity) {
+ if let Some(data) = self.pointers.get_mut(&pointer) {
+ data.queue.remove(requestee);
+ }
+ }
+}
+
+#[derive(Reflect, Clone, PartialEq, Debug)]
+struct CursorQueueData {
+ window: Entity,
+ queue: HashMap
+}
+
+/// This system will apply cursor changes to the windows it has in the resource.
+fn system_cursor_icon_queue_apply(
+ queue: Res,
+ mut windows: Query<(&Window, Option<&mut CursorIcon>)>,
+ mut commands: Commands,
+) {
+ if !queue.is_changed() { return; }
+ for (_, data) in &queue.pointers {
+ if let Ok((_window, window_cursor_option)) = windows.get_mut(data.window) {
+
+ let mut top_priority = 0;
+ let mut top_request = SystemCursorIcon::Default;
+
+ // Look for highest priority to use
+ for (_, (icon, priority)) in &data.queue {
+ if *priority > top_priority {
+ top_priority = *priority;
+ top_request = *icon;
+ }
+ }
+
+ // Apply the cursor icon somehow
+ if let Some(mut window_cursor) = window_cursor_option {
+ #[allow(clippy::single_match)]
+ match window_cursor.as_mut() {
+ CursorIcon::System(ref mut previous) => {
+ if *previous != top_request {
+ *previous = top_request;
+ }
+ },
+ _ => {},
+ }
+
+ } else {
+ commands.entity(data.window).insert(CursorIcon::System(top_request));
+ }
+ }
+ }
+}
+
+/// This system will cleanup the queue if any invalid data is found.
+fn system_cursor_icon_queue_purge(
+ mut queue: ResMut,
+ mut windows: Query<&Window>,
+ entities: Query,
+) {
+ let mut to_remove = Vec::new();
+ for (pointer, data) in &mut queue.pointers {
+
+ // Remove invalid pointers
+ if windows.get_mut(data.window).is_err() {
+ to_remove.push(*pointer);
+ }
+
+ // Remove despawned entities
+ let mut to_remove = Vec::new();
+ for (entity, _) in &data.queue {
+ if entities.get(*entity).is_err() {
+ to_remove.push(*entity);
+ }
+ }
+
+ // Cleanup
+ for entity in to_remove {
+ data.queue.remove(&entity);
+ }
+ }
+
+ // Cleanup
+ for pointer in to_remove {
+ queue.pointers.remove(&pointer);
+ }
+}
+
+
+// #========================#
+// #=== CURSOR ADDITIONS ===#
+
+/// Requests cursor icon on hover
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct OnHoverSetCursor {
+ /// SoftwareCursor type to request on hover
+ pub cursor: SystemCursorIcon,
+}
+impl OnHoverSetCursor {
+ /// Creates new struct
+ pub fn new(cursor: SystemCursorIcon) -> Self {
+ OnHoverSetCursor {
+ cursor,
+ }
+ }
+}
+
+fn observer_cursor_request_cursor_icon(mut trigger: Trigger>, mut pointers: Query<(&PointerId, &PointerLocation)>, query: Query<&OnHoverSetCursor>, mut queue: ResMut) {
+ // Find the pointer location that triggered this observer
+ let id = trigger.pointer_id;
+ for (pointer, location) in pointers.iter_mut().filter(|(p_id, _)| id == **p_id) {
+
+ // Check if the pointer is attached to a window
+ if let Some(location) = &location.location {
+ if let NormalizedRenderTarget::Window(window) = location.target {
+
+ // Request a cursor change
+ if let Ok(requestee) = query.get(trigger.target) {
+ trigger.propagate(false);
+ queue.request_cursor(*pointer, window.entity(), trigger.target, requestee.cursor, 1);
+ }
+ }
+ }
+ }
+}
+
+fn observer_cursor_cancel_cursor_icon(mut trigger: Trigger>, mut pointers: Query<(&PointerId, &PointerLocation)>, query: Query<&OnHoverSetCursor>, mut queue: ResMut) {
+ // Find the pointer location that triggered this observer
+ let id = trigger.pointer_id;
+ for (pointer, location) in pointers.iter_mut().filter(|(p_id, _)| id == **p_id) {
+
+ // Check if the pointer is attached to a window
+ if let Some(location) = &location.location {
+ if matches!(location.target, NormalizedRenderTarget::Window(_)) {
+
+ // Cancel existing cursor icon request if applicable
+ if query.get(trigger.target).is_ok() {
+ trigger.propagate(false);
+ queue.cancel_cursor(*pointer, &trigger.target);
+ }
+ }
+ }
+ }
+}
+
+
+
+// #=======================#
+// #=== SOFTWARE CURSOR ===#
+
+/// Component for creating software mouse.
+#[derive(Component, Reflect, Clone, PartialEq, Debug, Default)]
+#[require(PointerId, PickingBehavior(|| PickingBehavior::IGNORE))]
+pub struct SoftwareCursor {
+ /// Indicates which cursor is being requested.
+ cursor_request: SystemCursorIcon,
+ /// Indicates the priority of the requested cursor.
+ cursor_request_priority: f32,
+ /// Map which cursor has which atlas index and offset
+ cursor_atlas_map: HashMap,
+ /// Location of the cursor (same as [`Transform`] without sprite offset).
+ pub location: Vec2,
+}
+impl SoftwareCursor {
+ /// Creates new default SoftwareCursor.
+ pub fn new() -> SoftwareCursor {
+ SoftwareCursor {
+ cursor_request: SystemCursorIcon::Default,
+ cursor_request_priority: 0.0,
+ cursor_atlas_map: HashMap::new(),
+ location: Vec2::ZERO,
+ }
+ }
+ /// A method to request a new cursor icon. Works only if priority is higher than already set priority this tick.
+ pub fn request_cursor(&mut self, request: SystemCursorIcon, priority: f32) {
+ if priority > self.cursor_request_priority {
+ self.cursor_request = request;
+ self.cursor_request_priority = priority;
+ }
+ }
+ /// This function binds the specific cursor icon to an image index that is used if the entity has texture atlas attached to it.
+ pub fn set_index(mut self, icon: SystemCursorIcon, index: usize, offset: impl Into) -> Self {
+ self.cursor_atlas_map.insert(icon, (index, offset.into()));
+ self
+ }
+}
+
+/// This will make the [`SoftwareCursor`] controllable by a gamepad.
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct GamepadCursor {
+ /// This struct defines how should the cursor movement behave.
+ pub mode: GamepadCursorMode,
+ /// SoftwareCursor speed scale
+ pub speed: f32,
+}
+impl GamepadCursor {
+ /// Creates a new instance.
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+impl Default for GamepadCursor {
+ fn default() -> Self {
+ Self { mode: Default::default(), speed: 1.0 }
+ }
+}
+
+/// This struct defines how should the cursor movement behave.
+#[derive(Debug, Clone, Default, PartialEq, Reflect)]
+pub enum GamepadCursorMode {
+ /// SoftwareCursor will freely move on input.
+ #[default]
+ Free,
+ /// Will try to snap to nearby nodes on input.
+ /// # WORK IN PROGRESS
+ Snap,
+}
+
+/// This component is used for SoftwareCursor-Gamepad relation.
+/// - It is added to a Gamepad if he has a virtual cursor assigned.
+/// - It is added to a SoftwareCursor if he is assigned to an existing gamepad.
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct GamepadAttachedCursor(pub Entity);
+
+
+
+// #========================#
+// #=== CURSOR FUNCTIONS ===#
+
+/// This system will hide the native cursor.
+fn system_cursor_hide_native(
+ mut windows: Query<&mut Window>,
+ query: Query<(&PointerLocation, Has), With>
+) {
+ for (pointer_location, is_gamepad) in &query {
+ if let Some(location) = &pointer_location.location {
+ if let NormalizedRenderTarget::Window(window) = location.target {
+ if let Ok(mut window) = windows.get_mut(window.entity()) {
+ window.cursor_options.visible = is_gamepad;
+ }
+ }
+ }
+ }
+}
+
+/// This system will hide the native cursor.
+fn system_cursor_software_change_icon(
+ windows: Query<&CursorIcon, With>,
+ mut query: Query<(&PointerLocation, &SoftwareCursor, &mut Sprite), With>
+) {
+ for (pointer_location, software_cursor, mut sprite) in &mut query {
+ if let Some(location) = &pointer_location.location {
+ if let NormalizedRenderTarget::Window(window) = location.target {
+ if let Ok(cursor_icon) = windows.get(window.entity()) {
+ if let Some(atlas) = &mut sprite.texture_atlas {
+ #[allow(clippy::single_match)]
+ match *cursor_icon {
+ CursorIcon::System(icon) => {
+ atlas.index = software_cursor.cursor_atlas_map.get(&icon).unwrap_or(&(0, Vec2::ZERO)).0;
+ },
+ _ => {},
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/// This system will attach any free cursor to the first gamepad it can find.
+fn system_cursor_gamepad_assign(
+ mut commands: Commands,
+ cursors: Query<(Entity, &SoftwareCursor, &GamepadCursor), Without>,
+ gamepads: Query<(Entity, &Gamepad), Without>,
+) {
+ let mut gamepads = gamepads.iter();
+ if let Some((cursor, _, _)) = cursors.iter().next() {
+ if let Some((gamepad, _)) = gamepads.next() {
+ commands.entity(cursor).insert(GamepadAttachedCursor(gamepad));
+ commands.entity(gamepad).insert(GamepadAttachedCursor(cursor));
+ info!("Gamepad {gamepad} bound to cursor {cursor}");
+ }
+ }
+}
+
+
+
+/// This system will move the gamepad cursor.
+fn system_cursor_gamepad_move(
+ time: Res,
+ gamepads: Query<&Gamepad, With>,
+ mut cursors: Query<(&mut SoftwareCursor, &GamepadCursor, &GamepadAttachedCursor), Without>,
+) {
+ for (mut cursor, gamepad_settings, attached_gamepad) in &mut cursors {
+ if let Ok(gamepad) = gamepads.get(attached_gamepad.0) {
+
+ // Get the gamepad input
+ let mut input = Vec2::new(
+ gamepad.get(GamepadAxis::LeftStickX).unwrap_or(0.0),
+ gamepad.get(GamepadAxis::LeftStickY).unwrap_or(0.0),
+ );
+
+ // Clamp the deadzone as a vector
+ if input.length_squared() < 0.1 { input *= 0.0; }
+
+ // Compute the cursor position change
+ let x = input.x * gamepad_settings.speed * time.delta_secs() * 500.0;
+ let y = input.y * gamepad_settings.speed * time.delta_secs() * 500.0;
+
+ // Move the cursor if it changed
+ if x != 0.0 { cursor.location.x += x; }
+ if y != 0.0 { cursor.location.y += y; }
+ }
+ }
+}
+
+/// This system will move the mouse cursor.
+fn system_cursor_mouse_move(
+ windows: Query<&Window, With>,
+ cameras: Query<&OrthographicProjection>,
+ mut query: Query<(&mut SoftwareCursor, Option<&Parent>), Without>
+) {
+ if let Ok(window) = windows.get_single() {
+ for (mut cursor, parent_option) in &mut query {
+ if let Some(position) = window.cursor_position() {
+ // Get projection scale to account for zoomed cameras
+ let scale = if let Some(parent) = parent_option {
+ if let Ok(projection) = cameras.get(**parent) { projection.scale } else { 1.0 }
+ } else { 1.0 };
+
+ // Compute the cursor position
+ let x = (position.x - window.width()*0.5) * scale;
+ let y = -((position.y - window.height()*0.5) * scale);
+
+ // Move the cursor if it changed
+ if x != cursor.location.x { cursor.location.x = (position.x - window.width()*0.5) * scale; }
+ if y != cursor.location.y { cursor.location.y = -((position.y - window.height()*0.5) * scale); }
+ }
+ }
+ }
+}
+
+
+
+/// This system will update the transform component to reflect the sprite offset.
+fn system_cursor_update_tranform(
+ mut query: Query<(&SoftwareCursor, &mut Transform)>
+) {
+ for (cursor, mut transform) in &mut query {
+ let sprite_offset = cursor.cursor_atlas_map.get(&cursor.cursor_request).unwrap_or(&(0, Vec2::ZERO)).1;
+ transform.translation.x = cursor.location.x - sprite_offset.x * transform.scale.x;
+ transform.translation.y = cursor.location.y + sprite_offset.y * transform.scale.y;
+ }
+}
+
+/// This system will move the virtual pointer location.
+fn system_cursor_move_pointer(
+ windows: Query<(Entity, &Window), With>,
+ mut query: Query<(&mut PointerLocation, &SoftwareCursor)>,
+) {
+ if let Ok((win_entity, window)) = windows.get_single() {
+ for (mut pointer, cursor) in query.iter_mut() {
+ // Change the pointer location
+ pointer.location = Some(Location {
+ target: RenderTarget::Window(WindowRef::Primary).normalize(Some(win_entity)).unwrap(),
+ position: Vec2 {
+ x: cursor.location.x + window.width()/2.0,
+ y: -cursor.location.y + window.height()/2.0,
+ }.round(),
+ });
+ }
+ }
+}
+
+
+
+/// This system will send out pointer move events if they changed position
+fn system_cursor_send_move_events(
+ mut cursor_last: Local>,
+ pointers: Query<(&PointerId, &PointerLocation), With>,
+ mut pointer_output: EventWriter,
+) {
+ // Send mouse movement events
+ for (pointer, location) in &pointers {
+ if let Some(location) = &location.location {
+ let last = cursor_last.get(pointer).unwrap_or(&Vec2::ZERO);
+ if *last == location.position { continue; }
+
+ pointer_output.send(PointerInput::new(
+ *pointer,
+ Location {
+ target: location.target.clone(),
+ position: location.position,
+ },
+ PointerAction::Moved {
+ delta: location.position - *last,
+ },
+ ));
+ cursor_last.insert(*pointer, location.position);
+ }
+ }
+}
+
+/// This system will send out mouse pick events
+fn system_cursor_mouse_send_pick_events(
+ pointers: Query<&PointerLocation, (With, Without)>,
+ mut mouse_inputs: EventReader,
+ mut pointer_output: EventWriter,
+) {
+ // Send mouse movement events
+ for location in &pointers {
+ if let Some(location) = &location.location {
+
+ // Send mouse click events
+ for input in mouse_inputs.read() {
+
+ // Which button trigger
+ let button = match input.button {
+ MouseButton::Left => PointerButton::Primary,
+ MouseButton::Right => PointerButton::Secondary,
+ MouseButton::Middle => PointerButton::Middle,
+ MouseButton::Other(_) | MouseButton::Back | MouseButton::Forward => continue,
+ };
+
+ // Which state to change
+ let direction = match input.state {
+ ButtonState::Pressed => PressDirection::Down,
+ ButtonState::Released => PressDirection::Up,
+ };
+
+ // Send out the event
+ pointer_output.send(PointerInput::new(
+ PointerId::Mouse,
+ Location {
+ target: location.target.clone(),
+ position: location.position,
+ },
+ PointerAction::Pressed { direction, button },
+ ));
+ }
+ }
+ }
+}
+
+/// This system will send out gamepad pick events
+fn system_cursor_gamepad_send_pick_events(
+ pointers: Query<&PointerLocation, (With, Without)>,
+ mut mouse_inputs: EventReader,
+ mut pointer_output: EventWriter,
+) {
+ // Send mouse movement events
+ for location in &pointers {
+ if let Some(location) = &location.location {
+
+ // Send mouse click events
+ for input in mouse_inputs.read() {
+
+ // Which button trigger
+ let button = match input.button {
+ GamepadButton::South => PointerButton::Primary,
+ GamepadButton::East => PointerButton::Secondary,
+ GamepadButton::West => PointerButton::Middle,
+ _ => continue,
+ };
+
+ // Which state to change
+ let direction = match input.state {
+ ButtonState::Pressed => PressDirection::Down,
+ ButtonState::Released => PressDirection::Up,
+ };
+
+ // Send out the event
+ pointer_output.send(PointerInput::new(
+ PointerId::Mouse,
+ Location {
+ target: location.target.clone(),
+ position: location.position,
+ },
+ PointerAction::Pressed { direction, button },
+ ));
+ }
+ }
+ }
+}
+
+
+
+// #==============#
+// #=== PLUGIN ===#
+
+pub struct CursorPlugin;
+impl Plugin for CursorPlugin {
+ fn build(&self, app: &mut App) {
+ app
+ // Add SoftwareCursor Icon Queue resource to the app
+ .insert_resource(CursorIconQueue::default())
+ .add_systems(PostUpdate, (
+ system_cursor_icon_queue_purge,
+ system_cursor_icon_queue_apply,
+ ))
+
+ // OnHoverSetCursor observers
+ .add_observer(observer_cursor_request_cursor_icon)
+ .add_observer(observer_cursor_cancel_cursor_icon)
+
+
+ // #=== SOFTWARE CURSOR ===#
+
+ // Add systems for emulating picking events
+ .add_systems(First, (
+ system_cursor_send_move_events,
+ system_cursor_mouse_send_pick_events,
+ system_cursor_gamepad_send_pick_events,
+ apply_deferred
+ ).chain().in_set(PickSet::Input))
+
+ // Add core systems
+ .add_systems(PreUpdate, (
+ system_cursor_gamepad_move,
+ system_cursor_mouse_move,
+ system_cursor_update_tranform,
+ system_cursor_move_pointer,
+ ).chain())
+
+ // Other stuff
+ .add_systems(Update, (
+ system_cursor_hide_native,
+ system_cursor_software_change_icon,
+ system_cursor_gamepad_assign,
+ ));
+ }
+}
\ No newline at end of file
diff --git a/crate/src/layouts.rs b/crate/src/layouts.rs
new file mode 100644
index 0000000..a956be7
--- /dev/null
+++ b/crate/src/layouts.rs
@@ -0,0 +1,452 @@
+use crate::*;
+
+
+// #============================#
+// #=== MULTIPURPOSE STRUCTS ===#
+
+/// **Rectangle 2D** - Contains computed values from node layout.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
+pub struct Rectangle2D {
+ pub pos : Vec2,
+ pub size: Vec2,
+}
+impl Rectangle2D {
+ pub fn lerp(self, rhs: Self, lerp: f32) -> Self {
+ Rectangle2D {
+ pos: self.pos.lerp(rhs.pos, lerp),
+ size: self.size.lerp(rhs.size, lerp),
+ }
+ }
+}
+impl Rectangle2D {
+ /// A new empty [`Rectangle2D`]. Has `0` size.
+ pub const EMPTY: Rectangle2D = Rectangle2D { pos : Vec2::ZERO, size: Vec2::ZERO };
+ /// Creates new empty Window layout.
+ pub const fn new() -> Self {
+ Rectangle2D::EMPTY
+ }
+ /// Replaces the position with the new value.
+ pub fn with_pos(mut self, pos: impl Into) -> Self {
+ self.pos = pos.into();
+ self
+ }
+ /// Replaces the x position with the new value.
+ pub fn with_x(mut self, width: f32) -> Self {
+ self.pos.x = width;
+ self
+ }
+ /// Replaces the y position with the new value.
+ pub fn with_y(mut self, height: f32) -> Self {
+ self.pos.y = height;
+ self
+ }
+ /// Replaces the size with the new value.
+ pub fn with_size(mut self, size: impl Into) -> Self {
+ self.size = size.into();
+ self
+ }
+ /// Replaces the width with the new value.
+ pub fn with_width(mut self, width: f32) -> Self {
+ self.size.x = width;
+ self
+ }
+ /// Replaces the height with the new value.
+ pub fn with_height(mut self, height: f32) -> Self {
+ self.size.y = height;
+ self
+ }
+}
+
+/// **Align** - A type used to define alignment in a node layout.
+/// ## 🛠️ Example
+/// ```
+/// # use bevy_lunex::*;
+/// let align: Align = Align::START; // -> -1.0
+/// let align: Align = Align(-1.0); // -> -1.0
+/// let align: Align = (-1.0).into(); // -> -1.0
+/// ```
+/// The expected range is `-1.0` to `1.0`, but you can extrapolate.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
+pub struct Align (pub f32);
+impl Align {
+ pub const START: Align = Align(-1.0);
+ pub const LEFT: Align = Align(-1.0);
+ pub const CENTER: Align = Align(0.0);
+ pub const MIDDLE: Align = Align(0.0);
+ pub const END: Align = Align(1.0);
+ pub const RIGHT: Align = Align(1.0);
+}
+impl From for Align {
+ fn from(val: f32) -> Self {
+ Align(val)
+ }
+}
+
+
+/// **Scaling** - A type used to define how should a Solid node layout scale relative to a parent.
+/// ## 🛠️ Example
+/// ```
+/// # use bevy_lunex::*;
+/// let scaling: Scaling = Scaling::HorFill; // -> always cover the horizontal axis
+/// let scaling: Scaling = Scaling::VerFill; // -> always cover the vertical axis
+/// let scaling: Scaling = Scaling::Fit; // -> always fit inside
+/// let scaling: Scaling = Scaling::Fill; // -> always cover all
+/// ```
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
+pub enum Scaling {
+ /// Node layout should always cover the horizontal axis of the parent node.
+ HorFill,
+ /// Node layout should always cover the vertical axis of the parent node.
+ VerFill,
+ /// Node layout should always fit inside the parent node.
+ #[default] Fit,
+ /// Node layout should always cover all of the parent node.
+ Fill,
+}
+
+
+// #====================#
+// #=== LAYOUT TYPES ===#
+
+/// **Ui Layout Type** - Enum holding all UI layout variants.
+#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
+pub enum UiLayoutType {
+ Boundary(UiLayoutTypeBoundary),
+ Window(UiLayoutTypeWindow),
+ Solid(UiLayoutTypeSolid),
+}
+impl UiLayoutType {
+ /// Computes the layout based on given parameters.
+ pub(crate) fn compute(&self, parent: &Rectangle2D, absolute_scale: f32, viewport_size: Vec2, font_size: f32) -> Rectangle2D {
+ match self {
+ UiLayoutType::Boundary(layout) => layout.compute(parent, absolute_scale, viewport_size, font_size),
+ UiLayoutType::Window(layout) => layout.compute(parent, absolute_scale, viewport_size, font_size),
+ UiLayoutType::Solid(layout) => layout.compute(parent, absolute_scale, viewport_size, font_size),
+ }
+ }
+}
+impl From for UiLayoutType {
+ fn from(value: UiLayoutTypeBoundary) -> Self {
+ UiLayoutType::Boundary(value)
+ }
+}
+impl From for UiLayoutType {
+ fn from(value: UiLayoutTypeWindow) -> Self {
+ UiLayoutType::Window(value)
+ }
+}
+impl From for UiLayoutType {
+ fn from(value: UiLayoutTypeSolid) -> Self {
+ UiLayoutType::Solid(value)
+ }
+}
+
+
+/// **Boundary** - Declarative layout type that is defined by its top-left corner and bottom-right corner.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
+pub struct UiLayoutTypeBoundary {
+ /// Position of the top-left corner.
+ pub pos1: UiValue,
+ /// Position of the bottom-right corner.
+ pub pos2: UiValue,
+}
+impl UiLayoutTypeBoundary {
+ /// Creates new empty Boundary node layout.
+ pub const fn new() -> Self {
+ Self {
+ pos1: UiValue::new(),
+ pos2: UiValue::new(),
+ }
+ }
+ /// Replaces the position of the top-left corner with a new value.
+ pub fn pos1(mut self, pos: impl Into>) -> Self {
+ self.pos1 = pos.into();
+ self
+ }
+ /// Replaces the position of the bottom-right corner with a new value.
+ pub fn pos2(mut self, pos: impl Into>) -> Self {
+ self.pos2 = pos.into();
+ self
+ }
+ /// Replaces the x position of the top-left corner with a new value.
+ pub fn x1(mut self, x: impl Into>) -> Self {
+ self.pos1.set_x(x);
+ self
+ }
+ /// Replaces the y position of the top-left corner with a new value.
+ pub fn y1(mut self, y: impl Into>) -> Self {
+ self.pos1.set_y(y);
+ self
+ }
+ /// Replaces the x position of the bottom-right corner with a new value.
+ pub fn x2(mut self, x: impl Into>) -> Self {
+ self.pos2.set_x(x);
+ self
+ }
+ /// Replaces the y position of the bottom-right corner with a new value.
+ pub fn y2(mut self, y: impl Into>) -> Self {
+ self.pos2.set_y(y);
+ self
+ }
+ /// Sets the position of the top-left corner to a new value.
+ pub fn set_pos1(&mut self, pos: impl Into>) {
+ self.pos1 = pos.into();
+ }
+ /// Sets the position of the bottom-right corner to a new value.
+ pub fn set_pos2(&mut self, pos: impl Into>) {
+ self.pos2 = pos.into();
+ }
+ /// Sets the x position of the top-left corner to a new value.
+ pub fn set_x1(&mut self, x: impl Into>) {
+ self.pos1.set_x(x);
+ }
+ /// Sets the y position of the top-left corner to a new value.
+ pub fn set_y1(&mut self, y: impl Into>) {
+ self.pos1.set_y(y);
+ }
+ /// Sets the x position of the bottom-right corner to a new value.
+ pub fn set_x2(&mut self, x: impl Into>) {
+ self.pos2.set_x(x);
+ }
+ /// Sets the y position of the bottom-right corner to a new value.
+ pub fn set_y2(&mut self, y: impl Into>) {
+ self.pos2.set_y(y);
+ }
+ /// Pack the layout type into UiLayout
+ pub fn pack(self) -> UiLayout {
+ UiLayout::from(self)
+ }
+ /// Wrap the layout type into UiLayout
+ pub fn wrap(self) -> UiLayoutType {
+ UiLayoutType::from(self)
+ }
+ /// Computes the layout based on given parameters.
+ pub(crate) fn compute(&self, parent: &Rectangle2D, absolute_scale: f32, viewport_size: Vec2, font_size: f32) -> Rectangle2D {
+ let pos1 = self.pos1.evaluate(Vec2::splat(absolute_scale), parent.size, viewport_size, Vec2::splat(font_size));
+ let pos2 = self.pos2.evaluate(Vec2::splat(absolute_scale), parent.size, viewport_size, Vec2::splat(font_size));
+ let size = pos2 - pos1;
+ Rectangle2D {
+ pos: -parent.size / 2.0 + pos1 + size/2.0,
+ size,
+ }
+ }
+}
+
+/// **Window** - Declarative layout type that is defined by its size and position.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
+pub struct UiLayoutTypeWindow {
+ /// Position of the node.
+ pub pos : UiValue,
+ /// Decides where position should be applied at.
+ pub anchor: Anchor,
+ /// Size of the node layout.
+ pub size: UiValue,
+}
+impl UiLayoutTypeWindow {
+ /// Creates new empty Window node layout.
+ pub const fn new() -> Self {
+ Self {
+ pos: UiValue::new(),
+ anchor: Anchor::TopLeft,
+ size: UiValue::new(),
+ }
+ }
+ /// Replaces the size to make the window fully cover the parent.
+ pub fn full(self) -> Self {
+ self.size(Rl(100.0))
+ }
+ /// Replaces the position with a new value.
+ pub fn pos(mut self, pos: impl Into>) -> Self {
+ self.pos = pos.into();
+ self
+ }
+ /// Replaces the x position with a new value.
+ pub fn x(mut self, x: impl Into>) -> Self {
+ self.pos.set_x(x);
+ self
+ }
+ /// Replaces the y position with a new value.
+ pub fn y(mut self, y: impl Into>) -> Self {
+ self.pos.set_y(y);
+ self
+ }
+ /// Replaces the size with a new value.
+ pub fn size(mut self, size: impl Into>) -> Self {
+ self.size = size.into();
+ self
+ }
+ /// Replaces the width with a new value.
+ pub fn width(mut self, width: impl Into>) -> Self {
+ self.size.set_x(width);
+ self
+ }
+ /// Replaces the height with a new value.
+ pub fn height(mut self, height: impl Into>) -> Self {
+ self.size.set_y(height);
+ self
+ }
+ /// Replaces the anchor with a new value.
+ pub fn anchor(mut self, anchor: impl Into) -> Self {
+ self.anchor = anchor.into();
+ self
+ }
+ /// Sets the position to a new value.
+ pub fn set_pos(&mut self, pos: impl Into>){
+ self.pos = pos.into();
+ }
+ /// Sets the x position to a new value.
+ pub fn set_x(&mut self, x: impl Into>){
+ self.pos.set_x(x);
+ }
+ /// Sets the y position to a new value.
+ pub fn set_y(&mut self, y: impl Into>){
+ self.pos.set_y(y);
+ }
+ /// Sets the size to a new value.
+ pub fn set_size(&mut self, size: impl Into>){
+ self.size = size.into();
+ }
+ /// Sets the width to a new value.
+ pub fn set_width(&mut self, width: impl Into>){
+ self.size.set_x(width);
+ }
+ /// Sets the height to a new value.
+ pub fn set_height(&mut self, height: impl Into>){
+ self.size.set_y(height);
+ }
+ /// Sets the anchor to a new value.
+ pub fn set_anchor(&mut self, anchor: impl Into){
+ self.anchor = anchor.into();
+ }
+ /// Pack the layout type into UiLayout
+ pub fn pack(self) -> UiLayout {
+ UiLayout::from(self)
+ }
+ /// Wrap the layout type into UiLayout
+ pub fn wrap(self) -> UiLayoutType {
+ UiLayoutType::from(self)
+ }
+ /// Computes the layout based on given parameters.
+ pub(crate) fn compute(&self, parent: &Rectangle2D, absolute_scale: f32, viewport_size: Vec2, font_size: f32) -> Rectangle2D {
+ let pos = self.pos.evaluate(Vec2::splat(absolute_scale), parent.size, viewport_size, Vec2::splat(font_size));
+ let size = self.size.evaluate(Vec2::splat(absolute_scale), parent.size, viewport_size, Vec2::splat(font_size));
+ let mut anchor = self.anchor.as_vec();
+ anchor.y *= -1.0;
+ Rectangle2D {
+ pos: -parent.size / 2.0 + pos - size * (anchor),
+ size,
+ }
+ }
+}
+
+/// **Solid** - Declarative layout type that is defined by its width and height ratio.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
+pub struct UiLayoutTypeSolid {
+ /// Aspect ratio of the width and height. `1:1 == 10:10 == 100:100`.
+ pub size: UiValue,
+ /// Horizontal alignment within parent.
+ pub align_x: Align,
+ /// Vertical alignment within parent.
+ pub align_y: Align,
+ /// Specifies container scaling.
+ pub scaling: Scaling,
+}
+impl UiLayoutTypeSolid {
+ /// Creates new empty Solid node layout.
+ pub fn new() -> Self {
+ Self {
+ size: Ab(Vec2::ONE).into(),
+ align_x: Align::CENTER,
+ align_y: Align::CENTER,
+ scaling: Scaling::Fit,
+ }
+ }
+ /// Replaces the size with a new value.
+ pub fn size(mut self, size: impl Into>) -> Self {
+ self.size = size.into();
+ self
+ }
+ /// Replaces the width with a new value.
+ pub fn width(mut self, width: impl Into>) -> Self {
+ self.size.set_x(width);
+ self
+ }
+ /// Replaces the height with a new value.
+ pub fn height(mut self, height: impl Into>) -> Self {
+ self.size.set_y(height);
+ self
+ }
+ /// Replaces the x alignment with a new value.
+ pub fn align_x(mut self, align: impl Into) -> Self {
+ self.align_x = align.into();
+ self
+ }
+ /// Replaces the y alignment with a new value.
+ pub fn align_y(mut self, align: impl Into) -> Self {
+ self.align_y = align.into();
+ self
+ }
+ /// Replaces the scaling mode with a new value.
+ pub fn scaling(mut self, scaling: Scaling) -> Self {
+ self.scaling = scaling;
+ self
+ }
+ /// Sets the size to a new value.
+ pub fn set_size(&mut self, size: impl Into>) {
+ self.size = size.into();
+ }
+ /// Sets the width to a new value.
+ pub fn set_width(&mut self, width: impl Into>) {
+ self.size.set_x(width);
+ }
+ /// Sets the height to a new value.
+ pub fn set_height(&mut self, height: impl Into>) {
+ self.size.set_y(height);
+ }
+ /// Sets the x alignment to a new value.
+ pub fn set_align_x(&mut self, align: impl Into) {
+ self.align_x = align.into();
+ }
+ /// Sets the y alignment to a new value.
+ pub fn set_align_y(&mut self, align: impl Into) {
+ self.align_y = align.into();
+ }
+ /// Sets the scaling mode to a new value.
+ pub fn set_scaling(&mut self, scaling: Scaling) {
+ self.scaling = scaling;
+ }
+ /// Pack the layout type into UiLayout
+ pub fn pack(self) -> UiLayout {
+ UiLayout::from(self)
+ }
+ /// Wrap the layout type into UiLayout
+ pub fn wrap(self) -> UiLayoutType {
+ UiLayoutType::from(self)
+ }
+ /// Computes the layout based on given parameters.
+ pub(crate) fn compute(&self, parent: &Rectangle2D, absolute_scale: f32, viewport_size: Vec2, font_size: f32) -> Rectangle2D {
+
+ let size = self.size.evaluate(Vec2::splat(absolute_scale), parent.size, viewport_size, Vec2::splat(font_size));
+
+ let scale = match self.scaling {
+ Scaling::HorFill => parent.size.x / size.x,
+ Scaling::VerFill => parent.size.y / size.y,
+ Scaling::Fit => f32::min(parent.size.x / size.x, parent.size.y / size.y),
+ Scaling::Fill => f32::max(parent.size.x / size.x, parent.size.y / size.y),
+ };
+
+ let center_point = parent.size / 2.0;
+
+ let computed_width = size.x * scale;
+ let computed_height = size.y * scale;
+ let computed_point = Vec2::new(center_point.x - computed_width / 2.0, center_point.y - computed_height / 2.0);
+
+ Rectangle2D {
+ pos: Vec2::new(
+ computed_point.x * self.align_x.0,
+ computed_point.y * self.align_y.0,
+ ),
+ size: (computed_width, computed_height).into(),
+ }
+ }
+}
diff --git a/crate/src/lib.rs b/crate/src/lib.rs
new file mode 100644
index 0000000..fac9214
--- /dev/null
+++ b/crate/src/lib.rs
@@ -0,0 +1,1088 @@
+#![feature(const_type_id)]
+#![allow(clippy::type_complexity)]
+
+// Crate import only
+pub(crate) use std::any::TypeId;
+pub(crate) use bevy::prelude::*;
+pub(crate) use bevy::sprite::SpriteSource;
+pub(crate) use bevy::text::TextLayoutInfo;
+pub(crate) use bevy::utils::HashMap;
+pub(crate) use bevy::render::view::RenderLayers;
+pub(crate) use colored::Colorize;
+
+// Re-export
+pub use bevy::sprite::Anchor;
+
+mod cursor;
+pub use cursor::*;
+
+mod layouts;
+pub use layouts::*;
+
+mod picking;
+pub use picking::*;
+
+mod states;
+pub use states::*;
+
+mod units;
+pub use units::*;
+
+
+// #===============================#
+// #=== MULTIPURPOSE COMPONENTS ===#
+
+/// **Dimension** - This component holds width and height used for different Ui components
+#[derive(Component, Reflect, Deref, DerefMut, Default, Clone, PartialEq, Debug)]
+pub struct Dimension(pub Vec2);
+/// Conversion implementations
+impl > From for Dimension {
+ fn from(value: T) -> Self {
+ Dimension(value.into())
+ }
+}
+
+/// This system takes [`Dimension`] data and pipes them into querried [`Sprite`].
+pub fn system_pipe_sprite_size_from_dimension(
+ mut query: Query<(&mut Sprite, &Dimension), Changed>,
+) {
+ for (mut sprite, dimension) in &mut query {
+ sprite.custom_size = Some(**dimension)
+ }
+}
+
+
+// #=========================#
+// #=== TEXTURE EMBEDDING ===#
+
+/// **Ui Embedding** - Use this component to mark entities that their texture handles are embeddings instead of regular assets.
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct UiEmbedding;
+
+/// This system takes [`Dimension`] data and pipes them into querried [`Handle`] data to fit.
+/// This will resize the original image texture.
+pub fn system_embedd_resize(
+ query: Query<(&Sprite, &Dimension), (With, Changed)>,
+ mut images: ResMut>,
+) {
+ for (sprite, dimension) in &query {
+ if let Some(image) = images.get_mut(&sprite.image) {
+ if **dimension != Vec2::ZERO {
+ image.resize(bevy::render::render_resource::Extent3d { width: dimension.x as u32, height: dimension.y as u32, ..default() });
+ }
+ }
+ }
+}
+
+/// Provides utility constructor methods for [`Image`]
+pub trait ImageTextureConstructor {
+ /// Just a utility constructor hiding the necessary texture initialization
+ fn clear_render_texture() -> Image {
+ use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages};
+ use bevy::asset::RenderAssetUsages;
+
+ let mut image = Image::new_fill(
+ Extent3d {
+ width: 512,
+ height: 512,
+ ..default()
+ },
+ TextureDimension::D2,
+ &[0, 0, 0, 0],
+ TextureFormat::Bgra8UnormSrgb,
+ RenderAssetUsages::default(),
+ );
+ image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT;
+ image
+ }
+}
+impl ImageTextureConstructor for Image {}
+
+/// Provides utility costructor methods for [`Camera`]
+pub trait CameraTextureRenderConstructor {
+ /// Just a utility constructor for camera that renders to a transparent texture
+ fn clear_render_to(handle: Handle) -> Camera {
+ use bevy::render::camera::RenderTarget;
+ Camera {
+ target: RenderTarget::Image(handle),
+ clear_color: ClearColorConfig::Custom(Color::srgba(0.0, 0.0, 0.0, 0.0)),
+ ..default()
+ }
+ }
+ /// Modify the camera render order
+ fn with_order(self, order: isize) -> Self;
+}
+impl CameraTextureRenderConstructor for Camera {
+ fn with_order(mut self, order: isize) -> Self {
+ self.order = order;
+ self
+ }
+}
+
+
+// #===========================#
+// #=== LAYOUT ROOT CONTROL ===#
+
+/// **Ui Layout Root** - This component marks the start of a worldspace Ui-Tree. Spawn this standalone for worldspace 3D UI
+/// or spawn this as a child of camera for a HUD. For 2D UI, if your camera does not move you can spawn it standalone too.
+///
+/// Important components:
+/// - [`Transform`] - Set the position of the Ui-Tree
+/// - [`Dimension`] - Set the size of the Ui-Tree
+/// ## 🛠️ Example
+/// ```
+/// # use bevy::prelude::*;
+/// # use bevy_lunex::*;
+/// # fn spawn_main_menu(mut commands: Commands, asset_server: Res) {
+/// commands.spawn((
+/// UiLayoutRoot,
+/// UiFetchFromCamera::<0>, // Pipe the size from Camera
+/// )).with_children(|ui| {
+/// // ... spawn your Ui Here
+/// });
+/// # }
+/// ```
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+#[require(Visibility, Transform, Dimension)]
+pub struct UiLayoutRoot {
+ abs_scale: f32,
+}
+impl UiLayoutRoot {
+ pub fn new_2d() -> Self {
+ Self { abs_scale: 1.0 }
+ }
+ pub fn new_3d() -> Self {
+ Self { abs_scale: 0.001 }
+ }
+}
+
+
+/// Marker component for all entities that you can use to check if it is used for 3D UI.
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct UiRoot3d;
+
+/// This system traverses the hierarchy and adds [`UiRoot3d`] component to children.
+pub fn system_mark_3d(
+ root_query: Query<(Has, &Children), (With, Without, Changed)>,
+ node_query: Query<(Entity, Has, Option<&Children>), (With, Without)>,
+ mut commands: Commands,
+) {
+ for (is_root_3d, root_children) in &root_query {
+
+ // Stack-based traversal
+ let mut stack: Vec<(Entity, usize)> = root_children.iter().map(|&child| (child, 1)).rev().collect();
+
+ // Loop over the stack
+ while let Some((current_entity, depth)) = stack.pop() {
+ if let Ok((node, is_node_3d, node_children_option)) = node_query.get(current_entity) {
+
+
+ if is_root_3d != is_node_3d {
+ if is_root_3d {
+ commands.entity(node).insert(UiRoot3d);
+ } else {
+ commands.entity(node).remove::();
+ }
+ }
+
+
+ // Push children to the stack
+ if let Some(node_children) = node_children_option {
+ for &child in node_children.iter().rev() {
+ stack.push((child, depth + 1));
+ }
+ }
+ }
+ }
+ }
+}
+
+
+
+/// Trigger this event to recompute all [`UiLayoutRoot`] entities.
+#[derive(Event)]
+pub struct RecomputeUiLayout;
+
+/// This observer will mutably touch [`UiLayoutRoot`] which will trigger [`system_layout_compute`].
+pub fn observer_touch_layout_root(
+ _trigger: Trigger,
+ mut query: Query<&mut UiLayoutRoot>,
+){
+ for mut root in &mut query {
+ root.as_mut();
+ }
+}
+
+/// This system draws the outlines of [`UiLayout`] and [`UiLayoutRoot`] as gizmos.
+pub fn system_debug_draw_gizmo_2d(
+ query: Query<(&GlobalTransform, &Dimension), (Or<(With, With)>, Without)>,
+ mut gizmos: Gizmos
+) {
+ for (transform, dimension) in &query {
+ // Draw the gizmo outline
+ gizmos.rect(
+ Isometry3d::from(transform.translation()),
+ **dimension,
+ Color::linear_rgb(0.0, 1.0, 0.0),
+ );
+ }
+}
+
+/// This system draws the outlines of [`UiLayout`] and [`UiLayoutRoot`] as gizmos.
+pub fn system_debug_draw_gizmo_3d(
+ query: Query<(&GlobalTransform, &Dimension), (Or<(With, With)>, With)>,
+ mut gizmos: Gizmos
+) {
+ for (transform, dimension) in &query {
+ // Draw the gizmo outline
+ gizmos.rect(
+ Isometry3d::from(transform.translation()),
+ **dimension,
+ Color::linear_rgb(0.0, 1.0, 0.0),
+ );
+ }
+}
+
+/// This system traverses the hierarchy and prints the debug information.
+pub fn system_debug_print_data(
+ root_query: Query<(&UiLayoutRoot, NameOrEntity, &Dimension, &Children), (Without, Or<(Changed, Changed)>)>,
+ node_query: Query<(&UiLayout, &UiState, NameOrEntity, &Dimension, &Transform, Option<&Children>), Without>,
+) {
+ for (_, root_name, root_dimension, root_children) in &root_query {
+ // Create output string
+ let mut output_string = format!("▶ {}", format!("{root_name}").bold().underline().magenta());
+
+ output_string += " ⇒ ";
+ output_string += &format!("[w: {}, h: {}]", format!("{:.00}", root_dimension.x).green(), format!("{:.00}", root_dimension.y).green());
+
+ output_string += "\n";
+
+ // Stack-based traversal
+ let mut stack: Vec<(Entity, usize, bool)> = root_children
+ .iter()
+ .enumerate()
+ .map(|(i, &child)| (child, 1, i == root_children.len() - 1)) // Track last-child flag
+ .rev()
+ .collect();
+
+ // Tracks whether previous levels had last children (for vertical bars)
+ let mut last_child_levels: Vec = Vec::new();
+
+ while let Some((current_entity, depth, is_last)) = stack.pop() {
+ if let Ok((node_layout, _node_state, node_name, node_dimension, node_transform, node_children_option)) = node_query.get(current_entity) {
+
+ // Adjust last_child_levels size
+ if last_child_levels.len() < depth {
+ last_child_levels.push(is_last);
+ } else {
+ last_child_levels[depth - 1] = is_last;
+ }
+
+ // Create the tab level offset
+ for &last in &last_child_levels[..depth - 1] {
+ output_string += &if last { format!("{}", " ┆".black()) } else { " │".to_string() };
+ }
+
+ // Add the name
+ output_string += if is_last { " └" } else { " ├" };
+ if node_name.name.is_some() {
+ output_string += &format!("─ {}", format!("{node_name}").bold().yellow());
+ } else {
+ output_string += &format!("─ {}", format!("{node_name}").yellow());
+ }
+
+ output_string += " ⇒ ";
+
+ output_string += &format!("[w: {}, h: {}, d: {}]",
+ format!("{:.00}", node_dimension.x).green(),
+ format!("{:.00}", node_dimension.y).green(),
+ format!("{:.00}", node_transform.translation.z).green(),
+ );
+
+ match node_layout.layouts.get(&UiBase::id()).unwrap() {
+ UiLayoutType::Boundary(boundary) => {
+ output_string += &format!(" ➜ {} {} p1: {}, p2: {} {}",
+ "Boundary".bold(),
+ "{",
+ boundary.pos1.to_nicestr(),
+ boundary.pos2.to_nicestr(),
+ "}",
+ );
+ },
+ UiLayoutType::Window(window) => {
+ output_string += &format!(" ➜ {} {} p: {}, s: {}, a: {} {}",
+ "Window".bold(),
+ "{",
+ window.pos.to_nicestr(),
+ window.size.to_nicestr(),
+ window.anchor.to_nicestr(),
+ "}",
+ );
+ },
+ UiLayoutType::Solid(solid) => {
+ output_string += &format!(" ➜ {} {} s: {}, ax: {}, ay: {}, scl: {} {}",
+ "Solid".bold(),
+ "{",
+ solid.size.to_nicestr(),
+ format!("{:.02}", solid.align_x.0).green(),
+ format!("{:.02}", solid.align_y.0).green(),
+ format!("{:?}", solid.scaling).green(),
+ "}",
+ );
+ },
+ }
+
+ output_string += "\n";
+
+ if let Some(node_children) = node_children_option {
+ let child_count = node_children.len();
+ for (i, &child) in node_children.iter().enumerate().rev() {
+ stack.push((child, depth + 1, i == child_count - 1));
+ }
+ }
+ }
+ }
+
+ // Print to console
+ info!("UiLayout change detected:\n{}", output_string);
+ }
+}
+
+
+// #======================#
+// #=== LAYOUT CONTROL ===#
+
+/// **Ui Layout** - This component specifies the layout of a Ui-Node, which must be spawned as a child
+/// of either [`UiLayoutRoot`] or [`UiLayout`] to work. Based on the provided layout other attached
+/// components on this entity are overwritten to match the computed structure.
+///
+/// Direct output components:
+/// - [`Transform`] - The computed position of the Ui-Node _(Read-only)_
+/// - [`Dimension`] - The computed size of the Ui-Node _(Read-only)_
+///
+/// Indirectly affected components:
+/// - [`Sprite`] - `custom_size` to match [`Dimension`]
+///
+/// ## 🛠️ Example
+/// ```
+/// # use bevy::prelude::*;
+/// # use bevy_lunex::*;
+/// # fn spawn_main_menu(mut commands: Commands, asset_server: Res) {
+/// # commands.spawn((
+/// # UiLayoutRoot,
+/// # )).with_children(|ui| {
+/// // Must be spawned as a child
+/// ui.spawn((
+/// // Use 1 of the 3 available layout types
+/// UiLayout::solid().size((1920.0, 1080.0)).scaling(Scaling::Fill).pack(),
+/// // Attach image to the node
+/// Sprite::from_image(asset_server.load("images/ui/background.png")),
+/// ));
+/// # });
+/// # }
+/// ```
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+#[require(Visibility, SpriteSource, Transform, Dimension, UiState, UiDepth)]
+pub struct UiLayout {
+ /// Stored layout per state
+ layouts: HashMap
+}
+/// Constructors
+impl UiLayout {
+ /// **Boundary** - Declarative layout type that is defined by its top-left corner and bottom-right corner.
+ /// Nodes with this layout are not included in the ui flow.
+ /// ## 🛠️ Example
+ /// ```
+ /// # use bevy_lunex::{UiLayout, Rl};
+ /// let layout: UiLayout = UiLayout::boundary().pos1(Rl(20.0)).pos2(Rl(80.0)).pack();
+ /// ```
+ pub fn boundary() -> UiLayoutTypeBoundary {
+ UiLayoutTypeBoundary::new()
+ }
+ /// **Window** - Declarative layout type that is defined by its size and position.
+ /// Nodes with this layout are not included in the ui flow.
+ /// ## 🛠️ Example
+ /// ```
+ /// # use bevy_lunex::{UiLayout, Ab, Rl};
+ /// let layout: UiLayout = UiLayout::window().pos(Ab(100.0)).size(Rl(50.0)).pack();
+ /// ```
+ pub fn window() -> UiLayoutTypeWindow {
+ UiLayoutTypeWindow::new()
+ }
+ /// **Solid** - Declarative layout type that is defined by its width and height ratio.
+ /// Scales in a way to fit itself inside parent container. It never deforms.
+ /// Nodes with this layout are not included in the ui flow.
+ /// ## 🛠️ Example
+ /// ```
+ /// # use bevy_lunex::UiLayout;
+ /// let layout: UiLayout = UiLayout::solid().size((4.0, 3.0)).align_x(-0.8).pack();
+ /// ```
+ pub fn solid() -> UiLayoutTypeSolid {
+ UiLayoutTypeSolid::new()
+ }
+ /// Create multiple layouts for a different states at once.
+ pub fn new(value: Vec<(TypeId, impl Into)>) -> Self {
+ let mut map = HashMap::new();
+ for (state, layout) in value {
+ map.insert(state, layout.into());
+ }
+ Self { layouts: map }
+ }
+}
+/// Conversion implementations
+impl From for UiLayout {
+ fn from(value: UiLayoutType) -> Self {
+ let mut map = HashMap::new();
+ map.insert(UiBase::id(), value);
+ Self {
+ layouts: map,
+ }
+ }
+}
+impl From for UiLayout {
+ fn from(value: UiLayoutTypeBoundary) -> Self {
+ let value: UiLayoutType = value.into();
+ UiLayout::from(value)
+ }
+}
+impl From for UiLayout {
+ fn from(value: UiLayoutTypeWindow) -> Self {
+ let value: UiLayoutType = value.into();
+ UiLayout::from(value)
+ }
+}
+impl From for UiLayout {
+ fn from(value: UiLayoutTypeSolid) -> Self {
+ let value: UiLayoutType = value.into();
+ UiLayout::from(value)
+ }
+}
+
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub enum UiDepth {
+ Add(f32),
+ Set(f32),
+}
+impl Default for UiDepth {
+ fn default() -> Self {
+ UiDepth::Add(1.0)
+ }
+}
+
+
+/// This system traverses the hierarchy and computes all nodes.
+pub fn system_layout_compute(
+ root_query: Query<(&UiLayoutRoot, &Transform, &Dimension, &Children), (Without, Or<(Changed, Changed)>)>,
+ mut node_query: Query<(&UiLayout, &UiDepth, &UiState, &mut Transform, &mut Dimension, Option<&Children>), Without>,
+) {
+ for (root, root_transform, root_dimension, root_children) in &root_query {
+ // Size of the viewport
+ let root_rectangle = Rectangle2D {
+ pos: root_transform.translation.xy(),
+ size: **root_dimension,
+ };
+
+ // Stack-based traversal
+ let mut stack: Vec<(Entity, Rectangle2D, f32)> = root_children.iter().map(|&child| (child, root_rectangle, 0.0)).rev().collect();
+
+ while let Some((current_entity, parent_rectangle, depth)) = stack.pop() {
+ if let Ok((node_layout, node_depth, node_state, mut node_transform, mut node_dimension, node_children_option)) = node_query.get_mut(current_entity) {
+ // Compute all layouts for the node
+ let mut computed_rectangles = Vec::with_capacity(node_layout.layouts.len());
+ for (state, layout) in &node_layout.layouts {
+ computed_rectangles.push((state, layout.compute(&parent_rectangle, root.abs_scale, root_rectangle.size, 16.0)));
+ }
+
+ // Normalize the active state weights
+ let mut total_weight = 0.0;
+ for (state, _) in &node_layout.layouts {
+ if let Some(weight) = node_state.states.get(state) {
+ total_weight += weight;
+ }
+ }
+
+ // Combine the state rectangles into one normalized
+ let mut node_rectangle = Rectangle2D::EMPTY;
+
+ // Use base if no active state
+ if total_weight == 0.0 {
+ node_rectangle.pos += computed_rectangles[0].1.pos;
+ node_rectangle.size += computed_rectangles[0].1.size;
+
+ // Combine the active states into one rectangle
+ } else {
+ for (state, rectangle) in computed_rectangles {
+ if let Some(weight) = node_state.states.get(state) {
+ node_rectangle.pos += rectangle.pos * (weight / total_weight);
+ node_rectangle.size += rectangle.size * (weight / total_weight);
+ }
+ }
+ }
+
+ // Save the computed layout
+ node_transform.translation.x = node_rectangle.pos.x;
+ node_transform.translation.y = -node_rectangle.pos.y;
+ let depth = match node_depth {
+ UiDepth::Add(v) => {depth + v},
+ UiDepth::Set(v) => {*v},
+ };
+ node_transform.translation.z = depth * root.abs_scale;
+ **node_dimension = node_rectangle.size;
+
+ if let Some(node_children) = node_children_option {
+ // Add children to the stack
+ stack.extend(node_children.iter().map(|&child| (child, node_rectangle, depth)));
+ }
+ }
+ }
+ }
+}
+
+
+// #=====================#
+// #=== STATE CONTROL ===#
+
+/// **Ui State** - This component aggrages state transition values for later reference
+/// by other components. You don't directly control or spawn this component, but use an abstraction
+/// instead. You can use the prebuilt state components or create a custom ones with a completely
+/// unique transition logic. You just have to provide transition value to this component later.
+/// - [`UiBase`] _(Type only, not a component)_
+/// - [`UiHover`]
+/// - [`UiSelected`]
+/// - [`UiClicked`]
+/// - [`UiIntro`]
+/// - [`UiOutro`]
+///
+/// Dependant components:
+/// - [`UiLayout`]
+/// - [`UiColor`]
+///
+/// ## 🛠️ Example
+/// ```
+/// # use bevy::prelude::*;
+/// # use bevy::color::palettes::basic::*;
+/// # use bevy_lunex::*;
+/// # fn spawn_main_menu(mut commands: Commands, asset_server: Res) {
+/// # commands.spawn((
+/// # UiLayoutRoot,
+/// # )).with_children(|ui| {
+/// ui.spawn((
+/// // Like this you can enable a state
+/// UiHover::new().forward_speed(20.0).backward_speed(4.0),
+/// // You can define layouts per state
+/// UiLayout::new(vec![
+/// (UiBase::id(), UiLayout::window().full()),
+/// (UiHover::id(), UiLayout::window().x(Rl(10.0)).full())
+/// ]),
+/// // You can define colors per state
+/// UiColor::new(vec![
+/// (UiBase::id(), Color::Srgba(RED).with_alpha(0.8)),
+/// (UiHover::id(), Color::Srgba(YELLOW).with_alpha(1.2))
+/// ]),
+/// // ... Sprite, Text, etc.
+///
+/// // Add observers that enable/disable the hover state component
+/// )).observe(hover_set::, true>)
+/// .observe(hover_set::, false>);
+/// # });
+/// # }
+/// ```
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct UiState {
+ /// Stored transition per state
+ states: HashMap,
+}
+/// Default constructor
+impl Default for UiState {
+ fn default() -> Self {
+ let mut map = HashMap::new();
+ map.insert(UiBase::id(), 1.0);
+ Self {
+ states: map,
+ }
+ }
+}
+
+/// This system controls the [`UiBase`] state. This state is decreased based on total sum of all other active states.
+pub fn system_state_base_balancer(
+ mut query: Query<&mut UiState, Changed>,
+) {
+ for mut manager in &mut query {
+ // Normalize the active nobase state weights
+ let mut total_nonbase_weight = 0.0;
+ for (state, value) in &manager.states {
+ if *state == UiBase::id() { continue; }
+ total_nonbase_weight += value;
+ }
+
+ // Decrease base transition based on other states
+ if let Some(value) = manager.states.get_mut(&UiBase::id()) {
+ *value = (1.0 - total_nonbase_weight).clamp(0.0, 1.0);
+ }
+ }
+}
+/// This system pipes the attached state component data to the [`UiState`] component.
+pub fn system_state_pipe_into_manager(
+ mut commads: Commands,
+ mut query: Query<(&mut UiState, &S), Changed>,
+) {
+ for (mut manager, state) in &mut query {
+ // Send the value to the manager
+ if let Some(value) = manager.states.get_mut(&S::id()) {
+ *value = state.value();
+
+ // Insert the value if it does not exist
+ } else {
+ manager.states.insert(S::id(), state.value());
+ }
+ // Recompute layout
+ commads.trigger(RecomputeUiLayout);
+ }
+}
+
+/// Trait that all states must implement before being integrated into the state machine.
+pub trait UiStateTrait: Send + Sync + 'static {
+ /// This is used as a key to identify a Ui-Node state.
+ fn id() -> TypeId {
+ TypeId::of::()
+ }
+ /// This must return a value between `0.0 - 1.0`. It is used as transition value
+ /// for a state, with `0.0` being off and `1.0` being on. Any smoothing should happen
+ /// inside this function.
+ fn value(&self) -> f32;
+}
+
+/// **Ui Base** - The default state for a Ui-Node, used only for the [`UiBase::id`] key. It is not a component that you can control.
+#[derive(Clone, PartialEq, Debug)]
+pub struct UiBase;
+impl UiStateTrait for UiBase {
+ fn value(&self) -> f32 {
+ 1.0
+ }
+}
+
+
+// #====================#
+// #=== TEXT CONTROL ===#
+
+/// **Ui Text Size** - This component is used to control the size of a text compared
+/// to other Ui-Nodes. It works by overwritting the attached [`UiLayout`] window
+/// size parameter to match the text bounds. The value provided is used as a _scale_
+/// to adjust this size, specificaly it's height. It is recommended to use `non-relative`
+/// units such as [`Ab`], [`Rw`], [`Rh`], [`Vh`], [`Vw`] and [`Em`] for even values.
+///
+/// Affected components:
+/// - [`UiLayout`] - **MUST BE WINDOW TYPE** for this to work
+///
+/// ## 🛠️ Example
+/// ```
+/// # use bevy::prelude::*;
+/// # use bevy_lunex::*;
+/// # fn spawn_main_menu(mut commands: Commands, asset_server: Res) {
+/// # commands.spawn((
+/// # UiLayoutRoot,
+/// # )).with_children(|ui| {
+/// ui.spawn((
+/// // Position the text using the window layout's position and anchor
+/// UiLayout::window().pos((Rh(40.0), Rl(50.0))).anchor(Anchor::CenterLeft).pack(),
+/// // This controls the height of the text, so 60% of the parent's node height
+/// UiTextSize::from(Rh(60.0)),
+/// // You can attach text like this
+/// Text2d::new("Button"),
+/// // Font size now works as "text resolution"
+/// TextFont {
+/// font: asset_server.load("fonts/Rajdhani.ttf"),
+/// font_size: 64.0,
+/// ..default()
+/// },
+/// ));
+/// # });
+/// # }
+/// ```
+#[derive(Component, Reflect, Deref, DerefMut, Default, Clone, PartialEq, Debug)]
+pub struct UiTextSize (pub UiValue);
+/// Constructors
+impl >> From for UiTextSize {
+ fn from(value: T) -> Self {
+ UiTextSize(value.into())
+ }
+}
+
+/// This system takes [`TextLayoutInfo`] data and pipes them into querried [`Transform`] scale.
+pub fn system_text_size_from_dimension(
+ mut commands: Commands,
+ mut query: Query<(&mut Transform, &Dimension, &TextLayoutInfo), Changed>,
+) {
+ for (mut transform, dimension, text_info) in &mut query {
+ // Wait for text to render
+ if text_info.size.y == 0.0 {
+ commands.trigger(RecomputeUiLayout);
+ }
+
+ // Scale the text
+ let scale = **dimension / text_info.size;
+ transform.scale.x = scale.x;
+ transform.scale.y = scale.x;
+ }
+}
+
+/// This system takes updated [`TextLayoutInfo`] data and overwrites coresponding [`UiLayout`] data to match the text size.
+pub fn system_text_size_to_layout(
+ mut commands: Commands,
+ mut query: Query<(&mut UiLayout, &TextLayoutInfo, &UiTextSize), Changed>,
+) {
+ for (mut layout, text_info, text_size) in &mut query {
+ // Wait for text to render
+ if text_info.size.y == 0.0 {
+ commands.trigger(RecomputeUiLayout);
+ }
+
+ // Create the text layout
+ match layout.layouts.get_mut(&UiBase::id()).expect("UiBase state not found for Text") {
+ UiLayoutType::Window(window) => {
+ window.set_height(**text_size);
+ window.set_width(**text_size * (text_info.size.x / text_info.size.y));
+ },
+ UiLayoutType::Solid(solid) => {
+ solid.set_size(Ab(text_info.size));
+ },
+ _ => {},
+ }
+ }
+}
+
+
+// #=====================#
+// #=== STATE CONTROL ===#
+
+/// **Ui Mesh Plane 3d** - This component is used to mark mesh entities that can be freely replaced with quad mesh on demand.
+#[derive(Component, Reflect, Default, Clone, PartialEq, Debug)]
+#[require(Mesh3d)]
+pub struct UiMeshPlane3d;
+
+/// **Ui Mesh Plane 2d** - This component is used to mark mesh entities that can be freely replaced with quad mesh on demand.
+#[derive(Component, Reflect, Default, Clone, PartialEq, Debug)]
+#[require(Mesh2d)]
+pub struct UiMeshPlane2d;
+
+/// This system takes [`Dimension`] data and constructs a plane mesh.
+pub fn system_mesh_3d_reconstruct_from_dimension(
+ mut query: Query<(&Dimension, &mut Mesh3d), (With, Changed)>,
+ mut meshes: ResMut>,
+) {
+ for (dimension, mut mesh) in &mut query {
+ let plane_mesh = meshes.add(Rectangle::new(dimension.x, dimension.y));
+ mesh.0 = plane_mesh;
+ }
+}
+
+/// This system takes [`Dimension`] data and constructs a plane mesh.
+pub fn system_mesh_2d_reconstruct_from_dimension(
+ mut query: Query<(&Dimension, &mut Mesh2d), (With, Changed)>,
+ mut meshes: ResMut>,
+) {
+ for (dimension, mut mesh) in &mut query {
+ let plane_mesh = meshes.add(Rectangle::new(dimension.x, dimension.y));
+ mesh.0 = plane_mesh;
+ }
+}
+
+
+
+// #=======================#
+// #=== CAMERA FETCHING ===#
+
+/// **Ui Fetch From Camera** - Attaching this component to [`UiLayoutRoot`] will make the [`Dimension`]
+/// component pull data from a [`Camera`] with attached [`UiSourceCamera`] that has the same index.
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct UiFetchFromCamera;
+
+/// **Ui Source Camera** - Marks a [`Camera`] as a source for [`UiLayoutRoot`] with [`UiFetchFromCamera`].
+/// They must have the same index and only one [`UiSourceCamera`] can exist for a single index.
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct UiSourceCamera;
+
+/// This system takes [`Camera`] viewport data and pipes them into querried [`Dimension`] + [`UiLayoutRoot`] + [`UiFetchFromCamera`].
+pub fn system_fetch_dimension_from_camera(
+ src_query: Query<(&Camera, Option<&OrthographicProjection>), (With>, Changed)>,
+ mut dst_query: Query<&mut Dimension, (With, With>)>,
+) {
+ // Check if we have a camera dimension input
+ if src_query.is_empty() { return; }
+ let Ok((camera, projection_option)) = src_query.get_single() else {
+ warn_once!("Multiple UiSourceCamera<{INDEX}> exist at once! Ignoring all camera inputs to avoid unexpected behavior!");
+ return;
+ };
+
+ // Pipe the camera viewport size
+ if let Some(cam_size) = camera.physical_viewport_size() {
+ for mut size in &mut dst_query {
+ **size = Vec2::from((cam_size.x as f32, cam_size.y as f32)) * if let Some(p) = projection_option { p.scale } else { 1.0 };
+ }
+ }
+}
+
+/// This system listens for added [`UiFetchFromCamera`] components and if it finds one, mutable accesses all [`Camera`]s to trigger fetching systems.
+pub fn system_touch_camera_if_fetch_added(
+ query: Query>>,
+ mut cameras: Query<&mut Camera, With>>,
+){
+ if !query.is_empty() {
+ for mut camera in &mut cameras {
+ camera.as_mut();
+ }
+ }
+}
+
+
+// #===================#
+// #=== STYLE COLOR ===#
+
+/// **Ui Color** - This component is used to control the color of the Ui-Node.
+/// It is synchronized with a state machine and allows for specifying unique
+/// colors for each state.
+///
+/// Affected components:
+/// - [`Sprite`]
+/// - [`TextColor`]
+///
+/// ## 🛠️ Example
+/// ```
+/// # use bevy::prelude::*;
+/// # use bevy::color::palettes::basic::*;
+/// # use bevy_lunex::*;
+/// # fn spawn_main_menu(mut commands: Commands, asset_server: Res) {
+/// # commands.spawn((
+/// # UiLayoutRoot,
+/// # )).with_children(|ui| {
+/// // Spawn as a single color
+/// ui.spawn((
+/// // ... Layout, etc.
+/// UiColor::from(Color::Srgba(RED).with_alpha(0.8)),
+/// // ... Sprite, Text, etc.
+/// ));
+///
+/// // Spawn as a collection for different states
+/// ui.spawn((
+/// // ... Layout, etc.
+/// UiColor::new(vec![
+/// (UiBase::id(), Color::Srgba(RED).with_alpha(0.8)),
+/// (UiHover::id(), Color::Srgba(YELLOW).with_alpha(1.2))
+/// ]),
+/// // ... Sprite, Text, etc.
+/// ));
+/// # });
+/// # }
+/// ```
+#[derive(Component, Reflect, Deref, DerefMut, Default, Clone, PartialEq, Debug)]
+pub struct UiColor {
+ colors: HashMap
+}
+/// Constructors
+impl UiColor {
+ /// Define multiple states at once using a vec.
+ pub fn new(value: Vec<(TypeId, impl Into)>) -> Self {
+ let mut map = HashMap::new();
+ for (state, layout) in value {
+ map.insert(state, layout.into());
+ }
+ Self { colors: map }
+ }
+}
+/// Conversion implementations
+impl > From for UiColor {
+ fn from(value: T) -> Self {
+ let mut map = HashMap::new();
+ map.insert(UiBase::id(), value.into());
+ Self {
+ colors: map,
+ }
+ }
+}
+
+/// This system takes care of [`UiColor`] data and updates querried [`Sprite`] and [`TextColor`] components.
+pub fn system_color(
+ mut query: Query<(Option<&mut Sprite>, Option<&mut TextColor>, &UiColor, &UiState), Or<(Changed, Changed)>>,
+) {
+ for (node_sprite_option, node_text_option, node_color, node_state) in &mut query {
+
+ // Normalize the active state weights
+ let mut total_weight = 0.0;
+ for (state, _) in &node_color.colors {
+ if let Some(weight) = node_state.states.get(state) {
+ total_weight += weight;
+ }
+ }
+
+ // Combine the color into one normalized
+ let mut blend_color = Hsla::new(0.0, 0.0, 0.0, 0.0);
+
+ // If no state active just try to use base color
+ if total_weight == 0.0 {
+ if let Some(color) = node_color.colors.get(&UiBase::id()) {
+ blend_color = (*color).into();
+ }
+
+ // Blend colors from active states
+ } else {
+ for (state, color) in &node_color.colors {
+ if let Some(weight) = node_state.states.get(state) {
+ let converted: Hsla = (*color).into();
+
+ if blend_color.alpha == 0.0 {
+ blend_color.hue = converted.hue;
+ } else {
+ blend_color.hue = lerp_hue(blend_color.hue, converted.hue, weight / total_weight);
+ }
+
+ //blend_color.hue += converted.hue * (weight / total_weight);
+ blend_color.saturation += converted.saturation * (weight / total_weight);
+ blend_color.lightness += converted.lightness * (weight / total_weight);
+ blend_color.alpha += converted.alpha * (weight / total_weight);
+ }
+ }
+ }
+
+ // Apply the color to attached components
+ if let Some(mut sprite) = node_sprite_option {
+ sprite.color = blend_color.into();
+ }
+ if let Some(mut text) = node_text_option {
+ **text = blend_color.into();
+ }
+ }
+}
+fn lerp_hue(h1: f32, h2: f32, t: f32) -> f32 {
+ let diff = (h2 - h1 + 540.0) % 360.0 - 180.0; // Ensure shortest direction
+ (h1 + diff * t + 360.0) % 360.0
+}
+
+
+// #===============================#
+// #=== THE LUNEX SETS & GROUPS ===#
+
+/// System set for [`UiLunexPlugin`]
+#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
+pub enum UiSystems {
+ /// Systems that modify data pre-computation
+ PreCompute,
+ /// The computation
+ Compute,
+ /// Systems that modify data post-computation
+ PostCompute,
+}
+
+/// Gizmo group for UI 2D node debug outlines
+#[derive(GizmoConfigGroup, Default, Reflect, Clone, Debug)]
+pub struct LunexGizmoGroup2d;
+
+/// Gizmo group for UI 3D node debug outlines
+#[derive(GizmoConfigGroup, Default, Reflect, Clone, Debug)]
+pub struct LunexGizmoGroup3d;
+
+
+// #=========================#
+// #=== THE LUNEX PLUGINS ===#
+
+/// This plugin is used for the main logic.
+#[derive(Debug, Default, Clone)]
+pub struct UiLunexPlugin;
+impl Plugin for UiLunexPlugin {
+ fn build(&self, app: &mut App) {
+
+ // Configure the system set
+ app.configure_sets(PostUpdate, (
+ UiSystems::PreCompute.before(UiSystems::Compute),
+ UiSystems::PostCompute.after(UiSystems::Compute).before(bevy::transform::TransformSystem::TransformPropagate),
+ ));
+
+ // Add observers
+ app.add_observer(observer_touch_layout_root);
+
+ // PRE-COMPUTE SYSTEMS
+ app.add_systems(PostUpdate, (
+
+ system_state_base_balancer,
+ system_text_size_to_layout.after(bevy::text::update_text2d_layout),
+
+ ).in_set(UiSystems::PreCompute));
+
+
+ // COMPUTE SYSTEMS
+ app.add_systems(PostUpdate, (
+
+ system_layout_compute,
+
+ ).in_set(UiSystems::Compute));
+
+
+ // POST-COMPUTE SYSTEMS
+ app.add_systems(PostUpdate, (
+
+ system_color,
+ system_mark_3d,
+ system_pipe_sprite_size_from_dimension.before(bevy::sprite::SpriteSystem::ComputeSlices),
+ system_text_size_from_dimension,
+ system_mesh_3d_reconstruct_from_dimension,
+ system_mesh_2d_reconstruct_from_dimension,
+ system_embedd_resize,
+
+ ).in_set(UiSystems::PostCompute));
+
+
+ // Add index plugins
+ app.add_plugins((
+ CursorPlugin,
+ UiLunexStatePlugin,
+ UiLunexPickingPlugin,
+ UiLunexIndexPlugin::<0>,
+ UiLunexIndexPlugin::<1>,
+ UiLunexIndexPlugin::<2>,
+ UiLunexIndexPlugin::<3>,
+ ));
+ }
+}
+
+
+/// This plugin is used to enable debug functionality.
+#[derive(Debug, Default, Clone)]
+pub struct UiLunexDebugPlugin;
+impl Plugin for UiLunexDebugPlugin {
+ fn build(&self, app: &mut App) {
+
+ // Configure the Gizmo render groups
+ app .init_gizmo_group::()
+ .init_gizmo_group::()
+ .add_systems(Startup, |mut config_store: ResMut| {
+ let (my_config, _) = config_store.config_mut::();
+ my_config.render_layers = RenderLayers::layer(GIZMO_2D_LAYER);
+
+ let (my_config, _) = config_store.config_mut::();
+ my_config.render_layers = RenderLayers::layer(GIZMO_3D_LAYER);
+ });
+
+ // Add the 2d and 3d gizmo outlines
+ app.add_systems(PostUpdate, (
+ system_debug_draw_gizmo_2d,
+ system_debug_draw_gizmo_3d,
+ ));
+
+ // Add the debug tree printing
+ app.add_systems(PostUpdate, (
+ system_debug_print_data,
+ ).in_set(UiSystems::PostCompute));
+ }
+}
+
+
+/// This plugin is used to register index components.
+#[derive(Debug, Default, Clone)]
+pub struct UiLunexIndexPlugin;
+impl Plugin for UiLunexIndexPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(PostUpdate, (
+ system_fetch_dimension_from_camera::,
+ system_touch_camera_if_fetch_added::,
+ ).in_set(UiSystems::PreCompute));
+ }
+}
diff --git a/crate/src/picking.rs b/crate/src/picking.rs
new file mode 100644
index 0000000..590ffd4
--- /dev/null
+++ b/crate/src/picking.rs
@@ -0,0 +1,260 @@
+use std::cmp::Reverse;
+
+use bevy::math::{FloatExt, FloatOrd};
+use bevy::window::PrimaryWindow;
+use bevy::picking::backend::prelude::*;
+use bevy::picking::{backend::PointerHits, PickingBehavior};
+
+use crate::*;
+
+
+// #===============#
+// #=== BACKEND ===#
+
+/// Adds picking support for Lunex.
+#[derive(Clone)]
+pub struct UiLunexPickingPlugin;
+impl Plugin for UiLunexPickingPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(PreUpdate, lunex_picking.in_set(PickSet::Backend));
+ }
+}
+
+/// Checks if any Dimension entities are under a pointer.
+pub fn lunex_picking(
+ pointers: Query<(&PointerId, &PointerLocation)>,
+ cameras: Query<(Entity, &Camera, &GlobalTransform, &OrthographicProjection)>,
+ primary_window: Query>,
+ sprite_query: Query<(
+ Entity,
+ &Dimension,
+ &GlobalTransform,
+ Option<&PickingBehavior>,
+ &ViewVisibility,
+ )>,
+ mut output: EventWriter,
+) {
+ let mut sorted_sprites: Vec<_> = sprite_query.iter().filter_map(|(entity, dimension, transform, picking_behavior, vis)| {
+ if !transform.affine().is_nan() && vis.get() {
+ Some((entity, dimension, transform, picking_behavior))
+ } else {
+ None
+ }
+ }).collect();
+
+ sorted_sprites.sort_by_key(|x| Reverse(FloatOrd(x.2.translation().z)));
+
+ let primary_window = primary_window.get_single().ok();
+
+ for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| {
+ pointer_location.location().map(|loc| (pointer, loc))
+ }) {
+ let mut blocked = false;
+ let Some((cam_entity, camera, cam_transform, cam_ortho)) = cameras
+ .iter()
+ .filter(|(_, camera, _, _)| camera.is_active)
+ .find(|(_, camera, _, _)| {
+ camera
+ .target
+ .normalize(primary_window)
+ .map(|x| x == location.target)
+ .unwrap_or(false)
+ })
+ else {
+ continue;
+ };
+
+ let viewport_pos = camera
+ .logical_viewport_rect()
+ .map(|v| v.min)
+ .unwrap_or_default();
+ let pos_in_viewport = location.position - viewport_pos;
+
+ let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, pos_in_viewport) else {
+ continue;
+ };
+ let cursor_ray_len = cam_ortho.far - cam_ortho.near;
+ let cursor_ray_end = cursor_ray_world.origin + cursor_ray_world.direction * cursor_ray_len;
+
+ let picks: Vec<(Entity, HitData)> = sorted_sprites
+ .iter()
+ .copied()
+ .filter_map(|(entity, dimension, node_transform, picking_behavior)| {
+ if blocked {
+ return None;
+ }
+
+ let rect = Rect::from_center_size(Vec2::ZERO, **dimension);
+
+ // Transform cursor line segment to sprite coordinate system
+ let world_to_sprite = node_transform.affine().inverse();
+ let cursor_start_sprite = world_to_sprite.transform_point3(cursor_ray_world.origin);
+ let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);
+
+ // Find where the cursor segment intersects the plane Z=0 (which is the sprite's
+ // plane in sprite-local space). It may not intersect if, for example, we're
+ // viewing the sprite side-on
+ if cursor_start_sprite.z == cursor_end_sprite.z {
+ // Cursor ray is parallel to the sprite and misses it
+ return None;
+ }
+ let lerp_factor =
+ f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
+ if !(0.0..=1.0).contains(&lerp_factor) {
+ // Lerp factor is out of range, meaning that while an infinite line cast by
+ // the cursor would intersect the sprite, the sprite is not between the
+ // camera's near and far planes
+ return None;
+ }
+ // Otherwise we can interpolate the xy of the start and end positions by the
+ // lerp factor to get the cursor position in sprite space!
+ let cursor_pos_sprite = cursor_start_sprite
+ .lerp(cursor_end_sprite, lerp_factor)
+ .xy();
+
+ let is_cursor_in_sprite = rect.contains(cursor_pos_sprite);
+
+ blocked = is_cursor_in_sprite
+ && picking_behavior
+ .map(|p| p.should_block_lower)
+ .unwrap_or(true);
+
+ is_cursor_in_sprite.then(|| {
+ let hit_pos_world =
+ node_transform.transform_point(cursor_pos_sprite.extend(0.0));
+ // Transform point from world to camera space to get the Z distance
+ let hit_pos_cam = cam_transform
+ .affine()
+ .inverse()
+ .transform_point3(hit_pos_world);
+ // HitData requires a depth as calculated from the camera's near clipping plane
+ let depth = -cam_ortho.near - hit_pos_cam.z;
+ (
+ entity,
+ HitData::new(
+ cam_entity,
+ depth,
+ Some(hit_pos_world),
+ Some(*node_transform.back()),
+ ),
+ )
+ })
+ })
+ .collect();
+
+ let order = camera.order as f32;
+ output.send(PointerHits::new(*pointer, picks, order));
+ }
+}
+
+/* /// Checks if any Dimension entities are under a pointer
+fn lunex_picking(
+ pointers: Query<(&PointerId, &PointerLocation)>,
+ cameras: Query<(Entity, &Camera, &GlobalTransform, &Projection)>,
+ primary_window: Query>,
+ sprite_query: Query<(
+ Entity,
+ &Dimension,
+ &GlobalTransform,
+ Option<&PickingBehavior>,
+ &ViewVisibility,
+ )>,
+ mut output: EventWriter,
+) {
+ let mut sorted_sprites: Vec<_> = sprite_query.iter().filter_map(|(entity, dimension, transform, pickable, vis)| {
+ if !transform.affine().is_nan() && vis.get() {
+ Some((entity, dimension, transform, pickable))
+ } else {
+ None
+ }
+ }).collect();
+
+ // radsort is a stable radix sort that performed better than `slice::sort_by_key`
+ radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _)| {
+ -transform.translation().z
+ });
+
+ let primary_window = primary_window.get_single().ok();
+
+ for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| {
+ pointer_location.location().map(|loc| (pointer, loc))
+ }) {
+ let mut blocked = false;
+ let Some((cam_entity, camera, cam_transform, Projection::Orthographic(cam_ortho))) =
+ cameras.iter().filter(|(_, camera, _, _)| camera.is_active).find(|(_, camera, _, _)| {
+ camera.target.normalize(primary_window).is_some_and(|x| x == location.target)
+ })
+ else { continue; };
+
+ let viewport_pos = camera
+ .logical_viewport_rect()
+ .map(|v| v.min)
+ .unwrap_or_default();
+ let pos_in_viewport = location.position - viewport_pos;
+
+ let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, pos_in_viewport) else {
+ continue;
+ };
+ let cursor_ray_len = cam_ortho.far - cam_ortho.near;
+ let cursor_ray_end = cursor_ray_world.origin + cursor_ray_world.direction * cursor_ray_len;
+
+ let picks: Vec<(Entity, HitData)> = sorted_sprites
+ .iter()
+ .copied()
+ .filter_map(|(entity, dimension, node_transform, pickable)| {
+ if blocked {
+ return None;
+ }
+
+
+ // Transform cursor line segment to node coordinate system
+ let world_to_sprite = node_transform.affine().inverse();
+ let cursor_start_sprite = world_to_sprite.transform_point3(cursor_ray_world.origin);
+ let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);
+
+ // Find where the cursor segment intersects the plane Z=0 (which is the sprite's
+ // plane in sprite-local space). It may not intersect if, for example, we're
+ // viewing the sprite side-on
+ if cursor_start_sprite.z == cursor_end_sprite.z {
+ // Cursor ray is parallel to the sprite and misses it
+ return None;
+ }
+ let lerp_factor =
+ f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
+ if !(0.0..=1.0).contains(&lerp_factor) {
+ // Lerp factor is out of range, meaning that while an infinite line cast by
+ // the cursor would intersect the sprite, the sprite is not between the
+ // camera's near and far planes
+ return None;
+ }
+ // Otherwise we can interpolate the xy of the start and end positions by the
+ // lerp factor to get the cursor position in sprite space!
+ let cursor_pos_sprite = cursor_start_sprite
+ .lerp(cursor_end_sprite, lerp_factor)
+ .xy();
+
+ /* let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point(
+ cursor_pos_sprite,
+ &images,
+ &texture_atlas_layout,
+ ) else {
+ return None;
+ }; */
+
+ let rect = Rect::from_center_size(Vec2::ZERO, **dimension);
+ let is_cursor_in_sprite = rect.contains(cursor_pos_sprite);
+ blocked = is_cursor_in_sprite && pickable.map(|p| p.should_block_lower) != Some(false);
+
+ // HitData requires a depth as calculated from the camera's near clipping plane
+ let depth = -cam_ortho.near - node_transform.translation().z;
+
+
+ is_cursor_in_sprite.then_some((entity, HitData::new(cam_entity, depth, None, None)))
+
+ })
+ .collect();
+
+ let order = camera.order as f32;
+ output.send(PointerHits::new(*pointer, picks, order));
+ }
+} */
\ No newline at end of file
diff --git a/crate/src/states.rs b/crate/src/states.rs
new file mode 100644
index 0000000..fa35995
--- /dev/null
+++ b/crate/src/states.rs
@@ -0,0 +1,233 @@
+use crate::*;
+
+pub fn default_linear_curve() -> fn(f32) -> f32 { |v| {v} }
+
+// #=======================#
+// #=== THE HOVER STATE ===#
+
+/// **Ui Hover** - A built in state that should be triggered manually when a pointer hovers over a Ui-Node.
+/// This state first **needs to be enabled** for the entity by adding it as a component.
+///
+/// Then you can use the [`Self::id`] function to identify this state inside components
+/// that allow you to specify per state properties like [`Uilayout`].
+///
+/// For more information check the documentation on [`UiState`].
+///
+/// ```
+/// # use bevy::prelude::*;
+/// # use bevy_lunex::*;
+/// UiLayout::new(vec![
+/// (UiBase::id(), UiLayout::window().full()),
+/// (UiHover::id(), UiLayout::window().x(Rl(10.0)).full())
+/// ]);
+/// ```
+///
+/// To trigger the state we can either manually flip the [`UiHover::enable`] field or trigger the [`UiHoverSet`]
+/// helper event. To do this easily, there is a convinient observer [`hover_set`] provided for it.
+///
+/// ## 🛠️ Example
+/// ```
+/// # use bevy::prelude::*;
+/// # use bevy_lunex::*;
+/// # fn spawn_main_menu(mut commands: Commands, asset_server: Res) {
+/// # commands.spawn((
+/// # UiLayoutRoot,
+/// # )).with_children(|ui| {
+/// ui.spawn((
+/// // ... Layout, Color, etc.
+/// UiHover::new().forward_speed(20.0).backward_speed(4.0), // Enable the state
+///
+/// // Add the observers
+/// )).observe(hover_set::, true>)
+/// .observe(hover_set::, false>);
+/// # });
+/// # }
+/// ```
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct UiHover {
+ value: f32,
+ /// If the state is enabled
+ pub enable: bool,
+ /// The function to smooth the transition
+ #[reflect(ignore, default = "default_linear_curve")]
+ pub curve: fn(f32) -> f32,
+ /// The speed of transition forwards
+ pub forward_speed: f32,
+ /// The speed of transition backwards
+ pub backward_speed: f32,
+ /// Enable to have instant state transition
+ pub instant: bool,
+}
+/// Method implementations
+impl UiHover {
+ /// Create new instance
+ pub fn new() -> Self {
+ Self::default()
+ }
+ /// Replaces the curve function.
+ pub fn curve(mut self, curve: fn(f32) -> f32) -> Self {
+ self.curve = curve;
+ self
+ }
+ /// Replaces the speed with a new value.
+ pub fn forward_speed(mut self, forward_speed: f32) -> Self {
+ self.forward_speed = forward_speed;
+ self
+ }
+ /// Replaces the speed with a new value.
+ pub fn backward_speed(mut self, backward_speed: f32) -> Self {
+ self.backward_speed = backward_speed;
+ self
+ }
+ /// Replaces the instant property with a new value.
+ pub fn instant(mut self, instant: bool) -> Self {
+ self.instant = instant;
+ self
+ }
+}
+/// Constructor
+impl Default for UiHover {
+ fn default() -> Self {
+ Self {
+ value: 0.0,
+ enable: false,
+ curve: |v| {v},
+ forward_speed: 1.0,
+ backward_speed: 1.0,
+ instant: false,
+ }
+ }
+}
+/// State implementation
+impl UiStateTrait for UiHover {
+ fn value(&self) -> f32 {
+ (self.curve)(self.value)
+ }
+}
+
+/// This system updates the hover transition value over time
+pub fn system_state_hover_update(
+ time: Res,
+ mut query: Query<&mut UiHover>,
+) {
+ for mut hover in &mut query {
+ if hover.enable && hover.value < 1.0 {
+ if hover.instant { hover.value = 1.0; continue; }
+ hover.value = (hover.value + hover.forward_speed * time.delta_secs()).min(1.0);
+ }
+ if !hover.enable && hover.value > 0.0 {
+ if hover.instant { hover.value = 0.0; continue; }
+ hover.value = (hover.value - hover.backward_speed * time.delta_secs()).max(0.0);
+ }
+ }
+}
+
+/// Event that enables the hover transition
+#[derive(Event, Clone, Copy)]
+pub struct UiHoverSet(pub bool);
+
+/// This observer enables the hover transition on trigger
+fn observer_state_hover_set(
+ trigger: Trigger,
+ mut query: Query<&mut UiHover>,
+) {
+ if let Ok(mut hover) = query.get_mut(trigger.entity()) {
+ hover.enable = trigger.0;
+ }
+}
+
+/// Utility observer that triggers the [`UiHoverSet`] event on triggered event.
+pub fn hover_set(trigger: Trigger, mut commands: Commands) {
+ commands.trigger_targets(UiHoverSet(BOOL), trigger.entity());
+}
+
+
+// #==========================#
+// #=== THE SELECTED STATE ===#
+
+/// # WORK IN PROGRESS
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct UiSelected(pub f32);
+impl UiStateTrait for UiSelected {
+ fn value(&self) -> f32 {
+ self.0
+ }
+}
+
+
+// #=========================#
+// #=== THE CLICKED STATE ===#
+
+/// # WORK IN PROGRESS
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct UiClicked(pub f32);
+impl UiStateTrait for UiClicked {
+ fn value(&self) -> f32 {
+ self.0
+ }
+}
+
+
+// #=======================#
+// #=== THE INTRO STATE ===#
+
+/// # WORK IN PROGRESS
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct UiIntro(pub f32);
+impl UiStateTrait for UiIntro {
+ fn value(&self) -> f32 {
+ self.0
+ }
+}
+
+
+// #=======================#
+// #=== THE OUTRO STATE ===#
+
+/// # WORK IN PROGRESS
+#[derive(Component, Reflect, Clone, PartialEq, Debug)]
+pub struct UiOutro(pub f32);
+impl UiStateTrait for UiOutro {
+ fn value(&self) -> f32 {
+ self.0
+ }
+}
+
+
+
+
+// #========================#
+// #=== THE STATE PLUGIN ===#
+
+/// This observer will listen for said event and duplicate it to it's children
+fn observer_event_duplicator(trigger: Trigger, mut commands: Commands, mut query: Query<&Children>) {
+ if let Ok(children) = query.get_mut(trigger.entity()) {
+ let targets: Vec = children.iter().copied().collect();
+ commands.trigger_targets(*trigger.event(), targets);
+ }
+}
+
+/// This plugin is used for the main logic.
+pub struct UiLunexStatePlugin;
+impl Plugin for UiLunexStatePlugin {
+ fn build(&self, app: &mut App) {
+
+ // Add observers
+ app.add_observer(observer_state_hover_set);
+
+ // Add event child duplication
+ app.add_observer(observer_event_duplicator::);
+
+ // PRE-COMPUTE SYSTEMS
+ app.add_systems(Update, (
+
+ system_state_hover_update,
+ system_state_pipe_into_manager::,
+ system_state_pipe_into_manager::,
+ system_state_pipe_into_manager::,
+ system_state_pipe_into_manager::,
+ system_state_pipe_into_manager::,
+
+ ).in_set(UiSystems::PreCompute));
+ }
+}
\ No newline at end of file
diff --git a/crates/lunex_engine/src/core/value.rs b/crate/src/units.rs
similarity index 57%
rename from crates/lunex_engine/src/core/value.rs
rename to crate/src/units.rs
index cf2b26a..6124124 100644
--- a/crates/lunex_engine/src/core/value.rs
+++ b/crate/src/units.rs
@@ -1,3 +1,5 @@
+use crate::*;
+
use std::ops::Add;
use std::ops::AddAssign;
use std::ops::Neg;
@@ -6,17 +8,297 @@ use std::ops::SubAssign;
use std::ops::Mul;
use std::ops::MulAssign;
-use crate::import::*;
-use super::NiceDisplay;
+// #======================#
+// #=== THE UNIT TYPES ===#
+
+/// **Absolute** - Represents non-changing unit. Scale can be modified but by default `1Ab = 1Px`.
+/// ## 🛠️ Example
+/// ```
+/// # use bevy_lunex::*;
+/// let a: Ab = Ab(4.0) + Ab(6.0); // -> 10px
+/// let b: Ab = Ab(4.0) * 2.0; // -> 8px
+/// ```
+#[derive(Debug, Default, Clone, Copy, PartialEq, Deref, DerefMut, Reflect)]
+pub struct Ab(pub T);
+
+/// **Relative** - Represents scalable unit `0% to 100%`. `120%` is allowed.
+/// ## 🛠️ Example
+/// ```
+/// # use bevy_lunex::*;
+/// let a: Rl = Rl(25.0) + Rl(40.0); // -> 65%
+/// let b: Rl = Rl(25.0) * 3.0; // -> 75%
+/// ```
+#[derive(Debug, Default, Clone, Copy, PartialEq, Deref, DerefMut, Reflect)]
+pub struct Rl(pub T);
+
+/// **Relative width** - Represents scalable unit `0% to 100%`. `120%` is allowed.
+/// Proportional to a width measure even when used in a height field.
+/// ## 🛠️ Example
+/// ```
+/// # use bevy_lunex::*;
+/// let a: Rw = Rw(25.0) + Rw(40.0); // -> 65%
+/// let b: Rw = Rw(25.0) * 3.0; // -> 75%
+/// ```
+#[derive(Debug, Default, Clone, Copy, PartialEq, Deref, DerefMut, Reflect)]
+pub struct Rw(pub T);
+
+/// **Relative height** - Represents scalable unit `0% to 100%`. `120%` is allowed.
+/// Proportional to a height measure even when used in a width field.
+/// ## 🛠️ Example
+/// ```
+/// # use bevy_lunex::*;
+/// let a: Rh = Rh(25.0) + Rh(40.0); // -> 65%
+/// let b: Rh = Rh(25.0) * 3.0; // -> 75%
+/// ```
+#[derive(Debug, Default, Clone, Copy, PartialEq, Deref, DerefMut, Reflect)]
+pub struct Rh(pub T);
+
+/// **Size of M** - Represents unit that is the size of the symbol `M`. Which is `16px` with `font size 16px` and so on.
+/// ## 🛠️ Example
+/// ```
+/// # use bevy_lunex::*;
+/// let a: Em = Em(1.0) + Em(2.0); // -> 3em == 48px with font size 16px
+/// ```
+#[derive(Debug, Default, Clone, Copy, PartialEq, Deref, DerefMut, Reflect)]
+pub struct Em(pub T);
+
+/// **Viewport** - Represents scalable unit `0% to 100%` of the root container. `120%` is allowed.
+/// ## 🛠️ Example
+/// ```
+/// # use bevy_lunex::*;
+/// let a: Vp = Vp(25.0) + Vp(40.0); // -> 65%
+/// let b: Vp = Vp(25.0) * 3.0; // -> 75%
+/// ```
+#[derive(Debug, Default, Clone, Copy, PartialEq, Deref, DerefMut, Reflect)]
+pub struct Vp(pub T);
+
+/// **Viewport width** - Represents scalable unit `0% to 100%` of the root container. `120%` is allowed.
+/// Proportional to a width measure even when used in a height field.
+/// ## 🛠️ Example
+/// ```
+/// # use bevy_lunex::*;
+/// let a: Vw = Vw(25.0) + Vw(40.0); // -> 65%
+/// let b: Vw = Vw(25.0) * 3.0; // -> 75%
+/// ```
+#[derive(Debug, Default, Clone, Copy, PartialEq, Deref, DerefMut, Reflect)]
+pub struct Vw(pub T);
+
+/// **Viewport Height** - Represents scalable unit `0% to 100%` of the root container. `120%` is allowed.
+/// Proportional to a height measure even when used in a width field.
+/// ## 🛠️ Example
+/// ```
+/// # use bevy_lunex::*;
+/// let a: Vh = Vh(25.0) + Vh(40.0); // -> 65%
+/// let b: Vh = Vh(25.0) * 3.0; // -> 75%
+/// ```
+#[derive(Debug, Default, Clone, Copy, PartialEq, Deref, DerefMut, Reflect)]
+pub struct Vh(pub T);
+
+
+/// Implement basic math and conversions for a type
+macro_rules! init_unit {
+ ($($unit:ident), *) => {
+ $(
+ // Implement negation of the same type
+ impl > Neg for $unit {
+ type Output = Self;
+ fn neg(self) -> Self::Output {
+ $unit(-self.0)
+ }
+ }
+
+ // Implement addition of the same type
+ impl > Add for $unit {
+ type Output = Self;
+ fn add(self, other: Self) -> Self::Output {
+ $unit(self.0 + other.0)
+ }
+ }
+ impl > AddAssign for $unit {
+ fn add_assign(&mut self, rhs: Self) {
+ self.0 += rhs.0
+ }
+ }
+
+ // Implement subtraction of the same type
+ impl > Sub for $unit {
+ type Output = Self;
+ fn sub(self, other: Self) -> Self::Output {
+ $unit(self.0 - other.0)
+ }
+ }
+ impl > SubAssign for $unit {
+ fn sub_assign(&mut self, rhs: Self) {
+ self.0 -= rhs.0
+ }
+ }
+
+ // Implement multiplication of the same type
+ impl > Mul for $unit {
+ type Output = Self;
+ fn mul(self, other: Self) -> Self::Output {
+ $unit(self.0 * other.0)
+ }
+ }
+ impl > MulAssign for $unit {
+ fn mul_assign(&mut self, rhs: Self) {
+ self.0 *= rhs.0
+ }
+ }
+
+ // Implement multiplication with the f32 type
+ impl > Mul for $unit {
+ type Output = $unit;
+ fn mul(self, rhs: f32) -> Self::Output {
+ $unit(self.0 * rhs)
+ }
+ }
+ impl > MulAssign for $unit {
+ fn mul_assign(&mut self, rhs: f32) {
+ self.0 *= rhs
+ }
+ }
+ )*
+ };
+}
+init_unit!(Ab, Rl, Rw, Rh, Em, Vp, Vw, Vh);
+/// Implement basic math and conversions for a type
+macro_rules! impl_unit_operations {
+ ($($unit:ident), *) => {
+ $(
+ impl From<$unit<(f32, f32)>> for UiValue {
+ fn from(val: $unit<(f32, f32)>) -> UiValue {
+ $unit(Vec2::new(val.0.0, val.0.1)).into()
+ }
+ }
+ impl From<$unit<(f32, f32, f32)>> for UiValue {
+ fn from(val: $unit<(f32, f32, f32)>) -> UiValue {
+ $unit(Vec3::new(val.0.0, val.0.1, val.0.2)).into()
+ }
+ }
+ impl From<$unit<(f32, f32, f32, f32)>> for UiValue {
+ fn from(val: $unit<(f32, f32, f32, f32)>) -> UiValue {
+ $unit(Vec4::new(val.0.0, val.0.1, val.0.2, val.0.3)).into()
+ }
+ }
+
+ impl From<$unit> for UiValue {
+ fn from(val: $unit) -> UiValue