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. - ## ![image](https://github.com/bytestring-net/bevy_lunex/blob/main/promo/bevypunk_1.png?raw=true) -![image](https://github.com/bytestring-net/bevy_lunex/blob/main/promo/bevypunk_3.jpeg?raw=true) - -> *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