1use bevy_app::{App, Plugin};
2use bevy_asset::{Assets, Handle};
3use bevy_image::{Image, TextureAtlas, TextureAtlasLayout, TextureAtlasPlugin};
4use bevy_math::{ops, Rect, URect, UVec2, Vec2};
5use bevy_reflect::{std_traits::ReflectDefault, Reflect};
6use wgpu_types::TextureFormat;
7
8use crate::{cursor::CursorIcon, state::CustomCursorCache};
9
10#[derive(Debug, Clone, Default, Reflect, PartialEq, Eq, Hash)]
12#[reflect(Debug, Default, Hash, PartialEq, Clone)]
13pub struct CustomCursorImage {
14 pub handle: Handle<Image>,
17 pub texture_atlas: Option<TextureAtlas>,
19 pub flip_x: bool,
24 pub flip_y: bool,
29 pub rect: Option<URect>,
36 pub hotspot: (u16, u16),
43}
44
45#[cfg(all(target_family = "wasm", target_os = "unknown"))]
46#[derive(Debug, Clone, Default, Reflect, PartialEq, Eq, Hash)]
48#[reflect(Debug, Default, Hash, PartialEq, Clone)]
49pub struct CustomCursorUrl {
50 pub url: String,
53 pub hotspot: (u16, u16),
56}
57
58#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)]
60#[reflect(Clone, PartialEq, Hash)]
61pub enum CustomCursor {
62 Image(CustomCursorImage),
64 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
65 Url(CustomCursorUrl),
67}
68
69impl From<CustomCursor> for CursorIcon {
70 fn from(cursor: CustomCursor) -> Self {
71 CursorIcon::Custom(cursor)
72 }
73}
74
75pub(crate) struct CustomCursorPlugin;
77
78impl Plugin for CustomCursorPlugin {
79 fn build(&self, app: &mut App) {
80 if !app.is_plugin_added::<TextureAtlasPlugin>() {
81 app.add_plugins(TextureAtlasPlugin);
82 }
83
84 app.init_resource::<CustomCursorCache>();
85 }
86}
87
88#[inline(always)]
94pub(crate) fn calculate_effective_rect(
95 texture_atlas_layouts: &Assets<TextureAtlasLayout>,
96 image: &Image,
97 texture_atlas: &Option<TextureAtlas>,
98 rect: &Option<URect>,
99) -> (Rect, bool) {
100 let atlas_rect = texture_atlas
101 .as_ref()
102 .and_then(|s| s.texture_rect(texture_atlas_layouts))
103 .map(|r| r.as_rect());
104
105 match (atlas_rect, rect) {
106 (None, None) => (
107 Rect {
108 min: Vec2::ZERO,
109 max: Vec2::new(
110 image.texture_descriptor.size.width as f32,
111 image.texture_descriptor.size.height as f32,
112 ),
113 },
114 false,
115 ),
116 (None, Some(image_rect)) => (
117 image_rect.as_rect(),
118 image_rect
119 != &URect {
120 min: UVec2::ZERO,
121 max: UVec2::new(
122 image.texture_descriptor.size.width,
123 image.texture_descriptor.size.height,
124 ),
125 },
126 ),
127 (Some(atlas_rect), None) => (atlas_rect, true),
128 (Some(atlas_rect), Some(image_rect)) => (
129 {
130 let mut image_rect = image_rect.as_rect();
131 image_rect.min += atlas_rect.min;
132 image_rect.max += atlas_rect.min;
133 image_rect
134 },
135 true,
136 ),
137 }
138}
139
140pub(crate) fn extract_rgba_pixels(image: &Image) -> Option<Vec<u8>> {
144 match image.texture_descriptor.format {
145 TextureFormat::Rgba8Unorm
146 | TextureFormat::Rgba8UnormSrgb
147 | TextureFormat::Rgba8Snorm
148 | TextureFormat::Rgba8Uint
149 | TextureFormat::Rgba8Sint => Some(image.data.clone()?),
150 TextureFormat::Rgba32Float => image.data.as_ref().map(|data| {
151 data.chunks(4)
152 .map(|chunk| {
153 let chunk = chunk.try_into().unwrap();
154 let num = bytemuck::cast_ref::<[u8; 4], f32>(chunk);
155 ops::round(num.clamp(0.0, 1.0) * 255.0) as u8
156 })
157 .collect()
158 }),
159 _ => None,
160 }
161}
162
163pub(crate) fn extract_and_transform_rgba_pixels(
170 image: &Image,
171 flip_x: bool,
172 flip_y: bool,
173 rect: Rect,
174) -> Option<Vec<u8>> {
175 let image_data = extract_rgba_pixels(image)?;
176
177 let width = rect.width() as usize;
178 let height = rect.height() as usize;
179 let mut sub_image_data = Vec::with_capacity(width * height * 4); for y in 0..height {
182 for x in 0..width {
183 let src_x = if flip_x { width - 1 - x } else { x };
184 let src_y = if flip_y { height - 1 - y } else { y };
185 let index = ((rect.min.y as usize + src_y)
186 * image.texture_descriptor.size.width as usize
187 + (rect.min.x as usize + src_x))
188 * 4;
189 sub_image_data.extend_from_slice(&image_data[index..index + 4]);
190 }
191 }
192
193 Some(sub_image_data)
194}
195
196pub(crate) fn transform_hotspot(
199 hotspot: (u16, u16),
200 flip_x: bool,
201 flip_y: bool,
202 rect: Rect,
203) -> (u16, u16) {
204 let hotspot_x = hotspot.0 as f32;
205 let hotspot_y = hotspot.1 as f32;
206
207 let (width, height) = (rect.width(), rect.height());
208
209 let hotspot_x = if flip_x {
210 (width - 1.0).max(0.0) - hotspot_x
211 } else {
212 hotspot_x
213 };
214 let hotspot_y = if flip_y {
215 (height - 1.0).max(0.0) - hotspot_y
216 } else {
217 hotspot_y
218 };
219
220 (hotspot_x as u16, hotspot_y as u16)
221}
222
223#[cfg(test)]
224mod tests {
225 use bevy_app::App;
226 use bevy_asset::RenderAssetUsages;
227 use bevy_image::Image;
228 use bevy_math::Rect;
229 use bevy_math::Vec2;
230 use wgpu_types::{Extent3d, TextureDimension};
231
232 use super::*;
233
234 fn create_image_rgba8(data: &[u8]) -> Image {
235 Image::new(
236 Extent3d {
237 width: 3,
238 height: 3,
239 depth_or_array_layers: 1,
240 },
241 TextureDimension::D2,
242 data.to_vec(),
243 TextureFormat::Rgba8UnormSrgb,
244 RenderAssetUsages::default(),
245 )
246 }
247
248 macro_rules! test_calculate_effective_rect {
249 ($name:ident, $use_texture_atlas:expr, $rect:expr, $expected_rect:expr, $expected_needs_sub_image:expr) => {
250 #[test]
251 fn $name() {
252 let mut app = App::new();
253 let mut texture_atlas_layout_assets = Assets::<TextureAtlasLayout>::default();
254
255 let layout = TextureAtlasLayout::from_grid(UVec2::new(3, 3), 1, 1, None, None);
259 let layout_handle = texture_atlas_layout_assets.add(layout);
260
261 app.insert_resource(texture_atlas_layout_assets);
262
263 let texture_atlases = app
264 .world()
265 .get_resource::<Assets<TextureAtlasLayout>>()
266 .unwrap();
267
268 let image = create_image_rgba8(&[0; 3 * 3 * 4]); let texture_atlas = if $use_texture_atlas {
271 Some(TextureAtlas::from(layout_handle))
272 } else {
273 None
274 };
275
276 let rect = $rect;
277
278 let (result_rect, needs_sub_image) =
279 calculate_effective_rect(&texture_atlases, &image, &texture_atlas, &rect);
280
281 assert_eq!(result_rect, $expected_rect);
282 assert_eq!(needs_sub_image, $expected_needs_sub_image);
283 }
284 };
285 }
286
287 test_calculate_effective_rect!(
288 no_texture_atlas_no_rect,
289 false,
290 None,
291 Rect {
292 min: Vec2::ZERO,
293 max: Vec2::new(3.0, 3.0)
294 },
295 false
296 );
297
298 test_calculate_effective_rect!(
299 no_texture_atlas_with_partial_rect,
300 false,
301 Some(URect {
302 min: UVec2::new(1, 1),
303 max: UVec2::new(3, 3)
304 }),
305 Rect {
306 min: Vec2::new(1.0, 1.0),
307 max: Vec2::new(3.0, 3.0)
308 },
309 true
310 );
311
312 test_calculate_effective_rect!(
313 no_texture_atlas_with_full_rect,
314 false,
315 Some(URect {
316 min: UVec2::ZERO,
317 max: UVec2::new(3, 3)
318 }),
319 Rect {
320 min: Vec2::ZERO,
321 max: Vec2::new(3.0, 3.0)
322 },
323 false
324 );
325
326 test_calculate_effective_rect!(
327 texture_atlas_no_rect,
328 true,
329 None,
330 Rect {
331 min: Vec2::ZERO,
332 max: Vec2::new(3.0, 3.0)
333 },
334 true );
336
337 test_calculate_effective_rect!(
338 texture_atlas_rect,
339 true,
340 Some(URect {
341 min: UVec2::ZERO,
342 max: UVec2::new(3, 3)
343 }),
344 Rect {
345 min: Vec2::new(0.0, 0.0),
346 max: Vec2::new(3.0, 3.0)
347 },
348 true );
350
351 fn create_image_rgba32float(data: &[u8]) -> Image {
352 let float_data: Vec<f32> = data
353 .chunks(4)
354 .flat_map(|chunk| {
355 chunk
356 .iter()
357 .map(|&x| x as f32 / 255.0) .collect::<Vec<f32>>()
359 })
360 .collect();
361
362 Image::new(
363 Extent3d {
364 width: 3,
365 height: 3,
366 depth_or_array_layers: 1,
367 },
368 TextureDimension::D2,
369 bytemuck::cast_slice(&float_data).to_vec(),
370 TextureFormat::Rgba32Float,
371 RenderAssetUsages::default(),
372 )
373 }
374
375 macro_rules! test_extract_and_transform_rgba_pixels {
376 ($name:ident, $flip_x:expr, $flip_y:expr, $rect:expr, $expected:expr) => {
377 #[test]
378 fn $name() {
379 let image_data: &[u8] = &[
380 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, 0, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 128, 128, 128, 255, 0, 0, 0, 255, ];
393
394 {
396 let image = create_image_rgba8(image_data);
397 let rect = $rect;
398 let result = extract_and_transform_rgba_pixels(&image, $flip_x, $flip_y, rect);
399 assert_eq!(result, Some($expected.to_vec()));
400 }
401
402 {
404 let image = create_image_rgba32float(image_data);
405 let rect = $rect;
406 let result = extract_and_transform_rgba_pixels(&image, $flip_x, $flip_y, rect);
407 assert_eq!(result, Some($expected.to_vec()));
408 }
409 }
410 };
411 }
412
413 test_extract_and_transform_rgba_pixels!(
414 no_flip_full_image,
415 false,
416 false,
417 Rect {
418 min: Vec2::ZERO,
419 max: Vec2::new(3.0, 3.0)
420 },
421 [
422 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, 0, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 128, 128, 128, 255, 0, 0, 0, 255, ]
435 );
436
437 test_extract_and_transform_rgba_pixels!(
438 flip_x_full_image,
439 true,
440 false,
441 Rect {
442 min: Vec2::ZERO,
443 max: Vec2::new(3.0, 3.0)
444 },
445 [
446 0, 0, 255, 255, 0, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 0, 0, 0, 255, 128, 128, 128, 255, 255, 255, 255, 255, ]
459 );
460
461 test_extract_and_transform_rgba_pixels!(
462 flip_y_full_image,
463 false,
464 true,
465 Rect {
466 min: Vec2::ZERO,
467 max: Vec2::new(3.0, 3.0)
468 },
469 [
470 255, 255, 255, 255, 128, 128, 128, 255, 0, 0, 0, 255, 255, 255, 0, 255, 0, 255, 255, 255, 255, 0, 255, 255, 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, ]
483 );
484
485 test_extract_and_transform_rgba_pixels!(
486 flip_both_full_image,
487 true,
488 true,
489 Rect {
490 min: Vec2::ZERO,
491 max: Vec2::new(3.0, 3.0)
492 },
493 [
494 0, 0, 0, 255, 128, 128, 128, 255, 255, 255, 255, 255, 255, 0, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 0, 0, 255, 255, 0, 255, 0, 255, 255, 0, 0, 255, ]
507 );
508
509 test_extract_and_transform_rgba_pixels!(
510 no_flip_rect,
511 false,
512 false,
513 Rect {
514 min: Vec2::new(1.0, 1.0),
515 max: Vec2::new(3.0, 3.0)
516 },
517 [
518 0, 255, 255, 255, 255, 0, 255, 255, 128, 128, 128, 255, 0, 0, 0, 255, ]
526 );
527
528 test_extract_and_transform_rgba_pixels!(
529 flip_x_rect,
530 true,
531 false,
532 Rect {
533 min: Vec2::new(1.0, 1.0),
534 max: Vec2::new(3.0, 3.0)
535 },
536 [
537 255, 0, 255, 255, 0, 255, 255, 255, 0, 0, 0, 255, 128, 128, 128, 255, ]
544 );
545
546 test_extract_and_transform_rgba_pixels!(
547 flip_y_rect,
548 false,
549 true,
550 Rect {
551 min: Vec2::new(1.0, 1.0),
552 max: Vec2::new(3.0, 3.0)
553 },
554 [
555 128, 128, 128, 255, 0, 0, 0, 255, 0, 255, 255, 255, 255, 0, 255, 255, ]
562 );
563
564 test_extract_and_transform_rgba_pixels!(
565 flip_both_rect,
566 true,
567 true,
568 Rect {
569 min: Vec2::new(1.0, 1.0),
570 max: Vec2::new(3.0, 3.0)
571 },
572 [
573 0, 0, 0, 255, 128, 128, 128, 255, 255, 0, 255, 255, 0, 255, 255, 255, ]
580 );
581
582 #[test]
583 fn test_transform_hotspot() {
584 fn test(hotspot: (u16, u16), flip_x: bool, flip_y: bool, rect: Rect, expected: (u16, u16)) {
585 let transformed = transform_hotspot(hotspot, flip_x, flip_y, rect);
586 assert_eq!(transformed, expected);
587
588 let transformed = transform_hotspot(transformed, flip_x, flip_y, rect);
591 assert_eq!(transformed, hotspot);
592 }
593
594 let rect = Rect {
595 min: Vec2::ZERO,
596 max: Vec2::new(100.0, 200.0),
597 };
598
599 test((10, 20), false, false, rect, (10, 20)); test((10, 20), true, false, rect, (89, 20)); test((10, 20), false, true, rect, (10, 179)); test((10, 20), true, true, rect, (89, 179)); test((0, 0), true, true, rect, (99, 199)); }
605}