mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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:
parent
dd929ad867
commit
cd9fec3547
2 changed files with 79 additions and 132 deletions
|
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue