box_painter.dart 37.5 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 show Image, Gradient, lerpDouble;
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
}

238 239 240 241 242 243
// TODO(ianh): We should probably expose something that does this on Rect.
// https://github.com/flutter/flutter/issues/2318
Point _offsetToPoint(Offset offset, Rect rect) {
  return new Point(rect.left + offset.dx * rect.width, rect.top + offset.dy * rect.height);
}

Florian Loitsch's avatar
Florian Loitsch committed
244
/// A 2D gradient.
245
abstract class Gradient {
246
  const Gradient();
247
  Shader createShader(Rect rect);
248 249
}

Florian Loitsch's avatar
Florian Loitsch committed
250
/// A 2D linear gradient.
251
class LinearGradient extends Gradient {
252
  const LinearGradient({
253 254
    this.begin: const Offset(0.0, 0.5),
    this.end: const Offset(1.0, 0.5),
255
    this.colors,
256
    this.stops,
257
    this.tileMode: TileMode.clamp
258
  });
259

260 261 262 263 264 265 266
  /// The offset from coordinate (0.0,0.0) at which stop 0.0 of the
  /// gradient is placed, in a coordinate space that maps the top left
  /// of the paint box at (0.0,0.0) and the bottom right at (1.0,1.0).
  ///
  /// For example, a begin offset of (0.0,0.5) is half way down the
  /// left side of the box.
  final Offset begin;
267

268 269 270 271 272 273 274
  /// The offset from coordinate (0.0,0.0) at which stop 1.0 of the
  /// gradient is placed, in a coordinate space that maps the top left
  /// of the paint box at (0.0,0.0) and the bottom right at (1.0,1.0).
  ///
  /// For example, an end offset of (1.0,0.5) is half way down the
  /// right side of the box.
  final Offset end;
275

Florian Loitsch's avatar
Florian Loitsch committed
276
  /// The colors the gradient should obtain at each of the stops.
277 278
  ///
  /// Note: This list must have the same length as [stops].
279
  final List<Color> colors;
280

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

Florian Loitsch's avatar
Florian Loitsch committed
287
  /// How this gradient should tile the plane.
288
  final TileMode tileMode;
289

290 291 292 293 294
  Shader createShader(Rect rect) {
    return new ui.Gradient.linear(
      <Point>[_offsetToPoint(begin, rect), _offsetToPoint(end, rect)],
      colors, stops, tileMode
    );
295 296
  }

297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
  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;
  }

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

330
  String toString() {
331
    return 'LinearGradient($begin, $end, $colors, $stops, $tileMode)';
332
  }
333 334
}

Florian Loitsch's avatar
Florian Loitsch committed
335
/// A 2D radial gradient.
336
class RadialGradient extends Gradient {
337
  const RadialGradient({
338 339
    this.center: const Offset(0.5, 0.5),
    this.radius: 0.5,
340
    this.colors,
341
    this.stops,
342
    this.tileMode: TileMode.clamp
343 344
  });

345 346 347 348 349 350
  /// The center of the gradient, as an offset into the unit square
  /// describing the gradient which will be mapped onto the paint box.
  ///
  /// For example, an offset of (0.5,0.5) will place the radial
  /// gradient in the center of the box.
  final Offset center;
351

352 353 354 355 356 357
  /// The radius of the gradient, as a fraction of the shortest side
  /// of the paint box.
  ///
  /// For example, if a radial gradient is painted on a box that is
  /// 100.0 pixels wide and 200.0 pixels tall, then a radius of 1.0
  /// will place the 1.0 stop at 100.0 pixels from the [center].
358
  final double radius;
359

Florian Loitsch's avatar
Florian Loitsch committed
360
  /// The colors the gradient should obtain at each of the stops.
361 362
  ///
  /// Note: This list must have the same length as [stops].
363
  final List<Color> colors;
364

Florian Loitsch's avatar
Florian Loitsch committed
365
  /// A list of values from 0.0 to 1.0 that denote concentric rings.
366 367 368 369 370 371 372
  ///
  /// 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
373
  /// How this gradient should tile the plane.
374
  final TileMode tileMode;
375

376 377 378 379 380 381
  Shader createShader(Rect rect) {
    return new ui.Gradient.radial(
      _offsetToPoint(center, rect),
      radius * rect.shortestSide,
      colors, stops, tileMode
    );
382 383
  }

384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
  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;
  }

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

