blob: cb44b484c0ba85ac748d35805701db6804db31ac [file] [log] [blame]
// 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);
}
}