Add custom cursors (#14284)
# Objective - Add custom images as cursors - Fixes #9557 ## Solution - Change cursor type to accommodate both native and image cursors - I don't really like this solution because I couldn't use `Handle<Image>` directly. I would need to import `bevy_assets` and that causes a circular dependency. Alternatively we could use winit's `CustomCursor` smart pointers, but that seems hard because the event loop is needed to create those and is not easily accessable for users. So now I need to copy around rgba buffers which is sad. - I use a cache because especially on the web creating cursor images is really slow - Sorry to #14196 for yoinking, I just wanted to make a quick solution for myself and thought that I should probably share it too. Update: - Now uses `Handle<Image>`, reads rgba data in `bevy_render` and uses resources to send the data to `bevy_winit`, where the final cursors are created. ## Testing - Added example which works fine at least on Linux Wayland (winit side has been tested with all platforms). - I haven't tested if the url cursor works. ## Migration Guide - `CursorIcon` is no longer a field in `Window`, but a separate component can be inserted to a window entity. It has been changed to an enum that can hold custom images in addition to system icons. - `Cursor` is renamed to `CursorOptions` and `cursor` field of `Window` is renamed to `cursor_options` - `CursorIcon` is renamed to `SystemCursorIcon` --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
This commit is contained in:
		
							parent
							
								
									d4ec80d5d2
								
							
						
					
					
						commit
						47c4e3084a
					
				@ -58,6 +58,7 @@ bevy_render_macros = { path = "macros", version = "0.15.0-dev" }
 | 
				
			|||||||
bevy_time = { path = "../bevy_time", version = "0.15.0-dev" }
 | 
					bevy_time = { path = "../bevy_time", version = "0.15.0-dev" }
 | 
				
			||||||
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
 | 
					bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
 | 
				
			||||||
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
 | 
					bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
 | 
				
			||||||
 | 
					bevy_winit = { path = "../bevy_winit", version = "0.15.0-dev" }
 | 
				
			||||||
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
 | 
					bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
 | 
				
			||||||
bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" }
 | 
					bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										175
									
								
								crates/bevy_render/src/view/window/cursor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								crates/bevy_render/src/view/window/cursor.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,175 @@
 | 
				
			|||||||
 | 
					use bevy_asset::{AssetId, Assets, Handle};
 | 
				
			||||||
 | 
					use bevy_ecs::{
 | 
				
			||||||
 | 
					    change_detection::DetectChanges,
 | 
				
			||||||
 | 
					    component::Component,
 | 
				
			||||||
 | 
					    entity::Entity,
 | 
				
			||||||
 | 
					    query::With,
 | 
				
			||||||
 | 
					    reflect::ReflectComponent,
 | 
				
			||||||
 | 
					    system::{Commands, Local, Query, Res},
 | 
				
			||||||
 | 
					    world::Ref,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use bevy_reflect::{std_traits::ReflectDefault, Reflect};
 | 
				
			||||||
 | 
					use bevy_utils::{tracing::warn, HashSet};
 | 
				
			||||||
 | 
					use bevy_window::{SystemCursorIcon, Window};
 | 
				
			||||||
 | 
					use bevy_winit::{
 | 
				
			||||||
 | 
					    convert_system_cursor_icon, CursorSource, CustomCursorCache, CustomCursorCacheKey,
 | 
				
			||||||
 | 
					    PendingCursor,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use wgpu::TextureFormat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::prelude::Image;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Insert into a window entity to set the cursor for that window.
 | 
				
			||||||
 | 
					#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)]
 | 
				
			||||||
 | 
					#[reflect(Component, Debug, Default)]
 | 
				
			||||||
 | 
					pub enum CursorIcon {
 | 
				
			||||||
 | 
					    /// Custom cursor image.
 | 
				
			||||||
 | 
					    Custom(CustomCursor),
 | 
				
			||||||
 | 
					    /// System provided cursor icon.
 | 
				
			||||||
 | 
					    System(SystemCursorIcon),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Default for CursorIcon {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        CursorIcon::System(Default::default())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<SystemCursorIcon> for CursorIcon {
 | 
				
			||||||
 | 
					    fn from(icon: SystemCursorIcon) -> Self {
 | 
				
			||||||
 | 
					        CursorIcon::System(icon)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<CustomCursor> for CursorIcon {
 | 
				
			||||||
 | 
					    fn from(cursor: CustomCursor) -> Self {
 | 
				
			||||||
 | 
					        CursorIcon::Custom(cursor)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Custom cursor image data.
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)]
 | 
				
			||||||
 | 
					pub enum CustomCursor {
 | 
				
			||||||
 | 
					    /// Image to use as a cursor.
 | 
				
			||||||
 | 
					    Image {
 | 
				
			||||||
 | 
					        /// The image must be in 8 bit int or 32 bit float rgba. PNG images
 | 
				
			||||||
 | 
					        /// work well for this.
 | 
				
			||||||
 | 
					        handle: Handle<Image>,
 | 
				
			||||||
 | 
					        /// X and Y coordinates of the hotspot in pixels. The hotspot must be
 | 
				
			||||||
 | 
					        /// within the image bounds.
 | 
				
			||||||
 | 
					        hotspot: (u16, u16),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
 | 
				
			||||||
 | 
					    /// A URL to an image to use as the cursor.
 | 
				
			||||||
 | 
					    Url {
 | 
				
			||||||
 | 
					        /// Web URL to an image to use as the cursor. PNGs preferred. Cursor
 | 
				
			||||||
 | 
					        /// creation can fail if the image is invalid or not reachable.
 | 
				
			||||||
 | 
					        url: String,
 | 
				
			||||||
 | 
					        /// X and Y coordinates of the hotspot in pixels. The hotspot must be
 | 
				
			||||||
 | 
					        /// within the image bounds.
 | 
				
			||||||
 | 
					        hotspot: (u16, u16),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn update_cursors(
 | 
				
			||||||
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    mut windows: Query<(Entity, Ref<CursorIcon>), With<Window>>,
 | 
				
			||||||
 | 
					    cursor_cache: Res<CustomCursorCache>,
 | 
				
			||||||
 | 
					    images: Res<Assets<Image>>,
 | 
				
			||||||
 | 
					    mut queue: Local<HashSet<Entity>>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    for (entity, cursor) in windows.iter_mut() {
 | 
				
			||||||
 | 
					        if !(queue.remove(&entity) || cursor.is_changed()) {
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let cursor_source = match cursor.as_ref() {
 | 
				
			||||||
 | 
					            CursorIcon::Custom(CustomCursor::Image { handle, hotspot }) => {
 | 
				
			||||||
 | 
					                let cache_key = match handle.id() {
 | 
				
			||||||
 | 
					                    AssetId::Index { index, .. } => {
 | 
				
			||||||
 | 
					                        CustomCursorCacheKey::AssetIndex(index.to_bits())
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    AssetId::Uuid { uuid } => CustomCursorCacheKey::AssetUuid(uuid.as_u128()),
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                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 Some(rgba) = image_to_rgba_pixels(image) else {
 | 
				
			||||||
 | 
					                        warn!("Cursor image {handle:?} not accepted because it's not rgba8 or rgba32float format");
 | 
				
			||||||
 | 
					                        continue;
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    let width = image.texture_descriptor.size.width;
 | 
				
			||||||
 | 
					                    let height = image.texture_descriptor.size.height;
 | 
				
			||||||
 | 
					                    let source = match bevy_winit::WinitCustomCursor::from_rgba(
 | 
				
			||||||
 | 
					                        rgba,
 | 
				
			||||||
 | 
					                        width as u16,
 | 
				
			||||||
 | 
					                        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(target_family = "wasm", target_os = "unknown"))]
 | 
				
			||||||
 | 
					            CursorIcon::Custom(CustomCursor::Url { url, hotspot }) => {
 | 
				
			||||||
 | 
					                let cache_key = CustomCursorCacheKey::Url(url.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if cursor_cache.0.contains_key(&cache_key) {
 | 
				
			||||||
 | 
					                    CursorSource::CustomCached(cache_key)
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    use bevy_winit::CustomCursorExtWebSys;
 | 
				
			||||||
 | 
					                    let source =
 | 
				
			||||||
 | 
					                        bevy_winit::WinitCustomCursor::from_url(url.clone(), hotspot.0, 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)));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Returns the image data as a `Vec<u8>`.
 | 
				
			||||||
 | 
					/// Only supports rgba8 and rgba32float formats.
 | 
				
			||||||
 | 
					fn image_to_rgba_pixels(image: &Image) -> Option<Vec<u8>> {
 | 
				
			||||||
 | 
					    match image.texture_descriptor.format {
 | 
				
			||||||
 | 
					        TextureFormat::Rgba8Unorm
 | 
				
			||||||
 | 
					        | TextureFormat::Rgba8UnormSrgb
 | 
				
			||||||
 | 
					        | TextureFormat::Rgba8Snorm
 | 
				
			||||||
 | 
					        | TextureFormat::Rgba8Uint
 | 
				
			||||||
 | 
					        | TextureFormat::Rgba8Sint => Some(image.data.clone()),
 | 
				
			||||||
 | 
					        TextureFormat::Rgba32Float => Some(
 | 
				
			||||||
 | 
					            image
 | 
				
			||||||
 | 
					                .data
 | 
				
			||||||
 | 
					                .chunks(4)
 | 
				
			||||||
 | 
					                .map(|chunk| {
 | 
				
			||||||
 | 
					                    let chunk = chunk.try_into().unwrap();
 | 
				
			||||||
 | 
					                    let num = bytemuck::cast_ref::<[u8; 4], f32>(chunk);
 | 
				
			||||||
 | 
					                    (num * 255.0) as u8
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .collect(),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        _ => None,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -6,7 +6,7 @@ use crate::{
 | 
				
			|||||||
    texture::TextureFormatPixelInfo,
 | 
					    texture::TextureFormatPixelInfo,
 | 
				
			||||||
    Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper,
 | 
					    Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use bevy_app::{App, Plugin};
 | 
					use bevy_app::{App, Last, Plugin};
 | 
				
			||||||
use bevy_ecs::{entity::EntityHashMap, prelude::*};
 | 
					use bevy_ecs::{entity::EntityHashMap, prelude::*};
 | 
				
			||||||
#[cfg(target_os = "linux")]
 | 
					#[cfg(target_os = "linux")]
 | 
				
			||||||
use bevy_utils::warn_once;
 | 
					use bevy_utils::warn_once;
 | 
				
			||||||
@ -14,6 +14,7 @@ use bevy_utils::{default, tracing::debug, HashSet};
 | 
				
			|||||||
use bevy_window::{
 | 
					use bevy_window::{
 | 
				
			||||||
    CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing,
 | 
					    CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					use bevy_winit::CustomCursorCache;
 | 
				
			||||||
use std::{
 | 
					use std::{
 | 
				
			||||||
    num::NonZeroU32,
 | 
					    num::NonZeroU32,
 | 
				
			||||||
    ops::{Deref, DerefMut},
 | 
					    ops::{Deref, DerefMut},
 | 
				
			||||||
@ -24,17 +25,22 @@ use wgpu::{
 | 
				
			|||||||
    TextureViewDescriptor,
 | 
					    TextureViewDescriptor,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod cursor;
 | 
				
			||||||
pub mod screenshot;
 | 
					pub mod screenshot;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use screenshot::{
 | 
					use screenshot::{
 | 
				
			||||||
    ScreenshotManager, ScreenshotPlugin, ScreenshotPreparedState, ScreenshotToScreenPipeline,
 | 
					    ScreenshotManager, ScreenshotPlugin, ScreenshotPreparedState, ScreenshotToScreenPipeline,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use self::cursor::update_cursors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct WindowRenderPlugin;
 | 
					pub struct WindowRenderPlugin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Plugin for WindowRenderPlugin {
 | 
					impl Plugin for WindowRenderPlugin {
 | 
				
			||||||
    fn build(&self, app: &mut App) {
 | 
					    fn build(&self, app: &mut App) {
 | 
				
			||||||
        app.add_plugins(ScreenshotPlugin);
 | 
					        app.add_plugins(ScreenshotPlugin)
 | 
				
			||||||
 | 
					            .init_resource::<CustomCursorCache>()
 | 
				
			||||||
 | 
					            .add_systems(Last, update_cursors);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
 | 
					        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
 | 
				
			||||||
            render_app
 | 
					            render_app
 | 
				
			||||||
 | 
				
			|||||||
@ -15,19 +15,19 @@ use std::sync::{Arc, Mutex};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use bevy_a11y::Focus;
 | 
					use bevy_a11y::Focus;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod cursor;
 | 
					 | 
				
			||||||
mod event;
 | 
					mod event;
 | 
				
			||||||
mod monitor;
 | 
					mod monitor;
 | 
				
			||||||
mod raw_handle;
 | 
					mod raw_handle;
 | 
				
			||||||
mod system;
 | 
					mod system;
 | 
				
			||||||
 | 
					mod system_cursor;
 | 
				
			||||||
mod window;
 | 
					mod window;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use crate::raw_handle::*;
 | 
					pub use crate::raw_handle::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use cursor::*;
 | 
					 | 
				
			||||||
pub use event::*;
 | 
					pub use event::*;
 | 
				
			||||||
pub use monitor::*;
 | 
					pub use monitor::*;
 | 
				
			||||||
pub use system::*;
 | 
					pub use system::*;
 | 
				
			||||||
 | 
					pub use system_cursor::*;
 | 
				
			||||||
pub use window::*;
 | 
					pub use window::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[allow(missing_docs)]
 | 
					#[allow(missing_docs)]
 | 
				
			||||||
@ -35,7 +35,7 @@ pub mod prelude {
 | 
				
			|||||||
    #[allow(deprecated)]
 | 
					    #[allow(deprecated)]
 | 
				
			||||||
    #[doc(hidden)]
 | 
					    #[doc(hidden)]
 | 
				
			||||||
    pub use crate::{
 | 
					    pub use crate::{
 | 
				
			||||||
        CursorEntered, CursorIcon, CursorLeft, CursorMoved, FileDragAndDrop, Ime, MonitorSelection,
 | 
					        CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, MonitorSelection,
 | 
				
			||||||
        ReceivedCharacter, Window, WindowMoved, WindowPlugin, WindowPosition,
 | 
					        ReceivedCharacter, Window, WindowMoved, WindowPlugin, WindowPosition,
 | 
				
			||||||
        WindowResizeConstraints,
 | 
					        WindowResizeConstraints,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
				
			|||||||
@ -73,7 +73,7 @@ use bevy_reflect::{prelude::ReflectDefault, Reflect};
 | 
				
			|||||||
#[cfg(feature = "serialize")]
 | 
					#[cfg(feature = "serialize")]
 | 
				
			||||||
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
 | 
					use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// The icon to display for a [`Window`](crate::window::Window)'s [`Cursor`](crate::window::Cursor).
 | 
					/// The icon to display for a window.
 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
/// Examples of all of these cursors can be found [here](https://www.w3schools.com/cssref/playit.php?filename=playcss_cursor&preval=crosshair).
 | 
					/// Examples of all of these cursors can be found [here](https://www.w3schools.com/cssref/playit.php?filename=playcss_cursor&preval=crosshair).
 | 
				
			||||||
/// This `enum` is simply a copy of a similar `enum` found in [`winit`](https://docs.rs/winit/latest/winit/window/enum.CursorIcon.html).
 | 
					/// This `enum` is simply a copy of a similar `enum` found in [`winit`](https://docs.rs/winit/latest/winit/window/enum.CursorIcon.html).
 | 
				
			||||||
@ -89,7 +89,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
 | 
				
			|||||||
    reflect(Serialize, Deserialize)
 | 
					    reflect(Serialize, Deserialize)
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
#[reflect(Debug, PartialEq, Default)]
 | 
					#[reflect(Debug, PartialEq, Default)]
 | 
				
			||||||
pub enum CursorIcon {
 | 
					pub enum SystemCursorIcon {
 | 
				
			||||||
    /// The platform-dependent default cursor. Often rendered as arrow.
 | 
					    /// The platform-dependent default cursor. Often rendered as arrow.
 | 
				
			||||||
    #[default]
 | 
					    #[default]
 | 
				
			||||||
    Default,
 | 
					    Default,
 | 
				
			||||||
@ -107,7 +107,7 @@ pub enum CursorIcon {
 | 
				
			|||||||
    Pointer,
 | 
					    Pointer,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// A progress indicator. The program is performing some processing, but is
 | 
					    /// A progress indicator. The program is performing some processing, but is
 | 
				
			||||||
    /// different from [`CursorIcon::Wait`] in that the user may still interact
 | 
					    /// different from [`SystemCursorIcon::Wait`] in that the user may still interact
 | 
				
			||||||
    /// with the program.
 | 
					    /// with the program.
 | 
				
			||||||
    Progress,
 | 
					    Progress,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -12,8 +12,6 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use bevy_utils::tracing::warn;
 | 
					use bevy_utils::tracing::warn;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::CursorIcon;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Marker [`Component`] for the window considered the primary window.
 | 
					/// Marker [`Component`] for the window considered the primary window.
 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
/// Currently this is assumed to only exist on 1 entity at a time.
 | 
					/// Currently this is assumed to only exist on 1 entity at a time.
 | 
				
			||||||
@ -107,16 +105,16 @@ impl NormalizedWindowRef {
 | 
				
			|||||||
///
 | 
					///
 | 
				
			||||||
/// Because this component is synchronized with `winit`, it can be used to perform
 | 
					/// Because this component is synchronized with `winit`, it can be used to perform
 | 
				
			||||||
/// OS-integrated windowing operations. For example, here's a simple system
 | 
					/// OS-integrated windowing operations. For example, here's a simple system
 | 
				
			||||||
/// to change the cursor type:
 | 
					/// to change the window mode:
 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
/// # use bevy_ecs::query::With;
 | 
					/// # use bevy_ecs::query::With;
 | 
				
			||||||
/// # use bevy_ecs::system::Query;
 | 
					/// # use bevy_ecs::system::Query;
 | 
				
			||||||
/// # use bevy_window::{CursorIcon, PrimaryWindow, Window};
 | 
					/// # use bevy_window::{WindowMode, PrimaryWindow, Window, MonitorSelection};
 | 
				
			||||||
/// fn change_cursor(mut windows: Query<&mut Window, With<PrimaryWindow>>) {
 | 
					/// fn change_window_mode(mut windows: Query<&mut Window, With<PrimaryWindow>>) {
 | 
				
			||||||
///     // Query returns one window typically.
 | 
					///     // Query returns one window typically.
 | 
				
			||||||
///     for mut window in windows.iter_mut() {
 | 
					///     for mut window in windows.iter_mut() {
 | 
				
			||||||
///         window.cursor.icon = CursorIcon::Wait;
 | 
					///         window.mode = WindowMode::Fullscreen(MonitorSelection::Current);
 | 
				
			||||||
///     }
 | 
					///     }
 | 
				
			||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
@ -128,8 +126,9 @@ impl NormalizedWindowRef {
 | 
				
			|||||||
)]
 | 
					)]
 | 
				
			||||||
#[reflect(Component, Default)]
 | 
					#[reflect(Component, Default)]
 | 
				
			||||||
pub struct Window {
 | 
					pub struct Window {
 | 
				
			||||||
    /// The cursor of this window.
 | 
					    /// The cursor options of this window. Cursor icons are set with the `Cursor` component on the
 | 
				
			||||||
    pub cursor: Cursor,
 | 
					    /// window entity.
 | 
				
			||||||
 | 
					    pub cursor_options: CursorOptions,
 | 
				
			||||||
    /// What presentation mode to give the window.
 | 
					    /// What presentation mode to give the window.
 | 
				
			||||||
    pub present_mode: PresentMode,
 | 
					    pub present_mode: PresentMode,
 | 
				
			||||||
    /// Which fullscreen or windowing mode should be used.
 | 
					    /// Which fullscreen or windowing mode should be used.
 | 
				
			||||||
@ -316,7 +315,7 @@ impl Default for Window {
 | 
				
			|||||||
        Self {
 | 
					        Self {
 | 
				
			||||||
            title: "App".to_owned(),
 | 
					            title: "App".to_owned(),
 | 
				
			||||||
            name: None,
 | 
					            name: None,
 | 
				
			||||||
            cursor: Default::default(),
 | 
					            cursor_options: Default::default(),
 | 
				
			||||||
            present_mode: Default::default(),
 | 
					            present_mode: Default::default(),
 | 
				
			||||||
            mode: Default::default(),
 | 
					            mode: Default::default(),
 | 
				
			||||||
            position: Default::default(),
 | 
					            position: Default::default(),
 | 
				
			||||||
@ -543,23 +542,20 @@ impl WindowResizeConstraints {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Cursor data for a [`Window`].
 | 
					/// Cursor data for a [`Window`].
 | 
				
			||||||
#[derive(Debug, Copy, Clone, Reflect)]
 | 
					#[derive(Debug, Clone, Reflect)]
 | 
				
			||||||
#[cfg_attr(
 | 
					#[cfg_attr(
 | 
				
			||||||
    feature = "serialize",
 | 
					    feature = "serialize",
 | 
				
			||||||
    derive(serde::Serialize, serde::Deserialize),
 | 
					    derive(serde::Serialize, serde::Deserialize),
 | 
				
			||||||
    reflect(Serialize, Deserialize)
 | 
					    reflect(Serialize, Deserialize)
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
#[reflect(Debug, Default)]
 | 
					#[reflect(Debug, Default)]
 | 
				
			||||||
pub struct Cursor {
 | 
					pub struct CursorOptions {
 | 
				
			||||||
    /// What the cursor should look like while inside the window.
 | 
					 | 
				
			||||||
    pub icon: CursorIcon,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Whether the cursor is visible or not.
 | 
					    /// Whether the cursor is visible or not.
 | 
				
			||||||
    ///
 | 
					    ///
 | 
				
			||||||
    /// ## Platform-specific
 | 
					    /// ## Platform-specific
 | 
				
			||||||
    ///
 | 
					    ///
 | 
				
			||||||
    /// - **`Windows`**, **`X11`**, and **`Wayland`**: The cursor is hidden only when inside the window.
 | 
					    /// - **`Windows`**, **`X11`**, and **`Wayland`**: The cursor is hidden only when inside the window.
 | 
				
			||||||
    ///     To stop the cursor from leaving the window, change [`Cursor::grab_mode`] to [`CursorGrabMode::Locked`] or [`CursorGrabMode::Confined`]
 | 
					    ///     To stop the cursor from leaving the window, change [`CursorOptions::grab_mode`] to [`CursorGrabMode::Locked`] or [`CursorGrabMode::Confined`]
 | 
				
			||||||
    /// - **`macOS`**: The cursor is hidden only when the window is focused.
 | 
					    /// - **`macOS`**: The cursor is hidden only when the window is focused.
 | 
				
			||||||
    /// - **`iOS`** and **`Android`** do not have cursors
 | 
					    /// - **`iOS`** and **`Android`** do not have cursors
 | 
				
			||||||
    pub visible: bool,
 | 
					    pub visible: bool,
 | 
				
			||||||
@ -583,10 +579,9 @@ pub struct Cursor {
 | 
				
			|||||||
    pub hit_test: bool,
 | 
					    pub hit_test: bool,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Default for Cursor {
 | 
					impl Default for CursorOptions {
 | 
				
			||||||
    fn default() -> Self {
 | 
					    fn default() -> Self {
 | 
				
			||||||
        Cursor {
 | 
					        CursorOptions {
 | 
				
			||||||
            icon: CursorIcon::Default,
 | 
					 | 
				
			||||||
            visible: true,
 | 
					            visible: true,
 | 
				
			||||||
            grab_mode: CursorGrabMode::None,
 | 
					            grab_mode: CursorGrabMode::None,
 | 
				
			||||||
            hit_test: true,
 | 
					            hit_test: true,
 | 
				
			||||||
@ -870,7 +865,7 @@ impl From<DVec2> for WindowResolution {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Defines if and how the [`Cursor`] is grabbed by a [`Window`].
 | 
					/// Defines if and how the cursor is grabbed by a [`Window`].
 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
/// ## Platform-specific
 | 
					/// ## Platform-specific
 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ use bevy_input::{
 | 
				
			|||||||
    ButtonState,
 | 
					    ButtonState,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use bevy_math::Vec2;
 | 
					use bevy_math::Vec2;
 | 
				
			||||||
use bevy_window::{CursorIcon, EnabledButtons, WindowLevel, WindowTheme};
 | 
					use bevy_window::{EnabledButtons, SystemCursorIcon, WindowLevel, WindowTheme};
 | 
				
			||||||
use winit::keyboard::{Key, NamedKey, NativeKey};
 | 
					use winit::keyboard::{Key, NamedKey, NativeKey};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn convert_keyboard_input(
 | 
					pub fn convert_keyboard_input(
 | 
				
			||||||
@ -628,41 +628,42 @@ pub fn convert_native_key(native_key: &NativeKey) -> bevy_input::keyboard::Nativ
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn convert_cursor_icon(cursor_icon: CursorIcon) -> winit::window::CursorIcon {
 | 
					/// Converts a [`SystemCursorIcon`] to a [`winit::window::CursorIcon`].
 | 
				
			||||||
 | 
					pub fn convert_system_cursor_icon(cursor_icon: SystemCursorIcon) -> winit::window::CursorIcon {
 | 
				
			||||||
    match cursor_icon {
 | 
					    match cursor_icon {
 | 
				
			||||||
        CursorIcon::Crosshair => winit::window::CursorIcon::Crosshair,
 | 
					        SystemCursorIcon::Crosshair => winit::window::CursorIcon::Crosshair,
 | 
				
			||||||
        CursorIcon::Pointer => winit::window::CursorIcon::Pointer,
 | 
					        SystemCursorIcon::Pointer => winit::window::CursorIcon::Pointer,
 | 
				
			||||||
        CursorIcon::Move => winit::window::CursorIcon::Move,
 | 
					        SystemCursorIcon::Move => winit::window::CursorIcon::Move,
 | 
				
			||||||
        CursorIcon::Text => winit::window::CursorIcon::Text,
 | 
					        SystemCursorIcon::Text => winit::window::CursorIcon::Text,
 | 
				
			||||||
        CursorIcon::Wait => winit::window::CursorIcon::Wait,
 | 
					        SystemCursorIcon::Wait => winit::window::CursorIcon::Wait,
 | 
				
			||||||
        CursorIcon::Help => winit::window::CursorIcon::Help,
 | 
					        SystemCursorIcon::Help => winit::window::CursorIcon::Help,
 | 
				
			||||||
        CursorIcon::Progress => winit::window::CursorIcon::Progress,
 | 
					        SystemCursorIcon::Progress => winit::window::CursorIcon::Progress,
 | 
				
			||||||
        CursorIcon::NotAllowed => winit::window::CursorIcon::NotAllowed,
 | 
					        SystemCursorIcon::NotAllowed => winit::window::CursorIcon::NotAllowed,
 | 
				
			||||||
        CursorIcon::ContextMenu => winit::window::CursorIcon::ContextMenu,
 | 
					        SystemCursorIcon::ContextMenu => winit::window::CursorIcon::ContextMenu,
 | 
				
			||||||
        CursorIcon::Cell => winit::window::CursorIcon::Cell,
 | 
					        SystemCursorIcon::Cell => winit::window::CursorIcon::Cell,
 | 
				
			||||||
        CursorIcon::VerticalText => winit::window::CursorIcon::VerticalText,
 | 
					        SystemCursorIcon::VerticalText => winit::window::CursorIcon::VerticalText,
 | 
				
			||||||
        CursorIcon::Alias => winit::window::CursorIcon::Alias,
 | 
					        SystemCursorIcon::Alias => winit::window::CursorIcon::Alias,
 | 
				
			||||||
        CursorIcon::Copy => winit::window::CursorIcon::Copy,
 | 
					        SystemCursorIcon::Copy => winit::window::CursorIcon::Copy,
 | 
				
			||||||
        CursorIcon::NoDrop => winit::window::CursorIcon::NoDrop,
 | 
					        SystemCursorIcon::NoDrop => winit::window::CursorIcon::NoDrop,
 | 
				
			||||||
        CursorIcon::Grab => winit::window::CursorIcon::Grab,
 | 
					        SystemCursorIcon::Grab => winit::window::CursorIcon::Grab,
 | 
				
			||||||
        CursorIcon::Grabbing => winit::window::CursorIcon::Grabbing,
 | 
					        SystemCursorIcon::Grabbing => winit::window::CursorIcon::Grabbing,
 | 
				
			||||||
        CursorIcon::AllScroll => winit::window::CursorIcon::AllScroll,
 | 
					        SystemCursorIcon::AllScroll => winit::window::CursorIcon::AllScroll,
 | 
				
			||||||
        CursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn,
 | 
					        SystemCursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn,
 | 
				
			||||||
        CursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut,
 | 
					        SystemCursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut,
 | 
				
			||||||
        CursorIcon::EResize => winit::window::CursorIcon::EResize,
 | 
					        SystemCursorIcon::EResize => winit::window::CursorIcon::EResize,
 | 
				
			||||||
        CursorIcon::NResize => winit::window::CursorIcon::NResize,
 | 
					        SystemCursorIcon::NResize => winit::window::CursorIcon::NResize,
 | 
				
			||||||
        CursorIcon::NeResize => winit::window::CursorIcon::NeResize,
 | 
					        SystemCursorIcon::NeResize => winit::window::CursorIcon::NeResize,
 | 
				
			||||||
        CursorIcon::NwResize => winit::window::CursorIcon::NwResize,
 | 
					        SystemCursorIcon::NwResize => winit::window::CursorIcon::NwResize,
 | 
				
			||||||
        CursorIcon::SResize => winit::window::CursorIcon::SResize,
 | 
					        SystemCursorIcon::SResize => winit::window::CursorIcon::SResize,
 | 
				
			||||||
        CursorIcon::SeResize => winit::window::CursorIcon::SeResize,
 | 
					        SystemCursorIcon::SeResize => winit::window::CursorIcon::SeResize,
 | 
				
			||||||
        CursorIcon::SwResize => winit::window::CursorIcon::SwResize,
 | 
					        SystemCursorIcon::SwResize => winit::window::CursorIcon::SwResize,
 | 
				
			||||||
        CursorIcon::WResize => winit::window::CursorIcon::WResize,
 | 
					        SystemCursorIcon::WResize => winit::window::CursorIcon::WResize,
 | 
				
			||||||
        CursorIcon::EwResize => winit::window::CursorIcon::EwResize,
 | 
					        SystemCursorIcon::EwResize => winit::window::CursorIcon::EwResize,
 | 
				
			||||||
        CursorIcon::NsResize => winit::window::CursorIcon::NsResize,
 | 
					        SystemCursorIcon::NsResize => winit::window::CursorIcon::NsResize,
 | 
				
			||||||
        CursorIcon::NeswResize => winit::window::CursorIcon::NeswResize,
 | 
					        SystemCursorIcon::NeswResize => winit::window::CursorIcon::NeswResize,
 | 
				
			||||||
        CursorIcon::NwseResize => winit::window::CursorIcon::NwseResize,
 | 
					        SystemCursorIcon::NwseResize => winit::window::CursorIcon::NwseResize,
 | 
				
			||||||
        CursorIcon::ColResize => winit::window::CursorIcon::ColResize,
 | 
					        SystemCursorIcon::ColResize => winit::window::CursorIcon::ColResize,
 | 
				
			||||||
        CursorIcon::RowResize => winit::window::CursorIcon::RowResize,
 | 
					        SystemCursorIcon::RowResize => winit::window::CursorIcon::RowResize,
 | 
				
			||||||
        _ => winit::window::CursorIcon::Default,
 | 
					        _ => winit::window::CursorIcon::Default,
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -24,9 +24,14 @@ use bevy_app::{App, Last, Plugin};
 | 
				
			|||||||
use bevy_ecs::prelude::*;
 | 
					use bevy_ecs::prelude::*;
 | 
				
			||||||
#[allow(deprecated)]
 | 
					#[allow(deprecated)]
 | 
				
			||||||
use bevy_window::{exit_on_all_closed, Window, WindowCreated};
 | 
					use bevy_window::{exit_on_all_closed, Window, WindowCreated};
 | 
				
			||||||
 | 
					pub use converters::convert_system_cursor_icon;
 | 
				
			||||||
 | 
					pub use state::{CursorSource, CustomCursorCache, CustomCursorCacheKey, PendingCursor};
 | 
				
			||||||
use system::{changed_windows, despawn_windows};
 | 
					use system::{changed_windows, despawn_windows};
 | 
				
			||||||
pub use system::{create_monitors, create_windows};
 | 
					pub use system::{create_monitors, create_windows};
 | 
				
			||||||
pub use winit::event_loop::EventLoopProxy;
 | 
					pub use winit::event_loop::EventLoopProxy;
 | 
				
			||||||
 | 
					#[cfg(all(target_family = "wasm", target_os = "unknown"))]
 | 
				
			||||||
 | 
					pub use winit::platform::web::CustomCursorExtWebSys;
 | 
				
			||||||
 | 
					pub use winit::window::{CustomCursor as WinitCustomCursor, CustomCursorSource};
 | 
				
			||||||
pub use winit_config::*;
 | 
					pub use winit_config::*;
 | 
				
			||||||
pub use winit_event::*;
 | 
					pub use winit_event::*;
 | 
				
			||||||
pub use winit_windows::*;
 | 
					pub use winit_windows::*;
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ use bevy_log::{error, trace, warn};
 | 
				
			|||||||
use bevy_math::{ivec2, DVec2, Vec2};
 | 
					use bevy_math::{ivec2, DVec2, Vec2};
 | 
				
			||||||
#[cfg(not(target_arch = "wasm32"))]
 | 
					#[cfg(not(target_arch = "wasm32"))]
 | 
				
			||||||
use bevy_tasks::tick_global_task_pools_on_main_thread;
 | 
					use bevy_tasks::tick_global_task_pools_on_main_thread;
 | 
				
			||||||
use bevy_utils::Instant;
 | 
					use bevy_utils::{HashMap, Instant};
 | 
				
			||||||
use std::marker::PhantomData;
 | 
					use std::marker::PhantomData;
 | 
				
			||||||
use winit::application::ApplicationHandler;
 | 
					use winit::application::ApplicationHandler;
 | 
				
			||||||
use winit::dpi::PhysicalSize;
 | 
					use winit::dpi::PhysicalSize;
 | 
				
			||||||
@ -85,7 +85,7 @@ struct WinitAppRunnerState<T: Event> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
impl<T: Event> WinitAppRunnerState<T> {
 | 
					impl<T: Event> WinitAppRunnerState<T> {
 | 
				
			||||||
    fn new(mut app: App) -> Self {
 | 
					    fn new(mut app: App) -> Self {
 | 
				
			||||||
        app.add_event::<T>();
 | 
					        app.add_event::<T>().init_resource::<CustomCursorCache>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let event_writer_system_state: SystemState<(
 | 
					        let event_writer_system_state: SystemState<(
 | 
				
			||||||
            EventWriter<WindowResized>,
 | 
					            EventWriter<WindowResized>,
 | 
				
			||||||
@ -131,6 +131,39 @@ impl<T: Event> WinitAppRunnerState<T> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Identifiers for custom cursors used in caching.
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 | 
				
			||||||
 | 
					pub enum CustomCursorCacheKey {
 | 
				
			||||||
 | 
					    /// u64 is used instead of `AssetId`, because `bevy_asset` can't be imported here.
 | 
				
			||||||
 | 
					    AssetIndex(u64),
 | 
				
			||||||
 | 
					    /// u128 is used instead of `AssetId`, because `bevy_asset` can't be imported here.
 | 
				
			||||||
 | 
					    AssetUuid(u128),
 | 
				
			||||||
 | 
					    /// A URL to a cursor.
 | 
				
			||||||
 | 
					    Url(String),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// 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<CustomCursorCacheKey, winit::window::CustomCursor>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A source for a cursor. Is created in `bevy_render` and consumed by the winit event loop.
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub enum CursorSource {
 | 
				
			||||||
 | 
					    /// A custom cursor was identified to be cached, no reason to recreate it.
 | 
				
			||||||
 | 
					    CustomCached(CustomCursorCacheKey),
 | 
				
			||||||
 | 
					    /// 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<CursorSource>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
 | 
					impl<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
 | 
				
			||||||
    fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
 | 
					    fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
 | 
				
			||||||
        if event_loop.exiting() {
 | 
					        if event_loop.exiting() {
 | 
				
			||||||
@ -520,6 +553,7 @@ impl<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
 | 
				
			|||||||
            // This is a temporary solution, full solution is mentioned here: https://github.com/bevyengine/bevy/issues/1343#issuecomment-770091684
 | 
					            // This is a temporary solution, full solution is mentioned here: https://github.com/bevyengine/bevy/issues/1343#issuecomment-770091684
 | 
				
			||||||
            if !self.ran_update_since_last_redraw || all_invisible {
 | 
					            if !self.ran_update_since_last_redraw || all_invisible {
 | 
				
			||||||
                self.run_app_update();
 | 
					                self.run_app_update();
 | 
				
			||||||
 | 
					                self.update_cursors(event_loop);
 | 
				
			||||||
                self.ran_update_since_last_redraw = true;
 | 
					                self.ran_update_since_last_redraw = true;
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                self.redraw_requested = true;
 | 
					                self.redraw_requested = true;
 | 
				
			||||||
@ -528,7 +562,6 @@ impl<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
 | 
				
			|||||||
            // Running the app may have changed the WinitSettings resource, so we have to re-extract it.
 | 
					            // Running the app may have changed the WinitSettings resource, so we have to re-extract it.
 | 
				
			||||||
            let (config, windows) = focused_windows_state.get(self.world());
 | 
					            let (config, windows) = focused_windows_state.get(self.world());
 | 
				
			||||||
            let focused = windows.iter().any(|(_, window)| window.focused);
 | 
					            let focused = windows.iter().any(|(_, window)| window.focused);
 | 
				
			||||||
 | 
					 | 
				
			||||||
            update_mode = config.update_mode(focused);
 | 
					            update_mode = config.update_mode(focused);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -750,6 +783,42 @@ impl<T: Event> WinitAppRunnerState<T> {
 | 
				
			|||||||
            .resource_mut::<Events<WinitEvent>>()
 | 
					            .resource_mut::<Events<WinitEvent>>()
 | 
				
			||||||
            .send_batch(buffered_events);
 | 
					            .send_batch(buffered_events);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn update_cursors(&mut self, event_loop: &ActiveEventLoop) {
 | 
				
			||||||
 | 
					        let mut windows_state: SystemState<(
 | 
				
			||||||
 | 
					            NonSendMut<WinitWindows>,
 | 
				
			||||||
 | 
					            ResMut<CustomCursorCache>,
 | 
				
			||||||
 | 
					            Query<(Entity, &mut PendingCursor), Changed<PendingCursor>>,
 | 
				
			||||||
 | 
					        )> = SystemState::new(self.world_mut());
 | 
				
			||||||
 | 
					        let (winit_windows, mut cursor_cache, mut windows) =
 | 
				
			||||||
 | 
					            windows_state.get_mut(self.world_mut());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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 {
 | 
				
			||||||
 | 
					                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()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                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.
 | 
					/// The default [`App::runner`] for the [`WinitPlugin`](crate::WinitPlugin) plugin.
 | 
				
			||||||
 | 
				
			|||||||
@ -29,8 +29,7 @@ use crate::state::react_to_resize;
 | 
				
			|||||||
use crate::winit_monitors::WinitMonitors;
 | 
					use crate::winit_monitors::WinitMonitors;
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    converters::{
 | 
					    converters::{
 | 
				
			||||||
        self, convert_enabled_buttons, convert_window_level, convert_window_theme,
 | 
					        convert_enabled_buttons, convert_window_level, convert_window_theme, convert_winit_theme,
 | 
				
			||||||
        convert_winit_theme,
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    get_best_videomode, get_fitting_videomode, select_monitor, CreateMonitorParams,
 | 
					    get_best_videomode, get_fitting_videomode, select_monitor, CreateMonitorParams,
 | 
				
			||||||
    CreateWindowParams, WinitWindows,
 | 
					    CreateWindowParams, WinitWindows,
 | 
				
			||||||
@ -365,21 +364,17 @@ pub(crate) fn changed_windows(
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if window.cursor.icon != cache.window.cursor.icon {
 | 
					        if window.cursor_options.grab_mode != cache.window.cursor_options.grab_mode {
 | 
				
			||||||
            winit_window.set_cursor(converters::convert_cursor_icon(window.cursor.icon));
 | 
					            crate::winit_windows::attempt_grab(winit_window, window.cursor_options.grab_mode);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if window.cursor.grab_mode != cache.window.cursor.grab_mode {
 | 
					        if window.cursor_options.visible != cache.window.cursor_options.visible {
 | 
				
			||||||
            crate::winit_windows::attempt_grab(winit_window, window.cursor.grab_mode);
 | 
					            winit_window.set_cursor_visible(window.cursor_options.visible);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if window.cursor.visible != cache.window.cursor.visible {
 | 
					        if window.cursor_options.hit_test != cache.window.cursor_options.hit_test {
 | 
				
			||||||
            winit_window.set_cursor_visible(window.cursor.visible);
 | 
					            if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) {
 | 
				
			||||||
        }
 | 
					                window.cursor_options.hit_test = cache.window.cursor_options.hit_test;
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if window.cursor.hit_test != cache.window.cursor.hit_test {
 | 
					 | 
				
			||||||
            if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) {
 | 
					 | 
				
			||||||
                window.cursor.hit_test = cache.window.cursor.hit_test;
 | 
					 | 
				
			||||||
                warn!(
 | 
					                warn!(
 | 
				
			||||||
                    "Could not set cursor hit test for window {:?}: {:?}",
 | 
					                    "Could not set cursor hit test for window {:?}: {:?}",
 | 
				
			||||||
                    window.title, err
 | 
					                    window.title, err
 | 
				
			||||||
 | 
				
			|||||||
@ -247,16 +247,16 @@ impl WinitWindows {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Do not set the grab mode on window creation if it's none. It can fail on mobile.
 | 
					        // Do not set the grab mode on window creation if it's none. It can fail on mobile.
 | 
				
			||||||
        if window.cursor.grab_mode != CursorGrabMode::None {
 | 
					        if window.cursor_options.grab_mode != CursorGrabMode::None {
 | 
				
			||||||
            attempt_grab(&winit_window, window.cursor.grab_mode);
 | 
					            attempt_grab(&winit_window, window.cursor_options.grab_mode);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        winit_window.set_cursor_visible(window.cursor.visible);
 | 
					        winit_window.set_cursor_visible(window.cursor_options.visible);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Do not set the cursor hittest on window creation if it's false, as it will always fail on
 | 
					        // Do not set the cursor hittest on window creation if it's false, as it will always fail on
 | 
				
			||||||
        // some platforms and log an unfixable warning.
 | 
					        // some platforms and log an unfixable warning.
 | 
				
			||||||
        if !window.cursor.hit_test {
 | 
					        if !window.cursor_options.hit_test {
 | 
				
			||||||
            if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) {
 | 
					            if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) {
 | 
				
			||||||
                warn!(
 | 
					                warn!(
 | 
				
			||||||
                    "Could not set cursor hit test for window {:?}: {:?}",
 | 
					                    "Could not set cursor hit test for window {:?}: {:?}",
 | 
				
			||||||
                    window.title, err
 | 
					                    window.title, err
 | 
				
			||||||
 | 
				
			|||||||
@ -237,7 +237,7 @@ fn update_cursor_hit_test(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // If the window has decorations (e.g. a border) then it should be clickable
 | 
					    // If the window has decorations (e.g. a border) then it should be clickable
 | 
				
			||||||
    if primary_window.decorations {
 | 
					    if primary_window.decorations {
 | 
				
			||||||
        primary_window.cursor.hit_test = true;
 | 
					        primary_window.cursor_options.hit_test = true;
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -248,7 +248,7 @@ fn update_cursor_hit_test(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // If the cursor is within the radius of the Bevy logo make the window clickable otherwise the window is not clickable
 | 
					    // If the cursor is within the radius of the Bevy logo make the window clickable otherwise the window is not clickable
 | 
				
			||||||
    let bevy_logo_transform = q_bevy_logo.single();
 | 
					    let bevy_logo_transform = q_bevy_logo.single();
 | 
				
			||||||
    primary_window.cursor.hit_test = bevy_logo_transform
 | 
					    primary_window.cursor_options.hit_test = bevy_logo_transform
 | 
				
			||||||
        .translation
 | 
					        .translation
 | 
				
			||||||
        .truncate()
 | 
					        .truncate()
 | 
				
			||||||
        .distance(cursor_world_pos)
 | 
					        .distance(cursor_world_pos)
 | 
				
			||||||
 | 
				
			|||||||
@ -198,13 +198,13 @@ fn run_camera_controller(
 | 
				
			|||||||
                        continue;
 | 
					                        continue;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    window.cursor.grab_mode = CursorGrabMode::Locked;
 | 
					                    window.cursor_options.grab_mode = CursorGrabMode::Locked;
 | 
				
			||||||
                    window.cursor.visible = false;
 | 
					                    window.cursor_options.visible = false;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                for mut window in &mut windows {
 | 
					                for mut window in &mut windows {
 | 
				
			||||||
                    window.cursor.grab_mode = CursorGrabMode::None;
 | 
					                    window.cursor_options.grab_mode = CursorGrabMode::None;
 | 
				
			||||||
                    window.cursor.visible = true;
 | 
					                    window.cursor_options.visible = true;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -19,12 +19,12 @@ fn grab_mouse(
 | 
				
			|||||||
    let mut window = windows.single_mut();
 | 
					    let mut window = windows.single_mut();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if mouse.just_pressed(MouseButton::Left) {
 | 
					    if mouse.just_pressed(MouseButton::Left) {
 | 
				
			||||||
        window.cursor.visible = false;
 | 
					        window.cursor_options.visible = false;
 | 
				
			||||||
        window.cursor.grab_mode = CursorGrabMode::Locked;
 | 
					        window.cursor_options.grab_mode = CursorGrabMode::Locked;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if key.just_pressed(KeyCode::Escape) {
 | 
					    if key.just_pressed(KeyCode::Escape) {
 | 
				
			||||||
        window.cursor.visible = true;
 | 
					        window.cursor_options.visible = true;
 | 
				
			||||||
        window.cursor.grab_mode = CursorGrabMode::None;
 | 
					        window.cursor_options.grab_mode = CursorGrabMode::None;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -53,6 +53,6 @@ fn toggle_mouse_passthrough(
 | 
				
			|||||||
) {
 | 
					) {
 | 
				
			||||||
    if keyboard_input.just_pressed(KeyCode::KeyP) {
 | 
					    if keyboard_input.just_pressed(KeyCode::KeyP) {
 | 
				
			||||||
        let mut window = windows.single_mut();
 | 
					        let mut window = windows.single_mut();
 | 
				
			||||||
        window.cursor.hit_test = !window.cursor.hit_test;
 | 
					        window.cursor_options.hit_test = !window.cursor_options.hit_test;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,8 @@ use bevy::{
 | 
				
			|||||||
    core::FrameCount,
 | 
					    core::FrameCount,
 | 
				
			||||||
    diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
 | 
					    diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
 | 
				
			||||||
    prelude::*,
 | 
					    prelude::*,
 | 
				
			||||||
    window::{CursorGrabMode, PresentMode, WindowLevel, WindowTheme},
 | 
					    render::view::cursor::{CursorIcon, CustomCursor},
 | 
				
			||||||
 | 
					    window::{CursorGrabMode, PresentMode, SystemCursorIcon, WindowLevel, WindowTheme},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn main() {
 | 
					fn main() {
 | 
				
			||||||
@ -37,6 +38,7 @@ fn main() {
 | 
				
			|||||||
            LogDiagnosticsPlugin::default(),
 | 
					            LogDiagnosticsPlugin::default(),
 | 
				
			||||||
            FrameTimeDiagnosticsPlugin,
 | 
					            FrameTimeDiagnosticsPlugin,
 | 
				
			||||||
        ))
 | 
					        ))
 | 
				
			||||||
 | 
					        .add_systems(Startup, init_cursor_icons)
 | 
				
			||||||
        .add_systems(
 | 
					        .add_systems(
 | 
				
			||||||
            Update,
 | 
					            Update,
 | 
				
			||||||
            (
 | 
					            (
 | 
				
			||||||
@ -137,8 +139,8 @@ fn toggle_cursor(mut windows: Query<&mut Window>, input: Res<ButtonInput<KeyCode
 | 
				
			|||||||
    if input.just_pressed(KeyCode::Space) {
 | 
					    if input.just_pressed(KeyCode::Space) {
 | 
				
			||||||
        let mut window = windows.single_mut();
 | 
					        let mut window = windows.single_mut();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        window.cursor.visible = !window.cursor.visible;
 | 
					        window.cursor_options.visible = !window.cursor_options.visible;
 | 
				
			||||||
        window.cursor.grab_mode = match window.cursor.grab_mode {
 | 
					        window.cursor_options.grab_mode = match window.cursor_options.grab_mode {
 | 
				
			||||||
            CursorGrabMode::None => CursorGrabMode::Locked,
 | 
					            CursorGrabMode::None => CursorGrabMode::Locked,
 | 
				
			||||||
            CursorGrabMode::Locked | CursorGrabMode::Confined => CursorGrabMode::None,
 | 
					            CursorGrabMode::Locked | CursorGrabMode::Confined => CursorGrabMode::None,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
@ -159,31 +161,46 @@ fn toggle_theme(mut windows: Query<&mut Window>, input: Res<ButtonInput<KeyCode>
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Resource)]
 | 
				
			||||||
 | 
					struct CursorIcons(Vec<CursorIcon>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn init_cursor_icons(mut commands: Commands, asset_server: Res<AssetServer>) {
 | 
				
			||||||
 | 
					    commands.insert_resource(CursorIcons(vec![
 | 
				
			||||||
 | 
					        SystemCursorIcon::Default.into(),
 | 
				
			||||||
 | 
					        SystemCursorIcon::Pointer.into(),
 | 
				
			||||||
 | 
					        SystemCursorIcon::Wait.into(),
 | 
				
			||||||
 | 
					        SystemCursorIcon::Text.into(),
 | 
				
			||||||
 | 
					        CustomCursor::Image {
 | 
				
			||||||
 | 
					            handle: asset_server.load("branding/icon.png"),
 | 
				
			||||||
 | 
					            hotspot: (128, 128),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .into(),
 | 
				
			||||||
 | 
					    ]));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// This system cycles the cursor's icon through a small set of icons when clicking
 | 
					/// This system cycles the cursor's icon through a small set of icons when clicking
 | 
				
			||||||
fn cycle_cursor_icon(
 | 
					fn cycle_cursor_icon(
 | 
				
			||||||
    mut windows: Query<&mut Window>,
 | 
					    mut commands: Commands,
 | 
				
			||||||
 | 
					    windows: Query<Entity, With<Window>>,
 | 
				
			||||||
    input: Res<ButtonInput<MouseButton>>,
 | 
					    input: Res<ButtonInput<MouseButton>>,
 | 
				
			||||||
    mut index: Local<usize>,
 | 
					    mut index: Local<usize>,
 | 
				
			||||||
 | 
					    cursor_icons: Res<CursorIcons>,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
    let mut window = windows.single_mut();
 | 
					    let window_entity = windows.single();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const ICONS: &[CursorIcon] = &[
 | 
					 | 
				
			||||||
        CursorIcon::Default,
 | 
					 | 
				
			||||||
        CursorIcon::Pointer,
 | 
					 | 
				
			||||||
        CursorIcon::Wait,
 | 
					 | 
				
			||||||
        CursorIcon::Text,
 | 
					 | 
				
			||||||
        CursorIcon::Copy,
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if input.just_pressed(MouseButton::Left) {
 | 
					    if input.just_pressed(MouseButton::Left) {
 | 
				
			||||||
        *index = (*index + 1) % ICONS.len();
 | 
					        *index = (*index + 1) % cursor_icons.0.len();
 | 
				
			||||||
 | 
					        commands
 | 
				
			||||||
 | 
					            .entity(window_entity)
 | 
				
			||||||
 | 
					            .insert(cursor_icons.0[*index].clone());
 | 
				
			||||||
    } else if input.just_pressed(MouseButton::Right) {
 | 
					    } else if input.just_pressed(MouseButton::Right) {
 | 
				
			||||||
        *index = if *index == 0 {
 | 
					        *index = if *index == 0 {
 | 
				
			||||||
            ICONS.len() - 1
 | 
					            cursor_icons.0.len() - 1
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            *index - 1
 | 
					            *index - 1
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					        commands
 | 
				
			||||||
 | 
					            .entity(window_entity)
 | 
				
			||||||
 | 
					            .insert(cursor_icons.0[*index].clone());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    window.cursor.icon = ICONS[*index];
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user