417
  String toString() {
418
    return 'RadialGradient($center, $radius, $colors, $stops, $tileMode)';
419
  }
420 421
}

Florian Loitsch's avatar
Florian Loitsch committed
422
/// How an image should be inscribed into a box.
423
enum ImageFit {
Florian Loitsch's avatar
Florian Loitsch committed
424
  /// Fill the box by distorting the image's aspect ratio.
425 426
  fill,

Florian Loitsch's avatar
Florian Loitsch committed
427
  /// As large as possible while still containing the image entirely within the box.
428 429
  contain,

Florian Loitsch's avatar
Florian Loitsch committed
430
  /// As small as possible while still covering the entire box.
431 432 433
  cover,

  /// Center the image within the box and discard any portions of the image that
Florian Loitsch's avatar
Florian Loitsch committed
434
  /// lie outside the box.
435 436 437
  none,

  /// Center the image within the box and, if necessary, scale the image down to
Florian Loitsch's avatar
Florian Loitsch committed
438
  /// ensure that the image fits within the box.
439 440 441
  scaleDown
}

Florian Loitsch's avatar
Florian Loitsch committed
442
/// How to paint any portions of a box not covered by an image.
443
enum ImageRepeat {
Florian Loitsch's avatar
Florian Loitsch committed
444
  /// Repeat the image in both the x and y directions until the box is filled.
445
  repeat,
446

Florian Loitsch's avatar
Florian Loitsch committed
447
  /// Repeat the image in the x direction until the box is filled horizontally.
448
  repeatX,
449

Florian Loitsch's avatar
Florian Loitsch committed
450
  /// Repeat the image in the y direction until the box is filled vertically.
451 452
  repeatY,

Florian Loitsch's avatar
Florian Loitsch committed
453
  /// Leave uncovered poritions of the box transparent.
454 455 456
  noRepeat
}

Adam Barth's avatar
Adam Barth committed
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
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
486
/// Paints an image into the given rectangle in the canvas.
487
void paintImage({
Adam Barth's avatar
Adam Barth committed
488
  Canvas canvas,
489
  Rect rect,
490
  ui.Image image,
Adam Barth's avatar
Adam Barth committed
491
  ColorFilter colorFilter,
492
  ImageFit fit,
Adam Barth's avatar
Adam Barth committed
493
  ImageRepeat repeat: ImageRepeat.noRepeat,
494
  Rect centerSlice,
495 496
  double alignX,
  double alignY
497
}) {
Ian Hickson's avatar
Ian Hickson committed
498 499
  assert(canvas != null);
  assert(image != null);
500 501 502 503 504 505 506 507 508 509 510
  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;
  }
511 512
  Size sourceSize;
  Size destinationSize;
513 514 515
  fit ??= centerSlice == null ? ImageFit.scaleDown : ImageFit.fill;
  assert(centerSlice == null || (fit != ImageFit.none && fit != ImageFit.cover));
  switch (fit) {
516
    case ImageFit.fill:
517 518
      sourceSize = inputSize;
      destinationSize = outputSize;
519 520
      break;
    case ImageFit.contain:
521 522 523
      sourceSize = inputSize;
      if (outputSize.width / outputSize.height > sourceSize.width / sourceSize.height)
        destinationSize = new Size(sourceSize.width * outputSize.height / sourceSize.height, outputSize.height);
524
      else
525
        destinationSize = new Size(outputSize.width, sourceSize.height * outputSize.width / sourceSize.width);
526 527
      break;
    case ImageFit.cover:
528 529
      if (outputSize.width / outputSize.height > inputSize.width / inputSize.height)
        sourceSize = new Size(inputSize.width, inputSize.width * outputSize.height / outputSize.width);
530
      else
531 532
        sourceSize = new Size(inputSize.height * outputSize.width / outputSize.height, inputSize.height);
      destinationSize = outputSize;
533 534
      break;
    case ImageFit.none:
535 536
      sourceSize = new Size(math.min(inputSize.width, outputSize.width),
                            math.min(inputSize.height, outputSize.height));
537
      destinationSize = sourceSize;
538 539
      break;
    case ImageFit.scaleDown:
540 541
      sourceSize = inputSize;
      destinationSize = outputSize;
542 543 544 545
      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);
