mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-25 19:56:30 +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::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::BorderRadiiData radii_px {
|
||||||
Painting::BorderRadiusData bottom_right_radius_px {};
|
.top_left = {
|
||||||
Painting::BorderRadiusData top_left_radius_px {};
|
top_left_radius.horizontal_radius.to_px(node, rect.width()),
|
||||||
Painting::BorderRadiusData top_right_radius_px {};
|
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_left_radius_px.horizontal_radius = bottom_left_radius.horizontal_radius.to_px(node, rect.width());
|
.bottom_right = { bottom_right_radius.horizontal_radius.to_px(node, rect.width()), bottom_right_radius.vertical_radius.to_px(node, rect.height()) },
|
||||||
bottom_right_radius_px.horizontal_radius = bottom_right_radius.horizontal_radius.to_px(node, rect.width());
|
.bottom_left = { bottom_left_radius.horizontal_radius.to_px(node, rect.width()), bottom_left_radius.vertical_radius.to_px(node, rect.height()) }
|
||||||
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());
|
|
||||||
|
|
||||||
// Scale overlapping curves according to https://www.w3.org/TR/css-backgrounds-3/#corner-overlap
|
// 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},
|
// 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,
|
// 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.
|
// 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;
|
// NOTE: We iterate twice as a form of iterative refinement. A single scaling pass using
|
||||||
auto l_left = rect.height();
|
// fixed-point arithmetic can result in small rounding errors, causing the scaled radii to
|
||||||
auto l_right = l_left;
|
// still slightly overflow the box dimensions. A second pass corrects this remaining error.
|
||||||
auto s_top = (top_left_radius_px.horizontal_radius + top_right_radius_px.horizontal_radius);
|
for (int iteration = 0; iteration < 2; ++iteration) {
|
||||||
auto s_right = (top_right_radius_px.vertical_radius + bottom_right_radius_px.vertical_radius);
|
auto s_top = radii_px.top_left.horizontal_radius + radii_px.top_right.horizontal_radius;
|
||||||
auto s_bottom = (bottom_left_radius_px.horizontal_radius + bottom_right_radius_px.horizontal_radius);
|
auto s_right = radii_px.top_right.vertical_radius + radii_px.bottom_right.vertical_radius;
|
||||||
auto s_left = (top_left_radius_px.vertical_radius + bottom_left_radius_px.vertical_radius);
|
auto s_bottom = radii_px.bottom_right.horizontal_radius + radii_px.bottom_left.horizontal_radius;
|
||||||
CSSPixelFraction f = 1;
|
auto s_left = radii_px.bottom_left.vertical_radius + radii_px.top_left.vertical_radius;
|
||||||
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;
|
|
||||||
|
|
||||||
// If f < 1, then all corner radii are reduced by multiplying them by f.
|
CSSPixelFraction f = 1;
|
||||||
if (f < 1) {
|
if (s_top > rect.width())
|
||||||
top_left_radius_px.horizontal_radius *= f;
|
f = min(f, rect.width() / s_top);
|
||||||
top_left_radius_px.vertical_radius *= f;
|
if (s_right > rect.height())
|
||||||
top_right_radius_px.horizontal_radius *= f;
|
f = min(f, rect.height() / s_right);
|
||||||
top_right_radius_px.vertical_radius *= f;
|
if (s_bottom > rect.width())
|
||||||
bottom_right_radius_px.horizontal_radius *= f;
|
f = min(f, rect.width() / s_bottom);
|
||||||
bottom_right_radius_px.vertical_radius *= f;
|
if (s_left > rect.height())
|
||||||
bottom_left_radius_px.horizontal_radius *= f;
|
f = min(f, rect.height() / s_left);
|
||||||
bottom_left_radius_px.vertical_radius *= f;
|
|
||||||
|
// 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