// 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 'box.dart'; import 'debug.dart'; import 'object.dart'; /// Abstract class for one-child-layout render boxes that provide control over /// the child's position. abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> { RenderShiftedBox(RenderBox child) { this.child = child; } @override double getMinIntrinsicWidth(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); if (child != null) return child.getMinIntrinsicWidth(constraints); return super.getMinIntrinsicWidth(constraints); } @override double getMaxIntrinsicWidth(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); if (child != null) return child.getMaxIntrinsicWidth(constraints); return super.getMaxIntrinsicWidth(constraints); } @override double getMinIntrinsicHeight(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); if (child != null) return child.getMinIntrinsicHeight(constraints); return super.getMinIntrinsicHeight(constraints); } @override double getMaxIntrinsicHeight(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); if (child != null) return child.getMaxIntrinsicHeight(constraints); return super.getMaxIntrinsicHeight(constraints); } @override double computeDistanceToActualBaseline(TextBaseline baseline) { double result; if (child != null) { assert(!needsLayout); result = child.getDistanceToActualBaseline(baseline); final BoxParentData childParentData = child.parentData; if (result != null) result += childParentData.offset.dy; } else { result = super.computeDistanceToActualBaseline(baseline); } return result; } @override void paint(PaintingContext context, Offset offset) { if (child != null) { final BoxParentData childParentData = child.parentData; context.paintChild(child, childParentData.offset + offset); } } @override bool hitTestChildren(HitTestResult result, { Point position }) { if (child != null) { final BoxParentData childParentData = child.parentData; final Point childPosition = new Point(position.x - childParentData.offset.dx, position.y - childParentData.offset.dy); return child.hitTest(result, position: childPosition); } return false; } } /// Insets its child by the given padding. /// /// When passing layout constraints to its child, padding shrinks the /// constraints by the given padding, causing the child to layout at a smaller /// size. Padding then sizes itself to its child's size, inflated by the /// padding, effectively creating empty space around the child. class RenderPadding extends RenderShiftedBox { RenderPadding({ EdgeInsets padding, RenderBox child }) : _padding = padding, super(child) { assert(padding != null); assert(padding.isNonNegative); } /// The amount to pad the child in each dimension. EdgeInsets get padding => _padding; EdgeInsets _padding; void set padding (EdgeInsets value) { assert(value != null); assert(value.isNonNegative); if (_padding == value) return; _padding = value; markNeedsLayout(); } @override double getMinIntrinsicWidth(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); double totalPadding = padding.left + padding.right; if (child != null) return constraints.constrainWidth(child.getMinIntrinsicWidth(constraints.deflate(padding)) + totalPadding); return constraints.constrainWidth(totalPadding); } @override double getMaxIntrinsicWidth(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); double totalPadding = padding.left + padding.right; if (child != null) return constraints.constrainWidth(child.getMaxIntrinsicWidth(constraints.deflate(padding)) + totalPadding); return constraints.constrainWidth(totalPadding); } @override double getMinIntrinsicHeight(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); double totalPadding = padding.top + padding.bottom; if (child != null) return constraints.constrainHeight(child.getMinIntrinsicHeight(constraints.deflate(padding)) + totalPadding); return constraints.constrainHeight(totalPadding); } @override double getMaxIntrinsicHeight(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); double totalPadding = padding.top + padding.bottom; if (child != null) return constraints.constrainHeight(child.getMaxIntrinsicHeight(constraints.deflate(padding)) + totalPadding); return constraints.constrainHeight(totalPadding); } @override 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); final BoxParentData childParentData = child.parentData; childParentData.offset = new Offset(padding.left, padding.top); size = constraints.constrain(new Size( padding.left + child.size.width + padding.right, padding.top + child.size.height + padding.bottom )); } @override void debugPaintSize(PaintingContext context, Offset offset) { super.debugPaintSize(context, offset); assert(() { Paint paint; if (child != null && !child.size.isEmpty) { Path path; paint = new Paint() ..color = debugPaintPaddingColor; path = new Path() ..moveTo(offset.dx, offset.dy) ..lineTo(offset.dx + size.width, offset.dy) ..lineTo(offset.dx + size.width, offset.dy + size.height) ..lineTo(offset.dx, offset.dy + size.height) ..close() ..moveTo(offset.dx + padding.left, offset.dy + padding.top) ..lineTo(offset.dx + padding.left, offset.dy + size.height - padding.bottom) ..lineTo(offset.dx + size.width - padding.right, offset.dy + size.height - padding.bottom) ..lineTo(offset.dx + size.width - padding.right, offset.dy + padding.top) ..close(); context.canvas.drawPath(path, paint); paint = new Paint() ..color = debugPaintPaddingInnerEdgeColor; const double kOutline = 2.0; path = new Path() ..moveTo(offset.dx + math.max(padding.left - kOutline, 0.0), offset.dy + math.max(padding.top - kOutline, 0.0)) ..lineTo(offset.dx + math.min(size.width - padding.right + kOutline, size.width), offset.dy + math.max(padding.top - kOutline, 0.0)) ..lineTo(offset.dx + math.min(size.width - padding.right + kOutline, size.width), offset.dy + math.min(size.height - padding.bottom + kOutline, size.height)) ..lineTo(offset.dx + math.max(padding.left - kOutline, 0.0), offset.dy + math.min(size.height - padding.bottom + kOutline, size.height)) ..close() ..moveTo(offset.dx + padding.left, offset.dy + padding.top) ..lineTo(offset.dx + padding.left, offset.dy + size.height - padding.bottom) ..lineTo(offset.dx + size.width - padding.right, offset.dy + size.height - padding.bottom) ..lineTo(offset.dx + size.width - padding.right, offset.dy + padding.top) ..close(); context.canvas.drawPath(path, paint); } else { paint = new Paint() ..color = debugPaintSpacingColor; context.canvas.drawRect(offset & size, paint); } return true; }); } @override void debugFillDescription(List<String> description) { super.debugFillDescription(description); description.add('padding: $padding'); } } /// Aligns its child box within itself. /// /// 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 an alignment of [const FractionalOffset(1.0, 1.0)]. /// /// By default, sizes to be as big as possible in both axes. If either axis is /// unconstrained, then in that direction it will be sized to fit the child's /// dimensions. Using widthFactor and heightFactor you can force this latter /// behavior in all cases. class RenderPositionedBox extends RenderShiftedBox { RenderPositionedBox({ RenderBox child, FractionalOffset alignment: const FractionalOffset(0.5, 0.5), double widthFactor, double heightFactor }) : _alignment = alignment, _widthFactor = widthFactor, _heightFactor = heightFactor, super(child) { assert(alignment != null && alignment.dx != null && alignment.dy != null); assert(widthFactor == null || widthFactor >= 0.0); assert(heightFactor == null || heightFactor >= 0.0); } /// How to align the child. /// /// The x and y values of the alignment control the horizontal and vertical /// alignment, respectively. An x value of 0.0 means that the left edge of /// the child is aligned with the left edge of the parent whereas an x value /// of 1.0 means that the right edge of the child is aligned with the right /// edge of the parent. Other values interpolate (and extrapolate) linearly. /// For example, a value of 0.5 means that the center of the child is aligned /// with the center of the parent. FractionalOffset get alignment => _alignment; FractionalOffset _alignment; void set alignment (FractionalOffset newAlignment) { assert(newAlignment != null && newAlignment.dx != null && newAlignment.dy != null); if (_alignment == newAlignment) return; _alignment = newAlignment; markNeedsLayout(); } /// If non-null, sets its width to the child's width multipled by this factor. /// /// Can be both greater and less than 1.0 but must be positive. double get widthFactor => _widthFactor; double _widthFactor; void set widthFactor (double value) { assert(value == null || value >= 0.0); if (_widthFactor == value) return; _widthFactor = value; markNeedsLayout(); } /// If non-null, sets its height to the child's height multipled by this factor. /// /// Can be both greater and less than 1.0 but must be positive. double get heightFactor => _heightFactor; double _heightFactor; void set heightFactor (double value) { assert(value == null || value >= 0.0); if (_heightFactor == value) return; _heightFactor = value; markNeedsLayout(); } @override void performLayout() { final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.INFINITY; final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.INFINITY; if (child != null) { child.layout(constraints.loosen(), parentUsesSize: true); size = constraints.constrain(new Size(shrinkWrapWidth ? child.size.width * (_widthFactor ?? 1.0) : double.INFINITY, shrinkWrapHeight ? child.size.height * (_heightFactor ?? 1.0) : double.INFINITY)); final BoxParentData childParentData = child.parentData; childParentData.offset = _alignment.alongOffset(size - child.size); } else { size = constraints.constrain(new Size(shrinkWrapWidth ? 0.0 : double.INFINITY, shrinkWrapHeight ? 0.0 : double.INFINITY)); } } @override void debugPaintSize(PaintingContext context, Offset offset) { super.debugPaintSize(context, offset); assert(() { Paint paint; if (child != null && !child.size.isEmpty) { Path path; paint = new Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1.0 ..color = debugPaintArrowColor; path = new Path(); final BoxParentData childParentData = child.parentData; if (childParentData.offset.dy > 0.0) { // vertical alignment arrows double headSize = math.min(childParentData.offset.dy * 0.2, 10.0); path ..moveTo(offset.dx + size.width / 2.0, offset.dy) ..relativeLineTo(0.0, childParentData.offset.dy - headSize) ..relativeLineTo(headSize, 0.0) ..relativeLineTo(-headSize, headSize) ..relativeLineTo(-headSize, -headSize) ..relativeLineTo(headSize, 0.0) ..moveTo(offset.dx + size.width / 2.0, offset.dy + size.height) ..relativeLineTo(0.0, -childParentData.offset.dy + headSize) ..relativeLineTo(headSize, 0.0) ..relativeLineTo(-headSize, -headSize) ..relativeLineTo(-headSize, headSize) ..relativeLineTo(headSize, 0.0); context.canvas.drawPath(path, paint); } if (childParentData.offset.dx > 0.0) { // horizontal alignment arrows double headSize = math.min(childParentData.offset.dx * 0.2, 10.0); path ..moveTo(offset.dx, offset.dy + size.height / 2.0) ..relativeLineTo(childParentData.offset.dx - headSize, 0.0) ..relativeLineTo(0.0, headSize) ..relativeLineTo(headSize, -headSize) ..relativeLineTo(-headSize, -headSize) ..relativeLineTo(0.0, headSize) ..moveTo(offset.dx + size.width, offset.dy + size.height / 2.0) ..relativeLineTo(-childParentData.offset.dx + headSize, 0.0) ..relativeLineTo(0.0, headSize) ..relativeLineTo(-headSize, -headSize) ..relativeLineTo(headSize, -headSize) ..relativeLineTo(0.0, headSize); context.canvas.drawPath(path, paint); } } else { paint = new Paint() ..color = debugPaintSpacingColor; context.canvas.drawRect(offset & size, paint); } return true; }); } @override void debugFillDescription(List<String> description) { super.debugFillDescription(description); description.add('alignment: $alignment'); } } /// A render object that imposes different constraints on its child than it gets /// from its parent, possibly allowing the child to overflow the parent. /// /// A render overflow box proxies most functions in the render box protocol to /// its child, except that when laying out its child, it passes constraints /// based on the minWidth, maxWidth, minHeight, and maxHeight fields instead of /// just passing the parent's constraints in. Specifically, it overrides any of /// the equivalent fields on the constraints given by the parent with the /// constraints given by these fields for each such field that is not null. It /// then sizes itself based on the parent's constraints' maxWidth and maxHeight, /// ignoring the child's dimensions. /// /// For example, if you wanted a box to always render 50 pixels high, regardless /// of where it was rendered, you would wrap it in a RenderOverflow with /// minHeight and maxHeight set to 50.0. Generally speaking, to avoid confusing /// behavior around hit testing, a RenderOverflowBox should usually be wrapped /// in a RenderClipRect. /// /// The child is positioned at the top left of the box. To position a smaller /// child inside a larger parent, use [RenderPositionedBox] and /// [RenderConstrainedBox] rather than RenderOverflowBox. class RenderOverflowBox extends RenderShiftedBox { RenderOverflowBox({ RenderBox child, double minWidth, double maxWidth, double minHeight, double maxHeight, FractionalOffset alignment: const FractionalOffset(0.5, 0.5) }) : _minWidth = minWidth, _maxWidth = maxWidth, _minHeight = minHeight, _maxHeight = maxHeight, _alignment = alignment, super(child); /// The minimum width constraint to give the child. Set this to null (the /// default) to use the constraint from the parent instead. double get minWidth => _minWidth; double _minWidth; void set minWidth (double value) { if (_minWidth == value) return; _minWidth = value; markNeedsLayout(); } /// The maximum width constraint to give the child. Set this to null (the /// default) to use the constraint from the parent instead. double get maxWidth => _maxWidth; double _maxWidth; void set maxWidth (double value) { if (_maxWidth == value) return; _maxWidth = value; markNeedsLayout(); } /// The minimum height constraint to give the child. Set this to null (the /// default) to use the constraint from the parent instead. double get minHeight => _minHeight; double _minHeight; void set minHeight (double value) { if (_minHeight == value) return; _minHeight = value; markNeedsLayout(); } /// The maximum height constraint to give the child. Set this to null (the /// default) to use the constraint from the parent instead. double get maxHeight => _maxHeight; double _maxHeight; void set maxHeight (double value) { if (_maxHeight == value) return; _maxHeight = value; markNeedsLayout(); } /// How to align the child. /// /// The x and y values of the alignment control the horizontal and vertical /// alignment, respectively. An x value of 0.0 means that the left edge of /// the child is aligned with the left edge of the parent whereas an x value /// of 1.0 means that the right edge of the child is aligned with the right /// edge of the parent. Other values interpolate (and extrapolate) linearly. /// For example, a value of 0.5 means that the center of the child is aligned /// with the center of the parent. FractionalOffset get alignment => _alignment; FractionalOffset _alignment; void set alignment (FractionalOffset newAlignment) { assert(newAlignment != null && newAlignment.dx != null && newAlignment.dy != null); if (_alignment == newAlignment) return; _alignment = newAlignment; markNeedsLayout(); } BoxConstraints _getInnerConstraints(BoxConstraints constraints) { return new BoxConstraints( minWidth: _minWidth ?? constraints.minWidth, maxWidth: _maxWidth ?? constraints.maxWidth, minHeight: _minHeight ?? constraints.minHeight, maxHeight: _maxHeight ?? constraints.maxHeight ); } @override double getMinIntrinsicWidth(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); return constraints.minWidth; } @override double getMaxIntrinsicWidth(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); return constraints.minWidth; } @override double getMinIntrinsicHeight(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); return constraints.minHeight; } @override double getMaxIntrinsicHeight(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); return constraints.minHeight; } @override bool get sizedByParent => true; @override void performResize() { size = constraints.biggest; } @override void performLayout() { if (child != null) { child.layout(_getInnerConstraints(constraints), parentUsesSize: true); final BoxParentData childParentData = child.parentData; childParentData.offset = _alignment.alongOffset(size - child.size); } } @override void debugFillDescription(List<String> description) { super.debugFillDescription(description); description.add('minWidth: ${minWidth ?? "use parent minWidth constraint"}'); description.add('maxWidth: ${maxWidth ?? "use parent maxWidth constraint"}'); description.add('minHeight: ${minHeight ?? "use parent minHeight constraint"}'); description.add('maxHeight: ${maxHeight ?? "use parent maxHeight constraint"}'); description.add('alignment: $alignment'); } } /// A delegate for computing the layout of a render object with a single child. class SingleChildLayoutDelegate { /// Returns the size of this object given the incoming constraints. Size getSize(BoxConstraints constraints) => constraints.biggest; /// Returns the box constraints for the child given the incoming constraints. BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints; /// Returns the position where the child should be placed given the size of this object and the size of the child. Offset getPositionForChild(Size size, Size childSize) => Offset.zero; /// Override this method to return true when the child needs to be laid out. bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) => true; } /// Defers the layout of its single child to a delegate. /// /// The delegate can determine the layout constraints for the child and can /// decide where to position the child. The delegate can also determine the size /// of the parent, but the size of the parent cannot depend on the size of the /// child. class RenderCustomSingleChildLayoutBox extends RenderShiftedBox { RenderCustomSingleChildLayoutBox({ RenderBox child, SingleChildLayoutDelegate delegate }) : _delegate = delegate, super(child) { assert(delegate != null); } /// A delegate that controls this object's layout. SingleChildLayoutDelegate get delegate => _delegate; SingleChildLayoutDelegate _delegate; void set delegate (SingleChildLayoutDelegate newDelegate) { assert(newDelegate != null); if (_delegate == newDelegate) return; if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate)) markNeedsLayout(); _delegate = newDelegate; } Size _getSize(BoxConstraints constraints) { return constraints.constrain(_delegate.getSize(constraints)); } @override double getMinIntrinsicWidth(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); return _getSize(constraints).width; } @override double getMaxIntrinsicWidth(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); return _getSize(constraints).width; } @override double getMinIntrinsicHeight(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); return _getSize(constraints).height; } @override double getMaxIntrinsicHeight(BoxConstraints constraints) { assert(constraints.debugAssertIsNormalized); return _getSize(constraints).height; } @override bool get sizedByParent => true; @override void performResize() { size = _getSize(constraints); } @override void performLayout() { if (child != null) { BoxConstraints childConstraints = delegate.getConstraintsForChild(constraints); assert(childConstraints.debugAssertIsNormalized); child.layout(childConstraints, parentUsesSize: !childConstraints.isTight); final BoxParentData childParentData = child.parentData; childParentData.offset = delegate.getPositionForChild(size, childConstraints.isTight ? childConstraints.smallest : child.size); } } } /// Positions its child vertically according to the child's baseline. class RenderBaseline extends RenderShiftedBox { RenderBaseline({ RenderBox child, double baseline, TextBaseline baselineType }) : _baseline = baseline, _baselineType = baselineType, super(child) { assert(baseline != null); assert(baselineType != null); } /// The number of logical pixels from the top of this box at which to position /// the child's baseline. double get baseline => _baseline; double _baseline; void set baseline (double value) { assert(value != null); if (_baseline == value) return; _baseline = value; markNeedsLayout(); } /// The type of baseline to use for positioning the child. TextBaseline get baselineType => _baselineType; TextBaseline _baselineType; void set baselineType (TextBaseline value) { assert(value != null); if (_baselineType == value) return; _baselineType = value; markNeedsLayout(); } @override void performLayout() { if (child != null) { child.layout(constraints.loosen(), parentUsesSize: true); size = constraints.constrain(child.size); double delta = baseline - child.getDistanceToBaseline(baselineType); final BoxParentData childParentData = child.parentData; childParentData.offset = new Offset(0.0, delta); } else { performResize(); } } @override void debugFillDescription(List<String> description) { super.debugFillDescription(description); description.add('baseline: $baseline'); description.add('baselineType: $baselineType'); } }