Automatically transform cursor hotspot user asks to flip cursor image (#17540)
# Objective - As discussed in https://github.com/bevyengine/bevy/issues/17276#issuecomment-2611203714, 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
This commit is contained in:
parent
dfac3b9bfd
commit
e8cd12daf4
@ -8,7 +8,7 @@ use crate::{
|
||||
use crate::{
|
||||
custom_cursor::{
|
||||
calculate_effective_rect, extract_and_transform_rgba_pixels, extract_rgba_pixels,
|
||||
CustomCursorPlugin,
|
||||
transform_hotspot, CustomCursorPlugin,
|
||||
},
|
||||
state::{CustomCursorCache, CustomCursorCacheKey},
|
||||
WinitCustomCursor,
|
||||
@ -124,10 +124,13 @@ fn update_cursors(
|
||||
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)
|
||||
let (maybe_rgba, hotspot) = if *flip_x || *flip_y || needs_sub_image {
|
||||
(
|
||||
extract_and_transform_rgba_pixels(image, *flip_x, *flip_y, rect),
|
||||
transform_hotspot(*hotspot, *flip_x, *flip_y, rect),
|
||||
)
|
||||
} else {
|
||||
extract_rgba_pixels(image)
|
||||
(extract_rgba_pixels(image), *hotspot)
|
||||
};
|
||||
|
||||
let Some(rgba) = maybe_rgba else {
|
||||
|
@ -17,8 +17,14 @@ pub struct CustomCursorImage {
|
||||
/// An optional texture atlas used to render the image.
|
||||
pub texture_atlas: Option<TextureAtlas>,
|
||||
/// Whether the image should be flipped along its x-axis.
|
||||
///
|
||||
/// If true, the cursor's `hotspot` automatically flips along with the
|
||||
/// image.
|
||||
pub flip_x: bool,
|
||||
/// Whether the image should be flipped along its y-axis.
|
||||
///
|
||||
/// If true, the cursor's `hotspot` automatically flips along with the
|
||||
/// image.
|
||||
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
|
||||
@ -29,6 +35,10 @@ pub struct CustomCursorImage {
|
||||
pub rect: Option<URect>,
|
||||
/// X and Y coordinates of the hotspot in pixels. The hotspot must be within
|
||||
/// the image bounds.
|
||||
///
|
||||
/// If you are flipping the image using `flip_x` or `flip_y`, you don't need
|
||||
/// to adjust this field to account for the flip because it is adjusted
|
||||
/// automatically.
|
||||
pub hotspot: (u16, u16),
|
||||
}
|
||||
|
||||
@ -184,6 +194,28 @@ pub(crate) fn extract_and_transform_rgba_pixels(
|
||||
Some(sub_image_data)
|
||||
}
|
||||
|
||||
/// Transforms the `hotspot` coordinates based on whether the image is flipped
|
||||
/// or not. The `rect` is used to determine the image's dimensions.
|
||||
pub(crate) fn transform_hotspot(
|
||||
hotspot: (u16, u16),
|
||||
flip_x: bool,
|
||||
flip_y: bool,
|
||||
rect: Rect,
|
||||
) -> (u16, u16) {
|
||||
let hotspot_x = hotspot.0 as f32;
|
||||
let hotspot_y = hotspot.1 as f32;
|
||||
let (width, height) = (rect.width(), rect.height());
|
||||
|
||||
let hotspot_x = if flip_x { width - hotspot_x } else { hotspot_x };
|
||||
let hotspot_y = if flip_y {
|
||||
height - hotspot_y
|
||||
} else {
|
||||
hotspot_y
|
||||
};
|
||||
|
||||
(hotspot_x as u16, hotspot_y as u16)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_app::App;
|
||||
@ -542,4 +574,48 @@ mod tests {
|
||||
0, 255, 255, 255, // Cyan
|
||||
]
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn test_transform_hotspot_no_flip() {
|
||||
let hotspot = (10, 20);
|
||||
let rect = Rect {
|
||||
min: Vec2::ZERO,
|
||||
max: Vec2::new(100.0, 200.0),
|
||||
};
|
||||
let transformed = transform_hotspot(hotspot, false, false, rect);
|
||||
assert_eq!(transformed, (10, 20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_hotspot_flip_x() {
|
||||
let hotspot = (10, 20);
|
||||
let rect = Rect {
|
||||
min: Vec2::ZERO,
|
||||
max: Vec2::new(100.0, 200.0),
|
||||
};
|
||||
let transformed = transform_hotspot(hotspot, true, false, rect);
|
||||
assert_eq!(transformed, (90, 20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_hotspot_flip_y() {
|
||||
let hotspot = (10, 20);
|
||||
let rect = Rect {
|
||||
min: Vec2::ZERO,
|
||||
max: Vec2::new(100.0, 200.0),
|
||||
};
|
||||
let transformed = transform_hotspot(hotspot, false, true, rect);
|
||||
assert_eq!(transformed, (10, 180));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_hotspot_flip_both() {
|
||||
let hotspot = (10, 20);
|
||||
let rect = Rect {
|
||||
min: Vec2::ZERO,
|
||||
max: Vec2::new(100.0, 200.0),
|
||||
};
|
||||
let transformed = transform_hotspot(hotspot, true, true, rect);
|
||||
assert_eq!(transformed, (90, 180));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user