1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:collection';
6import 'dart:math' as math;
7
8import 'package:flutter/foundation.dart';
9
10import 'box.dart';
11import 'object.dart';
12import 'table_border.dart';
13
14/// Parent data used by [RenderTable] for its children.
15class TableCellParentData extends BoxParentData {
16 /// Where this cell should be placed vertically.
17 ///
18 /// When using [TableCellVerticalAlignment.baseline], the text baseline must be set as well.
19 TableCellVerticalAlignment? verticalAlignment;
20
21 /// The column that the child was in the last time it was laid out.
22 int? x;
23
24 /// The row that the child was in the last time it was laid out.
25 int? y;
26
27 @override
28 String toString() => '${super.toString()}; ${verticalAlignment == null ? "default vertical alignment" : "$verticalAlignment"}';
29}
30
31/// Base class to describe how wide a column in a [RenderTable] should be.
32///
33/// To size a column to a specific number of pixels, use a [FixedColumnWidth].
34/// This is the cheapest way to size a column.
35///
36/// Other algorithms that are relatively cheap include [FlexColumnWidth], which
37/// distributes the space equally among the flexible columns,
38/// [FractionColumnWidth], which sizes a column based on the size of the
39/// table's container.
40@immutable
41abstract class TableColumnWidth {
42 /// Abstract const constructor. This constructor enables subclasses to provide
43 /// const constructors so that they can be used in const expressions.
44 const TableColumnWidth();
45
46 /// The smallest width that the column can have.
47 ///
48 /// The `cells` argument is an iterable that provides all the cells
49 /// in the table for this column. Walking the cells is by definition
50 /// O(N), so algorithms that do that should be considered expensive.
51 ///
52 /// The `containerWidth` argument is the `maxWidth` of the incoming
53 /// constraints for the table, and might be infinite.
54 double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth);
55
56 /// The ideal width that the column should have. This must be equal
57 /// to or greater than the [minIntrinsicWidth]. The column might be
58 /// bigger than this width, e.g. if the column is flexible or if the
59 /// table's width ends up being forced to be bigger than the sum of
60 /// all the maxIntrinsicWidth values.
61 ///
62 /// The `cells` argument is an iterable that provides all the cells
63 /// in the table for this column. Walking the cells is by definition
64 /// O(N), so algorithms that do that should be considered expensive.
65 ///
66 /// The `containerWidth` argument is the `maxWidth` of the incoming
67 /// constraints for the table, and might be infinite.
68 double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth);
69
70 /// The flex factor to apply to the cell if there is any room left
71 /// over when laying out the table. The remaining space is
72 /// distributed to any columns with flex in proportion to their flex
73 /// value (higher values get more space).
74 ///
75 /// The `cells` argument is an iterable that provides all the cells
76 /// in the table for this column. Walking the cells is by definition
77 /// O(N), so algorithms that do that should be considered expensive.
78 double? flex(Iterable<RenderBox> cells) => null;
79
80 @override
81 String toString() => objectRuntimeType(this, 'TableColumnWidth');
82}
83
84/// Sizes the column according to the intrinsic dimensions of all the
85/// cells in that column.
86///
87/// This is a very expensive way to size a column.
88///
89/// A flex value can be provided. If specified (and non-null), the
90/// column will participate in the distribution of remaining space
91/// once all the non-flexible columns have been sized.
92class IntrinsicColumnWidth extends TableColumnWidth {
93 /// Creates a column width based on intrinsic sizing.
94 ///
95 /// This sizing algorithm is very expensive.
96 ///
97 /// The `flex` argument specifies the flex factor to apply to the column if
98 /// there is any room left over when laying out the table. If `flex` is
99 /// null (the default), the table will not distribute any extra space to the
100 /// column.
101 const IntrinsicColumnWidth({ double? flex }) : _flex = flex;
102
103 @override
104 double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
105 double result = 0.0;
106 for (final RenderBox cell in cells) {
107 result = math.max(result, cell.getMinIntrinsicWidth(double.infinity));
108 }
109 return result;
110 }
111
112 @override
113 double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
114 double result = 0.0;
115 for (final RenderBox cell in cells) {
116 result = math.max(result, cell.getMaxIntrinsicWidth(double.infinity));
117 }
118 return result;
119 }
120
121 final double? _flex;
122
123 @override
124 double? flex(Iterable<RenderBox> cells) => _flex;
125
126 @override
127 String toString() => '${objectRuntimeType(this, 'IntrinsicColumnWidth')}(flex: ${_flex?.toStringAsFixed(1)})';
128}
129
130/// Sizes the column to a specific number of pixels.
131///
132/// This is the cheapest way to size a column.
133class FixedColumnWidth extends TableColumnWidth {
134 /// Creates a column width based on a fixed number of logical pixels.
135 const FixedColumnWidth(this.value);
136
137 /// The width the column should occupy in logical pixels.
138 final double value;
139
140 @override
141 double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
142 return value;
143 }
144
145 @override
146 double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
147 return value;
148 }
149
150 @override
151 String toString() => '${objectRuntimeType(this, 'FixedColumnWidth')}(${debugFormatDouble(value)})';
152}
153
154/// Sizes the column to a fraction of the table's constraints' maxWidth.
155///
156/// This is a cheap way to size a column.
157class FractionColumnWidth extends TableColumnWidth {
158 /// Creates a column width based on a fraction of the table's constraints'
159 /// maxWidth.
160 const FractionColumnWidth(this.value);
161
162 /// The fraction of the table's constraints' maxWidth that this column should
163 /// occupy.
164 final double value;
165
166 @override
167 double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
168 if (!containerWidth.isFinite) {
169 return 0.0;
170 }
171 return value * containerWidth;
172 }
173
174 @override
175 double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
176 if (!containerWidth.isFinite) {
177 return 0.0;
178 }
179 return value * containerWidth;
180 }
181
182 @override
183 String toString() => '${objectRuntimeType(this, 'FractionColumnWidth')}($value)';
184}
185
186/// Sizes the column by taking a part of the remaining space once all
187/// the other columns have been laid out.
188///
189/// For example, if two columns have a [FlexColumnWidth], then half the
190/// space will go to one and half the space will go to the other.
191///
192/// This is a cheap way to size a column.
193class FlexColumnWidth extends TableColumnWidth {
194 /// Creates a column width based on a fraction of the remaining space once all
195 /// the other columns have been laid out.
196 const FlexColumnWidth([this.value = 1.0]);
197
198 /// The fraction of the remaining space once all the other columns have
199 /// been laid out that this column should occupy.
200 final double value;
201
202 @override
203 double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
204 return 0.0;
205 }
206
207 @override
208 double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
209 return 0.0;
210 }
211
212 @override
213 double flex(Iterable<RenderBox> cells) {
214 return value;
215 }
216
217 @override
218 String toString() => '${objectRuntimeType(this, 'FlexColumnWidth')}(${debugFormatDouble(value)})';
219}
220
221/// Sizes the column such that it is the size that is the maximum of
222/// two column width specifications.
223///
224/// For example, to have a column be 10% of the container width or
225/// 100px, whichever is bigger, you could use:
226///
227/// const MaxColumnWidth(const FixedColumnWidth(100.0), FractionColumnWidth(0.1))
228///
229/// Both specifications are evaluated, so if either specification is
230/// expensive, so is this.
231class MaxColumnWidth extends TableColumnWidth {
232 /// Creates a column width that is the maximum of two other column widths.
233 const MaxColumnWidth(this.a, this.b);
234
235 /// A lower bound for the width of this column.
236 final TableColumnWidth a;
237
238 /// Another lower bound for the width of this column.
239 final TableColumnWidth b;
240
241 @override
242 double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
243 return math.max(
244 a.minIntrinsicWidth(cells, containerWidth),
245 b.minIntrinsicWidth(cells, containerWidth),
246 );
247 }
248
249 @override
250 double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
251 return math.max(
252 a.maxIntrinsicWidth(cells, containerWidth),
253 b.maxIntrinsicWidth(cells, containerWidth),
254 );
255 }
256
257 @override
258 double? flex(Iterable<RenderBox> cells) {
259 final double? aFlex = a.flex(cells);
260 final double? bFlex = b.flex(cells);
261 if (aFlex == null) {
262 return bFlex;
263 } else if (bFlex == null) {
264 return aFlex;
265 }
266 return math.max(aFlex, bFlex);
267 }
268
269 @override
270 String toString() => '${objectRuntimeType(this, 'MaxColumnWidth')}($a, $b)';
271}
272
273/// Sizes the column such that it is the size that is the minimum of
274/// two column width specifications.
275///
276/// For example, to have a column be 10% of the container width but
277/// never bigger than 100px, you could use:
278///
279/// const MinColumnWidth(const FixedColumnWidth(100.0), FractionColumnWidth(0.1))
280///
281/// Both specifications are evaluated, so if either specification is
282/// expensive, so is this.
283class MinColumnWidth extends TableColumnWidth {
284 /// Creates a column width that is the minimum of two other column widths.
285 const MinColumnWidth(this.a, this.b);
286
287 /// An upper bound for the width of this column.
288 final TableColumnWidth a;
289
290 /// Another upper bound for the width of this column.
291 final TableColumnWidth b;
292
293 @override
294 double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
295 return math.min(
296 a.minIntrinsicWidth(cells, containerWidth),
297 b.minIntrinsicWidth(cells, containerWidth),
298 );
299 }
300
301 @override
302 double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) {
303 return math.min(
304 a.maxIntrinsicWidth(cells, containerWidth),
305 b.maxIntrinsicWidth(cells, containerWidth),
306 );
307 }
308
309 @override
310 double? flex(Iterable<RenderBox> cells) {
311 final double? aFlex = a.flex(cells);
312 final double? bFlex = b.flex(cells);
313 if (aFlex == null) {
314 return bFlex;
315 } else if (bFlex == null) {
316 return aFlex;
317 }
318 return math.min(aFlex, bFlex);
319 }
320
321 @override
322 String toString() => '${objectRuntimeType(this, 'MinColumnWidth')}($a, $b)';
323}
324
325/// Vertical alignment options for cells in [RenderTable] objects.
326///
327/// This is specified using [TableCellParentData] objects on the
328/// [RenderObject.parentData] of the children of the [RenderTable].
329enum TableCellVerticalAlignment {
330 /// Cells with this alignment are placed with their top at the top of the row.
331 top,
332
333 /// Cells with this alignment are vertically centered in the row.
334 middle,
335
336 /// Cells with this alignment are placed with their bottom at the bottom of the row.
337 bottom,
338
339 /// Cells with this alignment are aligned such that they all share the same
340 /// baseline. Cells with no baseline are top-aligned instead. The baseline
341 /// used is specified by [RenderTable.textBaseline]. It is not valid to use
342 /// the baseline value if [RenderTable.textBaseline] is not specified.
343 ///
344 /// This vertical alignment is relatively expensive because it causes the table
345 /// to compute the baseline for each cell in the row.
346 baseline,
347
348 /// Cells with this alignment are sized to be as tall as the row, then made to fit the row.
349 /// If all the cells have this alignment, then the row will have zero height.
350 fill,
351
352 /// Cells with this alignment are sized to be the same height as the tallest cell in the row.
353 intrinsicHeight
354}
355
356/// A table where the columns and rows are sized to fit the contents of the cells.
357class RenderTable extends RenderBox {
358 /// Creates a table render object.
359 ///
360 /// * `columns` must either be null or non-negative. If `columns` is null,
361 /// the number of columns will be inferred from length of the first sublist
362 /// of `children`.
363 /// * `rows` must either be null or non-negative. If `rows` is null, the
364 /// number of rows will be inferred from the `children`. If `rows` is not
365 /// null, then `children` must be null.
366 /// * `children` must either be null or contain lists of all the same length.
367 /// if `children` is not null, then `rows` must be null.
368 /// * [columnWidths] may be null, in which case it defaults to an empty map.
369 RenderTable({
370 int? columns,
371 int? rows,
372 Map<int, TableColumnWidth>? columnWidths,
373 TableColumnWidth defaultColumnWidth = const FlexColumnWidth(),
374 required TextDirection textDirection,
375 TableBorder? border,
376 List<Decoration?>? rowDecorations,
377 ImageConfiguration configuration = ImageConfiguration.empty,
378 TableCellVerticalAlignment defaultVerticalAlignment = TableCellVerticalAlignment.top,
379 TextBaseline? textBaseline,
380 List<List<RenderBox>>? children,
381 }) : assert(columns == null || columns >= 0),
382 assert(rows == null || rows >= 0),
383 assert(rows == null || children == null),
384 _textDirection = textDirection,
385 _columns = columns ?? (children != null && children.isNotEmpty ? children.first.length : 0),
386 _rows = rows ?? 0,
387 _columnWidths = columnWidths ?? HashMap<int, TableColumnWidth>(),
388 _defaultColumnWidth = defaultColumnWidth,
389 _border = border,
390 _textBaseline = textBaseline,
391 _defaultVerticalAlignment = defaultVerticalAlignment,
392 _configuration = configuration {
393 _children = <RenderBox?>[]..length = _columns * _rows;
394 this.rowDecorations = rowDecorations; // must use setter to initialize box painters array
395 children?.forEach(addRow);
396 }
397
398 // Children are stored in row-major order.
399 // _children.length must be rows * columns
400 List<RenderBox?> _children = const <RenderBox?>[];
401
402 /// The number of vertical alignment lines in this table.
403 ///
404 /// Changing the number of columns will remove any children that no longer fit
405 /// in the table.
406 ///
407 /// Changing the number of columns is an expensive operation because the table
408 /// needs to rearrange its internal representation.
409 int get columns => _columns;
410 int _columns;
411 set columns(int value) {
412 assert(value >= 0);
413 if (value == columns) {
414 return;
415 }
416 final int oldColumns = columns;
417 final List<RenderBox?> oldChildren = _children;
418 _columns = value;
419 _children = List<RenderBox?>.filled(columns * rows, null);
420 final int columnsToCopy = math.min(columns, oldColumns);
421 for (int y = 0; y < rows; y += 1) {
422 for (int x = 0; x < columnsToCopy; x += 1) {
423 _children[x + y * columns] = oldChildren[x + y * oldColumns];
424 }
425 }
426 if (oldColumns > columns) {
427 for (int y = 0; y < rows; y += 1) {
428 for (int x = columns; x < oldColumns; x += 1) {
429 final int xy = x + y * oldColumns;
430 if (oldChildren[xy] != null) {
431 dropChild(oldChildren[xy]!);
432 }
433 }
434 }
435 }
436 markNeedsLayout();
437 }
438
439 /// The number of horizontal alignment lines in this table.
440 ///
441 /// Changing the number of rows will remove any children that no longer fit
442 /// in the table.
443 int get rows => _rows;
444 int _rows;
445 set rows(int value) {
446 assert(value >= 0);
447 if (value == rows) {
448 return;
449 }
450 if (_rows > value) {
451 for (int xy = columns * value; xy < _children.length; xy += 1) {
452 if (_children[xy] != null) {
453 dropChild(_children[xy]!);
454 }
455 }
456 }
457 _rows = value;
458 _children.length = columns * rows;
459 markNeedsLayout();
460 }
461
462 /// How the horizontal extents of the columns of this table should be determined.
463 ///
464 /// If the [Map] has a null entry for a given column, the table uses the
465 /// [defaultColumnWidth] instead.
466 ///
467 /// The layout performance of the table depends critically on which column
468 /// sizing algorithms are used here. In particular, [IntrinsicColumnWidth] is
469 /// quite expensive because it needs to measure each cell in the column to
470 /// determine the intrinsic size of the column.
471 ///
472 /// This property can never return null. If it is set to null, and the existing
473 /// map is not empty, then the value is replaced by an empty map. (If it is set
474 /// to null while the current value is an empty map, the value is not changed.)
475 Map<int, TableColumnWidth>? get columnWidths => Map<int, TableColumnWidth>.unmodifiable(_columnWidths);
476 Map<int, TableColumnWidth> _columnWidths;
477 set columnWidths(Map<int, TableColumnWidth>? value) {
478 if (_columnWidths == value) {
479 return;
480 }
481 if (_columnWidths.isEmpty && value == null) {
482 return;
483 }
484 _columnWidths = value ?? HashMap<int, TableColumnWidth>();
485 markNeedsLayout();
486 }
487
488 /// Determines how the width of column with the given index is determined.
489 void setColumnWidth(int column, TableColumnWidth value) {
490 if (_columnWidths[column] == value) {
491 return;
492 }
493 _columnWidths[column] = value;
494 markNeedsLayout();
495 }
496
497 /// How to determine with widths of columns that don't have an explicit sizing algorithm.
498 ///
499 /// Specifically, the [defaultColumnWidth] is used for column `i` if
500 /// `columnWidths[i]` is null.
501 TableColumnWidth get defaultColumnWidth => _defaultColumnWidth;
502 TableColumnWidth _defaultColumnWidth;
503 set defaultColumnWidth(TableColumnWidth value) {
504 if (defaultColumnWidth == value) {
505 return;
506 }
507 _defaultColumnWidth = value;
508 markNeedsLayout();
509 }
510
511 /// The direction in which the columns are ordered.
512 TextDirection get textDirection => _textDirection;
513 TextDirection _textDirection;
514 set textDirection(TextDirection value) {
515 if (_textDirection == value) {
516 return;
517 }
518 _textDirection = value;
519 markNeedsLayout();
520 }
521
522 /// The style to use when painting the boundary and interior divisions of the table.
523 TableBorder? get border => _border;
524 TableBorder? _border;
525 set border(TableBorder? value) {
526 if (border == value) {
527 return;
528 }
529 _border = value;
530 markNeedsPaint();
531 }
532
533 /// The decorations to use for each row of the table.
534 ///
535 /// Row decorations fill the horizontal and vertical extent of each row in
536 /// the table, unlike decorations for individual cells, which might not fill
537 /// either.
538 List<Decoration?> get rowDecorations => List<Decoration?>.unmodifiable(_rowDecorations ?? const <Decoration>[]);
539 // _rowDecorations and _rowDecorationPainters need to be in sync. They have to
540 // either both be null or have same length.
541 List<Decoration?>? _rowDecorations;
542 List<BoxPainter?>? _rowDecorationPainters;
543 set rowDecorations(List<Decoration?>? value) {
544 if (_rowDecorations == value) {
545 return;
546 }
547 _rowDecorations = value;
548 if (_rowDecorationPainters != null) {
549 for (final BoxPainter? painter in _rowDecorationPainters!) {
550 painter?.dispose();
551 }
552 }
553 _rowDecorationPainters = _rowDecorations != null ? List<BoxPainter?>.filled(_rowDecorations!.length, null) : null;
554 }
555
556 /// The settings to pass to the [rowDecorations] when painting, so that they
557 /// can resolve images appropriately. See [ImageProvider.resolve] and
558 /// [BoxPainter.paint].
559 ImageConfiguration get configuration => _configuration;
560 ImageConfiguration _configuration;
561 set configuration(ImageConfiguration value) {
562 if (value == _configuration) {
563 return;
564 }
565 _configuration = value;
566 markNeedsPaint();
567 }
568
569 /// How cells that do not explicitly specify a vertical alignment are aligned vertically.
570 TableCellVerticalAlignment get defaultVerticalAlignment => _defaultVerticalAlignment;
571 TableCellVerticalAlignment _defaultVerticalAlignment;
572 set defaultVerticalAlignment(TableCellVerticalAlignment value) {
573 if (_defaultVerticalAlignment == value) {
574 return;
575 }
576 _defaultVerticalAlignment = value;
577 markNeedsLayout();
578 }
579
580 /// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline].
581 TextBaseline? get textBaseline => _textBaseline;
582 TextBaseline? _textBaseline;
583 set textBaseline(TextBaseline? value) {
584 if (_textBaseline == value) {
585 return;
586 }
587 _textBaseline = value;
588 markNeedsLayout();
589 }
590
591 @override
592 void setupParentData(RenderObject child) {
593 if (child.parentData is! TableCellParentData) {
594 child.parentData = TableCellParentData();
595 }
596 }
597
598 /// Replaces the children of this table with the given cells.
599 ///
600 /// The cells are divided into the specified number of columns before
601 /// replacing the existing children.
602 ///
603 /// If the new cells contain any existing children of the table, those
604 /// children are moved to their new location in the table rather than
605 /// removed from the table and re-added.
606 void setFlatChildren(int columns, List<RenderBox?> cells) {
607 if (cells == _children && columns == _columns) {
608 return;
609 }
610 assert(columns >= 0);
611 // consider the case of a newly empty table
612 if (columns == 0 || cells.isEmpty) {
613 assert(cells.isEmpty);
614 _columns = columns;
615 if (_children.isEmpty) {
616 assert(_rows == 0);
617 return;
618 }
619 for (final RenderBox? oldChild in _children) {
620 if (oldChild != null) {
621 dropChild(oldChild);
622 }
623 }
624 _rows = 0;
625 _children.clear();
626 markNeedsLayout();
627 return;
628 }
629 assert(cells.length % columns == 0);
630 // fill a set with the cells that are moving (it's important not
631 // to dropChild a child that's remaining with us, because that
632 // would clear their parentData field)
633 final Set<RenderBox> lostChildren = HashSet<RenderBox>();
634 for (int y = 0; y < _rows; y += 1) {
635 for (int x = 0; x < _columns; x += 1) {
636 final int xyOld = x + y * _columns;
637 final int xyNew = x + y * columns;
638 if (_children[xyOld] != null && (x >= columns || xyNew >= cells.length || _children[xyOld] != cells[xyNew])) {
639 lostChildren.add(_children[xyOld]!);
640 }
641 }
642 }
643 // adopt cells that are arriving, and cross cells that are just moving off our list of lostChildren
644 int y = 0;
645 while (y * columns < cells.length) {
646 for (int x = 0; x < columns; x += 1) {
647 final int xyNew = x + y * columns;
648 final int xyOld = x + y * _columns;
649 if (cells[xyNew] != null && (x >= _columns || y >= _rows || _children[xyOld] != cells[xyNew])) {
650 if (!lostChildren.remove(cells[xyNew])) {
651 adoptChild(cells[xyNew]!);
652 }
653 }
654 }
655 y += 1;
656 }
657 // drop all the lost children
658 lostChildren.forEach(dropChild);
659 // update our internal values
660 _columns = columns;
661 _rows = cells.length ~/ columns;
662 _children = List<RenderBox?>.of(cells);
663 assert(_children.length == rows * columns);
664 markNeedsLayout();
665 }
666
667 /// Replaces the children of this table with the given cells.
668 void setChildren(List<List<RenderBox>>? cells) {
669 // TODO(ianh): Make this smarter, like setFlatChildren
670 if (cells == null) {
671 setFlatChildren(0, const <RenderBox?>[]);
672 return;
673 }
674 for (final RenderBox? oldChild in _children) {
675 if (oldChild != null) {
676 dropChild(oldChild);
677 }
678 }
679 _children.clear();
680 _columns = cells.isNotEmpty ? cells.first.length : 0;
681 _rows = 0;
682 cells.forEach(addRow);
683 assert(_children.length == rows * columns);
684 }
685
686 /// Adds a row to the end of the table.
687 ///
688 /// The newly added children must not already have parents.
689 void addRow(List<RenderBox?> cells) {
690 assert(cells.length == columns);
691 assert(_children.length == rows * columns);
692 _rows += 1;
693 _children.addAll(cells);
694 for (final RenderBox? cell in cells) {
695 if (cell != null) {
696 adoptChild(cell);
697 }
698 }
699 markNeedsLayout();
700 }
701
702 /// Replaces the child at the given position with the given child.
703 ///
704 /// If the given child is already located at the given position, this function
705 /// does not modify the table. Otherwise, the given child must not already
706 /// have a parent.
707 void setChild(int x, int y, RenderBox? value) {
708 assert(x >= 0 && x < columns && y >= 0 && y < rows);
709 assert(_children.length == rows * columns);
710 final int xy = x + y * columns;
711 final RenderBox? oldChild = _children[xy];
712 if (oldChild == value) {
713 return;
714 }
715 if (oldChild != null) {
716 dropChild(oldChild);
717 }
718 _children[xy] = value;
719 if (value != null) {
720 adoptChild(value);
721 }
722 }
723
724 @override
725 void attach(PipelineOwner owner) {
726 super.attach(owner);
727 for (final RenderBox? child in _children) {
728 child?.attach(owner);
729 }
730 }
731
732 @override
733 void detach() {
734 super.detach();
735 if (_rowDecorationPainters != null) {
736 for (final BoxPainter? painter in _rowDecorationPainters!) {
737 painter?.dispose();
738 }
739 _rowDecorationPainters = List<BoxPainter?>.filled(_rowDecorations!.length, null);
740 }
741 for (final RenderBox? child in _children) {
742 child?.detach();
743 }
744 }
745
746 @override
747 void visitChildren(RenderObjectVisitor visitor) {
748 assert(_children.length == rows * columns);
749 for (final RenderBox? child in _children) {
750 if (child != null) {
751 visitor(child);
752 }
753 }
754 }
755
756 @override
757 double computeMinIntrinsicWidth(double height) {
758 assert(_children.length == rows * columns);
759 double totalMinWidth = 0.0;
760 for (int x = 0; x < columns; x += 1) {
761 final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth;
762 final Iterable<RenderBox> columnCells = column(x);
763 totalMinWidth += columnWidth.minIntrinsicWidth(columnCells, double.infinity);
764 }
765 return totalMinWidth;
766 }
767
768 @override
769 double computeMaxIntrinsicWidth(double height) {
770 assert(_children.length == rows * columns);
771 double totalMaxWidth = 0.0;
772 for (int x = 0; x < columns; x += 1) {
773 final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth;
774 final Iterable<RenderBox> columnCells = column(x);
775 totalMaxWidth += columnWidth.maxIntrinsicWidth(columnCells, double.infinity);
776 }
777 return totalMaxWidth;
778 }
779
780 @override
781 double computeMinIntrinsicHeight(double width) {
782 // winner of the 2016 world's most expensive intrinsic dimension function award
783 // honorable mention, most likely to improve if taught about memoization award
784 assert(_children.length == rows * columns);
785 final List<double> widths = _computeColumnWidths(BoxConstraints.tightForFinite(width: width));
786 double rowTop = 0.0;
787 for (int y = 0; y < rows; y += 1) {
788 double rowHeight = 0.0;
789 for (int x = 0; x < columns; x += 1) {
790 final int xy = x + y * columns;
791 final RenderBox? child = _children[xy];
792 if (child != null) {
793 rowHeight = math.max(rowHeight, child.getMaxIntrinsicHeight(widths[x]));
794 }
795 }
796 rowTop += rowHeight;
797 }
798 return rowTop;
799 }
800
801 @override
802 double computeMaxIntrinsicHeight(double width) {
803 return getMinIntrinsicHeight(width);
804 }
805
806 double? _baselineDistance;
807 @override
808 double? computeDistanceToActualBaseline(TextBaseline baseline) {
809 // returns the baseline offset of the cell in the first row with
810 // the lowest baseline, and uses `TableCellVerticalAlignment.baseline`.
811 assert(!debugNeedsLayout);
812 return _baselineDistance;
813 }
814
815 /// Returns the list of [RenderBox] objects that are in the given
816 /// column, in row order, starting from the first row.
817 ///
818 /// This is a lazily-evaluated iterable.
819 // The following uses sync* because it is public API documented to return a
820 // lazy iterable.
821 Iterable<RenderBox> column(int x) sync* {
822 for (int y = 0; y < rows; y += 1) {
823 final int xy = x + y * columns;
824 final RenderBox? child = _children[xy];
825 if (child != null) {
826 yield child;
827 }
828 }
829 }
830
831 /// Returns the list of [RenderBox] objects that are on the given
832 /// row, in column order, starting with the first column.
833 ///
834 /// This is a lazily-evaluated iterable.
835 // The following uses sync* because it is public API documented to return a
836 // lazy iterable.
837 Iterable<RenderBox> row(int y) sync* {
838 final int start = y * columns;
839 final int end = (y + 1) * columns;
840 for (int xy = start; xy < end; xy += 1) {
841 final RenderBox? child = _children[xy];
842 if (child != null) {
843 yield child;
844 }
845 }
846 }
847
848 List<double> _computeColumnWidths(BoxConstraints constraints) {
849 assert(_children.length == rows * columns);
850 // We apply the constraints to the column widths in the order of
851 // least important to most important:
852 // 1. apply the ideal widths (maxIntrinsicWidth)
853 // 2. grow the flex columns so that the table has the maxWidth (if
854 // finite) or the minWidth (if not)
855 // 3. if there were no flex columns, then grow the table to the
856 // minWidth.
857 // 4. apply the maximum width of the table, shrinking columns as
858 // necessary, applying minimum column widths as we go
859
860 // 1. apply ideal widths, and collect information we'll need later
861 final List<double> widths = List<double>.filled(columns, 0.0);
862 final List<double> minWidths = List<double>.filled(columns, 0.0);
863 final List<double?> flexes = List<double?>.filled(columns, null);
864 double tableWidth = 0.0; // running tally of the sum of widths[x] for all x
865 double unflexedTableWidth = 0.0; // sum of the maxIntrinsicWidths of any column that has null flex
866 double totalFlex = 0.0;
867 for (int x = 0; x < columns; x += 1) {
868 final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth;
869 final Iterable<RenderBox> columnCells = column(x);
870 // apply ideal width (maxIntrinsicWidth)
871 final double maxIntrinsicWidth = columnWidth.maxIntrinsicWidth(columnCells, constraints.maxWidth);
872 assert(maxIntrinsicWidth.isFinite);
873 assert(maxIntrinsicWidth >= 0.0);
874 widths[x] = maxIntrinsicWidth;
875 tableWidth += maxIntrinsicWidth;
876 // collect min width information while we're at it
877 final double minIntrinsicWidth = columnWidth.minIntrinsicWidth(columnCells, constraints.maxWidth);
878 assert(minIntrinsicWidth.isFinite);
879 assert(minIntrinsicWidth >= 0.0);
880 minWidths[x] = minIntrinsicWidth;
881 assert(maxIntrinsicWidth >= minIntrinsicWidth);
882 // collect flex information while we're at it
883 final double? flex = columnWidth.flex(columnCells);
884 if (flex != null) {
885 assert(flex.isFinite);
886 assert(flex > 0.0);
887 flexes[x] = flex;
888 totalFlex += flex;
889 } else {
890 unflexedTableWidth = unflexedTableWidth + maxIntrinsicWidth;
891 }
892 }
893 final double maxWidthConstraint = constraints.maxWidth;
894 final double minWidthConstraint = constraints.minWidth;
895
896 // 2. grow the flex columns so that the table has the maxWidth (if
897 // finite) or the minWidth (if not)
898 if (totalFlex > 0.0) {
899 // this can only grow the table, but it _will_ grow the table at
900 // least as big as the target width.
901 final double targetWidth;
902 if (maxWidthConstraint.isFinite) {
903 targetWidth = maxWidthConstraint;
904 } else {
905 targetWidth = minWidthConstraint;
906 }
907 if (tableWidth < targetWidth) {
908 final double remainingWidth = targetWidth - unflexedTableWidth;
909 assert(remainingWidth.isFinite);
910 assert(remainingWidth >= 0.0);
911 for (int x = 0; x < columns; x += 1) {
912 if (flexes[x] != null) {
913 final double flexedWidth = remainingWidth * flexes[x]! / totalFlex;
914 assert(flexedWidth.isFinite);
915 assert(flexedWidth >= 0.0);
916 if (widths[x] < flexedWidth) {
917 final double delta = flexedWidth - widths[x];
918 tableWidth += delta;
919 widths[x] = flexedWidth;
920 }
921 }
922 }
923 assert(tableWidth + precisionErrorTolerance >= targetWidth);
924 }
925 } // step 2 and 3 are mutually exclusive
926
927 // 3. if there were no flex columns, then grow the table to the
928 // minWidth.
929 else if (tableWidth < minWidthConstraint) {
930 final double delta = (minWidthConstraint - tableWidth) / columns;
931 for (int x = 0; x < columns; x += 1) {
932 widths[x] = widths[x] + delta;
933 }
934 tableWidth = minWidthConstraint;
935 }
936
937 // beyond this point, unflexedTableWidth is no longer valid
938
939 // 4. apply the maximum width of the table, shrinking columns as
940 // necessary, applying minimum column widths as we go
941 if (tableWidth > maxWidthConstraint) {
942 double deficit = tableWidth - maxWidthConstraint;
943 // Some columns may have low flex but have all the free space.
944 // (Consider a case with a 1px wide column of flex 1000.0 and
945 // a 1000px wide column of flex 1.0; the sizes coming from the
946 // maxIntrinsicWidths. If the maximum table width is 2px, then
947 // just applying the flexes to the deficit would result in a
948 // table with one column at -998px and one column at 990px,
949 // which is wildly unhelpful.)
950 // Similarly, some columns may be flexible, but not actually
951 // be shrinkable due to a large minimum width. (Consider a
952 // case with two columns, one is flex and one isn't, both have
953 // 1000px maxIntrinsicWidths, but the flex one has 1000px
954 // minIntrinsicWidth also. The whole deficit will have to come
955 // from the non-flex column.)
956 // So what we do is we repeatedly iterate through the flexible
957 // columns shrinking them proportionally until we have no
958 // available columns, then do the same to the non-flexible ones.
959 int availableColumns = columns;
960 while (deficit > precisionErrorTolerance && totalFlex > precisionErrorTolerance) {
961 double newTotalFlex = 0.0;
962 for (int x = 0; x < columns; x += 1) {
963 if (flexes[x] != null) {
964 final double newWidth = widths[x] - deficit * flexes[x]! / totalFlex;
965 assert(newWidth.isFinite);
966 if (newWidth <= minWidths[x]) {
967 // shrank to minimum
968 deficit -= widths[x] - minWidths[x];
969 widths[x] = minWidths[x];
970 flexes[x] = null;
971 availableColumns -= 1;
972 } else {
973 deficit -= widths[x] - newWidth;
974 widths[x] = newWidth;
975 newTotalFlex += flexes[x]!;
976 }
977 assert(widths[x] >= 0.0);
978 }
979 }
980 totalFlex = newTotalFlex;
981 }
982 while (deficit > precisionErrorTolerance && availableColumns > 0) {
983 // Now we have to take out the remaining space from the
984 // columns that aren't minimum sized.
985 // To make this fair, we repeatedly remove equal amounts from
986 // each column, clamped to the minimum width, until we run out
987 // of columns that aren't at their minWidth.
988 final double delta = deficit / availableColumns;
989 assert(delta != 0);
990 int newAvailableColumns = 0;
991 for (int x = 0; x < columns; x += 1) {
992 final double availableDelta = widths[x] - minWidths[x];
993 if (availableDelta > 0.0) {
994 if (availableDelta <= delta) {
995 // shrank to minimum
996 deficit -= widths[x] - minWidths[x];
997 widths[x] = minWidths[x];
998 } else {
999 deficit -= delta;
1000 widths[x] = widths[x] - delta;
1001 newAvailableColumns += 1;
1002 }
1003 }
1004 }
1005 availableColumns = newAvailableColumns;
1006 }
1007 }
1008 return widths;
1009 }
1010
1011 // cache the table geometry for painting purposes
1012 final List<double> _rowTops = <double>[];
1013 Iterable<double>? _columnLefts;
1014 late double _tableWidth;
1015
1016 /// Returns the position and dimensions of the box that the given
1017 /// row covers, in this render object's coordinate space (so the
1018 /// left coordinate is always 0.0).
1019 ///
1020 /// The row being queried must exist.
1021 ///
1022 /// This is only valid after layout.
1023 Rect getRowBox(int row) {
1024 assert(row >= 0);
1025 assert(row < rows);
1026 assert(!debugNeedsLayout);
1027 return Rect.fromLTRB(0.0, _rowTops[row], size.width, _rowTops[row + 1]);
1028 }
1029
1030 @override
1031 double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
1032 if (rows * columns == 0) {
1033 return null;
1034 }
1035 final List<double> widths = _computeColumnWidths(constraints);
1036 double? baselineOffset;
1037 for (int col = 0; col < columns; col += 1) {
1038 final RenderBox? child = _children[col];
1039 final BoxConstraints childConstraints = BoxConstraints.tightFor(width: widths[col]);
1040 if (child == null) {
1041 continue;
1042 }
1043 final TableCellParentData childParentData = child.parentData! as TableCellParentData;
1044 final double? childBaseline = switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
1045 TableCellVerticalAlignment.baseline => child.getDryBaseline(childConstraints, baseline),
1046 TableCellVerticalAlignment.baseline ||
1047 TableCellVerticalAlignment.top ||
1048 TableCellVerticalAlignment.middle ||
1049 TableCellVerticalAlignment.bottom ||
1050 TableCellVerticalAlignment.fill ||
1051 TableCellVerticalAlignment.intrinsicHeight => null,
1052 };
1053 if (childBaseline != null && (baselineOffset == null || baselineOffset < childBaseline)) {
1054 baselineOffset = childBaseline;
1055 }
1056 }
1057 return baselineOffset;
1058 }
1059
1060 @override
1061 @protected
1062 Size computeDryLayout(covariant BoxConstraints constraints) {
1063 if (rows * columns == 0) {
1064 return constraints.constrain(Size.zero);
1065 }
1066 final List<double> widths = _computeColumnWidths(constraints);
1067 final double tableWidth = widths.fold(0.0, (double a, double b) => a + b);
1068 double rowTop = 0.0;
1069 for (int y = 0; y < rows; y += 1) {
1070 double rowHeight = 0.0;
1071 for (int x = 0; x < columns; x += 1) {
1072 final int xy = x + y * columns;
1073 final RenderBox? child = _children[xy];
1074 if (child != null) {
1075 final TableCellParentData childParentData = child.parentData! as TableCellParentData;
1076 switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
1077 case TableCellVerticalAlignment.baseline:
1078 assert(debugCannotComputeDryLayout(
1079 reason: 'TableCellVerticalAlignment.baseline requires a full layout for baseline metrics to be available.',
1080 ));
1081 return Size.zero;
1082 case TableCellVerticalAlignment.top:
1083 case TableCellVerticalAlignment.middle:
1084 case TableCellVerticalAlignment.bottom:
1085 case TableCellVerticalAlignment.intrinsicHeight:
1086 final Size childSize = child.getDryLayout(BoxConstraints.tightFor(width: widths[x]));
1087 rowHeight = math.max(rowHeight, childSize.height);
1088 case TableCellVerticalAlignment.fill:
1089 break;
1090 }
1091 }
1092 }
1093 rowTop += rowHeight;
1094 }
1095 return constraints.constrain(Size(tableWidth, rowTop));
1096 }
1097
1098 @override
1099 void performLayout() {
1100 final BoxConstraints constraints = this.constraints;
1101 final int rows = this.rows;
1102 final int columns = this.columns;
1103 assert(_children.length == rows * columns);
1104 if (rows * columns == 0) {
1105 // TODO(ianh): if columns is zero, this should be zero width
1106 // TODO(ianh): if columns is not zero, this should be based on the column width specifications
1107 _tableWidth = 0.0;
1108 size = constraints.constrain(Size.zero);
1109 return;
1110 }
1111 final List<double> widths = _computeColumnWidths(constraints);
1112 final List<double> positions = List<double>.filled(columns, 0.0);
1113 switch (textDirection) {
1114 case TextDirection.rtl:
1115 positions[columns - 1] = 0.0;
1116 for (int x = columns - 2; x >= 0; x -= 1) {
1117 positions[x] = positions[x+1] + widths[x+1];
1118 }
1119 _columnLefts = positions.reversed;
1120 _tableWidth = positions.first + widths.first;
1121 case TextDirection.ltr:
1122 positions[0] = 0.0;
1123 for (int x = 1; x < columns; x += 1) {
1124 positions[x] = positions[x-1] + widths[x-1];
1125 }
1126 _columnLefts = positions;
1127 _tableWidth = positions.last + widths.last;
1128 }
1129 _rowTops.clear();
1130 _baselineDistance = null;
1131 // then, lay out each row
1132 double rowTop = 0.0;
1133 for (int y = 0; y < rows; y += 1) {
1134 _rowTops.add(rowTop);
1135 double rowHeight = 0.0;
1136 bool haveBaseline = false;
1137 double beforeBaselineDistance = 0.0;
1138 double afterBaselineDistance = 0.0;
1139 final List<double> baselines = List<double>.filled(columns, 0.0);
1140 for (int x = 0; x < columns; x += 1) {
1141 final int xy = x + y * columns;
1142 final RenderBox? child = _children[xy];
1143 if (child != null) {
1144 final TableCellParentData childParentData = child.parentData! as TableCellParentData;
1145 childParentData.x = x;
1146 childParentData.y = y;
1147 switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
1148 case TableCellVerticalAlignment.baseline:
1149 assert(textBaseline != null, 'An explicit textBaseline is required when using baseline alignment.');
1150 child.layout(BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true);
1151 final double? childBaseline = child.getDistanceToBaseline(textBaseline!, onlyReal: true);
1152 if (childBaseline != null) {
1153 beforeBaselineDistance = math.max(beforeBaselineDistance, childBaseline);
1154 afterBaselineDistance = math.max(afterBaselineDistance, child.size.height - childBaseline);
1155 baselines[x] = childBaseline;
1156 haveBaseline = true;
1157 } else {
1158 rowHeight = math.max(rowHeight, child.size.height);
1159 childParentData.offset = Offset(positions[x], rowTop);
1160 }
1161 case TableCellVerticalAlignment.top:
1162 case TableCellVerticalAlignment.middle:
1163 case TableCellVerticalAlignment.bottom:
1164 case TableCellVerticalAlignment.intrinsicHeight:
1165 child.layout(BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true);
1166 rowHeight = math.max(rowHeight, child.size.height);
1167 case TableCellVerticalAlignment.fill:
1168 break;
1169 }
1170 }
1171 }
1172 if (haveBaseline) {
1173 if (y == 0) {
1174 _baselineDistance = beforeBaselineDistance;
1175 }
1176 rowHeight = math.max(rowHeight, beforeBaselineDistance + afterBaselineDistance);
1177 }
1178 for (int x = 0; x < columns; x += 1) {
1179 final int xy = x + y * columns;
1180 final RenderBox? child = _children[xy];
1181 if (child != null) {
1182 final TableCellParentData childParentData = child.parentData! as TableCellParentData;
1183 switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
1184 case TableCellVerticalAlignment.baseline:
1185 childParentData.offset = Offset(positions[x], rowTop + beforeBaselineDistance - baselines[x]);
1186 case TableCellVerticalAlignment.top:
1187 childParentData.offset = Offset(positions[x], rowTop);
1188 case TableCellVerticalAlignment.middle:
1189 childParentData.offset = Offset(positions[x], rowTop + (rowHeight - child.size.height) / 2.0);
1190 case TableCellVerticalAlignment.bottom:
1191 childParentData.offset = Offset(positions[x], rowTop + rowHeight - child.size.height);
1192 case TableCellVerticalAlignment.fill:
1193 case TableCellVerticalAlignment.intrinsicHeight:
1194 child.layout(BoxConstraints.tightFor(width: widths[x], height: rowHeight));
1195 childParentData.offset = Offset(positions[x], rowTop);
1196 }
1197 }
1198 }
1199 rowTop += rowHeight;
1200 }
1201 _rowTops.add(rowTop);
1202 size = constraints.constrain(Size(_tableWidth, rowTop));
1203 assert(_rowTops.length == rows + 1);
1204 }
1205
1206 @override
1207 bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
1208 assert(_children.length == rows * columns);
1209 for (int index = _children.length - 1; index >= 0; index -= 1) {
1210 final RenderBox? child = _children[index];
1211 if (child != null) {
1212 final BoxParentData childParentData = child.parentData! as BoxParentData;
1213 final bool isHit = result.addWithPaintOffset(
1214 offset: childParentData.offset,
1215 position: position,
1216 hitTest: (BoxHitTestResult result, Offset transformed) {
1217 assert(transformed == position - childParentData.offset);
1218 return child.hitTest(result, position: transformed);
1219 },
1220 );
1221 if (isHit) {
1222 return true;
1223 }
1224 }
1225 }
1226 return false;
1227 }
1228
1229 @override
1230 void paint(PaintingContext context, Offset offset) {
1231 assert(_children.length == rows * columns);
1232 if (rows * columns == 0) {
1233 if (border != null) {
1234 final Rect borderRect = Rect.fromLTWH(offset.dx, offset.dy, _tableWidth, 0.0);
1235 border!.paint(context.canvas, borderRect, rows: const <double>[], columns: const <double>[]);
1236 }
1237 return;
1238 }
1239 assert(_rowTops.length == rows + 1);
1240 if (_rowDecorations != null) {
1241 assert(_rowDecorations!.length == _rowDecorationPainters!.length);
1242 final Canvas canvas = context.canvas;
1243 for (int y = 0; y < rows; y += 1) {
1244 if (_rowDecorations!.length <= y) {
1245 break;
1246 }
1247 if (_rowDecorations![y] != null) {
1248 _rowDecorationPainters![y] ??= _rowDecorations![y]!.createBoxPainter(markNeedsPaint);
1249 _rowDecorationPainters![y]!.paint(
1250 canvas,
1251 Offset(offset.dx, offset.dy + _rowTops[y]),
1252 configuration.copyWith(size: Size(size.width, _rowTops[y+1] - _rowTops[y])),
1253 );
1254 }
1255 }
1256 }
1257 for (int index = 0; index < _children.length; index += 1) {
1258 final RenderBox? child = _children[index];
1259 if (child != null) {
1260 final BoxParentData childParentData = child.parentData! as BoxParentData;
1261 context.paintChild(child, childParentData.offset + offset);
1262 }
1263 }
1264 assert(_rows == _rowTops.length - 1);
1265 assert(_columns == _columnLefts!.length);
1266 if (border != null) {
1267 // The border rect might not fill the entire height of this render object
1268 // if the rows underflow. We always force the columns to fill the width of
1269 // the render object, which means the columns cannot underflow.
1270 final Rect borderRect = Rect.fromLTWH(offset.dx, offset.dy, _tableWidth, _rowTops.last);
1271 final Iterable<double> rows = _rowTops.getRange(1, _rowTops.length - 1);
1272 final Iterable<double> columns = _columnLefts!.skip(1);
1273 border!.paint(context.canvas, borderRect, rows: rows, columns: columns);
1274 }
1275 }
1276
1277 @override
1278 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1279 super.debugFillProperties(properties);
1280 properties.add(DiagnosticsProperty<TableBorder>('border', border, defaultValue: null));
1281 properties.add(DiagnosticsProperty<Map<int, TableColumnWidth>>('specified column widths', _columnWidths, level: _columnWidths.isEmpty ? DiagnosticLevel.hidden : DiagnosticLevel.info));
1282 properties.add(DiagnosticsProperty<TableColumnWidth>('default column width', defaultColumnWidth));
1283 properties.add(MessageProperty('table size', '$columns\u00D7$rows'));
1284 properties.add(IterableProperty<String>('column offsets', _columnLefts?.map(debugFormatDouble), ifNull: 'unknown'));
1285 properties.add(IterableProperty<String>('row offsets', _rowTops.map(debugFormatDouble), ifNull: 'unknown'));
1286 }
1287
1288 @override
1289 List<DiagnosticsNode> debugDescribeChildren() {
1290 if (_children.isEmpty) {
1291 return <DiagnosticsNode>[DiagnosticsNode.message('table is empty')];
1292 }
1293
1294 return <DiagnosticsNode>[
1295 for (int y = 0; y < rows; y += 1)
1296 for (int x = 0; x < columns; x += 1)
1297 if (_children[x + y * columns] case final RenderBox child)
1298 child.toDiagnosticsNode(name: 'child ($x, $y)')
1299 else
1300 DiagnosticsProperty<Object>('child ($x, $y)', null, ifNull: 'is null', showSeparator: false),
1301 ];
1302 }
1303}
1304