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#[derive(Component, Clone, Debug, Reflect)]
13#[reflect(Component, Default, Debug, Clone)]
14#[require(Node, ImageNodeSize, ContentSize)]
15pub struct ImageNode {
16 pub color: Color,
21 pub image: Handle<Image>,
25 pub texture_atlas: Option<TextureAtlas>,
27 pub flip_x: bool,
29 pub flip_y: bool,
31 pub rect: Option<Rect>,
37 pub image_mode: NodeImageMode,
39}
40
41impl Default for ImageNode {
42 fn default() -> Self {
50 ImageNode {
51 color: Color::WHITE,
54 texture_atlas: None,
55 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 pub fn new(texture: Handle<Image>) -> Self {
68 Self {
69 image: texture,
70 color: Color::WHITE,
71 ..Default::default()
72 }
73 }
74
75 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 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 #[must_use]
101 pub const fn with_color(mut self, color: Color) -> Self {
102 self.color = color;
103 self
104 }
105
106 #[must_use]
108 pub const fn with_flip_x(mut self) -> Self {
109 self.flip_x = true;
110 self
111 }
112
113 #[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#[derive(Default, Debug, Clone, PartialEq, Reflect)]
149#[reflect(Clone, Default, PartialEq)]
150pub enum NodeImageMode {
151 #[default]
153 Auto,
154 Stretch,
156 Sliced(TextureSlicer),
158 Tiled {
160 tile_x: bool,
162 tile_y: bool,
164 stretch_value: f32,
167 },
168}
169
170impl NodeImageMode {
171 #[inline]
173 pub const fn uses_slices(&self) -> bool {
174 matches!(
175 self,
176 NodeImageMode::Sliced(..) | NodeImageMode::Tiled { .. }
177 )
178 }
179}
180
181#[derive(Component, Debug, Copy, Clone, Default, Reflect)]
185#[reflect(Component, Default, Debug, Clone)]
186pub struct ImageNodeSize {
187 size: UVec2,
191}
192
193impl ImageNodeSize {
194 #[inline]
196 pub const fn size(&self) -> UVec2 {
197 self.size
198 }
199}
200
201#[derive(Clone)]
202pub struct ImageMeasure {
204 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 let parent_width = available_width.into_option();
220 let parent_height = available_height.into_option();
221
222 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 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 let aspect_ratio = s_aspect_ratio.unwrap_or_else(|| self.size.x / self.size.y);
242
243 let taffy_size = taffy::Size { width, height }.maybe_apply_aspect_ratio(Some(aspect_ratio));
246
247 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
263pub 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 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 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 size: size.as_vec2() * computed_target.scale_factor(),
303 }));
304 }
305 }
306 }
307}