diff --git a/Cargo.toml b/Cargo.toml index 355b0a52aed6c..0bb1494cc1927 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1933,6 +1933,16 @@ description = "Illustrates how to scale the UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_texture_atlas" +path = "examples/ui/ui_texture_atlas.rs" + +[package.metadata.example.ui_texture_atlas] +name = "UI Texture Atlas" +description = "Illustrates how to use TextureAtlases in UI" +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 6b41048277203..a2eb9980edce3 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -157,8 +157,12 @@ impl Plugin for UiPlugin { .ambiguous_with(widget::text_system); system - }) - .add_systems( + }); + app.add_systems( + PostUpdate, + widget::update_atlas_content_size_system.before(UiSystem::Layout), + ); + app.add_systems( PostUpdate, ( ui_layout_system diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index dd04f780e83a4..468a155aa0802 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -3,13 +3,15 @@ use crate::{ widget::{Button, TextFlags, UiImageSize}, BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, - ZIndex, + UiTextureAtlasImage, ZIndex, }; +use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; use bevy_render::{ prelude::{Color, ComputedVisibility}, view::Visibility, }; +use bevy_sprite::TextureAtlas; #[cfg(feature = "bevy_text")] use bevy_text::{Text, TextAlignment, TextLayoutInfo, TextSection, TextStyle}; use bevy_transform::prelude::{GlobalTransform, Transform}; @@ -109,6 +111,51 @@ pub struct ImageBundle { pub z_index: ZIndex, } +/// A UI node that is a texture atlas sprite +#[derive(Bundle, Debug, Default)] +pub struct AtlasImageBundle { + /// Describes the logical size of the node + /// + /// This field is automatically managed by the UI layout system. + /// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component. + pub node: Node, + /// Styles which control the layout (size and position) of the node and it's children + /// In some cases these styles also affect how the node drawn/painted. + pub style: Style, + /// The calculated size based on the given image + pub calculated_size: ContentSize, + /// The background color, which serves as a "fill" for this node + /// + /// Combines with `UiImage` to tint the provided image. + pub background_color: BackgroundColor, + /// A handle to the texture atlas to use for this Ui Node + pub texture_atlas: Handle, + /// The descriptor for which sprite to use from the given texture atlas + pub texture_atlas_image: UiTextureAtlasImage, + /// Whether this node should block interaction with lower nodes + pub focus_policy: FocusPolicy, + /// The size of the image in pixels + /// + /// This field is set automatically + pub image_size: UiImageSize, + /// The transform of the node + /// + /// This field is automatically managed by the UI layout system. + /// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component. + pub transform: Transform, + /// The global transform of the node + /// + /// This field is automatically managed by the UI layout system. + /// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component. + pub global_transform: GlobalTransform, + /// Describes the visibility properties of the node + pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, + /// Indicates the depth at which the node should appear in the UI + pub z_index: ZIndex, +} + #[cfg(feature = "bevy_text")] /// A UI node that is text #[derive(Bundle, Debug)] diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 18e3bb560d8c2..a293658fb165e 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -9,6 +9,7 @@ use bevy_window::{PrimaryWindow, Window}; pub use pipeline::*; pub use render_pass::*; +use crate::UiTextureAtlasImage; use crate::{ prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, Node, UiImage, UiStack, }; @@ -82,6 +83,7 @@ pub fn build_ui_render(app: &mut App) { extract_default_ui_camera_view::, extract_default_ui_camera_view::, extract_uinodes.in_set(RenderUiSystem::ExtractNode), + extract_atlas_uinodes.after(RenderUiSystem::ExtractNode), extract_uinode_borders.after(RenderUiSystem::ExtractNode), #[cfg(feature = "bevy_text")] extract_text_uinodes.after(RenderUiSystem::ExtractNode), @@ -166,6 +168,83 @@ pub struct ExtractedUiNodes { pub uinodes: Vec, } +pub fn extract_atlas_uinodes( + mut extracted_uinodes: ResMut, + images: Extract>>, + texture_atlases: Extract>>, + + ui_stack: Extract>, + uinode_query: Extract< + Query< + ( + &Node, + &GlobalTransform, + &BackgroundColor, + &ComputedVisibility, + Option<&CalculatedClip>, + &Handle, + &UiTextureAtlasImage, + ), + Without, + >, + >, +) { + for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { + if let Ok((uinode, transform, color, visibility, clip, texture_atlas_handle, atlas_image)) = + uinode_query.get(*entity) + { + // Skip invisible and completely transparent nodes + if !visibility.is_visible() || color.0.a() == 0.0 { + continue; + } + + let (mut atlas_rect, mut atlas_size, image) = + if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { + let atlas_rect = *texture_atlas + .textures + .get(atlas_image.index) + .unwrap_or_else(|| { + panic!( + "Atlas index {:?} does not exist for texture atlas handle {:?}.", + atlas_image.index, + texture_atlas_handle.id(), + ) + }); + ( + atlas_rect, + texture_atlas.size, + texture_atlas.texture.clone(), + ) + } else { + // Atlas not present in assets resource (should this warn the user?) + continue; + }; + + // Skip loading images + if !images.contains(&image) { + continue; + } + + let scale = uinode.size() / atlas_rect.size(); + atlas_rect.min *= scale; + atlas_rect.max *= scale; + atlas_size *= scale; + + extracted_uinodes.uinodes.push(ExtractedUiNode { + stack_index, + transform: transform.compute_matrix(), + color: color.0, + rect: atlas_rect, + clip: clip.map(|clip| clip.clip), + image, + atlas_size: Some(atlas_size), + flip_x: atlas_image.flip_x, + flip_y: atlas_image.flip_y, + }); + } + } +} + fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 { match value { Val::Auto => 0., @@ -288,14 +367,17 @@ pub fn extract_uinodes( images: Extract>>, ui_stack: Extract>, uinode_query: Extract< - Query<( - &Node, - &GlobalTransform, - &BackgroundColor, - Option<&UiImage>, - &ComputedVisibility, - Option<&CalculatedClip>, - )>, + Query< + ( + &Node, + &GlobalTransform, + &BackgroundColor, + Option<&UiImage>, + &ComputedVisibility, + Option<&CalculatedClip>, + ), + Without, + >, >, ) { extracted_uinodes.uinodes.clear(); @@ -327,13 +409,13 @@ pub fn extract_uinodes( min: Vec2::ZERO, max: uinode.calculated_size, }, + clip: clip.map(|clip| clip.clip), image, atlas_size: None, - clip: clip.map(|clip| clip.clip), flip_x, flip_y, }); - } + }; } } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 96a69972024ad..f140bca9714bf 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1563,6 +1563,18 @@ impl From for BackgroundColor { } } +/// The atlas sprite to be used in a UI Texture Atlas Node +#[derive(Component, Clone, Debug, Reflect, FromReflect, Default)] +#[reflect(Component, Default)] +pub struct UiTextureAtlasImage { + /// Texture index in the TextureAtlas + pub index: usize, + /// Whether to flip the sprite in the X axis + pub flip_x: bool, + /// Whether to flip the sprite in the Y axis + pub flip_y: bool, +} + /// The border color of the UI node. #[derive(Component, Copy, Clone, Debug, Reflect, FromReflect)] #[reflect(FromReflect, Component, Default)] diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index 2453c9dd438a5..b96031029af40 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -1,5 +1,7 @@ -use crate::{measurement::AvailableSpace, ContentSize, Measure, Node, UiImage}; -use bevy_asset::Assets; +use crate::{ + measurement::AvailableSpace, ContentSize, Measure, Node, UiImage, UiTextureAtlasImage, +}; +use bevy_asset::{Assets, Handle}; #[cfg(feature = "bevy_text")] use bevy_ecs::query::Without; use bevy_ecs::{ @@ -11,6 +13,7 @@ use bevy_ecs::{ use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect, ReflectFromReflect}; use bevy_render::texture::Image; +use bevy_sprite::TextureAtlas; #[cfg(feature = "bevy_text")] use bevy_text::Text; @@ -89,3 +92,41 @@ pub fn update_image_content_size_system( } } } + +/// Updates content size of the node based on the texture atlas sprite +pub fn update_atlas_content_size_system( + atlases: Res>, + #[cfg(feature = "bevy_text")] mut atlas_query: Query< + ( + &mut ContentSize, + &Handle, + &UiTextureAtlasImage, + &mut UiImageSize, + ), + (With, Without, Without), + >, + #[cfg(not(feature = "bevy_text"))] mut atlas_query: Query< + ( + &mut ContentSize, + &Handle, + &UiTextureAtlasImage, + &mut UiImageSize, + ), + (With, Without), + >, +) { + for (mut content_size, atlas, atlas_image, mut image_size) in &mut atlas_query { + if let Some(atlas) = atlases.get(atlas) { + let texture_rect = atlas.textures[atlas_image.index]; + let size = Vec2::new( + texture_rect.max.x - texture_rect.min.x, + texture_rect.max.y - texture_rect.min.y, + ); + // Update only if size has changed to avoid needless layout calculations + if size != image_size.size { + image_size.size = size; + content_size.set(ImageMeasure { size }); + } + } + } +} diff --git a/examples/README.md b/examples/README.md index 904125a84b8b9..e0df27d8ad735 100644 --- a/examples/README.md +++ b/examples/README.md @@ -350,6 +350,7 @@ Example | Description [Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI [UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI [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 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 [Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality. diff --git a/examples/ui/ui_texture_atlas.rs b/examples/ui/ui_texture_atlas.rs new file mode 100644 index 0000000000000..31ba99e450f0a --- /dev/null +++ b/examples/ui/ui_texture_atlas.rs @@ -0,0 +1,86 @@ +//! This example illustrates how to use `TextureAtlases` within ui + +use bevy::{prelude::*, winit::WinitSettings}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set( + // This sets image filtering to nearest + // This is done to prevent textures with low resolution (e.g. pixel art) from being blurred + // by linear filtering. + ImagePlugin::default_nearest(), + )) + // 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, increment_atlas_index) + .run(); +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut texture_atlases: ResMut>, +) { + // Camera + commands.spawn(Camera2dBundle::default()); + + let text_style = TextStyle { + color: Color::ANTIQUE_WHITE, + font_size: 20., + ..default() + }; + + let texture_handle = asset_server.load("textures/rpg/chars/gabe/gabe-idle-run.png"); + let texture_atlas = + TextureAtlas::from_grid(texture_handle, Vec2::new(24.0, 24.0), 7, 1, None, None); + let texture_atlas_handle = texture_atlases.add(texture_atlas); + + // root node + commands + .spawn(NodeBundle { + style: Style { + flex_direction: FlexDirection::Column, + width: Val::Percent(100.0), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + row_gap: Val::Px(text_style.font_size * 2.), + ..default() + }, + ..default() + }) + .with_children(|parent| { + parent.spawn((AtlasImageBundle { + style: Style { + width: Val::Px(256.), + height: Val::Px(256.), + ..default() + }, + texture_atlas: texture_atlas_handle, + texture_atlas_image: UiTextureAtlasImage::default(), + ..default() + },)); + parent.spawn(TextBundle::from_sections([ + TextSection::new("press ".to_string(), text_style.clone()), + TextSection::new( + "space".to_string(), + TextStyle { + color: Color::YELLOW, + ..text_style.clone() + }, + ), + TextSection::new(" to advance frames".to_string(), text_style), + ])); + }); +} + +fn increment_atlas_index( + mut atlas_images: Query<&mut UiTextureAtlasImage>, + keyboard: Res>, +) { + if keyboard.just_pressed(KeyCode::Space) { + for mut atlas_image in &mut atlas_images { + atlas_image.index = (atlas_image.index + 1) % 6; + } + } +}