Support texture atlases in CustomCursor::Image (#17121)
# Objective - Bevy 0.15 added support for custom cursor images in https://github.com/bevyengine/bevy/pull/14284. - However, to do animated cursors using the initial support shipped in 0.15 means you'd have to animate the `Handle<Image>`: You can't use a `TextureAtlas` like you can with sprites and UI images. - For my use case, my cursors are spritesheets. To animate them, I'd have to break them down into multiple `Image` assets, but that seems less than ideal. ## Solution - Allow users to specify a `TextureAtlas` field when creating a custom cursor image. - To create parity with Bevy's `TextureAtlas` support on `Sprite`s and `ImageNode`s, this also allows users to specify `rect`, `flip_x` and `flip_y`. In fact, for my own use case, I need to `flip_y`. ## Testing - I added unit tests for `calculate_effective_rect` and `extract_and_transform_rgba_pixels`. - I added a brand new example for custom cursor images. It has controls to toggle fields on and off. I opted to add a new example because the existing cursor example (`window_settings`) would be far too messy for showcasing these custom cursor features (I did start down that path but decided to stop and make a brand new example). - The new example uses a [Kenny cursor icon] sprite sheet. I included the licence even though it's not required (and it's CC0). - I decided to make the example just loop through all cursor icons for its animation even though it's not a _realistic_ in-game animation sequence. - I ran the PNG through https://tinypng.com. Looks like it's about 35KB. - I'm open to adjusting the example spritesheet if required, but if it's fine as is, great. [Kenny cursor icon]: https://kenney-assets.itch.io/crosshair-pack --- ## Showcase https://github.com/user-attachments/assets/8f6be8d7-d1d4-42f9-b769-ef8532367749 ## Migration Guide The `CustomCursor::Image` enum variant has some new fields. Update your code to set them. Before: ```rust CustomCursor::Image { handle: asset_server.load("branding/icon.png"), hotspot: (128, 128), } ``` After: ```rust CustomCursor::Image { handle: asset_server.load("branding/icon.png"), texture_atlas: None, flip_x: false, flip_y: false, rect: None, hotspot: (128, 128), } ``` ## References - Feature request [originally raised in Discord]. [originally raised in Discord]: https://discord.com/channels/691052431525675048/692572690833473578/1319836362219847681
This commit is contained in:
parent
f2e00c8ed5
commit
0756a19f28
12
Cargo.toml
12
Cargo.toml
@ -3311,6 +3311,18 @@ description = "Creates a solid color window"
|
|||||||
category = "Window"
|
category = "Window"
|
||||||
wasm = true
|
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]]
|
[[example]]
|
||||||
name = "custom_user_event"
|
name = "custom_user_event"
|
||||||
path = "examples/window/custom_user_event.rs"
|
path = "examples/window/custom_user_event.rs"
|
||||||
|
19
assets/cursors/kenney_crosshairPack/License.txt
Normal file
19
assets/cursors/kenney_crosshairPack/License.txt
Normal file
@ -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)
|
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
@ -182,8 +182,12 @@ impl TextureAtlasLayout {
|
|||||||
/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
|
/// - [`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)
|
/// - [`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)
|
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Default, Debug))]
|
#[cfg_attr(
|
||||||
|
feature = "bevy_reflect",
|
||||||
|
derive(Reflect),
|
||||||
|
reflect(Default, Debug, PartialEq, Hash)
|
||||||
|
)]
|
||||||
pub struct TextureAtlas {
|
pub struct TextureAtlas {
|
||||||
/// Texture atlas layout handle
|
/// Texture atlas layout handle
|
||||||
pub layout: Handle<TextureAtlasLayout>,
|
pub layout: Handle<TextureAtlasLayout>,
|
||||||
|
@ -6,6 +6,10 @@ use crate::{
|
|||||||
};
|
};
|
||||||
#[cfg(feature = "custom_cursor")]
|
#[cfg(feature = "custom_cursor")]
|
||||||
use crate::{
|
use crate::{
|
||||||
|
custom_cursor::{
|
||||||
|
calculate_effective_rect, extract_and_transform_rgba_pixels, extract_rgba_pixels,
|
||||||
|
CustomCursorPlugin,
|
||||||
|
},
|
||||||
state::{CustomCursorCache, CustomCursorCacheKey},
|
state::{CustomCursorCache, CustomCursorCacheKey},
|
||||||
WinitCustomCursor,
|
WinitCustomCursor,
|
||||||
};
|
};
|
||||||
@ -25,21 +29,21 @@ use bevy_ecs::{
|
|||||||
world::{OnRemove, Ref},
|
world::{OnRemove, Ref},
|
||||||
};
|
};
|
||||||
#[cfg(feature = "custom_cursor")]
|
#[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_reflect::{std_traits::ReflectDefault, Reflect};
|
||||||
use bevy_utils::HashSet;
|
use bevy_utils::HashSet;
|
||||||
use bevy_window::{SystemCursorIcon, Window};
|
use bevy_window::{SystemCursorIcon, Window};
|
||||||
#[cfg(feature = "custom_cursor")]
|
#[cfg(feature = "custom_cursor")]
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
#[cfg(feature = "custom_cursor")]
|
|
||||||
use wgpu_types::TextureFormat;
|
|
||||||
|
|
||||||
pub(crate) struct CursorPlugin;
|
pub(crate) struct CursorPlugin;
|
||||||
|
|
||||||
impl Plugin for CursorPlugin {
|
impl Plugin for CursorPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
#[cfg(feature = "custom_cursor")]
|
#[cfg(feature = "custom_cursor")]
|
||||||
app.init_resource::<CustomCursorCache>();
|
app.add_plugins(CustomCursorPlugin);
|
||||||
|
|
||||||
app.register_type::<CursorIcon>()
|
app.register_type::<CursorIcon>()
|
||||||
.add_systems(Last, update_cursors);
|
.add_systems(Last, update_cursors);
|
||||||
@ -87,6 +91,19 @@ pub enum CustomCursor {
|
|||||||
/// The image must be in 8 bit int or 32 bit float rgba. PNG images
|
/// The image must be in 8 bit int or 32 bit float rgba. PNG images
|
||||||
/// work well for this.
|
/// work well for this.
|
||||||
handle: Handle<Image>,
|
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
|
/// X and Y coordinates of the hotspot in pixels. The hotspot must be
|
||||||
/// within the image bounds.
|
/// within the image bounds.
|
||||||
hotspot: (u16, u16),
|
hotspot: (u16, u16),
|
||||||
@ -108,6 +125,7 @@ fn update_cursors(
|
|||||||
windows: Query<(Entity, Ref<CursorIcon>), With<Window>>,
|
windows: Query<(Entity, Ref<CursorIcon>), With<Window>>,
|
||||||
#[cfg(feature = "custom_cursor")] cursor_cache: Res<CustomCursorCache>,
|
#[cfg(feature = "custom_cursor")] cursor_cache: Res<CustomCursorCache>,
|
||||||
#[cfg(feature = "custom_cursor")] images: Res<Assets<Image>>,
|
#[cfg(feature = "custom_cursor")] images: Res<Assets<Image>>,
|
||||||
|
#[cfg(feature = "custom_cursor")] texture_atlases: Res<Assets<TextureAtlasLayout>>,
|
||||||
mut queue: Local<HashSet<Entity>>,
|
mut queue: Local<HashSet<Entity>>,
|
||||||
) {
|
) {
|
||||||
for (entity, cursor) in windows.iter() {
|
for (entity, cursor) in windows.iter() {
|
||||||
@ -117,8 +135,22 @@ 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 { handle, hotspot }) => {
|
CursorIcon::Custom(CustomCursor::Image {
|
||||||
let cache_key = CustomCursorCacheKey::Asset(handle.id());
|
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) {
|
if cursor_cache.0.contains_key(&cache_key) {
|
||||||
CursorSource::CustomCached(cache_key)
|
CursorSource::CustomCached(cache_key)
|
||||||
@ -130,17 +162,25 @@ fn update_cursors(
|
|||||||
queue.insert(entity);
|
queue.insert(entity);
|
||||||
continue;
|
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");
|
warn!("Cursor image {handle:?} not accepted because it's not rgba8 or rgba32float format");
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let width = image.texture_descriptor.size.width;
|
|
||||||
let height = image.texture_descriptor.size.height;
|
|
||||||
let source = match WinitCustomCursor::from_rgba(
|
let source = match WinitCustomCursor::from_rgba(
|
||||||
rgba,
|
rgba,
|
||||||
width as u16,
|
rect.width() as u16,
|
||||||
height as u16,
|
rect.height() as u16,
|
||||||
hotspot.0,
|
hotspot.0,
|
||||||
hotspot.1,
|
hotspot.1,
|
||||||
) {
|
) {
|
||||||
@ -190,28 +230,3 @@ fn on_remove_cursor_icon(trigger: Trigger<OnRemove, CursorIcon>, mut commands: C
|
|||||||
convert_system_cursor_icon(SystemCursorIcon::Default),
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
490
crates/bevy_winit/src/custom_cursor.rs
Normal file
490
crates/bevy_winit/src/custom_cursor.rs
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_asset::Assets;
|
||||||
|
use bevy_image::{Image, TextureAtlas, TextureAtlasLayout, TextureAtlasPlugin};
|
||||||
|
use bevy_math::{ops, Rect, URect, UVec2, Vec2};
|
||||||
|
use wgpu_types::TextureFormat;
|
||||||
|
|
||||||
|
use crate::state::CustomCursorCache;
|
||||||
|
|
||||||
|
/// Adds support for custom cursors.
|
||||||
|
pub(crate) struct CustomCursorPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CustomCursorPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
if !app.is_plugin_added::<TextureAtlasPlugin>() {
|
||||||
|
app.add_plugins(TextureAtlasPlugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.init_resource::<CustomCursorCache>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the effective rect and returns it along with a flag to indicate
|
||||||
|
/// whether a sub-image operation is needed. The flag allows the caller to
|
||||||
|
/// determine whether the image data needs a sub-image extracted from it. Note:
|
||||||
|
/// To avoid lossy comparisons between [`Rect`] and [`URect`], the flag is
|
||||||
|
/// always set to `true` when a [`TextureAtlas`] is used.
|
||||||
|
#[inline(always)]
|
||||||
|
pub(crate) fn calculate_effective_rect(
|
||||||
|
texture_atlas_layouts: &Assets<TextureAtlasLayout>,
|
||||||
|
image: &Image,
|
||||||
|
texture_atlas: &Option<TextureAtlas>,
|
||||||
|
rect: &Option<URect>,
|
||||||
|
) -> (Rect, bool) {
|
||||||
|
let atlas_rect = texture_atlas
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.texture_rect(texture_atlas_layouts))
|
||||||
|
.map(|r| r.as_rect());
|
||||||
|
|
||||||
|
match (atlas_rect, rect) {
|
||||||
|
(None, None) => (
|
||||||
|
Rect {
|
||||||
|
min: Vec2::ZERO,
|
||||||
|
max: Vec2::new(
|
||||||
|
image.texture_descriptor.size.width as f32,
|
||||||
|
image.texture_descriptor.size.height as f32,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
(None, Some(image_rect)) => (
|
||||||
|
image_rect.as_rect(),
|
||||||
|
image_rect
|
||||||
|
!= &URect {
|
||||||
|
min: UVec2::ZERO,
|
||||||
|
max: UVec2::new(
|
||||||
|
image.texture_descriptor.size.width,
|
||||||
|
image.texture_descriptor.size.height,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(Some(atlas_rect), None) => (atlas_rect, true),
|
||||||
|
(Some(atlas_rect), Some(image_rect)) => (
|
||||||
|
{
|
||||||
|
let mut image_rect = image_rect.as_rect();
|
||||||
|
image_rect.min += atlas_rect.min;
|
||||||
|
image_rect.max += atlas_rect.min;
|
||||||
|
image_rect
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the RGBA pixel data from `image`, converting it if necessary.
|
||||||
|
///
|
||||||
|
/// Only supports rgba8 and rgba32float formats.
|
||||||
|
pub(crate) fn extract_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);
|
||||||
|
ops::round(num.clamp(0.0, 1.0) * 255.0) as u8
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `image` data as a `Vec<u8>` for the specified sub-region.
|
||||||
|
///
|
||||||
|
/// The image is flipped along the x and y axes if `flip_x` and `flip_y` are
|
||||||
|
/// `true`, respectively.
|
||||||
|
///
|
||||||
|
/// Only supports rgba8 and rgba32float formats.
|
||||||
|
pub(crate) fn extract_and_transform_rgba_pixels(
|
||||||
|
image: &Image,
|
||||||
|
flip_x: bool,
|
||||||
|
flip_y: bool,
|
||||||
|
rect: Rect,
|
||||||
|
) -> Option<Vec<u8>> {
|
||||||
|
let image_data = extract_rgba_pixels(image)?;
|
||||||
|
|
||||||
|
let width = rect.width() as usize;
|
||||||
|
let height = rect.height() as usize;
|
||||||
|
let mut sub_image_data = Vec::with_capacity(width * height * 4); // assuming 4 bytes per pixel (RGBA8)
|
||||||
|
|
||||||
|
for y in 0..height {
|
||||||
|
for x in 0..width {
|
||||||
|
let src_x = if flip_x { width - 1 - x } else { x };
|
||||||
|
let src_y = if flip_y { height - 1 - y } else { y };
|
||||||
|
let index = ((rect.min.y as usize + src_y)
|
||||||
|
* image.texture_descriptor.size.width as usize
|
||||||
|
+ (rect.min.x as usize + src_x))
|
||||||
|
* 4;
|
||||||
|
sub_image_data.extend_from_slice(&image_data[index..index + 4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(sub_image_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use bevy_app::App;
|
||||||
|
use bevy_asset::RenderAssetUsages;
|
||||||
|
use bevy_image::Image;
|
||||||
|
use bevy_math::Rect;
|
||||||
|
use bevy_math::Vec2;
|
||||||
|
use wgpu_types::{Extent3d, TextureDimension};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn create_image_rgba8(data: &[u8]) -> Image {
|
||||||
|
Image::new(
|
||||||
|
Extent3d {
|
||||||
|
width: 3,
|
||||||
|
height: 3,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
TextureDimension::D2,
|
||||||
|
data.to_vec(),
|
||||||
|
TextureFormat::Rgba8UnormSrgb,
|
||||||
|
RenderAssetUsages::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! test_calculate_effective_rect {
|
||||||
|
($name:ident, $use_texture_atlas:expr, $rect:expr, $expected_rect:expr, $expected_needs_sub_image:expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let mut app = App::new();
|
||||||
|
let mut texture_atlas_layout_assets = Assets::<TextureAtlasLayout>::default();
|
||||||
|
|
||||||
|
// Create a simple 3x3 texture atlas layout for the test cases
|
||||||
|
// that use a texture atlas. In the future we could adjust the
|
||||||
|
// test cases to use different texture atlas layouts.
|
||||||
|
let layout = TextureAtlasLayout::from_grid(UVec2::new(3, 3), 1, 1, None, None);
|
||||||
|
let layout_handle = texture_atlas_layout_assets.add(layout);
|
||||||
|
|
||||||
|
app.insert_resource(texture_atlas_layout_assets);
|
||||||
|
|
||||||
|
let texture_atlases = app
|
||||||
|
.world()
|
||||||
|
.get_resource::<Assets<TextureAtlasLayout>>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let image = create_image_rgba8(&[0; 3 * 3 * 4]); // 3x3 image
|
||||||
|
|
||||||
|
let texture_atlas = if $use_texture_atlas {
|
||||||
|
Some(TextureAtlas::from(layout_handle))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let rect = $rect;
|
||||||
|
|
||||||
|
let (result_rect, needs_sub_image) =
|
||||||
|
calculate_effective_rect(&texture_atlases, &image, &texture_atlas, &rect);
|
||||||
|
|
||||||
|
assert_eq!(result_rect, $expected_rect);
|
||||||
|
assert_eq!(needs_sub_image, $expected_needs_sub_image);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test_calculate_effective_rect!(
|
||||||
|
no_texture_atlas_no_rect,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
Rect {
|
||||||
|
min: Vec2::ZERO,
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
test_calculate_effective_rect!(
|
||||||
|
no_texture_atlas_with_partial_rect,
|
||||||
|
false,
|
||||||
|
Some(URect {
|
||||||
|
min: UVec2::new(1, 1),
|
||||||
|
max: UVec2::new(3, 3)
|
||||||
|
}),
|
||||||
|
Rect {
|
||||||
|
min: Vec2::new(1.0, 1.0),
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
test_calculate_effective_rect!(
|
||||||
|
no_texture_atlas_with_full_rect,
|
||||||
|
false,
|
||||||
|
Some(URect {
|
||||||
|
min: UVec2::ZERO,
|
||||||
|
max: UVec2::new(3, 3)
|
||||||
|
}),
|
||||||
|
Rect {
|
||||||
|
min: Vec2::ZERO,
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
test_calculate_effective_rect!(
|
||||||
|
texture_atlas_no_rect,
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
Rect {
|
||||||
|
min: Vec2::ZERO,
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
true // always needs sub-image to avoid comparing Rect against URect
|
||||||
|
);
|
||||||
|
|
||||||
|
test_calculate_effective_rect!(
|
||||||
|
texture_atlas_rect,
|
||||||
|
true,
|
||||||
|
Some(URect {
|
||||||
|
min: UVec2::ZERO,
|
||||||
|
max: UVec2::new(3, 3)
|
||||||
|
}),
|
||||||
|
Rect {
|
||||||
|
min: Vec2::new(0.0, 0.0),
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
true // always needs sub-image to avoid comparing Rect against URect
|
||||||
|
);
|
||||||
|
|
||||||
|
fn create_image_rgba32float(data: &[u8]) -> Image {
|
||||||
|
let float_data: Vec<f32> = data
|
||||||
|
.chunks(4)
|
||||||
|
.flat_map(|chunk| {
|
||||||
|
chunk
|
||||||
|
.iter()
|
||||||
|
.map(|&x| x as f32 / 255.0) // convert each channel to f32
|
||||||
|
.collect::<Vec<f32>>()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Image::new(
|
||||||
|
Extent3d {
|
||||||
|
width: 3,
|
||||||
|
height: 3,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
TextureDimension::D2,
|
||||||
|
bytemuck::cast_slice(&float_data).to_vec(),
|
||||||
|
TextureFormat::Rgba32Float,
|
||||||
|
RenderAssetUsages::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! test_extract_and_transform_rgba_pixels {
|
||||||
|
($name:ident, $flip_x:expr, $flip_y:expr, $rect:expr, $expected:expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let image_data: &[u8] = &[
|
||||||
|
// Row 1: Red, Green, Blue
|
||||||
|
255, 0, 0, 255, // Red
|
||||||
|
0, 255, 0, 255, // Green
|
||||||
|
0, 0, 255, 255, // Blue
|
||||||
|
// Row 2: Yellow, Cyan, Magenta
|
||||||
|
255, 255, 0, 255, // Yellow
|
||||||
|
0, 255, 255, 255, // Cyan
|
||||||
|
255, 0, 255, 255, // Magenta
|
||||||
|
// Row 3: White, Gray, Black
|
||||||
|
255, 255, 255, 255, // White
|
||||||
|
128, 128, 128, 255, // Gray
|
||||||
|
0, 0, 0, 255, // Black
|
||||||
|
];
|
||||||
|
|
||||||
|
// RGBA8 test
|
||||||
|
{
|
||||||
|
let image = create_image_rgba8(image_data);
|
||||||
|
let rect = $rect;
|
||||||
|
let result = extract_and_transform_rgba_pixels(&image, $flip_x, $flip_y, rect);
|
||||||
|
assert_eq!(result, Some($expected.to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA32Float test
|
||||||
|
{
|
||||||
|
let image = create_image_rgba32float(image_data);
|
||||||
|
let rect = $rect;
|
||||||
|
let result = extract_and_transform_rgba_pixels(&image, $flip_x, $flip_y, rect);
|
||||||
|
assert_eq!(result, Some($expected.to_vec()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test_extract_and_transform_rgba_pixels!(
|
||||||
|
no_flip_full_image,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
Rect {
|
||||||
|
min: Vec2::ZERO,
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Row 1: Red, Green, Blue
|
||||||
|
255, 0, 0, 255, // Red
|
||||||
|
0, 255, 0, 255, // Green
|
||||||
|
0, 0, 255, 255, // Blue
|
||||||
|
// Row 2: Yellow, Cyan, Magenta
|
||||||
|
255, 255, 0, 255, // Yellow
|
||||||
|
0, 255, 255, 255, // Cyan
|
||||||
|
255, 0, 255, 255, // Magenta
|
||||||
|
// Row 3: White, Gray, Black
|
||||||
|
255, 255, 255, 255, // White
|
||||||
|
128, 128, 128, 255, // Gray
|
||||||
|
0, 0, 0, 255, // Black
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test_extract_and_transform_rgba_pixels!(
|
||||||
|
flip_x_full_image,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Rect {
|
||||||
|
min: Vec2::ZERO,
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Row 1 flipped: Blue, Green, Red
|
||||||
|
0, 0, 255, 255, // Blue
|
||||||
|
0, 255, 0, 255, // Green
|
||||||
|
255, 0, 0, 255, // Red
|
||||||
|
// Row 2 flipped: Magenta, Cyan, Yellow
|
||||||
|
255, 0, 255, 255, // Magenta
|
||||||
|
0, 255, 255, 255, // Cyan
|
||||||
|
255, 255, 0, 255, // Yellow
|
||||||
|
// Row 3 flipped: Black, Gray, White
|
||||||
|
0, 0, 0, 255, // Black
|
||||||
|
128, 128, 128, 255, // Gray
|
||||||
|
255, 255, 255, 255, // White
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test_extract_and_transform_rgba_pixels!(
|
||||||
|
flip_y_full_image,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
Rect {
|
||||||
|
min: Vec2::ZERO,
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Row 3: White, Gray, Black
|
||||||
|
255, 255, 255, 255, // White
|
||||||
|
128, 128, 128, 255, // Gray
|
||||||
|
0, 0, 0, 255, // Black
|
||||||
|
// Row 2: Yellow, Cyan, Magenta
|
||||||
|
255, 255, 0, 255, // Yellow
|
||||||
|
0, 255, 255, 255, // Cyan
|
||||||
|
255, 0, 255, 255, // Magenta
|
||||||
|
// Row 1: Red, Green, Blue
|
||||||
|
255, 0, 0, 255, // Red
|
||||||
|
0, 255, 0, 255, // Green
|
||||||
|
0, 0, 255, 255, // Blue
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test_extract_and_transform_rgba_pixels!(
|
||||||
|
flip_both_full_image,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
Rect {
|
||||||
|
min: Vec2::ZERO,
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Row 3 flipped: Black, Gray, White
|
||||||
|
0, 0, 0, 255, // Black
|
||||||
|
128, 128, 128, 255, // Gray
|
||||||
|
255, 255, 255, 255, // White
|
||||||
|
// Row 2 flipped: Magenta, Cyan, Yellow
|
||||||
|
255, 0, 255, 255, // Magenta
|
||||||
|
0, 255, 255, 255, // Cyan
|
||||||
|
255, 255, 0, 255, // Yellow
|
||||||
|
// Row 1 flipped: Blue, Green, Red
|
||||||
|
0, 0, 255, 255, // Blue
|
||||||
|
0, 255, 0, 255, // Green
|
||||||
|
255, 0, 0, 255, // Red
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test_extract_and_transform_rgba_pixels!(
|
||||||
|
no_flip_rect,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
Rect {
|
||||||
|
min: Vec2::new(1.0, 1.0),
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Only includes part of the original image (sub-rectangle)
|
||||||
|
// Row 2, columns 2-3: Cyan, Magenta
|
||||||
|
0, 255, 255, 255, // Cyan
|
||||||
|
255, 0, 255, 255, // Magenta
|
||||||
|
// Row 3, columns 2-3: Gray, Black
|
||||||
|
128, 128, 128, 255, // Gray
|
||||||
|
0, 0, 0, 255, // Black
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test_extract_and_transform_rgba_pixels!(
|
||||||
|
flip_x_rect,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Rect {
|
||||||
|
min: Vec2::new(1.0, 1.0),
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Row 2 flipped: Magenta, Cyan
|
||||||
|
255, 0, 255, 255, // Magenta
|
||||||
|
0, 255, 255, 255, // Cyan
|
||||||
|
// Row 3 flipped: Black, Gray
|
||||||
|
0, 0, 0, 255, // Black
|
||||||
|
128, 128, 128, 255, // Gray
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test_extract_and_transform_rgba_pixels!(
|
||||||
|
flip_y_rect,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
Rect {
|
||||||
|
min: Vec2::new(1.0, 1.0),
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Row 3 first: Gray, Black
|
||||||
|
128, 128, 128, 255, // Gray
|
||||||
|
0, 0, 0, 255, // Black
|
||||||
|
// Row 2 second: Cyan, Magenta
|
||||||
|
0, 255, 255, 255, // Cyan
|
||||||
|
255, 0, 255, 255, // Magenta
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test_extract_and_transform_rgba_pixels!(
|
||||||
|
flip_both_rect,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
Rect {
|
||||||
|
min: Vec2::new(1.0, 1.0),
|
||||||
|
max: Vec2::new(3.0, 3.0)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Row 3 flipped: Black, Gray
|
||||||
|
0, 0, 0, 255, // Black
|
||||||
|
128, 128, 128, 255, // Gray
|
||||||
|
// Row 2 flipped: Magenta, Cyan
|
||||||
|
255, 0, 255, 255, // Magenta
|
||||||
|
0, 255, 255, 255, // Cyan
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
@ -50,6 +50,8 @@ use crate::{
|
|||||||
pub mod accessibility;
|
pub mod accessibility;
|
||||||
mod converters;
|
mod converters;
|
||||||
pub mod cursor;
|
pub mod cursor;
|
||||||
|
#[cfg(feature = "custom_cursor")]
|
||||||
|
mod custom_cursor;
|
||||||
mod state;
|
mod state;
|
||||||
mod system;
|
mod system;
|
||||||
mod winit_config;
|
mod winit_config;
|
||||||
|
@ -11,12 +11,14 @@ use bevy_ecs::{
|
|||||||
world::FromWorld,
|
world::FromWorld,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "custom_cursor")]
|
#[cfg(feature = "custom_cursor")]
|
||||||
use bevy_image::Image;
|
use bevy_image::{Image, TextureAtlasLayout};
|
||||||
use bevy_input::{
|
use bevy_input::{
|
||||||
gestures::*,
|
gestures::*,
|
||||||
mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
|
mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
|
||||||
};
|
};
|
||||||
use bevy_log::{error, trace, warn};
|
use bevy_log::{error, trace, warn};
|
||||||
|
#[cfg(feature = "custom_cursor")]
|
||||||
|
use bevy_math::URect;
|
||||||
use bevy_math::{ivec2, DVec2, Vec2};
|
use bevy_math::{ivec2, DVec2, Vec2};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use bevy_tasks::tick_global_task_pools_on_main_thread;
|
use bevy_tasks::tick_global_task_pools_on_main_thread;
|
||||||
@ -150,10 +152,17 @@ impl<T: Event> WinitAppRunnerState<T> {
|
|||||||
/// Identifiers for custom cursors used in caching.
|
/// Identifiers for custom cursors used in caching.
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
pub enum CustomCursorCacheKey {
|
pub enum CustomCursorCacheKey {
|
||||||
/// An `AssetId` to a cursor.
|
/// A custom cursor with an image.
|
||||||
Asset(AssetId<Image>),
|
Image {
|
||||||
|
id: AssetId<Image>,
|
||||||
|
texture_atlas_layout_id: Option<AssetId<TextureAtlasLayout>>,
|
||||||
|
texture_atlas_index: Option<usize>,
|
||||||
|
flip_x: bool,
|
||||||
|
flip_y: bool,
|
||||||
|
rect: Option<URect>,
|
||||||
|
},
|
||||||
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
|
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
|
||||||
/// An URL to a cursor.
|
/// A custom cursor with a URL.
|
||||||
Url(String),
|
Url(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -544,6 +544,7 @@ Example | Description
|
|||||||
Example | Description
|
Example | Description
|
||||||
--- | ---
|
--- | ---
|
||||||
[Clear Color](../examples/window/clear_color.rs) | Creates a solid color window
|
[Clear Color](../examples/window/clear_color.rs) | Creates a solid color window
|
||||||
|
[Custom Cursor Image](../examples/window/custom_cursor_image.rs) | Demonstrates creating an animated custom cursor from an image
|
||||||
[Custom User Event](../examples/window/custom_user_event.rs) | Handles custom user events within the event loop
|
[Custom User Event](../examples/window/custom_user_event.rs) | Handles custom user events within the event loop
|
||||||
[Low Power](../examples/window/low_power.rs) | Demonstrates settings to reduce power use for bevy applications
|
[Low Power](../examples/window/low_power.rs) | Demonstrates settings to reduce power use for bevy applications
|
||||||
[Monitor info](../examples/window/monitor_info.rs) | Displays information about available monitors (displays).
|
[Monitor info](../examples/window/monitor_info.rs) | Displays information about available monitors (displays).
|
||||||
|
228
examples/window/custom_cursor_image.rs
Normal file
228
examples/window/custom_cursor_image.rs
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
//! Illustrates how to use a custom cursor image with a texture atlas and
|
||||||
|
//! animation.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bevy::winit::cursor::CustomCursor;
|
||||||
|
use bevy::{prelude::*, winit::cursor::CursorIcon};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_systems(
|
||||||
|
Startup,
|
||||||
|
(setup_cursor_icon, setup_camera, setup_instructions),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
execute_animation,
|
||||||
|
toggle_texture_atlas,
|
||||||
|
toggle_flip_x,
|
||||||
|
toggle_flip_y,
|
||||||
|
cycle_rect,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_cursor_icon(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
|
||||||
|
window: Single<Entity, With<Window>>,
|
||||||
|
) {
|
||||||
|
let layout =
|
||||||
|
TextureAtlasLayout::from_grid(UVec2::splat(64), 20, 10, Some(UVec2::splat(5)), None);
|
||||||
|
let texture_atlas_layout = texture_atlas_layouts.add(layout);
|
||||||
|
|
||||||
|
let animation_config = AnimationConfig::new(0, 199, 1, 4);
|
||||||
|
|
||||||
|
commands.entity(*window).insert((
|
||||||
|
CursorIcon::Custom(CustomCursor::Image {
|
||||||
|
// Image to use as the cursor.
|
||||||
|
handle: asset_server
|
||||||
|
.load("cursors/kenney_crosshairPack/Tilesheet/crosshairs_tilesheet_white.png"),
|
||||||
|
// Optional texture atlas allows you to pick a section of the image
|
||||||
|
// and animate it.
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: texture_atlas_layout.clone(),
|
||||||
|
index: animation_config.first_sprite_index,
|
||||||
|
}),
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
// Optional section of the image to use as the cursor.
|
||||||
|
rect: None,
|
||||||
|
// The hotspot is the point in the cursor image that will be
|
||||||
|
// positioned at the mouse cursor's position.
|
||||||
|
hotspot: (0, 0),
|
||||||
|
}),
|
||||||
|
animation_config,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_camera(mut commands: Commands) {
|
||||||
|
commands.spawn(Camera3d::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_instructions(mut commands: Commands) {
|
||||||
|
commands.spawn((
|
||||||
|
Text::new(
|
||||||
|
"Press T to toggle the cursor's `texture_atlas`.\n
|
||||||
|
Press X to toggle the cursor's `flip_x` setting.\n
|
||||||
|
Press Y to toggle the cursor's `flip_y` setting.\n
|
||||||
|
Press C to cycle through the sections of the cursor's image using `rect`.",
|
||||||
|
),
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
bottom: Val::Px(12.0),
|
||||||
|
left: Val::Px(12.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct AnimationConfig {
|
||||||
|
first_sprite_index: usize,
|
||||||
|
last_sprite_index: usize,
|
||||||
|
increment: usize,
|
||||||
|
fps: u8,
|
||||||
|
frame_timer: Timer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationConfig {
|
||||||
|
fn new(first: usize, last: usize, increment: usize, fps: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
first_sprite_index: first,
|
||||||
|
last_sprite_index: last,
|
||||||
|
increment,
|
||||||
|
fps,
|
||||||
|
frame_timer: Self::timer_from_fps(fps),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timer_from_fps(fps: u8) -> Timer {
|
||||||
|
Timer::new(Duration::from_secs_f32(1.0 / (fps as f32)), TimerMode::Once)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This system loops through all the sprites in the [`CursorIcon`]'s
|
||||||
|
/// [`TextureAtlas`], from [`AnimationConfig`]'s `first_sprite_index` to
|
||||||
|
/// `last_sprite_index`.
|
||||||
|
fn execute_animation(time: Res<Time>, mut query: Query<(&mut AnimationConfig, &mut CursorIcon)>) {
|
||||||
|
for (mut config, mut cursor_icon) in &mut query {
|
||||||
|
if let CursorIcon::Custom(CustomCursor::Image {
|
||||||
|
ref mut texture_atlas,
|
||||||
|
..
|
||||||
|
}) = *cursor_icon
|
||||||
|
{
|
||||||
|
config.frame_timer.tick(time.delta());
|
||||||
|
|
||||||
|
if config.frame_timer.finished() {
|
||||||
|
if let Some(atlas) = texture_atlas {
|
||||||
|
atlas.index += config.increment;
|
||||||
|
|
||||||
|
if atlas.index > config.last_sprite_index {
|
||||||
|
atlas.index = config.first_sprite_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.frame_timer = AnimationConfig::timer_from_fps(config.fps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_texture_atlas(
|
||||||
|
input: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut query: Query<&mut CursorIcon, With<Window>>,
|
||||||
|
mut cached_atlas: Local<Option<TextureAtlas>>, // this lets us restore the previous value
|
||||||
|
) {
|
||||||
|
if input.just_pressed(KeyCode::KeyT) {
|
||||||
|
for mut cursor_icon in &mut query {
|
||||||
|
if let CursorIcon::Custom(CustomCursor::Image {
|
||||||
|
ref mut texture_atlas,
|
||||||
|
..
|
||||||
|
}) = *cursor_icon
|
||||||
|
{
|
||||||
|
*texture_atlas = match texture_atlas.take() {
|
||||||
|
Some(a) => {
|
||||||
|
*cached_atlas = Some(a.clone());
|
||||||
|
None
|
||||||
|
}
|
||||||
|
None => cached_atlas.take(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_flip_x(
|
||||||
|
input: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut query: Query<&mut CursorIcon, With<Window>>,
|
||||||
|
) {
|
||||||
|
if input.just_pressed(KeyCode::KeyX) {
|
||||||
|
for mut cursor_icon in &mut query {
|
||||||
|
if let CursorIcon::Custom(CustomCursor::Image { ref mut flip_x, .. }) = *cursor_icon {
|
||||||
|
*flip_x = !*flip_x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_flip_y(
|
||||||
|
input: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut query: Query<&mut CursorIcon, With<Window>>,
|
||||||
|
) {
|
||||||
|
if input.just_pressed(KeyCode::KeyY) {
|
||||||
|
for mut cursor_icon in &mut query {
|
||||||
|
if let CursorIcon::Custom(CustomCursor::Image { ref mut flip_y, .. }) = *cursor_icon {
|
||||||
|
*flip_y = !*flip_y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This system alternates the [`CursorIcon`]'s `rect` field between `None` and
|
||||||
|
/// 4 sections/rectangles of the cursor's image.
|
||||||
|
fn cycle_rect(input: Res<ButtonInput<KeyCode>>, mut query: Query<&mut CursorIcon, With<Window>>) {
|
||||||
|
if !input.just_pressed(KeyCode::KeyC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RECT_SIZE: u32 = 32; // half the size of a tile in the texture atlas
|
||||||
|
|
||||||
|
const SECTIONS: [Option<URect>; 5] = [
|
||||||
|
Some(URect {
|
||||||
|
min: UVec2::ZERO,
|
||||||
|
max: UVec2::splat(RECT_SIZE),
|
||||||
|
}),
|
||||||
|
Some(URect {
|
||||||
|
min: UVec2::new(RECT_SIZE, 0),
|
||||||
|
max: UVec2::new(2 * RECT_SIZE, RECT_SIZE),
|
||||||
|
}),
|
||||||
|
Some(URect {
|
||||||
|
min: UVec2::new(0, RECT_SIZE),
|
||||||
|
max: UVec2::new(RECT_SIZE, 2 * RECT_SIZE),
|
||||||
|
}),
|
||||||
|
Some(URect {
|
||||||
|
min: UVec2::new(RECT_SIZE, RECT_SIZE),
|
||||||
|
max: UVec2::splat(2 * RECT_SIZE),
|
||||||
|
}),
|
||||||
|
None, // reset to None
|
||||||
|
];
|
||||||
|
|
||||||
|
for mut cursor_icon in &mut query {
|
||||||
|
if let CursorIcon::Custom(CustomCursor::Image { ref mut rect, .. }) = *cursor_icon {
|
||||||
|
let next_rect = SECTIONS
|
||||||
|
.iter()
|
||||||
|
.cycle()
|
||||||
|
.skip_while(|&&corner| corner != *rect)
|
||||||
|
.nth(1) // move to the next element
|
||||||
|
.unwrap_or(&None);
|
||||||
|
|
||||||
|
*rect = *next_rect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -165,6 +165,10 @@ fn init_cursor_icons(
|
|||||||
#[cfg(feature = "custom_cursor")]
|
#[cfg(feature = "custom_cursor")]
|
||||||
CustomCursor::Image {
|
CustomCursor::Image {
|
||||||
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),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
|
Loading…
Reference in New Issue
Block a user