From 3aaf42a8f655863060e69a850e68b71641b0d813 Mon Sep 17 00:00:00 2001 From: Ben Frankel Date: Sun, 31 Dec 2023 17:33:30 -0800 Subject: [PATCH] Decouple `BackgroundColor` from `UiImage` --- crates/bevy_ui/src/node_bundles.rs | 54 +++- crates/bevy_ui/src/render/mod.rs | 275 ++++++++++-------- .../src/render/ui_material_pipeline.rs | 2 +- crates/bevy_ui/src/texture_slice.rs | 7 +- crates/bevy_ui/src/ui_node.rs | 10 +- examples/ui/ui_texture_atlas.rs | 20 +- examples/ui/ui_texture_slice.rs | 2 +- 7 files changed, 231 insertions(+), 139 deletions(-) diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 19845002e3d49e..d7f7649dbdad15 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -81,7 +81,7 @@ impl Default for NodeBundle { /// /// You may add the following components to enable additional behaviours /// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture -#[derive(Bundle, Debug, Default)] +#[derive(Bundle, Debug)] pub struct ImageBundle { /// Describes the logical size of the node pub node: Node, @@ -91,8 +91,6 @@ pub struct ImageBundle { /// 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, /// The image of the node pub image: UiImage, @@ -121,10 +119,30 @@ pub struct ImageBundle { pub z_index: ZIndex, } +impl Default for ImageBundle { + fn default() -> Self { + Self { + node: Default::default(), + style: Default::default(), + calculated_size: Default::default(), + background_color: BackgroundColor(LegacyColor::NONE), + image: Default::default(), + image_size: Default::default(), + focus_policy: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + visibility: Default::default(), + inherited_visibility: Default::default(), + view_visibility: Default::default(), + z_index: Default::default(), + } + } +} + /// A UI node that is a texture atlas sprite /// /// This bundle is identical to [`ImageBundle`] with an additional [`TextureAtlas`] component. -#[derive(Bundle, Debug, Default)] +#[derive(Bundle, Debug)] pub struct AtlasImageBundle { /// Describes the logical size of the node pub node: Node, @@ -134,8 +152,6 @@ pub struct AtlasImageBundle { /// 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, /// The image of the node pub image: UiImage, @@ -166,6 +182,27 @@ pub struct AtlasImageBundle { pub z_index: ZIndex, } +impl Default for AtlasImageBundle { + fn default() -> Self { + Self { + node: Default::default(), + style: Default::default(), + calculated_size: Default::default(), + background_color: BackgroundColor(LegacyColor::NONE), + image: Default::default(), + texture_atlas: Default::default(), + focus_policy: Default::default(), + image_size: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + visibility: Default::default(), + inherited_visibility: Default::default(), + view_visibility: Default::default(), + z_index: Default::default(), + } + } +} + #[cfg(feature = "bevy_text")] /// A UI node that is text /// @@ -310,13 +347,9 @@ pub struct ButtonBundle { /// Whether this node should block interaction with lower nodes pub focus_policy: FocusPolicy, /// The background color, which serves as a "fill" for this node - /// - /// When combined with `UiImage`, tints the provided image. pub background_color: BackgroundColor, /// The color of the Node's border pub border_color: BorderColor, - /// The image of the node - pub image: UiImage, /// The transform of the node /// /// This component is automatically managed by the UI layout system. @@ -346,7 +379,6 @@ impl Default for ButtonBundle { border_color: BorderColor(LegacyColor::NONE), interaction: Default::default(), background_color: Default::default(), - image: Default::default(), transform: Default::default(), global_transform: Default::default(), visibility: Default::default(), diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 934fa90813b561..9f33f73b4f9b3c 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -59,7 +59,10 @@ pub const UI_SHADER_HANDLE: Handle = Handle::weak_from_u128(130128470471 #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum RenderUiSystem { - ExtractNode, + ExtractBackgrounds, + ExtractImages, + ExtractBorders, + ExtractText, } pub fn build_ui_render(app: &mut App) { @@ -77,16 +80,27 @@ pub fn build_ui_render(app: &mut App) { .allow_ambiguous_resource::() .init_resource::>() .add_render_command::() + .configure_sets( + ExtractSchedule, + ( + RenderUiSystem::ExtractBackgrounds, + RenderUiSystem::ExtractImages, + RenderUiSystem::ExtractBorders, + RenderUiSystem::ExtractText, + ) + .chain(), + ) .add_systems( ExtractSchedule, ( extract_default_ui_camera_view::, extract_default_ui_camera_view::, - extract_uinodes.in_set(RenderUiSystem::ExtractNode), - extract_uinode_borders, + extract_uinode_background_colors.in_set(RenderUiSystem::ExtractBackgrounds), + extract_uinode_images.in_set(RenderUiSystem::ExtractImages), + extract_uinode_borders.in_set(RenderUiSystem::ExtractBorders), + extract_uinode_outlines.in_set(RenderUiSystem::ExtractBorders), #[cfg(feature = "bevy_text")] - extract_text_uinodes, - extract_uinode_outlines, + extract_uinode_text.in_set(RenderUiSystem::ExtractText), ), ) .add_systems( @@ -148,6 +162,134 @@ pub struct ExtractedUiNodes { pub uinodes: EntityHashMap, } +pub fn extract_uinode_background_colors( + mut extracted_uinodes: ResMut, + default_ui_camera: Extract, + uinode_query: Extract< + Query<( + Entity, + &Node, + &GlobalTransform, + &ViewVisibility, + Option<&CalculatedClip>, + Option<&TargetCamera>, + &BackgroundColor, + )>, + >, +) { + for (entity, uinode, transform, view_visibility, clip, camera, background_color) in + &uinode_query + { + let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) + else { + continue; + }; + + // Skip invisible backgrounds + if !view_visibility.get() || background_color.0.is_fully_transparent() { + continue; + } + + extracted_uinodes.uinodes.insert( + entity, + ExtractedUiNode { + stack_index: uinode.stack_index, + transform: transform.compute_matrix(), + color: background_color.0, + rect: Rect { + min: Vec2::ZERO, + max: uinode.calculated_size, + }, + clip: clip.map(|clip| clip.clip), + image: AssetId::default(), + atlas_size: None, + flip_x: false, + flip_y: false, + camera_entity, + }, + ); + } +} + +pub fn extract_uinode_images( + mut commands: Commands, + mut extracted_uinodes: ResMut, + texture_atlases: Extract>>, + default_ui_camera: Extract, + uinode_query: Extract< + Query<( + &Node, + &GlobalTransform, + &ViewVisibility, + Option<&CalculatedClip>, + Option<&TargetCamera>, + &UiImage, + Option<&TextureAtlas>, + Option<&ComputedTextureSlices>, + )>, + >, +) { + for (uinode, transform, view_visibility, clip, camera, image, atlas, slices) in &uinode_query { + let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) + else { + continue; + }; + + // Skip invisible images + if !view_visibility.get() || image.color.is_fully_transparent() { + continue; + } + + if let Some(slices) = slices { + extracted_uinodes.uinodes.extend( + slices + .extract_ui_nodes(transform, uinode, image.color, image, clip, camera_entity) + .map(|e| (commands.spawn_empty().id(), e)), + ); + continue; + } + + let (rect, atlas_size) = match atlas { + Some(atlas) => { + let Some(layout) = texture_atlases.get(&atlas.layout) else { + // Atlas not present in assets resource (should this warn the user?) + continue; + }; + let mut atlas_rect = layout.textures[atlas.index].as_rect(); + let mut atlas_size = layout.size.as_vec2(); + let scale = uinode.size() / atlas_rect.size(); + atlas_rect.min *= scale; + atlas_rect.max *= scale; + atlas_size *= scale; + (atlas_rect, Some(atlas_size)) + } + None => ( + Rect { + min: Vec2::ZERO, + max: uinode.calculated_size, + }, + None, + ), + }; + + extracted_uinodes.uinodes.insert( + commands.spawn_empty().id(), + ExtractedUiNode { + stack_index: uinode.stack_index, + transform: transform.compute_matrix(), + color: image.color, + rect, + clip: clip.map(|clip| clip.clip), + image: image.texture.id(), + atlas_size, + flip_x: image.flip_x, + flip_y: image.flip_y, + camera_entity, + }, + ); + } +} + pub(crate) fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 { match value { Val::Auto => 0., @@ -171,12 +313,12 @@ pub fn extract_uinode_borders( ( &Node, &GlobalTransform, - &Style, - &BorderColor, - Option<&Parent>, &ViewVisibility, Option<&CalculatedClip>, Option<&TargetCamera>, + Option<&Parent>, + &Style, + &BorderColor, ), Without, >, @@ -185,13 +327,14 @@ pub fn extract_uinode_borders( ) { let image = AssetId::::default(); - for (node, global_transform, style, border_color, parent, view_visibility, clip, camera) in + for (node, global_transform, view_visibility, clip, camera, parent, style, border_color) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; }; + // Skip invisible borders if !view_visibility.get() || border_color.0.is_fully_transparent() @@ -290,19 +433,20 @@ pub fn extract_uinode_outlines( Query<( &Node, &GlobalTransform, - &Outline, &ViewVisibility, Option<&CalculatedClip>, Option<&TargetCamera>, + &Outline, )>, >, ) { let image = AssetId::::default(); - for (node, global_transform, outline, view_visibility, maybe_clip, camera) in &uinode_query { + for (node, global_transform, view_visibility, maybe_clip, camera, outline) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; }; + // Skip invisible outlines if !view_visibility.get() || outline.color.is_fully_transparent() @@ -373,104 +517,6 @@ pub fn extract_uinode_outlines( } } -pub fn extract_uinodes( - mut commands: Commands, - mut extracted_uinodes: ResMut, - texture_atlases: Extract>>, - default_ui_camera: Extract, - uinode_query: Extract< - Query<( - Entity, - &Node, - &GlobalTransform, - &BackgroundColor, - Option<&UiImage>, - &ViewVisibility, - Option<&CalculatedClip>, - Option<&TextureAtlas>, - Option<&TargetCamera>, - Option<&ComputedTextureSlices>, - )>, - >, -) { - for ( - entity, - uinode, - transform, - color, - maybe_image, - view_visibility, - clip, - atlas, - camera, - slices, - ) in uinode_query.iter() - { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) - else { - continue; - }; - // Skip invisible and completely transparent nodes - if !view_visibility.get() || color.0.is_fully_transparent() { - continue; - } - - if let Some((image, slices)) = maybe_image.zip(slices) { - extracted_uinodes.uinodes.extend( - slices - .extract_ui_nodes(transform, uinode, color, image, clip, camera_entity) - .map(|e| (commands.spawn_empty().id(), e)), - ); - continue; - } - - let (image, flip_x, flip_y) = if let Some(image) = maybe_image { - (image.texture.id(), image.flip_x, image.flip_y) - } else { - (AssetId::default(), false, false) - }; - - let (rect, atlas_size) = match atlas { - Some(atlas) => { - let Some(layout) = texture_atlases.get(&atlas.layout) else { - // Atlas not present in assets resource (should this warn the user?) - continue; - }; - let mut atlas_rect = layout.textures[atlas.index].as_rect(); - let mut atlas_size = layout.size.as_vec2(); - let scale = uinode.size() / atlas_rect.size(); - atlas_rect.min *= scale; - atlas_rect.max *= scale; - atlas_size *= scale; - (atlas_rect, Some(atlas_size)) - } - None => ( - Rect { - min: Vec2::ZERO, - max: uinode.calculated_size, - }, - None, - ), - }; - - extracted_uinodes.uinodes.insert( - entity, - ExtractedUiNode { - stack_index: uinode.stack_index, - transform: transform.compute_matrix(), - color: color.0, - rect, - clip: clip.map(|clip| clip.clip), - image, - atlas_size, - flip_x, - flip_y, - camera_entity, - }, - ); - } -} - /// The UI camera is "moved back" by this many units (plus the [`UI_CAMERA_TRANSFORM_OFFSET`]) and also has a view /// distance of this many units. This ensures that with a left-handed projection, /// as ui elements are "stacked on top of each other", they are within the camera's view @@ -546,7 +592,7 @@ pub fn extract_default_ui_camera_view( } #[cfg(feature = "bevy_text")] -pub fn extract_text_uinodes( +pub fn extract_uinode_text( mut commands: Commands, mut extracted_uinodes: ResMut, camera_query: Extract>, @@ -557,21 +603,22 @@ pub fn extract_text_uinodes( Query<( &Node, &GlobalTransform, - &Text, - &TextLayoutInfo, &ViewVisibility, Option<&CalculatedClip>, Option<&TargetCamera>, + &Text, + &TextLayoutInfo, )>, >, ) { - for (uinode, global_transform, text, text_layout_info, view_visibility, clip, camera) in - uinode_query.iter() + for (uinode, global_transform, view_visibility, clip, camera, text, text_layout_info) in + &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; }; + // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !view_visibility.get() || uinode.size().x == 0. || uinode.size().y == 0. { continue; diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 2172b73ff4bd00..14a12f99a15c1c 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -74,7 +74,7 @@ where ExtractSchedule, ( extract_ui_materials::, - extract_ui_material_nodes::.in_set(RenderUiSystem::ExtractNode), + extract_ui_material_nodes::.in_set(RenderUiSystem::ExtractBackgrounds), ), ) .add_systems( diff --git a/crates/bevy_ui/src/texture_slice.rs b/crates/bevy_ui/src/texture_slice.rs index 24e77691e9bb88..af2e25753a8d20 100644 --- a/crates/bevy_ui/src/texture_slice.rs +++ b/crates/bevy_ui/src/texture_slice.rs @@ -5,12 +5,13 @@ use bevy_asset::{AssetEvent, Assets}; use bevy_ecs::prelude::*; use bevy_math::{Rect, Vec2}; +use bevy_render::color::LegacyColor; use bevy_render::texture::Image; use bevy_sprite::{ImageScaleMode, TextureSlice}; use bevy_transform::prelude::*; use bevy_utils::HashSet; -use crate::{BackgroundColor, CalculatedClip, ExtractedUiNode, Node, UiImage}; +use crate::{CalculatedClip, ExtractedUiNode, Node, UiImage}; /// Component storing texture slices for image nodes entities with a tiled or sliced [`ImageScaleMode`] /// @@ -35,7 +36,7 @@ impl ComputedTextureSlices { &'a self, transform: &'a GlobalTransform, node: &'a Node, - background_color: &'a BackgroundColor, + color: LegacyColor, image: &'a UiImage, clip: Option<&'a CalculatedClip>, camera_entity: Entity, @@ -60,7 +61,7 @@ impl ComputedTextureSlices { let atlas_size = Some(self.image_size * scale); ExtractedUiNode { stack_index: node.stack_index, - color: background_color.0, + color, transform: transform.compute_matrix(), rect, flip_x, diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index aef4e724a05b9c..c5c084d2e55d19 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1589,7 +1589,6 @@ pub enum GridPlacementError { /// The background color of the node /// /// This serves as the "fill" color. -/// When combined with [`UiImage`], tints the provided texture. #[derive(Component, Copy, Clone, Debug, Reflect)] #[reflect(Component, Default)] #[cfg_attr( @@ -1729,6 +1728,8 @@ impl Outline { #[derive(Component, Clone, Debug, Reflect, Default)] #[reflect(Component, Default)] pub struct UiImage { + /// The tint color used to draw the image + pub color: LegacyColor, /// Handle to the texture pub texture: Handle, /// Whether the image should be flipped along its x-axis @@ -1745,6 +1746,13 @@ impl UiImage { } } + /// Set the color tint + #[must_use] + pub const fn with_color(mut self, color: LegacyColor) -> Self { + self.color = color; + self + } + /// Flip the image along its x-axis #[must_use] pub const fn with_flip_x(mut self) -> Self { diff --git a/examples/ui/ui_texture_atlas.rs b/examples/ui/ui_texture_atlas.rs index 186ace924d5cb0..0e618bf6d5404c 100644 --- a/examples/ui/ui_texture_atlas.rs +++ b/examples/ui/ui_texture_atlas.rs @@ -49,16 +49,20 @@ fn setup( ..default() }) .with_children(|parent| { - parent.spawn(AtlasImageBundle { - style: Style { - width: Val::Px(256.), - height: Val::Px(256.), + parent.spawn(( + AtlasImageBundle { + style: Style { + width: Val::Px(256.), + height: Val::Px(256.), + ..default() + }, + background_color: LegacyColor::ANTIQUE_WHITE.into(), + texture_atlas: texture_atlas_handle.into(), + image: UiImage::new(texture_handle), ..default() }, - texture_atlas: texture_atlas_handle.into(), - image: UiImage::new(texture_handle), - ..default() - }); + Outline::new(Val::Px(8.0), Val::ZERO, LegacyColor::CRIMSON), + )); parent.spawn(TextBundle::from_sections([ TextSection::new("press ".to_string(), text_style.clone()), TextSection::new( diff --git a/examples/ui/ui_texture_slice.rs b/examples/ui/ui_texture_slice.rs index 33ea98eb6f8f2d..91b08cfd78daed 100644 --- a/examples/ui/ui_texture_slice.rs +++ b/examples/ui/ui_texture_slice.rs @@ -70,9 +70,9 @@ fn setup(mut commands: Commands, asset_server: Res) { margin: UiRect::all(Val::Px(20.0)), ..default() }, - image: image.clone().into(), ..default() }, + UiImage::new(image.clone()), ImageScaleMode::Sliced(slicer.clone()), )) .with_children(|parent| {