Make CustomCursor variants CustomCursorImage/CustomCursorUrl structs (#17518)

# Objective

- Make `CustomCursor::Image` easier to work with by splitting the enum
variants off into `CustomCursorImage` and `CustomCursorUrl` structs and
deriving `Default` on those structs.
- Refs #17276.

## Testing

- Ran two examples: `cargo run --example custom_cursor_image
--features=custom_cursor` and `cargo run --example window_settings
--features=custom_cursor`
- CI.

---

## Migration Guide

The `CustomCursor` enum's variants now hold instances of
`CustomCursorImage` or `CustomCursorUrl`. Update your uses of
`CustomCursor` accordingly.
This commit is contained in:
mgi388 2025-01-24 16:39:04 +11:00 committed by GitHub
parent e459dd94ec
commit 14ad25227b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 87 additions and 77 deletions

View File

@ -37,7 +37,7 @@ use bevy_window::{SystemCursorIcon, Window};
use tracing::warn; use tracing::warn;
#[cfg(feature = "custom_cursor")] #[cfg(feature = "custom_cursor")]
pub use crate::custom_cursor::CustomCursor; pub use crate::custom_cursor::{CustomCursor, CustomCursorImage};
pub(crate) struct CursorPlugin; pub(crate) struct CursorPlugin;
@ -91,14 +91,16 @@ fn update_cursors(
let cursor_source = match cursor.as_ref() { let cursor_source = match cursor.as_ref() {
#[cfg(feature = "custom_cursor")] #[cfg(feature = "custom_cursor")]
CursorIcon::Custom(CustomCursor::Image { CursorIcon::Custom(CustomCursor::Image(c)) => {
handle, let CustomCursorImage {
texture_atlas, handle,
flip_x, texture_atlas,
flip_y, flip_x,
rect, flip_y,
hotspot, rect,
}) => { hotspot,
} = c;
let cache_key = CustomCursorCacheKey::Image { let cache_key = CustomCursorCacheKey::Image {
id: handle.id(), id: handle.id(),
texture_atlas_layout_id: texture_atlas.as_ref().map(|a| a.layout.id()), texture_atlas_layout_id: texture_atlas.as_ref().map(|a| a.layout.id()),
@ -155,14 +157,15 @@ fn update_cursors(
target_family = "wasm", target_family = "wasm",
target_os = "unknown" target_os = "unknown"
))] ))]
CursorIcon::Custom(CustomCursor::Url { url, hotspot }) => { CursorIcon::Custom(CustomCursor::Url(c)) => {
let cache_key = CustomCursorCacheKey::Url(url.clone()); let cache_key = CustomCursorCacheKey::Url(c.url.clone());
if cursor_cache.0.contains_key(&cache_key) { if cursor_cache.0.contains_key(&cache_key) {
CursorSource::CustomCached(cache_key) CursorSource::CustomCached(cache_key)
} else { } else {
use crate::CustomCursorExtWebSys; use crate::CustomCursorExtWebSys;
let source = WinitCustomCursor::from_url(url.clone(), hotspot.0, hotspot.1); let source =
WinitCustomCursor::from_url(c.url.clone(), c.hotspot.0, c.hotspot.1);
CursorSource::Custom((cache_key, source)) CursorSource::Custom((cache_key, source))
} }
} }

View File

@ -2,46 +2,57 @@ use bevy_app::{App, Plugin};
use bevy_asset::{Assets, Handle}; use bevy_asset::{Assets, Handle};
use bevy_image::{Image, TextureAtlas, TextureAtlasLayout, TextureAtlasPlugin}; use bevy_image::{Image, TextureAtlas, TextureAtlasLayout, TextureAtlasPlugin};
use bevy_math::{ops, Rect, URect, UVec2, Vec2}; use bevy_math::{ops, Rect, URect, UVec2, Vec2};
use bevy_reflect::Reflect; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use wgpu_types::TextureFormat; use wgpu_types::TextureFormat;
use crate::{cursor::CursorIcon, state::CustomCursorCache}; 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)]
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<Image>,
/// An optional texture atlas used to render the image.
pub texture_atlas: Option<TextureAtlas>,
/// Whether the image should be flipped along its x-axis.
pub flip_x: bool,
/// Whether the image should be flipped along its y-axis.
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<URect>,
/// X and Y coordinates of the hotspot in pixels. The hotspot must be within
/// the image bounds.
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)]
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. /// Custom cursor image data.
#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)]
pub enum CustomCursor { pub enum CustomCursor {
/// Image to use as a cursor. /// Use an image as the cursor.
Image { Image(CustomCursorImage),
/// The image must be in 8 bit int or 32 bit float rgba. PNG images
/// work well for this.
handle: Handle<Image>,
/// The (optional) texture atlas used to render the image.
texture_atlas: Option<TextureAtlas>,
/// Whether the image should be flipped along its x-axis.
flip_x: bool,
/// Whether the image should be flipped along its y-axis.
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.
rect: Option<URect>,
/// 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"))] #[cfg(all(target_family = "wasm", target_os = "unknown"))]
/// A URL to an image to use as the cursor. /// Use a URL to an image as the cursor.
Url { Url(CustomCursorUrl),
/// 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),
},
} }
impl From<CustomCursor> for CursorIcon { impl From<CustomCursor> for CursorIcon {

View File

@ -3,8 +3,10 @@
use std::time::Duration; use std::time::Duration;
use bevy::winit::cursor::CustomCursor; use bevy::{
use bevy::{prelude::*, winit::cursor::CursorIcon}; prelude::*,
winit::cursor::{CursorIcon, CustomCursor, CustomCursorImage},
};
fn main() { fn main() {
App::new() App::new()
@ -39,7 +41,7 @@ fn setup_cursor_icon(
let animation_config = AnimationConfig::new(0, 199, 1, 4); let animation_config = AnimationConfig::new(0, 199, 1, 4);
commands.entity(*window).insert(( commands.entity(*window).insert((
CursorIcon::Custom(CustomCursor::Image { CursorIcon::Custom(CustomCursor::Image(CustomCursorImage {
// Image to use as the cursor. // Image to use as the cursor.
handle: asset_server handle: asset_server
.load("cursors/kenney_crosshairPack/Tilesheet/crosshairs_tilesheet_white.png"), .load("cursors/kenney_crosshairPack/Tilesheet/crosshairs_tilesheet_white.png"),
@ -56,7 +58,7 @@ fn setup_cursor_icon(
// The hotspot is the point in the cursor image that will be // The hotspot is the point in the cursor image that will be
// positioned at the mouse cursor's position. // positioned at the mouse cursor's position.
hotspot: (0, 0), hotspot: (0, 0),
}), })),
animation_config, animation_config,
)); ));
} }
@ -112,15 +114,11 @@ impl AnimationConfig {
/// `last_sprite_index`. /// `last_sprite_index`.
fn execute_animation(time: Res<Time>, mut query: Query<(&mut AnimationConfig, &mut CursorIcon)>) { fn execute_animation(time: Res<Time>, mut query: Query<(&mut AnimationConfig, &mut CursorIcon)>) {
for (mut config, mut cursor_icon) in &mut query { for (mut config, mut cursor_icon) in &mut query {
if let CursorIcon::Custom(CustomCursor::Image { if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
ref mut texture_atlas,
..
}) = *cursor_icon
{
config.frame_timer.tick(time.delta()); config.frame_timer.tick(time.delta());
if config.frame_timer.finished() { if config.frame_timer.finished() {
if let Some(atlas) = texture_atlas { if let Some(atlas) = image.texture_atlas.as_mut() {
atlas.index += config.increment; atlas.index += config.increment;
if atlas.index > config.last_sprite_index { if atlas.index > config.last_sprite_index {
@ -141,18 +139,19 @@ fn toggle_texture_atlas(
) { ) {
if input.just_pressed(KeyCode::KeyT) { if input.just_pressed(KeyCode::KeyT) {
for mut cursor_icon in &mut query { for mut cursor_icon in &mut query {
if let CursorIcon::Custom(CustomCursor::Image { if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
ref mut texture_atlas, match image.texture_atlas.take() {
..
}) = *cursor_icon
{
*texture_atlas = match texture_atlas.take() {
Some(a) => { Some(a) => {
// Save the current texture atlas.
*cached_atlas = Some(a.clone()); *cached_atlas = Some(a.clone());
None
} }
None => cached_atlas.take(), None => {
}; // Restore the cached texture atlas.
if let Some(cached_a) = cached_atlas.take() {
image.texture_atlas = Some(cached_a);
}
}
}
} }
} }
} }
@ -164,8 +163,8 @@ fn toggle_flip_x(
) { ) {
if input.just_pressed(KeyCode::KeyX) { if input.just_pressed(KeyCode::KeyX) {
for mut cursor_icon in &mut query { for mut cursor_icon in &mut query {
if let CursorIcon::Custom(CustomCursor::Image { ref mut flip_x, .. }) = *cursor_icon { if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
*flip_x = !*flip_x; image.flip_x = !image.flip_x;
} }
} }
} }
@ -177,8 +176,8 @@ fn toggle_flip_y(
) { ) {
if input.just_pressed(KeyCode::KeyY) { if input.just_pressed(KeyCode::KeyY) {
for mut cursor_icon in &mut query { for mut cursor_icon in &mut query {
if let CursorIcon::Custom(CustomCursor::Image { ref mut flip_y, .. }) = *cursor_icon { if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
*flip_y = !*flip_y; image.flip_y = !image.flip_y;
} }
} }
} }
@ -214,15 +213,15 @@ fn cycle_rect(input: Res<ButtonInput<KeyCode>>, mut query: Query<&mut CursorIcon
]; ];
for mut cursor_icon in &mut query { for mut cursor_icon in &mut query {
if let CursorIcon::Custom(CustomCursor::Image { ref mut rect, .. }) = *cursor_icon { if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
let next_rect = SECTIONS let next_rect = SECTIONS
.iter() .iter()
.cycle() .cycle()
.skip_while(|&&corner| corner != *rect) .skip_while(|&&corner| corner != image.rect)
.nth(1) // move to the next element .nth(1) // move to the next element
.unwrap_or(&None); .unwrap_or(&None);
*rect = *next_rect; image.rect = *next_rect;
} }
} }
} }

View File

@ -2,7 +2,7 @@
//! the mouse pointer in various ways. //! the mouse pointer in various ways.
#[cfg(feature = "custom_cursor")] #[cfg(feature = "custom_cursor")]
use bevy::winit::cursor::CustomCursor; use bevy::winit::cursor::{CustomCursor, CustomCursorImage};
use bevy::{ use bevy::{
diagnostic::{FrameCount, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, diagnostic::{FrameCount, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*, prelude::*,
@ -163,14 +163,11 @@ fn init_cursor_icons(
SystemCursorIcon::Wait.into(), SystemCursorIcon::Wait.into(),
SystemCursorIcon::Text.into(), SystemCursorIcon::Text.into(),
#[cfg(feature = "custom_cursor")] #[cfg(feature = "custom_cursor")]
CustomCursor::Image { CustomCursor::Image(CustomCursorImage {
handle: asset_server.load("branding/icon.png"), handle: asset_server.load("branding/icon.png"),
texture_atlas: None,
flip_x: false,
flip_y: false,
rect: None,
hotspot: (128, 128), hotspot: (128, 128),
} ..Default::default()
})
.into(), .into(),
])); ]));
} }