// 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:sky' as sky; import 'dart:typed_data'; import 'node.dart'; import 'package:vector_math/vector_math.dart'; // GENERIC BOX RENDERING // Anything that has a concept of x, y, width, height is going to derive from this 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.onlyLeft(double value) : top = 0.0, right = 0.0, bottom = 0.0, left = value; const EdgeDims.onlyRight(double value) : top = 0.0, right = value, bottom = 0.0, left = 0.0; const EdgeDims.onlyTop(double value) : top = value, right = 0.0, bottom = 0.0, left = 0.0; const EdgeDims.onlyBottom(double value) : top = 0.0, right = 0.0, bottom = value, left = 0.0; final double top; final double right; final double bottom; final double left; operator ==(EdgeDims other) => (top == other.top) || (right == other.right) || (bottom == other.bottom) || (left == other.left); } class BoxConstraints { const BoxConstraints({ this.minWidth: 0.0, this.maxWidth: double.INFINITY, this.minHeight: 0.0, this.maxHeight: double.INFINITY}); BoxConstraints.tight(sky.Size size) : minWidth = size.width, maxWidth = size.width, minHeight = size.height, maxHeight = size.height; BoxConstraints deflate(EdgeDims edges) { assert(edges != null); return new BoxConstraints( minWidth: minWidth, maxWidth: maxWidth - (edges.left + edges.right), minHeight: minHeight, maxHeight: maxHeight - (edges.top + edges.bottom) ); } 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); } sky.Size constrain(sky.Size size) { return new sky.Size(constrainWidth(size.width), constrainHeight(size.height)); } bool get isInfinite => maxWidth >= double.INFINITY || maxHeight >= double.INFINITY; } class BoxParentData extends ParentData { sky.Point position = new sky.Point(0.0, 0.0); } abstract class RenderBox extends RenderNode { void setParentData(RenderNode child) { if (child.parentData is! BoxParentData) child.parentData = new BoxParentData(); } // override this to report what dimensions you would have if you // were laid out with the given constraints this can walk the tree // if it must, but it should be as cheap as possible; just get the // dimensions and nothing else (e.g. don't calculate hypothetical // child positions if they're not needed to determine dimensions) sky.Size getIntrinsicDimensions(BoxConstraints constraints) { return constraints.constrain(new sky.Size(0.0, 0.0)); } BoxConstraints get constraints => super.constraints as BoxConstraints; void performResize() { // default behaviour for subclasses that have sizedByParent = true size = constraints.constrain(new sky.Size(0.0, 0.0)); 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, { sky.Point position }) { hitTestChildren(result, position: position); result.add(this); return true; } void hitTestChildren(HitTestResult result, { sky.Point position }) { } sky.Size size = new sky.Size(0.0, 0.0); } abstract class RenderProxyBox extends RenderBox with RenderNodeWithChildMixin { RenderProxyBox(RenderBox child) { this.child = child; } sky.Size getIntrinsicDimensions(BoxConstraints constraints) { if (child != null) return child.getIntrinsicDimensions(constraints); return super.getIntrinsicDimensions(constraints); } void performLayout() { if (child != null) { child.layout(constraints, parentUsesSize: true); size = child.size; } else { performResize(); } } void hitTestChildren(HitTestResult result, { sky.Point position }) { if (child != null) child.hitTest(result, position: position); else super.hitTestChildren(result, position: position); } void paint(RenderNodeDisplayList canvas) { if (child != null) child.paint(canvas); } } class RenderSizedBox extends RenderProxyBox { RenderSizedBox({ RenderBox child, sky.Size desiredSize: const sky.Size.infinite() }) : super(child) { assert(desiredSize != null); this.desiredSize = desiredSize; } sky.Size _desiredSize; sky.Size get desiredSize => _desiredSize; void set desiredSize (sky.Size value) { assert(value != null); if (_desiredSize == value) return; _desiredSize = value; markNeedsLayout(); } sky.Size getIntrinsicDimensions(BoxConstraints constraints) { return constraints.constrain(_desiredSize); } void performLayout() { size = constraints.constrain(_desiredSize); child.layout(new BoxConstraints.tight(size)); } } class RenderPadding extends RenderBox with RenderNodeWithChildMixin { RenderPadding({ EdgeDims padding, RenderBox child }) { assert(padding != null); this.padding = padding; this.child = child; } EdgeDims _padding; EdgeDims get padding => _padding; void set padding (EdgeDims value) { assert(value != null); if (_padding == value) return; _padding = value; markNeedsLayout(); } sky.Size getIntrinsicDimensions(BoxConstraints constraints) { assert(padding != null); constraints = constraints.deflate(padding); if (child == null) return super.getIntrinsicDimensions(constraints); return child.getIntrinsicDimensions(constraints); } void performLayout() { assert(padding != null); BoxConstraints innerConstraints = constraints.deflate(padding); if (child == null) { size = innerConstraints.constrain( new sky.Size(padding.left + padding.right, padding.top + padding.bottom)); return; } child.layout(innerConstraints, parentUsesSize: true); assert(child.parentData is BoxParentData); child.parentData.position = new sky.Point(padding.left, padding.top); size = constraints.constrain(new sky.Size(padding.left + child.size.width + padding.right, padding.top + child.size.height + padding.bottom)); } void paint(RenderNodeDisplayList canvas) { if (child != null) canvas.paintChild(child, child.parentData.position); } void hitTestChildren(HitTestResult result, { sky.Point position }) { if (child != null) { assert(child.parentData is BoxParentData); sky.Rect childBounds = new sky.Rect.fromPointAndSize(child.parentData.position, child.size); if (childBounds.contains(position)) { child.hitTest(result, position: new sky.Point(position.x - child.parentData.position.x, position.y - child.parentData.position.y)); } } } } // This must be immutable, because we won't notice when it changes class BoxDecoration { // TODO(mpcomplete): go through and change the users of this class to pass // a Color object. BoxDecoration({ backgroundColor }) : backgroundColor = new sky.Color(backgroundColor); final sky.Color backgroundColor; } class RenderDecoratedBox extends RenderProxyBox { RenderDecoratedBox({ BoxDecoration decoration, RenderBox child }) : _decoration = decoration, super(child); BoxDecoration _decoration; BoxDecoration get decoration => _decoration; void set decoration (BoxDecoration value) { if (value == _decoration) return; _decoration = value; markNeedsPaint(); } void paint(RenderNodeDisplayList canvas) { assert(size.width != null); assert(size.height != null); if (_decoration == null) return; if (_decoration.backgroundColor != null) { sky.Paint paint = new sky.Paint()..color = _decoration.backgroundColor; canvas.drawRect(new sky.Rect.fromLTRB(0.0, 0.0, size.width, size.height), paint); } super.paint(canvas); } } 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 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, { sky.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); sky.Point transformed = new sky.Point(transformed3.x, transformed3.y); super.hitTestChildren(result, position: transformed); } void paint(RenderNodeDisplayList canvas) { Float32List storage = _transform.storage; canvas.save(); canvas.concat([ storage[ 0], storage[ 1], storage[ 3], storage[ 4], storage[ 5], storage[ 7], storage[12], storage[13], storage[15], ]); super.paint(canvas); canvas.restore(); } } // 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 RenderNode with RenderNodeWithChildMixin { RenderView({ RenderBox child, this.timeForRotation: const Duration(microseconds: 83333) }) { this.child = child; } sky.Size _size = new sky.Size(0.0, 0.0); double get width => _size.width; double get height => _size.height; int _orientation; // 0..3 int get orientation => _orientation; Duration timeForRotation; ViewConstraints get constraints => super.constraints as ViewConstraints; bool get sizedByParent => true; void performResize() { if (constraints.orientation != _orientation) { if (_orientation != null && child != null) child.rotate(oldAngle: _orientation, newAngle: constraints.orientation, time: timeForRotation); _orientation = constraints.orientation; } _size = new sky.Size(constraints.width, constraints.height); assert(_size.height < double.INFINITY); assert(_size.width < double.INFINITY); } void performLayout() { 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, { sky.Point position }) { if (child != null) { sky.Rect childBounds = new sky.Rect.fromSize(child.size); if (childBounds.contains(position)) child.hitTest(result, position: position); } result.add(this); return true; } void paint(RenderNodeDisplayList canvas) { if (child != null) canvas.paintChild(child, new sky.Point(0.0, 0.0)); } void paintFrame() { RenderNode.debugDoingPaint = true; var canvas = new RenderNodeDisplayList(sky.view.width, sky.view.height); paint(canvas); sky.view.picture = canvas.endRecording(); RenderNode.debugDoingPaint = false; } } // DEFAULT BEHAVIORS FOR RENDERBOX CONTAINERS abstract class RenderBoxContainerDefaultsMixin> implements ContainerRenderNodeMixin { void defaultHitTestChildren(HitTestResult result, { sky.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); sky.Rect childBounds = new sky.Rect.fromPointAndSize(child.parentData.position, child.size); if (childBounds.contains(position)) { if (child.hitTest(result, position: new sky.Point(position.x - child.parentData.position.x, position.y - child.parentData.position.y))) break; } child = child.parentData.previousSibling; } } void defaultPaint(RenderNodeDisplayList canvas) { RenderBox child = firstChild; while (child != null) { assert(child.parentData is ParentDataType); canvas.paintChild(child, child.parentData.position); child = child.parentData.nextSibling; } } }