box_painter.dart 35.4 KB
Newer Older
1 2 3 4 5
// Copyright 2015 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 'dart:math' as math;
6
import 'dart:ui' as ui;
7

8
import 'package:flutter/services.dart';
9

10
import 'basic_types.dart';
11 12
import 'decoration.dart';
import 'edge_dims.dart';
13

14
export 'edge_dims.dart' show EdgeDims;
15

Florian Loitsch's avatar
Florian Loitsch committed
16
/// A side of a border of a box.
17 18 19 20 21
class BorderSide {
  const BorderSide({
    this.color: const Color(0xFF000000),
    this.width: 1.0
  });
22

Florian Loitsch's avatar
Florian Loitsch committed
23
  /// The color of this side of the border.
24
  final Color color;
25

Florian Loitsch's avatar
Florian Loitsch committed
26
  /// The width of this side of the border.
27 28
  final double width;

Florian Loitsch's avatar
Florian Loitsch committed
29
  /// A black border side of zero width.
30 31
  static const none = const BorderSide(width: 0.0);

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
  BorderSide copyWith({
    Color color,
    double width
  }) {
    return new BorderSide(
      color: color ?? this.color,
      width: width ?? this.width
    );
  }

  static BorderSide lerp(BorderSide a, BorderSide b, double t) {
    assert(a != null);
    assert(b != null);
    return new BorderSide(
      color: Color.lerp(a.color, b.color, t),
      width: ui.lerpDouble(a.width, b.width, t)
    );
  }

51 52 53 54 55 56 57 58 59 60
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BorderSide)
      return false;
    final BorderSide typedOther = other;
    return color == typedOther.color &&
           width == typedOther.width;
  }

61
  int get hashCode => hashValues(color, width);
62

63 64 65
  String toString() => 'BorderSide($color, $width)';
}

Florian Loitsch's avatar
Florian Loitsch committed
66
/// A border of a box, comprised of four sides.
67 68 69 70 71 72 73 74
class Border {
  const Border({
    this.top: BorderSide.none,
    this.right: BorderSide.none,
    this.bottom: BorderSide.none,
    this.left: BorderSide.none
  });

Florian Loitsch's avatar
Florian Loitsch committed
75
  /// A uniform border with all sides the same color and width.
76 77 78 79 80 81 82
  factory Border.all({
    Color color: const Color(0xFF000000),
    double width: 1.0
  }) {
    BorderSide side = new BorderSide(color: color, width: width);
    return new Border(top: side, right: side, bottom: side, left: side);
  }
83

Florian Loitsch's avatar
Florian Loitsch committed
84
  /// The top side of this border.
85
  final BorderSide top;
86

Florian Loitsch's avatar
Florian Loitsch committed
87
  /// The right side of this border.
88
  final BorderSide right;
89

Florian Loitsch's avatar
Florian Loitsch committed
90
  /// The bottom side of this border.
91
  final BorderSide bottom;
92

Florian Loitsch's avatar
Florian Loitsch committed
93
  /// The left side of this border.
94 95
  final BorderSide left;

Florian Loitsch's avatar
Florian Loitsch committed
96
  /// The widths of the sides of this border represented as an EdgeDims.
97
  EdgeDims get dimensions {
98
    return new EdgeDims.TRBL(top.width, right.width, bottom.width, left.width);
99 100
  }

101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
  Border scale(double t) {
    return new Border(
      top: top.copyWith(width: t * top.width),
      right: right.copyWith(width: t * right.width),
      bottom: bottom.copyWith(width: t * bottom.width),
      left: left.copyWith(width: t * left.width)
    );
  }

  static Border lerp(Border a, Border 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 Border(
      top: BorderSide.lerp(a.top, b.top, t),
      right: BorderSide.lerp(a.right, b.right, t),
      bottom: BorderSide.lerp(a.bottom, b.bottom, t),
      left: BorderSide.lerp(a.left, b.left, t)
    );
  }

125 126 127 128 129 130 131 132 133 134 135 136
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! Border)
      return false;
    final Border typedOther = other;
    return top == typedOther.top &&
           right == typedOther.right &&
           bottom == typedOther.bottom &&
           left == typedOther.left;
  }

137
  int get hashCode => hashValues(top, right, bottom, left);
138

139 140 141
  String toString() => 'Border($top, $right, $bottom, $left)';
}

