performLayout method

  1. @override
LayoutSize performLayout(
  1. LayoutConstraints constraints, [
  2. bool dry = false
])
override

Performs the complete flex layout algorithm.

This method implements the multi-pass CSS Flexbox layout algorithm:

  1. Initialization: Sets up viewport constraints and spacing calculations
  2. Line Breaking: Distributes children into flex lines based on wrapping rules
  3. Main Axis Sizing: Calculates flex grow/shrink for each line
  4. Cross Axis Sizing: Determines cross-axis sizes and alignment
  5. 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,
    ),
  };
}