mirror of
https://github.com/bevyengine/bevy
synced 2025-01-21 17:44:09 +00:00
6edb78a8c3
# Objective - `bevy_render` should not depend on `bevy_winit` - Fixes #15565 ## Solution - `bevy_render` no longer depends on `bevy_winit` - The following is behind the `custom_cursor` feature - Move custom cursor code from `bevy_render` to `bevy_winit` behind the `custom_cursor` feature - `bevy_winit` now depends on `bevy_render` (for `Image` and `TextureFormat`) - `bevy_winit` now depends on `bevy_asset` (for `Assets`, `Handle` and `AssetId`) - `bevy_winit` now depends on `bytemuck` (already in tree) - Custom cursor code in `bevy_winit` reworked to use `AssetId` (other than that it is taken over 1:1) - Rework `bevy_winit` custom cursor interface visibility now that the logic is all contained in `bevy_winit` ## Testing - I ran the screenshot and window_settings examples - Tested on linux wayland so far --- ## Migration Guide `CursorIcon` and `CustomCursor` previously provided by `bevy::render::view::cursor` is now available from `bevy::winit`. A new feature `custom_cursor` enables this functionality (default feature).
195 lines
6.6 KiB
Rust
195 lines
6.6 KiB
Rust
//! Components to customize winit cursor
|
|
|
|
use crate::{
|
|
converters::convert_system_cursor_icon,
|
|
state::{CursorSource, CustomCursorCache, CustomCursorCacheKey, PendingCursor},
|
|
WinitCustomCursor,
|
|
};
|
|
use bevy_app::{App, Last, Plugin};
|
|
use bevy_asset::{Assets, Handle};
|
|
use bevy_ecs::{
|
|
change_detection::DetectChanges,
|
|
component::Component,
|
|
entity::Entity,
|
|
observer::Trigger,
|
|
query::With,
|
|
reflect::ReflectComponent,
|
|
system::{Commands, Local, Query, Res},
|
|
world::{OnRemove, Ref},
|
|
};
|
|
use bevy_image::Image;
|
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
|
use bevy_utils::{tracing::warn, HashSet};
|
|
use bevy_window::{SystemCursorIcon, Window};
|
|
use wgpu_types::TextureFormat;
|
|
|
|
pub(crate) struct CursorPlugin;
|
|
|
|
impl Plugin for CursorPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.register_type::<CursorIcon>()
|
|
.init_resource::<CustomCursorCache>()
|
|
.add_systems(Last, update_cursors);
|
|
|
|
app.observe(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)]
|
|
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),
|
|
},
|
|
}
|
|
|
|
fn update_cursors(
|
|
mut commands: Commands,
|
|
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() {
|
|
if !(queue.remove(&entity) || cursor.is_changed()) {
|
|
continue;
|
|
}
|
|
|
|
let cursor_source = match cursor.as_ref() {
|
|
CursorIcon::Custom(CustomCursor::Image { handle, hotspot }) => {
|
|
let cache_key = CustomCursorCacheKey::Asset(handle.id());
|
|
|
|
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 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 crate::CustomCursorExtWebSys;
|
|
let source = 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)));
|
|
}
|
|
}
|
|
|
|
/// Resets the cursor to the default icon when `CursorIcon` is removed.
|
|
fn on_remove_cursor_icon(trigger: Trigger<OnRemove, CursorIcon>, mut commands: Commands) {
|
|
// Use `try_insert` to avoid panic if the window is being destroyed.
|
|
commands
|
|
.entity(trigger.entity())
|
|
.try_insert(PendingCursor(Some(CursorSource::System(
|
|
convert_system_cursor_icon(SystemCursorIcon::Default),
|
|
))));
|
|
}
|
|
|
|
/// 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,
|
|
}
|
|
}
|