Add clipRSuperellipse, and use them for dialogs (#161111)

This PR makes Flutter support `clipRSuperellipse`, and apply it to
`CupertinoAlertDialog`.

Hit tests related to RSuperellipse are performed based on its bounding
box, since the computation is too complicated and pixel perfect hit test
is not needed practically.

Native:
<img
src="https://github.com/user-attachments/assets/8f9b472a-e624-4eef-9cea-e81b80f32b86"
width="400"/>

Native vs before: (The two screenshots are stacked and offset by (1, 1)
pixels. See the bottom right corner for comparison.)
<img
src="https://github.com/user-attachments/assets/ffaf62fc-a82f-4c7a-9ff1-52374f4f2a67"
width="400"/>

Native vs after: 
<img
src="https://github.com/user-attachments/assets/3dfde2b0-bcc6-492a-8d97-ecabdf97f6f0"
width="400"/>

After only:
<img
src="https://github.com/user-attachments/assets/32b2a665-a0da-498f-acdb-598553940964"
width="400"/>


## 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
This commit is contained in:
Tong Mu 2025-03-04 18:06:12 -08:00 committed by GitHub
parent c35fd09792
commit 0357b45337
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 477 additions and 12 deletions

View File

@ -570,7 +570,7 @@ class CupertinoPopupSurface extends StatelessWidget {
static const double defaultBlurSigma = 30.0;
/// The default corner radius of a [CupertinoPopupSurface].
static const BorderRadius _clipper = BorderRadius.all(Radius.circular(14));
static const BorderRadius _clipper = BorderRadius.all(Radius.circular(12));
// The [ColorFilter] matrix used to saturate widgets underlying a
// [CupertinoPopupSurface] when the ambient [CupertinoThemeData.brightness] is
@ -719,13 +719,13 @@ class CupertinoPopupSurface extends StatelessWidget {
}
if (filter != null) {
return ClipRRect(
return ClipRSuperellipse(
borderRadius: _clipper,
child: BackdropFilter(filter: filter, child: contents),
);
}
return ClipRRect(borderRadius: _clipper, child: contents);
return ClipRSuperellipse(borderRadius: _clipper, child: contents);
}
}

View File

@ -36,6 +36,7 @@ export 'dart:ui'
PathOperation,
RRect,
RSTransform,
RSuperellipse,
Radius,
Rect,
Shader,

View File

@ -383,6 +383,23 @@ class BorderRadius extends BorderRadiusGeometry {
);
}
/// Creates an [RSuperellipse] from the current border radius and a [Rect].
///
/// If any of the radii have negative values in x or y, those values will be
/// clamped to zero in order to produce a valid [RRect].
RSuperellipse toRSuperellipse(Rect rect) {
// Because the current radii could be negative, we must clamp them before
// converting them to an RRect to be rendered, since negative radii on
// RRects don't make sense.
return RSuperellipse.fromRectAndCorners(
rect,
topLeft: topLeft.clamp(minimum: Radius.zero),
topRight: topRight.clamp(minimum: Radius.zero),
bottomLeft: bottomLeft.clamp(minimum: Radius.zero),
bottomRight: bottomRight.clamp(minimum: Radius.zero),
);
}
@override
BorderRadiusGeometry subtract(BorderRadiusGeometry other) {
if (other is BorderRadius) {

View File

@ -5,7 +5,7 @@
/// @docImport 'package:flutter/rendering.dart';
library;
import 'dart:ui' show Canvas, Clip, Paint, Path, RRect, Rect, VoidCallback;
import 'dart:ui' show Canvas, Clip, Paint, Path, RRect, RSuperellipse, Rect, VoidCallback;
/// Clip utilities used by [PaintingContext].
abstract class ClipContext {
@ -63,6 +63,25 @@ abstract class ClipContext {
);
}
/// Clip [canvas] with [Path] according to the given rounded superellipse and
/// then paint. [canvas] is restored to the pre-clip status afterwards.
///
/// The `bounds` is the saveLayer bounds used for
/// [Clip.antiAliasWithSaveLayer].
void clipRSuperellipseAndPaint(
RSuperellipse rse,
Clip clipBehavior,
Rect bounds,
VoidCallback painter,
) {
_clipAndPaint(
(bool doAntiAlias) => canvas.clipRSuperellipse(rse, doAntiAlias: doAntiAlias),
clipBehavior,
bounds,
painter,
);
}
/// Clip [canvas] with [Path] according to `rect` and then paint. [canvas] is
/// restored to the pre-clip status afterwards.
///

View File

@ -1773,6 +1773,96 @@ class ClipRRectLayer extends ContainerLayer {
}
}
/// A composite layer that clips its children using a rounded superellipse.
///
/// When debugging, setting [debugDisableClipLayers] to true will cause this
/// layer to be skipped (directly replaced by its children). This can be helpful
/// to track down the cause of performance problems.
///
/// Hit tests are performed based on the bounding box of the rounded
/// superellipse.
class ClipRSuperellipseLayer extends ContainerLayer {
/// Creates a layer with a rounded-rectangular clip.
///
/// The [clipRSuperellipse] and [clipBehavior] properties must be non-null before the
/// compositing phase of the pipeline.
ClipRSuperellipseLayer({RSuperellipse? clipRSuperellipse, Clip clipBehavior = Clip.antiAlias})
: _clipRSuperellipse = clipRSuperellipse,
_clipBehavior = clipBehavior,
assert(clipBehavior != Clip.none);
/// The rounded-rect to clip in the parent's coordinate system.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
RSuperellipse? get clipRSuperellipse => _clipRSuperellipse;
RSuperellipse? _clipRSuperellipse;
set clipRSuperellipse(RSuperellipse? value) {
if (value != _clipRSuperellipse) {
_clipRSuperellipse = value;
markNeedsAddToScene();
}
}
@override
Rect? describeClipBounds() => clipRSuperellipse?.outerRect;
/// {@macro flutter.rendering.ClipRectLayer.clipBehavior}
///
/// Defaults to [Clip.antiAlias].
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior;
set clipBehavior(Clip value) {
assert(value != Clip.none);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsAddToScene();
}
}
@override
bool findAnnotations<S extends Object>(
AnnotationResult<S> result,
Offset localPosition, {
required bool onlyFirst,
}) {
if (!clipRSuperellipse!.outerRect.contains(localPosition)) {
return false;
}
return super.findAnnotations<S>(result, localPosition, onlyFirst: onlyFirst);
}
@override
void addToScene(ui.SceneBuilder builder) {
assert(clipRSuperellipse != null);
bool enabled = true;
assert(() {
enabled = !debugDisableClipLayers;
return true;
}());
if (enabled) {
engineLayer = builder.pushClipRSuperellipse(
clipRSuperellipse!,
clipBehavior: clipBehavior,
oldLayer: _engineLayer as ui.ClipRSuperellipseEngineLayer?,
);
} else {
engineLayer = null;
}
addChildrenToScene(builder);
if (enabled) {
builder.pop();
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<RSuperellipse>('clipRSuperellipse', clipRSuperellipse));
properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior));
}
}
/// A composite layer that clips its children using a path.
///
/// When debugging, setting [debugDisableClipLayers] to true will cause this

