• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6import 'dart:ui' as ui show lerpDouble;
7
8import 'package:flutter/foundation.dart';
9
10import 'basic_types.dart';
11import 'edge_insets.dart';
12
13/// The style of line to draw for a [BorderSide] in a [Border].
14enum BorderStyle {
15  /// Skip the border.
16  none,
17
18  /// Draw the border as a solid line.
19  solid,
20
21  // if you add more, think about how they will lerp
22}
23
24/// A side of a border of a box.
25///
26/// A [Border] consists of four [BorderSide] objects: [Border.top],
27/// [Border.left], [Border.right], and [Border.bottom].
28///
29/// Note that setting [BorderSide.width] to 0.0 will result in hairline
30/// rendering. A more involved explanation is present in [BorderSide.width].
31///
32/// {@tool sample}
33///
34/// This sample shows how [BorderSide] objects can be used in a [Container], via
35/// a [BoxDecoration] and a [Border], to decorate some [Text]. In this example,
36/// the text has a thick bar above it that is light blue, and a thick bar below
37/// it that is a darker shade of blue.
38///
39/// ```dart
40/// Container(
41///   padding: EdgeInsets.all(8.0),
42///   decoration: BoxDecoration(
43///     border: Border(
44///       top: BorderSide(width: 16.0, color: Colors.lightBlue.shade50),
45///       bottom: BorderSide(width: 16.0, color: Colors.lightBlue.shade900),
46///     ),
47///   ),
48///   child: Text('Flutter in the sky', textAlign: TextAlign.center),
49/// )
50/// ```
51/// {@end-tool}
52///
53/// See also:
54///
55///  * [Border], which uses [BorderSide] objects to represent its sides.
56///  * [BoxDecoration], which optionally takes a [Border] object.
57///  * [TableBorder], which is similar to [Border] but has two more sides
58///    ([TableBorder.horizontalInside] and [TableBorder.verticalInside]), both
59///    of which are also [BorderSide] objects.
60@immutable
61class BorderSide {
62  /// Creates the side of a border.
63  ///
64  /// By default, the border is 1.0 logical pixels wide and solid black.
65  const BorderSide({
66    this.color = const Color(0xFF000000),
67    this.width = 1.0,
68    this.style = BorderStyle.solid,
69  }) : assert(color != null),
70       assert(width != null),
71       assert(width >= 0.0),
72       assert(style != null);
73
74  /// Creates a [BorderSide] that represents the addition of the two given
75  /// [BorderSide]s.
76  ///
77  /// It is only valid to call this if [canMerge] returns true for the two
78  /// sides.
79  ///
80  /// If one of the sides is zero-width with [BorderStyle.none], then the other
81  /// side is return as-is. If both of the sides are zero-width with
82  /// [BorderStyle.none], then [BorderSide.zero] is returned.
83  ///
84  /// The arguments must not be null.
85  static BorderSide merge(BorderSide a, BorderSide b) {
86    assert(a != null);
87    assert(b != null);
88    assert(canMerge(a, b));
89    final bool aIsNone = a.style == BorderStyle.none && a.width == 0.0;
90    final bool bIsNone = b.style == BorderStyle.none && b.width == 0.0;
91    if (aIsNone && bIsNone)
92      return BorderSide.none;
93    if (aIsNone)
94      return b;
95    if (bIsNone)
96      return a;
97    assert(a.color == b.color);
98    assert(a.style == b.style);
99    return BorderSide(
100      color: a.color, // == b.color
101      width: a.width + b.width,
102      style: a.style, // == b.style
103    );
104  }
105
106  /// The color of this side of the border.
107  final Color color;
108
109  /// The width of this side of the border, in logical pixels.
110  ///
111  /// Setting width to 0.0 will result in a hairline border. This means that
112  /// the border will have the width of one physical pixel. Also, hairline
113  /// rendering takes shortcuts when the path overlaps a pixel more than once.
114  /// This means that it will render faster than otherwise, but it might
115  /// double-hit pixels, giving it a slightly darker/lighter result.
116  ///
117  /// To omit the border entirely, set the [style] to [BorderStyle.none].
118  final double width;
119
120  /// The style of this side of the border.
121  ///
122  /// To omit a side, set [style] to [BorderStyle.none]. This skips
123  /// painting the border, but the border still has a [width].
124  final BorderStyle style;
125
126  /// A hairline black border that is not rendered.
127  static const BorderSide none = BorderSide(width: 0.0, style: BorderStyle.none);
128
129  /// Creates a copy of this border but with the given fields replaced with the new values.
130  BorderSide copyWith({
131    Color color,
132    double width,
133    BorderStyle style,
134  }) {
135    assert(width == null || width >= 0.0);
136    return BorderSide(
137      color: color ?? this.color,
138      width: width ?? this.width,
139      style: style ?? this.style,
140    );
141  }
142
143  /// Creates a copy of this border side description but with the width scaled
144  /// by the factor `t`.
145  ///
146  /// The `t` argument represents the multiplicand, or the position on the
147  /// timeline for an interpolation from nothing to `this`, with 0.0 meaning
148  /// that the object returned should be the nil variant of this object, 1.0
149  /// meaning that no change should be applied, returning `this` (or something
150  /// equivalent to `this`), and other values meaning that the object should be
151  /// multiplied by `t`. Negative values are treated like zero.
152  ///
153  /// Since a zero width is normally painted as a hairline width rather than no
154  /// border at all, the zero factor is special-cased to instead change the
155  /// style to [BorderStyle.none].
156  ///
157  /// Values for `t` are usually obtained from an [Animation<double>], such as
158  /// an [AnimationController].
159  BorderSide scale(double t) {
160    assert(t != null);
161    return BorderSide(
162      color: color,
163      width: math.max(0.0, width * t),
164      style: t <= 0.0 ? BorderStyle.none : style,
165    );
166  }
167
168  /// Create a [Paint] object that, if used to stroke a line, will draw the line
169  /// in this border's style.
170  ///
171  /// Not all borders use this method to paint their border sides. For example,
172  /// non-uniform rectangular [Border]s have beveled edges and so paint their
173  /// border sides as filled shapes rather than using a stroke.
174  Paint toPaint() {
175    switch (style) {
176      case BorderStyle.solid:
177        return Paint()
178          ..color = color
179          ..strokeWidth = width
180          ..style = PaintingStyle.stroke;
181      case BorderStyle.none:
182        return Paint()
183          ..color = const Color(0x00000000)
184          ..strokeWidth = 0.0
185          ..style = PaintingStyle.stroke;
186    }
187    return null;
188  }
189
190  /// Whether the two given [BorderSide]s can be merged using [new
191  /// BorderSide.merge].
192  ///
193  /// Two sides can be merged if one or both are zero-width with
194  /// [BorderStyle.none], or if they both have the same color and style.
195  ///
196  /// The arguments must not be null.
197  static bool canMerge(BorderSide a, BorderSide b) {
198    assert(a != null);
199    assert(b != null);
200    if ((a.style == BorderStyle.none && a.width == 0.0) ||
201        (b.style == BorderStyle.none && b.width == 0.0))
202      return true;
203    return a.style == b.style
204        && a.color == b.color;
205  }
206
207  /// Linearly interpolate between two border sides.
208  ///
209  /// The arguments must not be null.
210  ///
211  /// {@macro dart.ui.shadow.lerp}
212  static BorderSide lerp(BorderSide a, BorderSide b, double t) {
213    assert(a != null);
214    assert(b != null);
215    assert(t != null);
216    if (t == 0.0)
217      return a;
218    if (t == 1.0)
219      return b;
220    final double width = ui.lerpDouble(a.width, b.width, t);
221    if (width < 0.0)
222      return BorderSide.none;
223    if (a.style == b.style) {
224      return BorderSide(
225        color: Color.lerp(a.color, b.color, t),
226        width: width,
227        style: a.style, // == b.style
228      );
229    }
230    Color colorA, colorB;
231    switch (a.style) {
232      case BorderStyle.solid:
233        colorA = a.color;
234        break;
235      case BorderStyle.none:
236        colorA = a.color.withAlpha(0x00);
237        break;
238    }
239    switch (b.style) {
240      case BorderStyle.solid:
241        colorB = b.color;
242        break;
243      case BorderStyle.none:
244        colorB = b.color.withAlpha(0x00);
245        break;
246    }
247    return BorderSide(
248      color: Color.lerp(colorA, colorB, t),
249      width: width,
250      style: BorderStyle.solid,
251    );
252  }
253
254  @override
255  bool operator ==(dynamic other) {
256    if (identical(this, other))
257      return true;
258    if (runtimeType != other.runtimeType)
259      return false;
260    final BorderSide typedOther = other;
261    return color == typedOther.color &&
262           width == typedOther.width &&
263           style == typedOther.style;
264  }
265
266  @override
267  int get hashCode => hashValues(color, width, style);
268
269  @override
270  String toString() => '$runtimeType($color, ${width.toStringAsFixed(1)}, $style)';
271}
272
273/// Base class for shape outlines.
274///
275/// This class handles how to add multiple borders together. Subclasses define
276/// various shapes, like circles ([CircleBorder]), rounded rectangles
277/// ([RoundedRectangleBorder]), continuous rectangles
278/// ([ContinuousRectangleBorder]), or beveled rectangles
279/// ([BeveledRectangleBorder]).
280///
281/// See also:
282///
283///  * [ShapeDecoration], which can be used with [DecoratedBox] to show a shape.
284///  * [Material] (and many other widgets in the Material library), which takes
285///    a [ShapeBorder] to define its shape.
286///  * [NotchedShape], which describes a shape with a hole in it.
287@immutable
288abstract class ShapeBorder {
289  /// Abstract const constructor. This constructor enables subclasses to provide
290  /// const constructors so that they can be used in const expressions.
291  const ShapeBorder();
292
293  /// The widths of the sides of this border represented as an [EdgeInsets].
294  ///
295  /// Specifically, this is the amount by which a rectangle should be inset so
296  /// as to avoid painting over any important part of the border. It is the
297  /// amount by which additional borders will be inset before they are drawn.
298  ///
299  /// This can be used, for example, with a [Padding] widget to inset a box by
300  /// the size of these borders.
301  ///
302  /// Shapes that have a fixed ratio regardless of the area on which they are
303  /// painted, or that change their rendering based on the size they are given
304  /// when painting (for instance [CircleBorder]), will not return valid
305  /// [dimensions] information because they cannot know their eventual size when
306  /// computing their [dimensions].
307  EdgeInsetsGeometry get dimensions;
308
309  /// Attempts to create a new object that represents the amalgamation of `this`
310  /// border and the `other` border.
311  ///
312  /// If the type of the other border isn't known, or the given instance cannot
313  /// be reasonably added to this instance, then this should return null.
314  ///
315  /// This method is used by the [operator +] implementation.
316  ///
317  /// The `reversed` argument is true if this object was the right operand of
318  /// the `+` operator, and false if it was the left operand.
319  @protected
320  ShapeBorder add(ShapeBorder other, { bool reversed = false }) => null;
321
322  /// Creates a new border consisting of the two borders on either side of the
323  /// operator.
324  ///
325  /// If the borders belong to classes that know how to add themselves, then
326  /// this results in a new border that represents the intelligent addition of
327  /// those two borders (see [add]). Otherwise, an object is returned that
328  /// merely paints the two borders sequentially, with the left hand operand on
329  /// the inside and the right hand operand on the outside.
330  ShapeBorder operator +(ShapeBorder other) {
331    return add(other) ?? other.add(this, reversed: true) ?? _CompoundBorder(<ShapeBorder>[other, this]);
332  }
333
334  /// Creates a copy of this border, scaled by the factor `t`.
335  ///
336  /// Typically this means scaling the width of the border's side, but it can
337  /// also include scaling other artifacts of the border, e.g. the border radius
338  /// of a [RoundedRectangleBorder].
339  ///
340  /// The `t` argument represents the multiplicand, or the position on the
341  /// timeline for an interpolation from nothing to `this`, with 0.0 meaning
342  /// that the object returned should be the nil variant of this object, 1.0
343  /// meaning that no change should be applied, returning `this` (or something
344  /// equivalent to `this`), and other values meaning that the object should be
345  /// multiplied by `t`. Negative values are allowed but may be meaningless
346  /// (they correspond to extrapolating the interpolation from this object to
347  /// nothing, and going beyond nothing)
348  ///
349  /// Values for `t` are usually obtained from an [Animation<double>], such as
350  /// an [AnimationController].
351  ///
352  /// See also:
353  ///
354  ///  * [BorderSide.scale], which most [ShapeBorder] subclasses defer to for
355  ///    the actual computation.
356  ShapeBorder scale(double t);
357
358  /// Linearly interpolates from another [ShapeBorder] (possibly of another
359  /// class) to `this`.
360  ///
361  /// When implementing this method in subclasses, return null if this class
362  /// cannot interpolate from `a`. In that case, [lerp] will try `a`'s [lerpTo]
363  /// method instead. If `a` is null, this must not return null.
364  ///
365  /// The base class implementation handles the case of `a` being null by
366  /// deferring to [scale].
367  ///
368  /// The `t` argument represents position on the timeline, with 0.0 meaning
369  /// that the interpolation has not started, returning `a` (or something
370  /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
371  /// returning `this` (or something equivalent to `this`), and values in
372  /// between meaning that the interpolation is at the relevant point on the
373  /// timeline between `a` and `this`. The interpolation can be extrapolated
374  /// beyond 0.0 and 1.0, so negative values and values greater than 1.0 are
375  /// valid (and can easily be generated by curves such as
376  /// [Curves.elasticInOut]).
377  ///
378  /// Values for `t` are usually obtained from an [Animation<double>], such as
379  /// an [AnimationController].
380  ///
381  /// Instead of calling this directly, use [ShapeBorder.lerp].
382  @protected
383  ShapeBorder lerpFrom(ShapeBorder a, double t) {
384    if (a == null)
385      return scale(t);
386    return null;
387  }
388
389  /// Linearly interpolates from `this` to another [ShapeBorder] (possibly of
390  /// another class).
391  ///
392  /// This is called if `b`'s [lerpTo] did not know how to handle this class.
393  ///
394  /// When implementing this method in subclasses, return null if this class
395  /// cannot interpolate from `b`. In that case, [lerp] will apply a default
396  /// behavior instead. If `b` is null, this must not return null.
397  ///
398  /// The base class implementation handles the case of `b` being null by
399  /// deferring to [scale].
400  ///
401  /// The `t` argument represents position on the timeline, with 0.0 meaning
402  /// that the interpolation has not started, returning `this` (or something
403  /// equivalent to `this`), 1.0 meaning that the interpolation has finished,
404  /// returning `b` (or something equivalent to `b`), and values in between
405  /// meaning that the interpolation is at the relevant point on the timeline
406  /// between `this` and `b`. The interpolation can be extrapolated beyond 0.0
407  /// and 1.0, so negative values and values greater than 1.0 are valid (and can
408  /// easily be generated by curves such as [Curves.elasticInOut]).
409  ///
410  /// Values for `t` are usually obtained from an [Animation<double>], such as
411  /// an [AnimationController].
412  ///
413  /// Instead of calling this directly, use [ShapeBorder.lerp].
414  @protected
415  ShapeBorder lerpTo(ShapeBorder b, double t) {
416    if (b == null)
417      return scale(1.0 - t);
418    return null;
419  }
420
421  /// Linearly interpolates between two [ShapeBorder]s.
422  ///
423  /// This defers to `b`'s [lerpTo] function if `b` is not null. If `b` is
424  /// null or if its [lerpTo] returns null, it uses `a`'s [lerpFrom]
425  /// function instead. If both return null, it returns `a` before `t=0.5`
426  /// and `b` after `t=0.5`.
427  ///
428  /// {@macro dart.ui.shadow.lerp}
429  static ShapeBorder lerp(ShapeBorder a, ShapeBorder b, double t) {
430    assert(t != null);
431    ShapeBorder result;
432    if (b != null)
433      result = b.lerpFrom(a, t);
434    if (result == null && a != null)
435      result = a.lerpTo(b, t);
436    return result ?? (t < 0.5 ? a : b);
437  }
438
439  /// Create a [Path] that describes the outer edge of the border.
440  ///
441  /// This path must not cross the path given by [getInnerPath] for the same
442  /// [Rect].
443  ///
444  /// To obtain a [Path] that describes the area of the border itself, set the
445  /// [Path.fillType] of the returned object to [PathFillType.evenOdd], and add
446  /// to this object the path returned from [getInnerPath] (using
447  /// [Path.addPath]).
448  ///
449  /// The `textDirection` argument must be provided non-null if the border
450  /// has a text direction dependency (for example if it is expressed in terms
451  /// of "start" and "end" instead of "left" and "right"). It may be null if
452  /// the border will not need the text direction to paint itself.
453  ///
454  /// See also:
455  ///
456  ///  * [getInnerPath], which creates the path for the inner edge.
457  ///  * [Path.contains], which can tell if an [Offset] is within a [Path].
458  Path getOuterPath(Rect rect, { TextDirection textDirection });
459
460  /// Create a [Path] that describes the inner edge of the border.
461  ///
462  /// This path must not cross the path given by [getOuterPath] for the same
463  /// [Rect].
464  ///
465  /// To obtain a [Path] that describes the area of the border itself, set the
466  /// [Path.fillType] of the returned object to [PathFillType.evenOdd], and add
467  /// to this object the path returned from [getOuterPath] (using
468  /// [Path.addPath]).
469  ///
470  /// The `textDirection` argument must be provided and non-null if the border
471  /// has a text direction dependency (for example if it is expressed in terms
472  /// of "start" and "end" instead of "left" and "right"). It may be null if
473  /// the border will not need the text direction to paint itself.
474  ///
475  /// See also:
476  ///
477  ///  * [getOuterPath], which creates the path for the outer edge.
478  ///  * [Path.contains], which can tell if an [Offset] is within a [Path].
479  Path getInnerPath(Rect rect, { TextDirection textDirection });
480
481  /// Paints the border within the given [Rect] on the given [Canvas].
482  ///
483  /// The `textDirection` argument must be provided and non-null if the border
484  /// has a text direction dependency (for example if it is expressed in terms
485  /// of "start" and "end" instead of "left" and "right"). It may be null if
486  /// the border will not need the text direction to paint itself.
487  void paint(Canvas canvas, Rect rect, { TextDirection textDirection });
488
489  @override
490  String toString() {
491    return '$runtimeType()';
492  }
493}
494
495/// Represents the addition of two otherwise-incompatible borders.
496///
497/// The borders are listed from the outside to the inside.
498class _CompoundBorder extends ShapeBorder {
499  _CompoundBorder(this.borders)
500    : assert(borders != null),
501      assert(borders.length >= 2),
502      assert(!borders.any((ShapeBorder border) => border is _CompoundBorder));
503
504  final List<ShapeBorder> borders;
505
506  @override
507  EdgeInsetsGeometry get dimensions {
508    return borders.fold<EdgeInsetsGeometry>(
509      EdgeInsets.zero,
510      (EdgeInsetsGeometry previousValue, ShapeBorder border) {
511        return previousValue.add(border.dimensions);
512      },
513    );
514  }
515
516  @override
517  ShapeBorder add(ShapeBorder other, { bool reversed = false }) {
518    // This wraps the list of borders with "other", or, if "reversed" is true,
519    // wraps "other" with the list of borders.
520    // If "reversed" is false, "other" should end up being at the start of the
521    // list, otherwise, if "reversed" is true, it should end up at the end.
522    // First, see if we can merge the new adjacent borders.
523    if (other is! _CompoundBorder) {
524      // Here, "ours" is the border at the side where we're adding the new
525      // border, and "merged" is the result of attempting to merge it with the
526      // new border. If it's null, it couldn't be merged.
527      final ShapeBorder ours = reversed ? borders.last : borders.first;
528      final ShapeBorder merged = ours.add(other, reversed: reversed)
529                             ?? other.add(ours, reversed: !reversed);
530      if (merged != null) {
531        final List<ShapeBorder> result = <ShapeBorder>[...borders];
532        result[reversed ? result.length - 1 : 0] = merged;
533        return _CompoundBorder(result);
534      }
535    }
536    // We can't, so fall back to just adding the new border to the list.
537    final List<ShapeBorder> mergedBorders = <ShapeBorder>[
538      if (reversed) ...borders,
539      if (other is _CompoundBorder) ...other.borders
540      else other,
541      if (!reversed) ...borders,
542    ];
543    return _CompoundBorder(mergedBorders);
544  }
545
546  @override
547  ShapeBorder scale(double t) {
548    return _CompoundBorder(
549      borders.map<ShapeBorder>((ShapeBorder border) => border.scale(t)).toList()
550    );
551  }
552
553  @override
554  ShapeBorder lerpFrom(ShapeBorder a, double t) {
555    return _CompoundBorder.lerp(a, this, t);
556  }
557
558  @override
559  ShapeBorder lerpTo(ShapeBorder b, double t) {
560    return _CompoundBorder.lerp(this, b, t);
561  }
562
563  static _CompoundBorder lerp(ShapeBorder a, ShapeBorder b, double t) {
564    assert(t != null);
565    assert(a is _CompoundBorder || b is _CompoundBorder); // Not really necessary, but all call sites currently intend this.
566    final List<ShapeBorder> aList = a is _CompoundBorder ? a.borders : <ShapeBorder>[a];
567    final List<ShapeBorder> bList = b is _CompoundBorder ? b.borders : <ShapeBorder>[b];
568    final List<ShapeBorder> results = <ShapeBorder>[];
569    final int length = math.max(aList.length, bList.length);
570    for (int index = 0; index < length; index += 1) {
571      final ShapeBorder localA = index < aList.length ? aList[index] : null;
572      final ShapeBorder localB = index < bList.length ? bList[index] : null;
573      if (localA != null && localB != null) {
574        final ShapeBorder localResult = localA.lerpTo(localB, t) ?? localB.lerpFrom(localA, t);
575        if (localResult != null) {
576          results.add(localResult);
577          continue;
578        }
579      }
580      // If we're changing from one shape to another, make sure the shape that is coming in
581      // is inserted before the shape that is going away, so that the outer path changes to
582      // the new border earlier rather than later. (This affects, among other things, where
583      // the ShapeDecoration class puts its background.)
584      if (localB != null)
585        results.add(localB.scale(t));
586      if (localA != null)
587        results.add(localA.scale(1.0 - t));
588    }
589    return _CompoundBorder(results);
590  }
591
592  @override
593  Path getInnerPath(Rect rect, { TextDirection textDirection }) {
594    for (int index = 0; index < borders.length - 1; index += 1)
595      rect = borders[index].dimensions.resolve(textDirection).deflateRect(rect);
596    return borders.last.getInnerPath(rect, textDirection: textDirection);
597  }
598
599  @override
600  Path getOuterPath(Rect rect, { TextDirection textDirection }) {
601    return borders.first.getOuterPath(rect, textDirection: textDirection);
602  }
603
604  @override
605  void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {
606    for (ShapeBorder border in borders) {
607      border.paint(canvas, rect, textDirection: textDirection);
608      rect = border.dimensions.resolve(textDirection).deflateRect(rect);
609    }
610  }
611
612  @override
613  bool operator ==(dynamic other) {
614    if (identical(this, other))
615      return true;
616    if (runtimeType != other.runtimeType)
617      return false;
618    final _CompoundBorder typedOther = other;
619    if (borders == typedOther.borders)
620      return true;
621    if (borders.length != typedOther.borders.length)
622      return false;
623    for (int index = 0; index < borders.length; index += 1) {
624      if (borders[index] != typedOther.borders[index])
625        return false;
626    }
627    return true;
628  }
629
630  @override
631  int get hashCode => hashList(borders);
632
633  @override
634  String toString() {
635    // We list them in reverse order because when adding two borders they end up
636    // in the list in the opposite order of what the source looks like: a + b =>
637    // [b, a]. We do this to make the painting code more optimal, and most of
638    // the rest of the code doesn't care, except toString() (for debugging).
639    return borders.reversed.map<String>((ShapeBorder border) => border.toString()).join(' + ');
640  }
641}
642
643/// Paints a border around the given rectangle on the canvas.
644///
645/// The four sides can be independently specified. They are painted in the order
646/// top, right, bottom, left. This is only notable if the widths of the borders
647/// and the size of the given rectangle are such that the border sides will
648/// overlap each other. No effort is made to optimize the rendering of uniform
649/// borders (where all the borders have the same configuration); to render a
650/// uniform border, consider using [Canvas.drawRect] directly.
651///
652/// The arguments must not be null.
653///
654/// See also:
655///
656///  * [paintImage], which paints an image in a rectangle on a canvas.
657///  * [Border], which uses this function to paint its border when the border is
658///    not uniform.
659///  * [BoxDecoration], which describes its border using the [Border] class.
660void paintBorder(
661  Canvas canvas,
662  Rect rect, {
663  BorderSide top = BorderSide.none,
664  BorderSide right = BorderSide.none,
665  BorderSide bottom = BorderSide.none,
666  BorderSide left = BorderSide.none,
667}) {
668  assert(canvas != null);
669  assert(rect != null);
670  assert(top != null);
671  assert(right != null);
672  assert(bottom != null);
673  assert(left != null);
674
675  // We draw the borders as filled shapes, unless the borders are hairline
676  // borders, in which case we use PaintingStyle.stroke, with the stroke width
677  // specified here.
678  final Paint paint = Paint()
679    ..strokeWidth = 0.0;
680
681  final Path path = Path();
682
683  switch (top.style) {
684    case BorderStyle.solid:
685      paint.color = top.color;
686      path.reset();
687      path.moveTo(rect.left, rect.top);
688      path.lineTo(rect.right, rect.top);
689      if (top.width == 0.0) {
690        paint.style = PaintingStyle.stroke;
691      } else {
692        paint.style = PaintingStyle.fill;
693        path.lineTo(rect.right - right.width, rect.top + top.width);
694        path.lineTo(rect.left + left.width, rect.top + top.width);
695      }
696      canvas.drawPath(path, paint);
697      break;
698    case BorderStyle.none:
699      break;
700  }
701
702  switch (right.style) {
703    case BorderStyle.solid:
704      paint.color = right.color;
705      path.reset();
706      path.moveTo(rect.right, rect.top);
707      path.lineTo(rect.right, rect.bottom);
708      if (right.width == 0.0) {
709        paint.style = PaintingStyle.stroke;
710      } else {
711        paint.style = PaintingStyle.fill;
712        path.lineTo(rect.right - right.width, rect.bottom - bottom.width);
713        path.lineTo(rect.right - right.width, rect.top + top.width);
714      }
715      canvas.drawPath(path, paint);
716      break;
717    case BorderStyle.none:
718      break;
719  }
720
721  switch (bottom.style) {
722    case BorderStyle.solid:
723      paint.color = bottom.color;
724      path.reset();
725      path.moveTo(rect.right, rect.bottom);
726      path.lineTo(rect.left, rect.bottom);
727      if (bottom.width == 0.0) {
728        paint.style = PaintingStyle.stroke;
729      } else {
730        paint.style = PaintingStyle.fill;
731        path.lineTo(rect.left + left.width, rect.bottom - bottom.width);
732        path.lineTo(rect.right - right.width, rect.bottom - bottom.width);
733      }
734      canvas.drawPath(path, paint);
735      break;
736    case BorderStyle.none:
737      break;
738  }
739
740  switch (left.style) {
741    case BorderStyle.solid:
742      paint.color = left.color;
743      path.reset();
744      path.moveTo(rect.left, rect.bottom);
745      path.lineTo(rect.left, rect.top);
746      if (left.width == 0.0) {
747        paint.style = PaintingStyle.stroke;
748      } else {
749        paint.style = PaintingStyle.fill;
750        path.lineTo(rect.left + left.width, rect.top + top.width);
751        path.lineTo(rect.left + left.width, rect.bottom - bottom.width);
752      }
753      canvas.drawPath(path, paint);
754      break;
755    case BorderStyle.none:
756      break;
757  }
758}
759