blob: 09e675c6e6b3de74038e13822d6d99b740904c53 [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) {
return child?.getMinIntrinsicWidth(height) ?? 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
return child?.getMaxIntrinsicWidth(height) ?? 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
return child?.getMinIntrinsicHeight(width) ?? 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
return child?.getMaxIntrinsicHeight(width) ?? 0.0;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
double? result;
final RenderBox? child = this.child;
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) {
final RenderBox? child = this.child;
if (child != null) {
final BoxParentData childParentData = child.parentData! as BoxParentData;
context.paintChild(child, childParentData.offset + offset);
}
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
final RenderBox? child = this.child;
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) {
// 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) {
// 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) {
// 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) {
// 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);
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({
super.child,
double? widthFactor,
double? heightFactor,
super.alignment,
super.textDirection,
}) : assert(widthFactor == null || widthFactor >= 0.0),
assert(heightFactor == null || heightFactor >= 0.0),
_widthFactor = widthFactor,
_heightFactor = heightFactor;
/// 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:
///
/// * [RenderConstraintsTransformBox] for a render object that applies an
/// arbitrary transform to its constraints before sizing its child using
/// the new constraints, treating any overflow as 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({
super.child,
double? minWidth,
double? maxWidth,
double? minHeight,
double? maxHeight,
super.alignment,
super.textDirection,
}) : _minWidth = minWidth,
_maxWidth = maxWidth,
_minHeight = minHeight,
_maxHeight = maxHeight;
/// 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,
/// and sizes its child using the resulting [BoxConstraints], optionally
/// clipping, or treating the overflow as an 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 [clipBehavior] is [Clip.none] and the child overflows the
/// container, 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.
/// * [RenderConstraintsTransformBox] for a render object that applies an
/// arbitrary transform to its constraints before sizing its child using
/// the new constraints, treating any overflow as 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 super.alignment,
required super.textDirection,
required BoxConstraintsTransform constraintsTransform,
super.child,
Clip clipBehavior = Clip.none,
}) : assert(alignment != null),
assert(clipBehavior != null),
assert(constraintsTransform != null),
_constraintsTransform = constraintsTransform,
_clipBehavior = clipBehavior;
/// {@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}
///
/// {@macro flutter.widgets.ConstraintsTransformBox.clipBehavior}
///
/// Defaults to [Clip.none].
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;
}
// 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 if clipBehavior is Clip.none.
assert(() {
switch (clipBehavior) {
case Clip.none:
paintOverflowIndicator(context, offset, _overflowContainerRect, _overflowChildRect);
break;
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
break;
}
return true;
}());
}
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override
Rect? describeApproximatePaintClip(RenderObject child) {
switch (clipBehavior) {
case Clip.none:
return null;
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
return _isOverflowing ? Offset.zero & size : null;
}
}
@override
String toStringShort() {
String header = super.toStringShort();
if (!kReleaseMode) {
if (_isOverflowing) {
header += ' OVERFLOWING';
}
}
return header;
}
}
/// 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:
///
/// * [RenderConstraintsTransformBox] for a render object that applies an
/// arbitrary transform to its constraints before sizing its child using
/// the new constraints, treating any overflow as 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({
super.child,
required Size requestedSize,
super.alignment,
super.textDirection,
}) : assert(requestedSize != null),
_requestedSize = requestedSize;
/// 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({
super.child,
double? widthFactor,
double? heightFactor,
super.alignment,
super.textDirection,
}) : _widthFactor = widthFactor,
_heightFactor = heightFactor {
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));
}
}