610 lines
19 KiB
Dart
610 lines
19 KiB
Dart
// 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);
|
|
}
|
|
}
|