diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 3551d4b48a8ff..f760b5377086f 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -93,10 +93,6 @@ pub struct ImageBundle { 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, /// The image of the node pub image: UiImage, /// The size of the image in pixels @@ -140,10 +136,6 @@ pub struct AtlasImageBundle { 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, /// The image of the node pub image: UiImage, /// A handle to the texture atlas to use for this Ui Node @@ -319,10 +311,6 @@ pub struct ButtonBundle { pub interaction: Interaction, /// 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 @@ -349,13 +337,12 @@ pub struct ButtonBundle { impl Default for ButtonBundle { fn default() -> Self { Self { - focus_policy: FocusPolicy::Block, node: Default::default(), button: Default::default(), style: Default::default(), - border_color: BorderColor(Color::NONE), interaction: Default::default(), - background_color: Default::default(), + focus_policy: FocusPolicy::Block, + border_color: BorderColor(Color::NONE), image: Default::default(), transform: Default::default(), global_transform: Default::default(), diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 7c00c854cd257..f2fa85a71f3d5 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.into(), + 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, 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.into(), + 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.into(), - 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 d8f443a7eb401..171cd04d958ea 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 52a9112d8b14c..8d07000caaab8 100644 --- a/crates/bevy_ui/src/texture_slice.rs +++ b/crates/bevy_ui/src/texture_slice.rs @@ -10,7 +10,7 @@ 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 +35,6 @@ impl ComputedTextureSlices { &'a self, transform: &'a GlobalTransform, node: &'a Node, - background_color: &'a BackgroundColor, image: &'a UiImage, clip: Option<&'a CalculatedClip>, camera_entity: Entity, @@ -60,7 +59,7 @@ impl ComputedTextureSlices { let atlas_size = Some(self.image_size * scale); ExtractedUiNode { stack_index: node.stack_index, - color: background_color.0.into(), + color: image.color.into(), 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 3be256fe1efd9..e8051aff651d6 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: Color, /// 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: Color) -> 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/3d/split_screen.rs b/examples/3d/split_screen.rs index 5d9f9c87dbf2d..10458cd830e63 100644 --- a/examples/3d/split_screen.rs +++ b/examples/3d/split_screen.rs @@ -147,7 +147,7 @@ fn setup( ..default() }, border_color: Color::WHITE.into(), - background_color: DARK_GRAY.into(), + image: UiImage::default().with_color(DARK_GRAY.into()), ..default() }, )) diff --git a/examples/ecs/state.rs b/examples/ecs/state.rs index fb906a0c957c2..c2acde35be438 100644 --- a/examples/ecs/state.rs +++ b/examples/ecs/state.rs @@ -74,7 +74,7 @@ fn setup_menu(mut commands: Commands) { align_items: AlignItems::Center, ..default() }, - background_color: NORMAL_BUTTON.into(), + image: UiImage::default().with_color(NORMAL_BUTTON), ..default() }) .with_children(|parent| { @@ -95,21 +95,21 @@ fn setup_menu(mut commands: Commands) { fn menu( mut next_state: ResMut>, mut interaction_query: Query< - (&Interaction, &mut BackgroundColor), + (&Interaction, &mut UiImage), (Changed, With