Skip to content

Commit 3dc50b8

Browse files
ickshonpecart
authored andcommitted
Improved UiImage and Sprite scaling and slicing APIs (#16088)
1. UI texture slicing chops and scales an image to fit the size of a node and isn't meant to place any constraints on the size of the node itself, but because the required components changes required `ImageSize` and `ContentSize` for nodes with `UiImage`, texture sliced nodes are laid out using an `ImageMeasure`. 2. In 0.14 users could spawn a `(UiImage, NodeBundle)` which would display an image stretched to fill the UI node's bounds ignoring the image's instrinsic size. Now that `UiImage` requires `ContentSize`, there's no option to display an image without its size placing constrains on the UI layout (unless you force the `Node` to a fixed size, but that's not a solution). 3. It's desirable that the `Sprite` and `UiImage` share similar APIs. Fixes #16109 * Remove the `Component` impl from `ImageScaleMode`. * Add a `Stretch` variant to `ImageScaleMode`. * Add a field `scale_mode: ImageScaleMode` to `Sprite`. * Add a field `mode: UiImageMode` to `UiImage`. * Add an enum `UiImageMode` similar to `ImageScaleMode` but with additional UI specific variants. * Remove the queries for `ImageScaleMode` from Sprite and UI extraction, and refer to the new fields instead. * Change `ui_layout_system` to update measure funcs on any change to `ContentSize`s to enable manual clearing without removing the component. * Don't add a measure unless `UiImageMode::Auto` is set in `update_image_content_size_system`. Mutably deref the `Mut<ContentSize>` if the `UiImage` is changed to force removal of any existing measure func. Remove all the constraints from the ui_texture_slice example: ```rust //! This example illustrates how to create buttons with their textures sliced //! and kept in proportion instead of being stretched by the button dimensions use bevy::{ color::palettes::css::{GOLD, ORANGE}, prelude::*, winit::WinitSettings, }; fn main() { App::new() .add_plugins(DefaultPlugins) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) .add_systems(Startup, setup) .add_systems(Update, button_system) .run(); } fn button_system( mut interaction_query: Query< (&Interaction, &Children, &mut UiImage), (Changed<Interaction>, With<Button>), >, mut text_query: Query<&mut Text>, ) { for (interaction, children, mut image) in &mut interaction_query { let mut text = text_query.get_mut(children[0]).unwrap(); match *interaction { Interaction::Pressed => { **text = "Press".to_string(); image.color = GOLD.into(); } Interaction::Hovered => { **text = "Hover".to_string(); image.color = ORANGE.into(); } Interaction::None => { **text = "Button".to_string(); image.color = Color::WHITE; } } } } fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { let image = asset_server.load("textures/fantasy_ui_borders/panel-border-010.png"); let slicer = TextureSlicer { border: BorderRect::square(22.0), center_scale_mode: SliceScaleMode::Stretch, sides_scale_mode: SliceScaleMode::Stretch, max_corner_scale: 1.0, }; // ui camera commands.spawn(Camera2d); commands .spawn(Node { width: Val::Percent(100.0), height: Val::Percent(100.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, ..default() }) .with_children(|parent| { for [w, h] in [[150.0, 150.0], [300.0, 150.0], [150.0, 300.0]] { parent .spawn(( Button, Node { // width: Val::Px(w), // height: Val::Px(h), // horizontally center child text justify_content: JustifyContent::Center, // vertically center child text align_items: AlignItems::Center, margin: UiRect::all(Val::Px(20.0)), ..default() }, UiImage::new(image.clone()), ImageScaleMode::Sliced(slicer.clone()), )) .with_children(|parent| { // parent.spawn(( // Text::new("Button"), // TextFont { // font: asset_server.load("fonts/FiraSans-Bold.ttf"), // font_size: 33.0, // ..default() // }, // TextColor(Color::srgb(0.9, 0.9, 0.9)), // )); }); } }); } ``` This should result in a blank window, since without any constraints the texture slice image nodes should be zero-sized. But in main the image nodes are given the size of the underlying unsliced source image `textures/fantasy_ui_borders/panel-border-010.png`: <img width="321" alt="slicing" src="https://github.com/user-attachments/assets/cbd74c9c-14cd-4b4d-93c6-7c0152bb05ee"> For this PR need to change the lines: ``` UiImage::new(image.clone()), ImageScaleMode::Sliced(slicer.clone()), ``` to ``` UiImage::new(image.clone()).with_mode(UiImageMode::Sliced(slicer.clone()), ``` and then nothing should be rendered, as desired. --------- Co-authored-by: Carter Anderson <mcanders1@gmail.com>
1 parent 10433f6 commit 3dc50b8

File tree

18 files changed

+182
-111
lines changed

18 files changed

+182
-111
lines changed

crates/bevy_sprite/src/bundle.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ use bevy_render::{
88
use bevy_transform::components::{GlobalTransform, Transform};
99

1010
/// A [`Bundle`] of components for drawing a single sprite from an image.
11-
///
12-
/// # Extra behaviors
13-
///
14-
/// You may add one or both of the following components to enable additional behaviors:
15-
/// - [`ImageScaleMode`](crate::ImageScaleMode) to enable either slicing or tiling of the texture
16-
/// - [`TextureAtlas`](crate::TextureAtlas) to draw a specific section of the texture
1711
#[derive(Bundle, Clone, Debug, Default)]
1812
#[deprecated(
1913
since = "0.15.0",

crates/bevy_sprite/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub mod prelude {
3030
#[doc(hidden)]
3131
pub use crate::{
3232
bundle::SpriteBundle,
33-
sprite::{ImageScaleMode, Sprite},
33+
sprite::{Sprite, SpriteImageMode},
3434
texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources},
3535
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
3636
ColorMaterial, ColorMesh2dBundle, MeshMaterial2d, TextureAtlasBuilder,
@@ -106,7 +106,7 @@ impl Plugin for SpritePlugin {
106106
app.init_asset::<TextureAtlasLayout>()
107107
.register_asset_reflect::<TextureAtlasLayout>()
108108
.register_type::<Sprite>()
109-
.register_type::<ImageScaleMode>()
109+
.register_type::<SpriteImageMode>()
110110
.register_type::<TextureSlicer>()
111111
.register_type::<Anchor>()
112112
.register_type::<TextureAtlas>()

crates/bevy_sprite/src/sprite.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ pub struct Sprite {
3434
pub rect: Option<Rect>,
3535
/// [`Anchor`] point of the sprite in the world
3636
pub anchor: Anchor,
37+
/// How the sprite's image will be scaled.
38+
pub image_mode: SpriteImageMode,
3739
}
3840

3941
impl Sprite {
@@ -79,9 +81,12 @@ impl From<Handle<Image>> for Sprite {
7981
}
8082

8183
/// Controls how the image is altered when scaled.
82-
#[derive(Component, Debug, Clone, Reflect)]
83-
#[reflect(Component, Debug)]
84-
pub enum ImageScaleMode {
84+
#[derive(Default, Debug, Clone, Reflect, PartialEq)]
85+
#[reflect(Debug)]
86+
pub enum SpriteImageMode {
87+
/// The sprite will take on the size of the image by default, and will be stretched or shrunk if [`Sprite::custom_size`] is set.
88+
#[default]
89+
Auto,
8590
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize
8691
Sliced(TextureSlicer),
8792
/// The texture will be repeated if stretched beyond `stretched_value`
@@ -96,6 +101,17 @@ pub enum ImageScaleMode {
96101
},
97102
}
98103

104+
impl SpriteImageMode {
105+
/// Returns true if this mode uses slices internally ([`SpriteImageMode::Sliced`] or [`SpriteImageMode::Tiled`])
106+
#[inline]
107+
pub fn uses_slices(&self) -> bool {
108+
matches!(
109+
self,
110+
SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. }
111+
)
112+
}
113+
}
114+
99115
/// How a sprite is positioned relative to its [`Transform`].
100116
/// It defaults to `Anchor::Center`.
101117
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]

crates/bevy_sprite/src/texture_slice/computed_slices.rs

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{ExtractedSprite, ImageScaleMode, Sprite, TextureAtlasLayout};
1+
use crate::{ExtractedSprite, Sprite, SpriteImageMode, TextureAtlasLayout};
22

33
use super::TextureSlice;
44
use bevy_asset::{AssetEvent, Assets};
@@ -8,7 +8,7 @@ use bevy_render::texture::Image;
88
use bevy_transform::prelude::*;
99
use bevy_utils::HashSet;
1010

11-
/// Component storing texture slices for sprite entities with a [`ImageScaleMode`]
11+
/// Component storing texture slices for tiled or sliced sprite entities
1212
///
1313
/// This component is automatically inserted and updated
1414
#[derive(Debug, Clone, Component)]
@@ -69,24 +69,19 @@ impl ComputedTextureSlices {
6969
}
7070
}
7171

72-
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
72+
/// Generates sprite slices for a [`Sprite`] with [`SpriteImageMode::Sliced`] or [`SpriteImageMode::Sliced`]. The slices
7373
/// will be computed according to the `image_handle` dimensions or the sprite rect.
7474
///
7575
/// Returns `None` if the image asset is not loaded
7676
///
7777
/// # Arguments
7878
///
79-
/// * `sprite` - The sprite component, will be used to find the draw area size
80-
/// * `scale_mode` - The image scaling component
81-
/// * `image_handle` - The texture to slice or tile
79+
/// * `sprite` - The sprite component with the image handle and image mode
8280
/// * `images` - The image assets, use to retrieve the image dimensions
83-
/// * `atlas` - Optional texture atlas, if set the slicing will happen on the matching sub section
84-
/// of the texture
8581
/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect
8682
#[must_use]
8783
fn compute_sprite_slices(
8884
sprite: &Sprite,
89-
scale_mode: &ImageScaleMode,
9085
images: &Assets<Image>,
9186
atlas_layouts: &Assets<TextureAtlasLayout>,
9287
) -> Option<ComputedTextureSlices> {
@@ -111,9 +106,9 @@ fn compute_sprite_slices(
111106
(size, rect)
112107
}
113108
};
114-
let slices = match scale_mode {
115-
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
116-
ImageScaleMode::Tiled {
109+
let slices = match &sprite.image_mode {
110+
SpriteImageMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
111+
SpriteImageMode::Tiled {
117112
tile_x,
118113
tile_y,
119114
stretch_value,
@@ -125,18 +120,21 @@ fn compute_sprite_slices(
125120
};
126121
slice.tiled(*stretch_value, (*tile_x, *tile_y))
127122
}
123+
SpriteImageMode::Auto => {
124+
unreachable!("Slices should not be computed for SpriteImageMode::Stretch")
125+
}
128126
};
129127
Some(ComputedTextureSlices(slices))
130128
}
131129

132130
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
133-
/// on matching sprite entities with a [`ImageScaleMode`] component
131+
/// on sprite entities with a matching [`SpriteImageMode`]
134132
pub(crate) fn compute_slices_on_asset_event(
135133
mut commands: Commands,
136134
mut events: EventReader<AssetEvent<Image>>,
137135
images: Res<Assets<Image>>,
138136
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
139-
sprites: Query<(Entity, &ImageScaleMode, &Sprite)>,
137+
sprites: Query<(Entity, &Sprite)>,
140138
) {
141139
// We store the asset ids of added/modified image assets
142140
let added_handles: HashSet<_> = events
@@ -150,29 +148,31 @@ pub(crate) fn compute_slices_on_asset_event(
150148
return;
151149
}
152150
// We recompute the sprite slices for sprite entities with a matching asset handle id
153-
for (entity, scale_mode, sprite) in &sprites {
151+
for (entity, sprite) in &sprites {
152+
if !sprite.image_mode.uses_slices() {
153+
continue;
154+
}
154155
if !added_handles.contains(&sprite.image.id()) {
155156
continue;
156157
}
157-
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, &images, &atlas_layouts) {
158+
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
158159
commands.entity(entity).insert(slices);
159160
}
160161
}
161162
}
162163

163-
/// System reacting to changes on relevant sprite bundle components to compute the sprite slices
164-
/// on matching sprite entities with a [`ImageScaleMode`] component
164+
/// System reacting to changes on the [`Sprite`] component to compute the sprite slices
165165
pub(crate) fn compute_slices_on_sprite_change(
166166
mut commands: Commands,
167167
images: Res<Assets<Image>>,
168168
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
169-
changed_sprites: Query<
170-
(Entity, &ImageScaleMode, &Sprite),
171-
Or<(Changed<ImageScaleMode>, Changed<Sprite>)>,
172-
>,
169+
changed_sprites: Query<(Entity, &Sprite), Changed<Sprite>>,
173170
) {
174-
for (entity, scale_mode, sprite) in &changed_sprites {
175-
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, &images, &atlas_layouts) {
171+
for (entity, sprite) in &changed_sprites {
172+
if !sprite.image_mode.uses_slices() {
173+
continue;
174+
}
175+
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
176176
commands.entity(entity).insert(slices);
177177
}
178178
}

crates/bevy_sprite/src/texture_slice/slicer.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use bevy_reflect::Reflect;
1010
/// sections will be scaled or tiled.
1111
///
1212
/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures.
13-
#[derive(Debug, Clone, Reflect)]
13+
#[derive(Debug, Clone, Reflect, PartialEq)]
1414
pub struct TextureSlicer {
1515
/// The sprite borders, defining the 9 sections of the image
1616
pub border: BorderRect,
@@ -23,7 +23,7 @@ pub struct TextureSlicer {
2323
}
2424

2525
/// Defines how a texture slice scales when resized
26-
#[derive(Debug, Copy, Clone, Default, Reflect)]
26+
#[derive(Debug, Copy, Clone, Default, Reflect, PartialEq)]
2727
pub enum SliceScaleMode {
2828
/// The slice will be stretched to fit the area
2929
#[default]

crates/bevy_ui/src/layout/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ pub fn ui_layout_system(
221221
|| node.is_changed()
222222
|| content_size
223223
.as_ref()
224-
.map(|c| c.measure.is_some())
224+
.map(|c| c.is_changed() || c.measure.is_some())
225225
.unwrap_or(false)
226226
{
227227
let layout_context = LayoutContext::new(

crates/bevy_ui/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ pub mod prelude {
6262
Interaction, MaterialNode, UiMaterialPlugin, UiScale,
6363
},
6464
// `bevy_sprite` re-exports for texture slicing
65-
bevy_sprite::{BorderRect, ImageScaleMode, SliceScaleMode, TextureSlicer},
65+
bevy_sprite::{BorderRect, SliceScaleMode, SpriteImageMode, TextureSlicer},
6666
};
6767
}
6868

crates/bevy_ui/src/node_bundles.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,6 @@ pub struct NodeBundle {
5757
}
5858

5959
/// A UI node that is an image
60-
///
61-
/// # Extra behaviors
62-
///
63-
/// You may add one or both of the following components to enable additional behaviors:
64-
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
65-
/// - [`TextureAtlas`](bevy_sprite::TextureAtlas) to draw a specific section of the texture
6660
#[derive(Bundle, Debug, Default)]
6761
#[deprecated(
6862
since = "0.15.0",
@@ -110,12 +104,6 @@ pub struct ImageBundle {
110104
}
111105

112106
/// A UI node that is a button
113-
///
114-
/// # Extra behaviors
115-
///
116-
/// You may add one or both of the following components to enable additional behaviors:
117-
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
118-
/// - [`TextureAtlas`](bevy_sprite::TextureAtlas) to draw a specific section of the texture
119107
#[derive(Bundle, Clone, Debug)]
120108
#[deprecated(
121109
since = "0.15.0",

crates/bevy_ui/src/render/mod.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use bevy_render::{
4040
ExtractSchedule, Render,
4141
};
4242
use bevy_sprite::TextureAtlasLayout;
43-
use bevy_sprite::{BorderRect, ImageScaleMode, SpriteAssetEvents};
43+
use bevy_sprite::{BorderRect, SpriteAssetEvents};
4444

4545
use crate::{Display, Node};
4646
use bevy_text::{ComputedTextBlock, PositionedGlyph, TextColor, TextLayoutInfo};
@@ -308,18 +308,15 @@ pub fn extract_uinode_images(
308308
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
309309
default_ui_camera: Extract<DefaultUiCamera>,
310310
uinode_query: Extract<
311-
Query<
312-
(
313-
Entity,
314-
&ComputedNode,
315-
&GlobalTransform,
316-
&ViewVisibility,
317-
Option<&CalculatedClip>,
318-
Option<&TargetCamera>,
319-
&UiImage,
320-
),
321-
Without<ImageScaleMode>,
322-
>,
311+
Query<(
312+
Entity,
313+
&ComputedNode,
314+
&GlobalTransform,
315+
&ViewVisibility,
316+
Option<&CalculatedClip>,
317+
Option<&TargetCamera>,
318+
&UiImage,
319+
)>,
323320
>,
324321
mapping: Extract<Query<RenderEntity>>,
325322
) {
@@ -337,6 +334,7 @@ pub fn extract_uinode_images(
337334
if !view_visibility.get()
338335
|| image.color.is_fully_transparent()
339336
|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
337+
|| image.image_mode.uses_slices()
340338
{
341339
continue;
342340
}

crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use bevy_render::{
2424
Extract, ExtractSchedule, Render, RenderSet,
2525
};
2626
use bevy_sprite::{
27-
ImageScaleMode, SliceScaleMode, SpriteAssetEvents, TextureAtlasLayout, TextureSlicer,
27+
SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureAtlasLayout, TextureSlicer,
2828
};
2929
use bevy_transform::prelude::GlobalTransform;
3030
use bevy_utils::HashMap;
@@ -231,7 +231,7 @@ pub struct ExtractedUiTextureSlice {
231231
pub clip: Option<Rect>,
232232
pub camera_entity: Entity,
233233
pub color: LinearRgba,
234-
pub image_scale_mode: ImageScaleMode,
234+
pub image_scale_mode: SpriteImageMode,
235235
pub flip_x: bool,
236236
pub flip_y: bool,
237237
pub main_entity: MainEntity,
@@ -256,14 +256,11 @@ pub fn extract_ui_texture_slices(
256256
Option<&CalculatedClip>,
257257
Option<&TargetCamera>,
258258
&UiImage,
259-
&ImageScaleMode,
260259
)>,
261260
>,
262261
mapping: Extract<Query<RenderEntity>>,
263262
) {
264-
for (entity, uinode, transform, view_visibility, clip, camera, image, image_scale_mode) in
265-
&slicers_query
266-
{
263+
for (entity, uinode, transform, view_visibility, clip, camera, image) in &slicers_query {
267264
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
268265
else {
269266
continue;
@@ -273,6 +270,20 @@ pub fn extract_ui_texture_slices(
273270
continue;
274271
};
275272

273+
let image_scale_mode = match image.image_mode.clone() {
274+
NodeImageMode::Sliced(texture_slicer) => SpriteImageMode::Sliced(texture_slicer),
275+
NodeImageMode::Tiled {
276+
tile_x,
277+
tile_y,
278+
stretch_value,
279+
} => SpriteImageMode::Tiled {
280+
tile_x,
281+
tile_y,
282+
stretch_value,
283+
},
284+
_ => continue,
285+
};
286+
276287
// Skip invisible images
277288
if !view_visibility.get()
278289
|| image.color.is_fully_transparent()
@@ -311,7 +322,7 @@ pub fn extract_ui_texture_slices(
311322
clip: clip.map(|clip| clip.clip),
312323
image: image.image.id(),
313324
camera_entity,
314-
image_scale_mode: image_scale_mode.clone(),
325+
image_scale_mode,
315326
atlas_rect,
316327
flip_x: image.flip_x,
317328
flip_y: image.flip_y,
@@ -718,10 +729,10 @@ impl<P: PhaseItem> RenderCommand<P> for DrawSlicer {
718729
fn compute_texture_slices(
719730
image_size: Vec2,
720731
target_size: Vec2,
721-
image_scale_mode: &ImageScaleMode,
732+
image_scale_mode: &SpriteImageMode,
722733
) -> [[f32; 4]; 3] {
723734
match image_scale_mode {
724-
ImageScaleMode::Sliced(TextureSlicer {
735+
SpriteImageMode::Sliced(TextureSlicer {
725736
border: border_rect,
726737
center_scale_mode,
727738
sides_scale_mode,
@@ -774,7 +785,7 @@ fn compute_texture_slices(
774785
],
775786
]
776787
}
777-
ImageScaleMode::Tiled {
788+
SpriteImageMode::Tiled {
778789
tile_x,
779790
tile_y,
780791
stretch_value,
@@ -783,6 +794,9 @@ fn compute_texture_slices(
783794
let ry = compute_tiled_axis(*tile_y, image_size.y, target_size.y, *stretch_value);
784795
[[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]]
785796
}
797+
SpriteImageMode::Auto => {
798+
unreachable!("Slices should not be computed for ImageScaleMode::Stretch")
799+
}
786800
}
787801
}
788802

0 commit comments

Comments
 (0)