View File

@ -626,6 +626,60 @@ class PaintingContext extends ClipContext {
}
}
/// Clip further painting using a rounded superellipse.
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.needsCompositing}
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.offset}
///
/// The `bounds` argument is used to specify the region of the canvas (in the
/// caller's coordinate system) into which `painter` will paint.
///
/// The `clipRSuperellipse` argument specifies the rounded-superellipse (in the caller's
/// coordinate system) to use to clip the painting done by `painter`. It
/// should not include the `offset`.
///
/// The `painter` callback will be called while the `clipRSuperellipse` is applied. It
/// is called synchronously during the call to [pushClipRSuperellipse].
///
/// The `clipBehavior` argument controls how the rounded rectangle is clipped.
///
/// Hit tests are performed based on the bounding box of the [RSuperellipse].
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.oldLayer}
ClipRSuperellipseLayer? pushClipRSuperellipse(
bool needsCompositing,
Offset offset,
Rect bounds,
RSuperellipse clipRSuperellipse,
PaintingContextCallback painter, {
Clip clipBehavior = Clip.antiAlias,
ClipRSuperellipseLayer? oldLayer,
}) {
if (clipBehavior == Clip.none) {
painter(this, offset);
return null;
}
final Rect offsetBounds = bounds.shift(offset);
final RSuperellipse offsetShape = clipRSuperellipse.shift(offset);
if (needsCompositing) {
final ClipRSuperellipseLayer layer = oldLayer ?? ClipRSuperellipseLayer();
layer
..clipRSuperellipse = offsetShape
..clipBehavior = clipBehavior;
pushLayer(layer, painter, offset, childPaintBounds: offsetBounds);
return layer;
} else {
clipRSuperellipseAndPaint(
offsetShape,
clipBehavior,
offsetBounds,
() => painter(this, offset),
);
return null;
}
}
/// Clip further painting using a path.
///
/// {@macro flutter.rendering.PaintingContext.pushClipRect.needsCompositing}

