Skip to content

Commit e8cd12d

Browse files
authored
Automatically transform cursor hotspot user asks to flip cursor image (#17540)
# Objective - As discussed in #17276 (comment), we should transform the cursor's hotspot if the user is asking for the image to be flipped. - This becomes more important when a `scale` transform option exists. It's harder for users to transform the hotspot themselves when using `scale` because they'd need to look up the image to get its dimensions. Instead, we let Bevy handle the hotspot transforms and make the `hotspot` field the "original/source" hotspot. - Refs #17276. ## Solution - When the image needs to be transformed, also transform the hotspot. If the image does not need to be transformed (i.e. fast path), no hotspot transformation is applied. ## Testing - Ran the example: `cargo run --example custom_cursor_image --features=custom_cursor`. - Add unit tests for the hotspot transform function. - I also ran the example I have in my `bevy_cursor_kit` crate, which I think is a good illustration of the reason for this PR. - In the following videos, there is an arrow pointing up. The button hover event fires as I move the mouse over it. - When I press `Y`, the cursor flips. - In the first video, on `bevy@main` **before** this PR, notice how the hotspot is wrong after flipping and no longer hovering the button. The arrow head and hotspot are no longer synced. - In the second video, on the branch of **this** PR, notice how the hotspot gets flipped as soon as I press `Y` and the cursor arrow head is in the correct position on the screen and still hovering the button. Speaking back to the objective listed at the start: The user originally defined the _source_ hotspot for the arrow. Later, they decide they want to flip the cursor vertically: It's nice that Bevy can automatically flip the _source_ hotspot for them at the same time it flips the _source_ image. First video (main): https://github.com/user-attachments/assets/1955048c-2f85-4951-bfd6-f0e7cfef0cf8 Second video (this PR): https://github.com/user-attachments/assets/73cb9095-ecb5-4bfd-af5b-9f772e92bd16
1 parent dfac3b9 commit e8cd12d

File tree

2 files changed

+83
-4
lines changed

2 files changed

+83
-4
lines changed

crates/bevy_winit/src/cursor.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
use crate::{
99
custom_cursor::{
1010
calculate_effective_rect, extract_and_transform_rgba_pixels, extract_rgba_pixels,
11-
CustomCursorPlugin,
11+
transform_hotspot, CustomCursorPlugin,
1212
},
1313
state::{CustomCursorCache, CustomCursorCacheKey},
1414
WinitCustomCursor,
@@ -124,10 +124,13 @@ fn update_cursors(
124124
let (rect, needs_sub_image) =
125125
calculate_effective_rect(&texture_atlases, image, texture_atlas, rect);
126126

127-
let maybe_rgba = if *flip_x || *flip_y || needs_sub_image {
128-
extract_and_transform_rgba_pixels(image, *flip_x, *flip_y, rect)
127+
let (maybe_rgba, hotspot) = if *flip_x || *flip_y || needs_sub_image {
128+
(
129+
extract_and_transform_rgba_pixels(image, *flip_x, *flip_y, rect),
130+
transform_hotspot(*hotspot, *flip_x, *flip_y, rect),
131+
)
129132
} else {
130-
extract_rgba_pixels(image)
133+
(extract_rgba_pixels(image), *hotspot)
131134
};
132135

133136
let Some(rgba) = maybe_rgba else {

crates/bevy_winit/src/custom_cursor.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ pub struct CustomCursorImage {
1717
/// An optional texture atlas used to render the image.
1818
pub texture_atlas: Option<TextureAtlas>,
1919
/// Whether the image should be flipped along its x-axis.
20+
///
21+
/// If true, the cursor's `hotspot` automatically flips along with the
22+
/// image.
2023
pub flip_x: bool,
2124
/// Whether the image should be flipped along its y-axis.
25+
///
26+
/// If true, the cursor's `hotspot` automatically flips along with the
27+
/// image.
2228
pub flip_y: bool,
2329
/// An optional rectangle representing the region of the image to render,
2430
/// instead of rendering the full image. This is an easy one-off alternative
@@ -29,6 +35,10 @@ pub struct CustomCursorImage {
2935
pub rect: Option<URect>,
3036
/// X and Y coordinates of the hotspot in pixels. The hotspot must be within
3137
/// the image bounds.
38+
///
39+
/// If you are flipping the image using `flip_x` or `flip_y`, you don't need
40+
/// to adjust this field to account for the flip because it is adjusted
41+
/// automatically.
3242
pub hotspot: (u16, u16),
3343
}
3444

@@ -184,6 +194,28 @@ pub(crate) fn extract_and_transform_rgba_pixels(
184194
Some(sub_image_data)
185195
}
186196

197+
/// Transforms the `hotspot` coordinates based on whether the image is flipped
198+
/// or not. The `rect` is used to determine the image's dimensions.
199+
pub(crate) fn transform_hotspot(
200+
hotspot: (u16, u16),
201+
flip_x: bool,
202+
flip_y: bool,
203+
rect: Rect,
204+
) -> (u16, u16) {
205+
let hotspot_x = hotspot.0 as f32;
206+
let hotspot_y = hotspot.1 as f32;
207+
let (width, height) = (rect.width(), rect.height());
208+
209+
let hotspot_x = if flip_x { width - hotspot_x } else { hotspot_x };
210+
let hotspot_y = if flip_y {
211+
height - hotspot_y
212+
} else {
213+
hotspot_y
214+
};
215+
216+
(hotspot_x as u16, hotspot_y as u16)
217+
}
218+
187219
#[cfg(test)]
188220
mod tests {
189221
use bevy_app::App;
@@ -542,4 +574,48 @@ mod tests {
542574
0, 255, 255, 255, // Cyan
543575
]
544576
);
577+
578+
#[test]
579+
fn test_transform_hotspot_no_flip() {
580+
let hotspot = (10, 20);
581+
let rect = Rect {
582+
min: Vec2::ZERO,
583+
max: Vec2::new(100.0, 200.0),
584+
};
585+
let transformed = transform_hotspot(hotspot, false, false, rect);
586+
assert_eq!(transformed, (10, 20));
587+
}
588+
589+
#[test]
590+
fn test_transform_hotspot_flip_x() {
591+
let hotspot = (10, 20);
592+
let rect = Rect {
593+
min: Vec2::ZERO,
594+
max: Vec2::new(100.0, 200.0),
595+
};
596+
let transformed = transform_hotspot(hotspot, true, false, rect);
597+
assert_eq!(transformed, (90, 20));
598+
}
599+
600+
#[test]
601+
fn test_transform_hotspot_flip_y() {
602+
let hotspot = (10, 20);
603+
let rect = Rect {
604+
min: Vec2::ZERO,
605+
max: Vec2::new(100.0, 200.0),
606+
};
607+
let transformed = transform_hotspot(hotspot, false, true, rect);
608+
assert_eq!(transformed, (10, 180));
609+
}
610+
611+
#[test]
612+
fn test_transform_hotspot_flip_both() {
613+
let hotspot = (10, 20);
614+
let rect = Rect {
615+
min: Vec2::ZERO,
616+
max: Vec2::new(100.0, 200.0),
617+
};
618+
let transformed = transform_hotspot(hotspot, true, true, rect);
619+
assert_eq!(transformed, (90, 180));
620+
}
545621
}

0 commit comments

Comments
 (0)