546 547
      break;
  }
548 549 550 551 552 553 554
  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);
  }
555 556 557 558 559
  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;
  }
560
  Paint paint = new Paint()..isAntiAlias = false;
561
  if (colorFilter != null)
562
    paint.colorFilter = colorFilter;
563 564 565 566 567 568
  if (sourceSize != destinationSize) {
    // Use the "low" quality setting to scale the image, which corresponds to
    // bilinear interpolation, rather than the default "none" which corresponds
    // to nearest-neighbor.
    paint.filterQuality = FilterQuality.low;
  }
569 570
  double dx = (outputSize.width - destinationSize.width) * (alignX ?? 0.5);
  double dy = (outputSize.height - destinationSize.height) * (alignY ?? 0.5);
571
  Point destinationPosition = rect.topLeft + new Offset(dx, dy);
572
  Rect destinationRect = destinationPosition & destinationSize;
573 574 575 576
  if (repeat != ImageRepeat.noRepeat) {
    canvas.save();
    canvas.clipRect(rect);
  }
Adam Barth's avatar
Adam Barth committed
577 578 579 580 581 582 583 584
  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);
  }
585 586
  if (repeat != ImageRepeat.noRepeat)
    canvas.restore();
587
}
588

589 590 591 592 593
/// 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 {
594 595 596 597 598 599 600
  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);
  }
601
  FractionalOffset operator -(FractionalOffset other) {
602
    return new FractionalOffset(dx - other.dx, dy - other.dy);
603 604
  }
  FractionalOffset operator +(FractionalOffset other) {
605
    return new FractionalOffset(dx + other.dx, dy + other.dy);
606 607
  }
  FractionalOffset operator *(double other) {
608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623
    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);
624 625 626 627 628
  }
  bool operator ==(dynamic other) {
    if (other is! FractionalOffset)
      return false;
    final FractionalOffset typedOther = other;
629 630
    return dx == typedOther.dx &&
           dy == typedOther.dy;
631
  }
632
  int get hashCode => hashValues(dx, dy);
633 634 635 636
  static FractionalOffset lerp(FractionalOffset a, FractionalOffset b, double t) {
    if (a == null && b == null)
      return null;
    if (a == null)
637
      return new FractionalOffset(b.dx * t, b.dy * t);
638
    if (b == null)
639 640
      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));
641
  }
642
  String toString() => '$runtimeType($dx, $dy)';
643 644
}

645
/// A background image for a box.
646
class BackgroundImage {
647 648 649 650 651
  BackgroundImage({
    ImageResource image,
    this.fit,
    this.repeat: ImageRepeat.noRepeat,
    this.centerSlice,
652 653
    this.colorFilter,
    this.alignment
654 655
  }) : _imageResource = image;

656
  /// How the background image should be inscribed into the box.
657
  final ImageFit fit;
658

659
  /// How to paint any portions of the box not covered by the background image.
660
  final ImageRepeat repeat;
661

662 663 664 665 666 667 668 669 670 671
  /// 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
672
  final ColorFilter colorFilter;
673

674
  /// How to align the image within its bounds.
675 676 677 678
  ///
  /// 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.
679 680
  final FractionalOffset alignment;

681
  /// The image to be painted into the background.
682
  ui.Image get image => _image;
683
  ui.Image _image;
684

685
  final ImageResource _imageResource;
686

687
  final List<VoidCallback> _listeners = <VoidCallback>[];
688

689 690
  /// Adds a listener for background-image changes (e.g., for when it arrives
  /// from the network).
691
  void _addChangeListener(VoidCallback listener) {
692 693 694 695 696
    // 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);
697 698 699
    _listeners.add(listener);
  }

Florian Loitsch's avatar
Florian Loitsch committed
700
  /// Removes the listener for background-image changes.
701
  void _removeChangeListener(VoidCallback listener) {
702
    _listeners.remove(listener);
703 704 705 706 707 708
    // 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);
  }

709
  void _handleImageChanged(ImageInfo resolvedImage) {
710 711
    if (resolvedImage == null)
      return;
712
    _image = resolvedImage.image;
713 714 715
    final List<VoidCallback> localListeners =
      new List<VoidCallback>.from(_listeners);
    for (VoidCallback listener in localListeners)
716
      listener();
717 718 719 720 721 722 723 724 725 726 727 728
  }

  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 &&
729
           alignment == typedOther.alignment &&
730 731 732
           _imageResource == typedOther._imageResource;
  }

