| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'dart:collection'; | |
| import 'dart:math' as math; | |
| import 'package:flutter/foundation.dart'; | |
| import 'box.dart'; | |
| import 'object.dart'; | |
| import 'table_border.dart'; | |
| /// Parent data used by [RenderTable] for its children. | |
| class TableCellParentData extends BoxParentData { | |
| /// Where this cell should be placed vertically. | |
| /// | |
| /// When using [TableCellVerticalAlignment.baseline], the text baseline must be set as well. | |
| TableCellVerticalAlignment? verticalAlignment; | |
| /// The column that the child was in the last time it was laid out. | |
| int? x; | |
| /// The row that the child was in the last time it was laid out. | |
| int? y; | |
| @override | |
| String toString() => '${super.toString()}; ${verticalAlignment == null ? "default vertical alignment" : "$verticalAlignment"}'; | |
| } | |
| /// Base class to describe how wide a column in a [RenderTable] should be. | |
| /// | |
| /// To size a column to a specific number of pixels, use a [FixedColumnWidth]. | |
| /// This is the cheapest way to size a column. | |
| /// | |
| /// Other algorithms that are relatively cheap include [FlexColumnWidth], which | |
| /// distributes the space equally among the flexible columns, | |
| /// [FractionColumnWidth], which sizes a column based on the size of the | |
| /// table's container. | |
| @immutable | |
| abstract class TableColumnWidth { | |
| /// Abstract const constructor. This constructor enables subclasses to provide | |
| /// const constructors so that they can be used in const expressions. | |
| const TableColumnWidth(); | |
| /// The smallest width that the column can have. | |
| /// | |
| /// The `cells` argument is an iterable that provides all the cells | |
| /// in the table for this column. Walking the cells is by definition | |
| /// O(N), so algorithms that do that should be considered expensive. | |
| /// | |
| /// The `containerWidth` argument is the `maxWidth` of the incoming | |
| /// constraints for the table, and might be infinite. | |
| double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth); | |
| /// The ideal width that the column should have. This must be equal | |
| /// to or greater than the [minIntrinsicWidth]. The column might be | |
| /// bigger than this width, e.g. if the column is flexible or if the | |
| /// table's width ends up being forced to be bigger than the sum of | |
| /// all the maxIntrinsicWidth values. | |
| /// | |
| /// The `cells` argument is an iterable that provides all the cells | |
| /// in the table for this column. Walking the cells is by definition | |
| /// O(N), so algorithms that do that should be considered expensive. | |
| /// | |
| /// The `containerWidth` argument is the `maxWidth` of the incoming | |
| /// constraints for the table, and might be infinite. | |
| double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth); | |
| /// The flex factor to apply to the cell if there is any room left | |
| /// over when laying out the table. The remaining space is | |
| /// distributed to any columns with flex in proportion to their flex | |
| /// value (higher values get more space). | |
| /// | |
| /// The `cells` argument is an iterable that provides all the cells | |
| /// in the table for this column. Walking the cells is by definition | |
| /// O(N), so algorithms that do that should be considered expensive. | |
| double? flex(Iterable<RenderBox> cells) => null; | |
| @override | |
| String toString() => objectRuntimeType(this, 'TableColumnWidth'); | |
| } | |
| /// Sizes the column according to the intrinsic dimensions of all the | |
| /// cells in that column. | |
| /// | |
| /// This is a very expensive way to size a column. | |
| /// | |
| /// A flex value can be provided. If specified (and non-null), the | |
| /// column will participate in the distribution of remaining space | |
| /// once all the non-flexible columns have been sized. | |
| class IntrinsicColumnWidth extends TableColumnWidth { | |
| /// Creates a column width based on intrinsic sizing. | |
| /// | |
| /// This sizing algorithm is very expensive. | |
| /// | |
| /// The `flex` argument specifies the flex factor to apply to the column if | |
| /// there is any room left over when laying out the table. If `flex` is | |
| /// null (the default), the table will not distribute any extra space to the | |
| /// column. | |
| const IntrinsicColumnWidth({ double? flex }) : _flex = flex; | |
| @override | |
| double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| double result = 0.0; | |
| for (final RenderBox cell in cells) { | |
| result = math.max(result, cell.getMinIntrinsicWidth(double.infinity)); | |
| } | |
| return result; | |
| } | |
| @override | |
| double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| double result = 0.0; | |
| for (final RenderBox cell in cells) { | |
| result = math.max(result, cell.getMaxIntrinsicWidth(double.infinity)); | |
| } | |
| return result; | |
| } | |
| final double? _flex; | |
| @override | |
| double? flex(Iterable<RenderBox> cells) => _flex; | |
| @override | |
| String toString() => '${objectRuntimeType(this, 'IntrinsicColumnWidth')}(flex: ${_flex?.toStringAsFixed(1)})'; | |
| } | |
| /// Sizes the column to a specific number of pixels. | |
| /// | |
| /// This is the cheapest way to size a column. | |
| class FixedColumnWidth extends TableColumnWidth { | |
| /// Creates a column width based on a fixed number of logical pixels. | |
| /// | |
| /// The [value] argument must not be null. | |
| const FixedColumnWidth(this.value); | |
| /// The width the column should occupy in logical pixels. | |
| final double value; | |
| @override | |
| double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| return value; | |
| } | |
| @override | |
| double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| return value; | |
| } | |
| @override | |
| String toString() => '${objectRuntimeType(this, 'FixedColumnWidth')}(${debugFormatDouble(value)})'; | |
| } | |
| /// Sizes the column to a fraction of the table's constraints' maxWidth. | |
| /// | |
| /// This is a cheap way to size a column. | |
| class FractionColumnWidth extends TableColumnWidth { | |
| /// Creates a column width based on a fraction of the table's constraints' | |
| /// maxWidth. | |
| /// | |
| /// The [value] argument must not be null. | |
| const FractionColumnWidth(this.value); | |
| /// The fraction of the table's constraints' maxWidth that this column should | |
| /// occupy. | |
| final double value; | |
| @override | |
| double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| if (!containerWidth.isFinite) { | |
| return 0.0; | |
| } | |
| return value * containerWidth; | |
| } | |
| @override | |
| double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| if (!containerWidth.isFinite) { | |
| return 0.0; | |
| } | |
| return value * containerWidth; | |
| } | |
| @override | |
| String toString() => '${objectRuntimeType(this, 'FractionColumnWidth')}($value)'; | |
| } | |
| /// Sizes the column by taking a part of the remaining space once all | |
| /// the other columns have been laid out. | |
| /// | |
| /// For example, if two columns have a [FlexColumnWidth], then half the | |
| /// space will go to one and half the space will go to the other. | |
| /// | |
| /// This is a cheap way to size a column. | |
| class FlexColumnWidth extends TableColumnWidth { | |
| /// Creates a column width based on a fraction of the remaining space once all | |
| /// the other columns have been laid out. | |
| /// | |
| /// The [value] argument must not be null. | |
| const FlexColumnWidth([this.value = 1.0]); | |
| /// The fraction of the remaining space once all the other columns have | |
| /// been laid out that this column should occupy. | |
| final double value; | |
| @override | |
| double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| return 0.0; | |
| } | |
| @override | |
| double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| return 0.0; | |
| } | |
| @override | |
| double flex(Iterable<RenderBox> cells) { | |
| return value; | |
| } | |
| @override | |
| String toString() => '${objectRuntimeType(this, 'FlexColumnWidth')}(${debugFormatDouble(value)})'; | |
| } | |
| /// Sizes the column such that it is the size that is the maximum of | |
| /// two column width specifications. | |
| /// | |
| /// For example, to have a column be 10% of the container width or | |
| /// 100px, whichever is bigger, you could use: | |
| /// | |
| /// const MaxColumnWidth(const FixedColumnWidth(100.0), FractionColumnWidth(0.1)) | |
| /// | |
| /// Both specifications are evaluated, so if either specification is | |
| /// expensive, so is this. | |
| class MaxColumnWidth extends TableColumnWidth { | |
| /// Creates a column width that is the maximum of two other column widths. | |
| const MaxColumnWidth(this.a, this.b); | |
| /// A lower bound for the width of this column. | |
| final TableColumnWidth a; | |
| /// Another lower bound for the width of this column. | |
| final TableColumnWidth b; | |
| @override | |
| double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| return math.max( | |
| a.minIntrinsicWidth(cells, containerWidth), | |
| b.minIntrinsicWidth(cells, containerWidth), | |
| ); | |
| } | |
| @override | |
| double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| return math.max( | |
| a.maxIntrinsicWidth(cells, containerWidth), | |
| b.maxIntrinsicWidth(cells, containerWidth), | |
| ); | |
| } | |
| @override | |
| double? flex(Iterable<RenderBox> cells) { | |
| final double? aFlex = a.flex(cells); | |
| if (aFlex == null) { | |
| return b.flex(cells); | |
| } | |
| final double? bFlex = b.flex(cells); | |
| if (bFlex == null) { | |
| return null; | |
| } | |
| return math.max(aFlex, bFlex); | |
| } | |
| @override | |
| String toString() => '${objectRuntimeType(this, 'MaxColumnWidth')}($a, $b)'; | |
| } | |
| /// Sizes the column such that it is the size that is the minimum of | |
| /// two column width specifications. | |
| /// | |
| /// For example, to have a column be 10% of the container width but | |
| /// never bigger than 100px, you could use: | |
| /// | |
| /// const MinColumnWidth(const FixedColumnWidth(100.0), FractionColumnWidth(0.1)) | |
| /// | |
| /// Both specifications are evaluated, so if either specification is | |
| /// expensive, so is this. | |
| class MinColumnWidth extends TableColumnWidth { | |
| /// Creates a column width that is the minimum of two other column widths. | |
| const MinColumnWidth(this.a, this.b); | |
| /// An upper bound for the width of this column. | |
| final TableColumnWidth a; | |
| /// Another upper bound for the width of this column. | |
| final TableColumnWidth b; | |
| @override | |
| double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| return math.min( | |
| a.minIntrinsicWidth(cells, containerWidth), | |
| b.minIntrinsicWidth(cells, containerWidth), | |
| ); | |
| } | |
| @override | |
| double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { | |
| return math.min( | |
| a.maxIntrinsicWidth(cells, containerWidth), | |
| b.maxIntrinsicWidth(cells, containerWidth), | |
| ); | |
| } | |
| @override | |
| double? flex(Iterable<RenderBox> cells) { | |
| final double? aFlex = a.flex(cells); | |
| if (aFlex == null) { | |
| return b.flex(cells); | |
| } | |
| final double? bFlex = b.flex(cells); | |
| if (bFlex == null) { | |
| return null; | |
| } | |
| return math.min(aFlex, bFlex); | |
| } | |
| @override | |
| String toString() => '${objectRuntimeType(this, 'MinColumnWidth')}($a, $b)'; | |
| } | |
| /// Vertical alignment options for cells in [RenderTable] objects. | |
| /// | |
| /// This is specified using [TableCellParentData] objects on the | |
| /// [RenderObject.parentData] of the children of the [RenderTable]. | |
| enum TableCellVerticalAlignment { | |
| /// Cells with this alignment are placed with their top at the top of the row. | |
| top, | |
| /// Cells with this alignment are vertically centered in the row. | |
| middle, | |
| /// Cells with this alignment are placed with their bottom at the bottom of the row. | |
| bottom, | |
| /// Cells with this alignment are aligned such that they all share the same | |
| /// baseline. Cells with no baseline are top-aligned instead. The baseline | |
| /// used is specified by [RenderTable.textBaseline]. It is not valid to use | |
| /// the baseline value if [RenderTable.textBaseline] is not specified. | |
| /// | |
| /// This vertical alignment is relatively expensive because it causes the table | |
| /// to compute the baseline for each cell in the row. | |
| baseline, | |
| /// Cells with this alignment are sized to be as tall as the row, then made to fit the row. | |
| /// If all the cells have this alignment, then the row will have zero height. | |
| fill | |
| } | |
| /// A table where the columns and rows are sized to fit the contents of the cells. | |
| class RenderTable extends RenderBox { | |
| /// Creates a table render object. | |
| /// | |
| /// * `columns` must either be null or non-negative. If `columns` is null, | |
| /// the number of columns will be inferred from length of the first sublist | |
| /// of `children`. | |
| /// * `rows` must either be null or non-negative. If `rows` is null, the | |
| /// number of rows will be inferred from the `children`. If `rows` is not | |
| /// null, then `children` must be null. | |
| /// * `children` must either be null or contain lists of all the same length. | |
| /// if `children` is not null, then `rows` must be null. | |
| /// * [columnWidths] may be null, in which case it defaults to an empty map. | |
| /// * [defaultColumnWidth] must not be null. | |
| /// * [configuration] must not be null (but has a default value). | |
| RenderTable({ | |
| int? columns, | |
| int? rows, | |
| Map<int, TableColumnWidth>? columnWidths, | |
| TableColumnWidth defaultColumnWidth = const FlexColumnWidth(), | |
| required TextDirection textDirection, | |
| TableBorder? border, | |
| List<Decoration?>? rowDecorations, | |
| ImageConfiguration configuration = ImageConfiguration.empty, | |
| TableCellVerticalAlignment defaultVerticalAlignment = TableCellVerticalAlignment.top, | |
| TextBaseline? textBaseline, | |
| List<List<RenderBox>>? children, | |
| }) : assert(columns == null || columns >= 0), | |
| assert(rows == null || rows >= 0), | |
| assert(rows == null || children == null), | |
| _textDirection = textDirection, | |
| _columns = columns ?? (children != null && children.isNotEmpty ? children.first.length : 0), | |
| _rows = rows ?? 0, | |
| _columnWidths = columnWidths ?? HashMap<int, TableColumnWidth>(), | |
| _defaultColumnWidth = defaultColumnWidth, | |
| _border = border, | |
| _textBaseline = textBaseline, | |
| _defaultVerticalAlignment = defaultVerticalAlignment, | |
| _configuration = configuration { | |
| _children = <RenderBox?>[]..length = _columns * _rows; | |
| this.rowDecorations = rowDecorations; // must use setter to initialize box painters array | |
| children?.forEach(addRow); | |
| } | |
| // Children are stored in row-major order. | |
| // _children.length must be rows * columns | |
| List<RenderBox?> _children = const <RenderBox?>[]; | |
| /// The number of vertical alignment lines in this table. | |
| /// | |
| /// Changing the number of columns will remove any children that no longer fit | |
| /// in the table. | |
| /// | |
| /// Changing the number of columns is an expensive operation because the table | |
| /// needs to rearrange its internal representation. | |
| int get columns => _columns; | |
| int _columns; | |
| set columns(int value) { | |
| assert(value >= 0); | |
| if (value == columns) { | |
| return; | |
| } | |
| final int oldColumns = columns; | |
| final List<RenderBox?> oldChildren = _children; | |
| _columns = value; | |
| _children = List<RenderBox?>.filled(columns * rows, null); | |
| final int columnsToCopy = math.min(columns, oldColumns); | |
| for (int y = 0; y < rows; y += 1) { | |
| for (int x = 0; x < columnsToCopy; x += 1) { | |
| _children[x + y * columns] = oldChildren[x + y * oldColumns]; | |
| } | |
| } | |
| if (oldColumns > columns) { | |
| for (int y = 0; y < rows; y += 1) { | |
| for (int x = columns; x < oldColumns; x += 1) { | |
| final int xy = x + y * oldColumns; | |
| if (oldChildren[xy] != null) { | |
| dropChild(oldChildren[xy]!); | |
| } | |
| } | |
| } | |
| } | |
| markNeedsLayout(); | |
| } | |
| /// The number of horizontal alignment lines in this table. | |
| /// | |
| /// Changing the number of rows will remove any children that no longer fit | |
| /// in the table. | |
| int get rows => _rows; | |
| int _rows; | |
| set rows(int value) { | |
| assert(value >= 0); | |
| if (value == rows) { | |
| return; | |
| } | |
| if (_rows > value) { | |
| for (int xy = columns * value; xy < _children.length; xy += 1) { | |
| if (_children[xy] != null) { | |
| dropChild(_children[xy]!); | |
| } | |
| } | |
| } | |
| _rows = value; | |
| _children.length = columns * rows; | |
| markNeedsLayout(); | |
| } | |
| /// How the horizontal extents of the columns of this table should be determined. | |
| /// | |
| /// If the [Map] has a null entry for a given column, the table uses the | |
| /// [defaultColumnWidth] instead. | |
| /// | |
| /// The layout performance of the table depends critically on which column | |
| /// sizing algorithms are used here. In particular, [IntrinsicColumnWidth] is | |
| /// quite expensive because it needs to measure each cell in the column to | |
| /// determine the intrinsic size of the column. | |
| /// | |
| /// This property can never return null. If it is set to null, and the existing | |
| /// map is not empty, then the value is replaced by an empty map. (If it is set | |
| /// to null while the current value is an empty map, the value is not changed.) | |
| Map<int, TableColumnWidth>? get columnWidths => Map<int, TableColumnWidth>.unmodifiable(_columnWidths); | |
| Map<int, TableColumnWidth> _columnWidths; | |
| set columnWidths(Map<int, TableColumnWidth>? value) { | |
| if (_columnWidths == value) { | |
| return; | |
| } | |
| if (_columnWidths.isEmpty && value == null) { | |
| return; | |
| } | |
| _columnWidths = value ?? HashMap<int, TableColumnWidth>(); | |
| markNeedsLayout(); | |
| } | |
| /// Determines how the width of column with the given index is determined. | |
| void setColumnWidth(int column, TableColumnWidth value) { | |
| if (_columnWidths[column] == value) { | |
| return; | |
| } | |
| _columnWidths[column] = value; | |
| markNeedsLayout(); | |
| } | |
| /// How to determine with widths of columns that don't have an explicit sizing algorithm. | |
| /// | |
| /// Specifically, the [defaultColumnWidth] is used for column `i` if | |
| /// `columnWidths[i]` is null. | |
| TableColumnWidth get defaultColumnWidth => _defaultColumnWidth; | |
| TableColumnWidth _defaultColumnWidth; | |
| set defaultColumnWidth(TableColumnWidth value) { | |
| if (defaultColumnWidth == value) { | |
| return; | |
| } | |
| _defaultColumnWidth = value; | |
| markNeedsLayout(); | |
| } | |
| /// The direction in which the columns are ordered. | |
| TextDirection get textDirection => _textDirection; | |
| TextDirection _textDirection; | |
| set textDirection(TextDirection value) { | |
| if (_textDirection == value) { | |
| return; | |
| } | |
| _textDirection = value; | |
| markNeedsLayout(); | |
| } | |
| /// The style to use when painting the boundary and interior divisions of the table. | |
| TableBorder? get border => _border; | |
| TableBorder? _border; | |
| set border(TableBorder? value) { | |
| if (border == value) { | |
| return; | |
| } | |
| _border = value; | |
| markNeedsPaint(); | |
| } | |
| /// The decorations to use for each row of the table. | |
| /// | |
| /// Row decorations fill the horizontal and vertical extent of each row in | |
| /// the table, unlike decorations for individual cells, which might not fill | |
| /// either. | |
| List<Decoration> get rowDecorations => List<Decoration>.unmodifiable(_rowDecorations ?? const <Decoration>[]); | |
| // _rowDecorations and _rowDecorationPainters need to be in sync. They have to | |
| // either both be null or have same length. | |
| List<Decoration?>? _rowDecorations; | |
| List<BoxPainter?>? _rowDecorationPainters; | |
| set rowDecorations(List<Decoration?>? value) { | |
| if (_rowDecorations == value) { | |
| return; | |
| } | |
| _rowDecorations = value; | |
| if (_rowDecorationPainters != null) { | |
| for (final BoxPainter? painter in _rowDecorationPainters!) { | |
| painter?.dispose(); | |
| } | |
| } | |
| _rowDecorationPainters = _rowDecorations != null ? List<BoxPainter?>.filled(_rowDecorations!.length, null) : null; | |
| } | |
| /// The settings to pass to the [rowDecorations] when painting, so that they | |
| /// can resolve images appropriately. See [ImageProvider.resolve] and | |
| /// [BoxPainter.paint]. | |
| ImageConfiguration get configuration => _configuration; | |
| ImageConfiguration _configuration; | |
| set configuration(ImageConfiguration value) { | |
| if (value == _configuration) { | |
| return; | |
| } | |
| _configuration = value; | |
| markNeedsPaint(); | |
| } | |
| /// How cells that do not explicitly specify a vertical alignment are aligned vertically. | |
| TableCellVerticalAlignment get defaultVerticalAlignment => _defaultVerticalAlignment; | |
| TableCellVerticalAlignment _defaultVerticalAlignment; | |
| set defaultVerticalAlignment(TableCellVerticalAlignment value) { | |
| if (_defaultVerticalAlignment == value) { | |
| return; | |
| } | |
| _defaultVerticalAlignment = value; | |
| markNeedsLayout(); | |
| } | |
| /// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline]. | |
| TextBaseline? get textBaseline => _textBaseline; | |
| TextBaseline? _textBaseline; | |
| set textBaseline(TextBaseline? value) { | |
| if (_textBaseline == value) { | |
| return; | |
| } | |
| _textBaseline = value; | |
| markNeedsLayout(); | |
| } | |
| @override | |
| void setupParentData(RenderObject child) { | |
| if (child.parentData is! TableCellParentData) { | |
| child.parentData = TableCellParentData(); | |
| } | |
| } | |
| /// Replaces the children of this table with the given cells. | |
| /// | |
| /// The cells are divided into the specified number of columns before | |
| /// replacing the existing children. | |
| /// | |
| /// If the new cells contain any existing children of the table, those | |
| /// children are moved to their new location in the table rather than | |
| /// removed from the table and re-added. | |
| void setFlatChildren(int columns, List<RenderBox?> cells) { | |
| if (cells == _children && columns == _columns) { | |
| return; | |
| } | |
| assert(columns >= 0); | |
| // consider the case of a newly empty table | |
| if (columns == 0 || cells.isEmpty) { | |
| assert(cells.isEmpty); | |
| _columns = columns; | |
| if (_children.isEmpty) { | |
| assert(_rows == 0); | |
| return; | |
| } | |
| for (final RenderBox? oldChild in _children) { | |
| if (oldChild != null) { | |
| dropChild(oldChild); | |
| } | |
| } | |
| _rows = 0; | |
| _children.clear(); | |
| markNeedsLayout(); | |
| return; | |
| } | |
| assert(cells.length % columns == 0); | |
| // fill a set with the cells that are moving (it's important not | |
| // to dropChild a child that's remaining with us, because that | |
| // would clear their parentData field) | |
| final Set<RenderBox> lostChildren = HashSet<RenderBox>(); | |
| for (int y = 0; y < _rows; y += 1) { | |
| for (int x = 0; x < _columns; x += 1) { | |
| final int xyOld = x + y * _columns; | |
| final int xyNew = x + y * columns; | |
| if (_children[xyOld] != null && (x >= columns || xyNew >= cells.length || _children[xyOld] != cells[xyNew])) { | |
| lostChildren.add(_children[xyOld]!); | |
| } | |
| } | |
| } | |
| // adopt cells that are arriving, and cross cells that are just moving off our list of lostChildren | |
| int y = 0; | |
| while (y * columns < cells.length) { | |
| for (int x = 0; x < columns; x += 1) { | |
| final int xyNew = x + y * columns; | |
| final int xyOld = x + y * _columns; | |
| if (cells[xyNew] != null && (x >= _columns || y >= _rows || _children[xyOld] != cells[xyNew])) { | |
| if (!lostChildren.remove(cells[xyNew])) { | |
| adoptChild(cells[xyNew]!); | |
| } | |
| } | |
| } | |
| y += 1; | |
| } | |
| // drop all the lost children | |
| lostChildren.forEach(dropChild); | |
| // update our internal values | |
| _columns = columns; | |
| _rows = cells.length ~/ columns; | |
| _children = List<RenderBox?>.of(cells); | |
| assert(_children.length == rows * columns); | |
| markNeedsLayout(); | |
| } | |
| /// Replaces the children of this table with the given cells. | |
| void setChildren(List<List<RenderBox>>? cells) { | |
| // TODO(ianh): Make this smarter, like setFlatChildren | |
| if (cells == null) { | |
| setFlatChildren(0, const <RenderBox?>[]); | |
| return; | |
| } | |
| for (final RenderBox? oldChild in _children) { | |
| if (oldChild != null) { | |
| dropChild(oldChild); | |
| } | |
| } | |
| _children.clear(); | |
| _columns = cells.isNotEmpty ? cells.first.length : 0; | |
| _rows = 0; | |
| cells.forEach(addRow); | |
| assert(_children.length == rows * columns); | |
| } | |
| /// Adds a row to the end of the table. | |
| /// | |
| /// The newly added children must not already have parents. | |
| void addRow(List<RenderBox?> cells) { | |
| assert(cells.length == columns); | |
| assert(_children.length == rows * columns); | |
| _rows += 1; | |
| _children.addAll(cells); | |
| for (final RenderBox? cell in cells) { | |
| if (cell != null) { | |
| adoptChild(cell); | |
| } | |
| } | |
| markNeedsLayout(); | |
| } | |
| /// Replaces the child at the given position with the given child. | |
| /// | |
| /// If the given child is already located at the given position, this function | |
| /// does not modify the table. Otherwise, the given child must not already | |
| /// have a parent. | |
| void setChild(int x, int y, RenderBox? value) { | |
| assert(x >= 0 && x < columns && y >= 0 && y < rows); | |
| assert(_children.length == rows * columns); | |
| final int xy = x + y * columns; | |
| final RenderBox? oldChild = _children[xy]; | |
| if (oldChild == value) { | |
| return; | |
| } | |
| if (oldChild != null) { | |
| dropChild(oldChild); | |
| } | |
| _children[xy] = value; | |
| if (value != null) { | |
| adoptChild(value); | |
| } | |
| } | |
| @override | |
| void attach(PipelineOwner owner) { | |
| super.attach(owner); | |
| for (final RenderBox? child in _children) { | |
| child?.attach(owner); | |
| } | |
| } | |
| @override | |
| void detach() { | |
| super.detach(); | |
| if (_rowDecorationPainters != null) { | |
| for (final BoxPainter? painter in _rowDecorationPainters!) { | |
| painter?.dispose(); | |
| } | |
| _rowDecorationPainters = List<BoxPainter?>.filled(_rowDecorations!.length, null); | |
| } | |
| for (final RenderBox? child in _children) { | |
| child?.detach(); | |
| } | |
| } | |
| @override | |
| void visitChildren(RenderObjectVisitor visitor) { | |
| assert(_children.length == rows * columns); | |
| for (final RenderBox? child in _children) { | |
| if (child != null) { | |
| visitor(child); | |
| } | |
| } | |
| } | |
| @override | |
| double computeMinIntrinsicWidth(double height) { | |
| assert(_children.length == rows * columns); | |
| double totalMinWidth = 0.0; | |
| for (int x = 0; x < columns; x += 1) { | |
| final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth; | |
| final Iterable<RenderBox> columnCells = column(x); | |
| totalMinWidth += columnWidth.minIntrinsicWidth(columnCells, double.infinity); | |
| } | |
| return totalMinWidth; | |
| } | |
| @override | |
| double computeMaxIntrinsicWidth(double height) { | |
| assert(_children.length == rows * columns); | |
| double totalMaxWidth = 0.0; | |
| for (int x = 0; x < columns; x += 1) { | |
| final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth; | |
| final Iterable<RenderBox> columnCells = column(x); | |
| totalMaxWidth += columnWidth.maxIntrinsicWidth(columnCells, double.infinity); | |
| } | |
| return totalMaxWidth; | |
| } | |
| @override | |
| double computeMinIntrinsicHeight(double width) { | |
| // winner of the 2016 world's most expensive intrinsic dimension function award | |
| // honorable mention, most likely to improve if taught about memoization award | |
| assert(_children.length == rows * columns); | |
| final List<double> widths = _computeColumnWidths(BoxConstraints.tightForFinite(width: width)); | |
| double rowTop = 0.0; | |
| for (int y = 0; y < rows; y += 1) { | |
| double rowHeight = 0.0; | |
| for (int x = 0; x < columns; x += 1) { | |
| final int xy = x + y * columns; | |
| final RenderBox? child = _children[xy]; | |
| if (child != null) { | |
| rowHeight = math.max(rowHeight, child.getMaxIntrinsicHeight(widths[x])); | |
| } | |
| } | |
| rowTop += rowHeight; | |
| } | |
| return rowTop; | |
| } | |
| @override | |
| double computeMaxIntrinsicHeight(double width) { | |
| return computeMinIntrinsicHeight(width); | |
| } | |
| double? _baselineDistance; | |
| @override | |
| double? computeDistanceToActualBaseline(TextBaseline baseline) { | |
| // returns the baseline of the first cell that has a baseline in the first row | |
| assert(!debugNeedsLayout); | |
| return _baselineDistance; | |
| } | |
| /// Returns the list of [RenderBox] objects that are in the given | |
| /// column, in row order, starting from the first row. | |
| /// | |
| /// This is a lazily-evaluated iterable. | |
| // The following uses sync* because it is public API documented to return a | |
| // lazy iterable. | |
| Iterable<RenderBox> column(int x) sync* { | |
| for (int y = 0; y < rows; y += 1) { | |
| final int xy = x + y * columns; | |
| final RenderBox? child = _children[xy]; | |
| if (child != null) { | |
| yield child; | |
| } | |
| } | |
| } | |
| /// Returns the list of [RenderBox] objects that are on the given | |
| /// row, in column order, starting with the first column. | |
| /// | |
| /// This is a lazily-evaluated iterable. | |
| // The following uses sync* because it is public API documented to return a | |
| // lazy iterable. | |
| Iterable<RenderBox> row(int y) sync* { | |
| final int start = y * columns; | |
| final int end = (y + 1) * columns; | |
| for (int xy = start; xy < end; xy += 1) { | |
| final RenderBox? child = _children[xy]; | |
| if (child != null) { | |
| yield child; | |
| } | |
| } | |
| } | |
| List<double> _computeColumnWidths(BoxConstraints constraints) { | |
| assert(_children.length == rows * columns); | |
| // We apply the constraints to the column widths in the order of | |
| // least important to most important: | |
| // 1. apply the ideal widths (maxIntrinsicWidth) | |
| // 2. grow the flex columns so that the table has the maxWidth (if | |
| // finite) or the minWidth (if not) | |
| // 3. if there were no flex columns, then grow the table to the | |
| // minWidth. | |
| // 4. apply the maximum width of the table, shrinking columns as | |
| // necessary, applying minimum column widths as we go | |
| // 1. apply ideal widths, and collect information we'll need later | |
| final List<double> widths = List<double>.filled(columns, 0.0); | |
| final List<double> minWidths = List<double>.filled(columns, 0.0); | |
| final List<double?> flexes = List<double?>.filled(columns, null); | |
| double tableWidth = 0.0; // running tally of the sum of widths[x] for all x | |
| double unflexedTableWidth = 0.0; // sum of the maxIntrinsicWidths of any column that has null flex | |
| double totalFlex = 0.0; | |
| for (int x = 0; x < columns; x += 1) { | |
| final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth; | |
| final Iterable<RenderBox> columnCells = column(x); | |
| // apply ideal width (maxIntrinsicWidth) | |
| final double maxIntrinsicWidth = columnWidth.maxIntrinsicWidth(columnCells, constraints.maxWidth); | |
| assert(maxIntrinsicWidth.isFinite); | |
| assert(maxIntrinsicWidth >= 0.0); | |
| widths[x] = maxIntrinsicWidth; | |
| tableWidth += maxIntrinsicWidth; | |
| // collect min width information while we're at it | |
| final double minIntrinsicWidth = columnWidth.minIntrinsicWidth(columnCells, constraints.maxWidth); | |
| assert(minIntrinsicWidth.isFinite); | |
| assert(minIntrinsicWidth >= 0.0); | |
| minWidths[x] = minIntrinsicWidth; | |
| assert(maxIntrinsicWidth >= minIntrinsicWidth); | |
| // collect flex information while we're at it | |
| final double? flex = columnWidth.flex(columnCells); | |
| if (flex != null) { | |
| assert(flex.isFinite); | |
| assert(flex > 0.0); | |
| flexes[x] = flex; | |
| totalFlex += flex; | |
| } else { | |
| unflexedTableWidth = unflexedTableWidth + maxIntrinsicWidth; | |
| } | |
| } | |
| final double maxWidthConstraint = constraints.maxWidth; | |
| final double minWidthConstraint = constraints.minWidth; | |
| // 2. grow the flex columns so that the table has the maxWidth (if | |
| // finite) or the minWidth (if not) | |
| if (totalFlex > 0.0) { | |
| // this can only grow the table, but it _will_ grow the table at | |
| // least as big as the target width. | |
| final double targetWidth; | |
| if (maxWidthConstraint.isFinite) { | |
| targetWidth = maxWidthConstraint; | |
| } else { | |
| targetWidth = minWidthConstraint; | |
| } | |
| if (tableWidth < targetWidth) { | |
| final double remainingWidth = targetWidth - unflexedTableWidth; | |
| assert(remainingWidth.isFinite); | |
| assert(remainingWidth >= 0.0); | |
| for (int x = 0; x < columns; x += 1) { | |
| if (flexes[x] != null) { | |
| final double flexedWidth = remainingWidth * flexes[x]! / totalFlex; | |
| assert(flexedWidth.isFinite); | |
| assert(flexedWidth >= 0.0); | |
| if (widths[x] < flexedWidth) { | |
| final double delta = flexedWidth - widths[x]; | |
| tableWidth += delta; | |
| widths[x] = flexedWidth; | |
| } | |
| } | |
| } | |
| assert(tableWidth + precisionErrorTolerance >= targetWidth); | |
| } | |
| } // step 2 and 3 are mutually exclusive | |
| // 3. if there were no flex columns, then grow the table to the | |
| // minWidth. | |
| else if (tableWidth < minWidthConstraint) { | |
| final double delta = (minWidthConstraint - tableWidth) / columns; | |
| for (int x = 0; x < columns; x += 1) { | |
| widths[x] = widths[x] + delta; | |
| } | |
| tableWidth = minWidthConstraint; | |
| } | |
| // beyond this point, unflexedTableWidth is no longer valid | |
| // 4. apply the maximum width of the table, shrinking columns as | |
| // necessary, applying minimum column widths as we go | |
| if (tableWidth > maxWidthConstraint) { | |
| double deficit = tableWidth - maxWidthConstraint; | |
| // Some columns may have low flex but have all the free space. | |
| // (Consider a case with a 1px wide column of flex 1000.0 and | |
| // a 1000px wide column of flex 1.0; the sizes coming from the | |
| // maxIntrinsicWidths. If the maximum table width is 2px, then | |
| // just applying the flexes to the deficit would result in a | |
| // table with one column at -998px and one column at 990px, | |
| // which is wildly unhelpful.) | |
| // Similarly, some columns may be flexible, but not actually | |
| // be shrinkable due to a large minimum width. (Consider a | |
| // case with two columns, one is flex and one isn't, both have | |
| // 1000px maxIntrinsicWidths, but the flex one has 1000px | |
| // minIntrinsicWidth also. The whole deficit will have to come | |
| // from the non-flex column.) | |
| // So what we do is we repeatedly iterate through the flexible | |
| // columns shrinking them proportionally until we have no | |
| // available columns, then do the same to the non-flexible ones. | |
| int availableColumns = columns; | |
| while (deficit > precisionErrorTolerance && totalFlex > precisionErrorTolerance) { | |
| double newTotalFlex = 0.0; | |
| for (int x = 0; x < columns; x += 1) { | |
| if (flexes[x] != null) { | |
| final double newWidth = widths[x] - deficit * flexes[x]! / totalFlex; | |
| assert(newWidth.isFinite); | |
| if (newWidth <= minWidths[x]) { | |
| // shrank to minimum | |
| deficit -= widths[x] - minWidths[x]; | |
| widths[x] = minWidths[x]; | |
| flexes[x] = null; | |
| availableColumns -= 1; | |
| } else { | |
| deficit -= widths[x] - newWidth; | |
| widths[x] = newWidth; | |
| newTotalFlex += flexes[x]!; | |
| } | |
| assert(widths[x] >= 0.0); | |
| } | |
| } | |
| totalFlex = newTotalFlex; | |
| } | |
| while (deficit > precisionErrorTolerance && availableColumns > 0) { | |
| // Now we have to take out the remaining space from the | |
| // columns that aren't minimum sized. | |
| // To make this fair, we repeatedly remove equal amounts from | |
| // each column, clamped to the minimum width, until we run out | |
| // of columns that aren't at their minWidth. | |
| final double delta = deficit / availableColumns; | |
| assert(delta != 0); | |
| int newAvailableColumns = 0; | |
| for (int x = 0; x < columns; x += 1) { | |
| final double availableDelta = widths[x] - minWidths[x]; | |
| if (availableDelta > 0.0) { | |
| if (availableDelta <= delta) { | |
| // shrank to minimum | |
| deficit -= widths[x] - minWidths[x]; | |
| widths[x] = minWidths[x]; | |
| } else { | |
| deficit -= delta; | |
| widths[x] = widths[x] - delta; | |
| newAvailableColumns += 1; | |
| } | |
| } | |
| } | |
| availableColumns = newAvailableColumns; | |
| } | |
| } | |
| return widths; | |
| } | |
| // cache the table geometry for painting purposes | |
| final List<double> _rowTops = <double>[]; | |
| Iterable<double>? _columnLefts; | |
| late double _tableWidth; | |
| /// Returns the position and dimensions of the box that the given | |
| /// row covers, in this render object's coordinate space (so the | |
| /// left coordinate is always 0.0). | |
| /// | |
| /// The row being queried must exist. | |
| /// | |
| /// This is only valid after layout. | |
| Rect getRowBox(int row) { | |
| assert(row >= 0); | |
| assert(row < rows); | |
| assert(!debugNeedsLayout); | |
| return Rect.fromLTRB(0.0, _rowTops[row], size.width, _rowTops[row + 1]); | |
| } | |
| @override | |
| Size computeDryLayout(BoxConstraints constraints) { | |
| if (rows * columns == 0) { | |
| return constraints.constrain(Size.zero); | |
| } | |
| final List<double> widths = _computeColumnWidths(constraints); | |
| final double tableWidth = widths.fold(0.0, (double a, double b) => a + b); | |
| double rowTop = 0.0; | |
| for (int y = 0; y < rows; y += 1) { | |
| double rowHeight = 0.0; | |
| for (int x = 0; x < columns; x += 1) { | |
| final int xy = x + y * columns; | |
| final RenderBox? child = _children[xy]; | |
| if (child != null) { | |
| final TableCellParentData childParentData = child.parentData! as TableCellParentData; | |
| switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { | |
| case TableCellVerticalAlignment.baseline: | |
| assert(debugCannotComputeDryLayout( | |
| reason: 'TableCellVerticalAlignment.baseline requires a full layout for baseline metrics to be available.', | |
| )); | |
| return Size.zero; | |
| case TableCellVerticalAlignment.top: | |
| case TableCellVerticalAlignment.middle: | |
| case TableCellVerticalAlignment.bottom: | |
| final Size childSize = child.getDryLayout(BoxConstraints.tightFor(width: widths[x])); | |
| rowHeight = math.max(rowHeight, childSize.height); | |
| break; | |
| case TableCellVerticalAlignment.fill: | |
| break; | |
| } | |
| } | |
| } | |
| rowTop += rowHeight; | |
| } | |
| return constraints.constrain(Size(tableWidth, rowTop)); | |
| } | |
| @override | |
| void performLayout() { | |
| final BoxConstraints constraints = this.constraints; | |
| final int rows = this.rows; | |
| final int columns = this.columns; | |
| assert(_children.length == rows * columns); | |
| if (rows * columns == 0) { | |
| // TODO(ianh): if columns is zero, this should be zero width | |
| // TODO(ianh): if columns is not zero, this should be based on the column width specifications | |
| _tableWidth = 0.0; | |
| size = constraints.constrain(Size.zero); | |
| return; | |
| } | |
| final List<double> widths = _computeColumnWidths(constraints); | |
| final List<double> positions = List<double>.filled(columns, 0.0); | |
| switch (textDirection) { | |
| case TextDirection.rtl: | |
| positions[columns - 1] = 0.0; | |
| for (int x = columns - 2; x >= 0; x -= 1) { | |
| positions[x] = positions[x+1] + widths[x+1]; | |
| } | |
| _columnLefts = positions.reversed; | |
| _tableWidth = positions.first + widths.first; | |
| break; | |
| case TextDirection.ltr: | |
| positions[0] = 0.0; | |
| for (int x = 1; x < columns; x += 1) { | |
| positions[x] = positions[x-1] + widths[x-1]; | |
| } | |
| _columnLefts = positions; | |
| _tableWidth = positions.last + widths.last; | |
| break; | |
| } | |
| _rowTops.clear(); | |
| _baselineDistance = null; | |
| // then, lay out each row | |
| double rowTop = 0.0; | |
| for (int y = 0; y < rows; y += 1) { | |
| _rowTops.add(rowTop); | |
| double rowHeight = 0.0; | |
| bool haveBaseline = false; | |
| double beforeBaselineDistance = 0.0; | |
| double afterBaselineDistance = 0.0; | |
| final List<double> baselines = List<double>.filled(columns, 0.0); | |
| for (int x = 0; x < columns; x += 1) { | |
| final int xy = x + y * columns; | |
| final RenderBox? child = _children[xy]; | |
| if (child != null) { | |
| final TableCellParentData childParentData = child.parentData! as TableCellParentData; | |
| childParentData.x = x; | |
| childParentData.y = y; | |
| switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { | |
| case TableCellVerticalAlignment.baseline: | |
| assert(textBaseline != null, 'An explicit textBaseline is required when using baseline alignment.'); | |
| child.layout(BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true); | |
| final double? childBaseline = child.getDistanceToBaseline(textBaseline!, onlyReal: true); | |
| if (childBaseline != null) { | |
| beforeBaselineDistance = math.max(beforeBaselineDistance, childBaseline); | |
| afterBaselineDistance = math.max(afterBaselineDistance, child.size.height - childBaseline); | |
| baselines[x] = childBaseline; | |
| haveBaseline = true; | |
| } else { | |
| rowHeight = math.max(rowHeight, child.size.height); | |
| childParentData.offset = Offset(positions[x], rowTop); | |
| } | |
| break; | |
| case TableCellVerticalAlignment.top: | |
| case TableCellVerticalAlignment.middle: | |
| case TableCellVerticalAlignment.bottom: | |
| child.layout(BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true); | |
| rowHeight = math.max(rowHeight, child.size.height); | |
| break; | |
| case TableCellVerticalAlignment.fill: | |
| break; | |
| } | |
| } | |
| } | |
| if (haveBaseline) { | |
| if (y == 0) { | |
| _baselineDistance = beforeBaselineDistance; | |
| } | |
| rowHeight = math.max(rowHeight, beforeBaselineDistance + afterBaselineDistance); | |
| } | |
| for (int x = 0; x < columns; x += 1) { | |
| final int xy = x + y * columns; | |
| final RenderBox? child = _children[xy]; | |
| if (child != null) { | |
| final TableCellParentData childParentData = child.parentData! as TableCellParentData; | |
| switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { | |
| case TableCellVerticalAlignment.baseline: | |
| childParentData.offset = Offset(positions[x], rowTop + beforeBaselineDistance - baselines[x]); | |
| break; | |
| case TableCellVerticalAlignment.top: | |
| childParentData.offset = Offset(positions[x], rowTop); | |
| break; | |
| case TableCellVerticalAlignment.middle: | |
| childParentData.offset = Offset(positions[x], rowTop + (rowHeight - child.size.height) / 2.0); | |
| break; | |
| case TableCellVerticalAlignment.bottom: | |
| childParentData.offset = Offset(positions[x], rowTop + rowHeight - child.size.height); | |
| break; | |
| case TableCellVerticalAlignment.fill: | |
| child.layout(BoxConstraints.tightFor(width: widths[x], height: rowHeight)); | |
| childParentData.offset = Offset(positions[x], rowTop); | |
| break; | |
| } | |
| } | |
| } | |
| rowTop += rowHeight; | |
| } | |
| _rowTops.add(rowTop); | |
| size = constraints.constrain(Size(_tableWidth, rowTop)); | |
| assert(_rowTops.length == rows + 1); | |
| } | |
| @override | |
| bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { | |
| assert(_children.length == rows * columns); | |
| for (int index = _children.length - 1; index >= 0; index -= 1) { | |
| final RenderBox? child = _children[index]; | |
| if (child != null) { | |
| final BoxParentData childParentData = child.parentData! as BoxParentData; | |
| final bool isHit = result.addWithPaintOffset( | |
| offset: childParentData.offset, | |
| position: position, | |
| hitTest: (BoxHitTestResult result, Offset transformed) { | |
| assert(transformed == position - childParentData.offset); | |
| return child.hitTest(result, position: transformed); | |
| }, | |
| ); | |
| if (isHit) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| @override | |
| void paint(PaintingContext context, Offset offset) { | |
| assert(_children.length == rows * columns); | |
| if (rows * columns == 0) { | |
| if (border != null) { | |
| final Rect borderRect = Rect.fromLTWH(offset.dx, offset.dy, _tableWidth, 0.0); | |
| border!.paint(context.canvas, borderRect, rows: const <double>[], columns: const <double>[]); | |
| } | |
| return; | |
| } | |
| assert(_rowTops.length == rows + 1); | |
| if (_rowDecorations != null) { | |
| assert(_rowDecorations!.length == _rowDecorationPainters!.length); | |
| final Canvas canvas = context.canvas; | |
| for (int y = 0; y < rows; y += 1) { | |
| if (_rowDecorations!.length <= y) { | |
| break; | |
| } | |
| if (_rowDecorations![y] != null) { | |
| _rowDecorationPainters![y] ??= _rowDecorations![y]!.createBoxPainter(markNeedsPaint); | |
| _rowDecorationPainters![y]!.paint( | |
| canvas, | |
| Offset(offset.dx, offset.dy + _rowTops[y]), | |
| configuration.copyWith(size: Size(size.width, _rowTops[y+1] - _rowTops[y])), | |
| ); | |
| } | |
| } | |
| } | |
| for (int index = 0; index < _children.length; index += 1) { | |
| final RenderBox? child = _children[index]; | |
| if (child != null) { | |
| final BoxParentData childParentData = child.parentData! as BoxParentData; | |
| context.paintChild(child, childParentData.offset + offset); | |
| } | |
| } | |
| assert(_rows == _rowTops.length - 1); | |
| assert(_columns == _columnLefts!.length); | |
| if (border != null) { | |
| // The border rect might not fill the entire height of this render object | |
| // if the rows underflow. We always force the columns to fill the width of | |
| // the render object, which means the columns cannot underflow. | |
| final Rect borderRect = Rect.fromLTWH(offset.dx, offset.dy, _tableWidth, _rowTops.last); | |
| final Iterable<double> rows = _rowTops.getRange(1, _rowTops.length - 1); | |
| final Iterable<double> columns = _columnLefts!.skip(1); | |
| border!.paint(context.canvas, borderRect, rows: rows, columns: columns); | |
| } | |
| } | |
| @override | |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
| super.debugFillProperties(properties); | |
| properties.add(DiagnosticsProperty<TableBorder>('border', border, defaultValue: null)); | |
| properties.add(DiagnosticsProperty<Map<int, TableColumnWidth>>('specified column widths', _columnWidths, level: _columnWidths.isEmpty ? DiagnosticLevel.hidden : DiagnosticLevel.info)); | |
| properties.add(DiagnosticsProperty<TableColumnWidth>('default column width', defaultColumnWidth)); | |
| properties.add(MessageProperty('table size', '$columns\u00D7$rows')); | |
| properties.add(IterableProperty<String>('column offsets', _columnLefts?.map(debugFormatDouble), ifNull: 'unknown')); | |
| properties.add(IterableProperty<String>('row offsets', _rowTops.map(debugFormatDouble), ifNull: 'unknown')); | |
| } | |
| @override | |
| List<DiagnosticsNode> debugDescribeChildren() { | |
| if (_children.isEmpty) { | |
| return <DiagnosticsNode>[DiagnosticsNode.message('table is empty')]; | |
| } | |
| final List<DiagnosticsNode> children = <DiagnosticsNode>[]; | |
| for (int y = 0; y < rows; y += 1) { | |
| for (int x = 0; x < columns; x += 1) { | |
| final int xy = x + y * columns; | |
| final RenderBox? child = _children[xy]; | |
| final String name = 'child ($x, $y)'; | |
| if (child != null) { | |
| children.add(child.toDiagnosticsNode(name: name)); | |
| } else { | |
| children.add(DiagnosticsProperty<Object>(name, null, ifNull: 'is null', showSeparator: false)); | |
| } | |
| } | |
| } | |
| return children; | |
| } | |
| } |