Florian Loitsch's avatar
Florian Loitsch committed
142
/// A shadow cast by a box.
143 144 145
///
/// Note: BoxShadow can cast non-rectangular shadows if the box is
/// non-rectangular (e.g., has a border radius or a circular shape).
Hans Muller's avatar
Hans Muller committed
146
/// This class is similar to CSS box-shadow.
147 148 149 150
class BoxShadow {
  const BoxShadow({
    this.color,
    this.offset,
Hans Muller's avatar
Hans Muller committed
151 152
    this.blurRadius,
    this.spreadRadius: 0.0
153 154
  });

Florian Loitsch's avatar
Florian Loitsch committed
155
  /// The color of the shadow.
156
  final Color color;
157

Florian Loitsch's avatar
Florian Loitsch committed
158
  /// The displacement of the shadow from the box.
159
  final Offset offset;
160

Florian Loitsch's avatar
Florian Loitsch committed
161
  /// The standard deviation of the Gaussian to convolve with the box's shape.
Hans Muller's avatar
Hans Muller committed
162
  final double blurRadius;
163

Hans Muller's avatar
Hans Muller committed
164 165
  final double spreadRadius;

Florian Loitsch's avatar
Florian Loitsch committed
166 167
  // See SkBlurMask::ConvertRadiusToSigma().
  // https://github.com/google/skia/blob/bb5b77db51d2e149ee66db284903572a5aac09be/src/effects/SkBlurMask.cpp#L23
Hans Muller's avatar
Hans Muller committed
168 169
  double get _blurSigma => blurRadius * 0.57735 + 0.5;

Florian Loitsch's avatar
Florian Loitsch committed
170
  /// Returns a new box shadow with its offset, blurRadius, and spreadRadius scaled by the given factor.
171 172 173 174
  BoxShadow scale(double factor) {
    return new BoxShadow(
      color: color,
      offset: offset * factor,
Hans Muller's avatar
Hans Muller committed
175 176
      blurRadius: blurRadius * factor,
      spreadRadius: spreadRadius * factor
177 178 179
    );
  }

Florian Loitsch's avatar
Florian Loitsch committed
180
  /// Linearly interpolate between two box shadows.
181 182 183
  ///
  /// If either box shadow is null, this function linearly interpolates from a
  /// a box shadow that matches the other box shadow in color but has a zero
Hans Muller's avatar
Hans Muller committed
184
  /// offset and a zero blurRadius.
185 186 187 188 189 190 191 192
  static BoxShadow lerp(BoxShadow a, BoxShadow 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 BoxShadow(
Adam Barth's avatar
Adam Barth committed
193 194
      color: Color.lerp(a.color, b.color, t),
      offset: Offset.lerp(a.offset, b.offset, t),
Hans Muller's avatar
Hans Muller committed
195 196
      blurRadius: ui.lerpDouble(a.blurRadius, b.blurRadius, t),
      spreadRadius: ui.lerpDouble(a.spreadRadius, b.spreadRadius, t)
197 198
    );
  }
199

Florian Loitsch's avatar
Florian Loitsch committed
200
  /// Linearly interpolate between two lists of box shadows.
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
  ///
  /// If the lists differ in length, excess items are lerped with null.
  static List<BoxShadow> lerpList(List<BoxShadow> a, List<BoxShadow> b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
      a = new List<BoxShadow>();
    if (b == null)
      b = new List<BoxShadow>();
    List<BoxShadow> result = new List<BoxShadow>();
    int commonLength = math.min(a.length, b.length);
    for (int i = 0; i < commonLength; ++i)
      result.add(BoxShadow.lerp(a[i], b[i], t));
    for (int i = commonLength; i < a.length; ++i)
      result.add(a[i].scale(1.0 - t));
    for (int i = commonLength; i < b.length; ++i)
      result.add(b[i].scale(t));
    return result;
  }
220

221 222 223 224 225 226 227 228
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BoxShadow)
      return false;
    final BoxShadow typedOther = other;
    return color == typedOther.color &&
           offset == typedOther.offset &&
Hans Muller's avatar
Hans Muller committed
229 230
           blurRadius == typedOther.blurRadius &&
           spreadRadius == typedOther.spreadRadius;
231 232
  }

233
  int get hashCode => hashValues(color, offset, blurRadius, spreadRadius);
234

Hans Muller's avatar
Hans Muller committed
235
  String toString() => 'BoxShadow($color, $offset, $blurRadius, $spreadRadius)';
236 237
}

Florian Loitsch's avatar
Florian Loitsch committed
238
/// A 2D gradient.
239
abstract class Gradient {
240
  const Gradient();
241
  ui.Shader createShader();
242 243
}

Florian Loitsch's avatar
Florian Loitsch committed
244
/// A 2D linear gradient.
245
class LinearGradient extends Gradient {
246
  const LinearGradient({
247 248
    this.begin,
    this.end,
249
    this.colors,
250
    this.stops,
251
    this.tileMode: ui.TileMode.clamp
252
  });
253

Florian Loitsch's avatar
Florian Loitsch committed
254
  /// The point at which stop 0.0 of the gradient is placed.
255
  final Point begin;
256

Florian Loitsch's avatar
Florian Loitsch committed
257
  /// The point at which stop 1.0 of the gradient is placed.
258 259
  final Point end;

Florian Loitsch's avatar
Florian Loitsch committed
260
  /// The colors the gradient should obtain at each of the stops.
261 262
  ///
  /// Note: This list must have the same length as [stops].
263
  final List<Color> colors;
264

Florian Loitsch's avatar
Florian Loitsch committed
265
  /// A list of values from 0.0 to 1.0 that denote fractions of the vector from start to end.
266
  ///
267 268
  /// Note: If specified, this list must have the same length as [colors]. Otherwise the colors
  /// are distributed evenly between [begin] and [end].
269 270
  final List<double> stops;

Florian Loitsch's avatar
Florian Loitsch committed
271
  /// How this gradient should tile the plane.
272
  final ui.TileMode tileMode;
273

274
  ui.Shader createShader() {
Hixie's avatar
Hixie committed
275
    return new ui.Gradient.linear(<Point>[begin, end], this.colors, this.stops, this.tileMode);
276 277
  }

278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! LinearGradient)
      return false;
    final LinearGradient typedOther = other;
    if (begin != typedOther.begin ||
        end != typedOther.end ||
        tileMode != typedOther.tileMode ||
        colors?.length != typedOther.colors?.length ||
        stops?.length != typedOther.stops?.length)
      return false;
    if (colors != null) {
      assert(typedOther.colors != null);
      assert(colors.length == typedOther.colors.length);
      for (int i = 0; i < colors.length; i += 1) {
        if (colors[i] != typedOther.colors[i])
          return false;
      }
    }
    if (stops != null) {
      assert(typedOther.stops != null);
      assert(stops.length == typedOther.stops.length);
      for (int i = 0; i < stops.length; i += 1) {
        if (stops[i] != typedOther.stops[i])
          return false;
      }
    }
    return true;
  }