733
  int get hashCode => hashValues(fit, repeat, centerSlice, colorFilter, alignment, _imageResource);
734 735 736 737

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

738 739 740 741 742
/// 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.
743 744
  rectangle,

745 746
  /// 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,
747
  /// either the width or the height, such that the circle touches the edges of
748
  /// the box.
749 750
  circle
}
751

Florian Loitsch's avatar
Florian Loitsch committed
752
/// An immutable description of how to paint a box.
753
class BoxDecoration extends Decoration {
754 755 756 757 758 759 760
  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
761
    this.shape: BoxShape.rectangle
762 763
  });

764 765
  bool debugAssertValid() {
    assert(shape != BoxShape.circle ||
Florian Loitsch's avatar
Florian Loitsch committed
766
           borderRadius == null); // Can't have a border radius if you're a circle.
767 768 769
    return super.debugAssertValid();
  }

Florian Loitsch's avatar
Florian Loitsch committed
770
  /// The color to fill in the background of the box.
771 772 773
  ///
  /// The color is filled into the shape of the box (e.g., either a rectangle,
  /// potentially with a border radius, or a circle).
774
  final Color backgroundColor;
775

Florian Loitsch's avatar
Florian Loitsch committed
776
  /// An image to paint above the background color.
777
  final BackgroundImage backgroundImage;
778

Florian Loitsch's avatar
Florian Loitsch committed
779
  /// A border to draw above the background.
780
  final Border border;
781

Florian Loitsch's avatar
Florian Loitsch committed
782
  /// If non-null, the corners of this box are rounded by this radius.
783 784 785 786
  ///
  /// Applies only to boxes with rectangular shapes.
  final double borderRadius;

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

Florian Loitsch's avatar
Florian Loitsch committed
790
  /// A gradient to use when filling the background.
791
  final Gradient gradient;
792

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

796 797 798
  /// The inset space occupied by the border.
  EdgeDims get padding => border?.dimensions;

Florian Loitsch's avatar
Florian Loitsch committed
799
  /// Returns a new box decoration that is scaled by the given factor.
800 801 802
  BoxDecoration scale(double factor) {
    // TODO(abarth): Scale ALL the things.
    return new BoxDecoration(
Adam Barth's avatar
Adam Barth committed
803
      backgroundColor: Color.lerp(null, backgroundColor, factor),
804
      backgroundImage: backgroundImage,
805
      border: Border.lerp(null, border, factor),
806
      borderRadius: ui.lerpDouble(null, borderRadius, factor),
807
      boxShadow: BoxShadow.lerpList(null, boxShadow, factor),
808 809 810 811 812
      gradient: gradient,
      shape: shape
    );
  }

813
  /// Linearly interpolate between two box decorations.
814 815 816 817 818 819 820 821 822 823 824
  ///
  /// 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
825
      backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
826
      backgroundImage: b.backgroundImage,
827
      border: Border.lerp(a.border, b.border, t),
828
      borderRadius: ui.lerpDouble(a.borderRadius, b.borderRadius, t),
829 830 831 832 833 834
      boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t),
      gradient: b.gradient,
      shape: b.shape
    );
  }

835 836 837 838 839 840 841 842 843 844 845 846
  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);
  }

