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_ecs::{component::Component, reflect::ReflectComponent};
use bevy_math::{URect, UVec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
#[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.
/// 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)
/// - [`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)
#[derive(Component, Default, Debug, Clone, Reflect)]
#[reflect(Component, Default, Debug)]
#[derive(Default, Debug, Clone, Reflect)]
#[reflect(Default, Debug)]
pub struct TextureAtlas {
/// Texture atlas layout handle
pub layout: Handle<TextureAtlasLayout>,

View file

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

View file

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

View file

@ -9,7 +9,7 @@ use bevy_render::{
texture::{Image, TRANSPARENT_IMAGE_HANDLE},
view::Visibility,
};
use bevy_sprite::BorderRect;
use bevy_sprite::{BorderRect, TextureAtlas};
use bevy_transform::components::Transform;
use bevy_utils::warn_once;
use bevy_window::{PrimaryWindow, WindowRef};
@ -2053,15 +2053,17 @@ pub struct UiImage {
/// Handle to the 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
pub flip_x: bool,
/// 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`](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.
pub rect: Option<Rect>,
}
@ -2079,8 +2081,9 @@ impl Default for UiImage {
// 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
color: Color::WHITE,
texture_atlas: None,
// 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_y: false,
rect: None,
@ -2092,7 +2095,7 @@ impl UiImage {
/// Create a new [`UiImage`] with the given texture.
pub fn new(texture: Handle<Image>) -> Self {
Self {
texture,
image: texture,
color: Color::WHITE,
..Default::default()
}
@ -2103,14 +2106,24 @@ impl UiImage {
/// This is primarily useful for debugging / mocking the extents of your image.
pub fn solid_color(color: Color) -> Self {
Self {
texture: Handle::default(),
image: Handle::default(),
color,
flip_x: false,
flip_y: false,
texture_atlas: 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
#[must_use]
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_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::Image;
use bevy_sprite::{TextureAtlas, TextureAtlasLayout};
use bevy_sprite::TextureAtlasLayout;
use bevy_window::{PrimaryWindow, Window};
use taffy::{MaybeMath, MaybeResolve};
@ -97,15 +97,7 @@ pub fn update_image_content_size_system(
textures: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlasLayout>>,
mut query: Query<
(
&mut ContentSize,
&UiImage,
&mut UiImageSize,
Option<&TextureAtlas>,
),
UpdateImageFilter,
>,
mut query: Query<(&mut ContentSize, &UiImage, &mut UiImageSize), UpdateImageFilter>,
) {
let combined_scale_factor = windows
.get_single()
@ -113,10 +105,10 @@ pub fn update_image_content_size_system(
.unwrap_or(1.)
* ui_scale.0;
for (mut content_size, image, mut image_size, atlas_image) in &mut query {
if let Some(size) = match atlas_image {
for (mut content_size, image, mut image_size) in &mut query {
if let Some(size) = match &image.texture_atlas {
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
if size != image_size.size

View file

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

View file

@ -84,6 +84,7 @@ fn setup(
commands.spawn((
Sprite {
image: texture_handle.clone(),
texture_atlas: Some(TextureAtlas::from(texture_atlas_handle.clone())),
custom_size: Some(tile_size),
..default()
},
@ -92,7 +93,6 @@ fn setup(
rotation,
scale,
},
TextureAtlas::from(texture_atlas_handle.clone()),
AnimationTimer(timer),
));
}
@ -112,13 +112,16 @@ struct AnimationTimer(Timer);
fn animate_sprite(
time: Res<Time>,
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());
if timer.just_finished() {
let texture_atlas = texture_atlases.get(&sheet.layout).unwrap();
sheet.index = (sheet.index + 1) % texture_atlas.textures.len();
let Some(atlas) = &mut sprite.texture_atlas else {
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| {
parent.spawn((
UiImage::new(texture_handle),
UiImage::from_atlas_image(texture_handle, TextureAtlas::from(texture_atlas_handle)),
Node {
width: Val::Px(256.),
height: Val::Px(256.),
..default()
},
BackgroundColor(ANTIQUE_WHITE.into()),
TextureAtlas::from(texture_atlas_handle),
Outline::new(Val::Px(8.0), Val::ZERO, CRIMSON.into()),
));
parent
@ -65,13 +64,12 @@ fn setup(
});
}
fn increment_atlas_index(
mut atlas_images: Query<&mut TextureAtlas>,
keyboard: Res<ButtonInput<KeyCode>>,
) {
fn increment_atlas_index(mut ui_images: Query<&mut UiImage>, keyboard: Res<ButtonInput<KeyCode>>) {
if keyboard.just_pressed(KeyCode::Space) {
for mut atlas_image in &mut atlas_images {
atlas_image.index = (atlas_image.index + 1) % 6;
for mut ui_image in &mut ui_images {
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(
mut interaction_query: Query<
(&Interaction, &mut TextureAtlas, &Children, &mut UiImage),
(&Interaction, &Children, &mut UiImage),
(Changed<Interaction>, With<Button>),
>,
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();
match *interaction {
Interaction::Pressed => {
**text = "Press".to_string();
atlas.index = (atlas.index + 1) % 30;
if let Some(atlas) = &mut image.texture_atlas {
atlas.index = (atlas.index + 1) % 30;
}
image.color = GOLD.into();
}
Interaction::Hovered => {
@ -79,7 +81,13 @@ fn setup(
parent
.spawn((
Button,
UiImage::new(texture_handle.clone()),
UiImage::from_atlas_image(
texture_handle.clone(),
TextureAtlas {
index: idx,
layout: atlas_layout_handle.clone(),
},
),
Node {
width: Val::Px(w),
height: Val::Px(h),
@ -91,10 +99,6 @@ fn setup(
..default()
},
ImageScaleMode::Sliced(slicer.clone()),
TextureAtlas {
index: idx,
layout: atlas_layout_handle.clone(),
},
))
.with_children(|parent| {
parent.spawn((

View file

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