Skip to content

Commit

Permalink
UI outlines radius (#15018)
Browse files Browse the repository at this point in the history
# Objective

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. 

## Solution

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.

---

## Showcase

<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>
  • Loading branch information
ickshonpe and janhohenheim authored Sep 4, 2024
1 parent 82128d7 commit a0f5ea0
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 131 deletions.
202 changes: 72 additions & 130 deletions crates/bevy_ui/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,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::*;
Expand Down Expand Up @@ -107,7 +107,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),
),
Expand Down Expand Up @@ -447,37 +446,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;
}
Expand All @@ -493,7 +499,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);
Expand All @@ -508,11 +514,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,
Expand All @@ -522,110 +523,18 @@ pub fn extract_uinode_borders(
.map(|r| r * ui_scale.0);

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_scaling: 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,
Expand All @@ -634,13 +543,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_scaling: 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,
},
);
}
}
}

Expand Down
8 changes: 7 additions & 1 deletion crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit a0f5ea0

Please sign in to comment.