
# 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>
180 lines
6.1 KiB
Rust
180 lines
6.1 KiB
Rust
use crate::{ExtractedSprite, Sprite, SpriteImageMode, TextureAtlasLayout};
|
|
|
|
use super::TextureSlice;
|
|
use bevy_asset::{AssetEvent, Assets};
|
|
use bevy_ecs::prelude::*;
|
|
use bevy_math::{Rect, Vec2};
|
|
use bevy_render::texture::Image;
|
|
use bevy_transform::prelude::*;
|
|
use bevy_utils::HashSet;
|
|
|
|
/// Component storing texture slices for tiled or sliced sprite entities
|
|
///
|
|
/// This component is automatically inserted and updated
|
|
#[derive(Debug, Clone, Component)]
|
|
pub struct ComputedTextureSlices(Vec<TextureSlice>);
|
|
|
|
impl ComputedTextureSlices {
|
|
/// Computes [`ExtractedSprite`] iterator from the sprite slices
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `transform` - the sprite entity global transform
|
|
/// * `original_entity` - the sprite entity
|
|
/// * `sprite` - The sprite component
|
|
/// * `handle` - The sprite texture handle
|
|
#[must_use]
|
|
pub(crate) fn extract_sprites<'a>(
|
|
&'a self,
|
|
transform: &'a GlobalTransform,
|
|
original_entity: Entity,
|
|
sprite: &'a Sprite,
|
|
) -> impl ExactSizeIterator<Item = ExtractedSprite> + 'a {
|
|
let mut flip = Vec2::ONE;
|
|
let [mut flip_x, mut flip_y] = [false; 2];
|
|
if sprite.flip_x {
|
|
flip.x *= -1.0;
|
|
flip_x = true;
|
|
}
|
|
if sprite.flip_y {
|
|
flip.y *= -1.0;
|
|
flip_y = true;
|
|
}
|
|
self.0.iter().map(move |slice| {
|
|
let offset = (slice.offset * flip).extend(0.0);
|
|
let transform = transform.mul_transform(Transform::from_translation(offset));
|
|
ExtractedSprite {
|
|
original_entity: Some(original_entity),
|
|
color: sprite.color.into(),
|
|
transform,
|
|
rect: Some(slice.texture_rect),
|
|
custom_size: Some(slice.draw_size),
|
|
flip_x,
|
|
flip_y,
|
|
image_handle_id: sprite.image.id(),
|
|
anchor: Self::redepend_anchor_from_sprite_to_slice(sprite, slice),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn redepend_anchor_from_sprite_to_slice(sprite: &Sprite, slice: &TextureSlice) -> Vec2 {
|
|
let sprite_size = sprite
|
|
.custom_size
|
|
.unwrap_or(sprite.rect.unwrap_or_default().size());
|
|
if sprite_size == Vec2::ZERO {
|
|
sprite.anchor.as_vec()
|
|
} else {
|
|
sprite.anchor.as_vec() * sprite_size / slice.draw_size
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 with the image handle and image mode
|
|
/// * `images` - The image assets, use to retrieve the image dimensions
|
|
/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect
|
|
#[must_use]
|
|
fn compute_sprite_slices(
|
|
sprite: &Sprite,
|
|
images: &Assets<Image>,
|
|
atlas_layouts: &Assets<TextureAtlasLayout>,
|
|
) -> Option<ComputedTextureSlices> {
|
|
let (image_size, texture_rect) = match &sprite.texture_atlas {
|
|
Some(a) => {
|
|
let layout = atlas_layouts.get(&a.layout)?;
|
|
(
|
|
layout.size.as_vec2(),
|
|
layout.textures.get(a.index)?.as_rect(),
|
|
)
|
|
}
|
|
None => {
|
|
let image = images.get(&sprite.image)?;
|
|
let size = Vec2::new(
|
|
image.texture_descriptor.size.width as f32,
|
|
image.texture_descriptor.size.height as f32,
|
|
);
|
|
let rect = sprite.rect.unwrap_or(Rect {
|
|
min: Vec2::ZERO,
|
|
max: size,
|
|
});
|
|
(size, rect)
|
|
}
|
|
};
|
|
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,
|
|
} => {
|
|
let slice = TextureSlice {
|
|
texture_rect,
|
|
draw_size: sprite.custom_size.unwrap_or(image_size),
|
|
offset: Vec2::ZERO,
|
|
};
|
|
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 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, &Sprite)>,
|
|
) {
|
|
// We store the asset ids of added/modified image assets
|
|
let added_handles: HashSet<_> = events
|
|
.read()
|
|
.filter_map(|e| match e {
|
|
AssetEvent::Added { id } | AssetEvent::Modified { id } => Some(*id),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
if added_handles.is_empty() {
|
|
return;
|
|
}
|
|
// We recompute the sprite slices for sprite entities with a matching asset handle id
|
|
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, &images, &atlas_layouts) {
|
|
commands.entity(entity).insert(slices);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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, &Sprite), Changed<Sprite>>,
|
|
) {
|
|
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);
|
|
}
|
|
}
|
|
}
|