309
  int get hashCode => hashValues(begin, end, tileMode, hashList(colors), hashList(stops));
310

311
  String toString() {
312
    return 'LinearGradient($begin, $end, $colors, $stops, $tileMode)';
313
  }
314 315
}

Florian Loitsch's avatar
Florian Loitsch committed
316
/// A 2D radial gradient.
317
class RadialGradient extends Gradient {
318
  const RadialGradient({
319 320 321
    this.center,
    this.radius,
    this.colors,
322
    this.stops,
323
    this.tileMode: ui.TileMode.clamp
324 325
  });

Florian Loitsch's avatar
Florian Loitsch committed
326
  /// The center of the gradient.
327
  final Point center;
328

Florian Loitsch's avatar
Florian Loitsch committed
329
  /// The radius at which stop 1.0 is placed.
330
  final double radius;
331

Florian Loitsch's avatar
Florian Loitsch committed
332
  /// The colors the gradient should obtain at each of the stops.
333 334
  ///
  /// Note: This list must have the same length as [stops].
335
  final List<Color> colors;
336

Florian Loitsch's avatar
Florian Loitsch committed
337
  /// A list of values from 0.0 to 1.0 that denote concentric rings.
338 339 340 341 342 343 344
  ///
  /// The rings are centered at [center] and have a radius equal to the value of
  /// the stop times [radius].
  ///
  /// Note: This list must have the same length as [colors].
  final List<double> stops;

Florian Loitsch's avatar
Florian Loitsch committed
345
  /// How this gradient should tile the plane.
346
  final ui.TileMode tileMode;
347

348 349
  ui.Shader createShader() {
    return new ui.Gradient.radial(center, radius, colors, stops, tileMode);
350 351
  }

352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! RadialGradient)
      return false;
    final RadialGradient typedOther = other;
    if (center != typedOther.center ||
        radius != typedOther.radius ||
        tileMode != typedOther.tileMode ||
        colors?.length != typedOther.colors?.length ||
        stops?.length != typedOther.stops?.length)
      return false;
    if (colors != null) {
      assert(typedOther.colors != null);
      assert(colors.length == typedOther.colors.length);
      for (int i = 0; i < colors.length; i += 1) {
        if (colors[i] != typedOther.colors[i])
          return false;
      }
    }
    if (stops != null) {
      assert(typedOther.stops != null);
      assert(stops.length == typedOther.stops.length);
      for (int i = 0; i < stops.length; i += 1) {
        if (stops[i] != typedOther.stops[i])
          return false;
      }
    }
    return true;
  }

383
  int get hashCode => hashValues(center, radius, tileMode, hashList(colors), hashList(stops));
384

385
  String toString() {
386
    return 'RadialGradient($center, $radius, $colors, $stops, $tileMode)';
387
  }
388 389
}

Florian Loitsch's avatar
Florian Loitsch committed
390
/// How an image should be inscribed into a box.
391
enum ImageFit {
Florian Loitsch's avatar
Florian Loitsch committed
392
  /// Fill the box by distorting the image's aspect ratio.
393 394
  fill,

Florian Loitsch's avatar
Florian Loitsch committed
395
  /// As large as possible while still containing the image entirely within the box.
396 397
  contain,

Florian Loitsch's avatar
Florian Loitsch committed
398
  /// As small as possible while still covering the entire box.
399 400 401
  cover,

  /// Center the image within the box and discard any portions of the image that
Florian Loitsch's avatar
Florian Loitsch committed
402
  /// lie outside the box.
403 404 405
  none,

  /// Center the image within the box and, if necessary, scale the image down to
Florian Loitsch's avatar
Florian Loitsch committed
406
  /// ensure that the image fits within the box.
407 408 409
  scaleDown
}

Florian Loitsch's avatar
Florian Loitsch committed
410
/// How to paint any portions of a box not covered by an image.
411
enum ImageRepeat {
Florian Loitsch's avatar
Florian Loitsch committed
412
  /// Repeat the image in both the x and y directions until the box is filled.
413
  repeat,
414

Florian Loitsch's avatar
Florian Loitsch committed
415
  /// Repeat the image in the x direction until the box is filled horizontally.
416
  repeatX,
417

Florian Loitsch's avatar
Florian Loitsch committed
418
  /// Repeat the image in the y direction until the box is filled vertically.
419 420
  repeatY,

Florian Loitsch's avatar
Florian Loitsch committed
421
  /// Leave uncovered poritions of the box transparent.
422 423 424
  noRepeat
}

Adam Barth's avatar
Adam Barth committed
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
Iterable<Rect> _generateImageTileRects(Rect outputRect, Rect fundamentalRect, ImageRepeat repeat) sync* {
  if (repeat == ImageRepeat.noRepeat) {
    yield fundamentalRect;
    return;
  }

  int startX = 0;
  int startY = 0;
  int stopX = 0;
  int stopY = 0;
  double strideX = fundamentalRect.width;
  double strideY = fundamentalRect.height;

  if (repeat == ImageRepeat.repeat || repeat == ImageRepeat.repeatX) {
    startX = ((outputRect.left - fundamentalRect.left) / strideX).floor();
    stopX = ((outputRect.right - fundamentalRect.right) / strideX).ceil();
  }

  if (repeat == ImageRepeat.repeat || repeat == ImageRepeat.repeatY) {
    startY = ((outputRect.top - fundamentalRect.top) / strideY).floor();
    stopY = ((outputRect.bottom - fundamentalRect.bottom) / strideY).ceil();
  }

  for (int i = startX; i <= stopX; ++i) {
    for (int j = startY; j <= stopY; ++j)
      yield fundamentalRect.shift(new Offset(i * strideX, j * strideY));
  }
}