View File

@ -1714,6 +1714,113 @@ class RenderClipRRect extends _RenderCustomClip<RRect> {
}
}
/// Clips its child using a rounded superellipse.
///
/// By default, [RenderClipRSuperellipse] uses its own bounds as the base
/// rectangle for the clip, but the size and location of the clip can be
/// customized using a custom [clipper].
///
/// Hit tests are performed based on the bounding box of the RSuperellipse.
class RenderClipRSuperellipse extends _RenderCustomClip<RSuperellipse> {
/// Creates a rounded-superellipse clip.
///
/// The [borderRadius] defaults to [BorderRadius.zero], i.e. a rectangle with
/// right-angled corners.
///
/// If [clipBehavior] is [Clip.none], no clipping will be applied.
RenderClipRSuperellipse({
super.child,
BorderRadiusGeometry borderRadius = BorderRadius.zero,
super.clipper,
super.clipBehavior,
TextDirection? textDirection,
}) : _borderRadius = borderRadius,
_textDirection = textDirection;
/// The border radius of the rounded corners.
///
/// Values are clamped so that horizontal and vertical radii sums do not
/// exceed width/height.
///
/// This value is ignored if [clipper] is non-null.
BorderRadiusGeometry get borderRadius => _borderRadius;
BorderRadiusGeometry _borderRadius;
set borderRadius(BorderRadiusGeometry value) {
if (_borderRadius == value) {
return;
}
_borderRadius = value;
_markNeedsClip();
}
/// The text direction with which to resolve [borderRadius].
TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection;
set textDirection(TextDirection? value) {
if (_textDirection == value) {
return;
}
_textDirection = value;
_markNeedsClip();
}
@override
RSuperellipse get _defaultClip =>
_borderRadius.resolve(textDirection).toRSuperellipse(Offset.zero & size);
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (_clipper != null) {
_updateClip();
assert(_clip != null);
if (!_clip!.outerRect.contains(position)) {
return false;
}
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (clipBehavior != Clip.none) {
_updateClip();
layer = context.pushClipRSuperellipse(
needsCompositing,
offset,
_clip!.outerRect,
_clip!,
super.paint,
clipBehavior: clipBehavior,
oldLayer: layer as ClipRSuperellipseLayer?,
);
} else {
context.paintChild(child!, offset);
layer = null;
}
} else {
layer = null;
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
if (child != null) {
super.debugPaintSize(context, offset);
if (clipBehavior != Clip.none) {
context.canvas.drawRSuperellipse(_clip!.shift(offset), _debugPaint!);
_debugText!.paint(
context.canvas,
offset + Offset(_clip!.tlRadiusX, -_debugText!.text!.style!.fontSize! * 1.1),
);
}
}
return true;
}());
}
}
/// Clips its child using an oval.
///
/// By default, inscribes an axis-aligned oval into its layout dimensions and

