Move TextureAtlas into UiImage and remove impl Component for TextureAtlas (#16072)

# Objective

Fixes #16064

## Solution

- Add TextureAtlas to `UiImage::texture_atlas`
- Add `TextureAtlas::from_atlas_image` for parity with `Sprite`
- Rename `UiImage::texture` to `UiImage::image` for parity with `Sprite`
- Port relevant implementations and uses
- Remove `derive(Component)` for `TextureAtlas`

---

## Migration Guide

Before:
```rust
commands.spawn((
  UiImage::new(image),
  TextureAtlas { index, layout },
));
```

After:
```rust
commands.spawn(UiImage::from_atlas_image(image, TextureAtlas { index, layout }));
```

Before:
```rust
commands.spawn(UiImage {
    texture: some_image,
    ..default()
})
```

After:
```rust
commands.spawn(UiImage {
    image: some_image,
    ..default()
})
```
This commit is contained in:
Carter Anderson 2024-10-23 18:24:17 -05:00 committed by GitHub
parent 2cdad48b30
commit 9274bfed27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 71 additions and 70 deletions

View file

@ -1,5 +1,4 @@
use bevy_asset::{Asset, AssetId, Assets, Handle}; use bevy_asset::{Asset, AssetId, Assets, Handle};
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_math::{URect, UVec2}; use bevy_math::{URect, UVec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
#[cfg(feature = "serialize")] #[cfg(feature = "serialize")]
@ -152,7 +151,7 @@ impl TextureAtlasLayout {
} }
} }
/// Component used to draw a specific section of a texture. /// An index into a [`TextureAtlasLayout`], which corresponds to a specific section of a texture.
/// ///
/// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas. /// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas.
/// The texture atlas contains various *sections* of a given texture, allowing users to have a single /// The texture atlas contains various *sections* of a given texture, allowing users to have a single
@ -164,8 +163,8 @@ impl TextureAtlasLayout {
/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs) /// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
/// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs) /// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs) /// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
#[derive(Component, Default, Debug, Clone, Reflect)] #[derive(Default, Debug, Clone, Reflect)]
#[reflect(Component, Default, Debug)] #[reflect(Default, Debug)]
pub struct TextureAtlas { pub struct TextureAtlas {
/// Texture atlas layout handle /// Texture atlas layout handle
pub layout: Handle<TextureAtlasLayout>, pub layout: Handle<TextureAtlasLayout>,

View file

@ -40,7 +40,7 @@ use bevy_render::{
ExtractSchedule, Render, ExtractSchedule, Render,
}; };
use bevy_sprite::TextureAtlasLayout; use bevy_sprite::TextureAtlasLayout;
use bevy_sprite::{BorderRect, ImageScaleMode, SpriteAssetEvents, TextureAtlas}; use bevy_sprite::{BorderRect, ImageScaleMode, SpriteAssetEvents};
use crate::{Display, Node}; use crate::{Display, Node};
use bevy_text::{ComputedTextBlock, PositionedGlyph, TextColor, TextLayoutInfo}; use bevy_text::{ComputedTextBlock, PositionedGlyph, TextColor, TextLayoutInfo};
@ -317,14 +317,13 @@ pub fn extract_uinode_images(
Option<&CalculatedClip>, Option<&CalculatedClip>,
Option<&TargetCamera>, Option<&TargetCamera>,
&UiImage, &UiImage,
Option<&TextureAtlas>,
), ),
Without<ImageScaleMode>, Without<ImageScaleMode>,
>, >,
>, >,
mapping: Extract<Query<RenderEntity>>, mapping: Extract<Query<RenderEntity>>,
) { ) {
for (entity, uinode, transform, view_visibility, clip, camera, image, atlas) in &uinode_query { for (entity, uinode, transform, view_visibility, clip, camera, image) in &uinode_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else { else {
continue; continue;
@ -337,12 +336,14 @@ pub fn extract_uinode_images(
// Skip invisible images // Skip invisible images
if !view_visibility.get() if !view_visibility.get()
|| image.color.is_fully_transparent() || image.color.is_fully_transparent()
|| image.texture.id() == TRANSPARENT_IMAGE_HANDLE.id() || image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
{ {
continue; continue;
} }
let atlas_rect = atlas let atlas_rect = image
.texture_atlas
.as_ref()
.and_then(|s| s.texture_rect(&texture_atlases)) .and_then(|s| s.texture_rect(&texture_atlases))
.map(|r| r.as_rect()); .map(|r| r.as_rect());
@ -376,7 +377,7 @@ pub fn extract_uinode_images(
color: image.color.into(), color: image.color.into(),
rect, rect,
clip: clip.map(|clip| clip.clip), clip: clip.map(|clip| clip.clip),
image: image.texture.id(), image: image.image.id(),
camera_entity: render_camera_entity, camera_entity: render_camera_entity,
item: ExtractedUiItem::Node { item: ExtractedUiItem::Node {
atlas_scaling, atlas_scaling,

View file

@ -24,8 +24,7 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderSet, Extract, ExtractSchedule, Render, RenderSet,
}; };
use bevy_sprite::{ use bevy_sprite::{
ImageScaleMode, SliceScaleMode, SpriteAssetEvents, TextureAtlas, TextureAtlasLayout, ImageScaleMode, SliceScaleMode, SpriteAssetEvents, TextureAtlasLayout, TextureSlicer,
TextureSlicer,
}; };
use bevy_transform::prelude::GlobalTransform; use bevy_transform::prelude::GlobalTransform;
use bevy_utils::HashMap; use bevy_utils::HashMap;
@ -258,22 +257,12 @@ pub fn extract_ui_texture_slices(
Option<&TargetCamera>, Option<&TargetCamera>,
&UiImage, &UiImage,
&ImageScaleMode, &ImageScaleMode,
Option<&TextureAtlas>,
)>, )>,
>, >,
mapping: Extract<Query<RenderEntity>>, mapping: Extract<Query<RenderEntity>>,
) { ) {
for ( for (entity, uinode, transform, view_visibility, clip, camera, image, image_scale_mode) in
entity, &slicers_query
uinode,
transform,
view_visibility,
clip,
camera,
image,
image_scale_mode,
atlas,
) in &slicers_query
{ {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else { else {
@ -287,12 +276,14 @@ pub fn extract_ui_texture_slices(
// Skip invisible images // Skip invisible images
if !view_visibility.get() if !view_visibility.get()
|| image.color.is_fully_transparent() || image.color.is_fully_transparent()
|| image.texture.id() == TRANSPARENT_IMAGE_HANDLE.id() || image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
{ {
continue; continue;
} }
let atlas_rect = atlas let atlas_rect = image
.texture_atlas
.as_ref()
.and_then(|s| s.texture_rect(&texture_atlases)) .and_then(|s| s.texture_rect(&texture_atlases))
.map(|r| r.as_rect()); .map(|r| r.as_rect());
@ -318,7 +309,7 @@ pub fn extract_ui_texture_slices(
max: uinode.calculated_size, max: uinode.calculated_size,
}, },
clip: clip.map(|clip| clip.clip), clip: clip.map(|clip| clip.clip),
image: image.texture.id(), image: image.image.id(),
camera_entity, camera_entity,
image_scale_mode: image_scale_mode.clone(), image_scale_mode: image_scale_mode.clone(),
atlas_rect, atlas_rect,

View file

@ -9,7 +9,7 @@ use bevy_render::{
texture::{Image, TRANSPARENT_IMAGE_HANDLE}, texture::{Image, TRANSPARENT_IMAGE_HANDLE},
view::Visibility, view::Visibility,
}; };
use bevy_sprite::BorderRect; use bevy_sprite::{BorderRect, TextureAtlas};
use bevy_transform::components::Transform; use bevy_transform::components::Transform;
use bevy_utils::warn_once; use bevy_utils::warn_once;
use bevy_window::{PrimaryWindow, WindowRef}; use bevy_window::{PrimaryWindow, WindowRef};
@ -2053,15 +2053,17 @@ pub struct UiImage {
/// Handle to the texture. /// Handle to the texture.
/// ///
/// This defaults to a [`TRANSPARENT_IMAGE_HANDLE`], which points to a fully transparent 1x1 texture. /// This defaults to a [`TRANSPARENT_IMAGE_HANDLE`], which points to a fully transparent 1x1 texture.
pub texture: Handle<Image>, pub image: Handle<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, 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, pub flip_y: bool,
/// An optional rectangle representing the region of the image to render, instead of rendering /// 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`](bevy_sprite::TextureAtlas). /// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
/// ///
/// When used with a [`TextureAtlas`](bevy_sprite::TextureAtlas), the rect /// When used with a [`TextureAtlas`], the rect
/// is offset by the atlas's minimal (top-left) corner position. /// is offset by the atlas's minimal (top-left) corner position.
pub rect: Option<Rect>, pub rect: Option<Rect>,
} }
@ -2079,8 +2081,9 @@ impl Default for UiImage {
// This should be white because the tint is multiplied with the image, // This should be white because the tint is multiplied with the image,
// so if you set an actual image with default tint you'd want its original colors // so if you set an actual image with default tint you'd want its original colors
color: Color::WHITE, color: Color::WHITE,
texture_atlas: None,
// This texture needs to be transparent by default, to avoid covering the background color // This texture needs to be transparent by default, to avoid covering the background color
texture: TRANSPARENT_IMAGE_HANDLE, image: TRANSPARENT_IMAGE_HANDLE,
flip_x: false, flip_x: false,
flip_y: false, flip_y: false,
rect: None, rect: None,
@ -2092,7 +2095,7 @@ impl UiImage {
/// Create a new [`UiImage`] with the given texture. /// Create a new [`UiImage`] with the given texture.
pub fn new(texture: Handle<Image>) -> Self { pub fn new(texture: Handle<Image>) -> Self {
Self { Self {
texture, image: texture,
color: Color::WHITE, color: Color::WHITE,
..Default::default() ..Default::default()
} }
@ -2103,14 +2106,24 @@ impl UiImage {
/// This is primarily useful for debugging / mocking the extents of your image. /// This is primarily useful for debugging / mocking the extents of your image.
pub fn solid_color(color: Color) -> Self { pub fn solid_color(color: Color) -> Self {
Self { Self {
texture: Handle::default(), image: Handle::default(),
color, color,
flip_x: false, flip_x: false,
flip_y: false, flip_y: false,
texture_atlas: None,
rect: None, rect: None,
} }
} }
/// Create a [`UiImage`] from an image, with an associated texture atlas
pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
Self {
image,
texture_atlas: Some(atlas),
..Default::default()
}
}
/// Set the color tint /// Set the color tint
#[must_use] #[must_use]
pub const fn with_color(mut self, color: Color) -> Self { pub const fn with_color(mut self, color: Color) -> Self {

View file

@ -4,7 +4,7 @@ use bevy_ecs::prelude::*;
use bevy_math::{UVec2, Vec2}; use bevy_math::{UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::Image; use bevy_render::texture::Image;
use bevy_sprite::{TextureAtlas, TextureAtlasLayout}; use bevy_sprite::TextureAtlasLayout;
use bevy_window::{PrimaryWindow, Window}; use bevy_window::{PrimaryWindow, Window};
use taffy::{MaybeMath, MaybeResolve}; use taffy::{MaybeMath, MaybeResolve};
@ -97,15 +97,7 @@ pub fn update_image_content_size_system(
textures: Res<Assets<Image>>, textures: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlasLayout>>, atlases: Res<Assets<TextureAtlasLayout>>,
mut query: Query< mut query: Query<(&mut ContentSize, &UiImage, &mut UiImageSize), UpdateImageFilter>,
(
&mut ContentSize,
&UiImage,
&mut UiImageSize,
Option<&TextureAtlas>,
),
UpdateImageFilter,
>,
) { ) {
let combined_scale_factor = windows let combined_scale_factor = windows
.get_single() .get_single()
@ -113,10 +105,10 @@ pub fn update_image_content_size_system(
.unwrap_or(1.) .unwrap_or(1.)
* ui_scale.0; * ui_scale.0;
for (mut content_size, image, mut image_size, atlas_image) in &mut query { for (mut content_size, image, mut image_size) in &mut query {
if let Some(size) = match atlas_image { if let Some(size) = match &image.texture_atlas {
Some(atlas) => atlas.texture_rect(&atlases).map(|t| t.size()), Some(atlas) => atlas.texture_rect(&atlases).map(|t| t.size()),
None => textures.get(&image.texture).map(Image::size), None => textures.get(&image.image).map(Image::size),
} { } {
// Update only if size or scale factor has changed to avoid needless layout calculations // Update only if size or scale factor has changed to avoid needless layout calculations
if size != image_size.size if size != image_size.size

View file

@ -116,7 +116,7 @@ fn setup(
commands.spawn(( commands.spawn((
UiImage { UiImage {
texture: metering_mask, image: metering_mask,
..default() ..default()
}, },
Node { Node {

View file

@ -84,6 +84,7 @@ fn setup(
commands.spawn(( commands.spawn((
Sprite { Sprite {
image: texture_handle.clone(), image: texture_handle.clone(),
texture_atlas: Some(TextureAtlas::from(texture_atlas_handle.clone())),
custom_size: Some(tile_size), custom_size: Some(tile_size),
..default() ..default()
}, },
@ -92,7 +93,6 @@ fn setup(
rotation, rotation,
scale, scale,
}, },
TextureAtlas::from(texture_atlas_handle.clone()),
AnimationTimer(timer), AnimationTimer(timer),
)); ));
} }
@ -112,13 +112,16 @@ struct AnimationTimer(Timer);
fn animate_sprite( fn animate_sprite(
time: Res<Time>, time: Res<Time>,
texture_atlases: Res<Assets<TextureAtlasLayout>>, texture_atlases: Res<Assets<TextureAtlasLayout>>,
mut query: Query<(&mut AnimationTimer, &mut TextureAtlas)>, mut query: Query<(&mut AnimationTimer, &mut Sprite)>,
) { ) {
for (mut timer, mut sheet) in query.iter_mut() { for (mut timer, mut sprite) in query.iter_mut() {
timer.tick(time.delta()); timer.tick(time.delta());
if timer.just_finished() { if timer.just_finished() {
let texture_atlas = texture_atlases.get(&sheet.layout).unwrap(); let Some(atlas) = &mut sprite.texture_atlas else {
sheet.index = (sheet.index + 1) % texture_atlas.textures.len(); continue;
};
let texture_atlas = texture_atlases.get(&atlas.layout).unwrap();
atlas.index = (atlas.index + 1) % texture_atlas.textures.len();
} }
} }
} }

View file

@ -44,14 +44,13 @@ fn setup(
}) })
.with_children(|parent| { .with_children(|parent| {
parent.spawn(( parent.spawn((
UiImage::new(texture_handle), UiImage::from_atlas_image(texture_handle, TextureAtlas::from(texture_atlas_handle)),
Node { Node {
width: Val::Px(256.), width: Val::Px(256.),
height: Val::Px(256.), height: Val::Px(256.),
..default() ..default()
}, },
BackgroundColor(ANTIQUE_WHITE.into()), BackgroundColor(ANTIQUE_WHITE.into()),
TextureAtlas::from(texture_atlas_handle),
Outline::new(Val::Px(8.0), Val::ZERO, CRIMSON.into()), Outline::new(Val::Px(8.0), Val::ZERO, CRIMSON.into()),
)); ));
parent parent
@ -65,13 +64,12 @@ fn setup(
}); });
} }
fn increment_atlas_index( fn increment_atlas_index(mut ui_images: Query<&mut UiImage>, keyboard: Res<ButtonInput<KeyCode>>) {
mut atlas_images: Query<&mut TextureAtlas>,
keyboard: Res<ButtonInput<KeyCode>>,
) {
if keyboard.just_pressed(KeyCode::Space) { if keyboard.just_pressed(KeyCode::Space) {
for mut atlas_image in &mut atlas_images { for mut ui_image in &mut ui_images {
atlas_image.index = (atlas_image.index + 1) % 6; if let Some(atlas) = &mut ui_image.texture_atlas {
atlas.index = (atlas.index + 1) % 6;
}
} }
} }
} }

View file

@ -19,17 +19,19 @@ fn main() {
fn button_system( fn button_system(
mut interaction_query: Query< mut interaction_query: Query<
(&Interaction, &mut TextureAtlas, &Children, &mut UiImage), (&Interaction, &Children, &mut UiImage),
(Changed<Interaction>, With<Button>), (Changed<Interaction>, With<Button>),
>, >,
mut text_query: Query<&mut Text>, mut text_query: Query<&mut Text>,
) { ) {
for (interaction, mut atlas, children, mut image) in &mut interaction_query { for (interaction, children, mut image) in &mut interaction_query {
let mut text = text_query.get_mut(children[0]).unwrap(); let mut text = text_query.get_mut(children[0]).unwrap();
match *interaction { match *interaction {
Interaction::Pressed => { Interaction::Pressed => {
**text = "Press".to_string(); **text = "Press".to_string();
if let Some(atlas) = &mut image.texture_atlas {
atlas.index = (atlas.index + 1) % 30; atlas.index = (atlas.index + 1) % 30;
}
image.color = GOLD.into(); image.color = GOLD.into();
} }
Interaction::Hovered => { Interaction::Hovered => {
@ -79,7 +81,13 @@ fn setup(
parent parent
.spawn(( .spawn((
Button, Button,
UiImage::new(texture_handle.clone()), UiImage::from_atlas_image(
texture_handle.clone(),
TextureAtlas {
index: idx,
layout: atlas_layout_handle.clone(),
},
),
Node { Node {
width: Val::Px(w), width: Val::Px(w),
height: Val::Px(h), height: Val::Px(h),
@ -91,10 +99,6 @@ fn setup(
..default() ..default()
}, },
ImageScaleMode::Sliced(slicer.clone()), ImageScaleMode::Sliced(slicer.clone()),
TextureAtlas {
index: idx,
layout: atlas_layout_handle.clone(),
},
)) ))
.with_children(|parent| { .with_children(|parent| {
parent.spawn(( parent.spawn((

View file

@ -58,7 +58,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
] { ] {
parent.spawn(( parent.spawn((
UiImage { UiImage {
texture: image.clone(), image: image.clone(),
flip_x, flip_x,
flip_y, flip_y,
..default() ..default()