blob: b92d0b7f50f4b22e5564054a98c071de79026d18 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:sky' as sky;
import 'package:sky/painting.dart';
import 'package:sky/src/rendering/debug.dart';
import 'package:sky/src/rendering/object.dart';
import 'package:vector_math/vector_math.dart';
export 'package:sky/painting.dart' show TextBaseline;
// 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;
}
/// Immutable layout constraints for box layout
///
/// A size respects a BoxConstraints if, and only if, all of the following
/// relations hold:
///
/// * `minWidth <= size.width <= constraints.maxWidth`
/// * `minHeight <= size.height <= maxHeight`
///
/// The constraints themselves must satisfy these relations:
///
/// * `0.0 <= minWidth <= maxWidth <= double.INFINITY`
/// * `0.0 <= minHeight <= maxHeight <= double.INFINITY`
///
/// Note: `double.INFINITY` is a legal value for each constraint.
class BoxConstraints extends Constraints {
/// Constructs box constraints with the given constraints
const BoxConstraints({
this.minWidth: 0.0,
this.maxWidth: double.INFINITY,
this.minHeight: 0.0,
this.maxHeight: double.INFINITY
});
final double minWidth;
final double maxWidth;
final double minHeight;
final double maxHeight;
/// Constructs box constraints that is respected only by the given size
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
/// Constructs box constraints that require the given width or 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;
/// Constructs box constraints that forbid sizes larger than the given size
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;
/// Constructs box constraints that expand to fill another box contraints
///
/// If width or height is given, the constraints will require exactly the
/// given value in the given dimension.
const BoxConstraints.expand({
double width,
double height
}): minWidth = width != null ? width : double.INFINITY,
maxWidth = width != null ? width : double.INFINITY,
minHeight = height != null ? height : double.INFINITY,
maxHeight = height != null ? height : double.INFINITY;
/// Returns new box constraints that are smaller by the given edge dimensions
BoxConstraints deflate(EdgeDims edges) {
assert(edges != null);
double horizontal = edges.left + edges.right;
double vertical = edges.top + edges.bottom;
double deflatedMinWidth = math.max(0.0, minWidth - horizontal);
double deflatedMinHeight = math.max(0.0, minHeight - vertical);
return new BoxConstraints(
minWidth: deflatedMinWidth,
maxWidth: math.max(deflatedMinWidth, maxWidth - horizontal),
minHeight: deflatedMinHeight,
maxHeight: math.max(deflatedMinHeight, maxHeight - vertical)
);
}
/// Returns new box constraints that remove the minimum width and height requirements
BoxConstraints loosen() {
return new BoxConstraints(
minWidth: 0.0,
maxWidth: maxWidth,
minHeight: 0.0,
maxHeight: maxHeight
);
}
/// Returns new box constraints that respect the given constraints while being as close as possible to the original constraints
BoxConstraints enforce(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)
);
}
/// Returns new box constraints with a tight width as close to the given width as possible while still respecting the original box constraints
BoxConstraints tightenWidth(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);
}
/// Returns new box constraints with a tight height as close to the given height as possible while still respecting the original box constraints
BoxConstraints tightenHeight(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));
}
/// Returns box constraints with the same width constraints but with unconstrainted height
BoxConstraints widthConstraints() => new BoxConstraints(minWidth: minWidth, maxWidth: maxWidth);
/// Returns box constraints with the same height constraints but with unconstrainted width
BoxConstraints heightConstraints() => new BoxConstraints(minHeight: minHeight, maxHeight: maxHeight);
/// Returns the width that both satisfies the constraints and is as close as possible to the given width
double constrainWidth([double width = double.INFINITY]) {
return clamp(min: minWidth, max: maxWidth, value: width);
}
/// Returns the height that both satisfies the constraints and is as close as possible to the given height
double constrainHeight([double height = double.INFINITY]) {
return clamp(min: minHeight, max: maxHeight, value: height);
}
/// Returns the size that both satisfies the constraints and is as close as possible to the given size
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;
}
/// The biggest size that satisifes the constraints
Size get biggest => new Size(constrainWidth(), constrainHeight());
/// The smallest size that satisfies the constraints
Size get smallest => new Size(constrainWidth(0.0), constrainHeight(0.0));
/// Whether there is exactly one width value that satisfies the constraints
bool get hasTightWidth => minWidth >= maxWidth;
/// Whether there is exactly one height value that satisfies the constraints
bool get hasTightHeight => minHeight >= maxHeight;
/// Whether there is exactly one size that satifies the constraints
bool get isTight => hasTightWidth && hasTightHeight;
/// Whether the given size satisfies the constraints
bool isSatisfiedBy(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)";
}
/// A hit test entry used by [RenderBox]
class BoxHitTestEntry extends HitTestEntry {
const BoxHitTestEntry(HitTestTarget target, this.localPosition) : super(target);
/// The position of the hit test in the local coordinates of [target]
final Point localPosition;
}
/// Parent data used by [RenderBox] and its subclasses
class BoxParentData extends ParentData {
Point _position = Point.origin;
/// The point at which to paint the child in the parent's coordinate system
Point get position => _position;
void set position(Point value) {
assert(RenderObject.debugDoingLayout);
_position = value;
}
String toString() => 'position=$position';
}
/// A render object in a 2D cartesian coordinate system
///
/// The size of each box is expressed as a width and a height. Each box has its
/// own coordinate system in which its upper left corner is placed at (0, 0).
/// The lower right corner of the box is therefore at (width, height). The box
/// contains all the points including the upper left corner and extending to,
/// but not including, the lower right corner.
///
/// Box layout is performed by passing a [BoxConstraints] object down the tree.
/// The box constraints establish a min and max value for the child's width
/// and height. In determining its size, the child must respect the constraints
/// given to it by its parent.
///
/// This protocol is sufficient for expressing a number of common box layout
/// data flows. For example, to implement a width-in-height-out data flow, call
/// your child's [layout] function with a set of box constraints with a tight
/// width value (and pass true for parentUsesSize). After the child determines
/// its height, use the child's height to determine your size.
abstract class RenderBox extends RenderObject {
void setupParentData(RenderObject child) {
if (child.parentData is! BoxParentData)
child.parentData = new BoxParentData();
}
/// Returns the minimum width that this box could be without failing to paint
/// its contents within itself
///
/// Override in subclasses that implement [performLayout].
double getMinIntrinsicWidth(BoxConstraints constraints) {
return constraints.constrainWidth(0.0);
}
/// Returns the smallest width beyond which increasing the width never
/// decreases the height
///
/// Override in subclasses that implement [performLayout].
double getMaxIntrinsicWidth(BoxConstraints constraints) {
return constraints.constrainWidth(0.0);
}
/// Return the minimum height that this box could be without failing to render
/// its contents within itself.
///
/// Override in subclasses that implement [performLayout].
double getMinIntrinsicHeight(BoxConstraints constraints) {
return constraints.constrainHeight(0.0);
}
/// Returns 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().
///
/// Override in subclasses that implement [performLayout].
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;
}
/// 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.
///
/// 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, this function 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) unless the the caller passes true
/// for `onlyReal`, in which case the function returns null.
///
/// Only call this function calling [layout] on this box. You are only
/// allowed to call this from the parent of this box during that parent's
/// [performLayout] or [paint] functions.
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;
}
/// Calls [computeDistanceToActualBaseline] and caches the result.
///
/// This function must only be called from [getDistanceToBaseline] and
/// [computeDistanceToActualBaseline]. Do not call this function directly from
/// outside those two methods.
double getDistanceToActualBaseline(TextBaseline baseline) {
assert(_debugDoingBaseline);
_ancestorUsesBaseline = true;
if (_cachedBaselines == null)
_cachedBaselines = new Map<TextBaseline, double>();
_cachedBaselines.putIfAbsent(baseline, () => computeDistanceToActualBaseline(baseline));
return _cachedBaselines[baseline];
}
/// 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, if
/// any, or null otherwise.
///
/// Do not call this function directly. Instead, call [getDistanceToBaseline]
/// if you need to know the baseline of a child from an invocation of
/// [performLayout] or [paint] and call [getDistanceToActualBaseline] if you
/// are implementing [computeDistanceToActualBaseline] and need to defer to a
/// child.
///
/// Subclasses should override this function to supply the distances to their
/// baselines.
double computeDistanceToActualBaseline(TextBaseline baseline) {
assert(_debugDoingBaseline);
return null;
}
/// The box constraints most recently received from the parent
BoxConstraints get constraints => super.constraints;
bool debugDoesMeetConstraints() {
assert(constraints != null);
assert(_size != null);
assert(() {
'See https://flutter.github.io/layout/#unbounded-constraints';
return !_size.isInfinite;
});
bool result = constraints.isSatisfiedBy(_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);
}
/// Determines the set of render objects located at the given position
///
/// Returns true if the given point is contained in this render object or one
/// of its descendants. Adds any render objects that contain the point to the
/// given hit test result.
///
/// The caller is responsible for transforming [position] into the local
/// coordinate space of the callee. The callee is responsible for checking
/// whether the given position is within its bounds.
bool hitTest(HitTestResult result, { Point position }) {
if (position.x >= 0.0 && position.x < _size.width &&
position.y >= 0.0 && position.y < _size.height) {
hitTestChildren(result, position: position);
result.add(new BoxHitTestEntry(this, position));
return true;
}
return false;
}
/// Override this function to check whether any children are located at the
/// given position
///
/// Typically children should be hit tested in reverse paint order so that
/// hit tests at locations where children overlap hit the child that is
/// visually "on top" (i.e., paints later).
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;
/// The size of this render box computed during layout
///
/// This value is stale whenever this object is marked as needing layout.
/// During [performLayout], do not read the size of a child unless you pass
/// true for parentUsesSize when calling the child's [layout] function.
///
/// The size of a box should be set only during the box's [performLayout] or
/// [performResize] functions. If you wish to change the size of a box outside
/// of those functins, call [markNeedsLayout] instead to schedule a layout of
/// the box.
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;
assert(debugDoesMeetConstraints());
}
/// Multiply the transform from the parent's coordinate system to this box's
/// coordinate system into the given transform
///
/// This function is used to convert coordinate systems between boxes.
/// Subclasses that apply transforms during painting should override this
/// function to factor those transforms into the calculation.
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);
}
/// Convert the given point from the global coodinate system to the local
/// coordinate system for this box
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);
}
/// Convert the given point from the local coordiante system for this box to
/// the global coordinate sytem
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);
}
/// Returns a rectangle that contains all the pixels painted by this box
///
/// The paint bounds can be larger or smaller than [size], which is the amount
/// of space this box takes up during layout. For example, if this box casts a
/// shadow, that shadow might extend beyond the space allocated to this box
/// during layout.
///
/// The paint bounds are used to size the buffers into which this box paints.
/// If the box attempts to paints outside its paint bounds, there might not be
/// enough memory allocated to represent the box's visual appearance, which
/// can lead to undefined behavior.
///
/// The returned paint bounds are in the local coordinate system of this box.
Rect get paintBounds => Point.origin & size;
void debugPaint(PaintingContext context, Offset offset) {
if (debugPaintSizeEnabled)
debugPaintSize(context, offset);
if (debugPaintBaselinesEnabled)
debugPaintBaselines(context, offset);
}
void debugPaintSize(PaintingContext context, Offset offset) {
Paint paint = new Paint();
paint.setStyle(sky.PaintingStyle.stroke);
paint.strokeWidth = 1.0;
paint.color = debugPaintSizeColor;
context.canvas.drawRect(offset & size, paint);
}
void debugPaintBaselines(PaintingContext context, 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);
context.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);
context.canvas.drawPath(path, paint);
}
}
String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}size: ${size}\n';
}
/// A mixin that provides useful default behaviors for boxes with children managed by the [ContainerRenderObjectMixin] mixin
///
/// By convention, this class doesn't override any members of the superclass.
/// Instead, it provides helpful functions that subclasses can call as
/// appropriate.
abstract class RenderBoxContainerDefaultsMixin<ChildType extends RenderBox, ParentDataType extends ContainerParentDataMixin<ChildType>> implements ContainerRenderObjectMixin<ChildType, ParentDataType> {
/// Returns the baseline of the first child with a baseline
///
/// Useful when the children are displayed vertically in the same order they
/// appear in the child list.
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;
}
/// Returns the minimum baseline value among every child
///
/// Useful when the vertical position of the children isn't determined by the
/// order in the child list.
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;
}
/// Performs a hit test on each child by walking the child list backwards
///
/// Stops walking once after the first child reports that it contains the
/// given point. Returns whether any children contain the given point.
bool 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);
Point transformed = new Point(position.x - child.parentData.position.x,
position.y - child.parentData.position.y);
if (child.hitTest(result, position: transformed))
return true;
child = child.parentData.previousSibling;
}
return false;
}
/// Paints each child by walking the child list forwards
void defaultPaint(PaintingContext context, Offset offset) {
RenderBox child = firstChild;
while (child != null) {
assert(child.parentData is ParentDataType);
context.paintChild(child, child.parentData.position + offset);
child = child.parentData.nextSibling;
}
}
}