bevy_ui/widget/
image.rs

1use crate::{ComputedUiRenderTargetInfo, ContentSize, Measure, MeasureArgs, Node, NodeMeasure};
2use bevy_asset::{AsAssetId, AssetId, Assets, Handle};
3use bevy_color::Color;
4use bevy_ecs::prelude::*;
5use bevy_image::{prelude::*, TRANSPARENT_IMAGE_HANDLE};
6use bevy_math::{Rect, UVec2, Vec2};
7use bevy_reflect::{std_traits::ReflectDefault, Reflect};
8use bevy_sprite::TextureSlicer;
9use taffy::{MaybeMath, MaybeResolve};
10
11/// A UI Node that renders an image.
12#[derive(Component, Clone, Debug, Reflect)]
13#[reflect(Component, Default, Debug, Clone)]
14#[require(Node, ImageNodeSize, ContentSize)]
15pub struct ImageNode {
16    /// The tint color used to draw the image.
17    ///
18    /// This is multiplied by the color of each pixel in the image.
19    /// The field value defaults to solid white, which will pass the image through unmodified.
20    pub color: Color,
21    /// Handle to the texture.
22    ///
23    /// This defaults to a [`TRANSPARENT_IMAGE_HANDLE`], which points to a fully transparent 1x1 texture.
24    pub image: Handle<Image>,
25    /// The (optional) texture atlas used to render the image.
26    pub texture_atlas: Option<TextureAtlas>,
27    /// Whether the image should be flipped along its x-axis.
28    pub flip_x: bool,
29    /// Whether the image should be flipped along its y-axis.
30    pub flip_y: bool,
31    /// An optional rectangle representing the region of the image to render, instead of rendering
32    /// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
33    ///
34    /// When used with a [`TextureAtlas`], the rect
35    /// is offset by the atlas's minimal (top-left) corner position.
36    pub rect: Option<Rect>,
37    /// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space to allocate for the image.
38    pub image_mode: NodeImageMode,
39}
40
41impl Default for ImageNode {
42    /// A transparent 1x1 image with a solid white tint.
43    ///
44    /// # Warning
45    ///
46    /// This will be invisible by default.
47    /// To set this to a visible image, you need to set the `texture` field to a valid image handle,
48    /// or use [`Handle<Image>`]'s default 1x1 solid white texture (as is done in [`ImageNode::solid_color`]).
49    fn default() -> Self {
50        ImageNode {
51            // This should be white because the tint is multiplied with the image,
52            // so if you set an actual image with default tint you'd want its original colors
53            color: Color::WHITE,
54            texture_atlas: None,
55            // This texture needs to be transparent by default, to avoid covering the background color
56            image: TRANSPARENT_IMAGE_HANDLE,
57            flip_x: false,
58            flip_y: false,
59            rect: None,
60            image_mode: NodeImageMode::Auto,
61        }
62    }
63}
64
65impl ImageNode {
66    /// Create a new [`ImageNode`] with the given texture.
67    pub fn new(texture: Handle<Image>) -> Self {
68        Self {
69            image: texture,
70            color: Color::WHITE,
71            ..Default::default()
72        }
73    }
74
75    /// Create a solid color [`ImageNode`].
76    ///
77    /// This is primarily useful for debugging / mocking the extents of your image.
78    pub fn solid_color(color: Color) -> Self {
79        Self {
80            image: Handle::default(),
81            color,
82            flip_x: false,
83            flip_y: false,
84            texture_atlas: None,
85            rect: None,
86            image_mode: NodeImageMode::Auto,
87        }
88    }
89
90    /// Create a [`ImageNode`] from an image, with an associated texture atlas
91    pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
92        Self {
93            image,
94            texture_atlas: Some(atlas),
95            ..Default::default()
96        }
97    }
98
99    /// Set the color tint
100    #[must_use]
101    pub const fn with_color(mut self, color: Color) -> Self {
102        self.color = color;
103        self
104    }
105
106    /// Flip the image along its x-axis
107    #[must_use]
108    pub const fn with_flip_x(mut self) -> Self {
109        self.flip_x = true;
110        self
111    }
112
113    /// Flip the image along its y-axis
114    #[must_use]
115    pub const fn with_flip_y(mut self) -> Self {
116        self.flip_y = true;
117        self
118    }
119
120    #[must_use]
121    pub const fn with_rect(mut self, rect: Rect) -> Self {
122        self.rect = Some(rect);
123        self
124    }
125
126    #[must_use]
127    pub const fn with_mode(mut self, mode: NodeImageMode) -> Self {
128        self.image_mode = mode;
129        self
130    }
131}
132
133impl From<Handle<Image>> for ImageNode {
134    fn from(texture: Handle<Image>) -> Self {
135        Self::new(texture)
136    }
137}
138
139impl AsAssetId for ImageNode {
140    type Asset = Image;
141
142    fn as_asset_id(&self) -> AssetId<Self::Asset> {
143        self.image.id()
144    }
145}
146
147/// 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
148#[derive(Default, Debug, Clone, PartialEq, Reflect)]
149#[reflect(Clone, Default, PartialEq)]
150pub enum NodeImageMode {
151    /// The image will be sized automatically by taking the size of the source image and applying any layout constraints.
152    #[default]
153    Auto,
154    /// The image will be resized to match the size of the node. The image's original size and aspect ratio will be ignored.
155    Stretch,
156    /// The texture will be cut in 9 slices, keeping the texture in proportions on resize
157    Sliced(TextureSlicer),
158    /// The texture will be repeated if stretched beyond `stretched_value`
159    Tiled {
160        /// Should the image repeat horizontally
161        tile_x: bool,
162        /// Should the image repeat vertically
163        tile_y: bool,
164        /// The texture will repeat when the ratio between the *drawing dimensions* of texture and the
165        /// *original texture size* are above this value.
166        stretch_value: f32,
167    },
168}
169
170impl NodeImageMode {
171    /// Returns true if this mode uses slices internally ([`NodeImageMode::Sliced`] or [`NodeImageMode::Tiled`])
172    #[inline]
173    pub const fn uses_slices(&self) -> bool {
174        matches!(
175            self,
176            NodeImageMode::Sliced(..) | NodeImageMode::Tiled { .. }
177        )
178    }
179}
180
181/// The size of the image's texture
182///
183/// This component is updated automatically by [`update_image_content_size_system`]
184#[derive(Component, Debug, Copy, Clone, Default, Reflect)]
185#[reflect(Component, Default, Debug, Clone)]
186pub struct ImageNodeSize {
187    /// The size of the image's texture
188    ///
189    /// This field is updated automatically by [`update_image_content_size_system`]
190    size: UVec2,
191}
192
193impl ImageNodeSize {
194    /// The size of the image's texture
195    #[inline]
196    pub const fn size(&self) -> UVec2 {
197        self.size
198    }
199}
200
201#[derive(Clone)]
202/// Used to calculate the size of UI image nodes
203pub struct ImageMeasure {
204    /// The size of the image's texture
205    pub size: Vec2,
206}
207
208// NOOP function used to call into taffy API
209fn resolve_calc(_calc_ptr: *const (), _parent_size: f32) -> f32 {
210    0.0
211}
212
213impl Measure for ImageMeasure {
214    fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 {
215        let MeasureArgs {
216            width,
217            height,
218            available_width,
219            available_height,
220            ..
221        } = measure_args;
222
223        // Convert available width/height into an option
224        let parent_width = available_width.into_option();
225        let parent_height = available_height.into_option();
226
227        // Resolve styles
228        let s_aspect_ratio = style.aspect_ratio;
229        let s_width = style.size.width.maybe_resolve(parent_width, resolve_calc);
230        let s_min_width = style
231            .min_size
232            .width
233            .maybe_resolve(parent_width, resolve_calc);
234        let s_max_width = style
235            .max_size
236            .width
237            .maybe_resolve(parent_width, resolve_calc);
238        let s_height = style.size.height.maybe_resolve(parent_height, resolve_calc);
239        let s_min_height = style
240            .min_size
241            .height
242            .maybe_resolve(parent_height, resolve_calc);
243        let s_max_height = style
244            .max_size
245            .height
246            .maybe_resolve(parent_height, resolve_calc);
247
248        // Determine width and height from styles and known_sizes (if a size is available
249        // from any of these sources)
250        let width = width.or(s_width
251            .or(s_min_width)
252            .maybe_clamp(s_min_width, s_max_width));
253        let height = height.or(s_height
254            .or(s_min_height)
255            .maybe_clamp(s_min_height, s_max_height));
256
257        // Use aspect_ratio from style, fall back to inherent aspect ratio
258        let aspect_ratio = s_aspect_ratio.unwrap_or_else(|| self.size.x / self.size.y);
259
260        // Apply aspect ratio
261        // If only one of width or height was determined at this point, then the other is set beyond this point using the aspect ratio.
262        let taffy_size = taffy::Size { width, height }.maybe_apply_aspect_ratio(Some(aspect_ratio));
263
264        // Use computed sizes or fall back to image's inherent size
265        Vec2 {
266            x: taffy_size
267                .width
268                .unwrap_or(self.size.x)
269                .maybe_clamp(s_min_width, s_max_width),
270            y: taffy_size
271                .height
272                .unwrap_or(self.size.y)
273                .maybe_clamp(s_min_height, s_max_height),
274        }
275    }
276}
277
278type UpdateImageFilter = (With<Node>, Without<crate::prelude::Text>);
279
280/// Updates content size of the node based on the image provided
281pub fn update_image_content_size_system(
282    textures: Res<Assets<Image>>,
283    atlases: Res<Assets<TextureAtlasLayout>>,
284    mut query: Query<
285        (
286            &mut ContentSize,
287            Ref<ImageNode>,
288            &mut ImageNodeSize,
289            Ref<ComputedUiRenderTargetInfo>,
290        ),
291        UpdateImageFilter,
292    >,
293) {
294    for (mut content_size, image, mut image_size, computed_target) in &mut query {
295        if !matches!(image.image_mode, NodeImageMode::Auto)
296            || image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
297        {
298            if image.is_changed() {
299                // Mutably derefs, marking the `ContentSize` as changed ensuring `ui_layout_system` will remove the node's measure func if present.
300                content_size.measure = None;
301            }
302            continue;
303        }
304
305        if let Some(size) =
306            image
307                .rect
308                .map(|rect| rect.size().as_uvec2())
309                .or_else(|| match &image.texture_atlas {
310                    Some(atlas) => atlas.texture_rect(&atlases).map(|t| t.size()),
311                    None => textures.get(&image.image).map(Image::size),
312                })
313        {
314            // Update only if size or scale factor has changed to avoid needless layout calculations
315            if size != image_size.size || computed_target.is_changed() || content_size.is_added() {
316                image_size.size = size;
317                content_size.set(NodeMeasure::Image(ImageMeasure {
318                    // multiply the image size by the scale factor to get the physical size
319                    size: size.as_vec2() * computed_target.scale_factor(),
320                }));
321            }
322        }
323    }
324}