From 14230990e91d316805c0030f4124439761b6876a Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 4 Aug 2025 16:40:40 -0700 Subject: [PATCH 1/3] Move android code to bevy_android --- crates/bevy_android/Cargo.toml | 22 ++++ crates/bevy_android/LICENSE-APACHE | 176 ++++++++++++++++++++++++++++ crates/bevy_android/LICENSE-MIT | 19 +++ crates/bevy_android/src/lib.rs | 10 ++ crates/bevy_asset/Cargo.toml | 2 +- crates/bevy_asset/src/io/android.rs | 8 +- crates/bevy_derive/src/bevy_main.rs | 4 +- crates/bevy_internal/Cargo.toml | 3 + crates/bevy_internal/src/lib.rs | 2 + crates/bevy_window/Cargo.toml | 3 - crates/bevy_window/src/lib.rs | 9 -- crates/bevy_winit/Cargo.toml | 3 + crates/bevy_winit/src/lib.rs | 3 +- 13 files changed, 244 insertions(+), 20 deletions(-) create mode 100644 crates/bevy_android/Cargo.toml create mode 100644 crates/bevy_android/LICENSE-APACHE create mode 100644 crates/bevy_android/LICENSE-MIT create mode 100644 crates/bevy_android/src/lib.rs diff --git a/crates/bevy_android/Cargo.toml b/crates/bevy_android/Cargo.toml new file mode 100644 index 0000000000000..19a41377b4f79 --- /dev/null +++ b/crates/bevy_android/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bevy_android" +version = "0.17.0-dev" +edition = "2024" +description = "Provides android functionality for Bevy Engine." +homepage = "https://bevy.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[target.'cfg(target_os = "android")'.dependencies] +android-activity = "0.6" + +[features] +default = [] + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_android/LICENSE-APACHE b/crates/bevy_android/LICENSE-APACHE new file mode 100644 index 0000000000000..d9a10c0d8e868 --- /dev/null +++ b/crates/bevy_android/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_android/LICENSE-MIT b/crates/bevy_android/LICENSE-MIT new file mode 100644 index 0000000000000..9cf106272ac3b --- /dev/null +++ b/crates/bevy_android/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_android/src/lib.rs b/crates/bevy_android/src/lib.rs new file mode 100644 index 0000000000000..5e8b500add8ea --- /dev/null +++ b/crates/bevy_android/src/lib.rs @@ -0,0 +1,10 @@ +//! Provides Android functionality for Bevy Engine. + +#[cfg(target_os = "android")] +pub use android_activity; + +/// [`AndroidApp`] provides an interface to query the application state as well as monitor events +/// (for example lifecycle and input events). +#[cfg(target_os = "android")] +pub static ANDROID_APP: std::sync::OnceLock = + std::sync::OnceLock::new(); diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 518e20e596ae3..2476f967280b5 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -65,7 +65,7 @@ uuid = { version = "1.13.1", default-features = false, features = [ tracing = { version = "0.1", default-features = false } [target.'cfg(target_os = "android")'.dependencies] -bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_android = { path = "../bevy_android", version = "0.17.0-dev", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. diff --git a/crates/bevy_asset/src/io/android.rs b/crates/bevy_asset/src/io/android.rs index 67ca8e339a22c..fd6a71d219894 100644 --- a/crates/bevy_asset/src/io/android.rs +++ b/crates/bevy_asset/src/io/android.rs @@ -17,7 +17,7 @@ pub struct AndroidAssetReader; impl AssetReader for AndroidAssetReader { async fn read<'a>(&'a self, path: &'a Path) -> Result { - let asset_manager = bevy_window::ANDROID_APP + let asset_manager = bevy_android::ANDROID_APP .get() .expect("Bevy must be setup with the #[bevy_main] macro on Android") .asset_manager(); @@ -31,7 +31,7 @@ impl AssetReader for AndroidAssetReader { async fn read_meta<'a>(&'a self, path: &'a Path) -> Result { let meta_path = get_meta_path(path); - let asset_manager = bevy_window::ANDROID_APP + let asset_manager = bevy_android::ANDROID_APP .get() .expect("Bevy must be setup with the #[bevy_main] macro on Android") .asset_manager(); @@ -47,7 +47,7 @@ impl AssetReader for AndroidAssetReader { &'a self, path: &'a Path, ) -> Result, AssetReaderError> { - let asset_manager = bevy_window::ANDROID_APP + let asset_manager = bevy_android::ANDROID_APP .get() .expect("Bevy must be setup with the #[bevy_main] macro on Android") .asset_manager(); @@ -73,7 +73,7 @@ impl AssetReader for AndroidAssetReader { } async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { - let asset_manager = bevy_window::ANDROID_APP + let asset_manager = bevy_android::ANDROID_APP .get() .expect("Bevy must be setup with the #[bevy_main] macro on Android") .asset_manager(); diff --git a/crates/bevy_derive/src/bevy_main.rs b/crates/bevy_derive/src/bevy_main.rs index 6481823ad474a..5b40a0f91f4cf 100644 --- a/crates/bevy_derive/src/bevy_main.rs +++ b/crates/bevy_derive/src/bevy_main.rs @@ -15,8 +15,8 @@ pub fn bevy_main(_attr: TokenStream, item: TokenStream) -> TokenStream { // guarantee required from the caller. #[unsafe(no_mangle)] #[cfg(target_os = "android")] - fn android_main(android_app: bevy::window::android_activity::AndroidApp) { - let _ = bevy::window::ANDROID_APP.set(android_app); + fn android_main(android_app: bevy::android::android_activity::AndroidApp) { + let _ = bevy::android::ANDROID_APP.set(android_app); main(); } diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 850474ca9638b..290a35be3a717 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -458,6 +458,9 @@ bevy_window = { path = "../bevy_window", optional = true, version = "0.17.0-dev" ] } bevy_winit = { path = "../bevy_winit", optional = true, version = "0.17.0-dev", default-features = false } +[target.'cfg(target_os = "android")'.dependencies] +bevy_android = { path = "../bevy_android", version = "0.17.0-dev", default-features = false } + [lints] workspace = true diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 61879abdc9086..67b1e465e715f 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -16,6 +16,8 @@ pub use default_plugins::*; #[cfg(feature = "bevy_window")] pub use bevy_a11y as a11y; +#[cfg(target_os = "android")] +pub use bevy_android as android; #[cfg(feature = "bevy_animation")] pub use bevy_animation as animation; #[cfg(feature = "bevy_anti_aliasing")] diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index 2cd96053b4c9b..90b7dc9983164 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -64,9 +64,6 @@ raw-window-handle = { version = "0.6", features = [ ], default-features = false } log = { version = "0.4", default-features = false } -[target.'cfg(target_os = "android")'.dependencies] -android-activity = "0.6" - [lints] workspace = true diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 22e657cf038c0..9e3455fc5168d 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -30,9 +30,6 @@ mod window; pub use crate::raw_handle::*; -#[cfg(target_os = "android")] -pub use android_activity; - pub use event::*; pub use monitor::*; pub use system::*; @@ -204,9 +201,3 @@ pub enum ExitCondition { /// surprise your users. DontExit, } - -/// [`AndroidApp`] provides an interface to query the application state as well as monitor events -/// (for example lifecycle and input events). -#[cfg(target_os = "android")] -pub static ANDROID_APP: std::sync::OnceLock = - std::sync::OnceLock::new(); diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index 46b8c7fb50963..92fe811f6ef22 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -60,6 +60,9 @@ wgpu-types = { version = "26", optional = true } accesskit = "0.21" tracing = { version = "0.1", default-features = false, features = ["std"] } +[target.'cfg(target_os = "android")'.dependencies] +bevy_android = { path = "../bevy_android", version = "0.17.0-dev", default-features = false } + [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { version = "0.2" } web-sys = "0.3" diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 65b7b6c04a084..66b0308e06883 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -124,7 +124,8 @@ impl Plugin for WinitPlugin { { use winit::platform::android::EventLoopBuilderExtAndroid; let msg = "Bevy must be setup with the #[bevy_main] macro on Android"; - event_loop_builder.with_android_app(bevy_window::ANDROID_APP.get().expect(msg).clone()); + event_loop_builder + .with_android_app(bevy_android::ANDROID_APP.get().expect(msg).clone()); } let event_loop = event_loop_builder From 4f68558767c512c92bec604680ab4620bf7a18f7 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 4 Aug 2025 23:46:25 -0700 Subject: [PATCH 2/3] Move backend-agnostic cursor code to bevy_window --- crates/bevy_feathers/Cargo.toml | 3 +- crates/bevy_feathers/src/cursor.rs | 5 +- crates/bevy_internal/Cargo.toml | 4 +- crates/bevy_window/Cargo.toml | 9 +- .../bevy_window/src/cursor/custom_cursor.rs | 69 ++++++ crates/bevy_window/src/cursor/mod.rs | 38 +++ .../src/{ => cursor}/system_cursor.rs | 0 crates/bevy_window/src/lib.rs | 10 +- crates/bevy_winit/Cargo.toml | 17 +- crates/bevy_winit/src/cursor.rs | 202 --------------- .../src/{ => cursor}/custom_cursor.rs | 104 ++------ crates/bevy_winit/src/cursor/mod.rs | 229 ++++++++++++++++++ crates/bevy_winit/src/lib.rs | 6 +- crates/bevy_winit/src/state.rs | 103 +------- examples/3d/clustered_decals.rs | 3 +- examples/3d/light_textures.rs | 3 +- examples/window/custom_cursor_image.rs | 2 +- examples/window/screenshot.rs | 3 +- examples/window/window_settings.rs | 6 +- 19 files changed, 400 insertions(+), 416 deletions(-) create mode 100644 crates/bevy_window/src/cursor/custom_cursor.rs create mode 100644 crates/bevy_window/src/cursor/mod.rs rename crates/bevy_window/src/{ => cursor}/system_cursor.rs (100%) delete mode 100644 crates/bevy_winit/src/cursor.rs rename crates/bevy_winit/src/{ => cursor}/custom_cursor.rs (84%) create mode 100644 crates/bevy_winit/src/cursor/mod.rs diff --git a/crates/bevy_feathers/Cargo.toml b/crates/bevy_feathers/Cargo.toml index 746db79b85d6d..aee34f5158655 100644 --- a/crates/bevy_feathers/Cargo.toml +++ b/crates/bevy_feathers/Cargo.toml @@ -29,14 +29,13 @@ bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [ ] } bevy_ui_render = { path = "../bevy_ui_render", version = "0.17.0-dev" } bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } -bevy_winit = { path = "../bevy_winit", version = "0.17.0-dev" } # other accesskit = "0.21" [features] default = [] -custom_cursor = ["bevy_winit/custom_cursor"] +custom_cursor = ["bevy_window/custom_cursor"] [lints] workspace = true diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index c61d57cc5f3de..ea10a3910e999 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -12,10 +12,9 @@ use bevy_ecs::{ }; use bevy_picking::{hover::HoverMap, pointer::PointerId, PickingSystems}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_window::{SystemCursorIcon, Window}; -use bevy_winit::cursor::CursorIcon; #[cfg(feature = "custom_cursor")] -use bevy_winit::cursor::CustomCursor; +use bevy_window::CustomCursor; +use bevy_window::{CursorIcon, SystemCursorIcon, Window}; /// A resource that specifies the cursor icon to be used when the mouse is not hovering over /// any other entity. This is used to set the default cursor icon for the window. diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 290a35be3a717..b75dc368c4ef4 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -303,8 +303,8 @@ reflect_functions = [ # Enable documentation reflection reflect_documentation = ["bevy_reflect/documentation"] -# Enable winit custom cursor support -custom_cursor = ["bevy_winit/custom_cursor"] +# Enable custom cursor support +custom_cursor = ["bevy_window/custom_cursor", "bevy_winit/custom_cursor"] # Experimental support for nodes that are ignored for UI layouting ghost_nodes = ["bevy_ui/ghost_nodes"] diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index 90b7dc9983164..aab3f5312d401 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -24,6 +24,9 @@ bevy_reflect = [ ## Adds serialization support through `serde`. serialize = ["serde", "bevy_ecs/serialize", "bevy_input/serialize"] +# Enable custom cursor support +custom_cursor = ["bevy_image", "bevy_asset"] + # Platform Compatibility ## Allows access to the `std` crate. Enabling this feature will prevent compilation @@ -49,10 +52,14 @@ bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = fa bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } bevy_input = { path = "../bevy_input", version = "0.17.0-dev", default-features = false } bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } + +# bevy optional +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev", default-features = false, optional = true } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev", optional = true } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "glam", ], optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other serde = { version = "1.0", features = [ diff --git a/crates/bevy_window/src/cursor/custom_cursor.rs b/crates/bevy_window/src/cursor/custom_cursor.rs new file mode 100644 index 0000000000000..164559baa4279 --- /dev/null +++ b/crates/bevy_window/src/cursor/custom_cursor.rs @@ -0,0 +1,69 @@ +use crate::cursor::CursorIcon; +use alloc::string::String; +use bevy_asset::Handle; +use bevy_image::{Image, TextureAtlas}; +use bevy_math::URect; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; + +/// A custom cursor created from an image. +#[derive(Debug, Clone, Default, Reflect, PartialEq, Eq, Hash)] +#[reflect(Debug, Default, Hash, PartialEq, Clone)] +pub struct CustomCursorImage { + /// Handle to the image to use as the cursor. The image must be in 8 bit int + /// or 32 bit float rgba. PNG images work well for this. + pub handle: Handle, + /// An optional texture atlas used to render the image. + pub texture_atlas: Option, + /// Whether the image should be flipped along its x-axis. + /// + /// If true, the cursor's `hotspot` automatically flips along with the + /// image. + pub flip_x: bool, + /// Whether the image should be flipped along its y-axis. + /// + /// If true, the cursor's `hotspot` automatically flips along with the + /// image. + pub flip_y: bool, + /// An optional rectangle representing the region of the image to render, + /// instead of rendering the full image. This is an easy one-off alternative + /// to using a [`TextureAtlas`]. + /// + /// When used with a [`TextureAtlas`], the rect is offset by the atlas's + /// minimal (top-left) corner position. + pub rect: Option, + /// X and Y coordinates of the hotspot in pixels. The hotspot must be within + /// the image bounds. + /// + /// If you are flipping the image using `flip_x` or `flip_y`, you don't need + /// to adjust this field to account for the flip because it is adjusted + /// automatically. + pub hotspot: (u16, u16), +} + +/// A custom cursor created from a URL. Note that this currently only works on the web. +#[derive(Debug, Clone, Default, Reflect, PartialEq, Eq, Hash)] +#[reflect(Debug, Default, Hash, PartialEq, Clone)] +pub struct CustomCursorUrl { + /// Web URL to an image to use as the cursor. PNGs are preferred. Cursor + /// creation can fail if the image is invalid or not reachable. + pub url: String, + /// X and Y coordinates of the hotspot in pixels. The hotspot must be within + /// the image bounds. + pub hotspot: (u16, u16), +} + +/// Custom cursor image data. +#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)] +#[reflect(Clone, PartialEq, Hash)] +pub enum CustomCursor { + /// Use an image as the cursor. + Image(CustomCursorImage), + /// Use a URL to an image as the cursor. Note that this currently only works on the web. + Url(CustomCursorUrl), +} + +impl From for CursorIcon { + fn from(cursor: CustomCursor) -> Self { + CursorIcon::Custom(cursor) + } +} diff --git a/crates/bevy_window/src/cursor/mod.rs b/crates/bevy_window/src/cursor/mod.rs new file mode 100644 index 0000000000000..f35d0377901e3 --- /dev/null +++ b/crates/bevy_window/src/cursor/mod.rs @@ -0,0 +1,38 @@ +//! Components to customize the window cursor. + +#[cfg(feature = "custom_cursor")] +mod custom_cursor; +mod system_cursor; + +#[cfg(feature = "custom_cursor")] +pub use custom_cursor::*; +pub use system_cursor::*; + +use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; + +#[cfg(feature = "custom_cursor")] +pub use crate::cursor::{CustomCursor, CustomCursorImage}; + +/// Insert into a window entity to set the cursor for that window. +#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)] +#[reflect(Component, Debug, Default, PartialEq, Clone)] +pub enum CursorIcon { + #[cfg(feature = "custom_cursor")] + /// Custom cursor image. + Custom(CustomCursor), + /// System provided cursor icon. + System(SystemCursorIcon), +} + +impl Default for CursorIcon { + fn default() -> Self { + CursorIcon::System(Default::default()) + } +} + +impl From for CursorIcon { + fn from(icon: SystemCursorIcon) -> Self { + CursorIcon::System(icon) + } +} diff --git a/crates/bevy_window/src/system_cursor.rs b/crates/bevy_window/src/cursor/system_cursor.rs similarity index 100% rename from crates/bevy_window/src/system_cursor.rs rename to crates/bevy_window/src/cursor/system_cursor.rs diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 9e3455fc5168d..18c95cba3f65e 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -17,23 +17,19 @@ extern crate std; extern crate alloc; -use alloc::sync::Arc; - -use bevy_platform::sync::Mutex; - +mod cursor; mod event; mod monitor; mod raw_handle; mod system; -mod system_cursor; mod window; pub use crate::raw_handle::*; +pub use cursor::*; pub use event::*; pub use monitor::*; pub use system::*; -pub use system_cursor::*; pub use window::*; /// The windowing prelude. @@ -48,7 +44,9 @@ pub mod prelude { }; } +use alloc::sync::Arc; use bevy_app::prelude::*; +use bevy_platform::sync::Mutex; impl Default for WindowPlugin { fn default() -> Self { diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index 92fe811f6ef22..7b4a98b79aafa 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -22,8 +22,13 @@ serialize = [ ] android-native-activity = ["winit/android-native-activity"] android-game-activity = ["winit/android-game-activity"] - -custom_cursor = ["bevy_image", "bevy_asset", "bytemuck", "wgpu-types"] +custom_cursor = [ + "bevy_window/custom_cursor", + "bevy_image", + "bevy_asset", + "wgpu-types", + "bytemuck", +] [dependencies] # bevy @@ -43,8 +48,14 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea ] } # bevy optional +## used by custom_cursor bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev", optional = true } +## used by custom_cursor bevy_image = { path = "../bevy_image", version = "0.17.0-dev", optional = true } +## used by custom_cursor +wgpu-types = { version = "26", optional = true } +## used by custom_cursor +bytemuck = { version = "1.5", optional = true } # other # feature rwh_06 refers to window_raw_handle@v0.6 @@ -55,8 +66,6 @@ accesskit_winit = { version = "0.29", default-features = false, features = [ approx = { version = "0.5", default-features = false } cfg-if = "1.0" raw-window-handle = "0.6" -bytemuck = { version = "1.5", optional = true } -wgpu-types = { version = "26", optional = true } accesskit = "0.21" tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/bevy_winit/src/cursor.rs b/crates/bevy_winit/src/cursor.rs deleted file mode 100644 index c5c5e489a665a..0000000000000 --- a/crates/bevy_winit/src/cursor.rs +++ /dev/null @@ -1,202 +0,0 @@ -//! Components to customize winit cursor - -use crate::{ - converters::convert_system_cursor_icon, - state::{CursorSource, PendingCursor}, -}; -#[cfg(feature = "custom_cursor")] -use crate::{ - custom_cursor::{ - calculate_effective_rect, extract_and_transform_rgba_pixels, extract_rgba_pixels, - transform_hotspot, CustomCursorPlugin, - }, - state::{CustomCursorCache, CustomCursorCacheKey}, - WinitCustomCursor, -}; -use bevy_app::{App, Last, Plugin}; -#[cfg(feature = "custom_cursor")] -use bevy_asset::Assets; -#[cfg(feature = "custom_cursor")] -use bevy_ecs::system::Res; -use bevy_ecs::{ - change_detection::DetectChanges, - component::Component, - entity::Entity, - lifecycle::Remove, - observer::On, - query::With, - reflect::ReflectComponent, - system::{Commands, Local, Query}, - world::Ref, -}; -#[cfg(feature = "custom_cursor")] -use bevy_image::{Image, TextureAtlasLayout}; -use bevy_platform::collections::HashSet; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_window::{SystemCursorIcon, Window}; -#[cfg(feature = "custom_cursor")] -use tracing::warn; - -#[cfg(feature = "custom_cursor")] -pub use crate::custom_cursor::{CustomCursor, CustomCursorImage}; - -#[cfg(all( - feature = "custom_cursor", - target_family = "wasm", - target_os = "unknown" -))] -pub use crate::custom_cursor::CustomCursorUrl; - -pub(crate) struct CursorPlugin; - -impl Plugin for CursorPlugin { - fn build(&self, app: &mut App) { - #[cfg(feature = "custom_cursor")] - app.add_plugins(CustomCursorPlugin); - - app.register_type::() - .add_systems(Last, update_cursors); - - app.add_observer(on_remove_cursor_icon); - } -} - -/// Insert into a window entity to set the cursor for that window. -#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)] -#[reflect(Component, Debug, Default, PartialEq, Clone)] -pub enum CursorIcon { - #[cfg(feature = "custom_cursor")] - /// Custom cursor image. - Custom(CustomCursor), - /// System provided cursor icon. - System(SystemCursorIcon), -} - -impl Default for CursorIcon { - fn default() -> Self { - CursorIcon::System(Default::default()) - } -} - -impl From for CursorIcon { - fn from(icon: SystemCursorIcon) -> Self { - CursorIcon::System(icon) - } -} - -fn update_cursors( - mut commands: Commands, - windows: Query<(Entity, Ref), With>, - #[cfg(feature = "custom_cursor")] cursor_cache: Res, - #[cfg(feature = "custom_cursor")] images: Res>, - #[cfg(feature = "custom_cursor")] texture_atlases: Res>, - mut queue: Local>, -) { - for (entity, cursor) in windows.iter() { - if !(queue.remove(&entity) || cursor.is_changed()) { - continue; - } - - let cursor_source = match cursor.as_ref() { - #[cfg(feature = "custom_cursor")] - CursorIcon::Custom(CustomCursor::Image(c)) => { - let CustomCursorImage { - handle, - texture_atlas, - flip_x, - flip_y, - rect, - hotspot, - } = c; - - let cache_key = CustomCursorCacheKey::Image { - id: handle.id(), - texture_atlas_layout_id: texture_atlas.as_ref().map(|a| a.layout.id()), - texture_atlas_index: texture_atlas.as_ref().map(|a| a.index), - flip_x: *flip_x, - flip_y: *flip_y, - rect: *rect, - }; - - if cursor_cache.0.contains_key(&cache_key) { - CursorSource::CustomCached(cache_key) - } else { - let Some(image) = images.get(handle) else { - warn!( - "Cursor image {handle:?} is not loaded yet and couldn't be used. Trying again next frame." - ); - queue.insert(entity); - continue; - }; - - let (rect, needs_sub_image) = - calculate_effective_rect(&texture_atlases, image, texture_atlas, rect); - - let (maybe_rgba, hotspot) = if *flip_x || *flip_y || needs_sub_image { - ( - extract_and_transform_rgba_pixels(image, *flip_x, *flip_y, rect), - transform_hotspot(*hotspot, *flip_x, *flip_y, rect), - ) - } else { - (extract_rgba_pixels(image), *hotspot) - }; - - let Some(rgba) = maybe_rgba else { - warn!("Cursor image {handle:?} not accepted because it's not rgba8 or rgba32float format"); - continue; - }; - - let source = match WinitCustomCursor::from_rgba( - rgba, - rect.width() as u16, - rect.height() as u16, - hotspot.0, - hotspot.1, - ) { - Ok(source) => source, - Err(err) => { - warn!("Cursor image {handle:?} is invalid: {err}"); - continue; - } - }; - - CursorSource::Custom((cache_key, source)) - } - } - #[cfg(all( - feature = "custom_cursor", - target_family = "wasm", - target_os = "unknown" - ))] - CursorIcon::Custom(CustomCursor::Url(c)) => { - let cache_key = CustomCursorCacheKey::Url(c.url.clone()); - - if cursor_cache.0.contains_key(&cache_key) { - CursorSource::CustomCached(cache_key) - } else { - use crate::CustomCursorExtWebSys; - let source = - WinitCustomCursor::from_url(c.url.clone(), c.hotspot.0, c.hotspot.1); - CursorSource::Custom((cache_key, source)) - } - } - CursorIcon::System(system_cursor_icon) => { - CursorSource::System(convert_system_cursor_icon(*system_cursor_icon)) - } - }; - - commands - .entity(entity) - .insert(PendingCursor(Some(cursor_source))); - } -} - -/// Resets the cursor to the default icon when `CursorIcon` is removed. -fn on_remove_cursor_icon(trigger: On, mut commands: Commands) { - // Use `try_insert` to avoid panic if the window is being destroyed. - commands - .entity(trigger.target()) - .try_insert(PendingCursor(Some(CursorSource::System( - convert_system_cursor_icon(SystemCursorIcon::Default), - )))); -} diff --git a/crates/bevy_winit/src/custom_cursor.rs b/crates/bevy_winit/src/cursor/custom_cursor.rs similarity index 84% rename from crates/bevy_winit/src/custom_cursor.rs rename to crates/bevy_winit/src/cursor/custom_cursor.rs index dd8236e30e43d..00133cbb4bf98 100644 --- a/crates/bevy_winit/src/custom_cursor.rs +++ b/crates/bevy_winit/src/cursor/custom_cursor.rs @@ -1,88 +1,30 @@ -use bevy_app::{App, Plugin}; -use bevy_asset::{Assets, Handle}; -use bevy_image::{Image, TextureAtlas, TextureAtlasLayout, TextureAtlasPlugin}; +use bevy_asset::{AssetId, Assets}; +use bevy_ecs::resource::Resource; +use bevy_image::{Image, TextureAtlas, TextureAtlasLayout}; use bevy_math::{ops, Rect, URect, UVec2, Vec2}; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_platform::collections::HashMap; use wgpu_types::TextureFormat; -use crate::{cursor::CursorIcon, state::CustomCursorCache}; - -/// A custom cursor created from an image. -#[derive(Debug, Clone, Default, Reflect, PartialEq, Eq, Hash)] -#[reflect(Debug, Default, Hash, PartialEq, Clone)] -pub struct CustomCursorImage { - /// Handle to the image to use as the cursor. The image must be in 8 bit int - /// or 32 bit float rgba. PNG images work well for this. - pub handle: Handle, - /// An optional texture atlas used to render the image. - pub texture_atlas: Option, - /// Whether the image should be flipped along its x-axis. - /// - /// If true, the cursor's `hotspot` automatically flips along with the - /// image. - pub flip_x: bool, - /// Whether the image should be flipped along its y-axis. - /// - /// If true, the cursor's `hotspot` automatically flips along with the - /// image. - pub flip_y: bool, - /// An optional rectangle representing the region of the image to render, - /// instead of rendering the full image. This is an easy one-off alternative - /// to using a [`TextureAtlas`]. - /// - /// When used with a [`TextureAtlas`], the rect is offset by the atlas's - /// minimal (top-left) corner position. - pub rect: Option, - /// X and Y coordinates of the hotspot in pixels. The hotspot must be within - /// the image bounds. - /// - /// If you are flipping the image using `flip_x` or `flip_y`, you don't need - /// to adjust this field to account for the flip because it is adjusted - /// automatically. - pub hotspot: (u16, u16), -} - -#[cfg(all(target_family = "wasm", target_os = "unknown"))] -/// A custom cursor created from a URL. -#[derive(Debug, Clone, Default, Reflect, PartialEq, Eq, Hash)] -#[reflect(Debug, Default, Hash, PartialEq, Clone)] -pub struct CustomCursorUrl { - /// Web URL to an image to use as the cursor. PNGs are preferred. Cursor - /// creation can fail if the image is invalid or not reachable. - pub url: String, - /// X and Y coordinates of the hotspot in pixels. The hotspot must be within - /// the image bounds. - pub hotspot: (u16, u16), -} - -/// Custom cursor image data. -#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)] -#[reflect(Clone, PartialEq, Hash)] -pub enum CustomCursor { - /// Use an image as the cursor. - Image(CustomCursorImage), +/// Caches custom cursors. On many platforms, creating custom cursors is expensive, especially on +/// the web. +#[derive(Debug, Clone, Default, Resource)] +pub struct WinitCustomCursorCache(pub HashMap); + +/// Identifiers for custom cursors used in caching. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum CustomCursorCacheKey { + /// A custom cursor with an image. + Image { + id: AssetId, + texture_atlas_layout_id: Option>, + texture_atlas_index: Option, + flip_x: bool, + flip_y: bool, + rect: Option, + }, #[cfg(all(target_family = "wasm", target_os = "unknown"))] - /// Use a URL to an image as the cursor. - Url(CustomCursorUrl), -} - -impl From for CursorIcon { - fn from(cursor: CustomCursor) -> Self { - CursorIcon::Custom(cursor) - } -} - -/// Adds support for custom cursors. -pub(crate) struct CustomCursorPlugin; - -impl Plugin for CustomCursorPlugin { - fn build(&self, app: &mut App) { - if !app.is_plugin_added::() { - app.add_plugins(TextureAtlasPlugin); - } - - app.init_resource::(); - } + /// A custom cursor with a URL. + Url(String), } /// Determines the effective rect and returns it along with a flag to indicate diff --git a/crates/bevy_winit/src/cursor/mod.rs b/crates/bevy_winit/src/cursor/mod.rs new file mode 100644 index 0000000000000..0cefc782f1b5e --- /dev/null +++ b/crates/bevy_winit/src/cursor/mod.rs @@ -0,0 +1,229 @@ +#[cfg(feature = "custom_cursor")] +mod custom_cursor; + +#[cfg(feature = "custom_cursor")] +pub use custom_cursor::*; + +use crate::{converters::convert_system_cursor_icon, state::WinitAppRunnerState, WINIT_WINDOWS}; +use bevy_app::{App, Last, Plugin}; +#[cfg(feature = "custom_cursor")] +use bevy_asset::Assets; +use bevy_ecs::{prelude::*, system::SystemState}; +#[cfg(feature = "custom_cursor")] +use bevy_image::{Image, TextureAtlasLayout}; +use bevy_platform::collections::HashSet; +#[cfg(feature = "custom_cursor")] +use bevy_window::CustomCursor; +use bevy_window::{CursorIcon, SystemCursorIcon, Window}; +#[cfg(feature = "custom_cursor")] +use winit::event_loop::ActiveEventLoop; + +/// Adds support for custom cursors. +pub(crate) struct WinitCursorPlugin; + +impl Plugin for WinitCursorPlugin { + fn build(&self, app: &mut App) { + #[cfg(feature = "custom_cursor")] + { + if !app.is_plugin_added::() { + app.add_plugins(bevy_image::TextureAtlasPlugin); + } + + app.init_resource::(); + } + + app.add_systems(Last, update_cursors) + .add_observer(on_remove_cursor_icon); + } +} + +/// A source for a cursor. Consumed by the winit event loop. +#[derive(Debug)] +pub enum CursorSource { + #[cfg(feature = "custom_cursor")] + /// A custom cursor was identified to be cached, no reason to recreate it. + CustomCached(CustomCursorCacheKey), + #[cfg(feature = "custom_cursor")] + /// A custom cursor was not cached, so it needs to be created by the winit event loop. + Custom((CustomCursorCacheKey, winit::window::CustomCursorSource)), + /// A system cursor was requested. + System(winit::window::CursorIcon), +} + +/// Component that indicates what cursor should be used for a window. Inserted +/// automatically after changing `CursorIcon` and consumed by the winit event +/// loop. +#[derive(Component, Debug)] +pub struct PendingCursor(pub Option); + +impl WinitAppRunnerState { + pub(crate) fn update_cursors( + &mut self, + #[cfg(feature = "custom_cursor")] event_loop: &ActiveEventLoop, + ) { + #[cfg(feature = "custom_cursor")] + let mut windows_state: SystemState<( + ResMut, + Query<(Entity, &mut PendingCursor), Changed>, + )> = SystemState::new(self.world_mut()); + #[cfg(feature = "custom_cursor")] + let (mut cursor_cache, mut windows) = windows_state.get_mut(self.world_mut()); + #[cfg(not(feature = "custom_cursor"))] + let mut windows_state: SystemState<( + Query<(Entity, &mut PendingCursor), Changed>, + )> = SystemState::new(self.world_mut()); + #[cfg(not(feature = "custom_cursor"))] + let (mut windows,) = windows_state.get_mut(self.world_mut()); + + WINIT_WINDOWS.with_borrow(|winit_windows| { + for (entity, mut pending_cursor) in windows.iter_mut() { + let Some(winit_window) = winit_windows.get_window(entity) else { + continue; + }; + let Some(pending_cursor) = pending_cursor.0.take() else { + continue; + }; + + let final_cursor: winit::window::Cursor = match pending_cursor { + #[cfg(feature = "custom_cursor")] + CursorSource::CustomCached(cache_key) => { + let Some(cached_cursor) = cursor_cache.0.get(&cache_key) else { + tracing::error!("Cursor should have been cached, but was not found"); + continue; + }; + cached_cursor.clone().into() + } + #[cfg(feature = "custom_cursor")] + CursorSource::Custom((cache_key, cursor)) => { + let custom_cursor = event_loop.create_custom_cursor(cursor); + cursor_cache.0.insert(cache_key, custom_cursor.clone()); + custom_cursor.into() + } + CursorSource::System(system_cursor) => system_cursor.into(), + }; + winit_window.set_cursor(final_cursor); + } + }); + } +} + +fn update_cursors( + mut commands: Commands, + windows: Query<(Entity, Ref), With>, + #[cfg(feature = "custom_cursor")] cursor_cache: Res, + #[cfg(feature = "custom_cursor")] images: Res>, + #[cfg(feature = "custom_cursor")] texture_atlases: Res>, + mut queue: Local>, +) { + for (entity, cursor) in windows.iter() { + if !(queue.remove(&entity) || cursor.is_changed()) { + continue; + } + + let cursor_source = match cursor.as_ref() { + #[cfg(feature = "custom_cursor")] + CursorIcon::Custom(CustomCursor::Image(c)) => { + let bevy_window::CustomCursorImage { + handle, + texture_atlas, + flip_x, + flip_y, + rect, + hotspot, + } = c; + + let cache_key = CustomCursorCacheKey::Image { + id: handle.id(), + texture_atlas_layout_id: texture_atlas.as_ref().map(|a| a.layout.id()), + texture_atlas_index: texture_atlas.as_ref().map(|a| a.index), + flip_x: *flip_x, + flip_y: *flip_y, + rect: *rect, + }; + + if cursor_cache.0.contains_key(&cache_key) { + CursorSource::CustomCached(cache_key) + } else { + let Some(image) = images.get(handle) else { + tracing::warn!( + "Cursor image {handle:?} is not loaded yet and couldn't be used. Trying again next frame." + ); + queue.insert(entity); + continue; + }; + + let (rect, needs_sub_image) = + calculate_effective_rect(&texture_atlases, image, texture_atlas, rect); + + let (maybe_rgba, hotspot) = if *flip_x || *flip_y || needs_sub_image { + ( + extract_and_transform_rgba_pixels(image, *flip_x, *flip_y, rect), + transform_hotspot(*hotspot, *flip_x, *flip_y, rect), + ) + } else { + (extract_rgba_pixels(image), *hotspot) + }; + + let Some(rgba) = maybe_rgba else { + tracing::warn!("Cursor image {handle:?} not accepted because it's not rgba8 or rgba32float format"); + continue; + }; + + let source = match winit::window::CustomCursor::from_rgba( + rgba, + rect.width() as u16, + rect.height() as u16, + hotspot.0, + hotspot.1, + ) { + Ok(source) => source, + Err(err) => { + tracing::warn!("Cursor image {handle:?} is invalid: {err}"); + continue; + } + }; + + CursorSource::Custom((cache_key, source)) + } + } + #[cfg(feature = "custom_cursor")] + CursorIcon::Custom(CustomCursor::Url(_c)) => { + #[cfg(all(target_family = "wasm", target_os = "unknown"))] + { + let cache_key = CustomCursorCacheKey::Url(_c.url.clone()); + + if cursor_cache.0.contains_key(&cache_key) { + CursorSource::CustomCached(cache_key) + } else { + use crate::CustomCursorExtWebSys; + let source = + WinitCustomCursor::from_url(_c.url.clone(), _c.hotspot.0, _c.hotspot.1); + CursorSource::Custom((cache_key, source)) + } + } + #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] + { + bevy_log::error_once!("CustomCursor::Url is not supported on this platform. Falling back to CursorIcon::System(SystemCursorIcon::Default)"); + CursorSource::System(winit::window::CursorIcon::Default) + } + } + CursorIcon::System(system_cursor_icon) => { + CursorSource::System(convert_system_cursor_icon(*system_cursor_icon)) + } + }; + + commands + .entity(entity) + .insert(PendingCursor(Some(cursor_source))); + } +} + +/// Resets the cursor to the default icon when `CursorIcon` is removed. +fn on_remove_cursor_icon(trigger: On, mut commands: Commands) { + // Use `try_insert` to avoid panic if the window is being destroyed. + commands + .entity(trigger.target()) + .try_insert(PendingCursor(Some(CursorSource::System( + convert_system_cursor_icon(SystemCursorIcon::Default), + )))); +} diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 66b0308e06883..dc3eaf36b88c5 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -45,9 +45,7 @@ use crate::{ pub mod accessibility; mod converters; -pub mod cursor; -#[cfg(feature = "custom_cursor")] -mod custom_cursor; +mod cursor; mod state; mod system; mod winit_config; @@ -151,7 +149,7 @@ impl Plugin for WinitPlugin { ); app.add_plugins(AccessKitPlugin); - app.add_plugins(cursor::CursorPlugin); + app.add_plugins(cursor::WinitCursorPlugin); } } diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 4e828fe0b2d1c..cccc6e9aca4e3 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -1,7 +1,5 @@ use approx::relative_eq; use bevy_app::{App, AppExit, PluginsState}; -#[cfg(feature = "custom_cursor")] -use bevy_asset::AssetId; use bevy_ecs::{ change_detection::{DetectChanges, Res}, entity::Entity, @@ -10,8 +8,6 @@ use bevy_ecs::{ system::SystemState, world::FromWorld, }; -#[cfg(feature = "custom_cursor")] -use bevy_image::{Image, TextureAtlasLayout}; use bevy_input::{ gestures::*, mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, @@ -19,11 +15,7 @@ use bevy_input::{ #[cfg(any(not(target_arch = "wasm32"), feature = "custom_cursor"))] use bevy_log::error; use bevy_log::{trace, warn}; -#[cfg(feature = "custom_cursor")] -use bevy_math::URect; use bevy_math::{ivec2, DVec2, Vec2}; -#[cfg(feature = "custom_cursor")] -use bevy_platform::collections::HashMap; use bevy_platform::time::Instant; #[cfg(not(target_arch = "wasm32"))] use bevy_tasks::tick_global_task_pools_on_main_thread; @@ -58,7 +50,7 @@ use crate::{ /// Persistent state that is used to run the [`App`] according to the current /// [`UpdateMode`]. -struct WinitAppRunnerState { +pub(crate) struct WinitAppRunnerState { /// The running app. app: App, /// Exit value once the loop is finished. @@ -109,8 +101,6 @@ struct WinitAppRunnerState { impl WinitAppRunnerState { fn new(mut app: App) -> Self { app.add_event::(); - #[cfg(feature = "custom_cursor")] - app.init_resource::(); let event_writer_system_state: SystemState<( EventWriter, @@ -150,54 +140,11 @@ impl WinitAppRunnerState { self.app.world() } - fn world_mut(&mut self) -> &mut World { + pub(crate) fn world_mut(&mut self) -> &mut World { self.app.world_mut() } } -#[cfg(feature = "custom_cursor")] -/// Identifiers for custom cursors used in caching. -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub enum CustomCursorCacheKey { - /// A custom cursor with an image. - Image { - id: AssetId, - texture_atlas_layout_id: Option>, - texture_atlas_index: Option, - flip_x: bool, - flip_y: bool, - rect: Option, - }, - #[cfg(all(target_family = "wasm", target_os = "unknown"))] - /// A custom cursor with a URL. - Url(String), -} - -#[cfg(feature = "custom_cursor")] -/// Caches custom cursors. On many platforms, creating custom cursors is expensive, especially on -/// the web. -#[derive(Debug, Clone, Default, Resource)] -pub struct CustomCursorCache(pub HashMap); - -/// A source for a cursor. Consumed by the winit event loop. -#[derive(Debug)] -pub enum CursorSource { - #[cfg(feature = "custom_cursor")] - /// A custom cursor was identified to be cached, no reason to recreate it. - CustomCached(CustomCursorCacheKey), - #[cfg(feature = "custom_cursor")] - /// A custom cursor was not cached, so it needs to be created by the winit event loop. - Custom((CustomCursorCacheKey, winit::window::CustomCursorSource)), - /// A system cursor was requested. - System(winit::window::CursorIcon), -} - -/// Component that indicates what cursor should be used for a window. Inserted -/// automatically after changing `CursorIcon` and consumed by the winit event -/// loop. -#[derive(Component, Debug)] -pub struct PendingCursor(pub Option); - impl ApplicationHandler for WinitAppRunnerState { fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { if event_loop.exiting() { @@ -889,52 +836,6 @@ impl WinitAppRunnerState { .write_batch(buffered_events); } } - - fn update_cursors(&mut self, #[cfg(feature = "custom_cursor")] event_loop: &ActiveEventLoop) { - #[cfg(feature = "custom_cursor")] - let mut windows_state: SystemState<( - ResMut, - Query<(Entity, &mut PendingCursor), Changed>, - )> = SystemState::new(self.world_mut()); - #[cfg(feature = "custom_cursor")] - let (mut cursor_cache, mut windows) = windows_state.get_mut(self.world_mut()); - #[cfg(not(feature = "custom_cursor"))] - let mut windows_state: SystemState<( - Query<(Entity, &mut PendingCursor), Changed>, - )> = SystemState::new(self.world_mut()); - #[cfg(not(feature = "custom_cursor"))] - let (mut windows,) = windows_state.get_mut(self.world_mut()); - - WINIT_WINDOWS.with_borrow(|winit_windows| { - for (entity, mut pending_cursor) in windows.iter_mut() { - let Some(winit_window) = winit_windows.get_window(entity) else { - continue; - }; - let Some(pending_cursor) = pending_cursor.0.take() else { - continue; - }; - - let final_cursor: winit::window::Cursor = match pending_cursor { - #[cfg(feature = "custom_cursor")] - CursorSource::CustomCached(cache_key) => { - let Some(cached_cursor) = cursor_cache.0.get(&cache_key) else { - error!("Cursor should have been cached, but was not found"); - continue; - }; - cached_cursor.clone().into() - } - #[cfg(feature = "custom_cursor")] - CursorSource::Custom((cache_key, cursor)) => { - let custom_cursor = event_loop.create_custom_cursor(cursor); - cursor_cache.0.insert(cache_key, custom_cursor.clone()); - custom_cursor.into() - } - CursorSource::System(system_cursor) => system_cursor.into(), - }; - winit_window.set_cursor(final_cursor); - } - }); - } } /// The default [`App::runner`] for the [`WinitPlugin`](crate::WinitPlugin) plugin. diff --git a/examples/3d/clustered_decals.rs b/examples/3d/clustered_decals.rs index a0593af0e08cd..fc90086af0107 100644 --- a/examples/3d/clustered_decals.rs +++ b/examples/3d/clustered_decals.rs @@ -15,8 +15,7 @@ use bevy::{ render_resource::{AsBindGroup, ShaderRef}, renderer::{RenderAdapter, RenderDevice}, }, - window::SystemCursorIcon, - winit::cursor::CursorIcon, + window::{CursorIcon, SystemCursorIcon}, }; use ops::{acos, cos, sin}; use widgets::{ diff --git a/examples/3d/light_textures.rs b/examples/3d/light_textures.rs index 743f3b152e69a..6beeb8711ecbc 100644 --- a/examples/3d/light_textures.rs +++ b/examples/3d/light_textures.rs @@ -9,8 +9,7 @@ use bevy::{ pbr::{decal, DirectionalLightTexture, NotShadowCaster, PointLightTexture, SpotLightTexture}, prelude::*, render::renderer::{RenderAdapter, RenderDevice}, - window::SystemCursorIcon, - winit::cursor::CursorIcon, + window::{CursorIcon, SystemCursorIcon}, }; use light_consts::lux::{AMBIENT_DAYLIGHT, CLEAR_SUNRISE}; use ops::{acos, cos, sin}; diff --git a/examples/window/custom_cursor_image.rs b/examples/window/custom_cursor_image.rs index ea715a54d6d73..26e52781f53da 100644 --- a/examples/window/custom_cursor_image.rs +++ b/examples/window/custom_cursor_image.rs @@ -5,7 +5,7 @@ use std::time::Duration; use bevy::{ prelude::*, - winit::cursor::{CursorIcon, CustomCursor, CustomCursorImage}, + window::{CursorIcon, CustomCursor, CustomCursorImage}, }; fn main() { diff --git a/examples/window/screenshot.rs b/examples/window/screenshot.rs index 84c15a6837349..cd16539eaeee4 100644 --- a/examples/window/screenshot.rs +++ b/examples/window/screenshot.rs @@ -3,8 +3,7 @@ use bevy::{ prelude::*, render::view::screenshot::{save_to_disk, Capturing, Screenshot}, - window::SystemCursorIcon, - winit::cursor::CursorIcon, + window::{CursorIcon, SystemCursorIcon}, }; fn main() { diff --git a/examples/window/window_settings.rs b/examples/window/window_settings.rs index 7be899f6e315b..842d2c0ce0285 100644 --- a/examples/window/window_settings.rs +++ b/examples/window/window_settings.rs @@ -2,14 +2,14 @@ //! the mouse pointer in various ways. #[cfg(feature = "custom_cursor")] -use bevy::winit::cursor::{CustomCursor, CustomCursorImage}; +use bevy::window::{CustomCursor, CustomCursorImage}; use bevy::{ diagnostic::{FrameCount, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, prelude::*, window::{ - CursorGrabMode, CursorOptions, PresentMode, SystemCursorIcon, WindowLevel, WindowTheme, + CursorGrabMode, CursorIcon, CursorOptions, PresentMode, SystemCursorIcon, WindowLevel, + WindowTheme, }, - winit::cursor::CursorIcon, }; fn main() { From 70c56c0252ffe7a0b4bdf2572b9c8c0d7fdef1dc Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Tue, 5 Aug 2025 12:07:57 -0700 Subject: [PATCH 3/3] Fix wasm build --- crates/bevy_winit/src/cursor/mod.rs | 7 +++++-- crates/bevy_winit/src/state.rs | 6 ++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/bevy_winit/src/cursor/mod.rs b/crates/bevy_winit/src/cursor/mod.rs index 0cefc782f1b5e..a1b6b55e50f90 100644 --- a/crates/bevy_winit/src/cursor/mod.rs +++ b/crates/bevy_winit/src/cursor/mod.rs @@ -196,8 +196,11 @@ fn update_cursors( CursorSource::CustomCached(cache_key) } else { use crate::CustomCursorExtWebSys; - let source = - WinitCustomCursor::from_url(_c.url.clone(), _c.hotspot.0, _c.hotspot.1); + let source = winit::window::CustomCursor::from_url( + _c.url.clone(), + _c.hotspot.0, + _c.hotspot.1, + ); CursorSource::Custom((cache_key, source)) } } diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index cccc6e9aca4e3..4f90905463351 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -12,8 +12,6 @@ use bevy_input::{ gestures::*, mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, }; -#[cfg(any(not(target_arch = "wasm32"), feature = "custom_cursor"))] -use bevy_log::error; use bevy_log::{trace, warn}; use bevy_math::{ivec2, DVec2, Vec2}; use bevy_platform::time::Instant; @@ -863,11 +861,11 @@ pub fn winit_runner(mut app: App, event_loop: EventLoop) -> } else { let mut runner_state = runner_state; if let Err(err) = event_loop.run_app(&mut runner_state) { - error!("winit event loop returned an error: {err}"); + bevy_log::error!("winit event loop returned an error: {err}"); } // If everything is working correctly then the event loop only exits after it's sent an exit code. runner_state.app_exit.unwrap_or_else(|| { - error!("Failed to receive an app exit code! This is a bug"); + bevy_log::error!("Failed to receive an app exit code! This is a bug"); AppExit::error() }) }