View File

@ -965,6 +965,11 @@ class ClipRect extends SingleChildRenderObjectWidget {
///
/// * [CustomClipper], for information about creating custom clips.
/// * [ClipRect], for more efficient clips without rounded corners.
/// * [ClipRSuperellipse], for a similar clipping shape with smoother
/// transitions between the straight sides and the rounded corners. This
/// shape closely matches the rounded rectangles commonly used in Apples
/// design language, resembling the `RoundedRectangle` shape in SwiftUI with
/// the `.continuous` corner style.
/// * [ClipOval], for an elliptical clip.
/// * [ClipPath], for an arbitrarily shaped clip.
class ClipRRect extends SingleChildRenderObjectWidget {
@ -1036,6 +1041,94 @@ class ClipRRect extends SingleChildRenderObjectWidget {
}
}
/// A widget that clips its child using a rounded superellipse.
///
/// A rounded superellipse is a shape similar to a typical rounded rectangle
/// ([ClipRRect]), but with smoother transitions between the straight sides and
/// the rounded corners. It resembles the `RoundedRectangle` shape in SwiftUI
/// with the `.continuous` corner style. Technically, it is created by replacing
/// the four corners of a superellipse (also known as a Lamé curve) with
/// circular arcs.
///
/// By default, [ClipRSuperellipse] uses its own bounds as the base rectangle
/// for the clip, but the size and location of the clip can be customized using
/// a custom [clipper].
///
/// See also:
///
/// * [CustomClipper], for information about creating custom clips.
/// * [ClipRect], for more efficient clips without rounded corners.
/// * [ClipRRect], for a typical rounded rectangle, which is created by
/// replacing the four corners of a rectangle with circular arcs.
/// * [ClipOval], for an elliptical clip.
/// * [ClipPath], for an arbitrarily shaped clip.
class ClipRSuperellipse extends SingleChildRenderObjectWidget {
/// Creates a rounded-superellipse clip.
///
/// The [borderRadius] defaults to [BorderRadius.zero], i.e. a rectangle with
/// right-angled corners.
///
/// If [clipBehavior] is [Clip.none], no clipping will be applied.
const ClipRSuperellipse({
super.key,
this.borderRadius = BorderRadius.zero,
this.clipper,
this.clipBehavior = Clip.antiAlias,
super.child,
});
/// The border radius of the rounded corners.
///
/// Values are clamped so that horizontal and vertical radii sums do not
/// exceed width/height.
///
/// This value is ignored if [clipper] is non-null.
final BorderRadiusGeometry borderRadius;
/// If non-null, determines which clip to use.
final CustomClipper<RSuperellipse>? clipper;
/// {@macro flutter.rendering.ClipRectLayer.clipBehavior}
///
/// Defaults to [Clip.antiAlias].
final Clip clipBehavior;
@override
RenderClipRSuperellipse createRenderObject(BuildContext context) {
return RenderClipRSuperellipse(
borderRadius: borderRadius,
clipBehavior: clipBehavior,
clipper: clipper,
textDirection: Directionality.maybeOf(context),
);
}
@override
void updateRenderObject(BuildContext context, RenderClipRSuperellipse renderObject) {
renderObject
..borderRadius = borderRadius
..clipBehavior = clipBehavior
..clipper = clipper
..textDirection = Directionality.maybeOf(context);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
DiagnosticsProperty<BorderRadiusGeometry>(
'borderRadius',
borderRadius,
showName: false,
defaultValue: null,
),
);
properties.add(
DiagnosticsProperty<CustomClipper<RSuperellipse>>('clipper', clipper, defaultValue: null),
);
}
}
/// A widget that clips its child using an oval.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=vzWWDO6whIM}

View File

