performLayout method

  1. @override
void performLayout()
override

Do the work of computing the layout for this render object.

Do not call this function directly: call layout instead. This function is called by layout when there is actually work to be done by this render object during layout. The layout constraints provided by your parent are available via the constraints getter.

If sizedByParent is true, then this function should not actually change the dimensions of this render object. Instead, that work should be done by performResize. If sizedByParent is false, then this function should both change the dimensions of this render object and instruct its children to layout.

In implementing this function, you must call layout on each of your children, passing true for parentUsesSize if your layout information is dependent on your child's layout information. Passing true for parentUsesSize ensures that this render object will undergo layout if the child undergoes layout. Otherwise, the child can change its layout information without informing this render object.

Some special RenderObject subclasses (such as the one used by OverlayPortal.overlayChildLayoutBuilder) call applyPaintTransform in their performLayout implementation. To ensure such RenderObjects get the up-to-date paint transform, RenderObject subclasses should typically update the paint transform (as reported by applyPaintTransform) in this method instead of paint.

Implementation

@override
void performLayout() {
  // ── Build occupancy grid from data model ─────────────
  //
  // The data model may contain "orphan" cells in slots already covered by a
  // span (residue from previous operations). The occupancy grid is
  // the only source of truth: determines which cell occupies each logical slot
  // (row, logical-column) and ignores orphan cells during layout and
  // paint. This solves the problem at the root without requiring that the
  // data model is always perfectly canonical.

  final int numRows = node.rows.length;

  // First pass: calculate numCols respecting the occupancy grid.
  int numCols = 0;
  {
    final List<List<bool>> occ = List.generate(numRows, (_) => []);
    for (int r = 0; r < numRows; r++) {
      int logCol = 0;
      for (final cell in node.rows[r].cells) {
        while (logCol < occ[r].length && occ[r][logCol]) {
          logCol++;
        }
        final endCol = logCol + cell.colSpan;
        final endRow = math.min(r + cell.rowSpan, numRows);
        for (int rr = r; rr < endRow; rr++) {
          while (occ[rr].length < endCol) {
            occ[rr].add(false);
          }
          for (int cc = logCol; cc < endCol; cc++) {
            occ[rr][cc] = true;
          }
        }
        if (endCol > numCols) numCols = endCol;
        logCol = endCol;
      }
    }
  }

  if (numCols == 0 || numRows == 0) {
    size = constraints.constrain(Size.zero);
    return;
  }

  // ── Larghezze colonne ─────────────────────────────────────────────────
  List<double> colWidths;
  if (_colWidths.length == numCols) {
    colWidths = List.from(_colWidths);
    final total = colWidths.fold(0.0, (s, w) => s + w);
    if (total > constraints.maxWidth && constraints.maxWidth > 0) {
      final scale = constraints.maxWidth / total;
      colWidths = colWidths.map((w) => math.max(w * scale, minCellWidth)).toList();
    }
  } else {
    colWidths = List.filled(numCols, math.max(constraints.maxWidth / numCols, minCellWidth));
  }

  // ── Final grid: (r, logCol) → (physCol, cell) ────────────────
  //
  // physCol = physical index of the cell in its row (to match it to the
  // corresponding render child). Only origin cells are registered;
  // slots covered by others' spans remain null.
  final grid = List.generate(
    numRows, (_) => List<(int, FluentCell)?>.filled(numCols, null),
  );
  final occupied = List.generate(numRows, (_) => List.filled(numCols, false));

  for (int r = 0; r < numRows; r++) {
    int logCol = 0;
    final cells = node.rows[r].cells;
    for (int c = 0; c < cells.length; c++) {
      final cell = cells[c];
      while (logCol < numCols && occupied[r][logCol]) {
        logCol++;
      }
      if (logCol >= numCols) break; // orphan cell: no slot available

      final endCol = math.min(logCol + cell.colSpan, numCols);
      final endRow = math.min(r + cell.rowSpan, numRows);
      grid[r][logCol] = (c, cell);
      for (int rr = r; rr < endRow; rr++) {
        for (int cc = logCol; cc < endCol; cc++) {
          occupied[rr][cc] = true;
        }
      }
      logCol = endCol;
    }
  }

  // ── Map render children: (rowIdx, physColIdx) → RenderBox ──────────
  //
  // Render children arrive in the order of _flattenCells (row×physical col).
  // First copy row/col from _RenderCellPositioned into parentData.
  RenderBox? child = firstChild;
  while (child != null) {
    final pd = child.parentData as FluentTableCellParentData;
    if (child is _RenderCellPositioned) {
      pd.row = child.row;
      pd.col = child.col;
      pd.colSpan = child.colSpan;
      pd.rowSpan = child.rowSpan;
    }
    child = pd.nextSibling;
  }

  final Map<(int, int), RenderBox> childMap = {};
  child = firstChild;
  while (child != null) {
    final pd = child.parentData as FluentTableCellParentData;
    childMap[(pd.row, pd.col)] = child;
    child = pd.nextSibling;
  }

  // ── First pass: measure heights from content ────────────────────────
  final List<double> rowHeights = node.rows
      .map((r) => r.rowHeight ?? minCellHeight.toDouble())
      .toList();

  for (int r = 0; r < numRows; r++) {
    for (int logCol = 0; logCol < numCols; logCol++) {
      final entry = grid[r][logCol];
      if (entry == null) continue;
      final (physCol, cell) = entry;
      final rc = childMap[(r, physCol)];
      if (rc == null) continue;

      double cellWidth = 0;
      for (int c = logCol; c < math.min(logCol + cell.colSpan, numCols); c++) {
        cellWidth += colWidths[c];
      }
      rc.layout(
        BoxConstraints(minWidth: cellWidth, maxWidth: cellWidth, minHeight: minCellHeight),
        parentUsesSize: true,
      );
      final heightPerRow = rc.size.height / cell.rowSpan;
      for (int rr = r; rr < math.min(r + cell.rowSpan, numRows); rr++) {
        rowHeights[rr] = math.max(rowHeights[rr], heightPerRow);
      }
    }
  }

  // ── Offsets ───────────────────────────────────────────────────────────
  final List<double> rowOffsets = List.filled(numRows, 0);
  double ry = 0;
  for (int r = 0; r < numRows; r++) { rowOffsets[r] = ry; ry += rowHeights[r]; }

  final List<double> colOffsets = List.filled(numCols, 0);
  double cx = 0;
  for (int c = 0; c < numCols; c++) { colOffsets[c] = cx; cx += colWidths[c]; }

  // ── Second pass: position and final relayout ──────────────────────
  child = firstChild;
  while (child != null) {
    final pd = child.parentData as FluentTableCellParentData;
    final r = pd.row;
    final physCol = pd.col;

    // Find the logical column of this cell in the grid
    int? logCol;
    for (int lc = 0; lc < numCols; lc++) {
      final entry = grid[r][lc];
      if (entry != null && entry.$1 == physCol) { logCol = lc; break; }
    }

    if (logCol == null) {
      // Orphan cell: hide it completely outside the viewport
      pd.offset = const Offset(-10000, -10000);
      pd.width = 0;
      pd.height = 0;
      child.layout(const BoxConstraints(maxWidth: 0, maxHeight: 0), parentUsesSize: true);
    } else {
      final cell = grid[r][logCol]!.$2;
      double cellWidth = 0;
      for (int c = logCol; c < math.min(logCol + cell.colSpan, numCols); c++) {
        cellWidth += colWidths[c];
      }
      double cellHeight = 0;
      for (int rr = r; rr < math.min(r + cell.rowSpan, numRows); rr++) {
        cellHeight += rowHeights[rr];
      }
      pd.offset = Offset(colOffsets[logCol], rowOffsets[r]);
      pd.width = cellWidth;
      pd.height = cellHeight;
      child.layout(
        BoxConstraints(minWidth: cellWidth, maxWidth: cellWidth, minHeight: cellHeight),
        parentUsesSize: true,
      );
    }

    child = pd.nextSibling;
  }

  _computedRowHeights = List.from(rowHeights);
  size = constraints.constrain(Size(cx, ry));
}