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 | |
5 | import 'dart:collection'; |
6 | import 'dart:math' as math; |
7 | |
8 | import 'package:flutter/foundation.dart'; |
9 | |
10 | import 'box.dart'; |
11 | import 'object.dart'; |
12 | import 'table_border.dart'; |
13 | |
14 | /// Parent data used by [RenderTable] for its children. |
15 | class 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 |
41 | abstract 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. |
92 | class 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. |
133 | class 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. |
157 | class 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. |
193 | class 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. |
231 | class 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. |
283 | class 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]. |
329 | enum 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. |
357 | class RenderTable extends RenderBox { |
358 | /// Creates a table render object. |
359 | /// |
360 | /// * `columns` must either be null or non-negative. If `columns` is null, |
361 | /// the number of columns will be inferred from length of the first sublist |
362 | /// of `children`. |
363 | /// * `rows` must either be null or non-negative. If `rows` is null, the |
364 | /// number of rows will be inferred from the `children`. If `rows` is not |
365 | /// null, then `children` must be null. |
366 | /// * `children` must either be null or contain lists of all the same length. |
367 | /// if `children` is not null, then `rows` must be null. |
368 | /// * [columnWidths] may be null, in which case it defaults to an empty map. |
369 | RenderTable({ |
370 | int? columns, |
371 | int? rows, |
372 | Map<int, TableColumnWidth>? columnWidths, |
373 | TableColumnWidth defaultColumnWidth = const FlexColumnWidth(), |
374 | required TextDirection textDirection, |
375 | TableBorder? border, |
376 | List<Decoration?>? rowDecorations, |
377 | ImageConfiguration configuration = ImageConfiguration.empty, |
378 | TableCellVerticalAlignment defaultVerticalAlignment = TableCellVerticalAlignment.top, |
379 | TextBaseline? textBaseline, |
380 | List<List<RenderBox>>? children, |
381 | }) : assert(columns == null || columns >= 0), |
382 | assert(rows == null || rows >= 0), |
383 | assert(rows == null || children == null), |
384 | _textDirection = textDirection, |
385 | _columns = columns ?? (children != null && children.isNotEmpty ? children.first.length : 0), |
386 | _rows = rows ?? 0, |
387 | _columnWidths = columnWidths ?? HashMap<int, TableColumnWidth>(), |
388 | _defaultColumnWidth = defaultColumnWidth, |
389 | _border = border, |
390 | _textBaseline = textBaseline, |
391 | _defaultVerticalAlignment = defaultVerticalAlignment, |
392 | _configuration = configuration { |
393 | _children = <RenderBox?>[]..length = _columns * _rows; |
394 | this.rowDecorations = rowDecorations; // must use setter to initialize box painters array |
395 | children?.forEach(addRow); |
396 | } |
397 | |
398 | // Children are stored in row-major order. |
399 | // _children.length must be rows * columns |
400 | List<RenderBox?> _children = const <RenderBox?>[]; |
401 | |
402 | /// The number of vertical alignment lines in this table. |
403 | /// |
404 | /// Changing the number of columns will remove any children that no longer fit |
405 | /// in the table. |
406 | /// |
407 | /// Changing the number of columns is an expensive operation because the table |
408 | /// needs to rearrange its internal representation. |
409 | int get columns => _columns; |
410 | int _columns; |
411 | set columns(int value) { |
412 | assert(value >= 0); |
413 | if (value == columns) { |
414 | return; |
415 | } |
416 | final int oldColumns = columns; |
417 | final List<RenderBox?> oldChildren = _children; |
418 | _columns = value; |
419 | _children = List<RenderBox?>.filled(columns * rows, null); |
420 | final int columnsToCopy = math.min(columns, oldColumns); |
421 | for (int y = 0; y < rows; y += 1) { |
422 | for (int x = 0; x < columnsToCopy; x += 1) { |
423 | _children[x + y * columns] = oldChildren[x + y * oldColumns]; |
424 | } |
425 | } |
426 | if (oldColumns > columns) { |
427 | for (int y = 0; y < rows; y += 1) { |
428 | for (int x = columns; x < oldColumns; x += 1) { |
429 | final int xy = x + y * oldColumns; |
430 | if (oldChildren[xy] != null) { |
431 | dropChild(oldChildren[xy]!); |
432 | } |
433 | } |
434 | } |
435 | } |
436 | markNeedsLayout(); |
437 | } |
438 | |
439 | /// The number of horizontal alignment lines in this table. |
440 | /// |
441 | /// Changing the number of rows will remove any children that no longer fit |
442 | /// in the table. |
443 | int get rows => _rows; |
444 | int _rows; |
445 | set rows(int value) { |
446 | assert(value >= 0); |
447 | if (value == rows) { |
448 | return; |
449 | } |
450 | if (_rows > value) { |
451 | for (int xy = columns * value; xy < _children.length; xy += 1) { |
452 | if (_children[xy] != null) { |
453 | dropChild(_children[xy]!); |
454 | } |
455 | } |
456 | } |
457 | _rows = value; |
458 | _children.length = columns * rows; |
459 | markNeedsLayout(); |
460 | } |
461 | |
462 | /// How the horizontal extents of the columns of this table should be determined. |
463 | /// |
464 | /// If the [Map] has a null entry for a given column, the table uses the |
465 | /// [defaultColumnWidth] instead. |
466 | /// |
467 | /// The layout performance of the table depends critically on which column |
468 | /// sizing algorithms are used here. In particular, [IntrinsicColumnWidth] is |
469 | /// quite expensive because it needs to measure each cell in the column to |
470 | /// determine the intrinsic size of the column. |
471 | /// |
472 | /// This property can never return null. If it is set to null, and the existing |
473 | /// map is not empty, then the value is replaced by an empty map. (If it is set |
474 | /// to null while the current value is an empty map, the value is not changed.) |
475 | Map<int, TableColumnWidth>? get columnWidths => Map<int, TableColumnWidth>.unmodifiable(_columnWidths); |
476 | Map<int, TableColumnWidth> _columnWidths; |
477 | set columnWidths(Map<int, TableColumnWidth>? value) { |
478 | if (_columnWidths == value) { |
479 | return; |
480 | } |
481 | if (_columnWidths.isEmpty && value == null) { |
482 | return; |
483 | } |
484 | _columnWidths = value ?? HashMap<int, TableColumnWidth>(); |
485 | markNeedsLayout(); |
486 | } |
487 | |
488 | /// Determines how the width of column with the given index is determined. |
489 | void setColumnWidth(int column, TableColumnWidth value) { |
490 | if (_columnWidths[column] == value) { |
491 | return; |
492 | } |
493 | _columnWidths[column] = value; |
494 | markNeedsLayout(); |
495 | } |
496 | |
497 | /// How to determine with widths of columns that don't have an explicit sizing algorithm. |
498 | /// |
499 | /// Specifically, the [defaultColumnWidth] is used for column `i` if |
500 | /// `columnWidths[i]` is null. |
501 | TableColumnWidth get defaultColumnWidth => _defaultColumnWidth; |
502 | TableColumnWidth _defaultColumnWidth; |
503 | set defaultColumnWidth(TableColumnWidth value) { |
504 | if (defaultColumnWidth == value) { |
505 | return; |
506 | } |
507 | _defaultColumnWidth = value; |
508 | markNeedsLayout(); |
509 | } |
510 | |
511 | /// The direction in which the columns are ordered. |
512 | TextDirection get textDirection => _textDirection; |
513 | TextDirection _textDirection; |
514 | set textDirection(TextDirection value) { |
515 | if (_textDirection == value) { |
516 | return; |
517 | } |
518 | _textDirection = value; |
519 | markNeedsLayout(); |
520 | } |
521 | |
522 | /// The style to use when painting the boundary and interior divisions of the table. |
523 | TableBorder? get border => _border; |
524 | TableBorder? _border; |
525 | set border(TableBorder? value) { |
526 | if (border == value) { |
527 | return; |
528 | } |
529 | _border = value; |
530 | markNeedsPaint(); |
531 | } |
532 | |
533 | /// The decorations to use for each row of the table. |
534 | /// |
535 | /// Row decorations fill the horizontal and vertical extent of each row in |
536 | /// the table, unlike decorations for individual cells, which might not fill |
537 | /// either. |
538 | List<Decoration?> get rowDecorations => List<Decoration?>.unmodifiable(_rowDecorations ?? const <Decoration>[]); |
539 | // _rowDecorations and _rowDecorationPainters need to be in sync. They have to |
540 | // either both be null or have same length. |
541 | List<Decoration?>? _rowDecorations; |
542 | List<BoxPainter?>? _rowDecorationPainters; |
543 | set rowDecorations(List<Decoration?>? value) { |
544 | if (_rowDecorations == value) { |
545 | return; |
546 | } |
547 | _rowDecorations = value; |
548 | if (_rowDecorationPainters != null) { |
549 | for (final BoxPainter? painter in _rowDecorationPainters!) { |
550 | painter?.dispose(); |
551 | } |
552 | } |
553 | _rowDecorationPainters = _rowDecorations != null ? List<BoxPainter?>.filled(_rowDecorations!.length, null) : null; |
554 | } |
555 | |
556 | /// The settings to pass to the [rowDecorations] when painting, so that they |
557 | /// can resolve images appropriately. See [ImageProvider.resolve] and |
558 | /// [BoxPainter.paint]. |
559 | ImageConfiguration get configuration => _configuration; |
560 | ImageConfiguration _configuration; |
561 | set configuration(ImageConfiguration value) { |
562 | if (value == _configuration) { |
563 | return; |
564 | } |
565 | _configuration = value; |
566 | markNeedsPaint(); |
567 | } |
568 | |
569 | /// How cells that do not explicitly specify a vertical alignment are aligned vertically. |
570 | TableCellVerticalAlignment get defaultVerticalAlignment => _defaultVerticalAlignment; |
571 | TableCellVerticalAlignment _defaultVerticalAlignment; |
572 | set defaultVerticalAlignment(TableCellVerticalAlignment value) { |
573 | if (_defaultVerticalAlignment == value) { |
574 | return; |
575 | } |
576 | _defaultVerticalAlignment = value; |
577 | markNeedsLayout(); |
578 | } |
579 | |
580 | /// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline]. |
581 | TextBaseline? get textBaseline => _textBaseline; |
582 | TextBaseline? _textBaseline; |
583 | set textBaseline(TextBaseline? value) { |
584 | if (_textBaseline == value) { |
585 | return; |
586 | } |
587 | _textBaseline = value; |
588 | markNeedsLayout(); |
589 | } |
590 | |
591 | @override |
592 | void setupParentData(RenderObject child) { |
593 | if (child.parentData is! TableCellParentData) { |
594 | child.parentData = TableCellParentData(); |
595 | } |
596 | } |
597 | |
598 | /// Replaces the children of this table with the given cells. |
599 | /// |
600 | /// The cells are divided into the specified number of columns before |
601 | /// replacing the existing children. |
602 | /// |
603 | /// If the new cells contain any existing children of the table, those |
604 | /// children are moved to their new location in the table rather than |
605 | /// removed from the table and re-added. |
606 | void setFlatChildren(int columns, List<RenderBox?> cells) { |
607 | if (cells == _children && columns == _columns) { |
608 | return; |
609 | } |
610 | assert(columns >= 0); |
611 | // consider the case of a newly empty table |
612 | if (columns == 0 || cells.isEmpty) { |
613 | assert(cells.isEmpty); |
614 | _columns = columns; |
615 | if (_children.isEmpty) { |
616 | assert(_rows == 0); |
617 | return; |
618 | } |
619 | for (final RenderBox? oldChild in _children) { |
620 | if (oldChild != null) { |
621 | dropChild(oldChild); |
622 | } |
623 | } |
624 | _rows = 0; |
625 | _children.clear(); |
626 | markNeedsLayout(); |
627 | return; |
628 | } |
629 | assert(cells.length % columns == 0); |
630 | // fill a set with the cells that are moving (it's important not |
631 | // to dropChild a child that's remaining with us, because that |
632 | // would clear their parentData field) |
633 | final Set<RenderBox> lostChildren = HashSet<RenderBox>(); |
634 | for (int y = 0; y < _rows; y += 1) { |
635 | for (int x = 0; x < _columns; x += 1) { |
636 | final int xyOld = x + y * _columns; |
637 | final int xyNew = x + y * columns; |
638 | if (_children[xyOld] != null && (x >= columns || xyNew >= cells.length || _children[xyOld] != cells[xyNew])) { |
639 | lostChildren.add(_children[xyOld]!); |
640 | } |
641 | } |
642 | } |
643 | // adopt cells that are arriving, and cross cells that are just moving off our list of lostChildren |
644 | int y = 0; |
645 | while (y * columns < cells.length) { |
646 | for (int x = 0; x < columns; x += 1) { |
647 | final int xyNew = x + y * columns; |
648 | final int xyOld = x + y * _columns; |
649 | if (cells[xyNew] != null && (x >= _columns || y >= _rows || _children[xyOld] != cells[xyNew])) { |
650 | if (!lostChildren.remove(cells[xyNew])) { |
651 | adoptChild(cells[xyNew]!); |
652 | } |
653 | } |
654 | } |
655 | y += 1; |
656 | } |
657 | // drop all the lost children |
658 | lostChildren.forEach(dropChild); |
659 | // update our internal values |
660 | _columns = columns; |
661 | _rows = cells.length ~/ columns; |
662 | _children = List<RenderBox?>.of(cells); |
663 | assert(_children.length == rows * columns); |
664 | markNeedsLayout(); |
665 | } |
666 | |
667 | /// Replaces the children of this table with the given cells. |
668 | void setChildren(List<List<RenderBox>>? cells) { |
669 | // TODO(ianh): Make this smarter, like setFlatChildren |
670 | if (cells == null) { |
671 | setFlatChildren(0, const <RenderBox?>[]); |
672 | return; |
673 | } |
674 | for (final RenderBox? oldChild in _children) { |
675 | if (oldChild != null) { |
676 | dropChild(oldChild); |
677 | } |
678 | } |
679 | _children.clear(); |
680 | _columns = cells.isNotEmpty ? cells.first.length : 0; |
681 | _rows = 0; |
682 | cells.forEach(addRow); |
683 | assert(_children.length == rows * columns); |
684 | } |
685 | |
686 | /// Adds a row to the end of the table. |
687 | /// |
688 | /// The newly added children must not already have parents. |
689 | void addRow(List<RenderBox?> cells) { |
690 | assert(cells.length == columns); |
691 | assert(_children.length == rows * columns); |
692 | _rows += 1; |
693 | _children.addAll(cells); |
694 | for (final RenderBox? cell in cells) { |
695 | if (cell != null) { |
696 | adoptChild(cell); |
697 | } |
698 | } |
699 | markNeedsLayout(); |
700 | } |
701 | |
702 | /// Replaces the child at the given position with the given child. |
703 | /// |
704 | /// If the given child is already located at the given position, this function |
705 | /// does not modify the table. Otherwise, the given child must not already |
706 | /// have a parent. |
707 | void setChild(int x, int y, RenderBox? value) { |
708 | assert(x >= 0 && x < columns && y >= 0 && y < rows); |
709 | assert(_children.length == rows * columns); |
710 | final int xy = x + y * columns; |
711 | final RenderBox? oldChild = _children[xy]; |
712 | if (oldChild == value) { |
713 | return; |
714 | } |
715 | if (oldChild != null) { |
716 | dropChild(oldChild); |
717 | } |
718 | _children[xy] = value; |
719 | if (value != null) { |
720 | adoptChild(value); |
721 | } |
722 | } |
723 | |
724 | @override |
725 | void attach(PipelineOwner owner) { |
726 | super.attach(owner); |
727 | for (final RenderBox? child in _children) { |
728 | child?.attach(owner); |
729 | } |
730 | } |
731 | |
732 | @override |
733 | void detach() { |
734 | super.detach(); |
735 | if (_rowDecorationPainters != null) { |
736 | for (final BoxPainter? painter in _rowDecorationPainters!) { |
737 | painter?.dispose(); |
738 | } |
739 | _rowDecorationPainters = List<BoxPainter?>.filled(_rowDecorations!.length, null); |
740 | } |
741 | for (final RenderBox? child in _children) { |
742 | child?.detach(); |
743 | } |
744 | } |
745 | |
746 | @override |
747 | void visitChildren(RenderObjectVisitor visitor) { |
748 | assert(_children.length == rows * columns); |
749 | for (final RenderBox? child in _children) { |
750 | if (child != null) { |
751 | visitor(child); |
752 | } |
753 | } |
754 | } |
755 | |
756 | @override |
757 | double computeMinIntrinsicWidth(double height) { |
758 | assert(_children.length == rows * columns); |
759 | double totalMinWidth = 0.0; |
760 | for (int x = 0; x < columns; x += 1) { |
761 | final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth; |
762 | final Iterable<RenderBox> columnCells = column(x); |
763 | totalMinWidth += columnWidth.minIntrinsicWidth(columnCells, double.infinity); |
764 | } |
765 | return totalMinWidth; |
766 | } |
767 | |
768 | @override |
769 | double computeMaxIntrinsicWidth(double height) { |
770 | assert(_children.length == rows * columns); |
771 | double totalMaxWidth = 0.0; |
772 | for (int x = 0; x < columns; x += 1) { |
773 | final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth; |
774 | final Iterable<RenderBox> columnCells = column(x); |
775 | totalMaxWidth += columnWidth.maxIntrinsicWidth(columnCells, double.infinity); |
776 | } |
777 | return totalMaxWidth; |
778 | } |
779 | |
780 | @override |
781 | double computeMinIntrinsicHeight(double width) { |
782 | // winner of the 2016 world's most expensive intrinsic dimension function award |
783 | // honorable mention, most likely to improve if taught about memoization award |
784 | assert(_children.length == rows * columns); |
785 | final List<double> widths = _computeColumnWidths(BoxConstraints.tightForFinite(width: width)); |
786 | double rowTop = 0.0; |
787 | for (int y = 0; y < rows; y += 1) { |
788 | double rowHeight = 0.0; |
789 | for (int x = 0; x < columns; x += 1) { |
790 | final int xy = x + y * columns; |
791 | final RenderBox? child = _children[xy]; |
792 | if (child != null) { |
793 | rowHeight = math.max(rowHeight, child.getMaxIntrinsicHeight(widths[x])); |
794 | } |
795 | } |
796 | rowTop += rowHeight; |
797 | } |
798 | return rowTop; |
799 | } |
800 | |
801 | @override |
802 | double computeMaxIntrinsicHeight(double width) { |
803 | return getMinIntrinsicHeight(width); |
804 | } |
805 | |
806 | double? _baselineDistance; |
807 | @override |
808 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |
809 | // returns the baseline offset of the cell in the first row with |
810 | // the lowest baseline, and uses `TableCellVerticalAlignment.baseline`. |
811 | assert(!debugNeedsLayout); |
812 | return _baselineDistance; |
813 | } |
814 | |
815 | /// Returns the list of [RenderBox] objects that are in the given |
816 | /// column, in row order, starting from the first row. |
817 | /// |
818 | /// This is a lazily-evaluated iterable. |
819 | // The following uses sync* because it is public API documented to return a |
820 | // lazy iterable. |
821 | Iterable<RenderBox> column(int x) sync* { |
822 | for (int y = 0; y < rows; y += 1) { |
823 | final int xy = x + y * columns; |
824 | final RenderBox? child = _children[xy]; |
825 | if (child != null) { |
826 | yield child; |
827 | } |
828 | } |
829 | } |
830 | |
831 | /// Returns the list of [RenderBox] objects that are on the given |
832 | /// row, in column order, starting with the first column. |
833 | /// |
834 | /// This is a lazily-evaluated iterable. |
835 | // The following uses sync* because it is public API documented to return a |
836 | // lazy iterable. |
837 | Iterable<RenderBox> row(int y) sync* { |
838 | final int start = y * columns; |
839 | final int end = (y + 1) * columns; |
840 | for (int xy = start; xy < end; xy += 1) { |
841 | final RenderBox? child = _children[xy]; |
842 | if (child != null) { |
843 | yield child; |
844 | } |
845 | } |
846 | } |
847 | |
848 | List<double> _computeColumnWidths(BoxConstraints constraints) { |
849 | assert(_children.length == rows * columns); |
850 | // We apply the constraints to the column widths in the order of |
851 | // least important to most important: |
852 | // 1. apply the ideal widths (maxIntrinsicWidth) |
853 | // 2. grow the flex columns so that the table has the maxWidth (if |
854 | // finite) or the minWidth (if not) |
855 | // 3. if there were no flex columns, then grow the table to the |
856 | // minWidth. |
857 | // 4. apply the maximum width of the table, shrinking columns as |
858 | // necessary, applying minimum column widths as we go |
859 | |
860 | // 1. apply ideal widths, and collect information we'll need later |
861 | final List<double> widths = List<double>.filled(columns, 0.0); |
862 | final List<double> minWidths = List<double>.filled(columns, 0.0); |
863 | final List<double?> flexes = List<double?>.filled(columns, null); |
864 | double tableWidth = 0.0; // running tally of the sum of widths[x] for all x |
865 | double unflexedTableWidth = 0.0; // sum of the maxIntrinsicWidths of any column that has null flex |
866 | double totalFlex = 0.0; |
867 | for (int x = 0; x < columns; x += 1) { |
868 | final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth; |
869 | final Iterable<RenderBox> columnCells = column(x); |
870 | // apply ideal width (maxIntrinsicWidth) |
871 | final double maxIntrinsicWidth = columnWidth.maxIntrinsicWidth(columnCells, constraints.maxWidth); |
872 | assert(maxIntrinsicWidth.isFinite); |
873 | assert(maxIntrinsicWidth >= 0.0); |
874 | widths[x] = maxIntrinsicWidth; |
875 | tableWidth += maxIntrinsicWidth; |
876 | // collect min width information while we're at it |
877 | final double minIntrinsicWidth = columnWidth.minIntrinsicWidth(columnCells, constraints.maxWidth); |
878 | assert(minIntrinsicWidth.isFinite); |
879 | assert(minIntrinsicWidth >= 0.0); |
880 | minWidths[x] = minIntrinsicWidth; |
881 | assert(maxIntrinsicWidth >= minIntrinsicWidth); |
882 | // collect flex information while we're at it |
883 | final double? flex = columnWidth.flex(columnCells); |
884 | if (flex != null) { |
885 | assert(flex.isFinite); |
886 | assert(flex > 0.0); |
887 | flexes[x] = flex; |
888 | totalFlex += flex; |
889 | } else { |
890 | unflexedTableWidth = unflexedTableWidth + maxIntrinsicWidth; |
891 | } |
892 | } |
893 | final double maxWidthConstraint = constraints.maxWidth; |
894 | final double minWidthConstraint = constraints.minWidth; |
895 | |
896 | // 2. grow the flex columns so that the table has the maxWidth (if |
897 | // finite) or the minWidth (if not) |
898 | if (totalFlex > 0.0) { |
899 | // this can only grow the table, but it _will_ grow the table at |
900 | // least as big as the target width. |
901 | final double targetWidth; |
902 | if (maxWidthConstraint.isFinite) { |
903 | targetWidth = maxWidthConstraint; |
904 | } else { |
905 | targetWidth = minWidthConstraint; |
906 | } |
907 | if (tableWidth < targetWidth) { |
908 | final double remainingWidth = targetWidth - unflexedTableWidth; |
909 | assert(remainingWidth.isFinite); |
910 | assert(remainingWidth >= 0.0); |
911 | for (int x = 0; x < columns; x += 1) { |
912 | if (flexes[x] != null) { |
913 | final double flexedWidth = remainingWidth * flexes[x]! / totalFlex; |
914 | assert(flexedWidth.isFinite); |
915 | assert(flexedWidth >= 0.0); |
916 | if (widths[x] < flexedWidth) { |
917 | final double delta = flexedWidth - widths[x]; |
918 | tableWidth += delta; |
919 | widths[x] = flexedWidth; |
920 | } |
921 | } |
922 | } |
923 | assert(tableWidth + precisionErrorTolerance >= targetWidth); |
924 | } |
925 | } // step 2 and 3 are mutually exclusive |
926 | |
927 | // 3. if there were no flex columns, then grow the table to the |
928 | // minWidth. |
929 | else if (tableWidth < minWidthConstraint) { |
930 | final double delta = (minWidthConstraint - tableWidth) / columns; |
931 | for (int x = 0; x < columns; x += 1) { |
932 | widths[x] = widths[x] + delta; |
933 | } |
934 | tableWidth = minWidthConstraint; |
935 | } |
936 | |
937 | // beyond this point, unflexedTableWidth is no longer valid |
938 | |
939 | // 4. apply the maximum width of the table, shrinking columns as |
940 | // necessary, applying minimum column widths as we go |
941 | if (tableWidth > maxWidthConstraint) { |
942 | double deficit = tableWidth - maxWidthConstraint; |
943 | // Some columns may have low flex but have all the free space. |
944 | // (Consider a case with a 1px wide column of flex 1000.0 and |
945 | // a 1000px wide column of flex 1.0; the sizes coming from the |
946 | // maxIntrinsicWidths. If the maximum table width is 2px, then |
947 | // just applying the flexes to the deficit would result in a |
948 | // table with one column at -998px and one column at 990px, |
949 | // which is wildly unhelpful.) |
950 | // Similarly, some columns may be flexible, but not actually |
951 | // be shrinkable due to a large minimum width. (Consider a |
952 | // case with two columns, one is flex and one isn't, both have |
953 | // 1000px maxIntrinsicWidths, but the flex one has 1000px |
954 | // minIntrinsicWidth also. The whole deficit will have to come |
955 | // from the non-flex column.) |
956 | // So what we do is we repeatedly iterate through the flexible |
957 | // columns shrinking them proportionally until we have no |
958 | // available columns, then do the same to the non-flexible ones. |
959 | int availableColumns = columns; |
960 | while (deficit > precisionErrorTolerance && totalFlex > precisionErrorTolerance) { |
961 | double newTotalFlex = 0.0; |
962 | for (int x = 0; x < columns; x += 1) { |
963 | if (flexes[x] != null) { |
964 | final double newWidth = widths[x] - deficit * flexes[x]! / totalFlex; |
965 | assert(newWidth.isFinite); |
966 | if (newWidth <= minWidths[x]) { |
967 | // shrank to minimum |
968 | deficit -= widths[x] - minWidths[x]; |
969 | widths[x] = minWidths[x]; |
970 | flexes[x] = null; |
971 | availableColumns -= 1; |
972 | } else { |
973 | deficit -= widths[x] - newWidth; |
974 | widths[x] = newWidth; |
975 | newTotalFlex += flexes[x]!; |
976 | } |
977 | assert(widths[x] >= 0.0); |
978 | } |
979 | } |
980 | totalFlex = newTotalFlex; |
981 | } |
982 | while (deficit > precisionErrorTolerance && availableColumns > 0) { |
983 | // Now we have to take out the remaining space from the |
984 | // columns that aren't minimum sized. |
985 | // To make this fair, we repeatedly remove equal amounts from |
986 | // each column, clamped to the minimum width, until we run out |
987 | // of columns that aren't at their minWidth. |
988 | final double delta = deficit / availableColumns; |
989 | assert(delta != 0); |
990 | int newAvailableColumns = 0; |
991 | for (int x = 0; x < columns; x += 1) { |
992 | final double availableDelta = widths[x] - minWidths[x]; |
993 | if (availableDelta > 0.0) { |
994 | if (availableDelta <= delta) { |
995 | // shrank to minimum |
996 | deficit -= widths[x] - minWidths[x]; |
997 | widths[x] = minWidths[x]; |
998 | } else { |
999 | deficit -= delta; |
1000 | widths[x] = widths[x] - delta; |
1001 | newAvailableColumns += 1; |
1002 | } |
1003 | } |
1004 | } |
1005 | availableColumns = newAvailableColumns; |
1006 | } |
1007 | } |
1008 | return widths; |
1009 | } |
1010 | |
1011 | // cache the table geometry for painting purposes |
1012 | final List<double> _rowTops = <double>[]; |
1013 | Iterable<double>? _columnLefts; |
1014 | late double _tableWidth; |
1015 | |
1016 | /// Returns the position and dimensions of the box that the given |
1017 | /// row covers, in this render object's coordinate space (so the |
1018 | /// left coordinate is always 0.0). |
1019 | /// |
1020 | /// The row being queried must exist. |
1021 | /// |
1022 | /// This is only valid after layout. |
1023 | Rect getRowBox(int row) { |
1024 | assert(row >= 0); |
1025 | assert(row < rows); |
1026 | assert(!debugNeedsLayout); |
1027 | return Rect.fromLTRB(0.0, _rowTops[row], size.width, _rowTops[row + 1]); |
1028 | } |
1029 | |
1030 | @override |
1031 | double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { |
1032 | if (rows * columns == 0) { |
1033 | return null; |
1034 | } |
1035 | final List<double> widths = _computeColumnWidths(constraints); |
1036 | double? baselineOffset; |
1037 | for (int col = 0; col < columns; col += 1) { |
1038 | final RenderBox? child = _children[col]; |
1039 | final BoxConstraints childConstraints = BoxConstraints.tightFor(width: widths[col]); |
1040 | if (child == null) { |
1041 | continue; |
1042 | } |
1043 | final TableCellParentData childParentData = child.parentData! as TableCellParentData; |
1044 | final double? childBaseline = switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { |
1045 | TableCellVerticalAlignment.baseline => child.getDryBaseline(childConstraints, baseline), |
1046 | TableCellVerticalAlignment.baseline || |
1047 | TableCellVerticalAlignment.top || |
1048 | TableCellVerticalAlignment.middle || |
1049 | TableCellVerticalAlignment.bottom || |
1050 | TableCellVerticalAlignment.fill || |
1051 | TableCellVerticalAlignment.intrinsicHeight => null, |
1052 | }; |
1053 | if (childBaseline != null && (baselineOffset == null || baselineOffset < childBaseline)) { |
1054 | baselineOffset = childBaseline; |
1055 | } |
1056 | } |
1057 | return baselineOffset; |
1058 | } |
1059 | |
1060 | @override |
1061 | @protected |
1062 | Size computeDryLayout(covariant BoxConstraints constraints) { |
1063 | if (rows * columns == 0) { |
1064 | return constraints.constrain(Size.zero); |
1065 | } |
1066 | final List<double> widths = _computeColumnWidths(constraints); |
1067 | final double tableWidth = widths.fold(0.0, (double a, double b) => a + b); |
1068 | double rowTop = 0.0; |
1069 | for (int y = 0; y < rows; y += 1) { |
1070 | double rowHeight = 0.0; |
1071 | for (int x = 0; x < columns; x += 1) { |
1072 | final int xy = x + y * columns; |
1073 | final RenderBox? child = _children[xy]; |
1074 | if (child != null) { |
1075 | final TableCellParentData childParentData = child.parentData! as TableCellParentData; |
1076 | switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { |
1077 | case TableCellVerticalAlignment.baseline: |
1078 | assert(debugCannotComputeDryLayout( |
1079 | reason: 'TableCellVerticalAlignment.baseline requires a full layout for baseline metrics to be available.' , |
1080 | )); |
1081 | return Size.zero; |
1082 | case TableCellVerticalAlignment.top: |
1083 | case TableCellVerticalAlignment.middle: |
1084 | case TableCellVerticalAlignment.bottom: |
1085 | case TableCellVerticalAlignment.intrinsicHeight: |
1086 | final Size childSize = child.getDryLayout(BoxConstraints.tightFor(width: widths[x])); |
1087 | rowHeight = math.max(rowHeight, childSize.height); |
1088 | case TableCellVerticalAlignment.fill: |
1089 | break; |
1090 | } |
1091 | } |
1092 | } |
1093 | rowTop += rowHeight; |
1094 | } |
1095 | return constraints.constrain(Size(tableWidth, rowTop)); |
1096 | } |
1097 | |
1098 | @override |
1099 | void performLayout() { |
1100 | final BoxConstraints constraints = this.constraints; |
1101 | final int rows = this.rows; |
1102 | final int columns = this.columns; |
1103 | assert(_children.length == rows * columns); |
1104 | if (rows * columns == 0) { |
1105 | // TODO(ianh): if columns is zero, this should be zero width |
1106 | // TODO(ianh): if columns is not zero, this should be based on the column width specifications |
1107 | _tableWidth = 0.0; |
1108 | size = constraints.constrain(Size.zero); |
1109 | return; |
1110 | } |
1111 | final List<double> widths = _computeColumnWidths(constraints); |
1112 | final List<double> positions = List<double>.filled(columns, 0.0); |
1113 | switch (textDirection) { |
1114 | case TextDirection.rtl: |
1115 | positions[columns - 1] = 0.0; |
1116 | for (int x = columns - 2; x >= 0; x -= 1) { |
1117 | positions[x] = positions[x+1] + widths[x+1]; |
1118 | } |
1119 | _columnLefts = positions.reversed; |
1120 | _tableWidth = positions.first + widths.first; |
1121 | case TextDirection.ltr: |
1122 | positions[0] = 0.0; |
1123 | for (int x = 1; x < columns; x += 1) { |
1124 | positions[x] = positions[x-1] + widths[x-1]; |
1125 | } |
1126 | _columnLefts = positions; |
1127 | _tableWidth = positions.last + widths.last; |
1128 | } |
1129 | _rowTops.clear(); |
1130 | _baselineDistance = null; |
1131 | // then, lay out each row |
1132 | double rowTop = 0.0; |
1133 | for (int y = 0; y < rows; y += 1) { |
1134 | _rowTops.add(rowTop); |
1135 | double rowHeight = 0.0; |
1136 | bool haveBaseline = false; |
1137 | double beforeBaselineDistance = 0.0; |
1138 | double afterBaselineDistance = 0.0; |
1139 | final List<double> baselines = List<double>.filled(columns, 0.0); |
1140 | for (int x = 0; x < columns; x += 1) { |
1141 | final int xy = x + y * columns; |
1142 | final RenderBox? child = _children[xy]; |
1143 | if (child != null) { |
1144 | final TableCellParentData childParentData = child.parentData! as TableCellParentData; |
1145 | childParentData.x = x; |
1146 | childParentData.y = y; |
1147 | switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { |
1148 | case TableCellVerticalAlignment.baseline: |
1149 | assert(textBaseline != null, 'An explicit textBaseline is required when using baseline alignment.' ); |
1150 | child.layout(BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true); |
1151 | final double? childBaseline = child.getDistanceToBaseline(textBaseline!, onlyReal: true); |
1152 | if (childBaseline != null) { |
1153 | beforeBaselineDistance = math.max(beforeBaselineDistance, childBaseline); |
1154 | afterBaselineDistance = math.max(afterBaselineDistance, child.size.height - childBaseline); |
1155 | baselines[x] = childBaseline; |
1156 | haveBaseline = true; |
1157 | } else { |
1158 | rowHeight = math.max(rowHeight, child.size.height); |
1159 | childParentData.offset = Offset(positions[x], rowTop); |
1160 | } |
1161 | case TableCellVerticalAlignment.top: |
1162 | case TableCellVerticalAlignment.middle: |
1163 | case TableCellVerticalAlignment.bottom: |
1164 | case TableCellVerticalAlignment.intrinsicHeight: |
1165 | child.layout(BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true); |
1166 | rowHeight = math.max(rowHeight, child.size.height); |
1167 | case TableCellVerticalAlignment.fill: |
1168 | break; |
1169 | } |
1170 | } |
1171 | } |
1172 | if (haveBaseline) { |
1173 | if (y == 0) { |
1174 | _baselineDistance = beforeBaselineDistance; |
1175 | } |
1176 | rowHeight = math.max(rowHeight, beforeBaselineDistance + afterBaselineDistance); |
1177 | } |
1178 | for (int x = 0; x < columns; x += 1) { |
1179 | final int xy = x + y * columns; |
1180 | final RenderBox? child = _children[xy]; |
1181 | if (child != null) { |
1182 | final TableCellParentData childParentData = child.parentData! as TableCellParentData; |
1183 | switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { |
1184 | case TableCellVerticalAlignment.baseline: |
1185 | childParentData.offset = Offset(positions[x], rowTop + beforeBaselineDistance - baselines[x]); |
1186 | case TableCellVerticalAlignment.top: |
1187 | childParentData.offset = Offset(positions[x], rowTop); |
1188 | case TableCellVerticalAlignment.middle: |
1189 | childParentData.offset = Offset(positions[x], rowTop + (rowHeight - child.size.height) / 2.0); |
1190 | case TableCellVerticalAlignment.bottom: |
1191 | childParentData.offset = Offset(positions[x], rowTop + rowHeight - child.size.height); |
1192 | case TableCellVerticalAlignment.fill: |
1193 | case TableCellVerticalAlignment.intrinsicHeight: |
1194 | child.layout(BoxConstraints.tightFor(width: widths[x], height: rowHeight)); |
1195 | childParentData.offset = Offset(positions[x], rowTop); |
1196 | } |
1197 | } |
1198 | } |
1199 | rowTop += rowHeight; |
1200 | } |
1201 | _rowTops.add(rowTop); |
1202 | size = constraints.constrain(Size(_tableWidth, rowTop)); |
1203 | assert(_rowTops.length == rows + 1); |
1204 | } |
1205 | |
1206 | @override |
1207 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |
1208 | assert(_children.length == rows * columns); |
1209 | for (int index = _children.length - 1; index >= 0; index -= 1) { |
1210 | final RenderBox? child = _children[index]; |
1211 | if (child != null) { |
1212 | final BoxParentData childParentData = child.parentData! as BoxParentData; |
1213 | final bool isHit = result.addWithPaintOffset( |
1214 | offset: childParentData.offset, |
1215 | position: position, |
1216 | hitTest: (BoxHitTestResult result, Offset transformed) { |
1217 | assert(transformed == position - childParentData.offset); |
1218 | return child.hitTest(result, position: transformed); |
1219 | }, |
1220 | ); |
1221 | if (isHit) { |
1222 | return true; |
1223 | } |
1224 | } |
1225 | } |
1226 | return false; |
1227 | } |
1228 | |
1229 | @override |
1230 | void paint(PaintingContext context, Offset offset) { |
1231 | assert(_children.length == rows * columns); |
1232 | if (rows * columns == 0) { |
1233 | if (border != null) { |
1234 | final Rect borderRect = Rect.fromLTWH(offset.dx, offset.dy, _tableWidth, 0.0); |
1235 | border!.paint(context.canvas, borderRect, rows: const <double>[], columns: const <double>[]); |
1236 | } |
1237 | return; |
1238 | } |
1239 | assert(_rowTops.length == rows + 1); |
1240 | if (_rowDecorations != null) { |
1241 | assert(_rowDecorations!.length == _rowDecorationPainters!.length); |
1242 | final Canvas canvas = context.canvas; |
1243 | for (int y = 0; y < rows; y += 1) { |
1244 | if (_rowDecorations!.length <= y) { |
1245 | break; |
1246 | } |
1247 | if (_rowDecorations![y] != null) { |
1248 | _rowDecorationPainters![y] ??= _rowDecorations![y]!.createBoxPainter(markNeedsPaint); |
1249 | _rowDecorationPainters![y]!.paint( |
1250 | canvas, |
1251 | Offset(offset.dx, offset.dy + _rowTops[y]), |
1252 | configuration.copyWith(size: Size(size.width, _rowTops[y+1] - _rowTops[y])), |
1253 | ); |
1254 | } |
1255 | } |
1256 | } |
1257 | for (int index = 0; index < _children.length; index += 1) { |
1258 | final RenderBox? child = _children[index]; |
1259 | if (child != null) { |
1260 | final BoxParentData childParentData = child.parentData! as BoxParentData; |
1261 | context.paintChild(child, childParentData.offset + offset); |
1262 | } |
1263 | } |
1264 | assert(_rows == _rowTops.length - 1); |
1265 | assert(_columns == _columnLefts!.length); |
1266 | if (border != null) { |
1267 | // The border rect might not fill the entire height of this render object |
1268 | // if the rows underflow. We always force the columns to fill the width of |
1269 | // the render object, which means the columns cannot underflow. |
1270 | final Rect borderRect = Rect.fromLTWH(offset.dx, offset.dy, _tableWidth, _rowTops.last); |
1271 | final Iterable<double> rows = _rowTops.getRange(1, _rowTops.length - 1); |
1272 | final Iterable<double> columns = _columnLefts!.skip(1); |
1273 | border!.paint(context.canvas, borderRect, rows: rows, columns: columns); |
1274 | } |
1275 | } |
1276 | |
1277 | @override |
1278 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1279 | super.debugFillProperties(properties); |
1280 | properties.add(DiagnosticsProperty<TableBorder>('border' , border, defaultValue: null)); |
1281 | properties.add(DiagnosticsProperty<Map<int, TableColumnWidth>>('specified column widths' , _columnWidths, level: _columnWidths.isEmpty ? DiagnosticLevel.hidden : DiagnosticLevel.info)); |
1282 | properties.add(DiagnosticsProperty<TableColumnWidth>('default column width' , defaultColumnWidth)); |
1283 | properties.add(MessageProperty('table size' , ' $columns\u00D7 $rows' )); |
1284 | properties.add(IterableProperty<String>('column offsets' , _columnLefts?.map(debugFormatDouble), ifNull: 'unknown' )); |
1285 | properties.add(IterableProperty<String>('row offsets' , _rowTops.map(debugFormatDouble), ifNull: 'unknown' )); |
1286 | } |
1287 | |
1288 | @override |
1289 | List<DiagnosticsNode> debugDescribeChildren() { |
1290 | if (_children.isEmpty) { |
1291 | return <DiagnosticsNode>[DiagnosticsNode.message('table is empty' )]; |
1292 | } |
1293 | |
1294 | return <DiagnosticsNode>[ |
1295 | for (int y = 0; y < rows; y += 1) |
1296 | for (int x = 0; x < columns; x += 1) |
1297 | if (_children[x + y * columns] case final RenderBox child) |
1298 | child.toDiagnosticsNode(name: 'child ( $x, $y)' ) |
1299 | else |
1300 | DiagnosticsProperty<Object>('child ( $x, $y)' , null, ifNull: 'is null' , showSeparator: false), |
1301 | ]; |
1302 | } |
1303 | } |
1304 | |