Florian Loitsch's avatar
Florian Loitsch committed
454
/// Paints an image into the given rectangle in the canvas.
455
void paintImage({
Adam Barth's avatar
Adam Barth committed
456
  Canvas canvas,
457
  Rect rect,
458
  ui.Image image,
Adam Barth's avatar
Adam Barth committed
459
  ColorFilter colorFilter,
460
  ImageFit fit,
Adam Barth's avatar
Adam Barth committed
461
  ImageRepeat repeat: ImageRepeat.noRepeat,
462
  Rect centerSlice,
463 464
  double alignX,
  double alignY
465
}) {
Ian Hickson's avatar
Ian Hickson committed
466 467
  assert(canvas != null);
  assert(image != null);
468 469 470 471 472 473 474 475 476 477 478
  Size outputSize = rect.size;
  Size inputSize = new Size(image.width.toDouble(), image.height.toDouble());
  Offset sliceBorder;
  if (centerSlice != null) {
    sliceBorder = new Offset(
      centerSlice.left + inputSize.width - centerSlice.right,
      centerSlice.top + inputSize.height - centerSlice.bottom
    );
    outputSize -= sliceBorder;
    inputSize -= sliceBorder;
  }
479 480
  Size sourceSize;
  Size destinationSize;
481 482 483
  fit ??= centerSlice == null ? ImageFit.scaleDown : ImageFit.fill;
  assert(centerSlice == null || (fit != ImageFit.none && fit != ImageFit.cover));
  switch (fit) {
484
    case ImageFit.fill:
485 486
      sourceSize = inputSize;
      destinationSize = outputSize;
487 488
      break;
    case ImageFit.contain:
489 490 491
      sourceSize = inputSize;
      if (outputSize.width / outputSize.height > sourceSize.width / sourceSize.height)
        destinationSize = new Size(sourceSize.width * outputSize.height / sourceSize.height, outputSize.height);
492
      else
493
        destinationSize = new Size(outputSize.width, sourceSize.height * outputSize.width / sourceSize.width);
494 495
      break;
    case ImageFit.cover:
496 497
      if (outputSize.width / outputSize.height > inputSize.width / inputSize.height)
        sourceSize = new Size(inputSize.width, inputSize.width * outputSize.height / outputSize.width);
498
      else
499 500
        sourceSize = new Size(inputSize.height * outputSize.width / outputSize.height, inputSize.height);
      destinationSize = outputSize;
501 502
      break;
    case ImageFit.none:
503 504
      sourceSize = new Size(math.min(inputSize.width, outputSize.width),
                            math.min(inputSize.height, outputSize.height));
505
      destinationSize = sourceSize;
506 507
      break;
    case ImageFit.scaleDown:
508 509
      sourceSize = inputSize;
      destinationSize = outputSize;
510 511 512 513
      if (sourceSize.height > destinationSize.height)
        destinationSize = new Size(sourceSize.width * destinationSize.height / sourceSize.height, sourceSize.height);
      if (sourceSize.width > destinationSize.width)
        destinationSize = new Size(destinationSize.width, sourceSize.height * destinationSize.width / sourceSize.width);
514 515
      break;
  }
516 517 518 519 520 521 522
  if (centerSlice != null) {
    outputSize += sliceBorder;
    destinationSize += sliceBorder;
    // We don't have the ability to draw a subset of the image at the same time
    // as we apply a nine-patch stretch.
    assert(sourceSize == inputSize);
  }
523 524 525 526 527
  if (repeat != ImageRepeat.noRepeat && destinationSize == outputSize) {
    // There's no need to repeat the image because we're exactly filling the
    // output rect with the image.
    repeat = ImageRepeat.noRepeat;
  }
528
  Paint paint = new Paint()..isAntiAlias = false;
529
  if (colorFilter != null)
530
    paint.colorFilter = colorFilter;
531 532
  double dx = (outputSize.width - destinationSize.width) * (alignX ?? 0.5);
  double dy = (outputSize.height - destinationSize.height) * (alignY ?? 0.5);
533
  Point destinationPosition = rect.topLeft + new Offset(dx, dy);
534
  Rect destinationRect = destinationPosition & destinationSize;
535 536 537 538
  if (repeat != ImageRepeat.noRepeat) {
    canvas.save();
    canvas.clipRect(rect);
  }
Adam Barth's avatar
Adam Barth committed
539 540 541 542 543 544 545 546
  if (centerSlice == null) {
    Rect sourceRect = Point.origin & sourceSize;
    for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat))
      canvas.drawImageRect(image, sourceRect, tileRect, paint);
  } else {
    for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat))
      canvas.drawImageNine(image, centerSlice, tileRect, paint);
  }
547 548
  if (repeat != ImageRepeat.noRepeat)
    canvas.restore();
549
}
550