847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862
  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 {
863 864 865 866 867 868 869 870 871
    return hashValues(
      backgroundColor,
      backgroundImage,
      border,
      borderRadius,
      boxShadow,
      gradient,
      shape
    );
872 873
  }

874 875 876
  /// 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.
877
  String toString([String prefix = '']) {
Hixie's avatar
Hixie committed
878
    List<String> result = <String>[];
879 880 881 882 883 884 885 886 887
    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
888
      result.add('${prefix}boxShadow: ${boxShadow.map((BoxShadow shadow) => shadow.toString())}');
889 890
    if (gradient != null)
      result.add('${prefix}gradient: $gradient');
891
    if (shape != BoxShape.rectangle)
892
      result.add('${prefix}shape: $shape');
893 894
    if (prefix == '')
      return '$runtimeType(${result.join(', ')})';
895
    if (result.isEmpty)
Hixie's avatar
Hixie committed
896
      return '$prefix<no decorations specified>';
897 898
    return result.join('\n');
  }
899 900 901 902 903 904 905 906 907 908

  bool get needsListeners => backgroundImage != null;

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

909 910 911 912
  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
913
    // shortestSide and let Canvas do any remaining clamping.
914 915 916 917 918 919 920 921 922
    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) {
923
          RRect bounds = new RRect.fromRectXY(Point.origin & size, borderRadius, borderRadius);
924 925 926 927 928 929 930 931 932 933 934
          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;
    }
  }

935
  _BoxDecorationPainter createBoxPainter() => new _BoxDecorationPainter(this);
936 937
}

Florian Loitsch's avatar
Florian Loitsch committed
938
/// An object that paints a [BoxDecoration] into a canvas.
939 940 941
class _BoxDecorationPainter extends BoxPainter {
  _BoxDecorationPainter(this._decoration) {
    assert(_decoration != null);
942 943
  }

944
  final BoxDecoration _decoration;
945 946

  Paint _cachedBackgroundPaint;
947 948 949 950 951 952
  Rect _rectForCachedBackgroundPaint;
  Paint _getBackgroundPaint(Rect rect) {
    assert(rect != null);
    if (_cachedBackgroundPaint == null ||
        (_decoration.gradient == null && _rectForCachedBackgroundPaint != null) ||
        (_decoration.gradient != null && _rectForCachedBackgroundPaint != rect)) {
953 954 955 956 957
      Paint paint = new Paint();

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

958 959 960 961 962 963
      if (_decoration.gradient != null) {
        paint.shader = _decoration.gradient.createShader(rect);
        _rectForCachedBackgroundPaint = rect;
      } else {
        _rectForCachedBackgroundPaint = null;
      }
964 965 966 967 968 969 970

      _cachedBackgroundPaint = paint;
    }

    return _cachedBackgroundPaint;
  }

971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989
  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;
  }

990
  void _paintBox(Canvas canvas, Rect rect, Paint paint) {
Hans Muller's avatar
Hans Muller committed
991
    switch (_decoration.shape) {
992
      case BoxShape.circle:
Hans Muller's avatar
Hans Muller committed
993 994 995 996 997
        assert(_decoration.borderRadius == null);
        Point center = rect.center;
        double radius = rect.shortestSide / 2.0;
        canvas.drawCircle(center, radius, paint);
        break;
998
      case BoxShape.rectangle:
Hans Muller's avatar
Hans Muller committed
999 1000 1001
        if (_decoration.borderRadius == null) {
          canvas.drawRect(rect, paint);
        } else {
1002
          double radius = _decoration.getEffectiveBorderRadius(rect);
1003
          canvas.drawRRect(new RRect.fromRectXY(rect, radius, radius), paint);
Hans Muller's avatar
Hans Muller committed
1004 1005
        }
        break;
1006 1007 1008
    }
  }

