// 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:vector_math/vector_math.dart'; import '../base/debug.dart'; import '../mojo/net/image_cache.dart' as image_cache; import '../painting/box_painter.dart'; import 'object.dart'; export '../painting/box_painter.dart'; // 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; } 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; 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; 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)); } final double minWidth; final double maxWidth; final double minHeight; final double maxHeight; double constrainWidth(double width) { return clamp(min: minWidth, max: maxWidth, value: width); } double constrainHeight(double height) { 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; } 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 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'; } enum TextBaseline { alphabetic, ideographic } abstract class RenderBox extends RenderObject { void setParentData(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); } // getDistanceToBaseline() 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. 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, then it should return 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. double getDistanceToBaseline(TextBaseline baseline) { assert(!needsLayout); double result = getDistanceToActualBaseline(baseline); if (result == null) return size.height; return result; } // getDistanceToActualBaseline() 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. double getDistanceToActualBaseline(TextBaseline baseline) { assert(!needsLayout); return null; } // This whole block should only be here in debug builds bool _debugDoingThisLayout = false; bool _debugCanParentUseSize; void layoutWithoutResize() { _debugDoingThisLayout = true; _debugCanParentUseSize = false; super.layoutWithoutResize(); _debugCanParentUseSize = null; _debugDoingThisLayout = false; } void layout(dynamic constraints, { bool parentUsesSize: false }) { _debugDoingThisLayout = true; _debugCanParentUseSize = parentUsesSize; super.layout(constraints, parentUsesSize: parentUsesSize); _debugCanParentUseSize = null; _debugDoingThisLayout = false; } BoxConstraints get constraints => super.constraints; void performResize() { // default behaviour for subclasses that have sizedByParent = true size = constraints.constrain(Size.zero); assert(size.height < double.INFINITY); assert(size.width < double.INFINITY); } 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): In non-debug builds, this should all just be: // Size size = Size.zero; // In debug builds, however: Size _size = Size.zero; Size get size => _size; void set size(Size value) { assert(RenderObject.debugDoingLayout); assert(_debugDoingThisLayout); if (value is _DebugSize) { assert(value._canBeUsedByParent); assert(value._owner.parent == this); } _size = inDebugBuild ? new _DebugSize(value, this, _debugCanParentUseSize) : value; } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}size: ${size}\n'; } class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin { // 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 getDistanceToActualBaseline(TextBaseline baseline) { if (child != null) return child.getDistanceToActualBaseline(baseline); return super.getDistanceToActualBaseline(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(RenderCanvas canvas) { if (child != null) child.paint(canvas); } } 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 constraints.constrainWidth(0.0); } double getMaxIntrinsicWidth(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicWidth(_additionalConstraints.apply(constraints)); return constraints.constrainWidth(0.0); } double getMinIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMinIntrinsicHeight(_additionalConstraints.apply(constraints)); return constraints.constrainHeight(0.0); } double getMaxIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicHeight(_additionalConstraints.apply(constraints)); return 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 RenderShrinkWrapWidth extends RenderProxyBox { RenderShrinkWrapWidth({ RenderBox child }) : super(child); BoxConstraints _getInnerConstraints(BoxConstraints constraints) { double width = child.getMaxIntrinsicWidth(constraints); assert(width == constraints.constrainWidth(width)); return constraints.applyWidth(width); } double getMinIntrinsicWidth(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicWidth(constraints); return constraints.constrainWidth(0.0); } double getMaxIntrinsicWidth(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicWidth(constraints); return constraints.constrainWidth(0.0); } double getMinIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMinIntrinsicHeight(_getInnerConstraints(constraints)); return constraints.constrainWidth(0.0); } double getMaxIntrinsicHeight(BoxConstraints constraints) { if (child != null) return child.getMaxIntrinsicHeight(_getInnerConstraints(constraints)); return constraints.constrainWidth(0.0); } void performLayout() { if (child != null) { child.layout(_getInnerConstraints(constraints), parentUsesSize: true); size = child.size; } else { performResize(); } } } 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; markNeedsPaint(); } void paint(RenderCanvas canvas) { if (child != null) { int a = (_opacity * 255).round(); if (a == 0) return; if (a == 255) { child.paint(canvas); return; } Paint paint = new Paint() ..color = new Color.fromARGB(a, 0, 0, 0) ..setTransferMode(sky.TransferMode.srcOver); canvas.saveLayer(null, paint); child.paint(canvas); 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; markNeedsPaint(); } sky.TransferMode _transferMode; sky.TransferMode get transferMode => _transferMode; void set transferMode (sky.TransferMode value) { assert(value != null); if (_transferMode == value) return; _transferMode = value; markNeedsPaint(); } void paint(RenderCanvas canvas) { if (child != null) { Paint paint = new Paint() ..setColorFilter(new sky.ColorFilter.mode(_color, _transferMode)); canvas.saveLayer(null, paint); child.paint(canvas); canvas.restore(); } } } class RenderClipRect extends RenderProxyBox { RenderClipRect({ RenderBox child }) : super(child); void paint(RenderCanvas canvas) { if (child != null) { canvas.save(); canvas.clipRect(new Rect.fromSize(size)); child.paint(canvas); canvas.restore(); } } } class RenderClipOval extends RenderProxyBox { RenderClipOval({ RenderBox child }) : super(child); void paint(RenderCanvas canvas) { if (child != null) { Rect rect = new Rect.fromSize(size); canvas.saveLayer(rect, new Paint()); Path path = new Path(); path.addOval(rect); canvas.clipPath(path); child.paint(canvas); canvas.restore(); } } } abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin { // Abstract class for one-child-layout render boxes RenderShiftedBox(RenderBox child) { this.child = child; } void paint(RenderCanvas canvas) { if (child != null) canvas.paintChild(child, child.parentData.position); } double getDistanceToActualBaseline(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.getDistanceToActualBaseline(baseline); } return result; } void hitTestChildren(HitTestResult result, { Point position }) { if (child != null) { assert(child.parentData is BoxParentData); Rect childBounds = new Rect.fromPointAndSize(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); BoxConstraints innerConstraints = constraints.deflate(padding); if (child == null) { size = innerConstraints.constrain( new Size(padding.left + padding.right, padding.top + padding.bottom)); return; } 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 { 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(); } 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); } void performLayout() { if (child != null) { child.layout(constraints.loosen(), parentUsesSize: true); size = constraints.constrain(child.size); assert(child.parentData is BoxParentData); Size delta = size - child.size; child.parentData.position = new Point(delta.width * horizontal, delta.height * vertical); } else { performResize(); } } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}horizontal: ${horizontal}\n${prefix}vertical: ${vertical}\n'; } class RenderImage extends RenderBox { RenderImage(String url, Size dimensions) { requestedSize = dimensions; src = url; } sky.Image _image; String _src; String get src => _src; void set src (String value) { if (value == _src) return; _src = value; image_cache.load(_src, (result) { _image = result; if (requestedSize.width == null || requestedSize.height == null) markNeedsLayout(); markNeedsPaint(); }); } Size _requestedSize; Size get requestedSize => _requestedSize; void set requestedSize (Size value) { if (value == null) value = const Size(null, null); if (value == _requestedSize) return; _requestedSize = value; markNeedsLayout(); } Size _sizeForConstraints(BoxConstraints innerConstraints) { // If there's no image, we can't size ourselves automatically if (_image == null) { double width = requestedSize.width == null ? 0.0 : requestedSize.width; double height = requestedSize.height == null ? 0.0 : requestedSize.height; return constraints.constrain(new Size(width, height)); } // 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 if (requestedSize.width == null) { if (requestedSize.height == null) { return constraints.constrain(new Size(_image.width.toDouble(), _image.height.toDouble())); } else { double width = requestedSize.height * _image.width / _image.height; return constraints.constrain(new Size(width, requestedSize.height)); } } else if (requestedSize.height == null) { double height = requestedSize.width * _image.height / _image.width; return constraints.constrain(new Size(requestedSize.width, height)); } else { return constraints.constrain(requestedSize); } } double getMinIntrinsicWidth(BoxConstraints constraints) { if (requestedSize.width == null && requestedSize.height == null) return constraints.constrainWidth(0.0); return _sizeForConstraints(constraints).width; } double getMaxIntrinsicWidth(BoxConstraints constraints) { return _sizeForConstraints(constraints).width; } double getMinIntrinsicHeight(BoxConstraints constraints) { if (requestedSize.width == null && requestedSize.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(RenderCanvas canvas) { 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.scale(widthScale, heightScale); } Paint paint = new Paint(); canvas.drawImage(_image, 0.0, 0.0, paint); if (needsScale) canvas.restore(); } String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}url: ${src}\n${prefix}dimensions: ${requestedSize}\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 (value == _painter.decoration) return; _painter.decoration = value; markNeedsPaint(); } void paint(RenderCanvas canvas) { assert(size.width != null); assert(size.height != null); _painter.paint(canvas, new Rect.fromSize(size)); super.paint(canvas); } 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(RenderCanvas canvas) { canvas.save(); canvas.concat(_transform.storage); super.paint(canvas); canvas.restore(); } String debugDescribeSettings(String prefix) { List 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(RenderCanvas 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(RenderCanvas canvas) { assert(_callback != null); _callback(canvas, size); super.paint(canvas); } } // RENDER VIEW LAYOUT MANAGER class ViewConstraints { const ViewConstraints({ this.width: 0.0, this.height: 0.0, this.orientation: null }); final double width; final double height; final int orientation; } class RenderView extends RenderObject with RenderObjectWithChildMixin { RenderView({ RenderBox child, this.timeForRotation: const Duration(microseconds: 83333) }) { this.child = child; } Size _size = Size.zero; double get width => _size.width; double get height => _size.height; 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(); } 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 = new Size(_rootConstraints.width, _rootConstraints.height); assert(_size.height < double.INFINITY); assert(_size.width < double.INFINITY); if (child != null) { child.layout(new BoxConstraints.tight(_size)); assert(child.size.width == width); assert(child.size.height == height); } } 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 = new Rect.fromSize(child.size); if (childBounds.contains(position)) child.hitTest(result, position: position); } result.add(new HitTestEntry(this)); return true; } void paint(RenderCanvas canvas) { if (child != null) canvas.paintChild(child, Point.origin); } void paintFrame() { sky.tracing.begin('RenderView.paintFrame'); RenderObject.debugDoingPaint = true; try { sky.PictureRecorder recorder = new sky.PictureRecorder(); RenderCanvas canvas = new RenderCanvas(recorder, width, height); paint(canvas); sky.view.picture = recorder.endRecording(); } finally { RenderObject.debugDoingPaint = false; sky.tracing.end('RenderView.paintFrame'); } } } // DEFAULT BEHAVIORS FOR RENDERBOX CONTAINERS abstract class RenderBoxContainerDefaultsMixin> implements ContainerRenderObjectMixin { double defaultGetDistanceToFirstActualBaseline(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 defaultGetDistanceToHighestActualBaseline(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.x; 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 = new Rect.fromPointAndSize(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(RenderCanvas canvas) { RenderBox child = firstChild; while (child != null) { assert(child.parentData is ParentDataType); canvas.paintChild(child, child.parentData.position); child = child.parentData.nextSibling; } } }