Skip to content

Support texture atlases in CustomCursor::Image #17121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3311,6 +3311,18 @@ description = "Creates a solid color window"
category = "Window"
wasm = true

[[example]]
name = "custom_cursor_image"
path = "examples/window/custom_cursor_image.rs"
doc-scrape-examples = true
required-features = ["custom_cursor"]

[package.metadata.example.custom_cursor_image]
name = "Custom Cursor Image"
description = "Demonstrates creating an animated custom cursor from an image"
category = "Window"
wasm = true

[[example]]
name = "custom_user_event"
path = "examples/window/custom_user_event.rs"
Expand Down
19 changes: 19 additions & 0 deletions assets/cursors/kenney_crosshairPack/License.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@


Crosshair Pack

by Kenney Vleugels (Kenney.nl)

------------------------------

License (Creative Commons Zero, CC0)
http://creativecommons.org/publicdomain/zero/1.0/

You may use these assets in personal and commercial projects.
Credit (Kenney or www.kenney.nl) would be nice but is not mandatory.

------------------------------

Donate: http://support.kenney.nl

Follow on Twitter for updates: @KenneyNL (www.twitter.com/kenneynl)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions crates/bevy_image/src/texture_atlas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,12 @@ impl TextureAtlasLayout {
/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
/// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Default, Debug))]
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Default, Debug, PartialEq, Hash)
)]
pub struct TextureAtlas {
/// Texture atlas layout handle
pub layout: Handle<TextureAtlasLayout>,
Expand Down
87 changes: 51 additions & 36 deletions crates/bevy_winit/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ use crate::{
};
#[cfg(feature = "custom_cursor")]
use crate::{
custom_cursor::{
calculate_effective_rect, extract_and_transform_rgba_pixels, extract_rgba_pixels,
CustomCursorPlugin,
},
state::{CustomCursorCache, CustomCursorCacheKey},
WinitCustomCursor,
};
Expand All @@ -25,21 +29,21 @@ use bevy_ecs::{
world::{OnRemove, Ref},
};
#[cfg(feature = "custom_cursor")]
use bevy_image::Image;
use bevy_image::{Image, TextureAtlas, TextureAtlasLayout};
#[cfg(feature = "custom_cursor")]
use bevy_math::URect;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_utils::HashSet;
use bevy_window::{SystemCursorIcon, Window};
#[cfg(feature = "custom_cursor")]
use tracing::warn;
#[cfg(feature = "custom_cursor")]
use wgpu_types::TextureFormat;

pub(crate) struct CursorPlugin;

impl Plugin for CursorPlugin {
fn build(&self, app: &mut App) {
#[cfg(feature = "custom_cursor")]
app.init_resource::<CustomCursorCache>();
app.add_plugins(CustomCursorPlugin);

app.register_type::<CursorIcon>()
.add_systems(Last, update_cursors);
Expand Down Expand Up @@ -87,6 +91,19 @@ pub enum CustomCursor {
/// 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),
Expand All @@ -108,6 +125,7 @@ fn update_cursors(
windows: Query<(Entity, Ref<CursorIcon>), With<Window>>,
#[cfg(feature = "custom_cursor")] cursor_cache: Res<CustomCursorCache>,
#[cfg(feature = "custom_cursor")] images: Res<Assets<Image>>,
#[cfg(feature = "custom_cursor")] texture_atlases: Res<Assets<TextureAtlasLayout>>,
mut queue: Local<HashSet<Entity>>,
) {
for (entity, cursor) in windows.iter() {
Expand All @@ -117,8 +135,22 @@ fn update_cursors(

let cursor_source = match cursor.as_ref() {
#[cfg(feature = "custom_cursor")]
CursorIcon::Custom(CustomCursor::Image { handle, hotspot }) => {
let cache_key = CustomCursorCacheKey::Asset(handle.id());
CursorIcon::Custom(CustomCursor::Image {
handle,
texture_atlas,
flip_x,
flip_y,
rect,
hotspot,
}) => {
let cache_key = CustomCursorCacheKey::Image {
id: handle.id(),
texture_atlas_layout_id: texture_atlas.as_ref().map(|a| a.layout.id()),
texture_atlas_index: texture_atlas.as_ref().map(|a| a.index),
flip_x: *flip_x,
flip_y: *flip_y,
rect: *rect,
};

if cursor_cache.0.contains_key(&cache_key) {
CursorSource::CustomCached(cache_key)
Expand All @@ -130,17 +162,25 @@ fn update_cursors(
queue.insert(entity);
continue;
};
let Some(rgba) = image_to_rgba_pixels(image) else {

let (rect, needs_sub_image) =
calculate_effective_rect(&texture_atlases, image, texture_atlas, rect);

let maybe_rgba = if *flip_x || *flip_y || needs_sub_image {
extract_and_transform_rgba_pixels(image, *flip_x, *flip_y, rect)
} else {
extract_rgba_pixels(image)
};

let Some(rgba) = maybe_rgba 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,
rect.width() as u16,
rect.height() as u16,
hotspot.0,
hotspot.1,
) {
Expand Down Expand Up @@ -190,28 +230,3 @@ fn on_remove_cursor_icon(trigger: Trigger<OnRemove, CursorIcon>, mut commands: C
convert_system_cursor_icon(SystemCursorIcon::Default),
))));
}

#[cfg(feature = "custom_cursor")]
/// 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,
}
}
Loading
Loading