UI outlines radius (#15018)

Fixes #13479

This also fixes the gaps you can sometimes observe in outlines
(screenshot from main, not this PR):

<img width="636" alt="outline-gaps"
src="https://github.com/user-attachments/assets/c11dae24-20f5-4aea-8ffc-1894ad2a2b79">

The outline around the last item in each section has vertical gaps.

Draw the outlines with corner radius using the existing border rendering
for uinodes. The outline radius is very simple to calculate. We just
take the computed border radius of the node, and if it's greater than
zero, add it to the distance from the edge of the node to the outer edge
of the node's outline.

---

<img width="634" alt="outlines-radius"
src="https://github.com/user-attachments/assets/1ecda26c-65c5-41ef-87e4-5d9171ddc3ae">

---------

Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
This commit is contained in:
ickshonpe 2024-09-04 23:30:16 +01:00 committed by François
parent dd929ad867
commit cd9fec3547
No known key found for this signature in database
2 changed files with 79 additions and 132 deletions

View file

@ -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<DefaultUiCamera>,
ui_scale: Extract<Res<UiScale>>,
uinode_query: Extract<
Query<
(
&Node,
&GlobalTransform,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
Option<&Parent>,
&Style,
&BorderColor,
),
Without<ContentSize>,
>,
Query<(
&Node,
&GlobalTransform,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
Option<&Parent>,
&Style,
AnyOf<(&BorderColor, &Outline)>,
)>,
>,
node_query: Extract<Query<&Node>>,
) {
let image = AssetId::<Image>::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
// <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
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<ExtractedUiNodes>,
default_ui_camera: Extract<DefaultUiCamera>,
uinode_query: Extract<
Query<(
&Node,
&GlobalTransform,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&Outline,
)>,
>,
) {
let image = AssetId::<Image>::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,
},
);
}
}
}

View file

@ -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 {