performLayout method
Performs the complete flex layout algorithm.
This method implements the multi-pass CSS Flexbox layout algorithm:
- Initialization: Sets up viewport constraints and spacing calculations
- Line Breaking: Distributes children into flex lines based on wrapping rules
- Main Axis Sizing: Calculates flex grow/shrink for each line
- Cross Axis Sizing: Determines cross-axis sizes and alignment
- Positioning: Places children and lines within the container
When dry
is true, performs measurement without modifying child positions.
Absolute-positioned children are handled separately and don't affect sizing.
Returns the total size needed for the layout content.
Implementation
@override
LayoutSize performLayout(LayoutConstraints constraints, [bool dry = false]) {
double viewportWidth = constraints.maxWidth;
double viewportHeight = constraints.maxHeight;
bool avoidWrapping = false;
if (viewportWidth.isInfinite) {
viewportWidth = 0.0;
if (layout.direction.axis == LayoutAxis.horizontal) {
avoidWrapping = true;
}
}
if (viewportHeight.isInfinite) {
viewportHeight = 0.0;
if (layout.direction.axis == LayoutAxis.vertical) {
avoidWrapping = true;
}
}
double viewportMainSize = switch (layout.direction.axis) {
LayoutAxis.horizontal => viewportWidth,
LayoutAxis.vertical => viewportHeight,
};
double viewportCrossSize = switch (layout.direction.axis) {
LayoutAxis.horizontal => viewportHeight,
LayoutAxis.vertical => viewportWidth,
};
// viewport size might be infinite
FlexLayoutCache cache = FlexLayoutCache();
if (!dry) {
_cache = cache;
}
final mainSpacingStartUnit = switch (layout.direction.axis) {
LayoutAxis.horizontal => layout.padding.left,
LayoutAxis.vertical => layout.padding.top,
};
final mainSpacingEndUnit = switch (layout.direction.axis) {
LayoutAxis.horizontal => layout.padding.right,
LayoutAxis.vertical => layout.padding.bottom,
};
final crossSpacingStartUnit = switch (layout.direction.axis) {
LayoutAxis.horizontal => layout.padding.top,
LayoutAxis.vertical => layout.padding.left,
};
final crossSpacingEndUnit = switch (layout.direction.axis) {
LayoutAxis.horizontal => layout.padding.bottom,
LayoutAxis.vertical => layout.padding.right,
};
final mainSpacingUnit = switch (layout.direction.axis) {
LayoutAxis.horizontal => layout.rowGap,
LayoutAxis.vertical => layout.columnGap,
};
final crossSpacingUnit = switch (layout.direction.axis) {
LayoutAxis.horizontal => layout.columnGap,
LayoutAxis.vertical => layout.rowGap,
};
double mainSpacingStart = mainSpacingStartUnit.computeSpacing(
axis: layout.direction.axis,
viewportSize: viewportMainSize,
);
double mainSpacingEnd = mainSpacingEndUnit.computeSpacing(
axis: layout.direction.axis,
viewportSize: viewportMainSize,
);
double mainSpacing = mainSpacingUnit.computeSpacing(
viewportSize: viewportMainSize,
axis: layout.direction.axis,
);
double crossSpacingStart = crossSpacingStartUnit.computeSpacing(
axis: switch (layout.direction.axis) {
LayoutAxis.horizontal => LayoutAxis.vertical,
LayoutAxis.vertical => LayoutAxis.horizontal,
},
viewportSize: viewportCrossSize,
);
double crossSpacingEnd = crossSpacingEndUnit.computeSpacing(
viewportSize: viewportCrossSize,
axis: switch (layout.direction.axis) {
LayoutAxis.horizontal => LayoutAxis.vertical,
LayoutAxis.vertical => LayoutAxis.horizontal,
},
);
double crossSpacing = crossSpacingUnit.computeSpacing(
axis: switch (layout.direction.axis) {
LayoutAxis.horizontal => LayoutAxis.vertical,
LayoutAxis.vertical => LayoutAxis.horizontal,
},
viewportSize: viewportCrossSize,
);
// store padding in cache for later use
switch (layout.direction.axis) {
case LayoutAxis.horizontal:
cache.paddingTop = crossSpacingStart;
cache.paddingBottom = crossSpacingEnd;
cache.paddingLeft = mainSpacingStart;
cache.paddingRight = mainSpacingEnd;
case LayoutAxis.vertical:
cache.paddingTop = mainSpacingStart;
cache.paddingBottom = mainSpacingEnd;
cache.paddingLeft = crossSpacingStart;
cache.paddingRight = crossSpacingEnd;
}
// reduce viewport size by padding
viewportMainSize = max(
viewportMainSize - mainSpacingStart - mainSpacingEnd,
0.0,
);
viewportCrossSize = max(
viewportCrossSize - crossSpacingStart - crossSpacingEnd,
0.0,
);
ChildLayout? child = dry
? parent.getFirstDryLayout(this)
: parent.firstLayoutChild;
// first pass: measure flex-basis, total flex-grow, total shrink factor,
// and determine line breaks
FlexLineLayoutCache lineCache = cache.allocateNewLine();
// double usedCrossSize = 0.0;
double biggestCrossSize = 0.0;
while (child != null) {
final childCache = child.layoutCache as FlexChildLayoutCache;
assert(childCache.lineCache == null);
childCache.lineCache = lineCache;
lineCache.firstChild ??= child;
lineCache.lastChild = child;
final data = child.layoutData;
if (data.behavior == LayoutBehavior.absolute) {
child = child.nextSibling;
continue;
}
var mainSizeUnit = switch (layout.direction.axis) {
LayoutAxis.horizontal => data.width,
LayoutAxis.vertical => data.height,
};
var mainMaxSizeUnit = switch (layout.direction.axis) {
LayoutAxis.horizontal => data.maxWidth,
LayoutAxis.vertical => data.maxHeight,
};
var crossSizeUnit = switch (layout.direction.axis) {
LayoutAxis.horizontal => data.height,
LayoutAxis.vertical => data.width,
};
var crossMaxSizeUnit = switch (layout.direction.axis) {
LayoutAxis.horizontal => data.maxHeight,
LayoutAxis.vertical => data.maxWidth,
};
double? resolvedMainSize = mainSizeUnit?.computeSize(
parent: parent,
child: child,
layoutHandle: this,
axis: layout.direction.axis,
contentSize: LayoutSize.zero,
viewportSize: LayoutSize.zero,
);
double? resolvedCrossSize = crossSizeUnit?.computeSize(
parent: parent,
child: child,
layoutHandle: this,
axis: switch (layout.direction.axis) {
LayoutAxis.horizontal => LayoutAxis.vertical,
LayoutAxis.vertical => LayoutAxis.horizontal,
},
contentSize: LayoutSize.zero,
viewportSize: LayoutSize.zero,
);
double? aspectRatio = data.aspectRatio;
if (resolvedMainSize == null && resolvedCrossSize != null) {
if (aspectRatio != null) {
resolvedMainSize = resolvedCrossSize * aspectRatio;
}
} else if (resolvedMainSize != null && resolvedCrossSize == null) {
if (aspectRatio != null) {
resolvedCrossSize = resolvedMainSize / aspectRatio;
}
}
// am i missing something? why would they both still be nullable
final resolvedMaxMainSize = mainMaxSizeUnit?.computeSize(
parent: parent,
child: child,
layoutHandle: this,
axis: layout.direction.axis,
contentSize: LayoutSize.zero,
viewportSize: LayoutSize.zero,
);
final resolvedMaxCrossSize = crossMaxSizeUnit?.computeSize(
parent: parent,
child: child,
layoutHandle: this,
axis: switch (layout.direction.axis) {
LayoutAxis.horizontal => LayoutAxis.vertical,
LayoutAxis.vertical => LayoutAxis.horizontal,
},
contentSize: LayoutSize.zero,
viewportSize: LayoutSize.zero,
);
final resolvedMinMainSize = switch (layout.direction.axis) {
LayoutAxis.horizontal =>
data.minWidth?.computeSize(
parent: parent,
child: child,
layoutHandle: this,
axis: layout.direction.axis,
contentSize: LayoutSize.zero,
viewportSize: LayoutSize.zero,
) ??
0.0,
LayoutAxis.vertical =>
data.minHeight?.computeSize(
parent: parent,
child: child,
layoutHandle: this,
axis: layout.direction.axis,
contentSize: LayoutSize.zero,
viewportSize: LayoutSize.zero,
) ??
0.0,
};
final resolvedMinCrossSize = switch (layout.direction.axis) {
LayoutAxis.horizontal =>
data.minHeight?.computeSize(
parent: parent,
child: child,
layoutHandle: this,
axis: LayoutAxis.vertical,
contentSize: LayoutSize.zero,
viewportSize: LayoutSize.zero,
) ??
0.0,
LayoutAxis.vertical =>
data.minWidth?.computeSize(
parent: parent,
child: child,
layoutHandle: this,
axis: LayoutAxis.horizontal,
contentSize: LayoutSize.zero,
viewportSize: LayoutSize.zero,
) ??
0.0,
};
childCache.mainBasisSize = _clampNullableDouble(
resolvedMainSize,
resolvedMinMainSize,
resolvedMaxMainSize,
);
childCache.crossSize = _clampNullableDouble(
resolvedCrossSize,
resolvedMinCrossSize,
resolvedMaxCrossSize,
);
childCache.maxMainSize = resolvedMaxMainSize;
childCache.minMainSize = resolvedMinMainSize;
childCache.maxCrossSize = resolvedMaxCrossSize;
childCache.minCrossSize = resolvedMinCrossSize;
double newMainSize = lineCache.mainSize + (resolvedMainSize ?? 0.0);
if (lineCache.itemCount > 0) {
newMainSize += mainSpacing;
}
double usedMainSpace = newMainSize;
// determine if this child can fit in the current line
bool exceedsMaxItemsPerLine =
layout.maxItemsPerLine != null &&
lineCache.itemCount >= layout.maxItemsPerLine!;
bool shouldFlexWrap =
layout.wrap != FlexWrap.none &&
usedMainSpace > viewportMainSize &&
!avoidWrapping;
bool exceedsMaxLines =
layout.maxLines != null && lineCache.lineIndex >= layout.maxLines!;
bool hasMinimumOneItem = lineCache.itemCount > 0;
if ((shouldFlexWrap || exceedsMaxItemsPerLine) &&
hasMinimumOneItem &&
!exceedsMaxLines) {
lineCache = cache.allocateNewLine();
childCache.lineCache = lineCache;
lineCache.firstChild = child;
lineCache.lastChild = child;
newMainSize = (resolvedMainSize ?? 0.0);
// usedCrossSize += biggestCrossSize; // moved to the line-loop
biggestCrossSize = (resolvedCrossSize ?? 0.0);
} else if (resolvedCrossSize != null) {
biggestCrossSize = max(biggestCrossSize, resolvedCrossSize);
}
lineCache.usedMainSpacing += lineCache.itemCount > 0 ? mainSpacing : 0.0;
lineCache.mainSize = newMainSize;
double childShrinkFactor = (resolvedMainSize ?? 0.0) * data.flexShrink;
lineCache.totalShrinkFactor += childShrinkFactor;
lineCache.totalFlexGrow += data.flexGrow;
// note: cross size is determined by the biggest cross size in the line
// but it needs to be done after we determine the cross shrink factor
// lineCache.crossSize = max(
// lineCache.crossSize,
// resolvedCrossSize,
// );
lineCache.itemCount++;
child = child.nextSibling;
}
lineCache.lastChild = null; // last line last child is the end
// usedCrossSize += biggestCrossSize; // moved to the line-loop
// baseline checking
bool needsBaselineAlignment = layout.alignItems.needsBaseline(
parent: parent,
axis: switch (layout.direction.axis) {
LayoutAxis.horizontal => LayoutAxis.vertical,
LayoutAxis.vertical => LayoutAxis.horizontal,
},
);
//
double usedMainSize = 0.0;
double usedCrossSize = 0.0;
// check for flexes in the lines
FlexLineLayoutCache? line = cache.firstLine;
double stretchLineCrossSize = switch (layout.direction.axis) {
LayoutAxis.horizontal => viewportHeight / cache.lineCount,
LayoutAxis.vertical => viewportWidth / cache.lineCount,
};
while (line != null) {
bool lineResolved = false;
int resolveCount = 0;
double? biggestBaseline;
bool selfAlignNeedsBaseline = false;
double frozenMainSize = 0.0;
while (!lineResolved && resolveCount < 10) {
lineResolved = true;
double usedMainSpace = line.mainSize + frozenMainSize;
double availableMainSpace = viewportMainSize - usedMainSpace;
double biggestLineCrossSize = 0.0;
ChildLayout? child = line.firstChild;
ChildLayout? lastChild = line.lastChild;
while (child != null && child != lastChild) {
if (child.layoutData.behavior == LayoutBehavior.absolute) {
child = child.nextSibling;
continue;
}
final childCache = child.layoutCache as FlexChildLayoutCache;
if (needsBaselineAlignment) {
double? cachedBaseline = childCache.baseline;
if (cachedBaseline == null) {
double baseline = child.getDistanceToBaseline(
parent.textBaseline ?? LayoutTextBaseline.alphabetic,
);
childCache.baseline = cachedBaseline = baseline;
}
if (cachedBaseline.isFinite) {
biggestBaseline ??= 0.0;
biggestBaseline = max(biggestBaseline, cachedBaseline);
}
} else if (child.layoutData.alignSelf != null) {
// check for self-alignment that needs baseline
bool? cached = childCache.alignSelfNeedsBaseline;
if (cached == null) {
bool needsBaseline = child.layoutData.alignSelf!.needsBaseline(
parent: parent,
axis: switch (layout.direction.axis) {
LayoutAxis.horizontal => LayoutAxis.vertical,
LayoutAxis.vertical => LayoutAxis.horizontal,
},
);
childCache.alignSelfNeedsBaseline = cached = needsBaseline;
}
selfAlignNeedsBaseline = selfAlignNeedsBaseline || cached;
}
if (!childCache.frozen) {
double newSize = childCache.mainBasisSize ?? 0.0;
if (availableMainSpace > 0.0 && line.totalFlexGrow > 0.0) {
double growFactor =
child.layoutData.flexGrow / line.totalFlexGrow;
double growth = availableMainSpace * growFactor;
newSize += growth;
} else if (availableMainSpace < 0.0 &&
line.totalShrinkFactor > 0.0) {
double childShrink =
child.layoutData.flexShrink *
(childCache.mainBasisSize ?? 0.0);
double shrinkFactor = childShrink / line.totalShrinkFactor;
double shrinkage = availableMainSpace * shrinkFactor;
newSize += shrinkage;
}
double maxNewSize = childCache.maxMainSize ?? double.infinity;
double minNewSize = childCache.minMainSize ?? 0.0;
bool wasAdjusted = false;
if (newSize > maxNewSize) {
newSize = maxNewSize;
wasAdjusted = true;
} else if (newSize < minNewSize) {
newSize = minNewSize;
wasAdjusted = true;
}
if (wasAdjusted) {
// convert into non-flexible item
childCache.frozen = true;
line.totalFlexGrow -= child.layoutData.flexGrow;
line.totalShrinkFactor -=
child.layoutData.flexShrink *
(childCache.mainBasisSize ?? 0.0);
line.mainSize -= (childCache.mainBasisSize ?? 0.0);
frozenMainSize += newSize;
// request for another pass to distribute the remaining space
// without this item
lineResolved = false;
childCache.mainFlexSize = newSize;
break;
}
childCache.mainFlexSize = newSize;
}
if (childCache.crossSize != null) {
biggestLineCrossSize = max(
biggestLineCrossSize,
childCache.crossSize!,
);
}
child = child.nextSibling;
}
resolveCount++;
if (lineResolved) {
// line.crossSize = biggestLineCrossSize;
double lineCrossSize = biggestLineCrossSize;
line.crossSize = lineCrossSize;
line.mainSize += frozenMainSize;
}
}
if (!needsBaselineAlignment && selfAlignNeedsBaseline) {
// content does not need baseline alignment
// but apparently some of the items need it
// so we need to check the baselines anyway
// and since needsBaselineAlignment is false,
// we have not computed the baselines yet
ChildLayout? child = line.firstChild;
ChildLayout? lastChild = line.lastChild;
while (child != null && child != lastChild) {
if (child.layoutData.behavior == LayoutBehavior.absolute) {
child = child.nextSibling;
continue;
}
final childCache = child.layoutCache as FlexChildLayoutCache;
if (child.layoutData.alignSelf != null) {
// check for self-alignment that needs baseline
bool? cached = childCache.alignSelfNeedsBaseline;
if (cached == true) {
double? cachedBaseline = childCache.baseline;
if (cachedBaseline == null) {
double baseline = child.getDistanceToBaseline(
parent.textBaseline ?? LayoutTextBaseline.alphabetic,
);
childCache.baseline = cachedBaseline = baseline;
}
if (cachedBaseline.isFinite) {
biggestBaseline ??= 0.0;
biggestBaseline = max(biggestBaseline, cachedBaseline);
}
}
}
child = child.nextSibling;
}
}
// fallback of the baseline is the biggest cross size in the line
line.biggestBaseline = biggestBaseline ?? line.crossSize;
({
double additionalEndSpacing,
double additionalSpacing,
double additionalStartSpacing,
})?
spacingAdjustment = layout.justifyContent.adjustSpacing(
parent: parent,
axis: layout.direction.axis,
viewportSize: viewportMainSize,
contentSize: line.mainSize,
startSpacing: mainSpacingStart,
endSpacing: mainSpacingEnd,
spacing: mainSpacing,
affectedCount: line.itemCount,
);
line.mainSpacing = mainSpacing;
line.mainSpacingStart = mainSpacingStart;
line.mainSpacingEnd = mainSpacingEnd;
if (spacingAdjustment != null) {
// mainSpacingStart += spacingAdjustment.additionalStartSpacing;
// mainSpacingEnd += spacingAdjustment.additionalEndSpacing;
// mainSpacing += spacingAdjustment.additionalSpacing;
line.mainSpacingStart += spacingAdjustment.additionalStartSpacing;
line.mainSpacingEnd += spacingAdjustment.additionalEndSpacing;
line.mainSpacing += spacingAdjustment.additionalSpacing;
// usedMainSize += spacingAdjustment.additionalStartSpacing;
// usedMainSize += spacingAdjustment.additionalEndSpacing;
line.mainSize += spacingAdjustment.additionalStartSpacing;
line.mainSize += spacingAdjustment.additionalEndSpacing;
if (line.itemCount > 1) {
line.mainSize +=
(line.itemCount - 1) * spacingAdjustment.additionalSpacing;
}
}
// recompute cross spacing if needed
double? adjustedCrossSize = layout.alignItems.adjustSize(
parent: parent,
axis: switch (layout.direction.axis) {
LayoutAxis.horizontal => LayoutAxis.vertical,
LayoutAxis.vertical => LayoutAxis.horizontal,
},
viewportSize: switch (layout.direction.axis) {
LayoutAxis.horizontal => viewportHeight,
LayoutAxis.vertical => viewportWidth,
},
contentSize: stretchLineCrossSize,
);
if (adjustedCrossSize != null) {
line.crossSize = adjustedCrossSize;
}
usedMainSize = max(usedMainSize, line.mainSize);
usedCrossSize += line.crossSize;
line = line.nextLine;
}
if (cache.lineCount > 1) {
usedCrossSize +=
crossSpacingStart +
crossSpacingEnd +
(cache.lineCount - 1) * crossSpacing;
} else {
usedCrossSize += crossSpacingStart + crossSpacingEnd;
}
({
double additionalEndSpacing,
double additionalSpacing,
double additionalStartSpacing,
})?
spacingAdjustment = layout.alignContent.adjustSpacing(
parent: parent,
axis: layout.direction.axis,
viewportSize: viewportCrossSize,
contentSize: usedCrossSize,
startSpacing: crossSpacingStart,
endSpacing: crossSpacingEnd,
spacing: crossSpacing,
affectedCount: cache.lineCount,
);
if (spacingAdjustment != null) {
crossSpacingStart += spacingAdjustment.additionalStartSpacing;
crossSpacingEnd += spacingAdjustment.additionalEndSpacing;
crossSpacing += spacingAdjustment.additionalSpacing;
usedCrossSize += spacingAdjustment.additionalStartSpacing;
usedCrossSize += spacingAdjustment.additionalEndSpacing;
if (cache.lineCount > 1) {
usedCrossSize +=
(cache.lineCount - 1) * spacingAdjustment.additionalSpacing;
}
}
cache.crossStartSpacing = crossSpacingStart;
cache.crossEndSpacing = crossSpacingEnd;
cache.crossSpacing = crossSpacing;
return switch (layout.direction.axis) {
LayoutAxis.horizontal => LayoutSize(
usedMainSize + mainSpacingStart + mainSpacingEnd,
usedCrossSize,
),
LayoutAxis.vertical => LayoutSize(
usedCrossSize,
usedMainSize + mainSpacingStart + mainSpacingEnd,
),
};
}