551 552 553 554 555
/// An offset that's expressed as a fraction of a Size.
///
/// FractionalOffset(1.0, 0.0) represents the top right of the Size,
/// FractionalOffset(0.0, 1.0) represents the bottom left of the Size,
class FractionalOffset {
556 557 558 559 560 561 562
  const FractionalOffset(this.dx, this.dy);
  final double dx;
  final double dy;
  static const FractionalOffset zero = const FractionalOffset(0.0, 0.0);
  FractionalOffset operator -() {
    return new FractionalOffset(-dx, -dy);
  }
563
  FractionalOffset operator -(FractionalOffset other) {
564
    return new FractionalOffset(dx - other.dx, dy - other.dy);
565 566
  }
  FractionalOffset operator +(FractionalOffset other) {
567
    return new FractionalOffset(dx + other.dx, dy + other.dy);
568 569
  }
  FractionalOffset operator *(double other) {
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
    return new FractionalOffset(dx * other, dy * other);
  }
  FractionalOffset operator /(double other) {
    return new FractionalOffset(dx / other, dy / other);
  }
  FractionalOffset operator ~/(double other) {
    return new FractionalOffset((dx ~/ other).toDouble(), (dy ~/ other).toDouble());
  }
  FractionalOffset operator %(double other) {
    return new FractionalOffset(dx % other, dy % other);
  }
  Offset alongOffset(Offset other) {
    return new Offset(dx * other.dx, dy * other.dy);
  }
  Offset alongSize(Size other) {
    return new Offset(dx * other.width, dy * other.height);
586 587 588 589 590
  }
  bool operator ==(dynamic other) {
    if (other is! FractionalOffset)
      return false;
    final FractionalOffset typedOther = other;
591 592
    return dx == typedOther.dx &&
           dy == typedOther.dy;
593
  }
594
  int get hashCode => hashValues(dx, dy);
595 596 597 598
  static FractionalOffset lerp(FractionalOffset a, FractionalOffset b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
599
      return new FractionalOffset(b.dx * t, b.dy * t);
600
    if (b == null)
601 602
      return new FractionalOffset(b.dx * (1.0 - t), b.dy * (1.0 - t));
    return new FractionalOffset(ui.lerpDouble(a.dx, b.dx, t), ui.lerpDouble(a.dy, b.dy, t));
603
  }
604
  String toString() => '$runtimeType($dx, $dy)';
605 606
}

607
/// A background image for a box.
608
class BackgroundImage {
609 610 611 612 613
  BackgroundImage({
    ImageResource image,
    this.fit,
    this.repeat: ImageRepeat.noRepeat,
    this.centerSlice,
614 615
    this.colorFilter,
    this.alignment
616 617
  }) : _imageResource = image;

618
  /// How the background image should be inscribed into the box.
619
  final ImageFit fit;
620

621
  /// How to paint any portions of the box not covered by the background image.
622
  final ImageRepeat repeat;
623

624 625 626 627 628 629 630 631 632 633
  /// The center slice for a nine-patch image.
  ///
  /// The region of the image inside the center slice will be stretched both
  /// horizontally and vertically to fit the image into its destination. The
  /// region of the image above and below the center slice will be stretched
  /// only horizontally and the region of the image to the left and right of
  /// the center slice will be stretched only vertically.
  final Rect centerSlice;

  /// A color filter to apply to the background image before painting it.
Adam Barth's avatar
Adam Barth committed
634
  final ColorFilter colorFilter;
635

636
  /// How to align the image within its bounds.
637 638 639 640
  ///
  /// An alignment of (0.0, 0.0) aligns the image to the top-left corner of its
  /// layout bounds.  An alignment of (1.0, 0.5) aligns the image to the middle
  /// of the right edge of its layout bounds.
641 642
  final FractionalOffset alignment;

643
  /// The image to be painted into the background.
644
  ui.Image get image => _image;
645
  ui.Image _image;
646

647
  final ImageResource _imageResource;
648

649
  final List<VoidCallback> _listeners = <VoidCallback>[];
650

651 652
  /// Adds a listener for background-image changes (e.g., for when it arrives
  /// from the network).
653
  void _addChangeListener(VoidCallback listener) {
654 655 656 657 658
    // We add the listener to the _imageResource first so that the first change
    // listener doesn't get callback synchronously if the image resource is
    // already resolved.
    if (_listeners.isEmpty)
      _imageResource.addListener(_handleImageChanged);
659 660 661
    _listeners.add(listener);
  }

Florian Loitsch's avatar
Florian Loitsch committed
662
  /// Removes the listener for background-image changes.
663
  void _removeChangeListener(VoidCallback listener) {
664
    _listeners.remove(listener);
665 666 667 668 669 670
    // We need to remove ourselves as listeners from the _imageResource so that
    // we're not kept alive by the image_cache.
    if (_listeners.isEmpty)
      _imageResource.removeListener(_handleImageChanged);
  }

671
  void _handleImageChanged(ImageInfo resolvedImage) {
672 673
    if (resolvedImage == null)
      return;
674
    _image = resolvedImage.image;
675 676 677
    final List<VoidCallback> localListeners =
      new List<VoidCallback>.from(_listeners);
    for (VoidCallback listener in localListeners)
678
      listener();
679 680 681 682 683 684 685 686 687 688 689 690
  }

  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BackgroundImage)
      return false;
    final BackgroundImage typedOther = other;
    return fit == typedOther.fit &&
           repeat == typedOther.repeat &&
           centerSlice == typedOther.centerSlice &&
           colorFilter == typedOther.colorFilter &&
691
           alignment == typedOther.alignment &&
692 693 694
           _imageResource == typedOther._imageResource;
  }

695
  int get hashCode => hashValues(fit, repeat, centerSlice, colorFilter, alignment, _imageResource);
