Skip to content

Commit 4ab3c7c

Browse files
dkwingsmtmboetger
authored andcommitted
Add radius clamping to web RSuperellipse (flutter#172254)
This PR fixes rendering errors on Web when the provided corner radii sum up larger than the size. It implements radius scaling using the same algorithm as in [the C++ implementation](https://github.com/flutter/flutter/blob/b2d4210b3795413c2360968b685743a6df60ff50/engine/src/flutter/impeller/geometry/rounding_radii.cc). Before: (error emerges for r>100, since the height is 200) <img width="664" height="509" alt="image" src="https://github.com/user-attachments/assets/eb526338-84d9-4eca-975b-d44bee0c11ac" /> After: (it stays this way for r>100) <img width="611" height="471" alt="image" src="https://github.com/user-attachments/assets/08ca2499-d5f7-47e1-9ecf-29f60c968016" /> It also fixes a bug that uses an incorrect starting point. Both changes are backed by the new test cases in `rounded_superellipse_border_test.dart`. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 8a548f8 commit 4ab3c7c

File tree

3 files changed

+119
-5
lines changed

3 files changed

+119
-5
lines changed

engine/src/flutter/lib/web_ui/lib/geometry.dart

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,18 @@ class RSuperellipse extends _RRectLike<RSuperellipse> {
11641164
@override
11651165
final bool _uniformRadii;
11661166

1167+
static (double, double) _normalizeEmptyToZero(double inputX, double inputY) {
1168+
return (inputX > 0 && inputY > 0) ? (inputX, inputY) : (0, 0);
1169+
}
1170+
1171+
static double _adjustScale(double radius1, double radius2, double dimension, double scale) {
1172+
assert(radius1 >= 0.0 && radius2 >= 0.0 && dimension > 0.0);
1173+
if (radius1 + radius2 > dimension) {
1174+
return math.min(scale, dimension / (radius1 + radius2));
1175+
}
1176+
return scale;
1177+
}
1178+
11671179
/// (Web only) Returns a [Path] for this shape and an [Offset] for its
11681180
/// placement.
11691181
///
@@ -1180,9 +1192,9 @@ class RSuperellipse extends _RRectLike<RSuperellipse> {
11801192
/// `path` and the `offset` to the `addPath` method.
11811193
(Path, Offset) toPathOffset() {
11821194
if (_uniformRadii) {
1183-
return (_RSuperellipseCache.instance.get(width, height, tlRadius), center);
1195+
return (_RSuperellipseCache.instance.get(width, height, _scaledUniformRadii()), center);
11841196
} else {
1185-
return (_RSuperellipsePathBuilder.exact(this).path, Offset.zero);
1197+
return (_RSuperellipsePathBuilder.exact(_toScaledRadii()).path, Offset.zero);
11861198
}
11871199
}
11881200

@@ -1211,6 +1223,81 @@ class RSuperellipse extends _RRectLike<RSuperellipse> {
12111223
);
12121224
}
12131225

1226+
/// Returns a [RSuperellipse] whose corner radii are scaled based on this one,
1227+
/// ensuring that the sum of the corner radii on each side does not exceed the
1228+
/// width or height of the given bounds.
1229+
///
1230+
/// See the [Skia scaling
1231+
/// implementation](https://github.com/google/skia/blob/main/src/core/SkRRect.cpp)
1232+
/// for more details.
1233+
RSuperellipse _toScaledRadii() {
1234+
if (!(width > 0 && height > 0)) {
1235+
return RSuperellipse.fromLTRBXY(left, top, right, bottom, 0.0, 0.0);
1236+
}
1237+
1238+
// If any corner is flat or has a negative value, normalize it to zeros
1239+
// We do this first so that the unnecessary non-flat part of that radius
1240+
// does not contribute to the global scaling below.
1241+
final (double tlRadiusX, double tlRadiusY) = _normalizeEmptyToZero(
1242+
this.tlRadiusX,
1243+
this.tlRadiusY,
1244+
);
1245+
final (double trRadiusX, double trRadiusY) = _normalizeEmptyToZero(
1246+
this.trRadiusX,
1247+
this.trRadiusY,
1248+
);
1249+
final (double blRadiusX, double blRadiusY) = _normalizeEmptyToZero(
1250+
this.blRadiusX,
1251+
this.blRadiusY,
1252+
);
1253+
final (double brRadiusX, double brRadiusY) = _normalizeEmptyToZero(
1254+
this.brRadiusX,
1255+
this.brRadiusY,
1256+
);
1257+
1258+
// Now determine a global scale to apply to all of the radii to ensure
1259+
// that none of the adjacent pairs of radius values sum to larger than
1260+
// the corresponding dimension of the rectangle.
1261+
double scale = 1.0;
1262+
scale = _adjustScale(tlRadiusX, trRadiusX, width, scale);
1263+
scale = _adjustScale(blRadiusX, brRadiusX, width, scale);
1264+
scale = _adjustScale(tlRadiusY, blRadiusY, height, scale);
1265+
scale = _adjustScale(trRadiusY, brRadiusY, height, scale);
1266+
if (scale < 1.0) {
1267+
return _create(
1268+
left: left,
1269+
top: top,
1270+
right: right,
1271+
bottom: bottom,
1272+
tlRadiusX: tlRadiusX * scale,
1273+
tlRadiusY: tlRadiusY * scale,
1274+
trRadiusX: trRadiusX * scale,
1275+
trRadiusY: trRadiusY * scale,
1276+
brRadiusX: brRadiusX * scale,
1277+
brRadiusY: brRadiusY * scale,
1278+
blRadiusX: blRadiusX * scale,
1279+
blRadiusY: blRadiusY * scale,
1280+
uniformRadii: _uniformRadii,
1281+
);
1282+
} else {
1283+
return this;
1284+
}
1285+
}
1286+
1287+
// A variation of `_toScaledRadii` that deals with uniform radii and returns a
1288+
// `Radius`.
1289+
Radius _scaledUniformRadii() {
1290+
assert(_uniformRadii);
1291+
if (!(width > 0 && height > 0)) {
1292+
return Radius.zero;
1293+
}
1294+
final (double radiusX, double radiusY) = _normalizeEmptyToZero(tlRadiusX, tlRadiusY);
1295+
double scale = 1.0;
1296+
scale = _adjustScale(radiusX, radiusX, width, scale);
1297+
scale = _adjustScale(radiusY, radiusY, height, scale);
1298+
return Radius.elliptical(radiusX * scale, radiusY * scale);
1299+
}
1300+
12141301
static const RSuperellipse zero = RSuperellipse._raw();
12151302

12161303
bool contains(Offset point) {

engine/src/flutter/lib/web_ui/lib/rsuperellipse_param.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ class _RSuperellipseQuadrant {
336336
final _RSuperellipseOctant top;
337337
final _RSuperellipseOctant right;
338338

339+
bool get isSharpCorner => top.seN < 2 || right.seN < 2;
340+
339341
void addToPath(
340342
_RSuperellipsePath path, {
341343
required bool reverse,
@@ -345,7 +347,7 @@ class _RSuperellipseQuadrant {
345347
_Transform.makeTranslate(offset),
346348
_Transform.makeScale(signedScale.scale(extraScale.width, extraScale.height)),
347349
);
348-
if (top.seN < 2 || right.seN < 2) {
350+
if (isSharpCorner) {
349351
if (!reverse) {
350352
final _Transform transformOctant = _Transform.makeComposite(
351353
transform,
@@ -406,13 +408,14 @@ class _RSuperellipsePathBuilder {
406408
// Build a path for an RSuperellipse with arbitrary position and radii.
407409
_RSuperellipsePathBuilder.exact(RSuperellipse r) : path = Path() {
408410
final _RSuperellipsePath p = _RSuperellipsePath(path);
409-
final Offset start = Offset((r.left + r.right) / 2, r.top);
410-
path.moveTo(start.dx, start.dy);
411411

412412
final double topSplit = _split(r.left, r.right, r.tlRadiusX, r.trRadiusX);
413413
final double rightSplit = _split(r.top, r.bottom, r.trRadiusY, r.brRadiusY);
414414
final double bottomSplit = _split(r.left, r.right, r.blRadiusX, r.brRadiusX);
415415
final double leftSplit = _split(r.top, r.bottom, r.tlRadiusY, r.blRadiusY);
416+
417+
final Offset start = Offset(topSplit, r.top);
418+
path.moveTo(start.dx, start.dy);
416419
_RSuperellipseQuadrant(
417420
Offset(topSplit, rightSplit),
418421
Offset(r.right, r.top),

packages/flutter/test/painting/rounded_superellipse_border_test.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,30 @@ void main() {
253253
matchesGoldenFile('painting.rounded_superellipse_border.all_elliptical.png'),
254254
);
255255

256+
await tester.pumpWidget(
257+
containerWithBorder(const Size(120, 300), const BorderRadius.all(Radius.circular(600))),
258+
);
259+
await expectLater(
260+
find.byType(Container),
261+
matchesGoldenFile('painting.rounded_superellipse_border.clamping_uniform.png'),
262+
);
263+
264+
await tester.pumpWidget(
265+
containerWithBorder(
266+
const Size(120, 300),
267+
const BorderRadius.only(
268+
topLeft: Radius.elliptical(1000, 1000),
269+
topRight: Radius.elliptical(0, 1000),
270+
bottomRight: Radius.elliptical(800, 1000),
271+
bottomLeft: Radius.elliptical(100, 500),
272+
),
273+
),
274+
);
275+
await expectLater(
276+
find.byType(Container),
277+
matchesGoldenFile('painting.rounded_superellipse_border.clamping_non_uniform.png'),
278+
);
279+
256280
// Regression test for https://github.com/flutter/flutter/issues/170593
257281
await tester.pumpWidget(
258282
containerWithBorder(

0 commit comments

Comments
 (0)