diff --git a/Cargo.toml b/Cargo.toml index cf5d69ef54183..216db0ff923b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2510,6 +2510,17 @@ description = "Illustrates how to use 9 Slicing in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_texture_atlas_slice" +path = "examples/ui/ui_texture_atlas_slice.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_texture_atlas_slice] +name = "UI Texture Atlas Slice" +description = "Illustrates how to use 9 Slicing for TextureAtlases in UI" +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" diff --git a/assets/textures/fantasy_ui_borders/border_sheet.png b/assets/textures/fantasy_ui_borders/border_sheet.png new file mode 100644 index 0000000000000..8ee2d2a9b2d34 Binary files /dev/null and b/assets/textures/fantasy_ui_borders/border_sheet.png differ diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 40d0dd5f3ca68..ae57eb760c091 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -32,8 +32,6 @@ pub struct Sprite { } /// Controls how the image is altered when scaled. -/// -/// Note: This is not yet compatible with texture atlases #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component)] pub enum ImageScaleMode { diff --git a/crates/bevy_sprite/src/texture_slice/computed_slices.rs b/crates/bevy_sprite/src/texture_slice/computed_slices.rs index cbc5a370b43f9..1f08e9a817d49 100644 --- a/crates/bevy_sprite/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite/src/texture_slice/computed_slices.rs @@ -1,4 +1,4 @@ -use crate::{ExtractedSprite, ImageScaleMode, Sprite}; +use crate::{ExtractedSprite, ImageScaleMode, Sprite, TextureAtlas, TextureAtlasLayout}; use super::TextureSlice; use bevy_asset::{AssetEvent, Assets, Handle}; @@ -63,37 +63,55 @@ impl ComputedTextureSlices { /// 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 +/// * `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, image_handle: &Handle, images: &Assets, + atlas: Option<&TextureAtlas>, + atlas_layouts: &Assets, ) -> Option { - 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::Sliced(slicer) => slicer.compute_slices( - sprite.rect.unwrap_or(Rect { + let (image_size, texture_rect) = match 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(image_handle)?; + 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: image_size, - }), - sprite.custom_size, - ), + max: size, + }); + (size, rect) + } + }; + let slices = match scale_mode { + ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size), ImageScaleMode::Tiled { tile_x, tile_y, stretch_value, } => { let slice = TextureSlice { - texture_rect: sprite.rect.unwrap_or(Rect { - min: Vec2::ZERO, - max: image_size, - }), + texture_rect, draw_size: sprite.custom_size.unwrap_or(image_size), offset: Vec2::ZERO, }; @@ -109,7 +127,14 @@ pub(crate) fn compute_slices_on_asset_event( mut commands: Commands, mut events: EventReader>, images: Res>, - sprites: Query<(Entity, &ImageScaleMode, &Sprite, &Handle)>, + atlas_layouts: Res>, + sprites: Query<( + Entity, + &ImageScaleMode, + &Sprite, + &Handle, + Option<&TextureAtlas>, + )>, ) { // We store the asset ids of added/modified image assets let added_handles: HashSet<_> = events @@ -123,11 +148,18 @@ 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, image_handle) in &sprites { + for (entity, scale_mode, sprite, image_handle, atlas) in &sprites { if !added_handles.contains(&image_handle.id()) { continue; } - if let Some(slices) = compute_sprite_slices(sprite, scale_mode, image_handle, &images) { + if let Some(slices) = compute_sprite_slices( + sprite, + scale_mode, + image_handle, + &images, + atlas, + &atlas_layouts, + ) { commands.entity(entity).insert(slices); } } @@ -138,17 +170,32 @@ pub(crate) fn compute_slices_on_asset_event( pub(crate) fn compute_slices_on_sprite_change( mut commands: Commands, images: Res>, + atlas_layouts: Res>, changed_sprites: Query< - (Entity, &ImageScaleMode, &Sprite, &Handle), + ( + Entity, + &ImageScaleMode, + &Sprite, + &Handle, + Option<&TextureAtlas>, + ), Or<( Changed, Changed>, Changed, + Changed, )>, >, ) { - for (entity, scale_mode, sprite, image_handle) in &changed_sprites { - if let Some(slices) = compute_sprite_slices(sprite, scale_mode, image_handle, &images) { + for (entity, scale_mode, sprite, image_handle, atlas) in &changed_sprites { + if let Some(slices) = compute_sprite_slices( + sprite, + scale_mode, + image_handle, + &images, + atlas, + &atlas_layouts, + ) { commands.entity(entity).insert(slices); } } diff --git a/crates/bevy_sprite/src/texture_slice/slicer.rs b/crates/bevy_sprite/src/texture_slice/slicer.rs index ab12ce47488e5..d930aab705d12 100644 --- a/crates/bevy_sprite/src/texture_slice/slicer.rs +++ b/crates/bevy_sprite/src/texture_slice/slicer.rs @@ -72,7 +72,7 @@ impl TextureSlicer { TextureSlice { texture_rect: Rect { min: vec2(base_rect.max.x - right, base_rect.min.y), - max: vec2(base_rect.max.x, top), + max: vec2(base_rect.max.x, base_rect.min.y + top), }, draw_size: vec2(right, top) * min_coef, offset: vec2( @@ -198,6 +198,9 @@ impl TextureSlicer { /// /// * `rect` - The section of the texture to slice in 9 parts /// * `render_size` - The optional draw size of the texture. If not set the `rect` size will be used. + // + // TODO: Support `URect` and `UVec2` instead (See `https://github.com/bevyengine/bevy/pull/11698`) + // #[must_use] pub fn compute_slices(&self, rect: Rect, render_size: Option) -> Vec { let render_size = render_size.unwrap_or_else(|| rect.size()); diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index f760b5377086f..29164c6e22186 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -122,6 +122,11 @@ pub struct ImageBundle { /// A UI node that is a texture atlas sprite /// +/// # 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 +/// /// This bundle is identical to [`ImageBundle`] with an additional [`TextureAtlas`] component. #[deprecated( since = "0.14.0", @@ -295,7 +300,7 @@ where /// /// You may add the following components to enable additional behaviours: /// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture -/// - [`TextureAtlas`] to draw specific sections of the texture +/// - [`TextureAtlas`] to draw specific section of the texture /// /// Note that `ImageScaleMode` is currently not compatible with `TextureAtlas`. #[derive(Bundle, Clone, Debug)] diff --git a/crates/bevy_ui/src/texture_slice.rs b/crates/bevy_ui/src/texture_slice.rs index 8d07000caaab8..c0836a3b870bb 100644 --- a/crates/bevy_ui/src/texture_slice.rs +++ b/crates/bevy_ui/src/texture_slice.rs @@ -6,7 +6,7 @@ use bevy_asset::{AssetEvent, Assets}; use bevy_ecs::prelude::*; use bevy_math::{Rect, Vec2}; use bevy_render::texture::Image; -use bevy_sprite::{ImageScaleMode, TextureSlice}; +use bevy_sprite::{ImageScaleMode, TextureAtlas, TextureAtlasLayout, TextureSlice}; use bevy_transform::prelude::*; use bevy_utils::HashSet; @@ -74,25 +74,48 @@ 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. +/// will be computed according to the `image_handle` dimensions. /// /// Returns `None` if the image asset is not loaded +/// +/// # Arguments +/// +/// * `draw_area` - The size of the drawing area the slices will have to fit into +/// * `scale_mode` - The image scaling component +/// * `image_handle` - The texture to slice or tile +/// * `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_texture_slices( draw_area: Vec2, scale_mode: &ImageScaleMode, image_handle: &UiImage, images: &Assets, + atlas: Option<&TextureAtlas>, + atlas_layouts: &Assets, ) -> Option { - let image_size = images.get(&image_handle.texture).map(|i| { - Vec2::new( - i.texture_descriptor.size.width as f32, - i.texture_descriptor.size.height as f32, - ) - })?; - let texture_rect = Rect { - min: Vec2::ZERO, - max: image_size, + let (image_size, texture_rect) = match 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(&image_handle.texture)?; + let size = Vec2::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ); + let rect = Rect { + min: Vec2::ZERO, + max: size, + }; + (size, rect) + } }; let slices = match scale_mode { ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)), @@ -118,7 +141,14 @@ pub(crate) fn compute_slices_on_asset_event( mut commands: Commands, mut events: EventReader>, images: Res>, - ui_nodes: Query<(Entity, &ImageScaleMode, &Node, &UiImage)>, + atlas_layouts: Res>, + ui_nodes: Query<( + Entity, + &ImageScaleMode, + &Node, + &UiImage, + Option<&TextureAtlas>, + )>, ) { // We store the asset ids of added/modified image assets let added_handles: HashSet<_> = events @@ -132,11 +162,18 @@ 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, ui_node, image) in &ui_nodes { + for (entity, scale_mode, ui_node, image, atlas) in &ui_nodes { if !added_handles.contains(&image.texture.id()) { continue; } - if let Some(slices) = compute_texture_slices(ui_node.size(), scale_mode, image, &images) { + if let Some(slices) = compute_texture_slices( + ui_node.size(), + scale_mode, + image, + &images, + atlas, + &atlas_layouts, + ) { commands.entity(entity).insert(slices); } } @@ -147,13 +184,32 @@ pub(crate) fn compute_slices_on_asset_event( pub(crate) fn compute_slices_on_image_change( mut commands: Commands, images: Res>, + atlas_layouts: Res>, changed_nodes: Query< - (Entity, &ImageScaleMode, &Node, &UiImage), - Or<(Changed, Changed, Changed)>, + ( + Entity, + &ImageScaleMode, + &Node, + &UiImage, + Option<&TextureAtlas>, + ), + Or<( + Changed, + Changed, + Changed, + Changed, + )>, >, ) { - for (entity, scale_mode, ui_node, image) in &changed_nodes { - if let Some(slices) = compute_texture_slices(ui_node.size(), scale_mode, image, &images) { + for (entity, scale_mode, ui_node, image, atlas) in &changed_nodes { + if let Some(slices) = compute_texture_slices( + ui_node.size(), + scale_mode, + image, + &images, + atlas, + &atlas_layouts, + ) { commands.entity(entity).insert(slices); } } diff --git a/examples/README.md b/examples/README.md index 61eca36f5113f..3ee06cbb5a793 100644 --- a/examples/README.md +++ b/examples/README.md @@ -405,6 +405,7 @@ Example | Description [UI Material](../examples/ui/ui_material.rs) | Demonstrates creating and using custom Ui materials [UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI [UI Texture Atlas](../examples/ui/ui_texture_atlas.rs) | Illustrates how to use TextureAtlases in UI +[UI Texture Atlas Slice](../examples/ui/ui_texture_atlas_slice.rs) | Illustrates how to use 9 Slicing for TextureAtlases in UI [UI Texture Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI [UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements [Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates diff --git a/examples/ui/ui_texture_atlas_slice.rs b/examples/ui/ui_texture_atlas_slice.rs new file mode 100644 index 0000000000000..77901211b7ed0 --- /dev/null +++ b/examples/ui/ui_texture_atlas_slice.rs @@ -0,0 +1,115 @@ +//! This example illustrates how to create buttons with their texture atlases 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, &mut TextureAtlas, &Children, &mut UiImage), + (Changed, With