1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
use crate::{
ComputedUiRenderTargetInfo, ContentSize, Measure, MeasureArgs, Node, NodeMeasure, ResolvedAxis,
VisualBox,
};
use bevy_asset::{AsAssetId, AssetId, Assets, Handle};
use bevy_color::Color;
use bevy_ecs::prelude::*;
use bevy_image::{prelude::*, TRANSPARENT_IMAGE_HANDLE};
use bevy_math::{Rect, UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_sprite::TextureSlicer;
use taffy::{MaybeMath, ResolveOrZero};
/// A UI Node that renders an image.
#[derive(Component, Clone, Debug, Reflect, FromTemplate)]
#[reflect(Component, Default, Debug, Clone)]
#[require(Node, ImageNodeSize)]
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.
#[template(built_in)]
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,
/// Which region of the UI node the image should be drawn within.
pub visual_box: VisualBox,
}
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,
visual_box: VisualBox::ContentBox,
}
}
}
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,
visual_box: VisualBox::ContentBox,
}
}
/// 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)
}
}
impl AsAssetId for ImageNode {
type Asset = Image;
fn as_asset_id(&self) -> AssetId<Self::Asset> {
self.image.id()
}
}
/// 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, PartialEq, Reflect)]
#[reflect(Clone, Default, PartialEq)]
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 const 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, Clone)]
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
#[inline]
pub const 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,
/// The region of the UI node containing the image
pub visual_box: VisualBox,
}
impl Measure for ImageMeasure {
fn measure(&mut self, measure_args: MeasureArgs) -> Vec2 {
let mut width = measure_args.resolve_width();
let mut height = measure_args.resolve_height();
let calc = |_, _| 0.;
let padding = measure_args.style.padding.resolve_or_zero(
taffy::Size {
width: width.effective,
height: height.effective,
},
calc,
);
let border = measure_args.style.border.resolve_or_zero(
taffy::Size {
width: width.effective,
height: height.effective,
},
calc,
);
let content_inset = Vec2::new(
padding.left + padding.right + border.left + border.right,
padding.top + padding.bottom + border.top + border.bottom,
);
if measure_args.style.box_sizing == taffy::style::BoxSizing::BorderBox {
if measure_args.known_width.is_none() {
width.min = width.min.map(|min| (min - content_inset.x).max(0.));
width.preferred = width
.preferred
.map(|preferred| (preferred - content_inset.x).max(0.));
width.max = width.max.map(|max| (max - content_inset.x).max(0.));
width.effective = width
.effective
.map(|effective| (effective - content_inset.x).max(0.));
}
if measure_args.known_height.is_none() {
height.min = height.min.map(|min| (min - content_inset.y).max(0.));
height.preferred = height
.preferred
.map(|preferred| (preferred - content_inset.y).max(0.));
height.max = height.max.map(|max| (max - content_inset.y).max(0.));
height.effective = height
.effective
.map(|effective| (effective - content_inset.y).max(0.));
}
}
let inset = match self.visual_box {
VisualBox::ContentBox => Vec2::ZERO,
VisualBox::PaddingBox => {
Vec2::new(padding.left + padding.right, padding.top + padding.bottom)
}
VisualBox::BorderBox => Vec2::new(content_inset.x, content_inset.y),
};
let width = ResolvedAxis {
min: width.min.map(|min| min + inset.x),
preferred: width.preferred.map(|preferred| preferred + inset.x),
max: width.max.map(|max| max + inset.x),
effective: width.effective.map(|effective| effective + inset.x),
};
let height = ResolvedAxis {
min: height.min.map(|min| min + inset.y),
preferred: height.preferred.map(|preferred| preferred + inset.y),
max: height.max.map(|max| max + inset.y),
effective: height.effective.map(|effective| effective + inset.y),
};
// Use aspect_ratio from style, fall back to inherent aspect ratio
let aspect_ratio = measure_args
.style
.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: width.effective,
height: height.effective,
}
.maybe_apply_aspect_ratio(Some(aspect_ratio));
(Vec2::new(
taffy_size
.width
.unwrap_or(self.size.x)
.maybe_clamp(width.min, width.max),
taffy_size
.height
.unwrap_or(self.size.y)
.maybe_clamp(height.min, height.max),
) - inset)
.max(Vec2::ZERO)
}
}
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(
textures: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlasLayout>>,
mut query: Query<
(
&mut ContentSize,
Ref<ImageNode>,
&mut ImageNodeSize,
Ref<ComputedUiRenderTargetInfo>,
),
UpdateImageFilter,
>,
) {
for (mut content_size, image, mut image_size, computed_target) in &mut query {
if !matches!(image.image_mode, NodeImageMode::Auto)
|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
{
if image.is_changed() {
// Remove any existing measure.
content_size.clear();
}
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
|| computed_target.is_changed()
|| content_size.is_added()
|| image.is_changed()
{
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() * computed_target.scale_factor(),
visual_box: image.visual_box,
}));
}
}
}
}