diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 38512e957d..b4ba44b5d0 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -23,8 +23,8 @@ use ui_texture_slice_pipeline::UiTextureSlicerPlugin; use crate::graph::{NodeUi, SubGraphUi}; use crate::{ - BackgroundColor, BorderColor, CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline, - Style, TargetCamera, UiImage, UiScale, Val, + BackgroundColor, BorderColor, CalculatedClip, DefaultUiCamera, Display, Node, Outline, Style, + TargetCamera, UiImage, UiScale, Val, }; use bevy_app::prelude::*; @@ -106,7 +106,6 @@ pub fn build_ui_render(app: &mut App) { 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_uinode_text.in_set(RenderUiSystem::ExtractText), ), @@ -445,37 +444,44 @@ pub fn extract_uinode_borders( default_ui_camera: Extract, ui_scale: Extract>, uinode_query: Extract< - Query< - ( - &Node, - &GlobalTransform, - &ViewVisibility, - Option<&CalculatedClip>, - Option<&TargetCamera>, - Option<&Parent>, - &Style, - &BorderColor, - ), - Without, - >, + Query<( + &Node, + &GlobalTransform, + &ViewVisibility, + Option<&CalculatedClip>, + Option<&TargetCamera>, + Option<&Parent>, + &Style, + AnyOf<(&BorderColor, &Outline)>, + )>, >, node_query: Extract>, ) { let image = AssetId::::default(); - for (uinode, global_transform, view_visibility, clip, camera, parent, style, border_color) in - &uinode_query + for ( + uinode, + global_transform, + view_visibility, + maybe_clip, + maybe_camera, + maybe_parent, + style, + (maybe_border_color, maybe_outline), + ) in &uinode_query { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) + let Some(camera_entity) = maybe_camera + .map(TargetCamera::entity) + .or(default_ui_camera.get()) else { continue; }; // Skip invisible borders if !view_visibility.get() - || border_color.0.is_fully_transparent() - || uinode.size().x <= 0. - || uinode.size().y <= 0. + || style.display == Display::None + || maybe_border_color.is_some_and(|border_color| border_color.0.is_fully_transparent()) + && maybe_outline.is_some_and(|outline| outline.color.is_fully_transparent()) { continue; } @@ -491,7 +497,7 @@ pub fn extract_uinode_borders( // Both vertical and horizontal percentage border values are calculated based on the width of the parent node // - let parent_width = parent + let parent_width = maybe_parent .and_then(|parent| node_query.get(parent.get()).ok()) .map(|parent_node| parent_node.size().x) .unwrap_or(ui_logical_viewport_size.x); @@ -506,11 +512,6 @@ pub fn extract_uinode_borders( let border = [left, top, right, bottom]; - // don't extract border if no border - if left == 0.0 && top == 0.0 && right == 0.0 && bottom == 0.0 { - continue; - } - let border_radius = [ uinode.border_radius.top_left, uinode.border_radius.top_right, @@ -521,110 +522,17 @@ pub fn extract_uinode_borders( let border_radius = clamp_radius(border_radius, uinode.size(), border.into()); - let transform = global_transform.compute_matrix(); - - extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), - ExtractedUiNode { - stack_index: uinode.stack_index, - // This translates the uinode's transform to the center of the current border rectangle - transform, - color: border_color.0.into(), - rect: Rect { - max: uinode.size(), - ..Default::default() - }, - image, - atlas_size: None, - clip: clip.map(|clip| clip.clip), - flip_x: false, - flip_y: false, - camera_entity, - border_radius, - border, - node_type: NodeType::Border, - }, - ); - } -} - -pub fn extract_uinode_outlines( - mut commands: Commands, - mut extracted_uinodes: ResMut, - default_ui_camera: Extract, - uinode_query: Extract< - Query<( - &Node, - &GlobalTransform, - &ViewVisibility, - Option<&CalculatedClip>, - Option<&TargetCamera>, - &Outline, - )>, - >, -) { - let image = AssetId::::default(); - 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() - || node.outline_width == 0. - { - continue; - } - - // Calculate the outline rects. - let inner_rect = Rect::from_center_size(Vec2::ZERO, node.size() + 2. * node.outline_offset); - let outer_rect = inner_rect.inflate(node.outline_width()); - let outline_edges = [ - // Left edge - Rect::new( - outer_rect.min.x, - outer_rect.min.y, - inner_rect.min.x, - outer_rect.max.y, - ), - // Right edge - Rect::new( - inner_rect.max.x, - outer_rect.min.y, - outer_rect.max.x, - outer_rect.max.y, - ), - // Top edge - Rect::new( - inner_rect.min.x, - outer_rect.min.y, - inner_rect.max.x, - inner_rect.min.y, - ), - // Bottom edge - Rect::new( - inner_rect.min.x, - inner_rect.max.y, - inner_rect.max.x, - outer_rect.max.y, - ), - ]; - - let world_from_local = global_transform.compute_matrix(); - for edge in outline_edges { - if edge.min.x < edge.max.x && edge.min.y < edge.max.y { + // don't extract border if no border or the node is zero-sized (a zero sized node can still have an outline). + if uinode.size().x > 0. && uinode.size().y > 0. && border != [0.; 4] { + if let Some(border_color) = maybe_border_color { extracted_uinodes.uinodes.insert( commands.spawn_empty().id(), ExtractedUiNode { - stack_index: node.stack_index, - // This translates the uinode's transform to the center of the current border rectangle - transform: world_from_local - * Mat4::from_translation(edge.center().extend(0.)), - color: outline.color.into(), + stack_index: uinode.stack_index, + transform: global_transform.compute_matrix(), + color: border_color.0.into(), rect: Rect { - max: edge.size(), + max: uinode.size(), ..Default::default() }, image, @@ -633,13 +541,46 @@ pub fn extract_uinode_outlines( flip_x: false, flip_y: false, camera_entity, - border: [0.; 4], - border_radius: [0.; 4], - node_type: NodeType::Rect, + border_radius, + border, + node_type: NodeType::Border, }, ); } } + + if let Some(outline) = maybe_outline { + let outer_distance = uinode.outline_offset() + uinode.outline_width(); + let outline_radius = border_radius.map(|radius| { + if radius > 0. { + radius + outer_distance + } else { + 0. + } + }); + let outline_size = uinode.size() + 2. * outer_distance; + extracted_uinodes.uinodes.insert( + commands.spawn_empty().id(), + ExtractedUiNode { + stack_index: uinode.stack_index, + transform: global_transform.compute_matrix(), + color: outline.color.into(), + rect: Rect { + max: outline_size, + ..Default::default() + }, + image, + atlas_size: None, + clip: maybe_clip.map(|clip| clip.clip), + flip_x: false, + flip_y: false, + camera_entity, + border: [uinode.outline_width(); 4], + border_radius: outline_radius, + node_type: NodeType::Border, + }, + ); + } } } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index ce4edf5e5b..228dd2d5ea 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -113,11 +113,17 @@ impl Node { } #[inline] - /// Returns the thickness of the UI node's outline. + /// Returns the thickness of the UI node's outline in logical pixels. /// If this value is negative or `0.` then no outline will be rendered. pub fn outline_width(&self) -> f32 { self.outline_width } + + #[inline] + /// Returns the amount of space between the outline and the edge of the node in logical pixels. + pub fn outline_offset(&self) -> f32 { + self.outline_offset + } } impl Node {