1009
  void _paintShadows(Canvas canvas, Rect rect) {
Hans Muller's avatar
Hans Muller committed
1010 1011 1012 1013 1014
    if (_decoration.boxShadow == null)
      return;
    for (BoxShadow boxShadow in _decoration.boxShadow) {
      final Paint paint = new Paint()
        ..color = boxShadow.color
1015
        ..maskFilter = new MaskFilter.blur(BlurStyle.normal, boxShadow._blurSigma);
Hans Muller's avatar
Hans Muller committed
1016 1017 1018 1019 1020
      final Rect bounds = rect.shift(boxShadow.offset).inflate(boxShadow.spreadRadius);
      _paintBox(canvas, bounds, paint);
    }
  }

1021
  void _paintBackgroundColor(Canvas canvas, Rect rect) {
Hans Muller's avatar
Hans Muller committed
1022
    if (_decoration.backgroundColor != null || _decoration.gradient != null)
1023
      _paintBox(canvas, rect, _getBackgroundPaint(rect));
Hans Muller's avatar
Hans Muller committed
1024 1025
  }

1026
  void _paintBackgroundImage(Canvas canvas, Rect rect) {
1027 1028
    final BackgroundImage backgroundImage = _decoration.backgroundImage;
    if (backgroundImage == null)
1029
      return;
1030
    ui.Image image = backgroundImage.image;
1031 1032 1033 1034 1035 1036 1037
    if (image == null)
      return;
    paintImage(
      canvas: canvas,
      rect: rect,
      image: image,
      colorFilter: backgroundImage.colorFilter,
1038 1039
      alignX: backgroundImage.alignment?.dx,
      alignY: backgroundImage.alignment?.dy,
1040 1041 1042
      fit:  backgroundImage.fit,
      repeat: backgroundImage.repeat
    );
1043 1044
  }

1045
  void _paintBorder(Canvas canvas, Rect rect) {
1046 1047 1048
    if (_decoration.border == null)
      return;

1049 1050 1051 1052 1053
    if (_hasUniformBorder) {
      if (_decoration.borderRadius != null) {
        _paintBorderWithRadius(canvas, rect);
        return;
      }
1054
      if (_decoration.shape == BoxShape.circle) {
1055 1056 1057
        _paintBorderWithCircle(canvas, rect);
        return;
      }
1058 1059 1060
    }

    assert(_decoration.borderRadius == null); // TODO(abarth): Support non-uniform rounded borders.
1061
    assert(_decoration.shape == BoxShape.rectangle); // TODO(ianh): Support non-uniform borders on circles.
1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107

    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);
  }

1108
  void _paintBorderWithRadius(Canvas canvas, Rect rect) {
1109
    assert(_hasUniformBorder);
1110
    assert(_decoration.shape == BoxShape.rectangle);
1111 1112
    Color color = _decoration.border.top.color;
    double width = _decoration.border.top.width;
1113
    double radius = _decoration.getEffectiveBorderRadius(rect);
1114

1115 1116
    RRect outer = new RRect.fromRectXY(rect, radius, radius);
    RRect inner = new RRect.fromRectXY(rect.deflate(width), radius - width, radius - width);
1117 1118 1119
    canvas.drawDRRect(outer, inner, new Paint()..color = color);
  }

1120
  void _paintBorderWithCircle(Canvas canvas, Rect rect) {
1121
    assert(_hasUniformBorder);
1122
    assert(_decoration.shape == BoxShape.circle);
1123 1124
    assert(_decoration.borderRadius == null);
    double width = _decoration.border.top.width;
1125
    if (width <= 0.0)
1126
      return;
1127 1128 1129
    Paint paint = new Paint()
      ..color = _decoration.border.top.color
      ..strokeWidth = width
1130
      ..style = PaintingStyle.stroke;
1131 1132 1133 1134 1135
    Point center = rect.center;
    double radius = (rect.shortestSide - width) / 2.0;
    canvas.drawCircle(center, radius, paint);
  }

1136
  /// Paint the box decoration into the given location on the given canvas
1137
  void paint(Canvas canvas, Rect rect) {
Hans Muller's avatar
Hans Muller committed
1138
    _paintShadows(canvas, rect);
1139 1140 1141 1142 1143
    _paintBackgroundColor(canvas, rect);
    _paintBackgroundImage(canvas, rect);
    _paintBorder(canvas, rect);
  }
}