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 computeMinIntrinsicHeight(width);
804 }
805
806 double? _baselineDistance;
807 @override
808 double? computeDistanceToActualBaseline(TextBaseline baseline) {
809 // returns the baseline of the first cell that has a baseline in the first row
810 assert(!debugNeedsLayout);
811 return _baselineDistance;
812 }
813
814 /// Returns the list of [RenderBox] objects that are in the given
815 /// column, in row order, starting from the first row.
816 ///
817 /// This is a lazily-evaluated iterable.
818 // The following uses sync* because it is public API documented to return a
819 // lazy iterable.
820 Iterable<RenderBox> column(int x) sync* {
821 for (int y = 0; y < rows; y += 1) {
822 final int xy = x + y * columns;
823 final RenderBox? child = _children[xy];
824 if (child != null) {
825 yield child;
826 }
827 }
828 }
829
830 /// Returns the list of [RenderBox] objects that are on the given
831 /// row, in column order, starting with the first column.
832 ///
833 /// This is a lazily-evaluated iterable.
834 // The following uses sync* because it is public API documented to return a
835 // lazy iterable.
836 Iterable<RenderBox> row(int y) sync* {
837 final int start = y * columns;
838 final int end = (y + 1) * columns;
839 for (int xy = start; xy < end; xy += 1) {
840 final RenderBox? child = _children[xy];
841 if (child != null) {
842 yield child;
843 }
844 }
845 }
846
847 List<double> _computeColumnWidths(BoxConstraints constraints) {
848 assert(_children.length == rows * columns);
849 // We apply the constraints to the column widths in the order of
850 // least important to most important:
851 // 1. apply the ideal widths (maxIntrinsicWidth)
852 // 2. grow the flex columns so that the table has the maxWidth (if
853 // finite) or the minWidth (if not)
854 // 3. if there were no flex columns, then grow the table to the
855 // minWidth.
856 // 4. apply the maximum width of the table, shrinking columns as
857 // necessary, applying minimum column widths as we go
858
859 // 1. apply ideal widths, and collect information we'll need later
860 final List<double> widths = List<double>.filled(columns, 0.0);
861 final List<double> minWidths = List<double>.filled(columns, 0.0);
862 final List<double?> flexes = List<double?>.filled(columns, null);
863 double tableWidth = 0.0; // running tally of the sum of widths[x] for all x
864 double unflexedTableWidth = 0.0; // sum of the maxIntrinsicWidths of any column that has null flex
865 double totalFlex = 0.0;
866 for (int x = 0; x < columns; x += 1) {
867 final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth;
868 final Iterable<RenderBox> columnCells = column(x);
869 // apply ideal width (maxIntrinsicWidth)
870 final double maxIntrinsicWidth = columnWidth.maxIntrinsicWidth(columnCells, constraints.maxWidth);
871 assert(maxIntrinsicWidth.isFinite);
872 assert(maxIntrinsicWidth >= 0.0);
873 widths[x] = maxIntrinsicWidth;
874 tableWidth += maxIntrinsicWidth;
875 // collect min width information while we're at it
876 final double minIntrinsicWidth = columnWidth.minIntrinsicWidth(columnCells, constraints.maxWidth);
877 assert(minIntrinsicWidth.isFinite);
878 assert(minIntrinsicWidth >= 0.0);
879 minWidths[x] = minIntrinsicWidth;
880 assert(maxIntrinsicWidth >= minIntrinsicWidth);
881 // collect flex information while we're at it
882 final double? flex = columnWidth.flex(columnCells);
883 if (flex != null) {
884 assert(flex.isFinite);
885 assert(flex > 0.0);
886 flexes[x] = flex;
887 totalFlex += flex;
888 } else {
889 unflexedTableWidth = unflexedTableWidth + maxIntrinsicWidth;
890 }
891 }
892 final double maxWidthConstraint = constraints.maxWidth;
893 final double minWidthConstraint = constraints.minWidth;
894
895 // 2. grow the flex columns so that the table has the maxWidth (if
896 // finite) or the minWidth (if not)
897 if (totalFlex > 0.0) {
898 // this can only grow the table, but it _will_ grow the table at
899 // least as big as the target width.
900 final double targetWidth;
901 if (maxWidthConstraint.isFinite) {
902 targetWidth = maxWidthConstraint;
903 } else {
904 targetWidth = minWidthConstraint;
905 }
906 if (tableWidth < targetWidth) {
907 final double remainingWidth = targetWidth - unflexedTableWidth;
908 assert(remainingWidth.isFinite);
909 assert(remainingWidth >= 0.0);
910 for (int x = 0; x < columns; x += 1) {
911 if (flexes[x] != null) {
912 final double flexedWidth = remainingWidth * flexes[x]! / totalFlex;
913 assert(flexedWidth.isFinite);
914 assert(flexedWidth >= 0.0);
915 if (widths[x] < flexedWidth) {
916 final double delta = flexedWidth - widths[x];
917 tableWidth += delta;
918 widths[x] = flexedWidth;
919 }
920 }
921 }
922 assert(tableWidth + precisionErrorTolerance >= targetWidth);
923 }
924 } // step 2 and 3 are mutually exclusive
925
926 // 3. if there were no flex columns, then grow the table to the
927 // minWidth.
928 else if (tableWidth < minWidthConstraint) {
929 final double delta = (minWidthConstraint - tableWidth) / columns;
930 for (int x = 0; x < columns; x += 1) {
931 widths[x] = widths[x] + delta;
932 }
933 tableWidth = minWidthConstraint;
934 }
935
936 // beyond this point, unflexedTableWidth is no longer valid
937
938 // 4. apply the maximum width of the table, shrinking columns as
939 // necessary, applying minimum column widths as we go
940 if (tableWidth > maxWidthConstraint) {
941 double deficit = tableWidth - maxWidthConstraint;
942 // Some columns may have low flex but have all the free space.
943 // (Consider a case with a 1px wide column of flex 1000.0 and
944 // a 1000px wide column of flex 1.0; the sizes coming from the
945 // maxIntrinsicWidths. If the maximum table width is 2px, then
946 // just applying the flexes to the deficit would result in a
947 // table with one column at -998px and one column at 990px,
948 // which is wildly unhelpful.)
949 // Similarly, some columns may be flexible, but not actually
950 // be shrinkable due to a large minimum width. (Consider a
951 // case with two columns, one is flex and one isn't, both have
952 // 1000px maxIntrinsicWidths, but the flex one has 1000px
953 // minIntrinsicWidth also. The whole deficit will have to come
954 // from the non-flex column.)
955 // So what we do is we repeatedly iterate through the flexible
956 // columns shrinking them proportionally until we have no
957 // available columns, then do the same to the non-flexible ones.
958 int availableColumns = columns;
959 while (deficit > precisionErrorTolerance && totalFlex > precisionErrorTolerance) {
960 double newTotalFlex = 0.0;
961 for (int x = 0; x < columns; x += 1) {
962 if (flexes[x] != null) {
963 final double newWidth = widths[x] - deficit * flexes[x]! / totalFlex;
964 assert(newWidth.isFinite);
965 if (newWidth <= minWidths[x]) {
966 // shrank to minimum
967 deficit -= widths[x] - minWidths[x];
968 widths[x] = minWidths[x];
969 flexes[x] = null;
970 availableColumns -= 1;
971 } else {
972 deficit -= widths[x] - newWidth;
973 widths[x] = newWidth;
974 newTotalFlex += flexes[x]!;
975 }
976 assert(widths[x] >= 0.0);
977 }
978 }
979 totalFlex = newTotalFlex;
980 }
981 while (deficit > precisionErrorTolerance && availableColumns > 0) {
982 // Now we have to take out the remaining space from the
983 // columns that aren't minimum sized.
984 // To make this fair, we repeatedly remove equal amounts from
985 // each column, clamped to the minimum width, until we run out
986 // of columns that aren't at their minWidth.
987 final double delta = deficit / availableColumns;
988 assert(delta != 0);
989 int newAvailableColumns = 0;
990 for (int x = 0; x < columns; x += 1) {
991 final double availableDelta = widths[x] - minWidths[x];
992 if (availableDelta > 0.0) {
993 if (availableDelta <= delta) {
994 // shrank to minimum
995 deficit -= widths[x] - minWidths[x];
996 widths[x] = minWidths[x];
997 } else {
998 deficit -= delta;
999 widths[x] = widths[x] - delta;
1000 newAvailableColumns += 1;
1001 }
1002 }
1003 }
1004 availableColumns = newAvailableColumns;
1005 }
1006 }
1007 return widths;
1008 }
1009
1010 // cache the table geometry for painting purposes
1011 final List<double> _rowTops = <double>[];
1012 Iterable<double>? _columnLefts;
1013 late double _tableWidth;
1014
1015 /// Returns the position and dimensions of the box that the given
1016 /// row covers, in this render object's coordinate space (so the
1017 /// left coordinate is always 0.0).
1018 ///
1019 /// The row being queried must exist.
1020 ///
1021 /// This is only valid after layout.
1022 Rect getRowBox(int row) {
1023 assert(row >= 0);
1024 assert(row < rows);
1025 assert(!debugNeedsLayout);
1026 return Rect.fromLTRB(0.0, _rowTops[row], size.width, _rowTops[row + 1]);
1027 }
1028
1029 @override
1030 @protected
1031 Size computeDryLayout(covariant BoxConstraints constraints) {
1032 if (rows * columns == 0) {
1033 return constraints.constrain(Size.zero);
1034 }
1035 final List<double> widths = _computeColumnWidths(constraints);
1036 final double tableWidth = widths.fold(0.0, (double a, double b) => a + b);
1037 double rowTop = 0.0;
1038 for (int y = 0; y < rows; y += 1) {
1039 double rowHeight = 0.0;
1040 for (int x = 0; x < columns; x += 1) {
1041 final int xy = x + y * columns;
1042 final RenderBox? child = _children[xy];
1043 if (child != null) {
1044 final TableCellParentData childParentData = child.parentData! as TableCellParentData;
1045 switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
1046 case TableCellVerticalAlignment.baseline:
1047 assert(debugCannotComputeDryLayout(
1048 reason: 'TableCellVerticalAlignment.baseline requires a full layout for baseline metrics to be available.',
1049 ));
1050 return Size.zero;
1051 case TableCellVerticalAlignment.top:
1052 case TableCellVerticalAlignment.middle:
1053 case TableCellVerticalAlignment.bottom:
1054 case TableCellVerticalAlignment.intrinsicHeight:
1055 final Size childSize = child.getDryLayout(BoxConstraints.tightFor(width: widths[x]));
1056 rowHeight = math.max(rowHeight, childSize.height);
1057 case TableCellVerticalAlignment.fill:
1058 break;
1059 }
1060 }
1061 }
1062 rowTop += rowHeight;
1063 }
1064 return constraints.constrain(Size(tableWidth, rowTop));
1065 }
1066
1067 @override
1068 void performLayout() {
1069 final BoxConstraints constraints = this.constraints;
1070 final int rows = this.rows;
1071 final int columns = this.columns;
1072 assert(_children.length == rows * columns);
1073 if (rows * columns == 0) {
1074 // TODO(ianh): if columns is zero, this should be zero width
1075 // TODO(ianh): if columns is not zero, this should be based on the column width specifications
1076 _tableWidth = 0.0;
1077 size = constraints.constrain(Size.zero);
1078 return;
1079 }
1080 final List<double> widths = _computeColumnWidths(constraints);
1081 final List<double> positions = List<double>.filled(columns, 0.0);
1082 switch (textDirection) {
1083 case TextDirection.rtl:
1084 positions[columns - 1] = 0.0;
1085 for (int x = columns - 2; x >= 0; x -= 1) {
1086 positions[x] = positions[x+1] + widths[x+1];
1087 }
1088 _columnLefts = positions.reversed;
1089 _tableWidth = positions.first + widths.first;
1090 case TextDirection.ltr:
1091 positions[0] = 0.0;
1092 for (int x = 1; x < columns; x += 1) {
1093 positions[x] = positions[x-1] + widths[x-1];
1094 }
1095 _columnLefts = positions;
1096 _tableWidth = positions.last + widths.last;
1097 }
1098 _rowTops.clear();
1099 _baselineDistance = null;
1100 // then, lay out each row
1101 double rowTop = 0.0;
1102 for (int y = 0; y < rows; y += 1) {
1103 _rowTops.add(rowTop);
1104 double rowHeight = 0.0;
1105 bool haveBaseline = false;
1106 double beforeBaselineDistance = 0.0;
1107 double afterBaselineDistance = 0.0;
1108 final List<double> baselines = List<double>.filled(columns, 0.0);
1109 for (int x = 0; x < columns; x += 1) {
1110 final int xy = x + y * columns;
1111 final RenderBox? child = _children[xy];
1112 if (child != null) {
1113 final TableCellParentData childParentData = child.parentData! as TableCellParentData;
1114 childParentData.x = x;
1115 childParentData.y = y;
1116 switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
1117 case TableCellVerticalAlignment.baseline:
1118 assert(textBaseline != null, 'An explicit textBaseline is required when using baseline alignment.');
1119 child.layout(BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true);
1120 final double? childBaseline = child.getDistanceToBaseline(textBaseline!, onlyReal: true);
1121 if (childBaseline != null) {
1122 beforeBaselineDistance = math.max(beforeBaselineDistance, childBaseline);
1123 afterBaselineDistance = math.max(afterBaselineDistance, child.size.height - childBaseline);
1124 baselines[x] = childBaseline;
1125 haveBaseline = true;
1126 } else {
1127 rowHeight = math.max(rowHeight, child.size.height);
1128 childParentData.offset = Offset(positions[x], rowTop);
1129 }
1130 case TableCellVerticalAlignment.top:
1131 case TableCellVerticalAlignment.middle:
1132 case TableCellVerticalAlignment.bottom:
1133 case TableCellVerticalAlignment.intrinsicHeight:
1134 child.layout(BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true);
1135 rowHeight = math.max(rowHeight, child.size.height);
1136 case TableCellVerticalAlignment.fill:
1137 break;
1138 }
1139 }
1140 }
1141 if (haveBaseline) {
1142 if (y == 0) {
1143 _baselineDistance = beforeBaselineDistance;
1144 }
1145 rowHeight = math.max(rowHeight, beforeBaselineDistance + afterBaselineDistance);
1146 }
1147 for (int x = 0; x < columns; x += 1) {
1148 final int xy = x + y * columns;
1149 final RenderBox? child = _children[xy];
1150 if (child != null) {
1151 final TableCellParentData childParentData = child.parentData! as TableCellParentData;
1152 switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
1153 case TableCellVerticalAlignment.baseline:
1154 childParentData.offset = Offset(positions[x], rowTop + beforeBaselineDistance - baselines[x]);
1155 case TableCellVerticalAlignment.top:
1156 childParentData.offset = Offset(positions[x], rowTop);
1157 case TableCellVerticalAlignment.middle:
1158 childParentData.offset = Offset(positions[x], rowTop + (rowHeight - child.size.height) / 2.0);
1159 case TableCellVerticalAlignment.bottom:
1160 childParentData.offset = Offset(positions[x], rowTop + rowHeight - child.size.height);
1161 case TableCellVerticalAlignment.fill:
1162 case TableCellVerticalAlignment.intrinsicHeight:
1163 child.layout(BoxConstraints.tightFor(width: widths[x], height: rowHeight));
1164 childParentData.offset = Offset(positions[x], rowTop);
1165 }
1166 }
1167 }
1168 rowTop += rowHeight;
1169 }
1170 _rowTops.add(rowTop);
1171 size = constraints.constrain(Size(_tableWidth, rowTop));
1172 assert(_rowTops.length == rows + 1);
1173 }
1174
1175 @override
1176 bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
1177 assert(_children.length == rows * columns);
1178 for (int index = _children.length - 1; index >= 0; index -= 1) {
1179 final RenderBox? child = _children[index];
1180 if (child != null) {
1181 final BoxParentData childParentData = child.parentData! as BoxParentData;
1182 final bool isHit = result.addWithPaintOffset(
1183 offset: childParentData.offset,
1184 position: position,
1185 hitTest: (BoxHitTestResult result, Offset transformed) {
1186 assert(transformed == position - childParentData.offset);
1187 return child.hitTest(result, position: transformed);
1188 },
1189 );
1190 if (isHit) {
1191 return true;
1192 }
1193 }
1194 }
1195 return false;
1196 }
1197
1198 @override
1199 void paint(PaintingContext context, Offset offset) {
1200 assert(_children.length == rows * columns);
1201 if (rows * columns == 0) {
1202 if (border != null) {
1203 final Rect borderRect = Rect.fromLTWH(offset.dx, offset.dy, _tableWidth, 0.0);
1204 border!.paint(context.canvas, borderRect, rows: const <double>[], columns: const <double>[]);
1205 }
1206 return;
1207 }
1208 assert(_rowTops.length == rows + 1);
1209 if (_rowDecorations != null) {
1210 assert(_rowDecorations!.length == _rowDecorationPainters!.length);
1211 final Canvas canvas = context.canvas;
1212 for (int y = 0; y < rows; y += 1) {
1213 if (_rowDecorations!.length <= y) {
1214 break;
1215 }
1216 if (_rowDecorations![y] != null) {
1217 _rowDecorationPainters![y] ??= _rowDecorations![y]!.createBoxPainter(markNeedsPaint);
1218 _rowDecorationPainters![y]!.paint(
1219 canvas,
1220 Offset(offset.dx, offset.dy + _rowTops[y]),
1221 configuration.copyWith(size: Size(size.width, _rowTops[y+1] - _rowTops[y])),
1222 );
1223 }
1224 }
1225 }
1226 for (int index = 0; index < _children.length; index += 1) {
1227 final RenderBox? child = _children[index];
1228 if (child != null) {
1229 final BoxParentData childParentData = child.parentData! as BoxParentData;
1230 context.paintChild(child, childParentData.offset + offset);
1231 }
1232 }
1233 assert(_rows == _rowTops.length - 1);
1234 assert(_columns == _columnLefts!.length);
1235 if (border != null) {
1236 // The border rect might not fill the entire height of this render object
1237 // if the rows underflow. We always force the columns to fill the width of
1238 // the render object, which means the columns cannot underflow.
1239 final Rect borderRect = Rect.fromLTWH(offset.dx, offset.dy, _tableWidth, _rowTops.last);
1240 final Iterable<double> rows = _rowTops.getRange(1, _rowTops.length - 1);
1241 final Iterable<double> columns = _columnLefts!.skip(1);
1242 border!.paint(context.canvas, borderRect, rows: rows, columns: columns);
1243 }
1244 }
1245
1246 @override
1247 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1248 super.debugFillProperties(properties);
1249 properties.add(DiagnosticsProperty<TableBorder>('border', border, defaultValue: null));
1250 properties.add(DiagnosticsProperty<Map<int, TableColumnWidth>>('specified column widths', _columnWidths, level: _columnWidths.isEmpty ? DiagnosticLevel.hidden : DiagnosticLevel.info));
1251 properties.add(DiagnosticsProperty<TableColumnWidth>('default column width', defaultColumnWidth));
1252 properties.add(MessageProperty('table size', '$columns\u00D7$rows'));
1253 properties.add(IterableProperty<String>('column offsets', _columnLefts?.map(debugFormatDouble), ifNull: 'unknown'));
1254 properties.add(IterableProperty<String>('row offsets', _rowTops.map(debugFormatDouble), ifNull: 'unknown'));
1255 }
1256
1257 @override
1258 List<DiagnosticsNode> debugDescribeChildren() {
1259 if (_children.isEmpty) {
1260 return <DiagnosticsNode>[DiagnosticsNode.message('table is empty')];
1261 }
1262
1263 final List<DiagnosticsNode> children = <DiagnosticsNode>[];
1264 for (int y = 0; y < rows; y += 1) {
1265 for (int x = 0; x < columns; x += 1) {
1266 final int xy = x + y * columns;
1267 final RenderBox? child = _children[xy];
1268 final String name = 'child ($x, $y)';
1269 if (child != null) {
1270 children.add(child.toDiagnosticsNode(name: name));
1271 } else {
1272 children.add(DiagnosticsProperty<Object>(name, null, ifNull: 'is null', showSeparator: false));
1273 }
1274 }
1275 }
1276 return children;
1277 }
1278}
1279