flex static method

Vector2 flex({
  1. required Axis direction,
  2. required LayoutConstraints constraints,
  3. required Iterable<LayoutItem> items,
  4. required MainAxisAlignment mainAxisAlignment,
  5. required CrossAxisAlignment crossAxisAlignment,
  6. required MainAxisSize mainAxisSize,
  7. required double spacing,
})

Measures non-flex items, distributes remaining main-axis space to flex items, then positions everyone in exactly three passes.

Example

Let's say you have a 100 wide row holding a 10px leaf and two items flexed 1 and 3, set to MainAxisSize.max.

+----+---------+---------------------------+
|leaf|  flex 1 |          flex 3           |
| 10 |   22.5  |           67.5            |
+----+---------+---------------------------+
0    10        32.5                      100

The passes are as follows:

  1. Measure everything that isn't flexed, against an unbounded main axis. The leaf reports 10, consuming 10 of the row. The other two are skipped, owing 4 shares between them.
  2. Divide what's left. 90 across 4 shares is 22.5 each, so the flexed items are measured against 22.5 and 67.5. A tight fit must fill its share; a loose one may report back smaller.
  3. Walk a cursor along the main axis, placing each item and advancing by its extent plus spacing, landing them at 0, 10 and 32.5.

Implementation

static Vector2 flex({
  required Axis direction,
  required LayoutConstraints constraints,
  required Iterable<LayoutItem> items,
  required MainAxisAlignment mainAxisAlignment,
  required CrossAxisAlignment crossAxisAlignment,
  required MainAxisSize mainAxisSize,
  required double spacing,
}) {
  final crossAxis = flipAxis(direction);
  final maxMain = constraints.max.axis(direction);
  final canFlex = maxMain.isFinite;
  final crossMax = constraints.max.axis(crossAxis);
  final fillCross = crossAxisAlignment == .stretch;
  final childCount = items.length;

  // Flex divides whatever the main axis has left over, and an unbounded axis
  // never has a leftover. Pass 1 measures flexed items at their natural size
  // instead, which only holds up if nothing demanded to fill something.
  if (!canFlex) {
    assert(
      !items.any((item) => item.flex.factor > 0) || mainAxisSize == .min,
      'A flex with an unbounded main axis has no leftover space, so '
      'MainAxisSize.max has nothing to fill. Bound the main axis, or use '
      'MainAxisSize.min.',
    );

    assert(
      items.every((item) => item.flex.factor <= 0 || item.flex.fit == .loose),
      'A flex with an unbounded main axis has no share for FlexFit.tight to '
      'fill. Bound the main axis, or use LayoutFlex.flexible instead of '
      'LayoutFlex.expanded.',
    );
  }

  // Pass 1: lay out every non-flex item. If the main axis is unbounded,
  // every item (flex or not) is treated as non-flex here.
  //
  // Extents are kept as main/cross doubles rather than sizes, so passes 2
  // and 3 never re-derive them from an axis.
  final mains = List<double>.filled(childCount, 0);
  final crosses = List<double>.filled(childCount, 0);
  var totalFlex = 0;
  var consumedMain = spacing * math.max(0, childCount - 1);
  var maxCross = 0.0;

  // Identical for every item, so it is built once rather than per child.
  // Only a scaled item pays for one of its own, via [LayoutConstraints.descale].
  final looseConstraints = LayoutConstraints(
    min: direction.toVector2(main: 0, cross: fillCross ? crossMax : 0),
    max: direction.toVector2(main: double.infinity, cross: crossMax),
  );

  var index = 0;

  for (final item in items) {
    if (canFlex && item.flex.factor > 0) {
      totalFlex += item.flex.factor;
    } else {
      final scale = item.scale;
      item.layout(looseConstraints.descale(scale));
      final size = item.size;
      mains[index] = (size.axis(direction) * scale.axis(direction)).abs();
      crosses[index] = (size.axis(crossAxis) * scale.axis(crossAxis)).abs();
      consumedMain += mains[index];
      maxCross = math.max(maxCross, crosses[index]);
    }

    index += 1;
  }

  // Pass 2: distribute remaining main-axis space to flex items.
  if (canFlex && totalFlex > 0) {
    final spacePerFlex = math.max(0.0, maxMain - consumedMain) / totalFlex;

    index = 0;

    for (final item in items) {
      final flex = item.flex;

      if (flex.factor > 0) {
        final maxExtent = spacePerFlex * flex.factor;
        final minExtent = flex.fit == .tight ? maxExtent : 0.0;
        final childConstraints = LayoutConstraints(
          min: direction.toVector2(main: minExtent, cross: fillCross ? crossMax : 0),
          max: direction.toVector2(main: maxExtent, cross: crossMax),
        );

        final scale = item.scale;
        item.layout(childConstraints.descale(scale));
        final size = item.size;
        mains[index] = (size.axis(direction) * scale.axis(direction)).abs();
        crosses[index] = (size.axis(crossAxis) * scale.axis(crossAxis)).abs();
        consumedMain += mains[index];
        maxCross = math.max(maxCross, crosses[index]);
      }

      index += 1;
    }
  }

  final idealMain = mainAxisSize == .max && canFlex ? maxMain : consumedMain;
  final selfSize = switch (direction) {
    .horizontal => constraints.constrain(idealMain, maxCross),
    .vertical => constraints.constrain(maxCross, idealMain),
  };

  // Pass 3: position every item.
  final selfMain = selfSize.axis(direction);
  final selfCross = selfSize.axis(crossAxis);
  final freeMain = math.max(0.0, selfMain - consumedMain);
  final (leading, between) = distributeSpace(mainAxisAlignment, freeMain, childCount);

  var cursor = leading;

  index = 0;

  for (final item in items) {
    final crossOffset = crossAxisOffset(crossAxisAlignment, selfCross - crosses[index]);
    place(item, direction.toVector2(main: cursor, cross: crossOffset));
    cursor += mains[index] + spacing + between;
    index += 1;
  }

  return selfSize;
}