778 lines
30 KiB
Dart

// 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.
/// @docImport 'package:flutter/widgets.dart';
library;
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'box.dart';
import 'layer.dart';
import 'layout_helper.dart';
import 'object.dart';
typedef _NextChild = RenderBox? Function(RenderBox child);
typedef _PositionChild = void Function(Offset offset, RenderBox child);
typedef _GetChildSize = Size Function(RenderBox child);
// A 2D vector that uses a [RenderWrap]'s main axis and cross axis as its first and second coordinate axes.
// It represents the same vector as (double mainAxisExtent, double crossAxisExtent).
extension type const _AxisSize._(Size _size) {
_AxisSize({ required double mainAxisExtent, required double crossAxisExtent }) : this._(Size(mainAxisExtent, crossAxisExtent));
_AxisSize.fromSize({ required Size size, required Axis direction }) : this._(_convert(size, direction));
static const _AxisSize empty = _AxisSize._(Size.zero);
static Size _convert(Size size, Axis direction) {
return switch (direction) {
Axis.horizontal => size,
Axis.vertical => size.flipped,
};
}
double get mainAxisExtent => _size.width;
double get crossAxisExtent => _size.height;
Size toSize(Axis direction) => _convert(_size, direction);
_AxisSize applyConstraints(BoxConstraints constraints, Axis direction) {
final BoxConstraints effectiveConstraints = switch (direction) {
Axis.horizontal => constraints,
Axis.vertical => constraints.flipped,
};
return _AxisSize._(effectiveConstraints.constrain(_size));
}
_AxisSize get flipped => _AxisSize._(_size.flipped);
_AxisSize operator +(_AxisSize other) => _AxisSize._(Size(_size.width + other._size.width, math.max(_size.height, other._size.height)));
_AxisSize operator -(_AxisSize other) => _AxisSize._(Size(_size.width - other._size.width, _size.height - other._size.height));
}
/// How [Wrap] should align objects.
///
/// Used both to align children within a run in the main axis as well as to
/// align the runs themselves in the cross axis.
enum WrapAlignment {
/// Place the objects as close to the start of the axis as possible.
///
/// If this value is used in a horizontal direction, a [TextDirection] must be
/// available to determine if the start is the left or the right.
///
/// If this value is used in a vertical direction, a [VerticalDirection] must be
/// available to determine if the start is the top or the bottom.
start,
/// Place the objects as close to the end of the axis as possible.
///
/// If this value is used in a horizontal direction, a [TextDirection] must be
/// available to determine if the end is the left or the right.
///
/// If this value is used in a vertical direction, a [VerticalDirection] must be
/// available to determine if the end is the top or the bottom.
end,
/// Place the objects as close to the middle of the axis as possible.
center,
/// Place the free space evenly between the objects.
spaceBetween,
/// Place the free space evenly between the objects as well as half of that
/// space before and after the first and last objects.
spaceAround,
/// Place the free space evenly between the objects as well as before and
/// after the first and last objects.
spaceEvenly;
(double leadingSpace, double betweenSpace) _distributeSpace(double freeSpace, double itemSpacing, int itemCount, bool flipped) {
assert(itemCount > 0);
return switch (this) {
WrapAlignment.start => (flipped ? freeSpace : 0.0, itemSpacing),
WrapAlignment.end => WrapAlignment.start._distributeSpace(freeSpace, itemSpacing, itemCount, !flipped),
WrapAlignment.spaceBetween when itemCount < 2 => WrapAlignment.start._distributeSpace(freeSpace, itemSpacing, itemCount, flipped),
WrapAlignment.center => (freeSpace / 2.0, itemSpacing),
WrapAlignment.spaceBetween => (0, freeSpace / (itemCount - 1) + itemSpacing),
WrapAlignment.spaceAround => (freeSpace / itemCount / 2, freeSpace / itemCount + itemSpacing),
WrapAlignment.spaceEvenly => (freeSpace / (itemCount + 1), freeSpace / (itemCount + 1) + itemSpacing),
};
}
}
/// Who [Wrap] should align children within a run in the cross axis.
enum WrapCrossAlignment {
/// Place the children as close to the start of the run in the cross axis as
/// possible.
///
/// If this value is used in a horizontal direction, a [TextDirection] must be
/// available to determine if the start is the left or the right.
///
/// If this value is used in a vertical direction, a [VerticalDirection] must be
/// available to determine if the start is the top or the bottom.
start,
/// Place the children as close to the end of the run in the cross axis as
/// possible.
///
/// If this value is used in a horizontal direction, a [TextDirection] must be
/// available to determine if the end is the left or the right.
///
/// If this value is used in a vertical direction, a [VerticalDirection] must be
/// available to determine if the end is the top or the bottom.
end,
/// Place the children as close to the middle of the run in the cross axis as
/// possible.
center;
// TODO(ianh): baseline.
WrapCrossAlignment get _flipped => switch (this) {
WrapCrossAlignment.start => WrapCrossAlignment.end,
WrapCrossAlignment.end => WrapCrossAlignment.start,
WrapCrossAlignment.center => WrapCrossAlignment.center,
};
double get _alignment => switch (this) {
WrapCrossAlignment.start => 0,
WrapCrossAlignment.end => 1,
WrapCrossAlignment.center => 0.5,
};
}
class _RunMetrics {
_RunMetrics(this.leadingChild, this.axisSize);
_AxisSize axisSize;
int childCount = 1;
RenderBox leadingChild;
// Look ahead, creates a new run if incorporating the child would exceed the allowed line width.
_RunMetrics? tryAddingNewChild(RenderBox child, _AxisSize childSize, bool flipMainAxis, double spacing, double maxMainExtent) {
final bool needsNewRun = axisSize.mainAxisExtent + childSize.mainAxisExtent + spacing - maxMainExtent > precisionErrorTolerance;
if (needsNewRun) {
return _RunMetrics(child, childSize);
} else {
axisSize += childSize + _AxisSize(mainAxisExtent: spacing, crossAxisExtent: 0.0);
childCount += 1;
if (flipMainAxis) {
leadingChild = child;
}
return null;
}
}
}
/// Parent data for use with [RenderWrap].
class WrapParentData extends ContainerBoxParentData<RenderBox> { }
/// Displays its children in multiple horizontal or vertical runs.
///
/// A [RenderWrap] lays out each child and attempts to place the child adjacent
/// to the previous child in the main axis, given by [direction], leaving
/// [spacing] space in between. If there is not enough space to fit the child,
/// [RenderWrap] creates a new _run_ adjacent to the existing children in the
/// cross axis.
///
/// After all the children have been allocated to runs, the children within the
/// runs are positioned according to the [alignment] in the main axis and
/// according to the [crossAxisAlignment] in the cross axis.
///
/// The runs themselves are then positioned in the cross axis according to the
/// [runSpacing] and [runAlignment].
class RenderWrap extends RenderBox
with ContainerRenderObjectMixin<RenderBox, WrapParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, WrapParentData> {
/// Creates a wrap render object.
///
/// By default, the wrap layout is horizontal and both the children and the
/// runs are aligned to the start.
RenderWrap({
List<RenderBox>? children,
Axis direction = Axis.horizontal,
WrapAlignment alignment = WrapAlignment.start,
double spacing = 0.0,
WrapAlignment runAlignment = WrapAlignment.start,
double runSpacing = 0.0,
WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start,
TextDirection? textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
Clip clipBehavior = Clip.none,
}) : _direction = direction,
_alignment = alignment,
_spacing = spacing,
_runAlignment = runAlignment,
_runSpacing = runSpacing,
_crossAxisAlignment = crossAxisAlignment,
_textDirection = textDirection,
_verticalDirection = verticalDirection,
_clipBehavior = clipBehavior {
addAll(children);
}
/// The direction to use as the main axis.
///
/// For example, if [direction] is [Axis.horizontal], the default, the
/// children are placed adjacent to one another in a horizontal run until the
/// available horizontal space is consumed, at which point a subsequent
/// children are placed in a new run vertically adjacent to the previous run.
Axis get direction => _direction;
Axis _direction;
set direction (Axis value) {
if (_direction == value) {
return;
}
_direction = value;
markNeedsLayout();
}
/// How the children within a run should be placed in the main axis.
///
/// For example, if [alignment] is [WrapAlignment.center], the children in
/// each run are grouped together in the center of their run in the main axis.
///
/// Defaults to [WrapAlignment.start].
///
/// See also:
///
/// * [runAlignment], which controls how the runs are placed relative to each
/// other in the cross axis.
/// * [crossAxisAlignment], which controls how the children within each run
/// are placed relative to each other in the cross axis.
WrapAlignment get alignment => _alignment;
WrapAlignment _alignment;
set alignment (WrapAlignment value) {
if (_alignment == value) {
return;
}
_alignment = value;
markNeedsLayout();
}
/// How much space to place between children in a run in the main axis.
///
/// For example, if [spacing] is 10.0, the children will be spaced at least
/// 10.0 logical pixels apart in the main axis.
///
/// If there is additional free space in a run (e.g., because the wrap has a
/// minimum size that is not filled or because some runs are longer than
/// others), the additional free space will be allocated according to the
/// [alignment].
///
/// Defaults to 0.0.
double get spacing => _spacing;
double _spacing;
set spacing (double value) {
if (_spacing == value) {
return;
}
_spacing = value;
markNeedsLayout();
}
/// How the runs themselves should be placed in the cross axis.
///
/// For example, if [runAlignment] is [WrapAlignment.center], the runs are
/// grouped together in the center of the overall [RenderWrap] in the cross
/// axis.
///
/// Defaults to [WrapAlignment.start].
///
/// See also:
///
/// * [alignment], which controls how the children within each run are placed
/// relative to each other in the main axis.
/// * [crossAxisAlignment], which controls how the children within each run
/// are placed relative to each other in the cross axis.
WrapAlignment get runAlignment => _runAlignment;
WrapAlignment _runAlignment;
set runAlignment (WrapAlignment value) {
if (_runAlignment == value) {
return;
}
_runAlignment = value;
markNeedsLayout();
}
/// How much space to place between the runs themselves in the cross axis.
///
/// For example, if [runSpacing] is 10.0, the runs will be spaced at least
/// 10.0 logical pixels apart in the cross axis.
///
/// If there is additional free space in the overall [RenderWrap] (e.g.,
/// because the wrap has a minimum size that is not filled), the additional
/// free space will be allocated according to the [runAlignment].
///
/// Defaults to 0.0.
double get runSpacing => _runSpacing;
double _runSpacing;
set runSpacing (double value) {
if (_runSpacing == value) {
return;
}
_runSpacing = value;
markNeedsLayout();
}
/// How the children within a run should be aligned relative to each other in
/// the cross axis.
///
/// For example, if this is set to [WrapCrossAlignment.end], and the
/// [direction] is [Axis.horizontal], then the children within each
/// run will have their bottom edges aligned to the bottom edge of the run.
///
/// Defaults to [WrapCrossAlignment.start].
///
/// See also:
///
/// * [alignment], which controls how the children within each run are placed
/// relative to each other in the main axis.
/// * [runAlignment], which controls how the runs are placed relative to each
/// other in the cross axis.
WrapCrossAlignment get crossAxisAlignment => _crossAxisAlignment;
WrapCrossAlignment _crossAxisAlignment;
set crossAxisAlignment (WrapCrossAlignment value) {
if (_crossAxisAlignment == value) {
return;
}
_crossAxisAlignment = value;
markNeedsLayout();
}
/// Determines the order to lay children out horizontally and how to interpret
/// `start` and `end` in the horizontal direction.
///
/// If the [direction] is [Axis.horizontal], this controls the order in which
/// children are positioned (left-to-right or right-to-left), and the meaning
/// of the [alignment] property's [WrapAlignment.start] and
/// [WrapAlignment.end] values.
///
/// If the [direction] is [Axis.horizontal], and either the
/// [alignment] is either [WrapAlignment.start] or [WrapAlignment.end], or
/// there's more than one child, then the [textDirection] must not be null.
///
/// If the [direction] is [Axis.vertical], this controls the order in
/// which runs are positioned, the meaning of the [runAlignment] property's
/// [WrapAlignment.start] and [WrapAlignment.end] values, as well as the
/// [crossAxisAlignment] property's [WrapCrossAlignment.start] and
/// [WrapCrossAlignment.end] values.
///
/// If the [direction] is [Axis.vertical], and either the
/// [runAlignment] is either [WrapAlignment.start] or [WrapAlignment.end], the
/// [crossAxisAlignment] is either [WrapCrossAlignment.start] or
/// [WrapCrossAlignment.end], or there's more than one child, then the
/// [textDirection] must not be null.
TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection;
set textDirection(TextDirection? value) {
if (_textDirection != value) {
_textDirection = value;
markNeedsLayout();
}
}
/// Determines the order to lay children out vertically and how to interpret
/// `start` and `end` in the vertical direction.
///
/// If the [direction] is [Axis.vertical], this controls which order children
/// are painted in (down or up), the meaning of the [alignment] property's
/// [WrapAlignment.start] and [WrapAlignment.end] values.
///
/// If the [direction] is [Axis.vertical], and either the [alignment]
/// is either [WrapAlignment.start] or [WrapAlignment.end], or there's
/// more than one child, then the [verticalDirection] must not be null.
///
/// If the [direction] is [Axis.horizontal], this controls the order in which
/// runs are positioned, the meaning of the [runAlignment] property's
/// [WrapAlignment.start] and [WrapAlignment.end] values, as well as the
/// [crossAxisAlignment] property's [WrapCrossAlignment.start] and
/// [WrapCrossAlignment.end] values.
///
/// If the [direction] is [Axis.horizontal], and either the
/// [runAlignment] is either [WrapAlignment.start] or [WrapAlignment.end], the
/// [crossAxisAlignment] is either [WrapCrossAlignment.start] or
/// [WrapCrossAlignment.end], or there's more than one child, then the
/// [verticalDirection] must not be null.
VerticalDirection get verticalDirection => _verticalDirection;
VerticalDirection _verticalDirection;
set verticalDirection(VerticalDirection value) {
if (_verticalDirection != value) {
_verticalDirection = value;
markNeedsLayout();
}
}
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.none].
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.none;
set clipBehavior(Clip value) {
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
}
bool get _debugHasNecessaryDirections {
if (firstChild != null && lastChild != firstChild) {
// i.e. there's more than one child
switch (direction) {
case Axis.horizontal:
assert(textDirection != null, 'Horizontal $runtimeType with multiple children has a null textDirection, so the layout order is undefined.');
case Axis.vertical:
break;
}
}
if (alignment == WrapAlignment.start || alignment == WrapAlignment.end) {
switch (direction) {
case Axis.horizontal:
assert(textDirection != null, 'Horizontal $runtimeType with alignment $alignment has a null textDirection, so the alignment cannot be resolved.');
case Axis.vertical:
break;
}
}
if (runAlignment == WrapAlignment.start || runAlignment == WrapAlignment.end) {
switch (direction) {
case Axis.horizontal:
break;
case Axis.vertical:
assert(textDirection != null, 'Vertical $runtimeType with runAlignment $runAlignment has a null textDirection, so the alignment cannot be resolved.');
}
}
if (crossAxisAlignment == WrapCrossAlignment.start || crossAxisAlignment == WrapCrossAlignment.end) {
switch (direction) {
case Axis.horizontal:
break;
case Axis.vertical:
assert(textDirection != null, 'Vertical $runtimeType with crossAxisAlignment $crossAxisAlignment has a null textDirection, so the alignment cannot be resolved.');
}
}
return true;
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! WrapParentData) {
child.parentData = WrapParentData();
}
}
@override
double computeMinIntrinsicWidth(double height) {
switch (direction) {
case Axis.horizontal:
double width = 0.0;
RenderBox? child = firstChild;
while (child != null) {
width = math.max(width, child.getMinIntrinsicWidth(double.infinity));
child = childAfter(child);
}
return width;
case Axis.vertical:
return getDryLayout(BoxConstraints(maxHeight: height)).width;
}
}
@override
double computeMaxIntrinsicWidth(double height) {
switch (direction) {
case Axis.horizontal:
double width = 0.0;
RenderBox? child = firstChild;
while (child != null) {
width += child.getMaxIntrinsicWidth(double.infinity);
child = childAfter(child);
}
return width;
case Axis.vertical:
return getDryLayout(BoxConstraints(maxHeight: height)).width;
}
}
@override
double computeMinIntrinsicHeight(double width) {
switch (direction) {
case Axis.horizontal:
return getDryLayout(BoxConstraints(maxWidth: width)).height;
case Axis.vertical:
double height = 0.0;
RenderBox? child = firstChild;
while (child != null) {
height = math.max(height, child.getMinIntrinsicHeight(double.infinity));
child = childAfter(child);
}
return height;
}
}
@override
double computeMaxIntrinsicHeight(double width) {
switch (direction) {
case Axis.horizontal:
return getDryLayout(BoxConstraints(maxWidth: width)).height;
case Axis.vertical:
double height = 0.0;
RenderBox? child = firstChild;
while (child != null) {
height += child.getMaxIntrinsicHeight(double.infinity);
child = childAfter(child);
}
return height;
}
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
return defaultComputeDistanceToHighestActualBaseline(baseline);
}
double _getMainAxisExtent(Size childSize) {
return switch (direction) {
Axis.horizontal => childSize.width,
Axis.vertical => childSize.height,
};
}
double _getCrossAxisExtent(Size childSize) {
return switch (direction) {
Axis.horizontal => childSize.height,
Axis.vertical => childSize.width,
};
}
Offset _getOffset(double mainAxisOffset, double crossAxisOffset) {
return switch (direction) {
Axis.horizontal => Offset(mainAxisOffset, crossAxisOffset),
Axis.vertical => Offset(crossAxisOffset, mainAxisOffset),
};
}
(bool flipHorizontal, bool flipVertical) get _areAxesFlipped {
final bool flipHorizontal = switch (textDirection ?? TextDirection.ltr) {
TextDirection.ltr => false,
TextDirection.rtl => true,
};
final bool flipVertical = switch (verticalDirection) {
VerticalDirection.down => false,
VerticalDirection.up => true,
};
return switch (direction) {
Axis.horizontal => (flipHorizontal, flipVertical),
Axis.vertical => (flipVertical, flipHorizontal),
};
}
@override
double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
if (firstChild == null) {
return null;
}
final BoxConstraints childConstraints = switch (direction) {
Axis.horizontal => BoxConstraints(maxWidth: constraints.maxWidth),
Axis.vertical => BoxConstraints(maxHeight: constraints.maxHeight),
};
final (_AxisSize childrenAxisSize, List<_RunMetrics> runMetrics) = _computeRuns(constraints, ChildLayoutHelper.dryLayoutChild);
final _AxisSize containerAxisSize = childrenAxisSize.applyConstraints(constraints, direction);
BaselineOffset baselineOffset = BaselineOffset.noBaseline;
void findHighestBaseline(Offset offset, RenderBox child) {
baselineOffset = baselineOffset.minOf(BaselineOffset(child.getDryBaseline(childConstraints, baseline)) + offset.dy);
}
Size getChildSize(RenderBox child) => child.getDryLayout(childConstraints);
_positionChildren(runMetrics, childrenAxisSize, containerAxisSize, findHighestBaseline, getChildSize);
return baselineOffset.offset;
}
@override
@protected
Size computeDryLayout(covariant BoxConstraints constraints) {
return _computeDryLayout(constraints);
}
Size _computeDryLayout(BoxConstraints constraints, [ChildLayouter layoutChild = ChildLayoutHelper.dryLayoutChild]) {
final (BoxConstraints childConstraints, double mainAxisLimit) = switch (direction) {
Axis.horizontal => (BoxConstraints(maxWidth: constraints.maxWidth), constraints.maxWidth),
Axis.vertical => (BoxConstraints(maxHeight: constraints.maxHeight), constraints.maxHeight),
};
double mainAxisExtent = 0.0;
double crossAxisExtent = 0.0;
double runMainAxisExtent = 0.0;
double runCrossAxisExtent = 0.0;
int childCount = 0;
RenderBox? child = firstChild;
while (child != null) {
final Size childSize = layoutChild(child, childConstraints);
final double childMainAxisExtent = _getMainAxisExtent(childSize);
final double childCrossAxisExtent = _getCrossAxisExtent(childSize);
// There must be at least one child before we move on to the next run.
if (childCount > 0 && runMainAxisExtent + childMainAxisExtent + spacing > mainAxisLimit) {
mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent);
crossAxisExtent += runCrossAxisExtent + runSpacing;
runMainAxisExtent = 0.0;
runCrossAxisExtent = 0.0;
childCount = 0;
}
runMainAxisExtent += childMainAxisExtent;
runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent);
if (childCount > 0) {
runMainAxisExtent += spacing;
}
childCount += 1;
child = childAfter(child);
}
crossAxisExtent += runCrossAxisExtent;
mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent);
return constraints.constrain(switch (direction) {
Axis.horizontal => Size(mainAxisExtent, crossAxisExtent),
Axis.vertical => Size(crossAxisExtent, mainAxisExtent),
});
}
static Size _getChildSize(RenderBox child) => child.size;
static void _setChildPosition(Offset offset, RenderBox child) {
(child.parentData! as WrapParentData).offset = offset;
}
bool _hasVisualOverflow = false;
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
assert(_debugHasNecessaryDirections);
if (firstChild == null) {
size = constraints.smallest;
_hasVisualOverflow = false;
return;
}
final (_AxisSize childrenAxisSize, List<_RunMetrics> runMetrics) = _computeRuns(constraints, ChildLayoutHelper.layoutChild);
final _AxisSize containerAxisSize = childrenAxisSize.applyConstraints(constraints, direction);
size = containerAxisSize.toSize(direction);
final _AxisSize freeAxisSize = containerAxisSize - childrenAxisSize;
_hasVisualOverflow = freeAxisSize.mainAxisExtent < 0.0 || freeAxisSize.crossAxisExtent < 0.0;
_positionChildren(runMetrics, freeAxisSize, containerAxisSize, _setChildPosition, _getChildSize);
}
(_AxisSize childrenSize, List<_RunMetrics> runMetrics) _computeRuns(BoxConstraints constraints, ChildLayouter layoutChild) {
assert(firstChild != null);
final (BoxConstraints childConstraints, double mainAxisLimit) = switch (direction) {
Axis.horizontal => (BoxConstraints(maxWidth: constraints.maxWidth), constraints.maxWidth),
Axis.vertical => (BoxConstraints(maxHeight: constraints.maxHeight), constraints.maxHeight),
};
final (bool flipMainAxis, _) = _areAxesFlipped;
final double spacing = this.spacing;
final List<_RunMetrics> runMetrics = <_RunMetrics>[];
_RunMetrics? currentRun;
_AxisSize childrenAxisSize = _AxisSize.empty;
for (RenderBox? child = firstChild; child != null; child = childAfter(child)) {
final _AxisSize childSize = _AxisSize.fromSize(size: layoutChild(child, childConstraints), direction: direction);
final _RunMetrics? newRun = currentRun == null
? _RunMetrics(child, childSize)
: currentRun.tryAddingNewChild(child, childSize, flipMainAxis, spacing, mainAxisLimit);
if (newRun != null) {
runMetrics.add(newRun);
childrenAxisSize += currentRun?.axisSize.flipped ?? _AxisSize.empty;
currentRun = newRun;
}
}
assert(runMetrics.isNotEmpty);
final double totalRunSpacing = runSpacing * (runMetrics.length - 1);
childrenAxisSize += _AxisSize(mainAxisExtent: totalRunSpacing, crossAxisExtent: 0.0) + currentRun!.axisSize.flipped;
return (childrenAxisSize.flipped, runMetrics);
}
void _positionChildren(List<_RunMetrics> runMetrics, _AxisSize freeAxisSize, _AxisSize containerAxisSize, _PositionChild positionChild, _GetChildSize getChildSize) {
assert(runMetrics.isNotEmpty);
final double spacing = this.spacing;
final double crossAxisFreeSpace = math.max(0.0, freeAxisSize.crossAxisExtent);
final (bool flipMainAxis, bool flipCrossAxis) = _areAxesFlipped;
final WrapCrossAlignment effectiveCrossAlignment = flipCrossAxis ? crossAxisAlignment._flipped : crossAxisAlignment;
final (double runLeadingSpace, double runBetweenSpace) = runAlignment._distributeSpace(
crossAxisFreeSpace,
runSpacing,
runMetrics.length,
flipCrossAxis,
);
final _NextChild nextChild = flipMainAxis ? childBefore : childAfter;
double runCrossAxisOffset = runLeadingSpace;
final Iterable<_RunMetrics> runs = flipCrossAxis ? runMetrics.reversed : runMetrics;
for (final _RunMetrics run in runs) {
final double runCrossAxisExtent = run.axisSize.crossAxisExtent;
final int childCount = run.childCount;
final double mainAxisFreeSpace = math.max(0.0, containerAxisSize.mainAxisExtent - run.axisSize.mainAxisExtent);
final (double childLeadingSpace, double childBetweenSpace) = alignment._distributeSpace(mainAxisFreeSpace, spacing, childCount, flipMainAxis);
double childMainAxisOffset = childLeadingSpace;
int remainingChildCount = run.childCount;
for (RenderBox? child = run.leadingChild; child != null && remainingChildCount > 0; child = nextChild(child), remainingChildCount -= 1) {
final _AxisSize(mainAxisExtent: double childMainAxisExtent, crossAxisExtent: double childCrossAxisExtent) = _AxisSize.fromSize(size: getChildSize(child), direction: direction);
final double childCrossAxisOffset = effectiveCrossAlignment._alignment * (runCrossAxisExtent - childCrossAxisExtent);
positionChild(_getOffset(childMainAxisOffset, runCrossAxisOffset + childCrossAxisOffset), child);
childMainAxisOffset += childMainAxisExtent + childBetweenSpace;
}
runCrossAxisOffset += runCrossAxisExtent + runBetweenSpace;
}
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
return defaultHitTestChildren(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
// TODO(ianh): move the debug flex overflow paint logic somewhere common so
// it can be reused here
if (_hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
defaultPaint,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer.layer = null;
defaultPaint(context, offset);
}
}
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(EnumProperty<Axis>('direction', direction));
properties.add(EnumProperty<WrapAlignment>('alignment', alignment));
properties.add(DoubleProperty('spacing', spacing));
properties.add(EnumProperty<WrapAlignment>('runAlignment', runAlignment));
properties.add(DoubleProperty('runSpacing', runSpacing));
properties.add(DoubleProperty('crossAxisAlignment', runSpacing));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(EnumProperty<VerticalDirection>('verticalDirection', verticalDirection, defaultValue: VerticalDirection.down));
}
}