blob: 1d8b39b6c9cc3223515e1bdad723d61f97cdb6ca [file] [log] [blame]
// Copyright 2014 The Flutter 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 'package:flutter/foundation.dart';
import 'box.dart';
import 'debug.dart';
import 'debug_overflow_indicator.dart';
import 'layer.dart';
import 'object.dart';
import 'stack.dart' show RelativeRect;
/// Signature for a function that transforms a [BoxConstraints] to another
/// [BoxConstraints].
///
/// Used by [RenderConstraintsTransformBox] and [ConstraintsTransformBox].
/// Typically the caller requires the returned [BoxConstraints] to be
/// [BoxConstraints.isNormalized].
typedef BoxConstraintsTransform = BoxConstraints Function(BoxConstraints);
/// Abstract class for one-child-layout render boxes that provide control over
/// the child's position.
abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
/// Initializes the [child] property for subclasses.
RenderShiftedBox(RenderBox? child) {
this.child = child;
}
@override
double computeMinIntrinsicWidth(double height) {
if (child != null)
return child!.getMinIntrinsicWidth(height);
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child != null)
return child!.getMaxIntrinsicWidth(height);
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child != null)
return child!.getMinIntrinsicHeight(width);
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child != null)
return child!.getMaxIntrinsicHeight(width);
return 0.0;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
double? result;
if (child != null) {
assert(!debugNeedsLayout);
result = child!.getDistanceToActualBaseline(baseline);
final BoxParentData childParentData = child!.parentData! as BoxParentData;
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! as BoxParentData;
context.paintChild(child!, childParentData.offset + offset);
}
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
return result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return child!.hitTest(result, position: transformed);
},
);
}
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 {
/// Creates a render object that insets its child.
///
/// The [padding] argument must not be null and must have non-negative insets.
RenderPadding({
required EdgeInsetsGeometry padding,
TextDirection? textDirection,
RenderBox? child,
}) : assert(padding != null),
assert(padding.isNonNegative),
_textDirection = textDirection,
_padding = padding,
super(child);
EdgeInsets? _resolvedPadding;
void _resolve() {
if (_resolvedPadding != null)
return;
_resolvedPadding = padding.resolve(textDirection);
assert(_resolvedPadding!.isNonNegative);
}
void _markNeedResolution() {
_resolvedPadding = null;
markNeedsLayout();
}
/// The amount to pad the child in each dimension.
///
/// If this is set to an [EdgeInsetsDirectional] object, then [textDirection]
/// must not be null.
EdgeInsetsGeometry get padding => _padding;
EdgeInsetsGeometry _padding;
set padding(EdgeInsetsGeometry value) {
assert(value != null);
assert(value.isNonNegative);
if (_padding == value)
return;
_padding = value;
_markNeedResolution();
}
/// The text direction with which to resolve [padding].
///
/// This may be changed to null, but only after the [padding] has been changed
/// to a value that does not depend on the direction.
TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection;
set textDirection(TextDirection? value) {
if (_textDirection == value)
return;
_textDirection = value;
_markNeedResolution();
}
@override
double computeMinIntrinsicWidth(double height) {
_resolve();
final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
if (child != null) // next line relies on double.infinity absorption
return child!.getMinIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding;
return totalHorizontalPadding;
}
@override
double computeMaxIntrinsicWidth(double height) {
_resolve();
final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
if (child != null) // next line relies on double.infinity absorption
return child!.getMaxIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding;
return totalHorizontalPadding;
}
@override
double computeMinIntrinsicHeight(double width) {
_resolve();
final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
if (child != null) // next line relies on double.infinity absorption
return child!.getMinIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding;
return totalVerticalPadding;
}
@override
double computeMaxIntrinsicHeight(double width) {
_resolve();
final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
if (child != null) // next line relies on double.infinity absorption
return child!.getMaxIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding;
return totalVerticalPadding;
}
@override
Size computeDryLayout(BoxConstraints constraints) {
_resolve();
assert(_resolvedPadding != null);
if (child == null) {
return constraints.constrain(Size(
_resolvedPadding!.left + _resolvedPadding!.right,
_resolvedPadding!.top + _resolvedPadding!.bottom,
));
}
final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!);
final Size childSize = child!.getDryLayout(innerConstraints);
return constraints.constrain(Size(
_resolvedPadding!.left + childSize.width + _resolvedPadding!.right,
_resolvedPadding!.top + childSize.height + _resolvedPadding!.bottom,
));
}
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
_resolve();
assert(_resolvedPadding != null);
if (child == null) {
size = constraints.constrain(Size(
_resolvedPadding!.left + _resolvedPadding!.right,
_resolvedPadding!.top + _resolvedPadding!.bottom,
));
return;
}
final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!);
child!.layout(innerConstraints, parentUsesSize: true);
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = Offset(_resolvedPadding!.left, _resolvedPadding!.top);
size = constraints.constrain(Size(
_resolvedPadding!.left + child!.size.width + _resolvedPadding!.right,
_resolvedPadding!.top + child!.size.height + _resolvedPadding!.bottom,
));
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
super.debugPaintSize(context, offset);
assert(() {
final Rect outerRect = offset & size;
debugPaintPadding(context.canvas, outerRect, child != null ? _resolvedPadding!.deflateRect(outerRect) : null);
return true;
}());
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
}
}
/// Abstract class for one-child-layout render boxes that use a
/// [AlignmentGeometry] to align their children.
abstract class RenderAligningShiftedBox extends RenderShiftedBox {
/// Initializes member variables for subclasses.
///
/// The [alignment] argument must not be null.
///
/// The [textDirection] must be non-null if the [alignment] is
/// direction-sensitive.
RenderAligningShiftedBox({
AlignmentGeometry alignment = Alignment.center,
required TextDirection? textDirection,
RenderBox? child,
}) : assert(alignment != null),
_alignment = alignment,
_textDirection = textDirection,
super(child);
/// A constructor to be used only when the extending class also has a mixin.
// TODO(gspencer): Remove this constructor once https://github.com/dart-lang/sdk/issues/31543 is fixed.
@protected
RenderAligningShiftedBox.mixin(AlignmentGeometry alignment, TextDirection? textDirection, RenderBox? child)
: this(alignment: alignment, textDirection: textDirection, child: child);
Alignment? _resolvedAlignment;
void _resolve() {
if (_resolvedAlignment != null)
return;
_resolvedAlignment = alignment.resolve(textDirection);
}
void _markNeedResolution() {
_resolvedAlignment = null;
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 -1.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.0 means that the center of the child is aligned
/// with the center of the parent.
///
/// If this is set to an [AlignmentDirectional] object, then
/// [textDirection] must not be null.
AlignmentGeometry get alignment => _alignment;
AlignmentGeometry _alignment;
/// Sets the alignment to a new value, and triggers a layout update.
///
/// The new alignment must not be null.
set alignment(AlignmentGeometry value) {
assert(value != null);
if (_alignment == value)
return;
_alignment = value;
_markNeedResolution();
}
/// The text direction with which to resolve [alignment].
///
/// This may be changed to null, but only after [alignment] has been changed
/// to a value that does not depend on the direction.
TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection;
set textDirection(TextDirection? value) {
if (_textDirection == value)
return;
_textDirection = value;
_markNeedResolution();
}
/// Apply the current [alignment] to the [child].
///
/// Subclasses should call this method if they have a child, to have
/// this class perform the actual alignment. If there is no child,
/// do not call this method.
///
/// This method must be called after the child has been laid out and
/// this object's own size has been set.
@protected
void alignChild() {
_resolve();
assert(child != null);
assert(!child!.debugNeedsLayout);
assert(child!.hasSize);
assert(hasSize);
assert(_resolvedAlignment != null);
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
}
}
/// Positions its child using an [AlignmentGeometry].
///
/// 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 [Alignment.bottomRight].
///
/// 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 RenderAligningShiftedBox {
/// Creates a render object that positions its child.
RenderPositionedBox({
RenderBox? child,
double? widthFactor,
double? heightFactor,
AlignmentGeometry alignment = Alignment.center,
TextDirection? textDirection,
}) : assert(widthFactor == null || widthFactor >= 0.0),
assert(heightFactor == null || heightFactor >= 0.0),
_widthFactor = widthFactor,
_heightFactor = heightFactor,
super(child: child, alignment: alignment, textDirection: textDirection);
/// If non-null, sets its width to the child's width multiplied by this factor.
///
/// Can be both greater and less than 1.0 but must be positive.
double? get widthFactor => _widthFactor;
double? _widthFactor;
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 multiplied by this factor.
///
/// Can be both greater and less than 1.0 but must be positive.
double? get heightFactor => _heightFactor;
double? _heightFactor;
set heightFactor(double? value) {
assert(value == null || value >= 0.0);
if (_heightFactor == value)
return;
_heightFactor = value;
markNeedsLayout();
}
@override
Size computeDryLayout(BoxConstraints constraints) {
final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;
if (child != null) {
final Size childSize = child!.getDryLayout(constraints.loosen());
return constraints.constrain(Size(
shrinkWrapWidth ? childSize.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? childSize.height * (_heightFactor ?? 1.0) : double.infinity,
));
}
return constraints.constrain(Size(
shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity,
));
}
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
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(Size(
shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
));
alignChild();
} else {
size = constraints.constrain(Size(
shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity,
));
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
super.debugPaintSize(context, offset);
assert(() {
final Paint paint;
if (child != null && !child!.size.isEmpty) {
final Path path;
paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = const Color(0xFFFFFF00);
path = Path();
final BoxParentData childParentData = child!.parentData! as BoxParentData;
if (childParentData.offset.dy > 0.0) {
// vertical alignment arrows
final 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
final 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 = Paint()
..color = const Color(0x90909090);
context.canvas.drawRect(offset & size, paint);
}
return true;
}());
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('widthFactor', _widthFactor, ifNull: 'expand'));
properties.add(DoubleProperty('heightFactor', _heightFactor, ifNull: 'expand'));
}
}
/// 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
/// RenderConstrainedOverflowBox with minHeight and maxHeight set to 50.0.
/// Generally speaking, to avoid confusing behavior around hit testing, a
/// RenderConstrainedOverflowBox should usually be wrapped in a RenderClipRect.
///
/// The child is positioned according to [alignment]. To position a smaller
/// child inside a larger parent, use [RenderPositionedBox] and
/// [RenderConstrainedBox] rather than RenderConstrainedOverflowBox.
///
/// See also:
///
/// * [RenderUnconstrainedBox] for a render object that allows its children
/// to render themselves unconstrained, expands to fit them, and considers
/// overflow to be an error.
/// * [RenderSizedOverflowBox], a render object that is a specific size but
/// passes its original constraints through to its child, which it allows to
/// overflow.
class RenderConstrainedOverflowBox extends RenderAligningShiftedBox {
/// Creates a render object that lets its child overflow itself.
RenderConstrainedOverflowBox({
RenderBox? child,
double? minWidth,
double? maxWidth,
double? minHeight,
double? maxHeight,
AlignmentGeometry alignment = Alignment.center,
TextDirection? textDirection,
}) : _minWidth = minWidth,
_maxWidth = maxWidth,
_minHeight = minHeight,
_maxHeight = maxHeight,
super(child: child, alignment: alignment, textDirection: textDirection);
/// 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;
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;
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;
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;
set maxHeight(double? value) {
if (_maxHeight == value)
return;
_maxHeight = value;
markNeedsLayout();
}
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
return BoxConstraints(
minWidth: _minWidth ?? constraints.minWidth,
maxWidth: _maxWidth ?? constraints.maxWidth,
minHeight: _minHeight ?? constraints.minHeight,
maxHeight: _maxHeight ?? constraints.maxHeight,
);
}
@override
bool get sizedByParent => true;
@override
Size computeDryLayout(BoxConstraints constraints) {
return constraints.biggest;
}
@override
void performLayout() {
if (child != null) {
child?.layout(_getInnerConstraints(constraints), parentUsesSize: true);
alignChild();
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('minWidth', minWidth, ifNull: 'use parent minWidth constraint'));
properties.add(DoubleProperty('maxWidth', maxWidth, ifNull: 'use parent maxWidth constraint'));
properties.add(DoubleProperty('minHeight', minHeight, ifNull: 'use parent minHeight constraint'));
properties.add(DoubleProperty('maxHeight', maxHeight, ifNull: 'use parent maxHeight constraint'));
}
}
/// A [RenderBox] that applies an arbitrary transform to its [constraints]
/// before sizing its child using the new constraints, treating any overflow as
/// error.
///
/// This [RenderBox] sizes its child using a [BoxConstraints] created by
/// applying [constraintsTransform] to this [RenderBox]'s own [constraints].
/// This box will then attempt to adopt the same size, within the limits of its
/// own constraints. If it ends up with a different size, it will align the
/// child based on [alignment]. If the box cannot expand enough to accommodate
/// the entire child, the child will be clipped if [clipBehavior] is not
/// [Clip.none].
///
/// In debug mode, if the child overflows the box, a warning will be printed on
/// the console, and black and yellow striped areas will appear where the
/// overflow occurs.
///
/// When [child] is null, this [RenderBox] takes the smallest possible size and
/// never overflows.
///
/// This [RenderBox] can be used to ensure some of [child]'s natural dimensions
/// are honored, and get an early warning during development otherwise. For
/// instance, if [child] requires a minimum height to fully display its content,
/// [constraintsTransform] can be set to a function that removes the `maxHeight`
/// constraint from the incoming [BoxConstraints], so that if the parent
/// [RenderObject] fails to provide enough vertical space, a warning will be
/// displayed in debug mode, while still allowing [child] to grow vertically.
///
/// See also:
///
/// * [ConstraintsTransformBox], the widget that makes use of this
/// [RenderObject] and exposes the same functionality.
/// * [RenderConstrainedBox], which renders a box which imposes constraints
/// on its child.
/// * [RenderConstrainedOverflowBox], which renders a box that imposes different
/// constraints on its child than it gets from its parent, possibly allowing
/// the child to overflow the parent.
/// * [RenderUnconstrainedBox] which allows its children to render themselves
/// unconstrained, expands to fit them, and considers overflow to be an error.
class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugOverflowIndicatorMixin {
/// Creates a [RenderBox] that sizes itself to the child and modifies the
/// [constraints] before passing it down to that child.
///
/// The [alignment] and [clipBehavior] must not be null.
RenderConstraintsTransformBox({
required AlignmentGeometry alignment,
required TextDirection? textDirection,
required BoxConstraintsTransform constraintsTransform,
RenderBox? child,
Clip clipBehavior = Clip.none,
}) : assert(alignment != null),
assert(clipBehavior != null),
assert(constraintsTransform != null),
_constraintsTransform = constraintsTransform,
_clipBehavior = clipBehavior,
super.mixin(alignment, textDirection, child);
/// {@macro flutter.widgets.constraintsTransform}
BoxConstraintsTransform get constraintsTransform => _constraintsTransform;
BoxConstraintsTransform _constraintsTransform;
set constraintsTransform(BoxConstraintsTransform value) {
if (_constraintsTransform == value)
return;
_constraintsTransform = value;
// The RenderObject only needs layout if the new transform maps the current
// `constraints` to a different value, or the render object has never been
// laid out before.
final bool needsLayout = _childConstraints == null
|| _childConstraints != value(constraints);
if (needsLayout)
markNeedsLayout();
}
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.none], and must not be null.
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior;
set clipBehavior(Clip value) {
assert(value != null);
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
}
@override
double computeMinIntrinsicHeight(double width) {
return super.computeMinIntrinsicHeight(
constraintsTransform(BoxConstraints(maxWidth: width)).maxWidth,
);
}
@override
double computeMaxIntrinsicHeight(double width) {
return super.computeMaxIntrinsicHeight(
constraintsTransform(BoxConstraints(maxWidth: width)).maxWidth,
);
}
@override
double computeMinIntrinsicWidth(double height) {
return super.computeMinIntrinsicWidth(
constraintsTransform(BoxConstraints(maxHeight: height)).maxHeight,
);
}
@override
double computeMaxIntrinsicWidth(double height) {
return super.computeMaxIntrinsicWidth(
constraintsTransform(BoxConstraints(maxHeight: height)).maxHeight,
);
}
@override
Size computeDryLayout(BoxConstraints constraints) {
final Size? childSize = child?.getDryLayout(constraintsTransform(constraints));
return childSize == null ? constraints.smallest : constraints.constrain(childSize);
}
Rect _overflowContainerRect = Rect.zero;
Rect _overflowChildRect = Rect.zero;
bool _isOverflowing = false;
BoxConstraints? _childConstraints;
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
final RenderBox? child = this.child;
if (child != null) {
final BoxConstraints childConstraints = constraintsTransform(constraints);
assert(childConstraints != null);
assert(childConstraints.isNormalized, '$childConstraints is not normalized');
_childConstraints = childConstraints;
child.layout(childConstraints, parentUsesSize: true);
size = constraints.constrain(child.size);
alignChild();
final BoxParentData childParentData = child.parentData! as BoxParentData;
_overflowContainerRect = Offset.zero & size;
_overflowChildRect = childParentData.offset & child.size;
} else {
size = constraints.smallest;
_overflowContainerRect = Rect.zero;
_overflowChildRect = Rect.zero;
}
_isOverflowing = RelativeRect.fromRect(_overflowContainerRect, _overflowChildRect).hasInsets;
}
@override
void paint(PaintingContext context, Offset offset) {
// There's no point in drawing the child if we're empty, or there is no
// child.
if (child == null || size.isEmpty)
return;
if (!_isOverflowing) {
super.paint(context, offset);
return;
}
if (clipBehavior == Clip.none) {
_clipRectLayer.layer = null;
super.paint(context, offset);
} else {
// We have overflow and the clipBehavior isn't none. Clip it.
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
super.paint,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
}
// Display the overflow indicator.
assert(() {
paintOverflowIndicator(context, offset, _overflowContainerRect, _overflowChildRect);
return true;
}());
}
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override
Rect? describeApproximatePaintClip(RenderObject child) {
return _isOverflowing ? Offset.zero & size : null;
}
@override
String toStringShort() {
String header = super.toStringShort();
if (_isOverflowing)
header += ' OVERFLOWING';
return header;
}
}
/// Renders a box, imposing no constraints on its child, allowing the child to
/// render at its "natural" size.
///
/// The class is deprecated, use [RenderConstraintsTransformBox] instead.
///
/// This allows a child to render at the size it would render if it were alone
/// on an infinite canvas with no constraints. This box will then attempt to
/// adopt the same size, within the limits of its own constraints. If it ends
/// up with a different size, it will align the child based on [alignment].
/// If the box cannot expand enough to accommodate the entire child, the
/// child will be clipped.
///
/// In debug mode, if the child overflows the box, a warning will be printed on
/// the console, and black and yellow striped areas will appear where the
/// overflow occurs.
///
/// See also:
///
/// * [RenderConstrainedBox], which renders a box which imposes constraints
/// on its child.
/// * [RenderConstrainedOverflowBox], which renders a box that imposes different
/// constraints on its child than it gets from its parent, possibly allowing
/// the child to overflow the parent.
/// * [RenderSizedOverflowBox], a render object that is a specific size but
/// passes its original constraints through to its child, which it allows to
/// overflow.
///
@Deprecated(
'Use RenderConstraintsTransformBox instead. '
'This feature was deprecated after v2.1.0-11.0.pre.',
)
class RenderUnconstrainedBox extends RenderConstraintsTransformBox {
/// Create a render object that sizes itself to the child but does not
/// pass the [constraints] down to that child.
///
/// The [alignment] and [clipBehavior] must not be null.
@Deprecated(
'Use RenderConstraintsTransformBox instead. '
'This feature was deprecated after v2.1.0-11.0.pre.',
)
RenderUnconstrainedBox({
required AlignmentGeometry alignment,
required TextDirection? textDirection,
Axis? constrainedAxis,
RenderBox? child,
Clip clipBehavior = Clip.none,
}) : assert(alignment != null),
assert(clipBehavior != null),
_constrainedAxis = constrainedAxis,
super(
alignment: alignment,
textDirection: textDirection,
child: child,
clipBehavior: clipBehavior,
constraintsTransform: _convertAxis(constrainedAxis),
);
/// The axis to retain constraints on, if any.
///
/// If not set, or set to null (the default), neither axis will retain its
/// constraints. If set to [Axis.vertical], then vertical constraints will
/// be retained, and if set to [Axis.horizontal], then horizontal constraints
/// will be retained.
Axis? get constrainedAxis => _constrainedAxis;
Axis? _constrainedAxis;
set constrainedAxis(Axis? value) {
if (_constrainedAxis == value)
return;
_constrainedAxis = value;
constraintsTransform = _convertAxis(constrainedAxis);
}
static BoxConstraints _unconstrained(BoxConstraints constraints) => const BoxConstraints();
static BoxConstraints _widthConstrained(BoxConstraints constraints) => constraints.widthConstraints();
static BoxConstraints _heightConstrained(BoxConstraints constraints) => constraints.heightConstraints();
static BoxConstraintsTransform _convertAxis(Axis? constrainedAxis) {
if (constrainedAxis == null) {
return _unconstrained;
}
switch (constrainedAxis) {
case Axis.horizontal:
return _widthConstrained;
case Axis.vertical:
return _heightConstrained;
}
}
}
/// A render object that is a specific size but passes its original constraints
/// through to its child, which it allows to overflow.
///
/// If the child's resulting size differs from this render object's size, then
/// the child is aligned according to the [alignment] property.
///
/// See also:
///
/// * [RenderUnconstrainedBox] for a render object that allows its children
/// to render themselves unconstrained, expands to fit them, and considers
/// overflow to be an error.
/// * [RenderConstrainedOverflowBox] for a render object that imposes
/// different constraints on its child than it gets from its parent,
/// possibly allowing the child to overflow the parent.
class RenderSizedOverflowBox extends RenderAligningShiftedBox {
/// Creates a render box of a given size that lets its child overflow.
///
/// The [requestedSize] and [alignment] arguments must not be null.
///
/// The [textDirection] argument must not be null if the [alignment] is
/// direction-sensitive.
RenderSizedOverflowBox({
RenderBox? child,
required Size requestedSize,
AlignmentGeometry alignment = Alignment.center,
TextDirection? textDirection,
}) : assert(requestedSize != null),
_requestedSize = requestedSize,
super(child: child, alignment: alignment, textDirection: textDirection);
/// The size this render box should attempt to be.
Size get requestedSize => _requestedSize;
Size _requestedSize;
set requestedSize(Size value) {
assert(value != null);
if (_requestedSize == value)
return;
_requestedSize = value;
markNeedsLayout();
}
@override
double computeMinIntrinsicWidth(double height) {
return _requestedSize.width;
}
@override
double computeMaxIntrinsicWidth(double height) {
return _requestedSize.width;
}
@override
double computeMinIntrinsicHeight(double width) {
return _requestedSize.height;
}
@override
double computeMaxIntrinsicHeight(double width) {
return _requestedSize.height;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
if (child != null)
return child!.getDistanceToActualBaseline(baseline);
return super.computeDistanceToActualBaseline(baseline);
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return constraints.constrain(_requestedSize);
}
@override
void performLayout() {
size = constraints.constrain(_requestedSize);
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
alignChild();
}
}
}
/// Sizes its child to a fraction of the total available space.
///
/// For both its width and height, this render object imposes a tight
/// constraint on its child that is a multiple (typically less than 1.0) of the
/// maximum constraint it received from its parent on that axis. If the factor
/// for a given axis is null, then the constraints from the parent are just
/// passed through instead.
///
/// It then tries to size itself to the size of its child. Where this is not
/// possible (e.g. if the constraints from the parent are themselves tight), the
/// child is aligned according to [alignment].
class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox {
/// Creates a render box that sizes its child to a fraction of the total available space.
///
/// If non-null, the [widthFactor] and [heightFactor] arguments must be
/// non-negative.
///
/// The [alignment] must not be null.
///
/// The [textDirection] must be non-null if the [alignment] is
/// direction-sensitive.
RenderFractionallySizedOverflowBox({
RenderBox? child,
double? widthFactor,
double? heightFactor,
AlignmentGeometry alignment = Alignment.center,
TextDirection? textDirection,
}) : _widthFactor = widthFactor,
_heightFactor = heightFactor,
super(child: child, alignment: alignment, textDirection: textDirection) {
assert(_widthFactor == null || _widthFactor! >= 0.0);
assert(_heightFactor == null || _heightFactor! >= 0.0);
}
/// If non-null, the factor of the incoming width to use.
///
/// If non-null, the child is given a tight width constraint that is the max
/// incoming width constraint multiplied by this factor. If null, the child is
/// given the incoming width constraints.
double? get widthFactor => _widthFactor;
double? _widthFactor;
set widthFactor(double? value) {
assert(value == null || value >= 0.0);
if (_widthFactor == value)
return;
_widthFactor = value;
markNeedsLayout();
}
/// If non-null, the factor of the incoming height to use.
///
/// If non-null, the child is given a tight height constraint that is the max
/// incoming width constraint multiplied by this factor. If null, the child is
/// given the incoming width constraints.
double? get heightFactor => _heightFactor;
double? _heightFactor;
set heightFactor(double? value) {
assert(value == null || value >= 0.0);
if (_heightFactor == value)
return;
_heightFactor = value;
markNeedsLayout();
}
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
double minWidth = constraints.minWidth;
double maxWidth = constraints.maxWidth;
if (_widthFactor != null) {
final double width = maxWidth * _widthFactor!;
minWidth = width;
maxWidth = width;
}
double minHeight = constraints.minHeight;
double maxHeight = constraints.maxHeight;
if (_heightFactor != null) {
final double height = maxHeight * _heightFactor!;
minHeight = height;
maxHeight = height;
}
return BoxConstraints(
minWidth: minWidth,
maxWidth: maxWidth,
minHeight: minHeight,
maxHeight: maxHeight,
);
}
@override
double computeMinIntrinsicWidth(double height) {
final double result;
if (child == null) {
result = super.computeMinIntrinsicWidth(height);
} else { // the following line relies on double.infinity absorption
result = child!.getMinIntrinsicWidth(height * (_heightFactor ?? 1.0));
}
assert(result.isFinite);
return result / (_widthFactor ?? 1.0);
}
@override
double computeMaxIntrinsicWidth(double height) {
final double result;
if (child == null) {
result = super.computeMaxIntrinsicWidth(height);
} else { // the following line relies on double.infinity absorption
result = child!.getMaxIntrinsicWidth(height * (_heightFactor ?? 1.0));
}
assert(result.isFinite);
return result / (_widthFactor ?? 1.0);
}
@override
double computeMinIntrinsicHeight(double width) {
final double result;
if (child == null) {
result = super.computeMinIntrinsicHeight(width);
} else { // the following line relies on double.infinity absorption
result = child!.getMinIntrinsicHeight(width * (_widthFactor ?? 1.0));
}
assert(result.isFinite);
return result / (_heightFactor ?? 1.0);
}
@override
double computeMaxIntrinsicHeight(double width) {
final double result;
if (child == null) {
result = super.computeMaxIntrinsicHeight(width);
} else { // the following line relies on double.infinity absorption
result = child!.getMaxIntrinsicHeight(width * (_widthFactor ?? 1.0));
}
assert(result.isFinite);
return result / (_heightFactor ?? 1.0);
}
@override
Size computeDryLayout(BoxConstraints constraints) {
if (child != null) {
final Size childSize = child!.getDryLayout(_getInnerConstraints(constraints));
return constraints.constrain(childSize);
}
return constraints.constrain(_getInnerConstraints(constraints).constrain(Size.zero));
}
@override
void performLayout() {
if (child != null) {
child!.layout(_getInnerConstraints(constraints), parentUsesSize: true);
size = constraints.constrain(child!.size);
alignChild();
} else {
size = constraints.constrain(_getInnerConstraints(constraints).constrain(Size.zero));
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('widthFactor', _widthFactor, ifNull: 'pass-through'));
properties.add(DoubleProperty('heightFactor', _heightFactor, ifNull: 'pass-through'));
}
}
/// A delegate for computing the layout of a render object with a single child.
///
/// Used by [CustomSingleChildLayout] (in the widgets library) and
/// [RenderCustomSingleChildLayoutBox] (in the rendering library).
///
/// When asked to layout, [CustomSingleChildLayout] first calls [getSize] with
/// its incoming constraints to determine its size. It then calls
/// [getConstraintsForChild] to determine the constraints to apply to the child.
/// After the child completes its layout, [RenderCustomSingleChildLayoutBox]
/// calls [getPositionForChild] to determine the child's position.
///
/// The [shouldRelayout] method is called when a new instance of the class
/// is provided, to check if the new instance actually represents different
/// information.
///
/// The most efficient way to trigger a relayout is to supply a `relayout`
/// argument to the constructor of the [SingleChildLayoutDelegate]. The custom
/// layout will listen to this value and relayout whenever the Listenable
/// notifies its listeners, such as when an [Animation] ticks. This allows
/// the custom layout to avoid the build phase of the pipeline.
///
/// See also:
///
/// * [CustomSingleChildLayout], the widget that uses this delegate.
/// * [RenderCustomSingleChildLayoutBox], render object that uses this
/// delegate.
abstract class SingleChildLayoutDelegate {
/// Creates a layout delegate.
///
/// The layout will update whenever [relayout] notifies its listeners.
const SingleChildLayoutDelegate({ Listenable? relayout }) : _relayout = relayout;
final Listenable? _relayout;
/// The size of this object given the incoming constraints.
///
/// Defaults to the biggest size that satisfies the given constraints.
Size getSize(BoxConstraints constraints) => constraints.biggest;
/// The constraints for the child given the incoming constraints.
///
/// During layout, the child is given the layout constraints returned by this
/// function. The child is required to pick a size for itself that satisfies
/// these constraints.
///
/// Defaults to the given constraints.
BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints;
/// The position where the child should be placed.
///
/// The `size` argument is the size of the parent, which might be different
/// from the value returned by [getSize] if that size doesn't satisfy the
/// constraints passed to [getSize]. The `childSize` argument is the size of
/// the child, which will satisfy the constraints returned by
/// [getConstraintsForChild].
///
/// Defaults to positioning the child in the upper left corner of the parent.
Offset getPositionForChild(Size size, Size childSize) => Offset.zero;
/// Called whenever a new instance of the custom layout delegate class is
/// provided to the [RenderCustomSingleChildLayoutBox] object, or any time
/// that a new [CustomSingleChildLayout] object is created with a new instance
/// of the custom layout delegate class (which amounts to the same thing,
/// because the latter is implemented in terms of the former).
///
/// If the new instance represents different information than the old
/// instance, then the method should return true, otherwise it should return
/// false.
///
/// If the method returns false, then the [getSize],
/// [getConstraintsForChild], and [getPositionForChild] calls might be
/// optimized away.
///
/// It's possible that the layout methods will get called even if
/// [shouldRelayout] returns false (e.g. if an ancestor changed its layout).
/// It's also possible that the layout method will get called
/// without [shouldRelayout] being called at all (e.g. if the parent changes
/// size).
bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate);
}
/// 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 {
/// Creates a render box that defers its layout to a delegate.
///
/// The [delegate] argument must not be null.
RenderCustomSingleChildLayoutBox({
RenderBox? child,
required SingleChildLayoutDelegate delegate,
}) : assert(delegate != null),
_delegate = delegate,
super(child);
/// A delegate that controls this object's layout.
SingleChildLayoutDelegate get delegate => _delegate;
SingleChildLayoutDelegate _delegate;
set delegate(SingleChildLayoutDelegate newDelegate) {
assert(newDelegate != null);
if (_delegate == newDelegate)
return;
final SingleChildLayoutDelegate oldDelegate = _delegate;
if (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRelayout(oldDelegate))
markNeedsLayout();
_delegate = newDelegate;
if (attached) {
oldDelegate._relayout?.removeListener(markNeedsLayout);
newDelegate._relayout?.addListener(markNeedsLayout);
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_delegate._relayout?.addListener(markNeedsLayout);
}
@override
void detach() {
_delegate._relayout?.removeListener(markNeedsLayout);
super.detach();
}
Size _getSize(BoxConstraints constraints) {
return constraints.constrain(_delegate.getSize(constraints));
}
// TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to
// figure out the intrinsic dimensions. We really should either not support intrinsics,
// or we should expose intrinsic delegate callbacks and throw if they're not implemented.
@override
double computeMinIntrinsicWidth(double height) {
final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
if (width.isFinite)
return width;
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
if (width.isFinite)
return width;
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
if (height.isFinite)
return height;
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
if (height.isFinite)
return height;
return 0.0;
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return _getSize(constraints);
}
@override
void performLayout() {
size = _getSize(constraints);
if (child != null) {
final BoxConstraints childConstraints = delegate.getConstraintsForChild(constraints);
assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true));
child!.layout(childConstraints, parentUsesSize: !childConstraints.isTight);
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = delegate.getPositionForChild(size, childConstraints.isTight ? childConstraints.smallest : child!.size);
}
}
}
/// Shifts the child down such that the child's baseline (or the
/// bottom of the child, if the child has no baseline) is [baseline]
/// logical pixels below the top of this box, then sizes this box to
/// contain the child.
///
/// If [baseline] is less than the distance from the top of the child
/// to the baseline of the child, then the child will overflow the top
/// of the box. This is typically not desirable, in particular, that
/// part of the child will not be found when doing hit tests, so the
/// user cannot interact with that part of the child.
///
/// This box will be sized so that its bottom is coincident with the
/// bottom of the child. This means if this box shifts the child down,
/// there will be space between the top of this box and the top of the
/// child, but there is never space between the bottom of the child
/// and the bottom of the box.
class RenderBaseline extends RenderShiftedBox {
/// Creates a [RenderBaseline] object.
///
/// The [baseline] and [baselineType] arguments must not be null.
RenderBaseline({
RenderBox? child,
required double baseline,
required TextBaseline baselineType,
}) : assert(baseline != null),
assert(baselineType != null),
_baseline = baseline,
_baselineType = baselineType,
super(child);
/// 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;
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;
set baselineType(TextBaseline value) {
assert(value != null);
if (_baselineType == value)
return;
_baselineType = value;
markNeedsLayout();
}
@override
Size computeDryLayout(BoxConstraints constraints) {
if (child != null) {
assert(debugCannotComputeDryLayout(
reason: 'Baseline metrics are only available after a full layout.',
));
return Size.zero;
}
return constraints.smallest;
}
@override
void performLayout() {
if (child != null) {
final BoxConstraints constraints = this.constraints;
child!.layout(constraints.loosen(), parentUsesSize: true);
final double childBaseline = child!.getDistanceToBaseline(baselineType)!;
final double actualBaseline = baseline;
final double top = actualBaseline - childBaseline;
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = Offset(0.0, top);
final Size childSize = child!.size;
size = constraints.constrain(Size(childSize.width, top + childSize.height));
} else {
size = constraints.smallest;
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('baseline', baseline));
properties.add(EnumProperty<TextBaseline>('baselineType', baselineType));
}
}