diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 95fa780f906e0..49c3a609ddb3e 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -74,7 +74,7 @@ impl Default for NodeBundle { } /// A UI node that is an image -#[derive(Bundle, Debug, Default)] +#[derive(Bundle, Debug)] pub struct ImageBundle { /// Describes the logical size of the node pub node: Node, @@ -84,8 +84,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, @@ -114,10 +112,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(Color::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, @@ -127,8 +145,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, @@ -159,6 +175,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(Color::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 /// @@ -298,13 +335,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. @@ -334,7 +367,6 @@ impl Default for ButtonBundle { border_color: BorderColor(Color::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 b4d333735b6ef..4dd6523215b73 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -58,8 +58,10 @@ pub const UI_SHADER_HANDLE: Handle = Handle::weak_from_u128(130128470471 #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum RenderUiSystem { - ExtractNode, - ExtractAtlasNode, + ExtractBackgrounds, + ExtractImages, + ExtractDecorations, + ExtractText, } pub fn build_ui_render(app: &mut App) { @@ -77,16 +79,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::ExtractDecorations, + 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.after(RenderUiSystem::ExtractAtlasNode), + extract_uinode_background_colors.in_set(RenderUiSystem::ExtractBackgrounds), + extract_uinode_images.in_set(RenderUiSystem::ExtractImages), + extract_uinode_borders.in_set(RenderUiSystem::ExtractDecorations), + extract_uinode_outlines.in_set(RenderUiSystem::ExtractDecorations), #[cfg(feature = "bevy_text")] - extract_text_uinodes.after(RenderUiSystem::ExtractAtlasNode), - extract_uinode_outlines.after(RenderUiSystem::ExtractAtlasNode), + extract_uinode_text.in_set(RenderUiSystem::ExtractText), ), ) .add_systems( @@ -172,6 +185,130 @@ 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, + images: Extract>>, + texture_atlases: Extract>>, + default_ui_camera: Extract, + uinode_query: Extract< + Query<( + &Node, + &GlobalTransform, + &ViewVisibility, + Option<&CalculatedClip>, + Option<&TargetCamera>, + &UiImage, + Option<&TextureAtlas>, + )>, + >, +) { + for (uinode, transform, view_visibility, clip, camera, image, atlas) 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; + } + + // Skip loading images + if !images.contains(&image.texture) { + 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]; + let mut atlas_size = layout.size; + 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., @@ -195,12 +332,12 @@ pub fn extract_uinode_borders( ( &Node, &GlobalTransform, - &Style, - &BorderColor, - Option<&Parent>, &ViewVisibility, Option<&CalculatedClip>, Option<&TargetCamera>, + Option<&Parent>, + &Style, + &BorderColor, ), Without, >, @@ -209,13 +346,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() @@ -314,19 +452,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() @@ -397,88 +536,6 @@ pub fn extract_uinode_outlines( } } -pub fn extract_uinodes( - mut extracted_uinodes: ResMut, - images: Extract>>, - texture_atlases: Extract>>, - default_ui_camera: Extract, - uinode_query: Extract< - Query<( - Entity, - &Node, - &GlobalTransform, - &BackgroundColor, - Option<&UiImage>, - &ViewVisibility, - Option<&CalculatedClip>, - Option<&TextureAtlas>, - Option<&TargetCamera>, - )>, - >, -) { - for (entity, uinode, transform, color, maybe_image, view_visibility, clip, atlas, camera) 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; - } - - let (image, flip_x, flip_y) = if let Some(image) = maybe_image { - // Skip loading images - if !images.contains(&image.texture) { - continue; - } - (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]; - let mut atlas_size = layout.size; - 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 @@ -554,7 +611,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>, @@ -565,21 +622,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 2900560a6d059..d042e62041e01 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -77,7 +77,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/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index c92dafb5da4a9..a9eb6b44a2a71 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1582,7 +1582,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( @@ -1722,6 +1721,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 @@ -1738,6 +1739,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/ui/ui_texture_atlas.rs b/examples/ui/ui_texture_atlas.rs index 70af3e9743a40..8bbff9a636c5b 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: Color::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, Color::CRIMSON), + )); parent.spawn(TextBundle::from_sections([ TextSection::new("press ".to_string(), text_style.clone()), TextSection::new(