| // 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. |
| 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. |
| 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) : assert(value != null); |
| |
| /// 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) : assert(value != null); |
| |
| /// 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]) : assert(value != null); |
| |
| /// The reaction of the 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. |
| /// * [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(1.0), |
| @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), |
| assert(defaultColumnWidth != null), |
| assert(textDirection != null), |
| assert(configuration != null), |
| _textDirection = textDirection { |
| _columns = columns ?? (children != null && children.isNotEmpty ? children.first.length : 0); |
| _rows = rows ?? 0; |
| _children = <RenderBox>[]..length = _columns * _rows; |
| _columnWidths = columnWidths ?? HashMap<int, TableColumnWidth>(); |
| _defaultColumnWidth = defaultColumnWidth; |
| _border = border; |
| this.rowDecorations = rowDecorations; // must use setter to initialize box painters array |
| _configuration = configuration; |
| _defaultVerticalAlignment = defaultVerticalAlignment; |
| _textBaseline = textBaseline; |
| 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 != null); |
| assert(value >= 0); |
| if (value == columns) |
| return; |
| final int oldColumns = columns; |
| final List<RenderBox> oldChildren = _children; |
| _columns = value; |
| _children = <RenderBox>[]..length = columns * rows; |
| 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 != null); |
| 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. |
| Map<int, TableColumnWidth> get columnWidths => Map<int, TableColumnWidth>.unmodifiable(_columnWidths); |
| Map<int, TableColumnWidth> _columnWidths; |
| set columnWidths(Map<int, TableColumnWidth> value) { |
| value ??= HashMap<int, TableColumnWidth>(); |
| if (_columnWidths == value) |
| return; |
| _columnWidths = value; |
| 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) { |
| assert(value != null); |
| 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) { |
| assert(value != null); |
| 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>(_rowDecorations.length) : 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) { |
| assert(value != null); |
| 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 simply 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 == null || 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 != null); |
| 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 = cells.toList(); |
| 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, null); |
| 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 != null); |
| assert(y != null); |
| 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>(_rowDecorations.length); |
| } |
| 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. |
| 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. |
| 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(constraints != null); |
| 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>(columns); |
| final List<double> minWidths = List<double>(columns); |
| final List<double> flexes = List<double>(columns); |
| 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 += maxIntrinsicWidth; |
| } |
| } |
| assert(!widths.any((double value) => value == null)); |
| 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. |
| 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] += delta; |
| tableWidth = minWidthConstraint; |
| } |
| |
| // beyond this point, unflexedTableWidth is no longer valid |
| assert(() { |
| unflexedTableWidth = null; |
| return true; |
| }()); |
| |
| // 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] -= delta; |
| newAvailableColumns += 1; |
| } |
| } |
| } |
| availableColumns = newAvailableColumns; |
| } |
| } |
| return widths; |
| } |
| |
| // cache the table geometry for painting purposes |
| final List<double> _rowTops = <double>[]; |
| Iterable<double> _columnLefts; |
| |
| /// 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 |
| void performLayout() { |
| 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 |
| size = constraints.constrain(const Size(0.0, 0.0)); |
| return; |
| } |
| final List<double> widths = _computeColumnWidths(constraints); |
| final List<double> positions = List<double>(columns); |
| double tableWidth; |
| 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; |
| } |
| assert(!positions.any((double value) => value == null)); |
| _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>(columns); |
| 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; |
| assert(childParentData != null); |
| childParentData.x = x; |
| childParentData.y = y; |
| switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { |
| case TableCellVerticalAlignment.baseline: |
| assert(textBaseline != null); |
| 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: |
| if (baselines[x] != null) |
| 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, { 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, size.width, 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, size.width, _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; |
| } |
| } |