blob: 1bec305d844ba4b96541876d69f29ad487c9e9d0 [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:async';
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/lerp.dart';
import 'package:sky/painting/shadows.dart';
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;
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
});
String toString() =>
'LinearGradient($endPoints, $colors, $colorStops, $tileMode)';
sky.Shader createShader() {
return new sky.Gradient.linear(this.endPoints, this.colors, this.colorStops,
this.tileMode);
}
final List<Point> endPoints;
final List<Color> colors;
final List<double> colorStops;
final sky.TileMode tileMode;
}
class RadialGradient extends Gradient {
RadialGradient({
this.center,
this.radius,
this.colors,
this.colorStops,
this.tileMode: sky.TileMode.clamp
});
String toString() =>
'RadialGradient($center, $radius, $colors, $colorStops, $tileMode)';
sky.Shader createShader() {
return new sky.Gradient.radial(this.center, this.radius, this.colors,
this.colorStops, this.tileMode);
}
final Point center;
final double radius;
final List<Color> colors;
final List<double> colorStops;
final sky.TileMode tileMode;
}
enum BackgroundFit { fill, contain, cover, none, scaleDown }
enum BackgroundRepeat { repeat, repeatX, repeatY, noRepeat }
// TODO(jackson): We should abstract this out into a separate class
// that handles the image caching and so forth, which has callbacks
// for "size changed" and "image changed". This would also enable us
// to do animated images.
class BackgroundImage {
final BackgroundFit fit;
final BackgroundRepeat repeat;
BackgroundImage({
Future<sky.Image> image,
this.fit: BackgroundFit.scaleDown,
this.repeat: BackgroundRepeat.noRepeat
}) {
image.then((resolvedImage) {
if (resolvedImage == null)
return;
_image = resolvedImage;
_size = new Size(resolvedImage.width.toDouble(), resolvedImage.height.toDouble());
for (Function listener in _listeners) {
listener();
}
});
}
sky.Image _image;
sky.Image get image => _image;
Size _size;
final List<Function> _listeners = new List<Function>();
void addChangeListener(Function listener) {
_listeners.add(listener);
}
void removeChangeListener(Function listener) {
_listeners.remove(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) {
if (_decoration.backgroundImage == null)
return;
sky.Image image = _decoration.backgroundImage.image;
if (image != null) {
Size bounds = rect.size;
Size imageSize = _decoration.backgroundImage._size;
Size src;
Size dst;
switch(_decoration.backgroundImage.fit) {
case BackgroundFit.fill:
src = imageSize;
dst = bounds;
break;
case BackgroundFit.contain:
src = imageSize;
if (bounds.width / bounds.height > src.width / src.height) {
dst = new Size(bounds.width, src.height * bounds.width / src.width);
} else {
dst = new Size(src.width * bounds.height / src.height, bounds.height);
}
break;
case BackgroundFit.cover:
if (bounds.width / bounds.height > imageSize.width / imageSize.height) {
src = new Size(imageSize.width, imageSize.width * bounds.height / bounds.width);
} else {
src = new Size(imageSize.height * bounds.width / bounds.height, imageSize.height);
}
dst = bounds;
break;
case BackgroundFit.none:
src = new Size(math.min(imageSize.width, bounds.width),
math.min(imageSize.height, bounds.height));
dst = src;
break;
case BackgroundFit.scaleDown:
src = imageSize;
dst = bounds;
if (src.height > dst.height) {
dst = new Size(src.width * dst.height / src.height, src.height);
}
if (src.width > dst.width) {
dst = new Size(dst.width, src.height * dst.width / src.width);
}
break;
}
canvas.drawImageRect(image, Point.origin & src, rect.topLeft & dst, new Paint());
}
}
void _paintBorder(sky.Canvas canvas, Rect rect) {
if (_decoration.border == null)
return;
if (_hasUniformBorder && _decoration.borderRadius != null) {
_paintBorderWithRadius(canvas, rect);
return;
}
assert(_decoration.borderRadius == null); // TODO(abarth): Support non-uniform rounded borders.
assert(_decoration.shape == Shape.rectangle); // TODO(ianh): Support 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);
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 paint(sky.Canvas canvas, Rect rect) {
_paintBackgroundColor(canvas, rect);
_paintBackgroundImage(canvas, rect);
_paintBorder(canvas, rect);
}
}