Skip to content

Commit

Permalink
Optional ImageScaleMode (#11780)
Browse files Browse the repository at this point in the history
> Follow up to #11600 and #10588 

@mockersf expressed some [valid
concerns](#11600 (comment))
about the current system this PR attempts to fix:

The `ComputedTextureSlices` reacts to asset change in both `bevy_sprite`
and `bevy_ui`, meaning that if the `ImageScaleMode` is inserted by
default in the bundles, we will iterate through most 2d items every time
an asset is updated.

# Solution

- `ImageScaleMode` only has two variants: `Sliced` and `Tiled`. I
removed the `Stretched` default
- `ImageScaleMode` is no longer part of any bundle, but the relevant
bundles explain that this additional component can be inserted

This way, the *absence* of `ImageScaleMode` means the image will be
stretched, and its *presence* will include the entity to the various
slicing systems

Optional components in bundles would make this more straigthfoward

# Additional work

Should I add new bundles with the `ImageScaleMode` component ?
  • Loading branch information
ManevilleF authored Feb 9, 2024
1 parent 0ebba27 commit e0c296e
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 83 deletions.
12 changes: 7 additions & 5 deletions crates/bevy_sprite/src/bundle.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{texture_atlas::TextureAtlas, ImageScaleMode, Sprite};
use crate::{Sprite, TextureAtlas};
use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle;
use bevy_render::{
Expand All @@ -8,12 +8,16 @@ use bevy_render::{
use bevy_transform::components::{GlobalTransform, Transform};

/// A [`Bundle`] of components for drawing a single sprite from an image.
///
/// # Extra behaviours
///
/// You may add the following components to enable additional behaviours
/// - [`ImageScaleMode`](crate::ImageScaleMode) to enable either slicing or tiling of the texture
/// - [`TextureAtlas`] to draw specific sections of a sprite sheet, (See [`SpriteSheetBundle`])
#[derive(Bundle, Clone, Default)]
pub struct SpriteBundle {
/// Specifies the rendering properties of the sprite, such as color tint and flip.
pub sprite: Sprite,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// The local transform of the sprite, relative to its parent.
pub transform: Transform,
/// The absolute transform of the sprite. This should generally not be written to directly.
Expand Down Expand Up @@ -41,8 +45,6 @@ pub struct SpriteBundle {
pub struct SpriteSheetBundle {
/// Specifies the rendering properties of the sprite, such as color tint and flip.
pub sprite: Sprite,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// The local transform of the sprite, relative to its parent.
pub transform: Transform,
/// The absolute transform of the sprite. This should generally not be written to directly.
Expand Down
7 changes: 2 additions & 5 deletions crates/bevy_sprite/src/sprite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,9 @@ pub struct Sprite {
}

/// Controls how the image is altered when scaled.
#[derive(Component, Debug, Default, Clone, Reflect)]
#[reflect(Component, Default)]
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component)]
pub enum ImageScaleMode {
/// The entire texture stretches when its dimensions change. This is the default option.
#[default]
Stretched,
/// 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`
Expand Down
13 changes: 4 additions & 9 deletions crates/bevy_sprite/src/texture_slice/computed_slices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bevy_render::texture::Image;
use bevy_transform::prelude::*;
use bevy_utils::HashSet;

/// Component storing texture slices for sprite entities with a tiled or sliced [`ImageScaleMode`]
/// Component storing texture slices for sprite entities with a [`ImageScaleMode`]
///
/// This component is automatically inserted and updated
#[derive(Debug, Clone, Component)]
Expand Down Expand Up @@ -62,27 +62,21 @@ impl ComputedTextureSlices {
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
/// will be computed according to the `image_handle` dimensions or the sprite rect.
///
/// Returns `None` if either:
/// - The scale mode is [`ImageScaleMode::Stretched`]
/// - The image asset is not loaded
/// Returns `None` if the image asset is not loaded
#[must_use]
fn compute_sprite_slices(
sprite: &Sprite,
scale_mode: &ImageScaleMode,
image_handle: &Handle<Image>,
images: &Assets<Image>,
) -> Option<ComputedTextureSlices> {
if let ImageScaleMode::Stretched = scale_mode {
return None;
}
let image_size = images.get(image_handle).map(|i| {
Vec2::new(
i.texture_descriptor.size.width as f32,
i.texture_descriptor.size.height as f32,
)
})?;
let slices = match scale_mode {
ImageScaleMode::Stretched => unreachable!(),
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(
sprite.rect.unwrap_or(Rect {
min: Vec2::ZERO,
Expand Down Expand Up @@ -110,7 +104,7 @@ fn compute_sprite_slices(
}

/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
/// on matching sprite entities
/// on matching sprite entities with a [`ImageScaleMode`] component
pub(crate) fn compute_slices_on_asset_event(
mut commands: Commands,
mut events: EventReader<AssetEvent<Image>>,
Expand Down Expand Up @@ -140,6 +134,7 @@ pub(crate) fn compute_slices_on_asset_event(
}

/// System reacting to changes on relevant sprite bundle components to compute the sprite slices
/// on matching sprite entities with a [`ImageScaleMode`] component
pub(crate) fn compute_slices_on_sprite_change(
mut commands: Commands,
images: Res<Assets<Image>>,
Expand Down
17 changes: 11 additions & 6 deletions crates/bevy_ui/src/node_bundles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use bevy_render::{
prelude::Color,
view::{InheritedVisibility, ViewVisibility, Visibility},
};
use bevy_sprite::{ImageScaleMode, TextureAtlas};
use bevy_sprite::TextureAtlas;
#[cfg(feature = "bevy_text")]
use bevy_text::{BreakLineOn, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle};
use bevy_transform::prelude::{GlobalTransform, Transform};
Expand Down Expand Up @@ -76,6 +76,11 @@ impl Default for NodeBundle {
}

/// A UI node that is an image
///
/// # Extra behaviours
///
/// You may add the following components to enable additional behaviours
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
#[derive(Bundle, Debug, Default)]
pub struct ImageBundle {
/// Describes the logical size of the node
Expand All @@ -95,8 +100,6 @@ pub struct ImageBundle {
///
/// This component is set automatically
pub image_size: UiImageSize,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// Whether this node should block interaction with lower nodes
pub focus_policy: FocusPolicy,
/// The transform of the node
Expand Down Expand Up @@ -288,6 +291,11 @@ where
}

/// A UI node that is a button
///
/// # Extra behaviours
///
/// You may add the following components to enable additional behaviours
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
#[derive(Bundle, Clone, Debug)]
pub struct ButtonBundle {
/// Describes the logical size of the node
Expand All @@ -309,8 +317,6 @@ pub struct ButtonBundle {
pub border_color: BorderColor,
/// The image of the node
pub image: UiImage,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// The transform of the node
///
/// This component is automatically managed by the UI layout system.
Expand Down Expand Up @@ -347,7 +353,6 @@ impl Default for ButtonBundle {
inherited_visibility: Default::default(),
view_visibility: Default::default(),
z_index: Default::default(),
scale_mode: ImageScaleMode::default(),
}
}
}
Expand Down
11 changes: 3 additions & 8 deletions crates/bevy_ui/src/texture_slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,14 @@ impl ComputedTextureSlices {
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
/// will be computed according to the `image_handle` dimensions or the sprite rect.
///
/// Returns `None` if either:
/// - The scale mode is [`ImageScaleMode::Stretched`]
/// - The image asset is not loaded
/// Returns `None` if the image asset is not loaded
#[must_use]
fn compute_texture_slices(
draw_area: Vec2,
scale_mode: &ImageScaleMode,
image_handle: &UiImage,
images: &Assets<Image>,
) -> Option<ComputedTextureSlices> {
if let ImageScaleMode::Stretched = scale_mode {
return None;
}
let image_size = images.get(&image_handle.texture).map(|i| {
Vec2::new(
i.texture_descriptor.size.width as f32,
Expand All @@ -101,7 +96,6 @@ fn compute_texture_slices(
max: image_size,
};
let slices = match scale_mode {
ImageScaleMode::Stretched => unreachable!(),
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)),
ImageScaleMode::Tiled {
tile_x,
Expand All @@ -120,7 +114,7 @@ fn compute_texture_slices(
}

/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
/// on matching sprite entities
/// on matching sprite entities with a [`ImageScaleMode`] component
pub(crate) fn compute_slices_on_asset_event(
mut commands: Commands,
mut events: EventReader<AssetEvent<Image>>,
Expand Down Expand Up @@ -157,6 +151,7 @@ pub(crate) fn compute_slices_on_asset_event(
}

/// System reacting to changes on relevant sprite bundle components to compute the sprite slices
/// on matching sprite entities with a [`ImageScaleMode`] component
pub(crate) fn compute_slices_on_image_change(
mut commands: Commands,
images: Res<Assets<Image>>,
Expand Down
60 changes: 28 additions & 32 deletions examples/2d/sprite_slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,89 +25,85 @@ fn spawn_sprites(
) {
let cases = [
// Reference sprite
(
"Original texture",
style.clone(),
Vec2::splat(100.0),
ImageScaleMode::default(),
),
("Original texture", style.clone(), Vec2::splat(100.0), None),
// Scaled regular sprite
(
"Stretched texture",
style.clone(),
Vec2::new(100.0, 200.0),
ImageScaleMode::default(),
None,
),
// Stretched Scaled sliced sprite
(
"Stretched and sliced",
style.clone(),
Vec2::new(100.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Stretch,
..default()
}),
})),
),
// Scaled sliced sprite
(
"Sliced and Tiled",
style.clone(),
Vec2::new(100.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.5 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
..default()
}),
})),
),
// Scaled sliced sprite horizontally
(
"Sliced and Tiled",
style.clone(),
Vec2::new(300.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.3 },
..default()
}),
})),
),
// Scaled sliced sprite horizontally with max scale
(
"Sliced and Tiled with corner constraint",
style,
Vec2::new(300.0, 200.0),
ImageScaleMode::Sliced(TextureSlicer {
Some(ImageScaleMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.1 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
max_corner_scale: 0.2,
}),
})),
),
];

for (label, text_style, size, scale_mode) in cases {
position.x += 0.5 * size.x;
commands
.spawn(SpriteBundle {
transform: Transform::from_translation(position),
texture: texture_handle.clone(),
sprite: Sprite {
custom_size: Some(size),
..default()
},
scale_mode,
let mut cmd = commands.spawn(SpriteBundle {
transform: Transform::from_translation(position),
texture: texture_handle.clone(),
sprite: Sprite {
custom_size: Some(size),
..default()
},
..default()
});
if let Some(scale_mode) = scale_mode {
cmd.insert(scale_mode);
}
cmd.with_children(|builder| {
builder.spawn(Text2dBundle {
text: Text::from_section(label, text_style).with_justify(JustifyText::Center),
transform: Transform::from_xyz(0., -0.5 * size.y - 10., 0.0),
text_anchor: bevy::sprite::Anchor::TopCenter,
..default()
})
.with_children(|builder| {
builder.spawn(Text2dBundle {
text: Text::from_section(label, text_style).with_justify(JustifyText::Center),
transform: Transform::from_xyz(0., -0.5 * size.y - 10., 0.0),
text_anchor: bevy::sprite::Anchor::TopCenter,
..default()
});
});
});
position.x += 0.5 * size.x + gap;
}
}
Expand Down
12 changes: 7 additions & 5 deletions examples/2d/sprite_tile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
current: 128.0,
speed: 50.0,
});
commands.spawn(SpriteBundle {
texture: asset_server.load("branding/icon.png"),
scale_mode: ImageScaleMode::Tiled {
commands.spawn((
SpriteBundle {
texture: asset_server.load("branding/icon.png"),
..default()
},
ImageScaleMode::Tiled {
tile_x: true,
tile_y: true,
stretch_value: 0.5, // The image will tile every 128px
},
..default()
});
));
}

fn animate(mut sprites: Query<&mut Sprite>, mut state: ResMut<AnimationState>, time: Res<Time>) {
Expand Down
28 changes: 15 additions & 13 deletions examples/ui/ui_texture_slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,23 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
.with_children(|parent| {
for [w, h] in [[150.0, 150.0], [300.0, 150.0], [150.0, 300.0]] {
parent
.spawn(ButtonBundle {
style: Style {
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)),
.spawn((
ButtonBundle {
style: Style {
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()
},
image: image.clone().into(),
..default()
},
image: image.clone().into(),
scale_mode: ImageScaleMode::Sliced(slicer.clone()),
..default()
})
ImageScaleMode::Sliced(slicer.clone()),
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"Button",
Expand Down

0 comments on commit e0c296e

Please sign in to comment.