-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# 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 <[email protected]> Co-authored-by: Jan Hohenheim <[email protected]>
- Loading branch information
1 parent
d4ec80d
commit 47c4e30
Showing
16 changed files
with
375 additions
and
111 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.