Improved UiImage and Sprite scaling and slicing APIs (#16088)

# Objective

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

## Solution

* 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.

## Testing
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>
This commit is contained in:
ickshonpe 2024-11-04 15:14:03 +00:00 committed by GitHub
parent 1e47604506
commit 4e02d3cdb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 189 additions and 114 deletions

View file

@ -8,12 +8,6 @@ use bevy_render::{
use bevy_transform::components::{GlobalTransform, Transform};
/// A [`Bundle`] of components for drawing a single sprite from an image.
///
/// # Extra behaviors
///
/// You may add one or both of the following components to enable additional behaviors:
/// - [`ImageScaleMode`](crate::ImageScaleMode) to enable either slicing or tiling of the texture
/// - [`TextureAtlas`](crate::TextureAtlas) to draw a specific section of the texture
#[derive(Bundle, Clone, Debug, Default)]
#[deprecated(
since = "0.15.0",

View file

@ -30,7 +30,7 @@ pub mod prelude {
#[doc(hidden)]
pub use crate::{
bundle::SpriteBundle,
sprite::{ImageScaleMode, Sprite},
sprite::{Sprite, SpriteImageMode},
texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources},
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
ColorMaterial, ColorMesh2dBundle, MeshMaterial2d, TextureAtlasBuilder,
@ -106,7 +106,7 @@ impl Plugin for SpritePlugin {
app.init_asset::<TextureAtlasLayout>()
.register_asset_reflect::<TextureAtlasLayout>()
.register_type::<Sprite>()
.register_type::<ImageScaleMode>()
.register_type::<SpriteImageMode>()
.register_type::<TextureSlicer>()
.register_type::<Anchor>()
.register_type::<TextureAtlas>()

View file

@ -34,6 +34,8 @@ pub struct Sprite {
pub rect: Option<Rect>,
/// [`Anchor`] point of the sprite in the world
pub anchor: Anchor,
/// How the sprite's image will be scaled.
pub image_mode: SpriteImageMode,
}
impl Sprite {
@ -79,9 +81,12 @@ impl From<Handle<Image>> for Sprite {
}
/// Controls how the image is altered when scaled.
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Debug)]
pub enum ImageScaleMode {
#[derive(Default, Debug, Clone, Reflect, PartialEq)]
#[reflect(Debug)]
pub enum SpriteImageMode {
/// The sprite will take on the size of the image by default, and will be stretched or shrunk if [`Sprite::custom_size`] is set.
#[default]
Auto,
/// 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`
@ -96,6 +101,17 @@ pub enum ImageScaleMode {
},
}
impl SpriteImageMode {
/// Returns true if this mode uses slices internally ([`SpriteImageMode::Sliced`] or [`SpriteImageMode::Tiled`])
#[inline]
pub fn uses_slices(&self) -> bool {
matches!(
self,
SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. }
)
}
}
/// How a sprite is positioned relative to its [`Transform`].
/// It defaults to `Anchor::Center`.
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]

View file

@ -1,4 +1,4 @@
use crate::{ExtractedSprite, ImageScaleMode, Sprite, TextureAtlasLayout};
use crate::{ExtractedSprite, Sprite, SpriteImageMode, TextureAtlasLayout};
use super::TextureSlice;
use bevy_asset::{AssetEvent, Assets};
@ -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 [`ImageScaleMode`]
/// Component storing texture slices for tiled or sliced sprite entities
///
/// This component is automatically inserted and updated
#[derive(Debug, Clone, Component)]
@ -69,24 +69,19 @@ impl ComputedTextureSlices {
}
}
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
/// Generates sprite slices for a [`Sprite`] with [`SpriteImageMode::Sliced`] or [`SpriteImageMode::Sliced`]. The slices
/// will be computed according to the `image_handle` dimensions or the sprite rect.
///
/// Returns `None` if the image asset is not loaded
///
/// # Arguments
///
/// * `sprite` - The sprite component, will be used to find the draw area size
/// * `scale_mode` - The image scaling component
/// * `image_handle` - The texture to slice or tile
/// * `sprite` - The sprite component with the image handle and image mode
/// * `images` - The image assets, use to retrieve the image dimensions
/// * `atlas` - Optional texture atlas, if set the slicing will happen on the matching sub section
/// of the texture
/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect
#[must_use]
fn compute_sprite_slices(
sprite: &Sprite,
scale_mode: &ImageScaleMode,
images: &Assets<Image>,
atlas_layouts: &Assets<TextureAtlasLayout>,
) -> Option<ComputedTextureSlices> {
@ -111,9 +106,9 @@ fn compute_sprite_slices(
(size, rect)
}
};
let slices = match scale_mode {
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
ImageScaleMode::Tiled {
let slices = match &sprite.image_mode {
SpriteImageMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
SpriteImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
@ -125,18 +120,21 @@ fn compute_sprite_slices(
};
slice.tiled(*stretch_value, (*tile_x, *tile_y))
}
SpriteImageMode::Auto => {
unreachable!("Slices should not be computed for SpriteImageMode::Stretch")
}
};
Some(ComputedTextureSlices(slices))
}
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
/// on matching sprite entities with a [`ImageScaleMode`] component
/// on sprite entities with a matching [`SpriteImageMode`]
pub(crate) fn compute_slices_on_asset_event(
mut commands: Commands,
mut events: EventReader<AssetEvent<Image>>,
images: Res<Assets<Image>>,
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
sprites: Query<(Entity, &ImageScaleMode, &Sprite)>,
sprites: Query<(Entity, &Sprite)>,
) {
// We store the asset ids of added/modified image assets
let added_handles: HashSet<_> = events
@ -150,29 +148,31 @@ pub(crate) fn compute_slices_on_asset_event(
return;
}
// We recompute the sprite slices for sprite entities with a matching asset handle id
for (entity, scale_mode, sprite) in &sprites {
for (entity, sprite) in &sprites {
if !sprite.image_mode.uses_slices() {
continue;
}
if !added_handles.contains(&sprite.image.id()) {
continue;
}
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, &images, &atlas_layouts) {
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
commands.entity(entity).insert(slices);
}
}
}
/// System reacting to changes on relevant sprite bundle components to compute the sprite slices
/// on matching sprite entities with a [`ImageScaleMode`] component
/// System reacting to changes on the [`Sprite`] component to compute the sprite slices
pub(crate) fn compute_slices_on_sprite_change(
mut commands: Commands,
images: Res<Assets<Image>>,
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
changed_sprites: Query<
(Entity, &ImageScaleMode, &Sprite),
Or<(Changed<ImageScaleMode>, Changed<Sprite>)>,
>,
changed_sprites: Query<(Entity, &Sprite), Changed<Sprite>>,
) {
for (entity, scale_mode, sprite) in &changed_sprites {
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, &images, &atlas_layouts) {
for (entity, sprite) in &changed_sprites {
if !sprite.image_mode.uses_slices() {
continue;
}
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
commands.entity(entity).insert(slices);
}
}

View file

@ -10,7 +10,7 @@ use bevy_reflect::Reflect;
/// sections will be scaled or tiled.
///
/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures.
#[derive(Debug, Clone, Reflect)]
#[derive(Debug, Clone, Reflect, PartialEq)]
pub struct TextureSlicer {
/// The sprite borders, defining the 9 sections of the image
pub border: BorderRect,
@ -23,7 +23,7 @@ pub struct TextureSlicer {
}
/// Defines how a texture slice scales when resized
#[derive(Debug, Copy, Clone, Default, Reflect)]
#[derive(Debug, Copy, Clone, Default, Reflect, PartialEq)]
pub enum SliceScaleMode {
/// The slice will be stretched to fit the area
#[default]

View file

@ -221,7 +221,7 @@ pub fn ui_layout_system(
|| node.is_changed()
|| content_size
.as_ref()
.map(|c| c.measure.is_some())
.map(|c| c.is_changed() || c.measure.is_some())
.unwrap_or(false)
{
let layout_context = LayoutContext::new(

View file

@ -62,7 +62,7 @@ pub mod prelude {
Interaction, MaterialNode, UiMaterialPlugin, UiScale,
},
// `bevy_sprite` re-exports for texture slicing
bevy_sprite::{BorderRect, ImageScaleMode, SliceScaleMode, TextureSlicer},
bevy_sprite::{BorderRect, SliceScaleMode, SpriteImageMode, TextureSlicer},
};
}

View file

@ -57,12 +57,6 @@ pub struct NodeBundle {
}
/// A UI node that is an image
///
/// # Extra behaviors
///
/// You may add one or both of the following components to enable additional behaviors:
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
/// - [`TextureAtlas`](bevy_sprite::TextureAtlas) to draw a specific section of the texture
#[derive(Bundle, Debug, Default)]
#[deprecated(
since = "0.15.0",
@ -110,12 +104,6 @@ pub struct ImageBundle {
}
/// A UI node that is a button
///
/// # Extra behaviors
///
/// You may add one or both of the following components to enable additional behaviors:
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
/// - [`TextureAtlas`](bevy_sprite::TextureAtlas) to draw a specific section of the texture
#[derive(Bundle, Clone, Debug)]
#[deprecated(
since = "0.15.0",

View file

@ -41,7 +41,7 @@ use bevy_render::{
ExtractSchedule, Render,
};
use bevy_sprite::TextureAtlasLayout;
use bevy_sprite::{BorderRect, ImageScaleMode, SpriteAssetEvents};
use bevy_sprite::{BorderRect, SpriteAssetEvents};
use crate::{Display, Node};
use bevy_text::{ComputedTextBlock, PositionedGlyph, TextColor, TextLayoutInfo};
@ -309,18 +309,15 @@ pub fn extract_uinode_images(
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
default_ui_camera: Extract<DefaultUiCamera>,
uinode_query: Extract<
Query<
(
Entity,
&ComputedNode,
&GlobalTransform,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&UiImage,
),
Without<ImageScaleMode>,
>,
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&UiImage,
)>,
>,
mapping: Extract<Query<RenderEntity>>,
) {
@ -338,6 +335,7 @@ pub fn extract_uinode_images(
if !view_visibility.get()
|| image.color.is_fully_transparent()
|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
|| image.image_mode.uses_slices()
{
continue;
}

View file

@ -24,7 +24,7 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderSet,
};
use bevy_sprite::{
ImageScaleMode, SliceScaleMode, SpriteAssetEvents, TextureAtlasLayout, TextureSlicer,
SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureAtlasLayout, TextureSlicer,
};
use bevy_transform::prelude::GlobalTransform;
use bevy_utils::HashMap;
@ -232,7 +232,7 @@ pub struct ExtractedUiTextureSlice {
pub clip: Option<Rect>,
pub camera_entity: Entity,
pub color: LinearRgba,
pub image_scale_mode: ImageScaleMode,
pub image_scale_mode: SpriteImageMode,
pub flip_x: bool,
pub flip_y: bool,
pub main_entity: MainEntity,
@ -257,14 +257,11 @@ pub fn extract_ui_texture_slices(
Option<&CalculatedClip>,
Option<&TargetCamera>,
&UiImage,
&ImageScaleMode,
)>,
>,
mapping: Extract<Query<RenderEntity>>,
) {
for (entity, uinode, transform, view_visibility, clip, camera, image, image_scale_mode) in
&slicers_query
{
for (entity, uinode, transform, view_visibility, clip, camera, image) in &slicers_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
continue;
@ -274,6 +271,22 @@ pub fn extract_ui_texture_slices(
continue;
};
let image_scale_mode = match image.image_mode.clone() {
widget::NodeImageMode::Sliced(texture_slicer) => {
SpriteImageMode::Sliced(texture_slicer)
}
widget::NodeImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
} => SpriteImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
},
_ => continue,
};
// Skip invisible images
if !view_visibility.get()
|| image.color.is_fully_transparent()
@ -312,7 +325,7 @@ pub fn extract_ui_texture_slices(
clip: clip.map(|clip| clip.clip),
image: image.image.id(),
camera_entity,
image_scale_mode: image_scale_mode.clone(),
image_scale_mode,
atlas_rect,
flip_x: image.flip_x,
flip_y: image.flip_y,
@ -719,10 +732,10 @@ impl<P: PhaseItem> RenderCommand<P> for DrawSlicer {
fn compute_texture_slices(
image_size: Vec2,
target_size: Vec2,
image_scale_mode: &ImageScaleMode,
image_scale_mode: &SpriteImageMode,
) -> [[f32; 4]; 3] {
match image_scale_mode {
ImageScaleMode::Sliced(TextureSlicer {
SpriteImageMode::Sliced(TextureSlicer {
border: border_rect,
center_scale_mode,
sides_scale_mode,
@ -775,7 +788,7 @@ fn compute_texture_slices(
],
]
}
ImageScaleMode::Tiled {
SpriteImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
@ -784,6 +797,9 @@ fn compute_texture_slices(
let ry = compute_tiled_axis(*tile_y, image_size.y, target_size.y, *stretch_value);
[[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]]
}
SpriteImageMode::Auto => {
unreachable!("Slices should not be computed for ImageScaleMode::Stretch")
}
}
}

View file

@ -5,14 +5,14 @@ use bevy_ecs::prelude::*;
use bevy_math::{Rect, UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::{Image, TRANSPARENT_IMAGE_HANDLE};
use bevy_sprite::{TextureAtlas, TextureAtlasLayout};
use bevy_sprite::{TextureAtlas, TextureAtlasLayout, TextureSlicer};
use bevy_window::{PrimaryWindow, Window};
use taffy::{MaybeMath, MaybeResolve};
/// The 2D texture displayed for this UI node
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, Default, Debug)]
#[require(Node, UiImageSize, ContentSize)]
#[require(Node, UiImageSize)]
pub struct UiImage {
/// The tint color used to draw the image.
///
@ -23,11 +23,11 @@ pub struct UiImage {
///
/// 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
/// The (optional) texture atlas used to render the image.
pub texture_atlas: Option<TextureAtlas>,
/// Whether the image should be flipped along its x-axis
/// Whether the image should be flipped along its x-axis.
pub flip_x: bool,
/// Whether the image should be flipped along its y-axis
/// 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`].
@ -35,6 +35,8 @@ pub struct UiImage {
/// 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,
}
impl Default for UiImage {
@ -56,6 +58,7 @@ impl Default for UiImage {
flip_x: false,
flip_y: false,
rect: None,
image_mode: NodeImageMode::Auto,
}
}
}
@ -81,6 +84,7 @@ impl UiImage {
flip_y: false,
texture_atlas: None,
rect: None,
image_mode: NodeImageMode::Auto,
}
}
@ -119,6 +123,12 @@ impl UiImage {
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 UiImage {
@ -127,6 +137,39 @@ impl From<Handle<Image>> for UiImage {
}
}
/// 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, Reflect)]
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 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`]
@ -216,7 +259,7 @@ pub fn update_image_content_size_system(
textures: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlasLayout>>,
mut query: Query<(&mut ContentSize, &UiImage, &mut UiImageSize), UpdateImageFilter>,
mut query: Query<(&mut ContentSize, Ref<UiImage>, &mut UiImageSize), UpdateImageFilter>,
) {
let combined_scale_factor = windows
.get_single()
@ -225,6 +268,14 @@ pub fn update_image_content_size_system(
* ui_scale.0;
for (mut content_size, image, mut image_size) in &mut query {
if !matches!(image.image_mode, NodeImageMode::Auto) {
if image.is_changed() {
// Mutably derefs, marking the `ContentSize` as changed ensuring `ui_layout_system` will remove the node's measure func if present.
content_size.measure = None;
}
continue;
}
if let Some(size) = match &image.texture_atlas {
Some(atlas) => atlas.texture_rect(&atlases).map(|t| t.size()),
None => textures.get(&image.image).map(Image::size),

View file

@ -19,55 +19,65 @@ fn spawn_sprites(
) {
let cases = [
// Reference sprite
("Original", style.clone(), Vec2::splat(100.0), None),
(
"Original",
style.clone(),
Vec2::splat(100.0),
SpriteImageMode::Auto,
),
// Scaled regular sprite
("Stretched", style.clone(), Vec2::new(100.0, 200.0), None),
(
"Stretched",
style.clone(),
Vec2::new(100.0, 200.0),
SpriteImageMode::Auto,
),
// Stretched Scaled sliced sprite
(
"With Slicing",
style.clone(),
Vec2::new(100.0, 200.0),
Some(ImageScaleMode::Sliced(TextureSlicer {
SpriteImageMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Stretch,
..default()
})),
}),
),
// Scaled sliced sprite
(
"With Tiling",
style.clone(),
Vec2::new(100.0, 200.0),
Some(ImageScaleMode::Sliced(TextureSlicer {
SpriteImageMode::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
(
"With Tiling",
style.clone(),
Vec2::new(300.0, 200.0),
Some(ImageScaleMode::Sliced(TextureSlicer {
SpriteImageMode::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
(
"With Corners Constrained",
style,
Vec2::new(300.0, 200.0),
Some(ImageScaleMode::Sliced(TextureSlicer {
SpriteImageMode::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,
})),
}),
),
];
@ -77,13 +87,11 @@ fn spawn_sprites(
Sprite {
image: texture_handle.clone(),
custom_size: Some(size),
image_mode: scale_mode,
..default()
},
Transform::from_translation(position),
));
if let Some(scale_mode) = scale_mode {
cmd.insert(scale_mode);
}
cmd.with_children(|builder| {
builder.spawn((
Text2d::new(label),

View file

@ -26,14 +26,15 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
current: 128.0,
speed: 50.0,
});
commands.spawn((
Sprite::from_image(asset_server.load("branding/icon.png")),
ImageScaleMode::Tiled {
commands.spawn(Sprite {
image: asset_server.load("branding/icon.png"),
image_mode: SpriteImageMode::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>) {

View file

@ -9,6 +9,7 @@ use bevy::{
input::mouse::{MouseScrollUnit, MouseWheel},
picking::focus::HoverMap,
prelude::*,
ui::widget::NodeImageMode,
winit::WinitSettings,
};
@ -280,7 +281,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// bevy logo (image)
parent
.spawn((
UiImage::new(asset_server.load("branding/bevy_logo_dark_big.png")),
UiImage::new(asset_server.load("branding/bevy_logo_dark_big.png"))
.with_mode(NodeImageMode::Stretch),
Node {
width: Val::Px(500.0),
height: Val::Px(125.0),

View file

@ -4,6 +4,7 @@
use bevy::{
color::palettes::css::{GOLD, ORANGE},
prelude::*,
ui::widget::NodeImageMode,
winit::WinitSettings,
};
@ -87,7 +88,8 @@ fn setup(
index: idx,
layout: atlas_layout_handle.clone(),
},
),
)
.with_mode(NodeImageMode::Sliced(slicer.clone())),
Node {
width: Val::Px(w),
height: Val::Px(h),
@ -98,7 +100,6 @@ fn setup(
margin: UiRect::all(Val::Px(20.0)),
..default()
},
ImageScaleMode::Sliced(slicer.clone()),
))
.with_children(|parent| {
parent.spawn((

View file

@ -4,6 +4,7 @@
use bevy::{
color::palettes::css::{GOLD, ORANGE},
prelude::*,
ui::widget::NodeImageMode,
winit::WinitSettings,
};
@ -77,20 +78,18 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
margin: UiRect::all(Val::Px(20.0)),
..default()
},
UiImage::new(image.clone()),
ImageScaleMode::Sliced(slicer.clone()),
UiImage::new(image.clone())
.with_mode(NodeImageMode::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)),
));
});
.with_child((
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)),
));
}
});
}

View file

@ -3,6 +3,7 @@
use bevy::{
prelude::*,
render::texture::{ImageLoaderSettings, ImageSampler},
ui::widget::NodeImageMode,
winit::WinitSettings,
};
@ -61,6 +62,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
image: image.clone(),
flip_x,
flip_y,
image_mode: NodeImageMode::Sliced(slicer.clone()),
..default()
},
Node {
@ -68,7 +70,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
height: Val::Px(height),
..default()
},
ImageScaleMode::Sliced(slicer.clone()),
));
}
});