696 697 698 699

  String toString() => 'BackgroundImage($fit, $repeat)';
}

700 701 702 703 704
/// The shape to use when rendering a BoxDecoration.
enum BoxShape {
  /// An axis-aligned, 2D rectangle. May have rounded corners. The edges of the
  /// rectangle will match the edges of the box into which the BoxDecoration is
  /// painted.
705 706
  rectangle,

707 708
  /// A circle centered in the middle of the box into which the BoxDecoration is
  /// painted. The diameter of the circle is the shortest dimension of the box,
709
  /// either the width or the height, such that the circle touches the edges of
710
  /// the box.
711 712
  circle
}
713

Florian Loitsch's avatar
Florian Loitsch committed
714
/// An immutable description of how to paint a box.
715
class BoxDecoration extends Decoration {
716 717 718 719 720 721 722
  const BoxDecoration({
    this.backgroundColor, // null = don't draw background color
    this.backgroundImage, // null = don't draw background image
    this.border, // null = don't draw border
    this.borderRadius, // null = use more efficient background drawing; note that this must be null for circles
    this.boxShadow, // null = don't draw shadows
    this.gradient, // null = don't allocate gradient objects
723
    this.shape: BoxShape.rectangle
724 725
  });

726 727
  bool debugAssertValid() {
    assert(shape != BoxShape.circle ||
Florian Loitsch's avatar
Florian Loitsch committed
728
           borderRadius == null); // Can't have a border radius if you're a circle.
729 730 731
    return super.debugAssertValid();
  }

Florian Loitsch's avatar
Florian Loitsch committed
732
  /// The color to fill in the background of the box.
733 734 735
  ///
  /// The color is filled into the shape of the box (e.g., either a rectangle,
  /// potentially with a border radius, or a circle).
736
  final Color backgroundColor;
737

Florian Loitsch's avatar
Florian Loitsch committed
738
  /// An image to paint above the background color.
739
  final BackgroundImage backgroundImage;
740

Florian Loitsch's avatar
Florian Loitsch committed
741
  /// A border to draw above the background.
742
  final Border border;
743

Florian Loitsch's avatar
Florian Loitsch committed
744
  /// If non-null, the corners of this box are rounded by this radius.
745 746 747 748
  ///
  /// Applies only to boxes with rectangular shapes.
  final double borderRadius;

Florian Loitsch's avatar
Florian Loitsch committed
749
  /// A list of shadows cast by this box behind the background.
750
  final List<BoxShadow> boxShadow;
751

Florian Loitsch's avatar
Florian Loitsch committed
752
  /// A gradient to use when filling the background.
753
  final Gradient gradient;
754

Florian Loitsch's avatar
Florian Loitsch committed
755
  /// The shape to fill the background color into and to cast as a shadow.
756
  final BoxShape shape;
757

Florian Loitsch's avatar
Florian Loitsch committed
758
  /// Returns a new box decoration that is scaled by the given factor.
759 760 761
  BoxDecoration scale(double factor) {
    // TODO(abarth): Scale ALL the things.
    return new BoxDecoration(
Adam Barth's avatar
Adam Barth committed
762
      backgroundColor: Color.lerp(null, backgroundColor, factor),
763
      backgroundImage: backgroundImage,
764
      border: Border.lerp(null, border, factor),
765
      borderRadius: ui.lerpDouble(null, borderRadius, factor),
766
      boxShadow: BoxShadow.lerpList(null, boxShadow, factor),
767 768 769 770 771
      gradient: gradient,
      shape: shape
    );
  }

772
  /// Linearly interpolate between two box decorations.
773 774 775 776 777 778 779 780 781 782 783
  ///
  /// Interpolates each parameter of the box decoration separately.
  static BoxDecoration lerp(BoxDecoration a, BoxDecoration 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);
    // TODO(abarth): lerp ALL the fields.
    return new BoxDecoration(
Adam Barth's avatar
Adam Barth committed
784
      backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
785
      backgroundImage: b.backgroundImage,
786
      border: Border.lerp(a.border, b.border, t),
787
      borderRadius: ui.lerpDouble(a.borderRadius, b.borderRadius, t),
788 789 790 791 792 793
      boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t),
      gradient: b.gradient,
      shape: b.shape
    );
  }

794 795 796 797 798 799 800 801 802 803 804 805
  BoxDecoration lerpFrom(Decoration a, double t) {
    if (a is! BoxDecoration)
      return BoxDecoration.lerp(null, this, t);
    return BoxDecoration.lerp(a, this, t);
  }

  BoxDecoration lerpTo(Decoration b, double t) {
    if (b is! BoxDecoration)
      return BoxDecoration.lerp(this, null, t);
    return BoxDecoration.lerp(this, b, t);
  }

806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! BoxDecoration)
      return false;
    final BoxDecoration typedOther = other;
    return backgroundColor == typedOther.backgroundColor &&
           backgroundImage == typedOther.backgroundImage &&
           border == typedOther.border &&
           borderRadius == typedOther.borderRadius &&
           boxShadow == typedOther.boxShadow &&
           gradient == typedOther.gradient &&
           shape == typedOther.shape;
  }

  int get hashCode {
822 823 824 825 826 827 828 829 830
    return hashValues(
      backgroundColor,
      backgroundImage,
      border,
      borderRadius,
      boxShadow,
      gradient,
      shape
    );
831 832
  }

833 834 835
  /// Stringifies the BoxDecoration. By default, the output will be on one line.
  /// If the method is passed a non-empty string argument, then the output will
  /// span multiple lines, each prefixed by that argument.