@ -922,7 +922,10 @@ void main() {
await tester.pumpAndSettle();
// Expect the modal dialog box to take all available height.
expect(tester.getSize(find.byType(ClipRRect)), equals(const Size(310.0, 560.0 - 24.0 * 2)));
expect(
tester.getSize(find.byType(ClipRSuperellipse)),
equals(const Size(310.0, 560.0 - 24.0 * 2)),
);
// Check sizes/locations of the text. The text is large so these 2 buttons are stacked.
// Visually the "Cancel" button and "OK" button are the same height when using the
@ -974,7 +977,7 @@ void main() {
const double topAndBottomMargin = 40.0;
const double topAndBottomPadding = 24.0 * 2;
const double leftAndRightPadding = 40.0 * 2;
final Finder modalFinder = find.byType(ClipRRect);
final Finder modalFinder = find.byType(ClipRSuperellipse);
expect(
tester.getSize(modalFinder),
equals(
@ -1081,7 +1084,7 @@ void main() {
return element.widget.runtimeType.toString() == '_CupertinoAlertActionSection';
});
final Finder modalBoundaryFinder = find.byType(ClipRRect);
final Finder modalBoundaryFinder = find.byType(ClipRSuperellipse);
expect(tester.getSize(contentSectionFinder), tester.getSize(modalBoundaryFinder));
@ -1128,7 +1131,7 @@ void main() {
return element.widget.runtimeType.toString() == '_CupertinoAlertContentSection';
});
final Finder modalBoundaryFinder = find.byType(ClipRRect);
final Finder modalBoundaryFinder = find.byType(ClipRSuperellipse);
expect(tester.getSize(contentSectionFinder), tester.getSize(modalBoundaryFinder));
});
@ -1449,7 +1452,7 @@ void main() {
// The buttons should be out of the screen
expect(
tester.getTopLeft(find.text('Button 0')).dy,
greaterThan(tester.getBottomLeft(find.byType(ClipRRect)).dy),
greaterThan(tester.getBottomLeft(find.byType(ClipRSuperellipse)).dy),
);
await expectLater(
find.byType(CupertinoAlertDialog),

View File

@ -357,6 +357,14 @@ void main() {
);
});
test('ClipRSuperellipseLayer prints clipBehavior in debug info', () {
expect(getDebugInfo(ClipRSuperellipseLayer()), contains('clipBehavior: Clip.antiAlias'));
expect(
getDebugInfo(ClipRSuperellipseLayer(clipBehavior: Clip.antiAliasWithSaveLayer)),
contains('clipBehavior: Clip.antiAliasWithSaveLayer'),
);
});
test('ClipPathLayer prints clipBehavior in debug info', () {
expect(getDebugInfo(ClipPathLayer()), contains('clipBehavior: Clip.antiAlias'));
expect(
@ -464,6 +472,18 @@ void main() {
});
});
test('mutating ClipRSuperellipseLayer fields triggers needsAddToScene', () {
final ClipRSuperellipseLayer layer = ClipRSuperellipseLayer(
clipRSuperellipse: RSuperellipse.zero,
);
checkNeedsAddToScene(layer, () {
layer.clipRSuperellipse = RSuperellipse.fromRectAndRadius(unitRect, Radius.zero);
});
checkNeedsAddToScene(layer, () {
layer.clipBehavior = Clip.antiAliasWithSaveLayer;
});
});
test('mutating ClipPath fields triggers needsAddToScene', () {
final ClipPathLayer layer = ClipPathLayer(clipPath: Path());
checkNeedsAddToScene(layer, () {
@ -726,12 +746,16 @@ void main() {
expect(layer.describeClipBounds(), null);
const Rect bounds = Rect.fromLTRB(10, 10, 20, 20);
final RRect rbounds = RRect.fromRectXY(bounds, 2, 2);
final RRect rrBounds = RRect.fromRectXY(bounds, 2, 2);
final RSuperellipse rseBounds = RSuperellipse.fromRectXY(bounds, 2, 2);
layer = ClipRectLayer(clipRect: bounds);
expect(layer.describeClipBounds(), bounds);
layer = ClipRRectLayer(clipRRect: rbounds);
expect(layer.describeClipBounds(), rbounds.outerRect);
layer = ClipRRectLayer(clipRRect: rrBounds);
expect(layer.describeClipBounds(), rrBounds.outerRect);
layer = ClipRSuperellipseLayer(clipRSuperellipse: rseBounds);
expect(layer.describeClipBounds(), rseBounds.outerRect);
layer = ClipPathLayer(clipPath: Path()..addRect(bounds));
expect(layer.describeClipBounds(), bounds);
@ -990,6 +1014,7 @@ void main() {
final OpacityLayer opacityLayer = OpacityLayer();
final ClipRectLayer clipRectLayer = ClipRectLayer();
final ClipRRectLayer clipRRectLayer = ClipRRectLayer();
final ClipRSuperellipseLayer clipRSuperellipseLayer = ClipRSuperellipseLayer();
final ImageFilterLayer imageFilterLayer = ImageFilterLayer();
final BackdropFilterLayer backdropFilterLayer = BackdropFilterLayer();
final ColorFilterLayer colorFilterLayer = ColorFilterLayer();
@ -999,6 +1024,7 @@ void main() {
expect(opacityLayer.supportsRasterization(), true);
expect(clipRectLayer.supportsRasterization(), true);
expect(clipRRectLayer.supportsRasterization(), true);
expect(clipRSuperellipseLayer.supportsRasterization(), true);
expect(imageFilterLayer.supportsRasterization(), true);
expect(backdropFilterLayer.supportsRasterization(), true);
expect(colorFilterLayer.supportsRasterization(), true);

View File

@ -266,6 +266,24 @@ void main() {
});
});
test('PaintingContext.pushClipRSuperellipse reuses the layer', () {
_testPaintingContextLayerReuse<ClipRSuperellipseLayer>((
PaintingContextCallback painter,
PaintingContext context,
Offset offset,
Layer? oldLayer,
) {
return context.pushClipRSuperellipse(
true,
offset,
Rect.zero,
RSuperellipse.fromRectAndRadius(Rect.zero, const Radius.circular(1.0)),
painter,
oldLayer: oldLayer as ClipRSuperellipseLayer?,
);
});
});
test('PaintingContext.pushClipPath reuses the layer', () {
_testPaintingContextLayerReuse<ClipPathLayer>((
PaintingContextCallback painter,

View File

@ -289,6 +289,19 @@ abstract class PaintPattern {
PaintingStyle style,
});
/// Indicates that a rounded superellipse clip is expected next.
///
/// The next rounded superellipse clip is examined. Any arguments that are
/// passed to this method are compared to the actual
/// [Canvas.clipRSuperellipse] call's argument and any mismatches result in
/// failure.
///
/// If no call to [Canvas.clipRSuperellipse] was made, then this results in failure.
///
/// Any calls made between the last matched call (if any) and the
/// [Canvas.clipRSuperellipse] call are ignored.
void clipRSuperellipse({RSuperellipse? rsuperellipse});
/// Indicates that a circle is expected next.
///
/// The next circle is examined. Any arguments that are passed to this method
@ -903,6 +916,11 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher
);
}
@override
void clipRSuperellipse({RSuperellipse? rsuperellipse}) {
_predicates.add(_FunctionPaintPredicate(#clipRSuperellipse, <dynamic>[rsuperellipse]));
}
@override
void circle({
double? x,

View File

@ -151,6 +151,25 @@ class TestRecordingPaintingContext extends ClipContext implements PaintingContex
return null;
}
@override
ClipRSuperellipseLayer? pushClipRSuperellipse(
bool needsCompositing,
Offset offset,
Rect bounds,
RSuperellipse clipRSuperellipse,
PaintingContextCallback painter, {
Clip clipBehavior = Clip.antiAlias,
ClipRSuperellipseLayer? oldLayer,
}) {
clipRSuperellipseAndPaint(
clipRSuperellipse.shift(offset),
clipBehavior,
bounds.shift(offset),
() => painter(this, offset),
);
return null;
}
@override
ClipPathLayer? pushClipPath(
bool needsCompositing,