diff --git a/packages/flutter/lib/src/cupertino/dialog.dart b/packages/flutter/lib/src/cupertino/dialog.dart index 76386b11df..ccb068e870 100644 --- a/packages/flutter/lib/src/cupertino/dialog.dart +++ b/packages/flutter/lib/src/cupertino/dialog.dart @@ -1299,7 +1299,7 @@ class _CupertinoActionSheetState extends State { final List children = [ Flexible( - child: ClipRRect( + child: ClipRSuperellipse( borderRadius: const BorderRadius.all(Radius.circular(12.0)), child: BackdropFilter( filter: ImageFilter.blur( @@ -1593,24 +1593,30 @@ class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackgrou @override Widget build(BuildContext context) { - late final Color backgroundColor; - BorderRadius? borderRadius; + late final Widget child; if (!widget.isCancel) { - backgroundColor = widget.pressed ? _kActionSheetPressedColor : _kActionSheetBackgroundColor; - } else { - backgroundColor = widget.pressed ? _kActionSheetCancelPressedColor : _kActionSheetCancelColor; - borderRadius = const BorderRadius.all(Radius.circular(_kCornerRadius)); - } - return MetaData( - metaData: this, - child: Container( - decoration: BoxDecoration( - color: CupertinoDynamicColor.resolve(backgroundColor, context), - borderRadius: borderRadius, + child = ColoredBox( + color: CupertinoDynamicColor.resolve( + widget.pressed ? _kActionSheetPressedColor : _kActionSheetBackgroundColor, + context, ), child: widget.child, - ), - ); + ); + } else { + child = DecoratedBox( + decoration: ShapeDecoration( + shape: const RoundedSuperellipseBorder( + borderRadius: BorderRadius.all(Radius.circular(_kCornerRadius)), + ), + color: CupertinoDynamicColor.resolve( + widget.pressed ? _kActionSheetCancelPressedColor : _kActionSheetCancelColor, + context, + ), + ), + child: widget.child, + ); + } + return MetaData(metaData: this, child: child); } } diff --git a/packages/flutter/lib/src/painting/box_border.dart b/packages/flutter/lib/src/painting/box_border.dart index f5045f592d..edd0745f61 100644 --- a/packages/flutter/lib/src/painting/box_border.dart +++ b/packages/flutter/lib/src/painting/box_border.dart @@ -24,9 +24,11 @@ import 'edge_insets.dart'; /// interpolated or animated. The [Border] class cannot interpolate between /// different shapes. enum BoxShape { - /// An axis-aligned, 2D rectangle. May have rounded corners (described by a - /// [BorderRadius]). The edges of the rectangle will match the edges of the box - /// into which the [Border] or [BoxDecoration] is painted. + /// An axis-aligned rectangle, optionally with rounded corners. + /// + /// The amount of corner rounding, if any, is determined by the border radius + /// specified by classes such as [BoxDecoration] or [Border]. The rectangle's + /// edges match those of the box in which it is painted. /// /// See also: /// diff --git a/packages/flutter/lib/src/painting/box_decoration.dart b/packages/flutter/lib/src/painting/box_decoration.dart index ab2f757c5c..49842e3740 100644 --- a/packages/flutter/lib/src/painting/box_decoration.dart +++ b/packages/flutter/lib/src/painting/box_decoration.dart @@ -28,8 +28,9 @@ import 'image_provider.dart'; /// /// The box has a [border], a body, and may cast a [boxShadow]. /// -/// The [shape] of the box can be a circle or a rectangle. If it is a rectangle, -/// then the [borderRadius] property controls the roundness of the corners. +/// The [shape] of the box can be [BoxShape.circle] or [BoxShape.rectangle]. If +/// it is [BoxShape.rectangle], then the [borderRadius] property can be used to +/// make it a rounded rectangle ([RRect]). /// /// The body of the box is painted in layers. The bottom-most layer is the /// [color], which fills the box. Above that is the [gradient], which also fills diff --git a/packages/flutter/lib/src/painting/rounded_rectangle_border.dart b/packages/flutter/lib/src/painting/rounded_rectangle_border.dart index 6e21338de6..b33bb9e6d2 100644 --- a/packages/flutter/lib/src/painting/rounded_rectangle_border.dart +++ b/packages/flutter/lib/src/painting/rounded_rectangle_border.dart @@ -16,6 +16,11 @@ import 'border_radius.dart'; import 'borders.dart'; import 'circle_border.dart'; +// A common interface for [RoundedRectangleBorder] and [RoundedSuperellipseBorder]. +mixin _RRectLikeBorder on OutlinedBorder { + BorderRadiusGeometry get borderRadius; +} + /// A rectangular border with rounded corners. /// /// Typically used with [ShapeDecoration] to draw a box with a rounded @@ -28,11 +33,14 @@ import 'circle_border.dart'; /// * [BorderSide], which is used to describe each side of the box. /// * [Border], which, when used with [BoxDecoration], can also /// describe a rounded rectangle. -class RoundedRectangleBorder extends OutlinedBorder { +/// * [RoundedSuperellipseBorder], which uses a smoother shape similar to the one +/// used in iOS design. +class RoundedRectangleBorder extends OutlinedBorder with _RRectLikeBorder { /// Creates a rounded rectangle border. const RoundedRectangleBorder({super.side, this.borderRadius = BorderRadius.zero}); /// The radii for each corner. + @override final BorderRadiusGeometry borderRadius; @override @@ -149,21 +157,244 @@ class RoundedRectangleBorder extends OutlinedBorder { } } -class _RoundedRectangleToCircleBorder extends OutlinedBorder { +class _RoundedRectangleToCircleBorder extends _ShapeToCircleBorder { const _RoundedRectangleToCircleBorder({ + super.side, + super.borderRadius = BorderRadius.zero, + required super.circularity, + required super.eccentricity, + }); + + @override + void drawShape(Canvas canvas, Rect rect, BorderRadius radius, Paint paint, [double? inflation]) { + RRect rrect = radius.toRRect(rect); + if (inflation != null) { + rrect = rrect.inflate(inflation); + } + canvas.drawRRect(rrect, paint); + } + + @override + Path buildPath(Rect rect, BorderRadius radius, [double? inflation]) { + RRect rrect = radius.toRRect(rect); + if (inflation != null) { + rrect = rrect.inflate(inflation); + } + return Path()..addRRect(rrect); + } + + @override + _RoundedRectangleToCircleBorder copyWith({ + BorderSide? side, + BorderRadiusGeometry? borderRadius, + double? circularity, + double? eccentricity, + }) { + return _RoundedRectangleToCircleBorder( + side: side ?? this.side, + borderRadius: borderRadius ?? this.borderRadius, + circularity: circularity ?? this.circularity, + eccentricity: eccentricity ?? this.eccentricity, + ); + } +} + +/// A rectangular border with rounded corners following the shape of an +/// [RSuperellipse]. +/// +/// Typically used with [ShapeDecoration] to draw a box that mimics the rounded +/// rectangle style commonly seen in iOS design. +/// +/// See also: +/// +/// * [RSuperellipse], which defines the shape. +/// * [RoundedRectangleBorder], which uses the traditional [RRect] shape. +class RoundedSuperellipseBorder extends OutlinedBorder with _RRectLikeBorder { + /// Creates a rounded rectangle border. + const RoundedSuperellipseBorder({super.side, this.borderRadius = BorderRadius.zero}); + + /// The radii for each corner. + @override + final BorderRadiusGeometry borderRadius; + + @override + ShapeBorder scale(double t) { + return RoundedSuperellipseBorder(side: side.scale(t), borderRadius: borderRadius * t); + } + + @override + ShapeBorder? lerpFrom(ShapeBorder? a, double t) { + if (a is RoundedSuperellipseBorder) { + return RoundedSuperellipseBorder( + side: BorderSide.lerp(a.side, side, t), + borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!, + ); + } + if (a is CircleBorder) { + return _RoundedSuperellipseToCircleBorder( + side: BorderSide.lerp(a.side, side, t), + borderRadius: borderRadius, + circularity: 1.0 - t, + eccentricity: a.eccentricity, + ); + } + return super.lerpFrom(a, t); + } + + @override + ShapeBorder? lerpTo(ShapeBorder? b, double t) { + if (b is RoundedSuperellipseBorder) { + return RoundedSuperellipseBorder( + side: BorderSide.lerp(side, b.side, t), + borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!, + ); + } + if (b is CircleBorder) { + return _RoundedSuperellipseToCircleBorder( + side: BorderSide.lerp(side, b.side, t), + borderRadius: borderRadius, + circularity: t, + eccentricity: b.eccentricity, + ); + } + return super.lerpTo(b, t); + } + + /// Returns a copy of this RoundedSuperellipseBorder with the given fields + /// replaced with the new values. + @override + RoundedSuperellipseBorder copyWith({BorderSide? side, BorderRadiusGeometry? borderRadius}) { + return RoundedSuperellipseBorder( + side: side ?? this.side, + borderRadius: borderRadius ?? this.borderRadius, + ); + } + + @override + Path getInnerPath(Rect rect, {TextDirection? textDirection}) { + final RSuperellipse borderRect = borderRadius.resolve(textDirection).toRSuperellipse(rect); + final RSuperellipse adjustedRect = borderRect.deflate(side.strokeInset); + return Path()..addRSuperellipse(adjustedRect); + } + + @override + Path getOuterPath(Rect rect, {TextDirection? textDirection}) { + return Path()..addRSuperellipse(borderRadius.resolve(textDirection).toRSuperellipse(rect)); + } + + @override + void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) { + if (borderRadius == BorderRadius.zero) { + canvas.drawRect(rect, paint); + } else { + canvas.drawRSuperellipse(borderRadius.resolve(textDirection).toRSuperellipse(rect), paint); + } + } + + @override + bool get preferPaintInterior => true; + + @override + void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { + switch (side.style) { + case BorderStyle.none: + break; + case BorderStyle.solid: + if (side.width == 0.0) { + canvas.drawRSuperellipse( + borderRadius.resolve(textDirection).toRSuperellipse(rect), + side.toPaint(), + ); + } else { + final double strokeOffset = (side.strokeOutset - side.strokeInset) / 2; + final RSuperellipse base = borderRadius + .resolve(textDirection) + .toRSuperellipse(rect) + .inflate(strokeOffset); + canvas.drawRSuperellipse(base, side.toPaint()); + } + } + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is RoundedSuperellipseBorder && + other.side == side && + other.borderRadius == borderRadius; + } + + @override + int get hashCode => Object.hash(side, borderRadius); + + @override + String toString() { + return '${objectRuntimeType(this, 'RoundedSuperellipseBorder')}($side, $borderRadius)'; + } +} + +class _RoundedSuperellipseToCircleBorder extends _ShapeToCircleBorder { + const _RoundedSuperellipseToCircleBorder({ + super.side, + super.borderRadius = BorderRadius.zero, + required super.circularity, + required super.eccentricity, + }); + + @override + void drawShape(Canvas canvas, Rect rect, BorderRadius radius, Paint paint, [double? inflation]) { + RSuperellipse rsuperellipse = radius.toRSuperellipse(rect); + if (inflation != null) { + rsuperellipse = rsuperellipse.inflate(inflation); + } + canvas.drawRSuperellipse(rsuperellipse, paint); + } + + @override + Path buildPath(Rect rect, BorderRadius radius, [double? inflation]) { + RSuperellipse rsuperellipse = radius.toRSuperellipse(rect); + if (inflation != null) { + rsuperellipse = rsuperellipse.inflate(inflation); + } + return Path()..addRSuperellipse(rsuperellipse); + } + + @override + _RoundedSuperellipseToCircleBorder copyWith({ + BorderSide? side, + BorderRadiusGeometry? borderRadius, + double? circularity, + double? eccentricity, + }) { + return _RoundedSuperellipseToCircleBorder( + side: side ?? this.side, + borderRadius: borderRadius ?? this.borderRadius, + circularity: circularity ?? this.circularity, + eccentricity: eccentricity ?? this.eccentricity, + ); + } +} + +abstract class _ShapeToCircleBorder extends OutlinedBorder { + const _ShapeToCircleBorder({ super.side, this.borderRadius = BorderRadius.zero, required this.circularity, required this.eccentricity, }); + void drawShape(Canvas canvas, Rect rect, BorderRadius radius, Paint paint, [double? inflation]); + Path buildPath(Rect rect, BorderRadius radius, [double? inflation]); + final BorderRadiusGeometry borderRadius; final double circularity; final double eccentricity; @override ShapeBorder scale(double t) { - return _RoundedRectangleToCircleBorder( + return copyWith( side: side.scale(t), borderRadius: borderRadius * t, circularity: t, @@ -173,27 +404,27 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { @override ShapeBorder? lerpFrom(ShapeBorder? a, double t) { - if (a is RoundedRectangleBorder) { - return _RoundedRectangleToCircleBorder( + if (a is T) { + return copyWith( side: BorderSide.lerp(a.side, side, t), - borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!, + borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t), circularity: circularity * t, eccentricity: eccentricity, ); } if (a is CircleBorder) { - return _RoundedRectangleToCircleBorder( + return copyWith( side: BorderSide.lerp(a.side, side, t), borderRadius: borderRadius, circularity: circularity + (1.0 - circularity) * (1.0 - t), eccentricity: a.eccentricity, ); } - if (a is _RoundedRectangleToCircleBorder) { - return _RoundedRectangleToCircleBorder( + if (a is _ShapeToCircleBorder) { + return copyWith( side: BorderSide.lerp(a.side, side, t), - borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!, - circularity: ui.lerpDouble(a.circularity, circularity, t)!, + borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t), + circularity: ui.lerpDouble(a.circularity, circularity, t), eccentricity: eccentricity, ); } @@ -202,27 +433,27 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { @override ShapeBorder? lerpTo(ShapeBorder? b, double t) { - if (b is RoundedRectangleBorder) { - return _RoundedRectangleToCircleBorder( + if (b is T) { + return copyWith( side: BorderSide.lerp(side, b.side, t), - borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!, + borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t), circularity: circularity * (1.0 - t), eccentricity: eccentricity, ); } if (b is CircleBorder) { - return _RoundedRectangleToCircleBorder( + return copyWith( side: BorderSide.lerp(side, b.side, t), borderRadius: borderRadius, circularity: circularity + (1.0 - circularity) * t, eccentricity: b.eccentricity, ); } - if (b is _RoundedRectangleToCircleBorder) { - return _RoundedRectangleToCircleBorder( + if (b is _ShapeToCircleBorder) { + return copyWith( side: BorderSide.lerp(side, b.side, t), - borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!, - circularity: ui.lerpDouble(circularity, b.circularity, t)!, + borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t), + circularity: ui.lerpDouble(circularity, b.circularity, t), eccentricity: eccentricity, ); } @@ -244,7 +475,7 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { } } - BorderRadius? _adjustBorderRadius(Rect rect, TextDirection? textDirection) { + BorderRadius _adjustBorderRadius(Rect rect, TextDirection? textDirection) { final BorderRadius resolvedRadius = borderRadius.resolve(textDirection); if (circularity == 0.0) { return resolvedRadius; @@ -272,48 +503,42 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { resolvedRadius, BorderRadius.circular(rect.shortestSide / 2), circularity, - ); + )!; } @override Path getInnerPath(Rect rect, {TextDirection? textDirection}) { - final RRect borderRect = _adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect)); - final RRect adjustedRect = borderRect.deflate(ui.lerpDouble(side.width, 0, side.strokeAlign)!); - return Path()..addRRect(adjustedRect); + return buildPath( + _adjustRect(rect), + _adjustBorderRadius(rect, textDirection), + -ui.lerpDouble(side.width, 0, side.strokeAlign)!, + ); } @override Path getOuterPath(Rect rect, {TextDirection? textDirection}) { - return Path()..addRRect(_adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect))); + return buildPath(_adjustRect(rect), _adjustBorderRadius(rect, textDirection)); } @override void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) { - final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect, textDirection)!; + final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect, textDirection); if (adjustedBorderRadius == BorderRadius.zero) { canvas.drawRect(_adjustRect(rect), paint); } else { - canvas.drawRRect(adjustedBorderRadius.toRRect(_adjustRect(rect)), paint); + drawShape(canvas, _adjustRect(rect), adjustedBorderRadius, paint); } } @override bool get preferPaintInterior => true; - @override - _RoundedRectangleToCircleBorder copyWith({ + _ShapeToCircleBorder copyWith({ BorderSide? side, BorderRadiusGeometry? borderRadius, double? circularity, double? eccentricity, - }) { - return _RoundedRectangleToCircleBorder( - side: side ?? this.side, - borderRadius: borderRadius ?? this.borderRadius, - circularity: circularity ?? this.circularity, - eccentricity: eccentricity ?? this.eccentricity, - ); - } + }); @override void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { @@ -321,9 +546,13 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { case BorderStyle.none: break; case BorderStyle.solid: - final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect, textDirection)!; - final RRect borderRect = adjustedBorderRadius.toRRect(_adjustRect(rect)); - canvas.drawRRect(borderRect.inflate(side.strokeOffset / 2), side.toPaint()); + drawShape( + canvas, + _adjustRect(rect), + _adjustBorderRadius(rect, textDirection), + side.toPaint(), + side.strokeOffset / 2, + ); } } @@ -332,7 +561,7 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { if (other.runtimeType != runtimeType) { return false; } - return other is _RoundedRectangleToCircleBorder && + return other is _ShapeToCircleBorder && other.side == side && other.borderRadius == borderRadius && other.circularity == circularity; @@ -344,8 +573,8 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder { @override String toString() { if (eccentricity != 0.0) { - return 'RoundedRectangleBorder($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)'; + return '$T($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)'; } - return 'RoundedRectangleBorder($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)'; + return '$T($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)'; } } diff --git a/packages/flutter/test/cupertino/action_sheet_test.dart b/packages/flutter/test/cupertino/action_sheet_test.dart index 9f02faf91b..45aaf17884 100644 --- a/packages/flutter/test/cupertino/action_sheet_test.dart +++ b/packages/flutter/test/cupertino/action_sheet_test.dart @@ -380,19 +380,19 @@ void main() { // Content section should be at the bottom left of action sheet // (minus padding). expect( - tester.getBottomLeft(find.byType(ClipRRect)), + tester.getBottomLeft(find.byType(ClipRSuperellipse)), tester.getBottomLeft(find.byType(CupertinoActionSheet)) - const Offset(-8.0, 8.0), ); // Check that the dialog size is the same as the content section size // (minus padding). expect( - tester.getSize(find.byType(ClipRRect)).height, + tester.getSize(find.byType(ClipRSuperellipse)).height, tester.getSize(find.byType(CupertinoActionSheet)).height - 16.0, ); expect( - tester.getSize(find.byType(ClipRRect)).width, + tester.getSize(find.byType(ClipRSuperellipse)).width, tester.getSize(find.byType(CupertinoActionSheet)).width - 16.0, ); }); diff --git a/packages/flutter/test/painting/rounded_superellipse_border_test.dart b/packages/flutter/test/painting/rounded_superellipse_border_test.dart new file mode 100644 index 0000000000..be637ace4f --- /dev/null +++ b/packages/flutter/test/painting/rounded_superellipse_border_test.dart @@ -0,0 +1,210 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'common_matchers.dart'; + +void main() { + test('RoundedSuperellipseBorder defaults', () { + const RoundedSuperellipseBorder border = RoundedSuperellipseBorder(); + expect(border.side, BorderSide.none); + expect(border.borderRadius, BorderRadius.zero); + }); + + test('RoundedSuperellipseBorder copyWith, ==, hashCode', () { + expect(const RoundedSuperellipseBorder(), const RoundedSuperellipseBorder().copyWith()); + expect( + const RoundedSuperellipseBorder().hashCode, + const RoundedSuperellipseBorder().copyWith().hashCode, + ); + const BorderSide side = BorderSide(width: 10.0, color: Color(0xff123456)); + const BorderRadius radius = BorderRadius.all(Radius.circular(16.0)); + const BorderRadiusDirectional directionalRadius = BorderRadiusDirectional.all( + Radius.circular(16.0), + ); + + expect( + const RoundedSuperellipseBorder().copyWith(side: side, borderRadius: radius), + const RoundedSuperellipseBorder(side: side, borderRadius: radius), + ); + + expect( + const RoundedSuperellipseBorder().copyWith(side: side, borderRadius: directionalRadius), + const RoundedSuperellipseBorder(side: side, borderRadius: directionalRadius), + ); + }); + + test('RoundedSuperellipseBorder', () { + const RoundedSuperellipseBorder c10 = RoundedSuperellipseBorder( + side: BorderSide(width: 10.0), + borderRadius: BorderRadius.all(Radius.circular(100.0)), + ); + const RoundedSuperellipseBorder c15 = RoundedSuperellipseBorder( + side: BorderSide(width: 15.0), + borderRadius: BorderRadius.all(Radius.circular(150.0)), + ); + const RoundedSuperellipseBorder c20 = RoundedSuperellipseBorder( + side: BorderSide(width: 20.0), + borderRadius: BorderRadius.all(Radius.circular(200.0)), + ); + expect(c10.dimensions, const EdgeInsets.all(10.0)); + expect(c10.scale(2.0), c20); + expect(c20.scale(0.5), c10); + expect(ShapeBorder.lerp(c10, c20, 0.0), c10); + expect(ShapeBorder.lerp(c10, c20, 0.5), c15); + expect(ShapeBorder.lerp(c10, c20, 1.0), c20); + + const RoundedSuperellipseBorder c1 = RoundedSuperellipseBorder( + side: BorderSide(), + borderRadius: BorderRadius.all(Radius.circular(1.0)), + ); + const RoundedSuperellipseBorder c2 = RoundedSuperellipseBorder( + side: BorderSide(), + borderRadius: BorderRadius.all(Radius.circular(2.0)), + ); + expect(c2.getInnerPath(Rect.fromCircle(center: Offset.zero, radius: 2.0)), isUnitCircle); + expect(c1.getOuterPath(Rect.fromCircle(center: Offset.zero, radius: 1.0)), isUnitCircle); + const Rect rect = Rect.fromLTRB(10.0, 20.0, 80.0, 190.0); + expect( + (Canvas canvas) => c10.paint(canvas, rect), + paints..rsuperellipse( + rsuperellipse: RSuperellipse.fromRectAndRadius( + rect.deflate(5.0), + const Radius.circular(95.0), + ), + strokeWidth: 10.0, + ), + ); + + const RoundedSuperellipseBorder directional = RoundedSuperellipseBorder( + borderRadius: BorderRadiusDirectional.only(topStart: Radius.circular(20)), + ); + expect(ShapeBorder.lerp(directional, c10, 1.0), ShapeBorder.lerp(c10, directional, 0.0)); + }); + + test('RoundedSuperellipseBorder and CircleBorder', () { + const RoundedSuperellipseBorder r = RoundedSuperellipseBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ); + const CircleBorder c = CircleBorder(); + const Rect rect = Rect.fromLTWH(0.0, 0.0, 100.0, 20.0); // center is x=40..60 y=10 + final Matcher looksLikeR = isPathThat( + includes: const [Offset(30.0, 10.0), Offset(50.0, 10.0)], + excludes: const [Offset(1.0, 1.0), Offset(99.0, 19.0)], + ); + final Matcher looksLikeC = isPathThat( + includes: const [Offset(50.0, 10.0)], + excludes: const [Offset(1.0, 1.0), Offset(30.0, 10.0), Offset(99.0, 19.0)], + ); + expect(r.getOuterPath(rect), looksLikeR); + expect(c.getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(r, c, 0.1)!.getOuterPath(rect), looksLikeR); + expect(ShapeBorder.lerp(r, c, 0.9)!.getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.9), r, 0.1)!.getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.9), r, 0.9)!.getOuterPath(rect), looksLikeR); + expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.1), c, 0.1)!.getOuterPath(rect), looksLikeR); + expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.1), c, 0.9)!.getOuterPath(rect), looksLikeC); + expect( + ShapeBorder.lerp( + ShapeBorder.lerp(r, c, 0.1), + ShapeBorder.lerp(r, c, 0.9), + 0.1, + )!.getOuterPath(rect), + looksLikeR, + ); + expect( + ShapeBorder.lerp( + ShapeBorder.lerp(r, c, 0.1), + ShapeBorder.lerp(r, c, 0.9), + 0.9, + )!.getOuterPath(rect), + looksLikeC, + ); + expect(ShapeBorder.lerp(r, ShapeBorder.lerp(r, c, 0.9), 0.1)!.getOuterPath(rect), looksLikeR); + expect(ShapeBorder.lerp(r, ShapeBorder.lerp(r, c, 0.9), 0.9)!.getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(c, ShapeBorder.lerp(r, c, 0.1), 0.1)!.getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(c, ShapeBorder.lerp(r, c, 0.1), 0.9)!.getOuterPath(rect), looksLikeR); + + expect( + ShapeBorder.lerp(r, c, 0.1).toString(), + 'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(10.0), 10.0% of the way to being a CircleBorder)', + ); + expect( + ShapeBorder.lerp(r, c, 0.2).toString(), + 'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(10.0), 20.0% of the way to being a CircleBorder)', + ); + expect( + ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.1), ShapeBorder.lerp(r, c, 0.9), 0.9).toString(), + 'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(10.0), 82.0% of the way to being a CircleBorder)', + ); + + expect( + ShapeBorder.lerp(c, r, 0.9).toString(), + 'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(10.0), 10.0% of the way to being a CircleBorder)', + ); + expect( + ShapeBorder.lerp(c, r, 0.8).toString(), + 'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(10.0), 20.0% of the way to being a CircleBorder)', + ); + expect( + ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.9), ShapeBorder.lerp(r, c, 0.1), 0.1).toString(), + 'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(10.0), 82.0% of the way to being a CircleBorder)', + ); + + expect(ShapeBorder.lerp(r, c, 0.1), ShapeBorder.lerp(r, c, 0.1)); + expect(ShapeBorder.lerp(r, c, 0.1).hashCode, ShapeBorder.lerp(r, c, 0.1).hashCode); + + final ShapeBorder direct50 = ShapeBorder.lerp(r, c, 0.5)!; + final ShapeBorder indirect50 = + ShapeBorder.lerp(ShapeBorder.lerp(c, r, 0.1), ShapeBorder.lerp(c, r, 0.9), 0.5)!; + expect(direct50, indirect50); + expect(direct50.hashCode, indirect50.hashCode); + expect(direct50.toString(), indirect50.toString()); + }); + + test('RoundedSuperellipseBorder.dimensions and CircleBorder.dimensions', () { + const RoundedSuperellipseBorder insideRoundedSuperellipseBorder = RoundedSuperellipseBorder( + side: BorderSide(width: 10), + ); + expect(insideRoundedSuperellipseBorder.dimensions, const EdgeInsets.all(10)); + + const RoundedSuperellipseBorder centerRoundedSuperellipseBorder = RoundedSuperellipseBorder( + side: BorderSide(width: 10, strokeAlign: BorderSide.strokeAlignCenter), + ); + expect(centerRoundedSuperellipseBorder.dimensions, const EdgeInsets.all(5)); + + const RoundedSuperellipseBorder outsideRoundedSuperellipseBorder = RoundedSuperellipseBorder( + side: BorderSide(width: 10, strokeAlign: BorderSide.strokeAlignOutside), + ); + expect(outsideRoundedSuperellipseBorder.dimensions, EdgeInsets.zero); + + const CircleBorder insideCircleBorder = CircleBorder(side: BorderSide(width: 10)); + expect(insideCircleBorder.dimensions, const EdgeInsets.all(10)); + + const CircleBorder centerCircleBorder = CircleBorder( + side: BorderSide(width: 10, strokeAlign: BorderSide.strokeAlignCenter), + ); + expect(centerCircleBorder.dimensions, const EdgeInsets.all(5)); + + const CircleBorder outsideCircleBorder = CircleBorder( + side: BorderSide(width: 10, strokeAlign: BorderSide.strokeAlignOutside), + ); + expect(outsideCircleBorder.dimensions, EdgeInsets.zero); + }); + + test('RoundedSuperellipseBorder.lerp with different StrokeAlign', () { + const RoundedSuperellipseBorder rInside = RoundedSuperellipseBorder( + side: BorderSide(width: 10.0), + ); + const RoundedSuperellipseBorder rOutside = RoundedSuperellipseBorder( + side: BorderSide(width: 20.0, strokeAlign: BorderSide.strokeAlignOutside), + ); + const RoundedSuperellipseBorder rCenter = RoundedSuperellipseBorder( + side: BorderSide(width: 15.0, strokeAlign: BorderSide.strokeAlignCenter), + ); + expect(ShapeBorder.lerp(rInside, rOutside, 0.5), rCenter); + }); +}