836
  String toString([String prefix = '']) {
Hixie's avatar
Hixie committed
837
    List<String> result = <String>[];
838 839 840 841 842 843 844 845 846
    if (backgroundColor != null)
      result.add('${prefix}backgroundColor: $backgroundColor');
    if (backgroundImage != null)
      result.add('${prefix}backgroundImage: $backgroundImage');
    if (border != null)
      result.add('${prefix}border: $border');
    if (borderRadius != null)
      result.add('${prefix}borderRadius: $borderRadius');
    if (boxShadow != null)
Hixie's avatar
Hixie committed
847
      result.add('${prefix}boxShadow: ${boxShadow.map((BoxShadow shadow) => shadow.toString())}');
848 849
    if (gradient != null)
      result.add('${prefix}gradient: $gradient');
850
    if (shape != BoxShape.rectangle)
851
      result.add('${prefix}shape: $shape');
852 853
    if (prefix == '')
      return '$runtimeType(${result.join(', ')})';
854
    if (result.isEmpty)
Hixie's avatar
Hixie committed
855
      return '$prefix<no decorations specified>';
856 857
    return result.join('\n');
  }
858 859 860 861 862 863 864 865 866 867

  bool get needsListeners => backgroundImage != null;

  void addChangeListener(VoidCallback listener) {
    backgroundImage?._addChangeListener(listener);
  }
  void removeChangeListener(VoidCallback listener) {
    backgroundImage?._removeChangeListener(listener);
  }

868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
  double getEffectiveBorderRadius(Rect rect) {
    double shortestSide = rect.shortestSide;
    // In principle, we should use shortestSide / 2.0, but we don't want to
    // run into floating point rounding errors. Instead, we just use
    // shortestSide and let ui.Canvas do any remaining clamping.
    return borderRadius > shortestSide ? shortestSide : borderRadius;
  }

  bool hitTest(Size size, Point position) {
    assert(shape != null);
    assert((Point.origin & size).contains(position));
    switch (shape) {
      case BoxShape.rectangle:
        if (borderRadius != null) {
          ui.RRect bounds = new ui.RRect.fromRectXY(Point.origin & size, borderRadius, borderRadius);
          return bounds.contains(position);
        }
        return true;
      case BoxShape.circle:
        // Circles are inscribed into our smallest dimension.
        Point center = size.center(Point.origin);
        double distance = (position - center).distance;
        return distance <= math.min(size.width, size.height) / 2.0;
    }
  }

894
  _BoxDecorationPainter createBoxPainter() => new _BoxDecorationPainter(this);
895 896
}

Florian Loitsch's avatar
Florian Loitsch committed
897
/// An object that paints a [BoxDecoration] into a canvas.
898 899 900
class _BoxDecorationPainter extends BoxPainter {
  _BoxDecorationPainter(this._decoration) {
    assert(_decoration != null);
901 902
  }

903
  final BoxDecoration _decoration;
904 905 906 907 908 909 910 911 912 913

  Paint _cachedBackgroundPaint;
  Paint get _backgroundPaint {
    if (_cachedBackgroundPaint == null) {
      Paint paint = new Paint();

      if (_decoration.backgroundColor != null)
        paint.color = _decoration.backgroundColor;

      if (_decoration.gradient != null)
914
        paint.shader = _decoration.gradient.createShader();
915 916 917 918 919 920 921

      _cachedBackgroundPaint = paint;
    }

    return _cachedBackgroundPaint;
  }

922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940
  bool get _hasUniformBorder {
    Color color = _decoration.border.top.color;
    bool hasUniformColor =
      _decoration.border.right.color == color &&
      _decoration.border.bottom.color == color &&
      _decoration.border.left.color == color;

    if (!hasUniformColor)
      return false;

    double width = _decoration.border.top.width;
    bool hasUniformWidth =
      _decoration.border.right.width == width &&
      _decoration.border.bottom.width == width &&
      _decoration.border.left.width == width;

    return hasUniformWidth;
  }

Hans Muller's avatar
Hans Muller committed
941 942
  void _paintBox(ui.Canvas canvas, Rect rect, Paint paint) {
    switch (_decoration.shape) {
943
      case BoxShape.circle:
Hans Muller's avatar
Hans Muller committed
944 945 946 947 948
        assert(_decoration.borderRadius == null);
        Point center = rect.center;
        double radius = rect.shortestSide / 2.0;
        canvas.drawCircle(center, radius, paint);
        break;
949
      case BoxShape.rectangle:
Hans Muller's avatar
Hans Muller committed
950 951 952
        if (_decoration.borderRadius == null) {
          canvas.drawRect(rect, paint);
        } else {
953
          double radius = _decoration.getEffectiveBorderRadius(rect);
Hans Muller's avatar
Hans Muller committed
954 955 956
          canvas.drawRRect(new ui.RRect.fromRectXY(rect, radius, radius), paint);
        }
        break;
957 958 959
    }
  }

Hans Muller's avatar
Hans Muller committed
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976
  void _paintShadows(ui.Canvas canvas, Rect rect) {
    if (_decoration.boxShadow == null)
      return;
    for (BoxShadow boxShadow in _decoration.boxShadow) {
      final Paint paint = new Paint()
        ..color = boxShadow.color
        ..maskFilter = new ui.MaskFilter.blur(ui.BlurStyle.normal, boxShadow._blurSigma);
      final Rect bounds = rect.shift(boxShadow.offset).inflate(boxShadow.spreadRadius);
      _paintBox(canvas, bounds, paint);
    }
  }

