Skip to content

Fix various bugs with UI rounded borders #13523

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions crates/bevy_ui/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,11 @@ pub fn extract_uinode_background_colors(
Option<&TargetCamera>,
&BackgroundColor,
Option<&BorderRadius>,
&Style,
Option<&Parent>,
)>,
>,
node_query: Extract<Query<&Node>>,
) {
for (
entity,
Expand All @@ -211,6 +214,8 @@ pub fn extract_uinode_background_colors(
camera,
background_color,
border_radius,
style,
parent,
) in &uinode_query
{
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
Expand All @@ -232,6 +237,23 @@ pub fn extract_uinode_background_colors(
// so we have to divide by `UiScale` to get the size of the UI viewport.
/ ui_scale.0;

// 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>
Comment on lines +240 to +241
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

layout and style is a complex topic, having these comments that refers back to source material helps a lot, thanks!

let parent_width = parent
.and_then(|parent| node_query.get(parent.get()).ok())
.map(|parent_node| parent_node.size().x)
.unwrap_or(ui_logical_viewport_size.x);
let left =
resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size);
let right =
resolve_border_thickness(style.border.right, parent_width, ui_logical_viewport_size);
let top =
resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size);
let bottom =
resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size);

let border = [left, top, right, bottom];
Comment on lines +242 to +255
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this makes sense and is straightforward to read 👍


let border_radius = if let Some(border_radius) = border_radius {
resolve_border_radius(
border_radius,
Expand Down Expand Up @@ -259,14 +281,15 @@ pub fn extract_uinode_background_colors(
flip_x: false,
flip_y: false,
camera_entity,
border: [0.; 4],
border,
border_radius,
node_type: NodeType::Rect,
},
);
}
}

#[allow(clippy::too_many_arguments)]
pub fn extract_uinode_images(
mut commands: Commands,
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
Expand All @@ -285,11 +308,25 @@ pub fn extract_uinode_images(
Option<&TextureAtlas>,
Option<&ComputedTextureSlices>,
Option<&BorderRadius>,
Option<&Parent>,
&Style,
)>,
>,
node_query: Extract<Query<&Node>>,
) {
for (uinode, transform, view_visibility, clip, camera, image, atlas, slices, border_radius) in
&uinode_query
for (
uinode,
transform,
view_visibility,
clip,
camera,
image,
atlas,
slices,
border_radius,
parent,
style,
) in &uinode_query
{
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
Expand Down Expand Up @@ -342,6 +379,23 @@ pub fn extract_uinode_images(
// so we have to divide by `UiScale` to get the size of the UI viewport.
/ ui_scale.0;

// 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
.and_then(|parent| node_query.get(parent.get()).ok())
.map(|parent_node| parent_node.size().x)
.unwrap_or(ui_logical_viewport_size.x);
let left =
resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size);
let right =
resolve_border_thickness(style.border.right, parent_width, ui_logical_viewport_size);
let top =
resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size);
let bottom =
resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size);

let border = [left, top, right, bottom];

let border_radius = if let Some(border_radius) = border_radius {
resolve_border_radius(
border_radius,
Expand All @@ -366,7 +420,7 @@ pub fn extract_uinode_images(
flip_x: image.flip_x,
flip_y: image.flip_y,
camera_entity,
border: [0.; 4],
border,
border_radius,
node_type: NodeType::Rect,
},
Expand Down Expand Up @@ -513,6 +567,11 @@ 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 = resolve_border_radius(
border_radius,
node.size(),
Expand Down
47 changes: 26 additions & 21 deletions crates/bevy_ui/src/render/ui.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ fn sd_rounded_box(point: vec2<f32>, size: vec2<f32>, corner_radii: vec4<f32>) ->
// If 0.0 < y then select bottom left (w) and bottom right corner radius (z).
// Else select top left (x) and top right corner radius (y).
let rs = select(corner_radii.xy, corner_radii.wz, 0.0 < point.y);
// w and z are swapped so that both pairs are in left to right order, otherwise this second
// w and z are swapped above so that both pairs are in left to right order, otherwise this second
// select statement would return the incorrect value for the bottom pair.
let radius = select(rs.x, rs.y, 0.0 < point.x);
// Vector from the corner closest to the point, to the point.
Expand Down Expand Up @@ -120,6 +120,12 @@ fn sd_inset_rounded_box(point: vec2<f32>, size: vec2<f32>, radius: vec4<f32>, in
return sd_rounded_box(inner_point, inner_size, r);
}

// get alpha for antialiasing for sdf
fn antialias(distance: f32) -> f32 {
// Using the fwidth(distance) was causing artifacts, so just use the distance.
return clamp(0.0, 1.0, 0.5 - distance);
}

fn draw(in: VertexOutput) -> vec4<f32> {
let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv);

Expand All @@ -145,32 +151,31 @@ fn draw(in: VertexOutput) -> vec4<f32> {
// outside the outside edge, or inside the inner edge have positive signed distance.
let border_distance = max(external_distance, -internal_distance);

// The `fwidth` function returns an approximation of the rate of change of the signed distance
// value that is used to ensure that the smooth alpha transition created by smoothstep occurs
// over a range of distance values that is proportional to how quickly the distance is changing.
let fborder = fwidth(border_distance);
let fexternal = fwidth(external_distance);

if enabled(in.flags, BORDER) {
// The item is a border
// At external edges with no border, `border_distance` is equal to zero.
// This select statement ensures we only perform anti-aliasing where a non-zero width border
// is present, otherwise an outline about the external boundary would be drawn even without
// a border.
let t = select(1.0 - step(0.0, border_distance), antialias(border_distance), external_distance < internal_distance);

// At external edges with no border, `border_distance` is equal to zero.
// This select statement ensures we only perform anti-aliasing where a non-zero width border
// is present, otherwise an outline about the external boundary would be drawn even without
// a border.
let t = 1. - select(step(0.0, border_distance), smoothstep(0.0, fborder, border_distance), external_distance < internal_distance);

// Blend mode ALPHA_BLENDING is used for UI elements, so we don't premultiply alpha here.
return vec4(color.rgb, color.a * t);
}
// Blend mode ALPHA_BLENDING is used for UI elements, so we don't premultiply alpha here.
return vec4(color.rgb, color.a * t);
}

// The item is a rectangle, draw normally with anti-aliasing at the edges.
let t = 1. - smoothstep(0.0, fexternal, external_distance);
fn draw_background(in: VertexOutput) -> vec4<f32> {
let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv);
let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED));

// When drawing the background only draw the internal area and not the border.
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
let t = antialias(internal_distance);
return vec4(color.rgb, color.a * t);
}

@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
return draw(in);
if enabled(in.flags, BORDER) {
return draw(in);
} else {
return draw_background(in);
}
}