bevy/crates/bevy_ui/src/widget/image.rs
Marco Meijer d82bbd8370
fix: setting content size to rect size if image has a rect (#16457)
# Objective

When using a rect for a ui image, its content size is still equal to the
size of the full image instead of the size of the rect.

## Solution

Use the rect size if it is present.

## Testing

I tested it using all 4 possible combinations of having a rect and
texture atlas or not. See the showcase section.

---

## Showcase

<details>
  <summary>Click to view showcase</summary>

```rust
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
        .add_systems(Startup, create_ui)
        .run();
}

fn create_ui(
    mut commands: Commands,
    assets: Res<AssetServer>,
    mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
    mut ui_scale: ResMut<UiScale>,
) {
    let texture = assets.load("textures/fantasy_ui_borders/numbered_slices.png");
    let layout = TextureAtlasLayout::from_grid(UVec2::splat(16), 3, 3, None, None);
    let texture_atlas_layout = texture_atlas_layouts.add(layout);

    ui_scale.0 = 2.;

    commands.spawn(Camera2d::default());

    commands
        .spawn(Node {
            display: Display::Flex,
            align_items: AlignItems::Center,
            ..default()
        })
        .with_children(|parent| {
            // nothing
            parent.spawn(ImageNode::new(texture.clone()));

            // with rect
            parent.spawn(ImageNode::new(texture.clone()).with_rect(Rect::new(0., 0., 16., 16.)));

            // with rect and texture atlas
            parent.spawn(
                ImageNode::from_atlas_image(
                    texture.clone(),
                    TextureAtlas {
                        layout: texture_atlas_layout.clone(),
                        index: 1,
                    },
                )
                .with_rect(Rect::new(0., 0., 8., 8.)),
            );

            // with texture atlas
            parent.spawn(ImageNode::from_atlas_image(
                texture.clone(),
                TextureAtlas {
                    layout: texture_atlas_layout.clone(),
                    index: 2,
                },
            ));
        });
}
```

Before this change:
<img width="529" alt="Screenshot 2024-11-21 at 11 55 45"
src="https://github.com/user-attachments/assets/23196003-08ca-4049-8409-fe349bd5aa54">

After the change:
<img width="400" alt="Screenshot 2024-11-21 at 11 54 54"
src="https://github.com/user-attachments/assets/e2cd6ebf-859c-40a1-9fc4-43bb28b024e5">


</details>
2024-11-22 18:21:45 +00:00

307 lines
11 KiB
Rust

use crate::{ContentSize, Measure, MeasureArgs, Node, NodeMeasure, UiScale};
use bevy_asset::{Assets, Handle};
use bevy_color::Color;
use bevy_ecs::prelude::*;
use bevy_image::Image;
use bevy_math::{Rect, UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::TRANSPARENT_IMAGE_HANDLE;
use bevy_sprite::{TextureAtlas, TextureAtlasLayout, TextureSlicer};
use bevy_window::{PrimaryWindow, Window};
use taffy::{MaybeMath, MaybeResolve};
/// A UI Node that renders an image.
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, Default, Debug)]
#[require(Node, ImageNodeSize, ContentSize)]
pub struct ImageNode {
/// The tint color used to draw the image.
///
/// This is multiplied by the color of each pixel in the image.
/// The field value defaults to solid white, which will pass the image through unmodified.
pub color: Color,
/// Handle to the texture.
///
/// This defaults to a [`TRANSPARENT_IMAGE_HANDLE`], which points to a fully transparent 1x1 texture.
pub image: Handle<Image>,
/// The (optional) texture atlas used to render the image.
pub texture_atlas: Option<TextureAtlas>,
/// Whether the image should be flipped along its x-axis.
pub flip_x: bool,
/// Whether the image should be flipped along its y-axis.
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 to using a [`TextureAtlas`].
///
/// When used with a [`TextureAtlas`], the rect
/// is offset by the atlas's minimal (top-left) corner position.
pub rect: Option<Rect>,
/// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space to allocate for the image.
pub image_mode: NodeImageMode,
}
impl Default for ImageNode {
/// A transparent 1x1 image with a solid white tint.
///
/// # Warning
///
/// This will be invisible by default.
/// To set this to a visible image, you need to set the `texture` field to a valid image handle,
/// or use [`Handle<Image>`]'s default 1x1 solid white texture (as is done in [`ImageNode::solid_color`]).
fn default() -> Self {
ImageNode {
// This should be white because the tint is multiplied with the image,
// so if you set an actual image with default tint you'd want its original colors
color: Color::WHITE,
texture_atlas: None,
// This texture needs to be transparent by default, to avoid covering the background color
image: TRANSPARENT_IMAGE_HANDLE,
flip_x: false,
flip_y: false,
rect: None,
image_mode: NodeImageMode::Auto,
}
}
}
impl ImageNode {
/// Create a new [`ImageNode`] with the given texture.
pub fn new(texture: Handle<Image>) -> Self {
Self {
image: texture,
color: Color::WHITE,
..Default::default()
}
}
/// Create a solid color [`ImageNode`].
///
/// This is primarily useful for debugging / mocking the extents of your image.
pub fn solid_color(color: Color) -> Self {
Self {
image: Handle::default(),
color,
flip_x: false,
flip_y: false,
texture_atlas: None,
rect: None,
image_mode: NodeImageMode::Auto,
}
}
/// Create a [`ImageNode`] from an image, with an associated texture atlas
pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
Self {
image,
texture_atlas: Some(atlas),
..Default::default()
}
}
/// Set the color tint
#[must_use]
pub const fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
/// Flip the image along its x-axis
#[must_use]
pub const fn with_flip_x(mut self) -> Self {
self.flip_x = true;
self
}
/// Flip the image along its y-axis
#[must_use]
pub const fn with_flip_y(mut self) -> Self {
self.flip_y = true;
self
}
#[must_use]
pub const fn with_rect(mut self, rect: Rect) -> Self {
self.rect = Some(rect);
self
}
#[must_use]
pub const fn with_mode(mut self, mode: NodeImageMode) -> Self {
self.image_mode = mode;
self
}
}
impl From<Handle<Image>> for ImageNode {
fn from(texture: Handle<Image>) -> Self {
Self::new(texture)
}
}
/// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space in the layout for the image
#[derive(Default, Debug, Clone, Reflect)]
pub enum NodeImageMode {
/// The image will be sized automatically by taking the size of the source image and applying any layout constraints.
#[default]
Auto,
/// The image will be resized to match the size of the node. The image's original size and aspect ratio will be ignored.
Stretch,
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize
Sliced(TextureSlicer),
/// The texture will be repeated if stretched beyond `stretched_value`
Tiled {
/// Should the image repeat horizontally
tile_x: bool,
/// Should the image repeat vertically
tile_y: bool,
/// The texture will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* are above this value.
stretch_value: f32,
},
}
impl NodeImageMode {
/// Returns true if this mode uses slices internally ([`NodeImageMode::Sliced`] or [`NodeImageMode::Tiled`])
#[inline]
pub fn uses_slices(&self) -> bool {
matches!(
self,
NodeImageMode::Sliced(..) | NodeImageMode::Tiled { .. }
)
}
}
/// The size of the image's texture
///
/// This component is updated automatically by [`update_image_content_size_system`]
#[derive(Component, Debug, Copy, Clone, Default, Reflect)]
#[reflect(Component, Default, Debug)]
pub struct ImageNodeSize {
/// The size of the image's texture
///
/// This field is updated automatically by [`update_image_content_size_system`]
size: UVec2,
}
impl ImageNodeSize {
/// The size of the image's texture
pub fn size(&self) -> UVec2 {
self.size
}
}
#[derive(Clone)]
/// Used to calculate the size of UI image nodes
pub struct ImageMeasure {
/// The size of the image's texture
pub size: Vec2,
}
impl Measure for ImageMeasure {
fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 {
let MeasureArgs {
width,
height,
available_width,
available_height,
..
} = measure_args;
// Convert available width/height into an option
let parent_width = available_width.into_option();
let parent_height = available_height.into_option();
// Resolve styles
let s_aspect_ratio = style.aspect_ratio;
let s_width = style.size.width.maybe_resolve(parent_width);
let s_min_width = style.min_size.width.maybe_resolve(parent_width);
let s_max_width = style.max_size.width.maybe_resolve(parent_width);
let s_height = style.size.height.maybe_resolve(parent_height);
let s_min_height = style.min_size.height.maybe_resolve(parent_height);
let s_max_height = style.max_size.height.maybe_resolve(parent_height);
// Determine width and height from styles and known_sizes (if a size is available
// from any of these sources)
let width = width.or(s_width
.or(s_min_width)
.maybe_clamp(s_min_width, s_max_width));
let height = height.or(s_height
.or(s_min_height)
.maybe_clamp(s_min_height, s_max_height));
// Use aspect_ratio from style, fall back to inherent aspect ratio
let aspect_ratio = s_aspect_ratio.unwrap_or_else(|| self.size.x / self.size.y);
// Apply aspect ratio
// If only one of width or height was determined at this point, then the other is set beyond this point using the aspect ratio.
let taffy_size = taffy::Size { width, height }.maybe_apply_aspect_ratio(Some(aspect_ratio));
// Use computed sizes or fall back to image's inherent size
Vec2 {
x: taffy_size
.width
.unwrap_or(self.size.x)
.maybe_clamp(s_min_width, s_max_width),
y: taffy_size
.height
.unwrap_or(self.size.y)
.maybe_clamp(s_min_height, s_max_height),
}
}
}
type UpdateImageFilter = (With<Node>, Without<crate::prelude::Text>);
/// Updates content size of the node based on the image provided
pub fn update_image_content_size_system(
mut previous_combined_scale_factor: Local<f32>,
windows: Query<&Window, With<PrimaryWindow>>,
ui_scale: Res<UiScale>,
textures: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlasLayout>>,
mut query: Query<(&mut ContentSize, Ref<ImageNode>, &mut ImageNodeSize), UpdateImageFilter>,
) {
let combined_scale_factor = windows
.get_single()
.map(|window| window.resolution.scale_factor())
.unwrap_or(1.)
* ui_scale.0;
for (mut content_size, image, mut image_size) in &mut query {
if !matches!(image.image_mode, NodeImageMode::Auto)
|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
{
if image.is_changed() {
// Mutably derefs, marking the `ContentSize` as changed ensuring `ui_layout_system` will remove the node's measure func if present.
content_size.measure = None;
}
continue;
}
if let Some(size) =
image
.rect
.map(|rect| rect.size().as_uvec2())
.or_else(|| match &image.texture_atlas {
Some(atlas) => atlas.texture_rect(&atlases).map(|t| t.size()),
None => textures.get(&image.image).map(Image::size),
})
{
// Update only if size or scale factor has changed to avoid needless layout calculations
if size != image_size.size
|| combined_scale_factor != *previous_combined_scale_factor
|| content_size.is_added()
{
image_size.size = size;
content_size.set(NodeMeasure::Image(ImageMeasure {
// multiply the image size by the scale factor to get the physical size
size: size.as_vec2() * combined_scale_factor,
}));
}
}
}
*previous_combined_scale_factor = combined_scale_factor;
}