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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com