LibWeb: Implement CSS border radius overlapping algorithm

Fixes rendering of elements with large border-radius values by scaling
radii proportionally when they exceed element dimensions per CSS spec.

Co-authored-by: Samyat Gautam <thesamyatgautam@gmail.com>
This commit is contained in:
Dan Vittegleo 2025-08-13 21:09:57 -07:00 committed by Jelle Raaijmakers
commit c9fbc2db0b
Notes: github-actions[bot] 2025-08-21 09:53:43 +00:00
3 changed files with 149 additions and 38 deletions

View file

@ -217,52 +217,54 @@ Gfx::AffineTransform Paintable::compute_combined_css_transform() const
Painting::BorderRadiiData normalize_border_radii_data(Layout::Node const& node, CSSPixelRect const& rect, CSS::BorderRadiusData const& top_left_radius, CSS::BorderRadiusData const& top_right_radius, CSS::BorderRadiusData const& bottom_right_radius, CSS::BorderRadiusData const& bottom_left_radius)
{
Painting::BorderRadiusData bottom_left_radius_px {};
Painting::BorderRadiusData bottom_right_radius_px {};
Painting::BorderRadiusData top_left_radius_px {};
Painting::BorderRadiusData top_right_radius_px {};
bottom_left_radius_px.horizontal_radius = bottom_left_radius.horizontal_radius.to_px(node, rect.width());
bottom_right_radius_px.horizontal_radius = bottom_right_radius.horizontal_radius.to_px(node, rect.width());
top_left_radius_px.horizontal_radius = top_left_radius.horizontal_radius.to_px(node, rect.width());
top_right_radius_px.horizontal_radius = top_right_radius.horizontal_radius.to_px(node, rect.width());
bottom_left_radius_px.vertical_radius = bottom_left_radius.vertical_radius.to_px(node, rect.height());
bottom_right_radius_px.vertical_radius = bottom_right_radius.vertical_radius.to_px(node, rect.height());
top_left_radius_px.vertical_radius = top_left_radius.vertical_radius.to_px(node, rect.height());
top_right_radius_px.vertical_radius = top_right_radius.vertical_radius.to_px(node, rect.height());
Painting::BorderRadiiData radii_px {
.top_left = {
top_left_radius.horizontal_radius.to_px(node, rect.width()),
top_left_radius.vertical_radius.to_px(node, rect.height()) },
.top_right = { top_right_radius.horizontal_radius.to_px(node, rect.width()), top_right_radius.vertical_radius.to_px(node, rect.height()) },
.bottom_right = { bottom_right_radius.horizontal_radius.to_px(node, rect.width()), bottom_right_radius.vertical_radius.to_px(node, rect.height()) },
.bottom_left = { bottom_left_radius.horizontal_radius.to_px(node, rect.width()), bottom_left_radius.vertical_radius.to_px(node, rect.height()) }
};
// Scale overlapping curves according to https://www.w3.org/TR/css-backgrounds-3/#corner-overlap
// Let f = min(Li/Si), where i ∈ {top, right, bottom, left},
// Si is the sum of the two corresponding radii of the corners on side i,
// and Ltop = Lbottom = the width of the box, and Lleft = Lright = the height of the box.
auto l_top = rect.width();
auto l_bottom = l_top;
auto l_left = rect.height();
auto l_right = l_left;
auto s_top = (top_left_radius_px.horizontal_radius + top_right_radius_px.horizontal_radius);
auto s_right = (top_right_radius_px.vertical_radius + bottom_right_radius_px.vertical_radius);
auto s_bottom = (bottom_left_radius_px.horizontal_radius + bottom_right_radius_px.horizontal_radius);
auto s_left = (top_left_radius_px.vertical_radius + bottom_left_radius_px.vertical_radius);
CSSPixelFraction f = 1;
f = (s_top != 0) ? min(f, l_top / s_top) : f;
f = (s_right != 0) ? min(f, l_right / s_right) : f;
f = (s_bottom != 0) ? min(f, l_bottom / s_bottom) : f;
f = (s_left != 0) ? min(f, l_left / s_left) : f;
//
// NOTE: We iterate twice as a form of iterative refinement. A single scaling pass using
// fixed-point arithmetic can result in small rounding errors, causing the scaled radii to
// still slightly overflow the box dimensions. A second pass corrects this remaining error.
for (int iteration = 0; iteration < 2; ++iteration) {
auto s_top = radii_px.top_left.horizontal_radius + radii_px.top_right.horizontal_radius;
auto s_right = radii_px.top_right.vertical_radius + radii_px.bottom_right.vertical_radius;
auto s_bottom = radii_px.bottom_right.horizontal_radius + radii_px.bottom_left.horizontal_radius;
auto s_left = radii_px.bottom_left.vertical_radius + radii_px.top_left.vertical_radius;
// If f < 1, then all corner radii are reduced by multiplying them by f.
if (f < 1) {
top_left_radius_px.horizontal_radius *= f;
top_left_radius_px.vertical_radius *= f;
top_right_radius_px.horizontal_radius *= f;
top_right_radius_px.vertical_radius *= f;
bottom_right_radius_px.horizontal_radius *= f;
bottom_right_radius_px.vertical_radius *= f;
bottom_left_radius_px.horizontal_radius *= f;
bottom_left_radius_px.vertical_radius *= f;
CSSPixelFraction f = 1;
if (s_top > rect.width())
f = min(f, rect.width() / s_top);
if (s_right > rect.height())
f = min(f, rect.height() / s_right);
if (s_bottom > rect.width())
f = min(f, rect.width() / s_bottom);
if (s_left > rect.height())
f = min(f, rect.height() / s_left);
// If f is 1 or more, the radii fit perfectly and no more scaling is needed
if (f >= 1)
break;
Painting::BorderRadiusData* corners[] = {
&radii_px.top_left, &radii_px.top_right, &radii_px.bottom_right, &radii_px.bottom_left
};
for (auto* corner : corners) {
corner->horizontal_radius *= f;
corner->vertical_radius *= f;
}
}
return Painting::BorderRadiiData { top_left_radius_px, top_right_radius_px, bottom_right_radius_px, bottom_left_radius_px };
return radii_px;
}
}