  void _paintBackgroundColor(ui.Canvas canvas, Rect rect) {
    if (_decoration.backgroundColor != null || _decoration.gradient != null)
      _paintBox(canvas, rect, _backgroundPaint);
  }

977
  void _paintBackgroundImage(ui.Canvas canvas, Rect rect) {
978 979
    final BackgroundImage backgroundImage = _decoration.backgroundImage;
    if (backgroundImage == null)
980
      return;
981
    ui.Image image = backgroundImage.image;
982 983 984 985 986 987 988
    if (image == null)
      return;
    paintImage(
      canvas: canvas,
      rect: rect,
      image: image,
      colorFilter: backgroundImage.colorFilter,
989 990
      alignX: backgroundImage.alignment?.dx,
      alignY: backgroundImage.alignment?.dy,
991 992 993
      fit:  backgroundImage.fit,
      repeat: backgroundImage.repeat
    );
994 995
  }

996
  void _paintBorder(ui.Canvas canvas, Rect rect) {
997 998 999
    if (_decoration.border == null)
      return;

1000 1001 1002 1003 1004
    if (_hasUniformBorder) {
      if (_decoration.borderRadius != null) {
        _paintBorderWithRadius(canvas, rect);
        return;
      }
1005
      if (_decoration.shape == BoxShape.circle) {
1006 1007 1008
        _paintBorderWithCircle(canvas, rect);
        return;
      }
1009 1010 1011
    }

    assert(_decoration.borderRadius == null); // TODO(abarth): Support non-uniform rounded borders.
1012
    assert(_decoration.shape == BoxShape.rectangle); // TODO(ianh): Support non-uniform borders on circles.
1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058

    assert(_decoration.border.top != null);
    assert(_decoration.border.right != null);
    assert(_decoration.border.bottom != null);
    assert(_decoration.border.left != null);

    Paint paint = new Paint();
    Path path;

    paint.color = _decoration.border.top.color;
    path = new Path();
    path.moveTo(rect.left, rect.top);
    path.lineTo(rect.left + _decoration.border.left.width, rect.top + _decoration.border.top.width);
    path.lineTo(rect.right - _decoration.border.right.width, rect.top + _decoration.border.top.width);
    path.lineTo(rect.right, rect.top);
    path.close();
    canvas.drawPath(path, paint);

    paint.color = _decoration.border.right.color;
    path = new Path();
    path.moveTo(rect.right, rect.top);
    path.lineTo(rect.right - _decoration.border.right.width, rect.top + _decoration.border.top.width);
    path.lineTo(rect.right - _decoration.border.right.width, rect.bottom - _decoration.border.bottom.width);
    path.lineTo(rect.right, rect.bottom);
    path.close();
    canvas.drawPath(path, paint);

    paint.color = _decoration.border.bottom.color;
    path = new Path();
    path.moveTo(rect.right, rect.bottom);
    path.lineTo(rect.right - _decoration.border.right.width, rect.bottom - _decoration.border.bottom.width);
    path.lineTo(rect.left + _decoration.border.left.width, rect.bottom - _decoration.border.bottom.width);
    path.lineTo(rect.left, rect.bottom);
    path.close();
    canvas.drawPath(path, paint);

    paint.color = _decoration.border.left.color;
    path = new Path();
    path.moveTo(rect.left, rect.bottom);
    path.lineTo(rect.left + _decoration.border.left.width, rect.bottom - _decoration.border.bottom.width);
    path.lineTo(rect.left + _decoration.border.left.width, rect.top + _decoration.border.top.width);
    path.lineTo(rect.left, rect.top);
    path.close();
    canvas.drawPath(path, paint);
  }

1059
  void _paintBorderWithRadius(ui.Canvas canvas, Rect rect) {
1060
    assert(_hasUniformBorder);
1061
    assert(_decoration.shape == BoxShape.rectangle);
1062 1063
    Color color = _decoration.border.top.color;
    double width = _decoration.border.top.width;
1064
    double radius = _decoration.getEffectiveBorderRadius(rect);
1065

1066 1067
    ui.RRect outer = new ui.RRect.fromRectXY(rect, radius, radius);
    ui.RRect inner = new ui.RRect.fromRectXY(rect.deflate(width), radius - width, radius - width);
1068 1069 1070
    canvas.drawDRRect(outer, inner, new Paint()..color = color);
  }

1071
  void _paintBorderWithCircle(ui.Canvas canvas, Rect rect) {
1072
    assert(_hasUniformBorder);
1073
    assert(_decoration.shape == BoxShape.circle);
1074 1075
    assert(_decoration.borderRadius == null);
    double width = _decoration.border.top.width;
1076
    if (width <= 0.0)
1077
      return;
1078 1079 1080
    Paint paint = new Paint()
      ..color = _decoration.border.top.color
      ..strokeWidth = width
1081
      ..style = ui.PaintingStyle.stroke;
1082 1083 1084 1085 1086
    Point center = rect.center;
    double radius = (rect.shortestSide - width) / 2.0;
    canvas.drawCircle(center, radius, paint);
  }

1087
  /// Paint the box decoration into the given location on the given canvas
1088
  void paint(ui.Canvas canvas, Rect rect) {
Hans Muller's avatar
Hans Muller committed
1089
    _paintShadows(canvas, rect);
1090 1091 1092 1093 1094
    _paintBackgroundColor(canvas, rect);
    _paintBackgroundImage(canvas, rect);
    _paintBorder(canvas, rect);
  }
}