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 | import 'package:flutter/semantics.dart'; |
10 | |
11 | import 'box.dart'; |
12 | import 'object.dart'; |
13 | import 'table_border.dart'; |
14 | |
15 | /// Parent data used by [RenderTable] for its children. |
16 | class 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 |
43 | abstract 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. |
94 | class 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. |
136 | class 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. |
161 | class 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. |
197 | class 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. |
235 | class 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. |
287 | class 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]. |
333 | enum 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. |
361 | class 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. |
1561 | class _Index { |
1562 | _Index(this.y, this.x); |
1563 | int y; |
1564 | int x; |
1565 | } |
1566 |
Definitions
- TableCellParentData
- toString
- TableColumnWidth
- TableColumnWidth
- minIntrinsicWidth
- maxIntrinsicWidth
- flex
- toString
- IntrinsicColumnWidth
- IntrinsicColumnWidth
- minIntrinsicWidth
- maxIntrinsicWidth
- flex
- toString
- FixedColumnWidth
- FixedColumnWidth
- minIntrinsicWidth
- maxIntrinsicWidth
- toString
- FractionColumnWidth
- FractionColumnWidth
- minIntrinsicWidth
- maxIntrinsicWidth
- toString
- FlexColumnWidth
- FlexColumnWidth
- minIntrinsicWidth
- maxIntrinsicWidth
- flex
- toString
- MaxColumnWidth
- MaxColumnWidth
- minIntrinsicWidth
- maxIntrinsicWidth
- flex
- toString
- MinColumnWidth
- MinColumnWidth
- minIntrinsicWidth
- maxIntrinsicWidth
- flex
- toString
- TableCellVerticalAlignment
- RenderTable
- RenderTable
- columns
- columns
- rows
- rows
- columnWidths
- columnWidths
- setColumnWidth
- defaultColumnWidth
- defaultColumnWidth
- textDirection
- textDirection
- border
- border
- rowDecorations
- rowDecorations
- configuration
- configuration
- defaultVerticalAlignment
- defaultVerticalAlignment
- textBaseline
- textBaseline
- setupParentData
- describeSemanticsConfiguration
- assembleSemanticsNode
- rectWithOffset
- findRowIndex
- findColumnIndex
- shiftTransform
- setFlatChildren
- setChildren
- addRow
- setChild
- attach
- detach
- visitChildren
- redepthChildren
- computeMinIntrinsicWidth
- computeMaxIntrinsicWidth
- computeMinIntrinsicHeight
- computeMaxIntrinsicHeight
- computeDistanceToActualBaseline
- column
- row
- _computeColumnWidths
- getRowBox
- computeDryBaseline
- computeDryLayout
- performLayout
- hitTestChildren
- paint
- debugFillProperties
- debugDescribeChildren
- _Index
Learn more about Flutter for embedded and desktop on industrialflutter.com