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
208impl Measure for ImageMeasure {
209    fn measure(&mut self, measure_args: MeasureArgs, style: &taffy::Style) -> Vec2 {
210        let MeasureArgs {
211            width,
212            height,
213            available_width,
214            available_height,
215            ..
216        } = measure_args;
217
218        // Convert available width/height into an option
219        let parent_width = available_width.into_option();
220        let parent_height = available_height.into_option();
221
222        // Resolve styles
223        let s_aspect_ratio = style.aspect_ratio;
224        let s_width = style.size.width.maybe_resolve(parent_width);
225        let s_min_width = style.min_size.width.maybe_resolve(parent_width);
226        let s_max_width = style.max_size.width.maybe_resolve(parent_width);
227        let s_height = style.size.height.maybe_resolve(parent_height);
228        let s_min_height = style.min_size.height.maybe_resolve(parent_height);
229        let s_max_height = style.max_size.height.maybe_resolve(parent_height);
230
231        // Determine width and height from styles and known_sizes (if a size is available
232        // from any of these sources)
233        let width = width.or(s_width
234            .or(s_min_width)
235            .maybe_clamp(s_min_width, s_max_width));
236        let height = height.or(s_height
237            .or(s_min_height)
238            .maybe_clamp(s_min_height, s_max_height));
239
240        // Use aspect_ratio from style, fall back to inherent aspect ratio
241        let aspect_ratio = s_aspect_ratio.unwrap_or_else(|| self.size.x / self.size.y);
242
243        // Apply aspect ratio
244        // If only one of width or height was determined at this point, then the other is set beyond this point using the aspect ratio.
245        let taffy_size = taffy::Size { width, height }.maybe_apply_aspect_ratio(Some(aspect_ratio));
246
247        // Use computed sizes or fall back to image's inherent size
248        Vec2 {
249            x: taffy_size
250                .width
251                .unwrap_or(self.size.x)
252                .maybe_clamp(s_min_width, s_max_width),
253            y: taffy_size
254                .height
255                .unwrap_or(self.size.y)
256                .maybe_clamp(s_min_height, s_max_height),
257        }
258    }
259}
260
261type UpdateImageFilter = (With<Node>, Without<crate::prelude::Text>);
262
263/// Updates content size of the node based on the image provided
264pub fn update_image_content_size_system(
265    textures: Res<Assets<Image>>,
266    atlases: Res<Assets<TextureAtlasLayout>>,
267    mut query: Query<
268        (
269            &mut ContentSize,
270            Ref<ImageNode>,
271            &mut ImageNodeSize,
272            Ref<ComputedUiRenderTargetInfo>,
273        ),
274        UpdateImageFilter,
275    >,
276) {
277    for (mut content_size, image, mut image_size, computed_target) in &mut query {
278        if !matches!(image.image_mode, NodeImageMode::Auto)
279            || image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
280        {
281            if image.is_changed() {
282                // Mutably derefs, marking the `ContentSize` as changed ensuring `ui_layout_system` will remove the node's measure func if present.
283                content_size.measure = None;
284            }
285            continue;
286        }
287
288        if let Some(size) =
289            image
290                .rect
291                .map(|rect| rect.size().as_uvec2())
292                .or_else(|| match &image.texture_atlas {
293                    Some(atlas) => atlas.texture_rect(&atlases).map(|t| t.size()),
294                    None => textures.get(&image.image).map(Image::size),
295                })
296        {
297            // Update only if size or scale factor has changed to avoid needless layout calculations
298            if size != image_size.size || computed_target.is_changed() || content_size.is_added() {
299                image_size.size = size;
300                content_size.set(NodeMeasure::Image(ImageMeasure {
301                    // multiply the image size by the scale factor to get the physical size
302                    size: size.as_vec2() * computed_target.scale_factor(),
303                }));
304            }
305        }
306    }
307}