From cd3715a854c517c83750db186e10385440dba595 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 5 Oct 2017 01:12:21 -0700 Subject: [PATCH] Border RTL (#12407) --- .../flutter/lib/src/painting/borders.dart | 7 +- .../flutter/lib/src/painting/box_border.dart | 569 +++++++++++++-- .../lib/src/painting/box_decoration.dart | 20 +- .../flutter/lib/src/painting/decoration.dart | 7 +- .../test/painting/border_rtl_test.dart | 671 ++++++++++++++++++ .../test/painting/border_side_test.dart | 7 +- .../flutter/test/painting/border_test.dart | 139 ++++ .../test/painting/shape_border_test.dart | 65 ++ .../flutter/test/rendering/mock_canvas.dart | 146 +++- .../test/widgets/transitions_test.dart | 16 +- 10 files changed, 1544 insertions(+), 103 deletions(-) create mode 100644 packages/flutter/test/painting/border_rtl_test.dart diff --git a/packages/flutter/lib/src/painting/borders.dart b/packages/flutter/lib/src/painting/borders.dart index 3c4fab6188..03f910ea4e 100644 --- a/packages/flutter/lib/src/painting/borders.dart +++ b/packages/flutter/lib/src/painting/borders.dart @@ -194,10 +194,13 @@ class BorderSide { return a; if (t == 1.0) return b; + final double width = ui.lerpDouble(a.width, b.width, t); + if (width < 0.0) + return BorderSide.none; if (a.style == b.style) { return new BorderSide( color: Color.lerp(a.color, b.color, t), - width: math.max(0.0, ui.lerpDouble(a.width, b.width, t)), + width: width, style: a.style, // == b.style ); } @@ -220,7 +223,7 @@ class BorderSide { } return new BorderSide( color: Color.lerp(colorA, colorB, t), - width: math.max(0.0, ui.lerpDouble(a.width, b.width, t)), + width: width, style: BorderStyle.solid, ); } diff --git a/packages/flutter/lib/src/painting/box_border.dart b/packages/flutter/lib/src/painting/box_border.dart index e9b5876e43..fb987d59b4 100644 --- a/packages/flutter/lib/src/painting/box_border.dart +++ b/packages/flutter/lib/src/painting/box_border.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; + import 'basic_types.dart'; import 'border_radius.dart'; import 'borders.dart'; @@ -21,7 +23,180 @@ enum BoxShape { circle, } -/// A border of a box, comprised of four sides. +/// Base class for box borders that can paint as rectangle, circles, or rounded +/// rectangles. +/// +/// This class is extended by [Border] and [BorderDirectional] to provide +/// concrete versions of four-sided borders using different conventions for +/// specifying the sides. +/// +/// The only API difference that this class introduces over [ShapeBorder] is +/// that its [paint] method takes additional arguments. +/// +/// See also: +/// +/// * [BorderSide], which is used to describe each side of the box. +/// * [RoundedRectangleBorder], another way of describing a box's border. +/// * [CircleBorder], another way of describing a circle border. +/// * [BoxDecoration], which uses a [BoxBorder] to describe its borders. +abstract class BoxBorder extends ShapeBorder { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const BoxBorder(); + + // We override this to tighten the return value, so that callers can assume + // that we'll return a BoxBorder. + @override + BoxBorder add(ShapeBorder other, { bool reversed: false }) => null; + + /// Linearly interpolate between two borders. + /// + /// If a border is null, it is treated as having four [BorderSide.none] + /// borders. + /// + /// This supports interpolating between [Border] and [BorderDirectional] + /// objects. If both objects are different types but both have sides on one or + /// both of their lateral edges (the two sides that aren't the top and bottom) + /// other than [BorderSide.none], then the sides are interpolated by reducing + /// `a`'s lateral edges to [BorderSide.none] over the first half of the + /// animation, and then bringing `b`'s lateral edges _from_ [BorderSide.none] + /// over the second half of the animation. + /// + /// For a more flexible approach, consider [ShapeBorder.lerp], which would + /// instead [add] the two sets of sides and interpolate them simultaneously. + static BoxBorder lerp(BoxBorder a, BoxBorder b, double t) { + if ((a is Border || a == null) && (b is Border || b == null)) + return Border.lerp(a, b, t); + if ((a is BorderDirectional || a == null) && (b is BorderDirectional || b == null)) + return BorderDirectional.lerp(a, b, t); + if (b is Border && a is BorderDirectional) { + final BoxBorder c = b; + b = a; + a = c; + t = 1.0 - t; + // fall through to next case + } + if (a is Border && b is BorderDirectional) { + if (b.start == BorderSide.none && b.end == BorderSide.none) { + // The fact that b is a BorderDirectional really doesn't matter, it turns out. + return new Border( + top: BorderSide.lerp(a.top, b.top, t), + right: BorderSide.lerp(a.right, BorderSide.none, t), + bottom: BorderSide.lerp(a.bottom, b.bottom, t), + left: BorderSide.lerp(a.left, BorderSide.none, t), + ); + } + if (a.left == BorderSide.none && a.right == BorderSide.none) { + // The fact that a is a Border really doesn't matter, it turns out. + return new BorderDirectional( + top: BorderSide.lerp(a.top, b.top, t), + start: BorderSide.lerp(BorderSide.none, b.start, t), + end: BorderSide.lerp(BorderSide.none, b.end, t), + bottom: BorderSide.lerp(a.bottom, b.bottom, t), + ); + } + // Since we have to swap a visual border for a directional one, + // we speed up the horizontal sides' transitions and switch from + // one mode to the other at t=0.5. + if (t < 0.5) { + return new Border( + top: BorderSide.lerp(a.top, b.top, t), + right: BorderSide.lerp(a.right, BorderSide.none, t * 2.0), + bottom: BorderSide.lerp(a.bottom, b.bottom, t), + left: BorderSide.lerp(a.left, BorderSide.none, t * 2.0), + ); + } + return new BorderDirectional( + top: BorderSide.lerp(a.top, b.top, t), + start: BorderSide.lerp(BorderSide.none, b.start, (t - 0.5) * 2.0), + end: BorderSide.lerp(BorderSide.none, b.end, (t - 0.5) * 2.0), + bottom: BorderSide.lerp(a.bottom, b.bottom, t), + ); + } + throw new FlutterError( + 'BoxBorder.lerp can only interpolate Border and BorderDirectional classes.\n' + 'BoxBorder.lerp() was called with two objects of type ${a.runtimeType} and ${b.runtimeType}:\n' + ' $a\n' + ' $b\n' + 'However, only Border and BorderDirectional classes are supported by this method. ' + 'For a more general interpolation method, consider using ShapeBorder.lerp instead.' + ); + } + + @override + Path getInnerPath(Rect rect, { @required TextDirection textDirection }) { + assert(textDirection != null, 'The textDirection argument to $runtimeType.getInnerPath must not be null.'); + return new Path() + ..addRect(dimensions.resolve(textDirection).deflateRect(rect)); + } + + @override + Path getOuterPath(Rect rect, { @required TextDirection textDirection }) { + assert(textDirection != null, 'The textDirection argument to $runtimeType.getOuterPath must not be null.'); + return new Path() + ..addRect(rect); + } + + /// Paints the border within the given [Rect] on the given [Canvas]. + /// + /// This is an extension of the [ShapeBorder.paint] method. It allows + /// [BoxBorder] borders to be applied to different [BoxShape]s and with + /// different [borderRadius] parameters, without changing the [BoxBorder] + /// object itself. + /// + /// The `shape` argument specifies the [BoxShape] to draw the border on. + /// + /// If the `shape` is specifies a rectangular box shape + /// ([BoxShape.rectangle]), then the `borderRadius` argument describes the + /// corners of the rectangle. + /// + /// The [getInnerPath] and [getOuterPath] methods do not know about the + /// `shape` and `borderRadius` arguments. + /// + /// See also: + /// + /// * [paintBorder], which is used if the border is not uniform. + @override + void paint(Canvas canvas, Rect rect, { + TextDirection textDirection, + BoxShape shape: BoxShape.rectangle, + BorderRadius borderRadius, + }); + + static void _paintUniformBorderWithRadius(Canvas canvas, Rect rect, BorderSide side, BorderRadius borderRadius) { + assert(side.style != BorderStyle.none); + final Paint paint = new Paint() + ..color = side.color; + final RRect outer = borderRadius.toRRect(rect); + final double width = side.width; + if (width == 0.0) { + paint + ..style = PaintingStyle.stroke + ..strokeWidth = 0.0; + canvas.drawRRect(outer, paint); + } else { + final RRect inner = outer.deflate(width); + canvas.drawDRRect(outer, inner, paint); + } + } + + static void _paintUniformBorderWithCircle(Canvas canvas, Rect rect, BorderSide side) { + assert(side.style != BorderStyle.none); + final double width = side.width; + final Paint paint = side.toPaint(); + final double radius = (rect.shortestSide - width) / 2.0; + canvas.drawCircle(rect.center, radius, paint); + } + + static void _paintUniformBorderWithRectangle(Canvas canvas, Rect rect, BorderSide side) { + assert(side.style != BorderStyle.none); + final double width = side.width; + final Paint paint = side.toPaint(); + canvas.drawRect(rect.deflate(width / 2.0), paint); + } +} + +/// A border of a box, comprised of four sides: top, right, bottom, left. /// /// The sides are represented by [BorderSide] objects. /// @@ -77,16 +252,21 @@ enum BoxShape { /// * [BorderSide], which is used to describe each side of the box. /// * [Theme], from the material layer, which can be queried to obtain appropriate colors /// to use for borders in a material app, as shown in the "divider" sample above. -class Border extends ShapeBorder { +class Border extends BoxBorder { /// Creates a border. /// /// All the sides of the border default to [BorderSide.none]. + /// + /// The arguments must not be null. const Border({ this.top: BorderSide.none, this.right: BorderSide.none, this.bottom: BorderSide.none, this.left: BorderSide.none, - }); + }) : assert(top != null), + assert(right != null), + assert(bottom != null), + assert(left != null); /// A uniform border with all sides the same color and width. /// @@ -142,11 +322,6 @@ class Border extends ShapeBorder { /// Whether all four sides of the border are identical. Uniform borders are /// typically more efficient to paint. bool get isUniform { - assert(top != null); - assert(right != null); - assert(bottom != null); - assert(left != null); - final Color topColor = top.color; if (right.color != topColor || bottom.color != topColor || @@ -240,18 +415,6 @@ class Border extends ShapeBorder { ); } - @override - Path getInnerPath(Rect rect, { TextDirection textDirection }) { - return new Path() - ..addRect(dimensions.resolve(textDirection).deflateRect(rect)); - } - - @override - Path getOuterPath(Rect rect, { TextDirection textDirection }) { - return new Path() - ..addRect(rect); - } - /// Paints the border within the given [Rect] on the given [Canvas]. /// /// Uniform borders are more efficient to paint than more complex borders. @@ -284,14 +447,14 @@ class Border extends ShapeBorder { case BorderStyle.solid: if (shape == BoxShape.circle) { assert(borderRadius == null, 'A borderRadius can only be given for rectangular boxes.'); - _paintUniformBorderWithCircle(canvas, rect); + BoxBorder._paintUniformBorderWithCircle(canvas, rect, top); return; } if (borderRadius != null) { - _paintUniformBorderWithRadius(canvas, rect, borderRadius); + BoxBorder._paintUniformBorderWithRadius(canvas, rect, top, borderRadius); return; } - _paintUniformBorderWithRectangle(canvas, rect); + BoxBorder._paintUniformBorderWithRectangle(canvas, rect, top); return; } } @@ -302,42 +465,6 @@ class Border extends ShapeBorder { paintBorder(canvas, rect, top: top, right: right, bottom: bottom, left: left); } - void _paintUniformBorderWithRadius(Canvas canvas, Rect rect, - BorderRadius borderRadius) { - assert(isUniform); - assert(top.style != BorderStyle.none); - final Paint paint = new Paint() - ..color = top.color; - final RRect outer = borderRadius.toRRect(rect); - final double width = top.width; - if (width == 0.0) { - paint - ..style = PaintingStyle.stroke - ..strokeWidth = 0.0; - canvas.drawRRect(outer, paint); - } else { - final RRect inner = outer.deflate(width); - canvas.drawDRRect(outer, inner, paint); - } - } - - void _paintUniformBorderWithCircle(Canvas canvas, Rect rect) { - assert(isUniform); - assert(top.style != BorderStyle.none); - final double width = top.width; - final Paint paint = top.toPaint(); - final double radius = (rect.shortestSide - width) / 2.0; - canvas.drawCircle(rect.center, radius, paint); - } - - void _paintUniformBorderWithRectangle(Canvas canvas, Rect rect) { - assert(isUniform); - assert(top.style != BorderStyle.none); - final double width = top.width; - final Paint paint = top.toPaint(); - canvas.drawRect(rect.deflate(width / 2.0), paint); - } - @override bool operator ==(dynamic other) { if (identical(this, other)) @@ -358,6 +485,328 @@ class Border extends ShapeBorder { String toString() { if (isUniform) return 'Border.all($top)'; - return 'Border($top, $right, $bottom, $left)'; + final List arguments = []; + if (top != BorderSide.none) + arguments.add('top: $top'); + if (right != BorderSide.none) + arguments.add('right: $right'); + if (bottom != BorderSide.none) + arguments.add('bottom: $bottom'); + if (left != BorderSide.none) + arguments.add('left: $left'); + return 'Border(${arguments.join(", ")})'; + } +} + +/// A border of a box, comprised of four sides, the lateral sides of which +/// flip over based on the reading direction. +/// +/// The lateral sides are called [start] and [end]. When painted in +/// left-to-right environments, the [start] side will be painted on the left and +/// the [end] side on the right; in right-to-left environments, it is the +/// reverse. The other two sides are [top] and [bottom]. +/// +/// The sides are represented by [BorderSide] objects. +/// +/// If the [start] and [end] sides are the same, then it is slightly more +/// efficient to use a [Border] object rather than a [BorderDirectional] object. +/// +/// See also: +/// +/// * [BoxDecoration], which uses this class to describe its edge decoration. +/// * [BorderSide], which is used to describe each side of the box. +/// * [Theme], from the material layer, which can be queried to obtain appropriate colors +/// to use for borders in a material app, as shown in the "divider" sample above. +class BorderDirectional extends BoxBorder { + /// Creates a border. + /// + /// The [start] and [end] sides represent the horizontal sides; the start side + /// is on the leading edge given the reading direction, and the end side is on + /// the trailing edge. They are resolved during [paint]. + /// + /// All the sides of the border default to [BorderSide.none]. + /// + /// The arguments must not be null. + const BorderDirectional({ + this.top: BorderSide.none, + this.start: BorderSide.none, + this.end: BorderSide.none, + this.bottom: BorderSide.none, + }) : assert(top != null), + assert(start != null), + assert(end != null), + assert(bottom != null); + + /// Creates a [BorderDirectional] that represents the addition of the two + /// given [BorderDirectional]s. + /// + /// It is only valid to call this if [BorderSide.canMerge] returns true for + /// the pairwise combination of each side on both [BorderDirectional]s. + /// + /// The arguments must not be null. + static BorderDirectional merge(BorderDirectional a, BorderDirectional b) { + assert(a != null); + assert(b != null); + assert(BorderSide.canMerge(a.top, b.top)); + assert(BorderSide.canMerge(a.start, b.start)); + assert(BorderSide.canMerge(a.end, b.end)); + assert(BorderSide.canMerge(a.bottom, b.bottom)); + return new BorderDirectional( + top: BorderSide.merge(a.top, b.top), + start: BorderSide.merge(a.start, b.start), + end: BorderSide.merge(a.end, b.end), + bottom: BorderSide.merge(a.bottom, b.bottom), + ); + } + + /// The top side of this border. + final BorderSide top; + + /// The start side of this border. + /// + /// This is the side on the left in left-to-right text and on the right in + /// right-to-left text. + /// + /// See also: + /// + /// * [TextDirection], which is used to describe the reading direction. + final BorderSide start; + + /// The end side of this border. + /// + /// This is the side on the right in left-to-right text and on the left in + /// right-to-left text. + /// + /// See also: + /// + /// * [TextDirection], which is used to describe the reading direction. + final BorderSide end; + + /// The bottom side of this border. + final BorderSide bottom; + + @override + EdgeInsetsGeometry get dimensions { + return new EdgeInsetsDirectional.fromSTEB(start.width, top.width, end.width, bottom.width); + } + + /// Whether all four sides of the border are identical. Uniform borders are + /// typically more efficient to paint. + bool get isUniform { + final Color topColor = top.color; + if (start.color != topColor || + end.color != topColor || + bottom.color != topColor) + return false; + + final double topWidth = top.width; + if (start.width != topWidth || + end.width != topWidth || + bottom.width != topWidth) + return false; + + final BorderStyle topStyle = top.style; + if (start.style != topStyle || + end.style != topStyle || + bottom.style != topStyle) + return false; + + return true; + } + + @override + BoxBorder add(ShapeBorder other, { bool reversed: false }) { + if (other is BorderDirectional) { + final BorderDirectional typedOther = other; + if (BorderSide.canMerge(top, typedOther.top) && + BorderSide.canMerge(start, typedOther.start) && + BorderSide.canMerge(end, typedOther.end) && + BorderSide.canMerge(bottom, typedOther.bottom)) { + return BorderDirectional.merge(this, typedOther); + } + return null; + } + if (other is Border) { + final Border typedOther = other; + if (!BorderSide.canMerge(typedOther.top, top) || + !BorderSide.canMerge(typedOther.bottom, bottom)) + return null; + if (start != BorderSide.none || + end != BorderSide.none) { + if (typedOther.left != BorderSide.none || + typedOther.right != BorderSide.none) + return null; + assert(typedOther.left == BorderSide.none); + assert(typedOther.right == BorderSide.none); + return new BorderDirectional( + top: BorderSide.merge(typedOther.top, top), + start: start, + end: end, + bottom: BorderSide.merge(typedOther.bottom, bottom), + ); + } + assert(start == BorderSide.none); + assert(end == BorderSide.none); + return new Border( + top: BorderSide.merge(typedOther.top, top), + right: typedOther.right, + bottom: BorderSide.merge(typedOther.bottom, bottom), + left: typedOther.left, + ); + } + return null; + } + + /// Creates a new border with the widths of this border multiplied by `t`. + @override + BorderDirectional scale(double t) { + return new BorderDirectional( + top: top.scale(t), + start: start.scale(t), + end: end.scale(t), + bottom: bottom.scale(t), + ); + } + + /// Linearly interpolates from `a` to [this]. + /// + /// If `a` is null, this defers to [scale]. + /// + /// If `a` is also a [BorderDirectional], this uses [BorderDirectional.lerp]. + /// + /// Otherwise, it defers to [ShapeBorder.lerpFrom]. + @override + ShapeBorder lerpFrom(ShapeBorder a, double t) { + if (a is BorderDirectional) + return BorderDirectional.lerp(a, this, t); + return super.lerpFrom(a, t); + } + + /// Linearly interpolates from [this] to `b`. + /// + /// If `b` is null, this defers to [scale]. + /// + /// If `b` is also a [BorderDirectional], this uses [BorderDirectional.lerp]. + /// + /// Otherwise, it defers to [ShapeBorder.lerpTo]. + @override + ShapeBorder lerpTo(ShapeBorder b, double t) { + if (b is BorderDirectional) + return BorderDirectional.lerp(this, b, t); + return super.lerpTo(b, t); + } + + /// Linearly interpolate between two borders. + /// + /// If a border is null, it is treated as having four [BorderSide.none] + /// borders. + static BorderDirectional lerp(BorderDirectional a, BorderDirectional b, double t) { + if (a == null && b == null) + return null; + if (a == null) + return b.scale(t); + if (b == null) + return a.scale(1.0 - t); + return new BorderDirectional( + top: BorderSide.lerp(a.top, b.top, t), + end: BorderSide.lerp(a.end, b.end, t), + bottom: BorderSide.lerp(a.bottom, b.bottom, t), + start: BorderSide.lerp(a.start, b.start, t) + ); + } + + /// Paints the border within the given [Rect] on the given [Canvas]. + /// + /// Uniform borders are more efficient to paint than more complex borders. + /// + /// You can provide a [BoxShape] to draw the border on. If the `shape` in + /// [BoxShape.circle], there is the requirement that the border [isUniform]. + /// + /// If you specify a rectangular box shape ([BoxShape.rectangle]), then you + /// may specify a [BorderRadius]. If a `borderRadius` is specified, there is + /// the requirement that the border [isUniform]. + /// + /// The [getInnerPath] and [getOuterPath] methods do not know about the + /// `shape` and `borderRadius` arguments. + /// + /// The `textDirection` argument is used to determine which of [start] and + /// [end] map to the left and right. For [TextDirection.ltr], the [start] is + /// the left and the [end] is the right; for [TextDirection.rtl], it is the + /// reverse. + /// + /// See also: + /// + /// * [paintBorder], which is used if the border is not uniform. + @override + void paint(Canvas canvas, Rect rect, { + TextDirection textDirection, + BoxShape shape: BoxShape.rectangle, + BorderRadius borderRadius, + }) { + if (isUniform) { + switch (top.style) { + case BorderStyle.none: + return; + case BorderStyle.solid: + if (shape == BoxShape.circle) { + assert(borderRadius == null, 'A borderRadius can only be given for rectangular boxes.'); + BoxBorder._paintUniformBorderWithCircle(canvas, rect, top); + return; + } + if (borderRadius != null) { + BoxBorder._paintUniformBorderWithRadius(canvas, rect, top, borderRadius); + return; + } + BoxBorder._paintUniformBorderWithRectangle(canvas, rect, top); + return; + } + } + + assert(borderRadius == null, 'A borderRadius can only be given for uniform borders.'); + assert(shape == BoxShape.rectangle, 'A border can only be drawn as a circle if it is uniform.'); + + BorderSide left, right; + assert(textDirection != null, 'Non-uniform BorderDirectional objects require a TextDirection when painting.'); + switch (textDirection) { + case TextDirection.rtl: + left = end; + right = start; + break; + case TextDirection.ltr: + left = start; + right = end; + break; + } + paintBorder(canvas, rect, top: top, left: left, bottom: bottom, right: right); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (runtimeType != other.runtimeType) + return false; + final BorderDirectional typedOther = other; + return top == typedOther.top && + start == typedOther.start && + end == typedOther.end && + bottom == typedOther.bottom; + } + + @override + int get hashCode => hashValues(top, start, end, bottom); + + @override + String toString() { + final List arguments = []; + if (top != BorderSide.none) + arguments.add('top: $top'); + if (start != BorderSide.none) + arguments.add('start: $start'); + if (end != BorderSide.none) + arguments.add('end: $end'); + if (bottom != BorderSide.none) + arguments.add('bottom: $bottom'); + return 'BorderDirectional(${arguments.join(", ")})'; } } diff --git a/packages/flutter/lib/src/painting/box_decoration.dart b/packages/flutter/lib/src/painting/box_decoration.dart index 00db38df77..a008430317 100644 --- a/packages/flutter/lib/src/painting/box_decoration.dart +++ b/packages/flutter/lib/src/painting/box_decoration.dart @@ -110,7 +110,14 @@ class BoxDecoration extends Decoration { /// A border to draw above the background [color], [gradient], or [image]. /// /// Follows the [shape] and [borderRadius]. - final Border border; + /// + /// Use [Border] objects to describe borders that do not depend on the reading + /// direction. + /// + /// Use [BoxBorder] objects to describe borders that should flip their left + /// and right edges based on whether the text is being read left-to-right or + /// right-to-left. + final BoxBorder border; /// If non-null, the corners of this box are rounded by this [BorderRadius]. /// @@ -137,7 +144,7 @@ class BoxDecoration extends Decoration { final BoxShape shape; @override - EdgeInsets get padding => border?.dimensions; + EdgeInsetsGeometry get padding => border?.dimensions; /// Returns a new box decoration that is scaled by the given factor. BoxDecoration scale(double factor) { @@ -145,7 +152,7 @@ class BoxDecoration extends Decoration { return new BoxDecoration( color: Color.lerp(null, color, factor), image: image, - border: Border.lerp(null, border, factor), + border: BoxBorder.lerp(null, border, factor), borderRadius: BorderRadius.lerp(null, borderRadius, factor), boxShadow: BoxShadow.lerpList(null, boxShadow, factor), gradient: gradient, @@ -192,7 +199,7 @@ class BoxDecoration extends Decoration { return new BoxDecoration( color: Color.lerp(a.color, b.color, t), image: t < 0.5 ? a.image : b.image, - border: Border.lerp(a.border, b.border, t), + border: BoxBorder.lerp(a.border, b.border, t), borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t), gradient: t < 0.5 ? a.gradient : b.gradient, @@ -238,7 +245,7 @@ class BoxDecoration extends Decoration { properties.add(new DiagnosticsProperty('color', color, defaultValue: null)); properties.add(new DiagnosticsProperty('image', image, defaultValue: null)); - properties.add(new DiagnosticsProperty('border', border, defaultValue: null)); + properties.add(new DiagnosticsProperty('border', border, defaultValue: null)); properties.add(new DiagnosticsProperty('borderRadius', borderRadius, defaultValue: null)); properties.add(new IterableProperty('boxShadow', boxShadow, defaultValue: null, style: DiagnosticsTreeStyle.whitespace)); properties.add(new DiagnosticsProperty('gradient', gradient, defaultValue: null)); @@ -421,7 +428,8 @@ class _BoxDecorationPainter extends BoxPainter { canvas, rect, shape: _decoration.shape, - borderRadius: _decoration.borderRadius + borderRadius: _decoration.borderRadius, + textDirection: configuration.textDirection, ); } } diff --git a/packages/flutter/lib/src/painting/decoration.dart b/packages/flutter/lib/src/painting/decoration.dart index 37f94d31cb..3595f0c479 100644 --- a/packages/flutter/lib/src/painting/decoration.dart +++ b/packages/flutter/lib/src/painting/decoration.dart @@ -53,7 +53,12 @@ abstract class Decoration extends Diagnosticable { /// does not take into account that the circle is drawn in the center of the /// box regardless of the ratio of the box; it does not provide the extra /// padding that is implied by changing the ratio. - EdgeInsets get padding => EdgeInsets.zero; + /// + /// The value returned by this getter must be resolved (using + /// [EdgeInsetsGeometry.resolve] to obtain an absolute [EdgeInsets]. (For + /// example, [BorderDirectional] will return an [EdgeInsetsDirectional] for + /// its [padding].) + EdgeInsetsGeometry get padding => EdgeInsets.zero; /// Whether this decoration is complex enough to benefit from caching its painting. bool get isComplex => false; diff --git a/packages/flutter/test/painting/border_rtl_test.dart b/packages/flutter/test/painting/border_rtl_test.dart new file mode 100644 index 0000000000..d5c6598b33 --- /dev/null +++ b/packages/flutter/test/painting/border_rtl_test.dart @@ -0,0 +1,671 @@ +// Copyright 2017 The Chromium 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 '../rendering/mock_canvas.dart'; + +class SillyBorder extends BoxBorder { + @override + dynamic noSuchMethod(Invocation invocation) => null; +} + +void main() { + test('BoxBorder.lerp', () { + // names of the form fooAtX are foo, lerped X% of the way to null + final BoxBorder directionalWithMagentaTop5 = const BorderDirectional(top: const BorderSide(color: const Color(0xFFFF00FF), width: 5.0)); + final BoxBorder directionalWithMagentaTop5At25 = const BorderDirectional(top: const BorderSide(color: const Color(0x3F3F003F), width: 1.25)); + final BoxBorder directionalWithMagentaTop5At75 = const BorderDirectional(top: const BorderSide(color: const Color(0xBFBF00BF), width: 3.75)); + final BoxBorder directionalWithSides10 = const BorderDirectional(start: const BorderSide(width: 10.0), end: const BorderSide(width: 20.0)); + final BoxBorder directionalWithSides10At25 = const BorderDirectional(start: const BorderSide(width: 2.5, color: const Color(0x3F000000)), end: const BorderSide(width: 5.0, color: const Color(0x3F000000))); + final BoxBorder directionalWithSides10At50 = const BorderDirectional(start: const BorderSide(width: 5.0, color: const Color(0x7F000000)), end: const BorderSide(width: 10.0, color: const Color(0x7F000000))); + final BoxBorder directionalWithSides10At75 = const BorderDirectional(start: const BorderSide(width: 7.5, color: const Color(0xBF000000)), end: const BorderSide(width: 15.0, color: const Color(0xBF000000))); + final BoxBorder directionalWithSides20 = const BorderDirectional(start: const BorderSide(width: 20.0), end: const BorderSide(width: 40.0)); + final BoxBorder directionalWithSides30 = const BorderDirectional(start: const BorderSide(width: 30.0), end: const BorderSide(width: 60.0)); + final BoxBorder directionalWithTop10 = const BorderDirectional(top: const BorderSide(width: 10.0)); + final BoxBorder directionalWithYellowTop10 = const BorderDirectional(top: const BorderSide(color: const Color(0xFFFFFF00), width: 10.0)); + final BoxBorder directionalWithYellowTop5 = const BorderDirectional(top: const BorderSide(color: const Color(0xFFFFFF00), width: 5.0)); + final BoxBorder visualWithMagentaTop10 = const Border(top: const BorderSide(color: const Color(0xFFFF00FF), width: 10.0)); + final BoxBorder visualWithMagentaTop5 = const Border(top: const BorderSide(color: const Color(0xFFFF00FF), width: 5.0)); + final BoxBorder visualWithSides10 = const Border(left: const BorderSide(width: 10.0), right: const BorderSide(width: 20.0)); + final BoxBorder visualWithSides10At25 = const Border(left: const BorderSide(width: 2.5, color: const Color(0x3F000000)), right: const BorderSide(width: 5.0, color: const Color(0x3F000000))); + final BoxBorder visualWithSides10At50 = const Border(left: const BorderSide(width: 5.0, color: const Color(0x7F000000)), right: const BorderSide(width: 10.0, color: const Color(0x7F000000))); + final BoxBorder visualWithSides10At75 = const Border(left: const BorderSide(width: 7.5, color: const Color(0xBF000000)), right: const BorderSide(width: 15.0, color: const Color(0xBF000000))); + final BoxBorder visualWithSides20 = const Border(left: const BorderSide(width: 20.0), right: const BorderSide(width: 40.0)); + final BoxBorder visualWithSides30 = const Border(left: const BorderSide(width: 30.0), right: const BorderSide(width: 60.0)); + final BoxBorder visualWithTop10 = const Border(top: const BorderSide(width: 10.0)); + final BoxBorder visualWithTop100 = const Border(top: const BorderSide(width: 100.0)); + final BoxBorder visualWithTop190 = const Border(top: const BorderSide(width: 190.0)); + final BoxBorder visualWithYellowTop5 = const Border(top: const BorderSide(color: const Color(0xFFFFFF00), width: 5.0)); + final BoxBorder visualWithYellowTop5At25 = const Border(top: const BorderSide(color: const Color(0x3F3F3F00), width: 1.25)); + final BoxBorder visualWithYellowTop5At75 = const Border(top: const BorderSide(color: const Color(0xBFBFBF00), width: 3.75)); + + expect(BoxBorder.lerp(null, null, -1.0), null); + expect(BoxBorder.lerp(new Border.all(width: 10.0), null, -1.0), new Border.all(width: 20.0)); + expect(BoxBorder.lerp(null, new Border.all(width: 10.0), -1.0), new Border.all(width: 0.0, style: BorderStyle.none)); + expect(BoxBorder.lerp(directionalWithTop10, null, -1.0), const BorderDirectional(top: const BorderSide(width: 20.0))); + expect(BoxBorder.lerp(null, directionalWithTop10, -1.0), const BorderDirectional()); + expect(BoxBorder.lerp(directionalWithTop10, visualWithTop100, -1.0), const Border()); + expect(BoxBorder.lerp(visualWithSides10, directionalWithMagentaTop5, -1.0), visualWithSides20); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithMagentaTop5, -1.0), const Border(top: const BorderSide(color: const Color(0xFFFFFF00), width: 5.0))); + expect(BoxBorder.lerp(visualWithSides10, directionalWithSides10, -1.0), visualWithSides30); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithSides10, -1.0), directionalWithYellowTop10); + expect(() => BoxBorder.lerp(new SillyBorder(), const Border(), -1.0), throwsFlutterError); + + expect(BoxBorder.lerp(null, null, 0.0), null); + expect(BoxBorder.lerp(new Border.all(width: 10.0), null, 0.0), new Border.all(width: 10.0)); + expect(BoxBorder.lerp(null, new Border.all(width: 10.0), 0.0), const Border()); + expect(BoxBorder.lerp(directionalWithTop10, null, 0.0), const BorderDirectional(top: const BorderSide(width: 10.0))); + expect(BoxBorder.lerp(null, directionalWithTop10, 0.0), const BorderDirectional()); + expect(BoxBorder.lerp(directionalWithTop10, visualWithTop100, 0.0), visualWithTop10); + expect(BoxBorder.lerp(visualWithSides10, directionalWithMagentaTop5, 0.0), visualWithSides10); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithMagentaTop5, 0.0), const Border(top: const BorderSide(color: const Color(0xFFFFFF00), width: 5.0))); + expect(BoxBorder.lerp(visualWithSides10, directionalWithSides10, 0.0), visualWithSides10); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithSides10, 0.0), directionalWithYellowTop5); + expect(() => BoxBorder.lerp(new SillyBorder(), const Border(), 0.0), throwsFlutterError); + + expect(BoxBorder.lerp(null, null, 0.25), null); + expect(BoxBorder.lerp(new Border.all(width: 10.0), null, 0.25), new Border.all(width: 7.5)); + expect(BoxBorder.lerp(null, new Border.all(width: 10.0), 0.25), new Border.all(width: 2.5)); + expect(BoxBorder.lerp(directionalWithTop10, null, 0.25), const BorderDirectional(top: const BorderSide(width: 7.5))); + expect(BoxBorder.lerp(null, directionalWithTop10, 0.25), const BorderDirectional(top: const BorderSide(width: 2.5))); + expect(BoxBorder.lerp(directionalWithTop10, visualWithTop100, 0.25), const Border(top: const BorderSide(width: 32.5))); + expect(BoxBorder.lerp(visualWithSides10, directionalWithMagentaTop5, 0.25), visualWithSides10At75 + directionalWithMagentaTop5At25); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithMagentaTop5, 0.25), new Border(top: new BorderSide(width: 5.0, color: Color.lerp(const Color(0xFFFFFF00), const Color(0xFFFF00FF), 0.25)))); + expect(BoxBorder.lerp(visualWithSides10, directionalWithSides10, 0.25), visualWithSides10At50); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithSides10, 0.25), visualWithYellowTop5At75 + directionalWithSides10At25); + expect(() => BoxBorder.lerp(new SillyBorder(), const Border(), 0.25), throwsFlutterError); + + expect(BoxBorder.lerp(null, null, 0.75), null); + expect(BoxBorder.lerp(new Border.all(width: 10.0), null, 0.75), new Border.all(width: 2.5)); + expect(BoxBorder.lerp(null, new Border.all(width: 10.0), 0.75), new Border.all(width: 7.5)); + expect(BoxBorder.lerp(directionalWithTop10, null, 0.75), const BorderDirectional(top: const BorderSide(width: 2.5))); + expect(BoxBorder.lerp(null, directionalWithTop10, 0.75), const BorderDirectional(top: const BorderSide(width: 7.5))); + expect(BoxBorder.lerp(directionalWithTop10, visualWithTop100, 0.75), const Border(top: const BorderSide(width: 77.5))); + expect(BoxBorder.lerp(visualWithSides10, directionalWithMagentaTop5, 0.75), visualWithSides10At25 + directionalWithMagentaTop5At75); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithMagentaTop5, 0.75), new Border(top: new BorderSide(width: 5.0, color: Color.lerp(const Color(0xFFFFFF00), const Color(0xFFFF00FF), 0.75)))); + expect(BoxBorder.lerp(visualWithSides10, directionalWithSides10, 0.75), directionalWithSides10At50); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithSides10, 0.75), visualWithYellowTop5At25 + directionalWithSides10At75); + expect(() => BoxBorder.lerp(new SillyBorder(), const Border(), 0.75), throwsFlutterError); + + expect(BoxBorder.lerp(null, null, 1.0), null); + expect(BoxBorder.lerp(new Border.all(width: 10.0), null, 1.0), new Border.all(width: 0.0, style: BorderStyle.none)); + expect(BoxBorder.lerp(null, new Border.all(width: 10.0), 1.0), new Border.all(width: 10.0)); + expect(BoxBorder.lerp(directionalWithTop10, null, 1.0), const BorderDirectional()); + expect(BoxBorder.lerp(null, directionalWithTop10, 1.0), const BorderDirectional(top: const BorderSide(width: 10.0))); + expect(BoxBorder.lerp(directionalWithTop10, visualWithTop100, 1.0), visualWithTop100); + expect(BoxBorder.lerp(visualWithSides10, directionalWithMagentaTop5, 1.0), visualWithMagentaTop5); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithMagentaTop5, 1.0), visualWithMagentaTop5); + expect(BoxBorder.lerp(visualWithSides10, directionalWithSides10, 1.0), directionalWithSides10); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithSides10, 1.0), directionalWithSides10); + expect(() => BoxBorder.lerp(new SillyBorder(), const Border(), 1.0), throwsFlutterError); + + expect(BoxBorder.lerp(null, null, 2.0), null); + expect(BoxBorder.lerp(new Border.all(width: 10.0), null, 2.0), new Border.all(width: 0.0, style: BorderStyle.none)); + expect(BoxBorder.lerp(null, new Border.all(width: 10.0), 2.0), new Border.all(width: 20.0)); + expect(BoxBorder.lerp(directionalWithTop10, null, 2.0), const BorderDirectional()); + expect(BoxBorder.lerp(null, directionalWithTop10, 2.0), const BorderDirectional(top: const BorderSide(width: 20.0))); + expect(BoxBorder.lerp(directionalWithTop10, visualWithTop100, 2.0), visualWithTop190); + expect(BoxBorder.lerp(visualWithSides10, directionalWithMagentaTop5, 2.0), visualWithMagentaTop10); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithMagentaTop5, 2.0), visualWithMagentaTop5); + expect(BoxBorder.lerp(visualWithSides10, directionalWithSides10, 2.0), directionalWithSides30); + expect(BoxBorder.lerp(visualWithYellowTop5, directionalWithSides10, 2.0), directionalWithSides20); + expect(() => BoxBorder.lerp(new SillyBorder(), const Border(), 2.0), throwsFlutterError); + }); + + void verifyPath(Path path, { + Iterable includes: const [], + Iterable excludes: const [], + }) { + for (Offset offset in includes) + expect(path.contains(offset), isTrue, reason: 'Offset $offset should be inside the path.'); + for (Offset offset in excludes) + expect(path.contains(offset), isFalse, reason: 'Offset $offset should be outside the path.'); + } + + test('BoxBorder.getInnerPath / BoxBorder.getOuterPath', () { + // for Border, BorderDirectional + final Border border = const Border(top: const BorderSide(width: 10.0), right: const BorderSide(width: 20.0)); + final BorderDirectional borderDirectional = const BorderDirectional(top: const BorderSide(width: 10.0), end: const BorderSide(width: 20.0)); + verifyPath( + border.getOuterPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl), + includes: [ + const Offset(50.0, 60.0), + const Offset(60.0, 60.0), + const Offset(60.0, 70.0), + const Offset(80.0, 190.0), + const Offset(109.0, 189.0), + const Offset(110.0, 80.0), + const Offset(110.0, 190.0), + ], + excludes: [ + const Offset(40.0, 60.0), + const Offset(50.0, 50.0), + const Offset(111.0, 190.0), + const Offset(110.0, 191.0), + const Offset(111.0, 191.0), + const Offset(0.0, 0.0), + const Offset(-10.0, -10.0), + const Offset(0.0, -10.0), + const Offset(-10.0, 0.0), + const Offset(1000.0, 1000.0), + ], + ); + verifyPath( + border.getInnerPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl), + // inner path is a rect from 50.0,70.0 to 90.0,190.0 + includes: [ + const Offset(50.0, 70.0), + const Offset(55.0, 70.0), + const Offset(50.0, 75.0), + const Offset(70.0, 70.0), + const Offset(70.0, 71.0), + const Offset(71.0, 70.0), + const Offset(71.0, 71.0), + const Offset(80.0, 180.0), + const Offset(80.0, 190.0), + const Offset(89.0, 189.0), + const Offset(90.0, 190.0), + ], + excludes: [ + const Offset(40.0, 60.0), + const Offset(50.0, 50.0), + const Offset(50.0, 60.0), + const Offset(60.0, 60.0), + const Offset(0.0, 0.0), + const Offset(-10.0, -10.0), + const Offset(0.0, -10.0), + const Offset(-10.0, 0.0), + const Offset(110.0, 80.0), + const Offset(89.0, 191.0), + const Offset(90.0, 191.0), + const Offset(91.0, 189.0), + const Offset(91.0, 190.0), + const Offset(91.0, 191.0), + const Offset(109.0, 189.0), + const Offset(110.0, 190.0), + const Offset(1000.0, 1000.0), + ], + ); + verifyPath( + borderDirectional.getOuterPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl), + includes: [ + const Offset(50.0, 60.0), + const Offset(60.0, 60.0), + const Offset(60.0, 70.0), + const Offset(80.0, 190.0), + const Offset(109.0, 189.0), + const Offset(110.0, 80.0), + const Offset(110.0, 190.0), + ], + excludes: [ + const Offset(40.0, 60.0), + const Offset(50.0, 50.0), + const Offset(111.0, 190.0), + const Offset(110.0, 191.0), + const Offset(111.0, 191.0), + const Offset(0.0, 0.0), + const Offset(-10.0, -10.0), + const Offset(0.0, -10.0), + const Offset(-10.0, 0.0), + const Offset(1000.0, 1000.0), + ], + ); + verifyPath( + borderDirectional.getInnerPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl), + // inner path is a rect from 70.0,70.0 to 110.0,190.0 + includes: [ + const Offset(70.0, 70.0), + const Offset(70.0, 71.0), + const Offset(71.0, 70.0), + const Offset(71.0, 71.0), + const Offset(80.0, 180.0), + const Offset(80.0, 190.0), + const Offset(89.0, 189.0), + const Offset(90.0, 190.0), + const Offset(91.0, 189.0), + const Offset(91.0, 190.0), + const Offset(109.0, 189.0), + const Offset(110.0, 80.0), + const Offset(110.0, 190.0), + ], + excludes: [ + const Offset(40.0, 60.0), + const Offset(50.0, 50.0), + const Offset(50.0, 60.0), + const Offset(50.0, 70.0), + const Offset(50.0, 75.0), + const Offset(55.0, 70.0), + const Offset(60.0, 60.0), + const Offset(0.0, 0.0), + const Offset(-10.0, -10.0), + const Offset(0.0, -10.0), + const Offset(-10.0, 0.0), + const Offset(89.0, 191.0), + const Offset(90.0, 191.0), + const Offset(91.0, 191.0), + const Offset(110.0, 191.0), + const Offset(111.0, 190.0), + const Offset(111.0, 191.0), + const Offset(1000.0, 1000.0), + ], + ); + verifyPath( + borderDirectional.getOuterPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.ltr), + includes: [ + const Offset(50.0, 60.0), + const Offset(60.0, 60.0), + const Offset(60.0, 70.0), + const Offset(80.0, 190.0), + const Offset(109.0, 189.0), + const Offset(110.0, 80.0), + const Offset(110.0, 190.0), + ], + excludes: [ + const Offset(40.0, 60.0), + const Offset(50.0, 50.0), + const Offset(111.0, 190.0), + const Offset(110.0, 191.0), + const Offset(111.0, 191.0), + const Offset(0.0, 0.0), + const Offset(-10.0, -10.0), + const Offset(0.0, -10.0), + const Offset(-10.0, 0.0), + const Offset(1000.0, 1000.0), + ], + ); + verifyPath( + borderDirectional.getInnerPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.ltr), + // inner path is a rect from 50.0,70.0 to 90.0,190.0 + includes: [ + const Offset(50.0, 70.0), + const Offset(50.0, 75.0), + const Offset(55.0, 70.0), + const Offset(70.0, 70.0), + const Offset(70.0, 71.0), + const Offset(71.0, 70.0), + const Offset(71.0, 71.0), + const Offset(80.0, 180.0), + const Offset(80.0, 190.0), + const Offset(89.0, 189.0), + const Offset(90.0, 190.0), + ], + excludes: [ + const Offset(50.0, 50.0), + const Offset(40.0, 60.0), + const Offset(50.0, 60.0), + const Offset(60.0, 60.0), + const Offset(0.0, 0.0), + const Offset(-10.0, -10.0), + const Offset(0.0, -10.0), + const Offset(-10.0, 0.0), + const Offset(110.0, 80.0), + const Offset(89.0, 191.0), + const Offset(90.0, 191.0), + const Offset(91.0, 189.0), + const Offset(91.0, 190.0), + const Offset(91.0, 191.0), + const Offset(109.0, 189.0), + const Offset(110.0, 190.0), + const Offset(1000.0, 1000.0), + ], + ); + }); + + test('BorderDirectional constructor', () { + final Null $null = null; + expect(() => new BorderDirectional(top: $null), throwsAssertionError); + expect(() => new BorderDirectional(start: $null), throwsAssertionError); + expect(() => new BorderDirectional(end: $null), throwsAssertionError); + expect(() => new BorderDirectional(bottom: $null), throwsAssertionError); + }); + + test('BorderDirectional.merge', () { + final BorderSide magenta3 = const BorderSide(color: const Color(0xFFFF00FF), width: 3.0); + final BorderSide magenta6 = const BorderSide(color: const Color(0xFFFF00FF), width: 6.0); + final BorderSide yellow2 = const BorderSide(color: const Color(0xFFFFFF00), width: 2.0); + final BorderSide yellowNone0 = const BorderSide(color: const Color(0xFFFFFF00), width: 0.0, style: BorderStyle.none); + expect( + BorderDirectional.merge( + new BorderDirectional(top: yellow2), + new BorderDirectional(end: magenta3), + ), + new BorderDirectional(top: yellow2, end: magenta3), + ); + expect( + BorderDirectional.merge( + new BorderDirectional(bottom: magenta3), + new BorderDirectional(bottom: magenta3), + ), + new BorderDirectional(bottom: magenta6), + ); + expect( + BorderDirectional.merge( + new BorderDirectional(start: magenta3, end: yellowNone0), + new BorderDirectional(end: yellow2), + ), + new BorderDirectional(start: magenta3, end: yellow2), + ); + expect( + BorderDirectional.merge(const BorderDirectional(), const BorderDirectional()), + const BorderDirectional(), + ); + expect( + () => BorderDirectional.merge( + new BorderDirectional(start: magenta3), + new BorderDirectional(start: yellow2), + ), + throwsAssertionError, + ); + }); + + test('BorderDirectional.dimensions', () { + expect( + const BorderDirectional( + top: const BorderSide(width: 3.0), + start: const BorderSide(width: 2.0), + end: const BorderSide(width: 7.0), + bottom: const BorderSide(width: 5.0), + ).dimensions, + const EdgeInsetsDirectional.fromSTEB(2.0, 3.0, 7.0, 5.0), + ); + }); + + test('BorderDirectional.isUniform', () { + expect( + const BorderDirectional( + top: const BorderSide(width: 3.0), + start: const BorderSide(width: 3.0), + end: const BorderSide(width: 3.0), + bottom: const BorderSide(width: 3.1), + ).isUniform, + false, + ); + expect( + const BorderDirectional( + top: const BorderSide(width: 3.0), + start: const BorderSide(width: 3.0), + end: const BorderSide(width: 3.0), + bottom: const BorderSide(width: 3.0), + ).isUniform, + true, + ); + expect( + const BorderDirectional( + top: const BorderSide(color: const Color(0xFFFFFFFF)), + start: const BorderSide(color: const Color(0xFFFFFFFE)), + end: const BorderSide(color: const Color(0xFFFFFFFF)), + bottom: const BorderSide(color: const Color(0xFFFFFFFF)), + ).isUniform, + false, + ); + expect( + const BorderDirectional( + top: const BorderSide(color: const Color(0xFFFFFFFF)), + start: const BorderSide(color: const Color(0xFFFFFFFF)), + end: const BorderSide(color: const Color(0xFFFFFFFF)), + bottom: const BorderSide(color: const Color(0xFFFFFFFF)), + ).isUniform, + true, + ); + expect( + const BorderDirectional( + top: const BorderSide(style: BorderStyle.none), + start: const BorderSide(style: BorderStyle.none), + end: const BorderSide(style: BorderStyle.none), + bottom: const BorderSide(style: BorderStyle.solid, width: 0.0), + ).isUniform, + false, + ); + expect( + const BorderDirectional( + top: const BorderSide(style: BorderStyle.none), + start: const BorderSide(style: BorderStyle.none), + end: const BorderSide(style: BorderStyle.none), + bottom: BorderSide.none, + ).isUniform, + false, + ); + expect( + const BorderDirectional( + top: const BorderSide(style: BorderStyle.none, width: 0.0), + start: const BorderSide(style: BorderStyle.none, width: 0.0), + end: const BorderSide(style: BorderStyle.none, width: 0.0), + bottom: BorderSide.none, + ).isUniform, + true, + ); + expect( + const BorderDirectional().isUniform, + true, + ); + }); + + test('BorderDirectional.add - all directional', () { + final BorderSide magenta3 = const BorderSide(color: const Color(0xFFFF00FF), width: 3.0); + final BorderSide magenta6 = const BorderSide(color: const Color(0xFFFF00FF), width: 6.0); + final BorderSide yellow2 = const BorderSide(color: const Color(0xFFFFFF00), width: 2.0); + final BorderSide yellowNone0 = const BorderSide(color: const Color(0xFFFFFF00), width: 0.0, style: BorderStyle.none); + expect( + new BorderDirectional(top: yellow2) + new BorderDirectional(end: magenta3), + new BorderDirectional(top: yellow2, end: magenta3), + ); + expect( + new BorderDirectional(bottom: magenta3) + new BorderDirectional(bottom: magenta3), + new BorderDirectional(bottom: magenta6), + ); + expect( + new BorderDirectional(start: magenta3, end: yellowNone0) + new BorderDirectional(end: yellow2), + new BorderDirectional(start: magenta3, end: yellow2), + ); + expect( + const BorderDirectional() + const BorderDirectional(), + const BorderDirectional(), + ); + expect( + new BorderDirectional(start: magenta3) + new BorderDirectional(start: yellow2), + isNot(const isInstanceOf()), // see shape_border_test.dart for better tests of this case + ); + final BorderDirectional b3 = new BorderDirectional(top: magenta3); + final BorderDirectional b6 = new BorderDirectional(top: magenta6); + expect(b3 + b3, b6); + final BorderDirectional b0 = new BorderDirectional(top: yellowNone0); + final BorderDirectional bZ = const BorderDirectional(); + expect(b0 + b0, bZ); + expect(bZ + bZ, bZ); + expect(b0 + bZ, bZ); + expect(bZ + b0, bZ); + }); + + test('BorderDirectional.add', () { + const BorderSide side1 = const BorderSide(color: const Color(0x11111111)); + const BorderSide doubleSide1 = const BorderSide(color: const Color(0x11111111), width: 2.0); + const BorderSide side2 = const BorderSide(color: const Color(0x22222222)); + const BorderSide doubleSide2 = const BorderSide(color: const Color(0x22222222), width: 2.0); + + // adding tops and sides + expect(const Border(left: side1) + const BorderDirectional(top: side2), const Border(left: side1, top: side2)); + expect(const BorderDirectional(start: side1) + const Border(top: side2), const BorderDirectional(start: side1, top: side2)); + expect(const Border(top: side2) + const BorderDirectional(start: side1), const BorderDirectional(start: side1, top: side2)); + expect(const BorderDirectional(top: side2) + const Border(left: side1), const Border(left: side1, top: side2)); + + // adding incompatible tops and bottoms + expect((const Border(top: side1) + const BorderDirectional(top: side2)).toString(), contains(' + ')); + expect((const BorderDirectional(top: side2) + const Border(top: side1)).toString(), contains(' + ')); + expect((const Border(bottom: side1) + const BorderDirectional(bottom: side2)).toString(), contains(' + ')); + expect((const BorderDirectional(bottom: side2) + const Border(bottom: side1)).toString(), contains(' + ')); + + // adding compatible tops and bottoms + expect(const BorderDirectional(top: side1) + const Border(top: side1), const Border(top: doubleSide1)); + expect(const Border(top: side1) + const BorderDirectional(top: side1), const Border(top: doubleSide1)); + expect(const BorderDirectional(bottom: side1) + const Border(bottom: side1), const Border(bottom: doubleSide1)); + expect(const Border(bottom: side1) + const BorderDirectional(bottom: side1), const Border(bottom: doubleSide1)); + + const Border borderWithLeft = const Border(left: side1, top: side2, bottom: side2); + const Border borderWithRight = const Border(right: side1, top: side2, bottom: side2); + const Border borderWithoutSides = const Border(top: side2, bottom: side2); + const BorderDirectional borderDirectionalWithStart = const BorderDirectional(start: side1, top: side2, bottom: side2); + const BorderDirectional borderDirectionalWithEnd = const BorderDirectional(end: side1, top: side2, bottom: side2); + const BorderDirectional borderDirectionalWithoutSides = const BorderDirectional(top: side2, bottom: side2); + + expect((borderWithLeft + borderDirectionalWithStart).toString(), '$borderWithLeft + $borderDirectionalWithStart'); + expect((borderWithLeft + borderDirectionalWithEnd).toString(), '$borderWithLeft + $borderDirectionalWithEnd'); + expect((borderWithLeft + borderDirectionalWithoutSides).toString(), '${const Border(left: side1, top: doubleSide2, bottom: doubleSide2)}'); + expect((borderWithRight + borderDirectionalWithStart).toString(), '$borderWithRight + $borderDirectionalWithStart'); + expect((borderWithRight + borderDirectionalWithEnd).toString(), '$borderWithRight + $borderDirectionalWithEnd'); + expect((borderWithRight + borderDirectionalWithoutSides).toString(), '${const Border(right: side1, top: doubleSide2, bottom: doubleSide2)}'); + expect((borderWithoutSides + borderDirectionalWithStart).toString(), '${const BorderDirectional(start: side1, top: doubleSide2, bottom: doubleSide2)}'); + expect((borderWithoutSides + borderDirectionalWithEnd).toString(), '${const BorderDirectional(end: side1, top: doubleSide2, bottom: doubleSide2)}'); + expect((borderWithoutSides + borderDirectionalWithoutSides).toString(), '${const Border(top: doubleSide2, bottom: doubleSide2)}'); + + expect((borderDirectionalWithStart + borderWithLeft).toString(), '$borderDirectionalWithStart + $borderWithLeft'); + expect((borderDirectionalWithEnd + borderWithLeft).toString(), '$borderDirectionalWithEnd + $borderWithLeft'); + expect((borderDirectionalWithoutSides + borderWithLeft).toString(), '${const Border(left: side1, top: doubleSide2, bottom: doubleSide2)}'); + expect((borderDirectionalWithStart + borderWithRight).toString(), '$borderDirectionalWithStart + $borderWithRight'); + expect((borderDirectionalWithEnd + borderWithRight).toString(), '$borderDirectionalWithEnd + $borderWithRight'); + expect((borderDirectionalWithoutSides + borderWithRight).toString(), '${const Border(right: side1, top: doubleSide2, bottom: doubleSide2)}'); + expect((borderDirectionalWithStart + borderWithoutSides).toString(), '${const BorderDirectional(start: side1, top: doubleSide2, bottom: doubleSide2)}'); + expect((borderDirectionalWithEnd + borderWithoutSides).toString(), '${const BorderDirectional(end: side1, top: doubleSide2, bottom: doubleSide2)}'); + expect((borderDirectionalWithoutSides + borderWithoutSides).toString(), '${const Border(top: doubleSide2, bottom: doubleSide2)}'); + }); + + test('BorderDirectional.scale', () { + final BorderSide magenta3 = const BorderSide(color: const Color(0xFFFF00FF), width: 3.0); + final BorderSide magenta6 = const BorderSide(color: const Color(0xFFFF00FF), width: 6.0); + final BorderSide yellow2 = const BorderSide(color: const Color(0xFFFFFF00), width: 2.0); + final BorderSide yellowNone0 = const BorderSide(color: const Color(0xFFFFFF00), width: 0.0, style: BorderStyle.none); + final BorderDirectional b3 = new BorderDirectional(start: magenta3); + final BorderDirectional b6 = new BorderDirectional(start: magenta6); + expect(b3.scale(2.0), b6); + final BorderDirectional bY0 = new BorderDirectional(top: yellowNone0); + expect(bY0.scale(3.0), bY0); + final BorderDirectional bY2 = new BorderDirectional(top: yellow2); + expect(bY2.scale(0.0), bY0); + }); + + test('BorderDirectional.lerp', () { + final BorderDirectional directionalWithTop10 = const BorderDirectional(top: const BorderSide(width: 10.0)); + final BorderDirectional atMinus100 = const BorderDirectional(start: const BorderSide(width: 0.0), end: const BorderSide(width: 300.0)); + final BorderDirectional at0 = const BorderDirectional(start: const BorderSide(width: 100.0), end: const BorderSide(width: 200.0)); + final BorderDirectional at25 = const BorderDirectional(start: const BorderSide(width: 125.0), end: const BorderSide(width: 175.0)); + final BorderDirectional at75 = const BorderDirectional(start: const BorderSide(width: 175.0), end: const BorderSide(width: 125.0)); + final BorderDirectional at100 = const BorderDirectional(start: const BorderSide(width: 200.0), end: const BorderSide(width: 100.0)); + final BorderDirectional at200 = const BorderDirectional(start: const BorderSide(width: 300.0), end: const BorderSide(width: 0.0)); + + expect(BorderDirectional.lerp(null, null, -1.0), null); + expect(BorderDirectional.lerp(directionalWithTop10, null, -1.0), const BorderDirectional(top: const BorderSide(width: 20.0))); + expect(BorderDirectional.lerp(null, directionalWithTop10, -1.0), const BorderDirectional()); + expect(BorderDirectional.lerp(at0, at100, -1.0), atMinus100); + + expect(BorderDirectional.lerp(null, null, 0.0), null); + expect(BorderDirectional.lerp(directionalWithTop10, null, 0.0), const BorderDirectional(top: const BorderSide(width: 10.0))); + expect(BorderDirectional.lerp(null, directionalWithTop10, 0.0), const BorderDirectional()); + expect(BorderDirectional.lerp(at0, at100, 0.0), at0); + + expect(BorderDirectional.lerp(null, null, 0.25), null); + expect(BorderDirectional.lerp(directionalWithTop10, null, 0.25), const BorderDirectional(top: const BorderSide(width: 7.5))); + expect(BorderDirectional.lerp(null, directionalWithTop10, 0.25), const BorderDirectional(top: const BorderSide(width: 2.5))); + expect(BorderDirectional.lerp(at0, at100, 0.25), at25); + + expect(BorderDirectional.lerp(null, null, 0.75), null); + expect(BorderDirectional.lerp(directionalWithTop10, null, 0.75), const BorderDirectional(top: const BorderSide(width: 2.5))); + expect(BorderDirectional.lerp(null, directionalWithTop10, 0.75), const BorderDirectional(top: const BorderSide(width: 7.5))); + expect(BorderDirectional.lerp(at0, at100, 0.75), at75); + + expect(BorderDirectional.lerp(null, null, 1.0), null); + expect(BorderDirectional.lerp(directionalWithTop10, null, 1.0), const BorderDirectional()); + expect(BorderDirectional.lerp(null, directionalWithTop10, 1.0), const BorderDirectional(top: const BorderSide(width: 10.0))); + expect(BorderDirectional.lerp(at0, at100, 1.0), at100); + + expect(BorderDirectional.lerp(null, null, 2.0), null); + expect(BorderDirectional.lerp(directionalWithTop10, null, 2.0), const BorderDirectional()); + expect(BorderDirectional.lerp(null, directionalWithTop10, 2.0), const BorderDirectional(top: const BorderSide(width: 20.0))); + expect(BorderDirectional.lerp(at0, at100, 2.0), at200); + }); + + test('BorderDirectional.paint', () { + expect( + (Canvas canvas) { + const BorderDirectional(end: const BorderSide(width: 10.0, color: const Color(0xFF00FF00))) + .paint(canvas, new Rect.fromLTRB(10.0, 20.0, 30.0, 40.0), textDirection: TextDirection.rtl); + }, + paints + ..path( + includes: [const Offset(15.0, 30.0)], + excludes: [const Offset(25.0, 30.0)], + color: const Color(0xFF00FF00), + ) + ); + expect( + (Canvas canvas) { + const BorderDirectional(end: const BorderSide(width: 10.0, color: const Color(0xFF00FF00))) + .paint(canvas, new Rect.fromLTRB(10.0, 20.0, 30.0, 40.0), textDirection: TextDirection.ltr); + }, + paints + ..path( + includes: [const Offset(25.0, 30.0)], + excludes: [const Offset(15.0, 30.0)], + color: const Color(0xFF00FF00), + ) + ); + expect( + (Canvas canvas) { + const BorderDirectional(end: const BorderSide(width: 10.0, color: const Color(0xFF00FF00))) + .paint(canvas, new Rect.fromLTRB(10.0, 20.0, 30.0, 40.0)); + }, + paintsAssertion // no TextDirection + ); + }); + + test('BorderDirectional hashCode', () { + final BorderSide side = const BorderSide(width: 2.0); + expect(new BorderDirectional(top: side).hashCode, new BorderDirectional(top: side).hashCode); + expect(new BorderDirectional(top: side).hashCode, isNot(new BorderDirectional(bottom: side).hashCode)); + }); + + test('BoxDecoration.border takes a BorderDirectional', () { + const BoxDecoration decoration2 = const BoxDecoration( + border: const BorderDirectional(start: const BorderSide(width: 2.0)), + ); + const BoxDecoration decoration4 = const BoxDecoration( + border: const BorderDirectional(start: const BorderSide(width: 4.0)), + ); + const BoxDecoration decoration6 = const BoxDecoration( + border: const BorderDirectional(start: const BorderSide(width: 6.0)), + ); + final BoxPainter painter = decoration2.createBoxPainter(); + expect( + (Canvas canvas) { + painter.paint( + canvas, + const Offset(30.0, 0.0), + const ImageConfiguration(size: const Size(20.0, 20.0), textDirection: TextDirection.rtl), + ); + }, + paints + ..path( + includes: [const Offset(49.0, 10.0)], + excludes: [const Offset(31.0, 10.0)], + ) + ); + expect( + (Canvas canvas) { + painter.paint( + canvas, + const Offset(30.0, 0.0), + const ImageConfiguration(size: const Size(20.0, 20.0), textDirection: TextDirection.ltr), + ); + }, + paints + ..path( + includes: [const Offset(31.0, 10.0)], + excludes: [const Offset(49.0, 10.0)], + ) + ); + expect(decoration2.padding, const EdgeInsetsDirectional.fromSTEB(2.0, 0.0, 0.0, 0.0)); + expect(decoration2.scale(2.0), decoration4); + expect(BoxDecoration.lerp(decoration2, decoration6, 0.5), decoration4); + }); +} \ No newline at end of file diff --git a/packages/flutter/test/painting/border_side_test.dart b/packages/flutter/test/painting/border_side_test.dart index b85fc01b20..44cf9ccf22 100644 --- a/packages/flutter/test/painting/border_side_test.dart +++ b/packages/flutter/test/painting/border_side_test.dart @@ -128,10 +128,11 @@ void main() { final BorderSide side0 = const BorderSide(width: 0.0); final BorderSide side1 = const BorderSide(width: 1.0); final BorderSide side2 = const BorderSide(width: 2.0); - expect(BorderSide.lerp(side2, side1, 10.0), side0); - expect(BorderSide.lerp(side1, side2, -10.0), side0); + expect(BorderSide.lerp(side2, side1, 10.0), BorderSide.none); + expect(BorderSide.lerp(side1, side2, -10.0), BorderSide.none); expect(BorderSide.lerp(side0, side1, 2.0), side2); - expect(BorderSide.lerp(side1, side0, 2.0), side0); + expect(BorderSide.lerp(side1, side0, 2.0), BorderSide.none); + expect(BorderSide.lerp(side2, side1, 2.0), side0); }); test('BorderSide - toString', () { expect( diff --git a/packages/flutter/test/painting/border_test.dart b/packages/flutter/test/painting/border_test.dart index 5f77ee7f5e..3d8f95c473 100644 --- a/packages/flutter/test/painting/border_test.dart +++ b/packages/flutter/test/painting/border_test.dart @@ -6,6 +6,14 @@ import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + test('Border constructor', () { + final Null $null = null; + expect(() => new Border(left: $null), throwsAssertionError); + expect(() => new Border(top: $null), throwsAssertionError); + expect(() => new Border(right: $null), throwsAssertionError); + expect(() => new Border(bottom: $null), throwsAssertionError); + }); + test('Border.merge', () { final BorderSide magenta3 = const BorderSide(color: const Color(0xFFFF00FF), width: 3.0); final BorderSide magenta6 = const BorderSide(color: const Color(0xFFFF00FF), width: 6.0); @@ -94,4 +102,135 @@ void main() { final Border bY2 = new Border(top: yellow2); expect(bY2.scale(0.0), bY0); }); + + test('Border.dimensions', () { + expect( + const Border( + left: const BorderSide(width: 2.0), + top: const BorderSide(width: 3.0), + bottom: const BorderSide(width: 5.0), + right: const BorderSide(width: 7.0), + ).dimensions, + const EdgeInsets.fromLTRB(2.0, 3.0, 7.0, 5.0), + ); + }); + + test('Border.isUniform', () { + expect( + const Border( + left: const BorderSide(width: 3.0), + top: const BorderSide(width: 3.0), + right: const BorderSide(width: 3.0), + bottom: const BorderSide(width: 3.1), + ).isUniform, + false, + ); + expect( + const Border( + left: const BorderSide(width: 3.0), + top: const BorderSide(width: 3.0), + right: const BorderSide(width: 3.0), + bottom: const BorderSide(width: 3.0), + ).isUniform, + true, + ); + expect( + const Border( + left: const BorderSide(color: const Color(0xFFFFFFFE)), + top: const BorderSide(color: const Color(0xFFFFFFFF)), + right: const BorderSide(color: const Color(0xFFFFFFFF)), + bottom: const BorderSide(color: const Color(0xFFFFFFFF)), + ).isUniform, + false, + ); + expect( + const Border( + left: const BorderSide(color: const Color(0xFFFFFFFF)), + top: const BorderSide(color: const Color(0xFFFFFFFF)), + right: const BorderSide(color: const Color(0xFFFFFFFF)), + bottom: const BorderSide(color: const Color(0xFFFFFFFF)), + ).isUniform, + true, + ); + expect( + const Border( + left: const BorderSide(style: BorderStyle.none), + top: const BorderSide(style: BorderStyle.none), + right: const BorderSide(style: BorderStyle.none), + bottom: const BorderSide(style: BorderStyle.solid, width: 0.0), + ).isUniform, + false, + ); + expect( + const Border( + left: const BorderSide(style: BorderStyle.none), + top: const BorderSide(style: BorderStyle.none), + right: const BorderSide(style: BorderStyle.none), + bottom: const BorderSide(style: BorderStyle.solid, width: 0.0), + ).isUniform, + false, + ); + expect( + const Border( + left: const BorderSide(style: BorderStyle.none), + top: const BorderSide(style: BorderStyle.none), + right: const BorderSide(style: BorderStyle.none), + bottom: BorderSide.none, + ).isUniform, + false, + ); + expect( + const Border( + left: const BorderSide(style: BorderStyle.none, width: 0.0), + top: const BorderSide(style: BorderStyle.none, width: 0.0), + right: const BorderSide(style: BorderStyle.none, width: 0.0), + bottom: BorderSide.none, + ).isUniform, + true, + ); + expect( + const Border().isUniform, + true, + ); + }); + + test('Border.lerp', () { + final Border visualWithTop10 = const Border(top: const BorderSide(width: 10.0)); + final Border atMinus100 = const Border(left: const BorderSide(width: 0.0), right: const BorderSide(width: 300.0)); + final Border at0 = const Border(left: const BorderSide(width: 100.0), right: const BorderSide(width: 200.0)); + final Border at25 = const Border(left: const BorderSide(width: 125.0), right: const BorderSide(width: 175.0)); + final Border at75 = const Border(left: const BorderSide(width: 175.0), right: const BorderSide(width: 125.0)); + final Border at100 = const Border(left: const BorderSide(width: 200.0), right: const BorderSide(width: 100.0)); + final Border at200 = const Border(left: const BorderSide(width: 300.0), right: const BorderSide(width: 0.0)); + + expect(Border.lerp(null, null, -1.0), null); + expect(Border.lerp(visualWithTop10, null, -1.0), const Border(top: const BorderSide(width: 20.0))); + expect(Border.lerp(null, visualWithTop10, -1.0), const Border()); + expect(Border.lerp(at0, at100, -1.0), atMinus100); + + expect(Border.lerp(null, null, 0.0), null); + expect(Border.lerp(visualWithTop10, null, 0.0), const Border(top: const BorderSide(width: 10.0))); + expect(Border.lerp(null, visualWithTop10, 0.0), const Border()); + expect(Border.lerp(at0, at100, 0.0), at0); + + expect(Border.lerp(null, null, 0.25), null); + expect(Border.lerp(visualWithTop10, null, 0.25), const Border(top: const BorderSide(width: 7.5))); + expect(Border.lerp(null, visualWithTop10, 0.25), const Border(top: const BorderSide(width: 2.5))); + expect(Border.lerp(at0, at100, 0.25), at25); + + expect(Border.lerp(null, null, 0.75), null); + expect(Border.lerp(visualWithTop10, null, 0.75), const Border(top: const BorderSide(width: 2.5))); + expect(Border.lerp(null, visualWithTop10, 0.75), const Border(top: const BorderSide(width: 7.5))); + expect(Border.lerp(at0, at100, 0.75), at75); + + expect(Border.lerp(null, null, 1.0), null); + expect(Border.lerp(visualWithTop10, null, 1.0), const Border()); + expect(Border.lerp(null, visualWithTop10, 1.0), const Border(top: const BorderSide(width: 10.0))); + expect(Border.lerp(at0, at100, 1.0), at100); + + expect(Border.lerp(null, null, 2.0), null); + expect(Border.lerp(visualWithTop10, null, 2.0), const Border()); + expect(Border.lerp(null, visualWithTop10, 2.0), const Border(top: const BorderSide(width: 20.0))); + expect(Border.lerp(at0, at100, 2.0), at200); + }); } \ No newline at end of file diff --git a/packages/flutter/test/painting/shape_border_test.dart b/packages/flutter/test/painting/shape_border_test.dart index 172a9d1df3..1e05595974 100644 --- a/packages/flutter/test/painting/shape_border_test.dart +++ b/packages/flutter/test/painting/shape_border_test.dart @@ -70,4 +70,69 @@ void main() { ..rect(rect: rect.deflate(2.5), color: b1.top.color) ); }); + + test('Compound borders', () { + final BorderSide side1 = const BorderSide(color: const Color(0xFF00FF00)); + final BorderSide side2 = const BorderSide(color: const Color(0xFF0000FF)); + final BorderDirectional b1 = new BorderDirectional(top: side1, start: side1, end: side1, bottom: side1); + final BorderDirectional b2 = new BorderDirectional(top: side2, start: side2, end: side2, bottom: side2); + expect( + (b1 + b2).toString(), + 'BorderDirectional(top: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid)) + ' + 'BorderDirectional(top: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid))', + ); + expect( + (b1 + (b2 + b2)).toString(), + 'BorderDirectional(top: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid)) + ' + 'BorderDirectional(top: BorderSide(Color(0xff0000ff), 2.0, BorderStyle.solid), start: BorderSide(Color(0xff0000ff), 2.0, BorderStyle.solid), end: BorderSide(Color(0xff0000ff), 2.0, BorderStyle.solid), bottom: BorderSide(Color(0xff0000ff), 2.0, BorderStyle.solid))', + ); + expect( + ((b1 + b2) + b2).toString(), + 'BorderDirectional(top: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid)) + ' + 'BorderDirectional(top: BorderSide(Color(0xff0000ff), 2.0, BorderStyle.solid), start: BorderSide(Color(0xff0000ff), 2.0, BorderStyle.solid), end: BorderSide(Color(0xff0000ff), 2.0, BorderStyle.solid), bottom: BorderSide(Color(0xff0000ff), 2.0, BorderStyle.solid))', + ); + expect((b1 + b2) + b2, b1 + (b2 + b2)); + expect( + (b1 + b2).scale(3.0).toString(), + 'BorderDirectional(top: BorderSide(Color(0xff00ff00), 3.0, BorderStyle.solid), start: BorderSide(Color(0xff00ff00), 3.0, BorderStyle.solid), end: BorderSide(Color(0xff00ff00), 3.0, BorderStyle.solid), bottom: BorderSide(Color(0xff00ff00), 3.0, BorderStyle.solid)) + ' + 'BorderDirectional(top: BorderSide(Color(0xff0000ff), 3.0, BorderStyle.solid), start: BorderSide(Color(0xff0000ff), 3.0, BorderStyle.solid), end: BorderSide(Color(0xff0000ff), 3.0, BorderStyle.solid), bottom: BorderSide(Color(0xff0000ff), 3.0, BorderStyle.solid))', + ); + expect( + (b1 + b2).scale(0.0).toString(), + 'BorderDirectional(top: BorderSide(Color(0xff00ff00), 0.0, BorderStyle.none), start: BorderSide(Color(0xff00ff00), 0.0, BorderStyle.none), end: BorderSide(Color(0xff00ff00), 0.0, BorderStyle.none), bottom: BorderSide(Color(0xff00ff00), 0.0, BorderStyle.none)) + ' + 'BorderDirectional(top: BorderSide(Color(0xff0000ff), 0.0, BorderStyle.none), start: BorderSide(Color(0xff0000ff), 0.0, BorderStyle.none), end: BorderSide(Color(0xff0000ff), 0.0, BorderStyle.none), bottom: BorderSide(Color(0xff0000ff), 0.0, BorderStyle.none))', + ); + expect( + ShapeBorder.lerp(b2 + b1, b1 + b2, 0.0).toString(), + 'BorderDirectional(top: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid)) + ' + 'BorderDirectional(top: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid))', + ); + expect( + ShapeBorder.lerp(b2 + b1, b1 + b2, 0.25).toString(), + 'BorderDirectional(top: BorderSide(Color(0xff003fbf), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff003fbf), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff003fbf), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff003fbf), 1.0, BorderStyle.solid)) + ' + 'BorderDirectional(top: BorderSide(Color(0xff00bf3f), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff00bf3f), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff00bf3f), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff00bf3f), 1.0, BorderStyle.solid))', + ); + expect( + ShapeBorder.lerp(b2 + b1, b1 + b2, 0.5).toString(), + 'BorderDirectional(top: BorderSide(Color(0xff007f7f), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff007f7f), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff007f7f), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff007f7f), 1.0, BorderStyle.solid)) + ' + 'BorderDirectional(top: BorderSide(Color(0xff007f7f), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff007f7f), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff007f7f), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff007f7f), 1.0, BorderStyle.solid))', + ); + expect( + ShapeBorder.lerp(b2 + b1, b1 + b2, 1.0).toString(), + 'BorderDirectional(top: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff00ff00), 1.0, BorderStyle.solid)) + ' + 'BorderDirectional(top: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid), start: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid), end: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid), bottom: BorderSide(Color(0xff0000ff), 1.0, BorderStyle.solid))' + ); + expect((b1 + b2).dimensions, const EdgeInsetsDirectional.fromSTEB(2.0, 2.0, 2.0, 2.0)); + final Rect rect = new Rect.fromLTRB(11.0, 15.0, 299.0, 175.0); + expect((Canvas canvas) => (b1 + b2).paint(canvas, rect, textDirection: TextDirection.rtl), paints + ..rect(rect: rect.deflate(0.5), color: b2.top.color) + ..rect(rect: rect.deflate(1.5), color: b1.top.color) + ); + expect((b1 + b2 + b1).dimensions, const EdgeInsetsDirectional.fromSTEB(3.0, 3.0, 3.0, 3.0)); + expect((Canvas canvas) => (b1 + b2 + b1).paint(canvas, rect, textDirection: TextDirection.rtl), paints + ..rect(rect: rect.deflate(0.5), color: b1.top.color) + ..rect(rect: rect.deflate(1.5), color: b2.top.color) + ..rect(rect: rect.deflate(2.5), color: b1.top.color) + ); + }); } diff --git a/packages/flutter/test/rendering/mock_canvas.dart b/packages/flutter/test/rendering/mock_canvas.dart index 20aa7950cd..3f37a5ce0a 100644 --- a/packages/flutter/test/rendering/mock_canvas.dart +++ b/packages/flutter/test/rendering/mock_canvas.dart @@ -38,11 +38,16 @@ import 'recording_canvas.dart'; /// See [PaintPattern] for a discussion of the semantics of paint patterns. /// /// To match something which paints nothing, see [paintsNothing]. +/// +/// To match something which asserts instead of painting, see [paintsAssertion]. PaintPattern get paints => new _TestRecordingCanvasPatternMatcher(); /// Matches objects or functions that paint an empty display list. Matcher get paintsNothing => new _TestRecordingCanvasPaintsNothingMatcher(); +/// Matches objects or functions that assert when they try to paint. +Matcher get paintsAssertion => new _TestRecordingCanvasPaintsAssertionMatcher(); + /// Signature for [PaintPattern.something] predicate argument. /// /// Used by the [paints] matcher. @@ -218,8 +223,10 @@ abstract class PaintPattern { /// are compared to the actual [Canvas.drawPath] call's `paint` argument, and /// any mismatches result in failure. /// - /// There is currently no way to check the actual path itself. - // See https://github.com/flutter/flutter/issues/93 which tracks that issue. + /// To introspect the Path object (as it stands after the painting has + /// completed), the `includes` and `excludes` arguments can be provided to + /// specify points that should be considered inside or outside the path + /// (respectively). /// /// If no call to [Canvas.drawPath] was made, then this results in failure. /// @@ -231,7 +238,7 @@ abstract class PaintPattern { /// painting has completed, not at the time of the call. If the same [Paint] /// object is reused multiple times, then this may not match the actual /// arguments as they were seen by the method. - void path({ Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }); + void path({ Iterable includes, Iterable excludes, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }); /// Indicates that a line is expected next. /// @@ -327,6 +334,29 @@ class _MismatchedCall { final RecordedInvocation call; } +bool _evaluatePainter(Object object, Canvas canvas, PaintingContext context) { + if (object is _ContextPainterFunction) { + final _ContextPainterFunction function = object; + function(context, Offset.zero); + } else if (object is _CanvasPainterFunction) { + final _CanvasPainterFunction function = object; + function(canvas); + } else { + if (object is Finder) { + TestAsyncUtils.guardSync(); + final Finder finder = object; + object = finder.evaluate().single.renderObject; + } + if (object is RenderObject) { + final RenderObject renderObject = object; + renderObject.paint(context, Offset.zero); + } else { + return false; + } + } + return true; +} + abstract class _TestRecordingCanvasMatcher extends Matcher { @override bool matches(Object object, Map matchState) { @@ -336,25 +366,9 @@ abstract class _TestRecordingCanvasMatcher extends Matcher { String prefixMessage = 'unexpectedly failed.'; bool result = false; try { - if (object is _ContextPainterFunction) { - final _ContextPainterFunction function = object; - function(context, Offset.zero); - } else if (object is _CanvasPainterFunction) { - final _CanvasPainterFunction function = object; - function(canvas); - } else { - if (object is Finder) { - TestAsyncUtils.guardSync(); - final Finder finder = object; - object = finder.evaluate().single.renderObject; - } - if (object is RenderObject) { - final RenderObject renderObject = object; - renderObject.paint(context, Offset.zero); - } else { - matchState[this] = 'was not one of the supported objects for the "paints" matcher.'; - return false; - } + if (!_evaluatePainter(object, canvas, context)) { + matchState[this] = 'was not one of the supported objects for the "paints" matcher.'; + return false; } result = _evaluatePredicates(canvas.invocations, description); if (!result) @@ -407,6 +421,55 @@ class _TestRecordingCanvasPaintsNothingMatcher extends _TestRecordingCanvasMatch } } +class _TestRecordingCanvasPaintsAssertionMatcher extends Matcher { + @override + bool matches(Object object, Map matchState) { + final TestRecordingCanvas canvas = new TestRecordingCanvas(); + final TestRecordingPaintingContext context = new TestRecordingPaintingContext(canvas); + final StringBuffer description = new StringBuffer(); + String prefixMessage = 'unexpectedly failed.'; + bool result = false; + try { + if (!_evaluatePainter(object, canvas, context)) { + matchState[this] = 'was not one of the supported objects for the "paints" matcher.'; + return false; + } + prefixMessage = 'did not assert.'; + } on AssertionError { + result = true; + } catch (error, stack) { + prefixMessage = 'threw the following exception:'; + description.writeln(error.toString()); + description.write(stack.toString()); + result = false; + } + if (!result) { + if (canvas.invocations.isNotEmpty) { + description.write('The complete display list was:'); + for (RecordedInvocation call in canvas.invocations) + description.write('\n * $call'); + } + matchState[this] = '$prefixMessage\n$description'; + } + return result; + } + + @override + Description describe(Description description) { + return description.add('An object or closure that asserts when it tries to paint.'); + } + + @override + Description describeMismatch( + dynamic item, + Description description, + Map matchState, + bool verbose, + ) { + return description.add(matchState[this]); + } +} + class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher implements PaintPattern { final List<_PaintPredicate> _predicates = <_PaintPredicate>[]; @@ -471,8 +534,8 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp } @override - void path({ Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) { - _predicates.add(new _PathPaintPredicate(color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style)); + void path({ Iterable includes, Iterable excludes, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) { + _predicates.add(new _PathPaintPredicate(includes: includes, excludes: excludes, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style)); } @override @@ -805,9 +868,42 @@ class _CirclePaintPredicate extends _DrawCommandPaintPredicate { } class _PathPaintPredicate extends _DrawCommandPaintPredicate { - _PathPaintPredicate({ Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) : super( + _PathPaintPredicate({ this.includes, this.excludes, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) : super( #drawPath, 'a path', 2, 1, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style ); + + final Iterable includes; + final Iterable excludes; + + @override + void verifyArguments(List arguments) { + super.verifyArguments(arguments); + final Path pathArgument = arguments[0]; + if (includes != null) { + for (Offset offset in includes) { + if (!pathArgument.contains(offset)) + throw 'It called $methodName with a path that unexpectedly did not contain $offset.'; + } + } + if (excludes != null) { + for (Offset offset in excludes) { + if (pathArgument.contains(offset)) + throw 'It called $methodName with a path that unexpectedly contained $offset.'; + } + } + } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + if (includes != null && excludes != null) { + description.add('that contains $includes and does not contain $excludes'); + } else if (includes != null) { + description.add('that contains $includes'); + } else if (excludes != null) { + description.add('that does not contain $excludes'); + } + } } // TODO(ianh): add arguments to test the points, length, angle, that kind of thing diff --git a/packages/flutter/test/widgets/transitions_test.dart b/packages/flutter/test/widgets/transitions_test.dart index 875d562d36..e270023080 100644 --- a/packages/flutter/test/widgets/transitions_test.dart +++ b/packages/flutter/test/widgets/transitions_test.dart @@ -77,9 +77,11 @@ void main() { actualDecoration = actualBox.decoration; expect(actualDecoration.color, const Color(0xFF7F7F7F)); - expect(actualDecoration.border.left.width, 2.5); - expect(actualDecoration.border.left.style, BorderStyle.solid); - expect(actualDecoration.border.left.color, const Color(0xFF101010)); + expect(actualDecoration.border, const isInstanceOf()); + final Border border = actualDecoration.border; + expect(border.left.width, 2.5); + expect(border.left.style, BorderStyle.solid); + expect(border.left.color, const Color(0xFF101010)); expect(actualDecoration.borderRadius, new BorderRadius.circular(5.0)); expect(actualDecoration.shape, BoxShape.rectangle); expect(actualDecoration.boxShadow[0].blurRadius, 5.0); @@ -131,9 +133,11 @@ void main() { // Same as the test above but the values should be much closer to the // tween's end values given the easeOut curve. expect(actualDecoration.color, const Color(0xFF505050)); - expect(actualDecoration.border.left.width, closeTo(1.9, 0.1)); - expect(actualDecoration.border.left.style, BorderStyle.solid); - expect(actualDecoration.border.left.color, const Color(0xFF151515)); + expect(actualDecoration.border, const isInstanceOf()); + final Border border = actualDecoration.border; + expect(border.left.width, closeTo(1.9, 0.1)); + expect(border.left.style, BorderStyle.solid); + expect(border.left.color, const Color(0xFF151515)); expect(actualDecoration.borderRadius.topLeft.x, closeTo(6.8, 0.1)); expect(actualDecoration.shape, BoxShape.rectangle); expect(actualDecoration.boxShadow[0].blurRadius, closeTo(3.1, 0.1));