| // 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; |
| import 'dart:sky' as sky; |
| import 'dart:sky' show Point, Offset, Size, Rect, Color, Paint, Path; |
| |
| import 'package:sky/base/image_resource.dart'; |
| import 'package:sky/base/lerp.dart'; |
| import 'package:sky/painting/shadows.dart'; |
| |
| class EdgeDims { |
| // used for e.g. padding |
| const EdgeDims(this.top, this.right, this.bottom, this.left); |
| const EdgeDims.all(double value) |
| : top = value, right = value, bottom = value, left = value; |
| const EdgeDims.only({ this.top: 0.0, |
| this.right: 0.0, |
| this.bottom: 0.0, |
| this.left: 0.0 }); |
| const EdgeDims.symmetric({ double vertical: 0.0, |
| double horizontal: 0.0 }) |
| : top = vertical, left = horizontal, bottom = vertical, right = horizontal; |
| |
| final double top; |
| final double right; |
| final double bottom; |
| final double left; |
| |
| bool operator ==(other) { |
| if (identical(this, other)) |
| return true; |
| return other is EdgeDims |
| && top == other.top |
| && right == other.right |
| && bottom == other.bottom |
| && left == other.left; |
| } |
| |
| EdgeDims operator+(EdgeDims other) { |
| return new EdgeDims(top + other.top, |
| right + other.right, |
| bottom + other.bottom, |
| left + other.left); |
| } |
| |
| EdgeDims operator-(EdgeDims other) { |
| return new EdgeDims(top - other.top, |
| right - other.right, |
| bottom - other.bottom, |
| left - other.left); |
| } |
| |
| static const EdgeDims zero = const EdgeDims(0.0, 0.0, 0.0, 0.0); |
| |
| int get hashCode { |
| int value = 373; |
| value = 37 * value + top.hashCode; |
| value = 37 * value + left.hashCode; |
| value = 37 * value + bottom.hashCode; |
| value = 37 * value + right.hashCode; |
| return value; |
| } |
| String toString() => "EdgeDims($top, $right, $bottom, $left)"; |
| } |
| |
| class BorderSide { |
| const BorderSide({ |
| this.color: const Color(0xFF000000), |
| this.width: 1.0 |
| }); |
| final Color color; |
| final double width; |
| |
| static const none = const BorderSide(width: 0.0); |
| |
| int get hashCode { |
| int value = 373; |
| value = 37 * value * color.hashCode; |
| value = 37 * value * width.hashCode; |
| return value; |
| } |
| String toString() => 'BorderSide($color, $width)'; |
| } |
| |
| class Border { |
| const Border({ |
| this.top: BorderSide.none, |
| this.right: BorderSide.none, |
| this.bottom: BorderSide.none, |
| this.left: BorderSide.none |
| }); |
| |
| 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); |
| } |
| |
| final BorderSide top; |
| final BorderSide right; |
| final BorderSide bottom; |
| final BorderSide left; |
| |
| EdgeDims get dimensions { |
| return new EdgeDims(top.width, right.width, bottom.width, left.width); |
| } |
| |
| int get hashCode { |
| int value = 373; |
| value = 37 * value * top.hashCode; |
| value = 37 * value * right.hashCode; |
| value = 37 * value * bottom.hashCode; |
| value = 37 * value * left.hashCode; |
| return value; |
| } |
| String toString() => 'Border($top, $right, $bottom, $left)'; |
| } |
| |
| class BoxShadow { |
| const BoxShadow({ |
| this.color, |
| this.offset, |
| this.blur |
| }); |
| |
| final Color color; |
| final Offset offset; |
| final double blur; |
| |
| BoxShadow scale(double factor) { |
| return new BoxShadow( |
| color: color, |
| offset: offset * factor, |
| blur: blur * factor |
| ); |
| } |
| |
| String toString() => 'BoxShadow($color, $offset, $blur)'; |
| } |
| |
| BoxShadow lerpBoxShadow(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( |
| color: lerpColor(a.color, b.color, t), |
| offset: lerpOffset(a.offset, b.offset, t), |
| blur: lerpNum(a.blur, b.blur, t) |
| ); |
| } |
| |
| List<BoxShadow> lerpListBoxShadow(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(lerpBoxShadow(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; |
| } |
| |
| abstract class Gradient { |
| sky.Shader createShader(); |
| } |
| |
| class LinearGradient extends Gradient { |
| LinearGradient({ |
| this.endPoints, |
| this.colors, |
| this.colorStops, |
| this.tileMode: sky.TileMode.clamp |
| }); |
| |
| final List<Point> endPoints; |
| final List<Color> colors; |
| final List<double> colorStops; |
| final sky.TileMode tileMode; |
| |
| sky.Shader createShader() { |
| return new sky.Gradient.linear(this.endPoints, this.colors, |
| this.colorStops, this.tileMode); |
| } |
| |
| String toString() { |
| return 'LinearGradient($endPoints, $colors, $colorStops, $tileMode)'; |
| } |
| } |
| |
| class RadialGradient extends Gradient { |
| RadialGradient({ |
| this.center, |
| this.radius, |
| this.colors, |
| this.colorStops, |
| this.tileMode: sky.TileMode.clamp |
| }); |
| |
| final Point center; |
| final double radius; |
| final List<Color> colors; |
| final List<double> colorStops; |
| final sky.TileMode tileMode; |
| |
| sky.Shader createShader() { |
| return new sky.Gradient.radial(this.center, this.radius, this.colors, |
| this.colorStops, this.tileMode); |
| } |
| |
| String toString() { |
| return 'RadialGradient($center, $radius, $colors, $colorStops, $tileMode)'; |
| } |
| } |
| |
| enum ImageFit { fill, contain, cover, none, scaleDown } |
| |
| enum ImageRepeat { repeat, repeatX, repeatY, noRepeat } |
| |
| void paintImage({ |
| sky.Canvas canvas, |
| Rect rect, |
| sky.Image image, |
| sky.ColorFilter colorFilter, |
| fit: ImageFit.scaleDown, |
| repeat: ImageRepeat.noRepeat, |
| double positionX: 0.5, |
| double positionY: 0.5 |
| }) { |
| Size bounds = rect.size; |
| Size imageSize = new Size(image.width.toDouble(), image.height.toDouble()); |
| Size sourceSize; |
| Size destinationSize; |
| switch(fit) { |
| case ImageFit.fill: |
| sourceSize = imageSize; |
| destinationSize = bounds; |
| break; |
| case ImageFit.contain: |
| sourceSize = imageSize; |
| if (bounds.width / bounds.height > sourceSize.width / sourceSize.height) |
| destinationSize = new Size(sourceSize.width * bounds.height / sourceSize.height, bounds.height); |
| else |
| destinationSize = new Size(bounds.width, sourceSize.height * bounds.width / sourceSize.width); |
| break; |
| case ImageFit.cover: |
| if (bounds.width / bounds.height > imageSize.width / imageSize.height) |
| sourceSize = new Size(imageSize.width, imageSize.width * bounds.height / bounds.width); |
| else |
| sourceSize = new Size(imageSize.height * bounds.width / bounds.height, imageSize.height); |
| destinationSize = bounds; |
| break; |
| case ImageFit.none: |
| sourceSize = new Size(math.min(imageSize.width, bounds.width), |
| math.min(imageSize.height, bounds.height)); |
| destinationSize = sourceSize; |
| break; |
| case ImageFit.scaleDown: |
| sourceSize = imageSize; |
| destinationSize = bounds; |
| 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); |
| break; |
| } |
| // TODO(abarth): Implement |repeat|. |
| Paint paint = new Paint(); |
| if (colorFilter != null) |
| paint.setColorFilter(colorFilter); |
| double dx = (bounds.width - destinationSize.width) * positionX; |
| double dy = (bounds.height - destinationSize.height) * positionY; |
| Point destinationPosition = rect.topLeft + new Offset(dx, dy); |
| canvas.drawImageRect(image, Point.origin & sourceSize, destinationPosition & destinationSize, paint); |
| } |
| |
| typedef void BackgroundImageChangeListener(); |
| |
| class BackgroundImage { |
| final ImageFit fit; |
| final ImageRepeat repeat; |
| final sky.ColorFilter colorFilter; |
| |
| BackgroundImage({ |
| ImageResource image, |
| this.fit: ImageFit.scaleDown, |
| this.repeat: ImageRepeat.noRepeat, |
| this.colorFilter |
| }) : _imageResource = image; |
| |
| sky.Image _image; |
| sky.Image get image => _image; |
| |
| ImageResource _imageResource; |
| |
| final List<BackgroundImageChangeListener> _listeners = |
| new List<BackgroundImageChangeListener>(); |
| |
| void addChangeListener(BackgroundImageChangeListener listener) { |
| // 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); |
| _listeners.add(listener); |
| } |
| |
| void removeChangeListener(BackgroundImageChangeListener listener) { |
| _listeners.remove(listener); |
| // 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); |
| } |
| |
| void _handleImageChanged(sky.Image resolvedImage) { |
| if (resolvedImage == null) |
| return; |
| _image = resolvedImage; |
| final List<BackgroundImageChangeListener> localListeners = |
| new List<BackgroundImageChangeListener>.from(_listeners); |
| for (BackgroundImageChangeListener listener in localListeners) { |
| listener(); |
| } |
| } |
| |
| String toString() => 'BackgroundImage($fit, $repeat)'; |
| } |
| |
| enum Shape { rectangle, circle } |
| |
| // This must be immutable, because we won't notice when it changes |
| class BoxDecoration { |
| 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 |
| this.shape: Shape.rectangle |
| }); |
| |
| final Color backgroundColor; |
| final BackgroundImage backgroundImage; |
| final double borderRadius; |
| final Border border; |
| final List<BoxShadow> boxShadow; |
| final Gradient gradient; |
| final Shape shape; |
| |
| BoxDecoration scale(double factor) { |
| // TODO(abarth): Scale ALL the things. |
| return new BoxDecoration( |
| backgroundColor: lerpColor(null, backgroundColor, factor), |
| backgroundImage: backgroundImage, |
| border: border, |
| borderRadius: lerpNum(null, borderRadius, factor), |
| boxShadow: lerpListBoxShadow(null, boxShadow, factor), |
| gradient: gradient, |
| shape: shape |
| ); |
| } |
| |
| String toString([String prefix = '']) { |
| List<String> result = []; |
| 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) |
| result.add('${prefix}boxShadow: ${boxShadow.map((shadow) => shadow.toString())}'); |
| if (gradient != null) |
| result.add('${prefix}gradient: $gradient'); |
| if (shape != Shape.rectangle) |
| result.add('${prefix}shape: $shape'); |
| if (result.isEmpty) |
| return '${prefix}<no decorations specified>'; |
| return result.join('\n'); |
| } |
| } |
| |
| BoxDecoration lerpBoxDecoration(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( |
| backgroundColor: lerpColor(a.backgroundColor, b.backgroundColor, t), |
| backgroundImage: b.backgroundImage, |
| border: b.border, |
| borderRadius: lerpNum(a.borderRadius, b.borderRadius, t), |
| boxShadow: lerpListBoxShadow(a.boxShadow, b.boxShadow, t), |
| gradient: b.gradient, |
| shape: b.shape |
| ); |
| } |
| |
| class BoxPainter { |
| BoxPainter(BoxDecoration decoration) : _decoration = decoration { |
| assert(decoration != null); |
| } |
| |
| BoxDecoration _decoration; |
| BoxDecoration get decoration => _decoration; |
| void set decoration (BoxDecoration value) { |
| assert(value != null); |
| if (value == _decoration) |
| return; |
| _decoration = value; |
| _cachedBackgroundPaint = null; |
| } |
| |
| Paint _cachedBackgroundPaint; |
| Paint get _backgroundPaint { |
| if (_cachedBackgroundPaint == null) { |
| Paint paint = new Paint(); |
| |
| if (_decoration.backgroundColor != null) |
| paint.color = _decoration.backgroundColor; |
| |
| if (_decoration.boxShadow != null) { |
| var builder = new ShadowDrawLooperBuilder(); |
| for (BoxShadow boxShadow in _decoration.boxShadow) |
| builder.addShadow(boxShadow.offset, boxShadow.color, boxShadow.blur); |
| paint.setDrawLooper(builder.build()); |
| } |
| |
| if (_decoration.gradient != null) |
| paint.setShader(_decoration.gradient.createShader()); |
| |
| _cachedBackgroundPaint = paint; |
| } |
| |
| return _cachedBackgroundPaint; |
| } |
| |
| 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; |
| } |
| |
| void _paintBackgroundColor(sky.Canvas canvas, Rect rect) { |
| if (_decoration.backgroundColor != null || |
| _decoration.boxShadow != null || |
| _decoration.gradient != null) { |
| switch (_decoration.shape) { |
| case Shape.circle: |
| assert(_decoration.borderRadius == null); |
| Point center = rect.center; |
| double radius = rect.shortestSide / 2.0; |
| canvas.drawCircle(center, radius, _backgroundPaint); |
| break; |
| case Shape.rectangle: |
| if (_decoration.borderRadius == null) |
| canvas.drawRect(rect, _backgroundPaint); |
| else |
| canvas.drawRRect(new sky.RRect()..setRectXY(rect, _decoration.borderRadius, _decoration.borderRadius), _backgroundPaint); |
| break; |
| } |
| } |
| } |
| |
| void _paintBackgroundImage(sky.Canvas canvas, Rect rect) { |
| final BackgroundImage backgroundImage = _decoration.backgroundImage; |
| if (backgroundImage == null) |
| return; |
| sky.Image image = backgroundImage.image; |
| if (image == null) |
| return; |
| paintImage( |
| canvas: canvas, |
| rect: rect, |
| image: image, |
| colorFilter: backgroundImage.colorFilter, |
| fit: backgroundImage.fit, |
| repeat: backgroundImage.repeat |
| ); |
| } |
| |
| void _paintBorder(sky.Canvas canvas, Rect rect) { |
| if (_decoration.border == null) |
| return; |
| |
| if (_hasUniformBorder) { |
| if (_decoration.borderRadius != null) { |
| _paintBorderWithRadius(canvas, rect); |
| return; |
| } |
| if (_decoration.shape == Shape.circle) { |
| _paintBorderWithCircle(canvas, rect); |
| return; |
| } |
| } |
| |
| assert(_decoration.borderRadius == null); // TODO(abarth): Support non-uniform rounded borders. |
| assert(_decoration.shape == Shape.rectangle); // TODO(ianh): Support non-uniform borders on circles. |
| |
| 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); |
| } |
| |
| void _paintBorderWithRadius(sky.Canvas canvas, Rect rect) { |
| assert(_hasUniformBorder); |
| assert(_decoration.shape == Shape.rectangle); |
| Color color = _decoration.border.top.color; |
| double width = _decoration.border.top.width; |
| double radius = _decoration.borderRadius; |
| |
| sky.RRect outer = new sky.RRect()..setRectXY(rect, radius, radius); |
| sky.RRect inner = new sky.RRect()..setRectXY(rect.deflate(width), radius - width, radius - width); |
| canvas.drawDRRect(outer, inner, new Paint()..color = color); |
| } |
| |
| void _paintBorderWithCircle(sky.Canvas canvas, Rect rect) { |
| assert(_hasUniformBorder); |
| assert(_decoration.shape == Shape.circle); |
| assert(_decoration.borderRadius == null); |
| double width = _decoration.border.top.width; |
| Paint paint = new Paint() |
| ..color = _decoration.border.top.color |
| ..strokeWidth = width |
| ..setStyle(sky.PaintingStyle.stroke); |
| Point center = rect.center; |
| double radius = (rect.shortestSide - width) / 2.0; |
| canvas.drawCircle(center, radius, paint); |
| } |
| |
| void paint(sky.Canvas canvas, Rect rect) { |
| _paintBackgroundColor(canvas, rect); |
| _paintBackgroundImage(canvas, rect); |
| _paintBorder(canvas, rect); |
| } |
| } |