mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-22 10:19:20 +00:00
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:
parent
9330bf4b72
commit
c9fbc2db0b
Notes:
github-actions[bot]
2025-08-21 09:53:43 +00:00
Author: https://github.com/dan-v 🔰
Commit: c9fbc2db0b
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/5850
Reviewed-by: https://github.com/gmta ✅
Reviewed-by: https://github.com/konradekk
3 changed files with 149 additions and 38 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
55
Tests/LibWeb/Ref/input/border-radius-normalization.html
Normal file
55
Tests/LibWeb/Ref/input/border-radius-normalization.html
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue