Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ui/src/widget/image.rs
6599 views
1
use crate::{ComputedUiRenderTargetInfo, ContentSize, Measure, MeasureArgs, Node, NodeMeasure};
2
use bevy_asset::{AsAssetId, AssetId, Assets, Handle};
3
use bevy_color::Color;
4
use bevy_ecs::prelude::*;
5
use bevy_image::{prelude::*, TRANSPARENT_IMAGE_HANDLE};
6
use bevy_math::{Rect, UVec2, Vec2};
7
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
8
use bevy_sprite::TextureSlicer;
9
use 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)]
15
pub 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
41
impl 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
65
impl 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
133
impl From<Handle<Image>> for ImageNode {
134
fn from(texture: Handle<Image>) -> Self {
135
Self::new(texture)
136
}
137
}
138
139
impl 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)]
150
pub 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
170
impl 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)]
186
pub 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
193
impl 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
203
pub struct ImageMeasure {
204
/// The size of the image's texture
205
pub size: Vec2,
206
}
207
208
impl 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
261
type UpdateImageFilter = (With<Node>, Without<crate::prelude::Text>);
262
263
/// Updates content size of the node based on the image provided
264
pub 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
}
308
309