View file

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
padding: 20px;
background-color: #f5f5f5;
font-family: sans-serif;
}
.test-case {
margin: 15px 0;
display: flex;
align-items: center;
gap: 15px;
}
.label {
width: 200px;
font-size: 14px;
color: #333;
}
.box {
background-color: #4CAF50;
border: 2px solid #333;
}
/* Float max values properly normalized to perfect circle */
.float-max-precision {
width: 300px;
height: 40px;
border-radius: 21px; /* Float max values normalized to (content height + borders)/2 = (40+2)/2 = 21px */
background-color: #4CAF50;
border: 1px solid #333;
}
h2 {
font-size: 18px;
margin-bottom: 20px;
color: #333;
}
</style>
</head>
<body>
<h2>Border Radius Normalization Test</h2>
<div class="test-case">
<div class="label">Float max precision test:</div>
<div class="float-max-precision"></div>
</div>
</body>
</html>

View file

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<link rel="match" href="../expected/border-radius-normalization-ref.html" />
<head>
<style>
body {
margin: 0;
padding: 20px;
background-color: #f5f5f5;
font-family: sans-serif;
}
.test-case {
margin: 15px 0;
display: flex;
align-items: center;
gap: 15px;
}
.label {
width: 200px;
font-size: 14px;
color: #333;
}
.box {
background-color: #4CAF50;
border: 2px solid #333;
}
/* Float max precision test - tests extreme border radius values */
.float-max-precision {
width: 300px;
height: 40px;
border-radius: 3.40282e+38px; /* Float max values trigger precision issues */
background-color: #4CAF50;
border: 1px solid #333;
}
h2 {
font-size: 18px;
margin-bottom: 20px;
color: #333;
}
</style>
</head>
<body>
<h2>Border Radius Normalization Test</h2>
<div class="test-case">
<div class="label">Float max precision test:</div>
<div class="float-max-precision"></div>
</div>
</body>
</html>