// 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 'package:sky/base/debug.dart'; import 'package:sky/painting/box_painter.dart'; import 'package:sky/painting/text_style.dart'; import 'package:sky/rendering/object.dart'; import 'package:vector_math/vector_math.dart'; export 'package:sky/painting/box_painter.dart'; export 'package:sky/painting/text_style.dart' show TextBaseline; // GENERIC BOX RENDERING // Anything that has a concept of x, y, width, height is going to derive from this // This class should only be used in debug builds class _DebugSize extends Size { _DebugSize(Size source, this._owner, this._canBeUsedByParent): super.copy(source); final RenderBox _owner; final bool _canBeUsedByParent; } 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 BoxConstraints extends Constraints { const BoxConstraints({ this.minWidth: 0.0, this.maxWidth: double.INFINITY, this.minHeight: 0.0, this.maxHeight: double.INFINITY }); BoxConstraints.tight(Size size) : minWidth = size.width, maxWidth = size.width, minHeight = size.height, maxHeight = size.height; const BoxConstraints.tightFor({ double width, double height }): minWidth = width != null ? width : 0.0, maxWidth = width != null ? width : double.INFINITY, minHeight = height != null ? height : 0.0, maxHeight = height != null ? height : double.INFINITY; BoxConstraints.loose(Size size) : minWidth = 0.0, maxWidth = size.width, minHeight = 0.0, maxHeight = size.height; const BoxConstraints.expandWidth({ this.maxHeight: double.INFINITY }): minWidth = double.INFINITY, maxWidth = double.INFINITY, minHeight = 0.0; const BoxConstraints.expandHeight({ this.maxWidth: double.INFINITY }): minWidth = 0.0, minHeight = double.INFINITY, maxHeight = double.INFINITY; static const BoxConstraints expand = const BoxConstraints( minWidth: double.INFINITY, maxWidth: double.INFINITY, minHeight: double.INFINITY, maxHeight: double.INFINITY ); BoxConstraints deflate(EdgeDims edges) { assert(edges != null); double horizontal = edges.left + edges.right; double vertical = edges.top + edges.bottom; return new BoxConstraints( minWidth: math.max(0.0, minWidth - horizontal), maxWidth: maxWidth - horizontal, minHeight: math.max(0.0, minHeight - vertical), maxHeight: maxHeight - vertical ); } BoxConstraints loosen() { return new BoxConstraints( minWidth: 0.0, maxWidth: maxWidth, minHeight: 0.0, maxHeight: maxHeight ); } BoxConstraints apply(BoxConstraints constraints) { return new BoxConstraints( minWidth: clamp(min: constraints.minWidth, max: constraints.maxWidth, value: minWidth), maxWidth: clamp(min: constraints.minWidth, max: constraints.maxWidth, value: maxWidth), minHeight: clamp(min: constraints.minHeight, max: constraints.maxHeight, value: minHeight), maxHeight: clamp(min: constraints.minHeight, max: constraints.maxHeight, value: maxHeight) ); } BoxConstraints applyWidth(double width) { return new BoxConstraints(minWidth: math.max(math.min(maxWidth, width), minWidth), maxWidth: math.max(math.min(maxWidth, width), minWidth), minHeight: minHeight, maxHeight: maxHeight); } BoxConstraints applyMinWidth(double newMinWidth) { return new BoxConstraints(minWidth: math.max(minWidth, newMinWidth), maxWidth: math.max(maxWidth, newMinWidth), minHeight: minHeight, maxHeight: maxHeight); } BoxConstraints applyMaxWidth(double newMaxWidth) { return new BoxConstraints(minWidth: minWidth, maxWidth: math.min(maxWidth, newMaxWidth), minHeight: minHeight, maxHeight: maxHeight); } BoxConstraints applyHeight(double height) { return new BoxConstraints(minWidth: minWidth, maxWidth: maxWidth, minHeight: math.max(math.min(maxHeight, height), minHeight), maxHeight: math.max(math.min(maxHeight, height), minHeight)); } BoxConstraints applyMinHeight(double newMinHeight) { return new BoxConstraints(minWidth: minWidth, maxWidth: maxWidth, minHeight: math.max(minHeight, newMinHeight), maxHeight: math.max(maxHeight, newMinHeight)); } BoxConstraints applyMaxHeight(double newMaxHeight) { return new BoxConstraints(minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: math.min(maxHeight, newMaxHeight)); } BoxConstraints widthConstraints() => new BoxConstraints(minWidth: minWidth, maxWidth: maxWidth); BoxConstraints heightConstraints() => new BoxConstraints(minHeight: minHeight, maxHeight: maxHeight); final double minWidth; final double maxWidth; final double minHeight; final double maxHeight; double constrainWidth([double width = double.INFINITY]) { return clamp(min: minWidth, max: maxWidth, value: width); } double constrainHeight([double height = double.INFINITY]) { return clamp(min: minHeight, max: maxHeight, value: height); } Size constrain(Size size) { Size result = new Size(constrainWidth(size.width), constrainHeight(size.height)); if (size is _DebugSize) result = new _DebugSize(result, size._owner, size._canBeUsedByParent); return result; } Size get biggest => new Size(constrainWidth(), constrainHeight()); Size get smallest => new Size(constrainWidth(0.0), constrainHeight(0.0)); bool get isInfinite => maxWidth >= double.INFINITY && maxHeight >= double.INFINITY; bool get hasTightWidth => minWidth >= maxWidth; bool get hasTightHeight => minHeight >= maxHeight; bool get isTight => hasTightWidth && hasTightHeight; bool contains(Size size) { return (minWidth <= size.width) && (size.width <= math.max(minWidth, maxWidth)) && (minHeight <= size.height) && (size.height <= math.max(minHeight, maxHeight)); } bool operator ==(other) { if (identical(this, other)) return true; return other is BoxConstraints && minWidth == other.minWidth && maxWidth == other.maxWidth && minHeight == other.minHeight && maxHeight == other.maxHeight; } int get hashCode { int value = 373; value = 37 * value + minWidth.hashCode; value = 37 * value + maxWidth.hashCode; value = 37 * value + minHeight.hashCode; value = 37 * value + maxHeight.hashCode; return value; } String toString() => "BoxConstraints($minWidth<=w<$maxWidth, $minHeight<=h<$maxHeight)"; } class BoxHitTestEntry extends HitTestEntry { const BoxHitTestEntry(HitTestTarget target, this.localPosition) : super(target); final Point localPosition; } class BoxParentData extends ParentData { Point _position = Point.origin; Point get position => _position; void set position(Point value) { assert(RenderObject.debugDoingLayout); _position = value; } String toString() => 'position=$position'; } abstract class RenderBox extends RenderObject { void setupParentData(RenderObject child) { if (child.parentData is! BoxParentData) child.parentData = new BoxParentData(); } // getMinIntrinsicWidth() should return the minimum width that this box could // be without failing to render its contents within itself. double getMinIntrinsicWidth(BoxConstraints constraints) { return constraints.constrainWidth(0.0); } // getMaxIntrinsicWidth() should return the smallest width beyond which // increasing the width never decreases the height. double getMaxIntrinsicWidth(BoxConstraints constraints) { return constraints.constrainWidth(0.0); } // getMinIntrinsicHeight() should return the minimum height that this box could // be without failing to render its contents within itself. double getMinIntrinsicHeight(BoxConstraints constraints) { return constraints.constrainHeight(0.0); } // getMaxIntrinsicHeight should return the smallest height beyond which // increasing the height never decreases the width. // If the layout algorithm used is width-in-height-out, i.e. the height // depends on the width and not vice versa, then this will return the same // as getMinIntrinsicHeight(). double getMaxIntrinsicHeight(BoxConstraints constraints) { return constraints.constrainHeight(0.0); } Map<TextBaseline, double> _cachedBaselines; bool _ancestorUsesBaseline = false; static bool _debugDoingBaseline = false; static bool _debugSetDoingBaseline(bool value) { _debugDoingBaseline = value; return true; } // getDistanceToBaseline() returns the distance from the // y-coordinate of the position of the box to the y-coordinate of // the first given baseline in the box's contents. This is used by // certain layout models to align adjacent boxes on a common // baseline, regardless of padding, font size differences, etc. If // there is no baseline, and the 'onlyReal' argument was not set to // true, then it returns the distance from the y-coordinate of the // position of the box to the y-coordinate of the bottom of the box, // i.e., the height of the box. Only call this after layout has been // performed. You are only allowed to call this from the parent of // this node during that parent's performLayout() or paint(). double getDistanceToBaseline(TextBaseline baseline, { bool onlyReal: false }) { assert(!needsLayout); assert(!_debugDoingBaseline); final parent = this.parent; // TODO(ianh): Remove this once the analyzer is cleverer assert(parent is RenderObject); assert(() { if (RenderObject.debugDoingLayout) return (RenderObject.debugActiveLayout == parent) && parent.debugDoingThisLayout; if (RenderObject.debugDoingPaint) return ((RenderObject.debugActivePaint == parent) && parent.debugDoingThisPaint) || ((RenderObject.debugActivePaint == this) && debugDoingThisPaint); return false; }); assert(_debugSetDoingBaseline(true)); double result = getDistanceToActualBaseline(baseline); assert(_debugSetDoingBaseline(false)); assert(parent == this.parent); // TODO(ianh): Remove this once the analyzer is cleverer if (result == null && !onlyReal) return size.height; return result; } // getDistanceToActualBaseline() must only be called from // getDistanceToBaseline() and computeDistanceToActualBaseline(). Do // not call it directly from outside those two methods. It just // calls computeDistanceToActualBaseline() and caches the result. double getDistanceToActualBaseline(TextBaseline baseline) { assert(_debugDoingBaseline); _ancestorUsesBaseline = true; if (_cachedBaselines == null) _cachedBaselines = new Map<TextBaseline, double>(); _cachedBaselines.putIfAbsent(baseline, () => computeDistanceToActualBaseline(baseline)); return _cachedBaselines[baseline]; } // computeDistanceToActualBaseline() should return the distance from // the y-coordinate of the position of the box to the y-coordinate // of the first given baseline in the box's contents, if any, or // null otherwise. This is the method that you should override in // subclasses. This method (computeDistanceToActualBaseline()) // should not be called directly. Use getDistanceToBaseline() if you // need to know the baseline of a child from performLayout(). If you // need the baseline during paint, cache it during performLayout(). // Use getDistanceToActualBaseline() if you are implementing // computeDistanceToActualBaseline() and need to defer to a child. double computeDistanceToActualBaseline(TextBaseline baseline) { assert(_debugDoingBaseline); return null; } BoxConstraints get constraints => super.constraints; bool debugDoesMeetConstraints() { assert(constraints != null); assert(_size != null); assert(!_size.isInfinite); bool result = constraints.contains(_size); if (!result) print("${this.runtimeType} does not meet its constraints. Constraints: $constraints, size: $_size"); return result; } void markNeedsLayout() { if (_cachedBaselines != null && _cachedBaselines.isNotEmpty) { // if we have cached data, then someone must have used our data assert(_ancestorUsesBaseline); final parent = this.parent; // TODO(ianh): Remove this once the analyzer is cleverer assert(parent is RenderObject); parent.markNeedsLayout(); assert(parent == this.parent); // TODO(ianh): Remove this once the analyzer is cleverer // Now that they're dirty, we can forget that they used the // baseline. If they use it again, then we'll set the bit // again, and if we get dirty again, we'll notify them again. _ancestorUsesBaseline = false; _cachedBaselines.clear(); } else { // if we've never cached any data, then nobody can have used it assert(!_ancestorUsesBaseline); } super.markNeedsLayout(); } void performResize() { // default behaviour for subclasses that have sizedByParent = true size = constraints.constrain(Size.zero); assert(!size.isInfinite); } void performLayout() { // descendants have to either override performLayout() to set both // width and height and lay out children, or, set sizedByParent to // true so that performResize()'s logic above does its thing. assert(sizedByParent); } bool hitTest(HitTestResult result, { Point position }) { hitTestChildren(result, position: position); result.add(new BoxHitTestEntry(this, position)); return true; } void hitTestChildren(HitTestResult result, { Point position }) { } // TODO(ianh): move size up to before constraints // TODO(ianh): In non-debug builds, this should all just be: // Size size = Size.zero; // In debug builds, however: Size _size = Size.zero; Size get size { if (_size is _DebugSize) { final _DebugSize _size = this._size; // TODO(ianh): Remove this once the analyzer is cleverer assert(_size._owner == this); if (RenderObject.debugActiveLayout != null) { // we are always allowed to access our own size (for print debugging and asserts if nothing else) // other than us, the only object that's allowed to read our size is our parent, if they're said they will // if you hit this assert trying to access a child's size, pass parentUsesSize: true in layout() assert(debugDoingThisResize || debugDoingThisLayout || (RenderObject.debugActiveLayout == parent && _size._canBeUsedByParent)); } assert(_size == this._size); // TODO(ianh): Remove this once the analyzer is cleverer } return _size; } void set size(Size value) { assert((sizedByParent && debugDoingThisResize) || (!sizedByParent && debugDoingThisLayout)); if (value is _DebugSize) { assert(value._canBeUsedByParent); assert(value._owner.parent == this); } _size = inDebugBuild ? new _DebugSize(value, this, debugCanParentUseSize) : value; } void applyPaintTransform(Matrix4 transform) { if (parentData is BoxParentData) { Point position = (parentData as BoxParentData).position; transform.translate(position.x, position.y); } } static Point _transformPoint(Matrix4 transform, Point point) { Vector3 position3 = new Vector3(point.x, point.y, 0.0); Vector3 transformed3 = transform.transform3(position3); return new Point(transformed3.x, transformed3.y); } Point globalToLocal(Point point) { assert(attached); Matrix4 transform = new Matrix4.identity(); RenderObject renderer = this; while(renderer != null) { renderer.applyPaintTransform(transform); renderer = renderer.parent; } /* double det = */ transform.invert(); // TODO(abarth): Check the determinant for degeneracy. return _transformPoint(transform, point); } Point localToGlobal(Point point) { List <RenderObject> renderers = <RenderObject>[]; for (RenderObject renderer = this; renderer != null; renderer = renderer.parent) renderers.add(renderer); Matrix4 transform = new Matrix4.identity(); for (RenderObject renderer in renderers.reversed) renderer.applyPaintTransform(transform); return _transformPoint(transform, point); } Rect get paintBounds => Point.origin & size; void debugPaint(PaintingCanvas canvas, Offset offset) { if (debugPaintSizeEnabled) debugPaintSize(canvas, offset); if (debugPaintBaselinesEnabled) debugPaintBaselines(canvas, offset); } void debugPaintSize(PaintingCanvas canvas, Offset offset) { Paint paint = new Paint(); paint.setStyle(sky.PaintingStyle.stroke); paint.strokeWidth = 1.0; paint.color = debugPaintSizeColor; canvas.drawRect(offset & size, paint); } void debugPaintBaselines(PaintingCanvas canvas, Offset offset) { Paint paint = new Paint(); paint.setStyle(sky.PaintingStyle.stroke); paint.strokeWidth = 0.25; Path path; // ideographic baseline double baselineI = getDistanceToBaseline(TextBaseline.ideographic, onlyReal: true); if (baselineI != null) { paint.color = debugPaintIdeographicBaselineColor; path = new Path(); path.moveTo(offset.dx, offset.dy + baselineI); path.lineTo(offset.dx + size.width, offset.dy + baselineI); canvas.drawPath(path, paint); } // alphabetic baseline double baselineA = getDistanceToBaseline(TextBaseline.alphabetic, onlyReal: true); if (baselineA != null) { paint.color = debugPaintAlphabeticBaselineColor; path = new Path(); path.moveTo(offset.dx, offset.dy + baselineA); path.lineTo(offset.dx + size.width, offset.dy + baselineA); canvas.drawPath(path, paint); } } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}size: ${size}\n'; } class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> { // ProxyBox assumes the child will be at 0,0 and will have the same size RenderProxyBox([RenderBox child = null]) { this.child = child; } double getMinIntrinsicWidth(BoxConstraints constraints) { if (child != null) return child.getMinIntrinsicWidth(constraints); return super.getMinIntrinsicWidth(constraints); } double getMaxIntrinsicWidth(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicWidth(constraints); return super.getMaxIntrinsicWidth(constraints); } double getMinIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMinIntrinsicHeight(constraints); return super.getMinIntrinsicHeight(constraints); } double getMaxIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicHeight(constraints); return super.getMaxIntrinsicHeight(constraints); } double computeDistanceToActualBaseline(TextBaseline baseline) { if (child != null) return child.getDistanceToActualBaseline(baseline); return super.computeDistanceToActualBaseline(baseline); } void performLayout() { if (child != null) { child.layout(constraints, parentUsesSize: true); size = child.size; } else { performResize(); } } void hitTestChildren(HitTestResult result, { Point position }) { if (child != null) child.hitTest(result, position: position); else super.hitTestChildren(result, position: position); } void paint(PaintingCanvas canvas, Offset offset) { if (child != null) canvas.paintChild(child, offset.toPoint()); } } class RenderConstrainedBox extends RenderProxyBox { RenderConstrainedBox({ RenderBox child, BoxConstraints additionalConstraints }) : super(child), _additionalConstraints = additionalConstraints { assert(additionalConstraints != null); } BoxConstraints _additionalConstraints; BoxConstraints get additionalConstraints => _additionalConstraints; void set additionalConstraints (BoxConstraints value) { assert(value != null); if (_additionalConstraints == value) return; _additionalConstraints = value; markNeedsLayout(); } double getMinIntrinsicWidth(BoxConstraints constraints) { if (child != null) return child.getMinIntrinsicWidth(_additionalConstraints.apply(constraints)); return _additionalConstraints.apply(constraints).constrainWidth(0.0); } double getMaxIntrinsicWidth(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicWidth(_additionalConstraints.apply(constraints)); return _additionalConstraints.apply(constraints).constrainWidth(0.0); } double getMinIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMinIntrinsicHeight(_additionalConstraints.apply(constraints)); return _additionalConstraints.apply(constraints).constrainHeight(0.0); } double getMaxIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicHeight(_additionalConstraints.apply(constraints)); return _additionalConstraints.apply(constraints).constrainHeight(0.0); } void performLayout() { if (child != null) { child.layout(_additionalConstraints.apply(constraints), parentUsesSize: true); size = child.size; } else { size = _additionalConstraints.apply(constraints).constrain(Size.zero); } } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}additionalConstraints: ${additionalConstraints}\n'; } class RenderAspectRatio extends RenderProxyBox { RenderAspectRatio({ RenderBox child, double aspectRatio }) : super(child), _aspectRatio = aspectRatio { assert(_aspectRatio != null); } double _aspectRatio; double get aspectRatio => _aspectRatio; void set aspectRatio (double value) { assert(value != null); if (_aspectRatio == value) return; _aspectRatio = value; markNeedsLayout(); } double getMinIntrinsicHeight(BoxConstraints constraints) { return _applyAspectRatio(constraints).height; } double getMaxIntrinsicHeight(BoxConstraints constraints) { return _applyAspectRatio(constraints).height; } Size _applyAspectRatio(BoxConstraints constraints) { double width = constraints.constrainWidth(); double height = constraints.constrainHeight(width / _aspectRatio); return new Size(width, height); } bool get sizedByParent => true; void performResize() { size = _applyAspectRatio(constraints); } void performLayout() { if (child != null) child.layout(new BoxConstraints.tight(size)); } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}aspectRatio: ${aspectRatio}\n'; } class RenderShrinkWrapWidth extends RenderProxyBox { // This class will attempt to size its child to the child's maximum // intrinsic width, snapped to a multiple of the stepWidth, if one // is provided, and given the provided constraints; and will then // adopt the child's resulting dimensions. // Note: laying out this class is relatively expensive. Avoid using // it where possible. RenderShrinkWrapWidth({ double stepWidth, double stepHeight, RenderBox child }) : _stepWidth = stepWidth, _stepHeight = stepHeight, super(child); double _stepWidth; double get stepWidth => _stepWidth; void set stepWidth(double value) { if (value == _stepWidth) return; _stepWidth = value; markNeedsLayout(); } double _stepHeight; double get stepHeight => _stepHeight; void set stepHeight(double value) { if (value == _stepHeight) return; _stepHeight = value; markNeedsLayout(); } static double applyStep(double input, double step) { if (step == null) return input; return (input / step).ceil() * step; } BoxConstraints _getInnerConstraints(BoxConstraints constraints) { if (constraints.hasTightWidth) return constraints; double width = child.getMaxIntrinsicWidth(constraints); assert(width == constraints.constrainWidth(width)); return constraints.applyWidth(applyStep(width, _stepWidth)); } double getMinIntrinsicWidth(BoxConstraints constraints) { return getMaxIntrinsicWidth(constraints); } double getMaxIntrinsicWidth(BoxConstraints constraints) { if (child == null) return constraints.constrainWidth(0.0); double childResult = child.getMaxIntrinsicWidth(constraints); return constraints.constrainWidth(applyStep(childResult, _stepWidth)); } double getMinIntrinsicHeight(BoxConstraints constraints) { if (child == null) return constraints.constrainWidth(0.0); double childResult = child.getMinIntrinsicHeight(_getInnerConstraints(constraints)); return constraints.constrainHeight(applyStep(childResult, _stepHeight)); } double getMaxIntrinsicHeight(BoxConstraints constraints) { if (child == null) return constraints.constrainWidth(0.0); double childResult = child.getMaxIntrinsicHeight(_getInnerConstraints(constraints)); return constraints.constrainHeight(applyStep(childResult, _stepHeight)); } void performLayout() { if (child != null) { BoxConstraints childConstraints = _getInnerConstraints(constraints); if (_stepHeight != null) childConstraints.applyHeight(getMaxIntrinsicHeight(childConstraints)); child.layout(childConstraints, parentUsesSize: true); size = child.size; } else { performResize(); } } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}stepWidth: ${stepWidth}\n${prefix}stepHeight: ${stepHeight}\n'; } class RenderOpacity extends RenderProxyBox { RenderOpacity({ RenderBox child, double opacity }) : this._opacity = opacity, super(child) { assert(opacity >= 0.0 && opacity <= 1.0); } double _opacity; double get opacity => _opacity; void set opacity (double value) { assert(value != null); assert(value >= 0.0 && value <= 1.0); if (_opacity == value) return; _opacity = value; _cachedPaint = null; markNeedsPaint(); } int get _alpha => (_opacity * 255).round(); Paint _cachedPaint; Paint get _paint { if (_cachedPaint == null) { _cachedPaint = new Paint() ..color = new Color.fromARGB(_alpha, 0, 0, 0) ..setTransferMode(sky.TransferMode.srcOver); } return _cachedPaint; } void paint(PaintingCanvas canvas, Offset offset) { if (child != null) { int a = _alpha; if (a == 0) return; if (a == 255) { canvas.paintChild(child, offset.toPoint()); return; } canvas.saveLayer(null, _paint); canvas.paintChild(child, offset.toPoint()); canvas.restore(); } } } class RenderColorFilter extends RenderProxyBox { RenderColorFilter({ RenderBox child, Color color, sky.TransferMode transferMode }) : _color = color, _transferMode = transferMode, super(child) { } Color _color; Color get color => _color; void set color (Color value) { assert(value != null); if (_color == value) return; _color = value; _cachedPaint = null; markNeedsPaint(); } sky.TransferMode _transferMode; sky.TransferMode get transferMode => _transferMode; void set transferMode (sky.TransferMode value) { assert(value != null); if (_transferMode == value) return; _transferMode = value; _cachedPaint = null; markNeedsPaint(); } Paint _cachedPaint; Paint get _paint { if (_cachedPaint == null) { _cachedPaint = new Paint() ..setColorFilter(new sky.ColorFilter.mode(_color, _transferMode)); } return _cachedPaint; } void paint(PaintingCanvas canvas, Offset offset) { if (child != null) { canvas.saveLayer(offset & size, _paint); canvas.paintChild(child, offset.toPoint()); canvas.restore(); } } } class RenderClipRect extends RenderProxyBox { RenderClipRect({ RenderBox child }) : super(child); void paint(PaintingCanvas canvas, Offset offset) { if (child != null) { canvas.save(); canvas.clipRect(offset & size); canvas.paintChild(child, offset.toPoint()); canvas.restore(); } } } class RenderClipRRect extends RenderProxyBox { RenderClipRRect({ RenderBox child, double xRadius, double yRadius }) : _xRadius = xRadius, _yRadius = yRadius, super(child) { assert(_xRadius != null); assert(_yRadius != null); } double _xRadius; double get xRadius => _xRadius; void set xRadius (double value) { assert(value != null); if (_xRadius == value) return; _xRadius = value; markNeedsPaint(); } double _yRadius; double get yRadius => _yRadius; void set yRadius (double value) { assert(value != null); if (_yRadius == value) return; _yRadius = value; markNeedsPaint(); } void paint(PaintingCanvas canvas, Offset offset) { if (child != null) { Rect rect = offset & size; canvas.saveLayer(rect, new Paint()); sky.RRect rrect = new sky.RRect()..setRectXY(rect, xRadius, yRadius); canvas.clipRRect(rrect); canvas.paintChild(child, offset.toPoint()); canvas.restore(); } } } class RenderClipOval extends RenderProxyBox { RenderClipOval({ RenderBox child }) : super(child); void paint(PaintingCanvas canvas, Offset offset) { if (child != null) { Rect rect = offset & size; canvas.saveLayer(rect, new Paint()); Path path = new Path(); path.addOval(rect); canvas.clipPath(path); canvas.paintChild(child, offset.toPoint()); canvas.restore(); } } } abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> { // Abstract class for one-child-layout render boxes RenderShiftedBox(RenderBox child) { this.child = child; } double getMinIntrinsicWidth(BoxConstraints constraints) { if (child != null) return child.getMinIntrinsicWidth(constraints); return super.getMinIntrinsicWidth(constraints); } double getMaxIntrinsicWidth(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicWidth(constraints); return super.getMaxIntrinsicWidth(constraints); } double getMinIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMinIntrinsicHeight(constraints); return super.getMinIntrinsicHeight(constraints); } double getMaxIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicHeight(constraints); return super.getMaxIntrinsicHeight(constraints); } double computeDistanceToActualBaseline(TextBaseline baseline) { double result; if (child != null) { assert(!needsLayout); result = child.getDistanceToActualBaseline(baseline); assert(child.parentData is BoxParentData); if (result != null) result += child.parentData.position.y; } else { result = super.computeDistanceToActualBaseline(baseline); } return result; } void paint(PaintingCanvas canvas, Offset offset) { if (child != null) canvas.paintChild(child, child.parentData.position + offset); } void hitTestChildren(HitTestResult result, { Point position }) { if (child != null) { assert(child.parentData is BoxParentData); Rect childBounds = child.parentData.position & child.size; if (childBounds.contains(position)) { child.hitTest(result, position: new Point(position.x - child.parentData.position.x, position.y - child.parentData.position.y)); } } } } class RenderPadding extends RenderShiftedBox { RenderPadding({ EdgeDims padding, RenderBox child }) : super(child) { assert(padding != null); this.padding = padding; } EdgeDims _padding; EdgeDims get padding => _padding; void set padding (EdgeDims value) { assert(value != null); if (_padding == value) return; _padding = value; markNeedsLayout(); } double getMinIntrinsicWidth(BoxConstraints constraints) { double totalPadding = padding.left + padding.right; if (child != null) return child.getMinIntrinsicWidth(constraints.deflate(padding)) + totalPadding; return constraints.constrainWidth(totalPadding); } double getMaxIntrinsicWidth(BoxConstraints constraints) { double totalPadding = padding.left + padding.right; if (child != null) return child.getMaxIntrinsicWidth(constraints.deflate(padding)) + totalPadding; return constraints.constrainWidth(totalPadding); } double getMinIntrinsicHeight(BoxConstraints constraints) { double totalPadding = padding.top + padding.bottom; if (child != null) return child.getMinIntrinsicHeight(constraints.deflate(padding)) + totalPadding; return constraints.constrainHeight(totalPadding); } double getMaxIntrinsicHeight(BoxConstraints constraints) { double totalPadding = padding.top + padding.bottom; if (child != null) return child.getMaxIntrinsicHeight(constraints.deflate(padding)) + totalPadding; return constraints.constrainHeight(totalPadding); } void performLayout() { assert(padding != null); if (child == null) { size = constraints.constrain(new Size( padding.left + padding.right, padding.top + padding.bottom )); return; } BoxConstraints innerConstraints = constraints.deflate(padding); child.layout(innerConstraints, parentUsesSize: true); assert(child.parentData is BoxParentData); child.parentData.position = new Point(padding.left, padding.top); size = constraints.constrain(new Size( padding.left + child.size.width + padding.right, padding.top + child.size.height + padding.bottom )); } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}padding: ${padding}\n'; } class RenderPositionedBox extends RenderShiftedBox { // This box aligns a child box within itself. It's only useful for // children that don't always size to fit their parent. For example, // to align a box at the bottom right, you would pass this box a // tight constraint that is bigger than the child's natural size, // with horizontal and vertical set to 1.0. RenderPositionedBox({ RenderBox child, double horizontal: 0.5, double vertical: 0.5 }) : _horizontal = horizontal, _vertical = vertical, super(child) { assert(horizontal != null); assert(vertical != null); } double _horizontal; double get horizontal => _horizontal; void set horizontal (double value) { assert(value != null); if (_horizontal == value) return; _horizontal = value; markNeedsLayout(); } double _vertical; double get vertical => _vertical; void set vertical (double value) { assert(value != null); if (_vertical == value) return; _vertical = value; markNeedsLayout(); } void performLayout() { if (child != null) { child.layout(constraints.loosen(), parentUsesSize: true); size = constraints.constrain(child.size); assert(child.parentData is BoxParentData); Offset delta = size - child.size; child.parentData.position = (delta.scale(horizontal, vertical)).toPoint(); } else { performResize(); } } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}horizontal: ${horizontal}\n${prefix}vertical: ${vertical}\n'; } class RenderBaseline extends RenderShiftedBox { RenderBaseline({ RenderBox child, double baseline, TextBaseline baselineType }) : _baseline = baseline, _baselineType = baselineType, super(child) { assert(baseline != null); assert(baselineType != null); } double _baseline; double get baseline => _baseline; void set baseline (double value) { assert(value != null); if (_baseline == value) return; _baseline = value; markNeedsLayout(); } TextBaseline _baselineType; TextBaseline get baselineType => _baselineType; void set baselineType (TextBaseline value) { assert(value != null); if (_baselineType == value) return; _baselineType = value; markNeedsLayout(); } void performLayout() { if (child != null) { child.layout(constraints.loosen(), parentUsesSize: true); size = constraints.constrain(child.size); assert(child.parentData is BoxParentData); double delta = baseline - child.getDistanceToBaseline(baselineType); child.parentData.position = new Point(0.0, delta); } else { performResize(); } } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}baseline: ${baseline}\nbaselineType: ${baselineType}'; } enum ScrollDirection { horizontal, vertical, both } class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> { RenderViewport({ RenderBox child, Offset scrollOffset, ScrollDirection scrollDirection: ScrollDirection.vertical }) : _scrollOffset = scrollOffset, _scrollDirection = scrollDirection { assert(_offsetIsSane(scrollOffset, scrollDirection)); this.child = child; } bool _offsetIsSane(Offset offset, ScrollDirection direction) { switch (direction) { case ScrollDirection.both: return true; case ScrollDirection.horizontal: return offset.dy == 0.0; case ScrollDirection.vertical: return offset.dx == 0.0; } } Offset _scrollOffset; Offset get scrollOffset => _scrollOffset; void set scrollOffset(Offset value) { if (value == _scrollOffset) return; assert(_offsetIsSane(value, scrollDirection)); _scrollOffset = value; markNeedsPaint(); } ScrollDirection _scrollDirection; ScrollDirection get scrollDirection => _scrollDirection; void set scrollDirection(ScrollDirection value) { if (value == _scrollDirection) return; assert(_offsetIsSane(scrollOffset, value)); _scrollDirection = value; markNeedsLayout(); } BoxConstraints _getInnerConstraints(BoxConstraints constraints) { BoxConstraints innerConstraints; switch (scrollDirection) { case ScrollDirection.both: innerConstraints = new BoxConstraints(); break; case ScrollDirection.horizontal: innerConstraints = constraints.heightConstraints(); break; case ScrollDirection.vertical: innerConstraints = constraints.widthConstraints(); break; } return innerConstraints; } double getMinIntrinsicWidth(BoxConstraints constraints) { if (child != null) return child.getMinIntrinsicWidth(_getInnerConstraints(constraints)); return super.getMinIntrinsicWidth(constraints); } double getMaxIntrinsicWidth(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicWidth(_getInnerConstraints(constraints)); return super.getMaxIntrinsicWidth(constraints); } double getMinIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMinIntrinsicHeight(_getInnerConstraints(constraints)); return super.getMinIntrinsicHeight(constraints); } double getMaxIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicHeight(_getInnerConstraints(constraints)); return super.getMaxIntrinsicHeight(constraints); } // We don't override computeDistanceToActualBaseline(), because we // want the default behaviour (returning null). Otherwise, as you // scroll the RenderViewport, it would shift in its parent if the // parent was baseline-aligned, which makes no sense. void performLayout() { if (child != null) { child.layout(_getInnerConstraints(constraints), parentUsesSize: true); size = constraints.constrain(child.size); assert(child.parentData is BoxParentData); child.parentData.position = Point.origin; } else { performResize(); } } Offset get _scrollOffsetRoundedToIntegerDevicePixels { double devicePixelRatio = sky.view.devicePixelRatio; int dxInDevicePixels = (scrollOffset.dx * devicePixelRatio).round(); int dyInDevicePixels = (scrollOffset.dy * devicePixelRatio).round(); return new Offset(dxInDevicePixels / devicePixelRatio, dyInDevicePixels / devicePixelRatio); } void paint(PaintingCanvas canvas, Offset offset) { if (child != null) { Offset roundedScrollOffset = _scrollOffsetRoundedToIntegerDevicePixels; bool _needsClip = offset < Offset.zero || !(offset & size).contains(((offset - roundedScrollOffset) & child.size).bottomRight); if (_needsClip) { canvas.save(); canvas.clipRect(offset & size); } canvas.paintChild(child, (offset - roundedScrollOffset).toPoint()); if (_needsClip) canvas.restore(); } } void applyPaintTransform(Matrix4 transform) { super.applyPaintTransform(transform); transform.translate(-scrollOffset.dx, -scrollOffset.dy); } void hitTestChildren(HitTestResult result, { Point position }) { if (child != null) { assert(child.parentData is BoxParentData); Rect childBounds = child.parentData.position & child.size; Point transformedPosition = position + _scrollOffsetRoundedToIntegerDevicePixels; if (childBounds.contains(transformedPosition)) child.hitTest(result, position: transformedPosition); } } } class RenderImage extends RenderBox { RenderImage({ sky.Image image, double width, double height, sky.ColorFilter colorFilter }) : _image = image, _width = width, _height = height, _colorFilter = colorFilter; sky.Image _image; sky.Image get image => _image; void set image (sky.Image value) { if (value == _image) return; _image = value; markNeedsPaint(); if (_width == null || _height == null) markNeedsLayout(); } double _width; double get width => _width; void set width (double value) { if (value == _width) return; _width = value; markNeedsLayout(); } double _height; double get height => _height; void set height (double value) { if (value == _height) return; _height = value; markNeedsLayout(); } sky.ColorFilter _colorFilter; sky.ColorFilter get colorFilter => _colorFilter; void set colorFilter (sky.ColorFilter value) { if (value == _colorFilter) return; _colorFilter = value; _cachedPaint = null; markNeedsPaint(); } Paint _cachedPaint; Paint get _paint { if (_cachedPaint == null) { _cachedPaint = new Paint(); if (colorFilter != null) _cachedPaint.setColorFilter(colorFilter); } return _cachedPaint; } Size _sizeForConstraints(BoxConstraints constraints) { // If there's no image, we can't size ourselves automatically if (_image == null) { double width = _width == null ? 0.0 : _width; double height = _height == null ? 0.0 : _height; return constraints.constrain(new Size(width, height)); } if (!constraints.isTight) { // If neither height nor width are specified, use inherent image // dimensions. If only one dimension is specified, adjust the // other dimension to maintain the aspect ratio. In both cases, // constrain dimensions first, otherwise we end up losing the // ratio after constraining. if (_width == null) { if (_height == null) { // autosize double width = constraints.constrainWidth(_image.width.toDouble()); double maxHeight = constraints.constrainHeight(_image.height.toDouble()); double ratio = _image.height / _image.width; double height = width * ratio; if (height > maxHeight) { height = maxHeight; width = maxHeight / ratio; } return constraints.constrain(new Size(width, height)); } // Determine width from height double width = _height * _image.width / _image.height; return constraints.constrain(new Size(width, height)); } if (_height == null) { // Determine height from width double height = _width * _image.height / _image.width; return constraints.constrain(new Size(width, height)); } } return constraints.constrain(new Size(width, height)); } double getMinIntrinsicWidth(BoxConstraints constraints) { if (_width == null && _height == null) return constraints.constrainWidth(0.0); return _sizeForConstraints(constraints).width; } double getMaxIntrinsicWidth(BoxConstraints constraints) { return _sizeForConstraints(constraints).width; } double getMinIntrinsicHeight(BoxConstraints constraints) { if (_width == null && _height == null) return constraints.constrainHeight(0.0); return _sizeForConstraints(constraints).height; } double getMaxIntrinsicHeight(BoxConstraints constraints) { return _sizeForConstraints(constraints).height; } void performLayout() { size = _sizeForConstraints(constraints); } void paint(PaintingCanvas canvas, Offset offset) { if (_image == null) return; bool needsScale = size.width != _image.width || size.height != _image.height; if (needsScale) { double widthScale = size.width / _image.width; double heightScale = size.height / _image.height; canvas.save(); canvas.translate(offset.dx, offset.dy); canvas.scale(widthScale, heightScale); offset = Offset.zero; } canvas.drawImage(_image, offset.toPoint(), _paint); if (needsScale) canvas.restore(); } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}width: ${width}\n${prefix}height: ${height}\n'; } class RenderDecoratedBox extends RenderProxyBox { RenderDecoratedBox({ BoxDecoration decoration, RenderBox child }) : _painter = new BoxPainter(decoration), super(child); BoxPainter _painter; BoxDecoration get decoration => _painter.decoration; void set decoration (BoxDecoration value) { assert(value != null); if (_painter.decoration.backgroundImage != null) _painter.decoration.backgroundImage.removeChangeListener(markNeedsPaint); if (value.backgroundImage != null) value.backgroundImage.addChangeListener(markNeedsPaint); if (value == _painter.decoration) return; _painter.decoration = value; markNeedsPaint(); } void paint(PaintingCanvas canvas, Offset offset) { assert(size.width != null); assert(size.height != null); _painter.paint(canvas, offset & size); super.paint(canvas, offset); } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}decoration:\n${_painter.decoration.toString(prefix + " ")}\n'; } class RenderTransform extends RenderProxyBox { RenderTransform({ Matrix4 transform, RenderBox child }) : super(child) { assert(transform != null); this.transform = transform; } Matrix4 _transform; void set transform(Matrix4 value) { assert(value != null); if (_transform == value) return; _transform = new Matrix4.copy(value); markNeedsPaint(); } void setIdentity() { _transform.setIdentity(); markNeedsPaint(); } void rotateX(double radians) { _transform.rotateX(radians); markNeedsPaint(); } void rotateY(double radians) { _transform.rotateY(radians); markNeedsPaint(); } void rotateZ(double radians) { _transform.rotateZ(radians); markNeedsPaint(); } void translate(x, [double y = 0.0, double z = 0.0]) { _transform.translate(x, y, z); markNeedsPaint(); } void scale(x, [double y, double z]) { _transform.scale(x, y, z); markNeedsPaint(); } void hitTestChildren(HitTestResult result, { Point position }) { Matrix4 inverse = new Matrix4.zero(); /* double det = */ inverse.copyInverse(_transform); // TODO(abarth): Check the determinant for degeneracy. Vector3 position3 = new Vector3(position.x, position.y, 0.0); Vector3 transformed3 = inverse.transform3(position3); Point transformed = new Point(transformed3.x, transformed3.y); super.hitTestChildren(result, position: transformed); } void paint(PaintingCanvas canvas, Offset offset) { canvas.save(); canvas.translate(offset.dx, offset.dy); canvas.concat(_transform.storage); super.paint(canvas, Offset.zero); canvas.restore(); } void applyPaintTransform(Matrix4 transform) { super.applyPaintTransform(transform); transform.multiply(_transform); } String debugDescribeSettings(String prefix) { List<String> result = _transform.toString().split('\n').map((s) => '$prefix $s\n').toList(); result.removeLast(); return '${super.debugDescribeSettings(prefix)}${prefix}transform matrix:\n${result.join()}'; } } typedef void SizeChangedCallback(Size newSize); class RenderSizeObserver extends RenderProxyBox { RenderSizeObserver({ this.callback, RenderBox child }) : super(child) { assert(callback != null); } SizeChangedCallback callback; void performLayout() { Size oldSize = size; super.performLayout(); if (oldSize != size) callback(size); } } typedef void CustomPaintCallback(PaintingCanvas canvas, Size size); class RenderCustomPaint extends RenderProxyBox { RenderCustomPaint({ CustomPaintCallback callback, RenderBox child }) : super(child) { assert(callback != null); _callback = callback; } CustomPaintCallback _callback; void set callback (CustomPaintCallback value) { assert(value != null || !attached); if (_callback == value) return; _callback = value; markNeedsPaint(); } void attach() { assert(_callback != null); super.attach(); } void paint(PaintingCanvas canvas, Offset offset) { assert(_callback != null); canvas.translate(offset.dx, offset.dy); _callback(canvas, size); super.paint(canvas, Offset.zero); canvas.translate(-offset.dx, -offset.dy); } } // RENDER VIEW LAYOUT MANAGER class ViewConstraints { const ViewConstraints({ this.size: Size.zero, this.orientation }); final Size size; final int orientation; } class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> { bool get createNewDisplayList => true; RenderView({ RenderBox child, this.timeForRotation: const Duration(microseconds: 83333) }) { this.child = child; } Size _size = Size.zero; Size get size => _size; int _orientation; // 0..3 int get orientation => _orientation; Duration timeForRotation; ViewConstraints _rootConstraints; ViewConstraints get rootConstraints => _rootConstraints; void set rootConstraints(ViewConstraints value) { if (_rootConstraints == value) return; _rootConstraints = value; markNeedsLayout(); } // We never call layout() on this class, so this should never get // checked. (This class is laid out using scheduleInitialLayout().) bool debugDoesMeetConstraints() { assert(false); return false; } void performResize() { assert(false); } void performLayout() { if (_rootConstraints.orientation != _orientation) { if (_orientation != null && child != null) child.rotate(oldAngle: _orientation, newAngle: _rootConstraints.orientation, time: timeForRotation); _orientation = _rootConstraints.orientation; } _size = _rootConstraints.size; assert(!_size.isInfinite); if (child != null) child.layout(new BoxConstraints.tight(_size)); } void rotate({ int oldAngle, int newAngle, Duration time }) { assert(false); // nobody tells the screen to rotate, the whole rotate() dance is started from our performResize() } bool hitTest(HitTestResult result, { Point position }) { if (child != null) { Rect childBounds = Point.origin & child.size; if (childBounds.contains(position)) child.hitTest(result, position: position); } result.add(new HitTestEntry(this)); return true; } void paint(PaintingCanvas canvas, Offset offset) { if (child != null) canvas.paintChild(child, offset.toPoint()); } void paintFrame() { sky.tracing.begin('RenderView.paintFrame'); try { sky.PictureRecorder recorder = new sky.PictureRecorder(); PaintingCanvas canvas = new PaintingCanvas(recorder, paintBounds); canvas.drawPaintingNode(paintingNode, Point.origin); sky.view.picture = recorder.endRecording(); } finally { sky.tracing.end('RenderView.paintFrame'); } } Rect get paintBounds => Point.origin & size; } // HELPER METHODS FOR RENDERBOX CONTAINERS abstract class RenderBoxContainerDefaultsMixin<ChildType extends RenderBox, ParentDataType extends ContainerParentDataMixin<ChildType>> implements ContainerRenderObjectMixin<ChildType, ParentDataType> { // This class, by convention, doesn't override any members of the superclass. // It only provides helper functions that subclasses can call. double defaultComputeDistanceToFirstActualBaseline(TextBaseline baseline) { assert(!needsLayout); RenderBox child = firstChild; while (child != null) { assert(child.parentData is ParentDataType); double result = child.getDistanceToActualBaseline(baseline); if (result != null) return result + child.parentData.position.y; child = child.parentData.nextSibling; } return null; } double defaultComputeDistanceToHighestActualBaseline(TextBaseline baseline) { assert(!needsLayout); double result; RenderBox child = firstChild; while (child != null) { assert(child.parentData is ParentDataType); double candidate = child.getDistanceToActualBaseline(baseline); if (candidate != null) { candidate += child.parentData.position.y; if (result != null) result = math.min(result, candidate); else result = candidate; } child = child.parentData.nextSibling; } return result; } void defaultHitTestChildren(HitTestResult result, { Point position }) { // the x, y parameters have the top left of the node's box as the origin ChildType child = lastChild; while (child != null) { assert(child.parentData is ParentDataType); Rect childBounds = child.parentData.position & child.size; if (childBounds.contains(position)) { if (child.hitTest(result, position: new Point(position.x - child.parentData.position.x, position.y - child.parentData.position.y))) break; } child = child.parentData.previousSibling; } } void defaultPaint(PaintingCanvas canvas, Offset offset) { RenderBox child = firstChild; while (child != null) { assert(child.parentData is ParentDataType); canvas.paintChild(child, child.parentData.position + offset); child = child.parentData.nextSibling; } } }