bevy/crates/bevy_winit/src/cursor.rs
Bude 6edb78a8c3
Inverse bevy_render bevy_winit dependency and move cursor to bevy_winit (#15649)
# 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).
2024-10-06 18:25:50 +00:00

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,
}
}