// 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 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'debug.dart'; import 'framework.dart'; import 'image.dart'; export 'package:flutter/rendering.dart' show FixedColumnWidth, FlexColumnWidth, FractionColumnWidth, IntrinsicColumnWidth, MaxColumnWidth, MinColumnWidth, TableBorder, TableCellVerticalAlignment, TableColumnWidth; /// A horizontal group of cells in a [Table]. /// /// Every row in a table must have the same number of children. /// /// The alignment of individual cells in a row can be controlled using a /// [TableCell]. @immutable class TableRow { /// Creates a row in a [Table]. const TableRow({ this.key, this.decoration, this.children }); /// An identifier for the row. final LocalKey? key; /// A decoration to paint behind this row. /// /// Row decorations fill the horizontal and vertical extent of each row in /// the table, unlike decorations for individual cells, which might not fill /// either. final Decoration? decoration; /// The widgets that comprise the cells in this row. /// /// Children may be wrapped in [TableCell] widgets to provide per-cell /// configuration to the [Table], but children are not required to be wrapped /// in [TableCell] widgets. final List<Widget>? children; @override String toString() { final StringBuffer result = StringBuffer(); result.write('TableRow('); if (key != null) { result.write('$key, '); } if (decoration != null) { result.write('$decoration, '); } if (children == null) { result.write('child list is null'); } else if (children!.isEmpty) { result.write('no children'); } else { result.write('$children'); } result.write(')'); return result.toString(); } } class _TableElementRow { const _TableElementRow({ this.key, required this.children }); final LocalKey? key; final List<Element> children; } /// A widget that uses the table layout algorithm for its children. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=_lbE0wsVZSw} /// /// {@tool dartpad} /// This sample shows a `Table` with borders, multiple types of column widths and different vertical cell alignments. /// /// ** See code in examples/api/lib/widgets/table/table.0.dart ** /// {@end-tool} /// /// If you only have one row, the [Row] widget is more appropriate. If you only /// have one column, the [SliverList] or [Column] widgets will be more /// appropriate. /// /// Rows size vertically based on their contents. To control the individual /// column widths, use the [columnWidths] property to specify a /// [TableColumnWidth] for each column. If [columnWidths] is null, or there is a /// null entry for a given column in [columnWidths], the table uses the /// [defaultColumnWidth] instead. /// /// By default, [defaultColumnWidth] is a [FlexColumnWidth]. This /// [TableColumnWidth] divides up the remaining space in the horizontal axis to /// determine the column width. If wrapping a [Table] in a horizontal /// [ScrollView], choose a different [TableColumnWidth], such as /// [FixedColumnWidth]. /// /// For more details about the table layout algorithm, see [RenderTable]. /// To control the alignment of children, see [TableCell]. /// /// See also: /// /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class Table extends RenderObjectWidget { /// Creates a table. /// /// The [children], [defaultColumnWidth], and [defaultVerticalAlignment] /// arguments must not be null. Table({ super.key, this.children = const <TableRow>[], this.columnWidths, this.defaultColumnWidth = const FlexColumnWidth(), this.textDirection, this.border, this.defaultVerticalAlignment = TableCellVerticalAlignment.top, this.textBaseline, // NO DEFAULT: we don't know what the text's baseline should be }) : assert(children != null), assert(defaultColumnWidth != null), assert(defaultVerticalAlignment != null), assert(defaultVerticalAlignment != TableCellVerticalAlignment.baseline || textBaseline != null, 'textBaseline is required if you specify the defaultVerticalAlignment with TableCellVerticalAlignment.baseline'), assert(() { if (children.any((TableRow row) => row.children == null)) { throw FlutterError( 'One of the rows of the table had null children.\n' 'The children property of TableRow must not be null.', ); } return true; }()), assert(() { if (children.any((TableRow row) => row.children!.any((Widget cell) => cell == null))) { throw FlutterError( 'One of the children of one of the rows of the table was null.\n' 'The children of a TableRow must not be null.', ); } return true; }()), assert(() { if (children.any((TableRow row1) => row1.key != null && children.any((TableRow row2) => row1 != row2 && row1.key == row2.key))) { throw FlutterError( 'Two or more TableRow children of this Table had the same key.\n' 'All the keyed TableRow children of a Table must have different Keys.', ); } return true; }()), assert(() { if (children.isNotEmpty) { final int cellCount = children.first.children!.length; if (children.any((TableRow row) => row.children!.length != cellCount)) { throw FlutterError( 'Table contains irregular row lengths.\n' 'Every TableRow in a Table must have the same number of children, so that every cell is filled. ' 'Otherwise, the table will contain holes.', ); } } return true; }()), _rowDecorations = children.any((TableRow row) => row.decoration != null) ? children.map<Decoration?>((TableRow row) => row.decoration).toList(growable: false) : null { assert(() { final List<Widget> flatChildren = children.expand<Widget>((TableRow row) => row.children!).toList(growable: false); if (debugChildrenHaveDuplicateKeys(this, flatChildren)) { throw FlutterError( 'Two or more cells in this Table contain widgets with the same key.\n' 'Every widget child of every TableRow in a Table must have different keys. The cells of a Table are ' 'flattened out for processing, so separate cells cannot have duplicate keys even if they are in ' 'different rows.', ); } return true; }()); } /// The rows of the table. /// /// Every row in a table must have the same number of children, and all the /// children must be non-null. final List<TableRow> children; /// 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. By default, that uses flex sizing to /// distribute free space equally among the columns. /// /// The [FixedColumnWidth] class can be used to specify a specific width in /// pixels. That is the cheapest way to size a table's columns. /// /// 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. /// /// The keys of this map (column indexes) are zero-based. /// /// If this is set to null, then an empty map is assumed. final Map<int, TableColumnWidth>? columnWidths; /// 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. Defaults to [FlexColumnWidth], which will /// divide the remaining horizontal space up evenly between columns of the /// same type [TableColumnWidth]. /// /// A [Table] in a horizontal [ScrollView] must use a [FixedColumnWidth], or /// an [IntrinsicColumnWidth] as the horizontal space is infinite. final TableColumnWidth defaultColumnWidth; /// The direction in which the columns are ordered. /// /// Defaults to the ambient [Directionality]. final TextDirection? textDirection; /// The style to use when painting the boundary and interior divisions of the table. final TableBorder? border; /// How cells that do not explicitly specify a vertical alignment are aligned vertically. /// /// Cells may specify a vertical alignment by wrapping their contents in a /// [TableCell] widget. final TableCellVerticalAlignment defaultVerticalAlignment; /// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline]. /// /// This must be set if using baseline alignment. There is no default because there is no /// way for the framework to know the correct baseline _a priori_. final TextBaseline? textBaseline; final List<Decoration?>? _rowDecorations; @override RenderObjectElement createElement() => _TableElement(this); @override RenderTable createRenderObject(BuildContext context) { assert(debugCheckHasDirectionality(context)); return RenderTable( columns: children.isNotEmpty ? children[0].children!.length : 0, rows: children.length, columnWidths: columnWidths, defaultColumnWidth: defaultColumnWidth, textDirection: textDirection ?? Directionality.of(context), border: border, rowDecorations: _rowDecorations, configuration: createLocalImageConfiguration(context), defaultVerticalAlignment: defaultVerticalAlignment, textBaseline: textBaseline, ); } @override void updateRenderObject(BuildContext context, RenderTable renderObject) { assert(debugCheckHasDirectionality(context)); assert(renderObject.columns == (children.isNotEmpty ? children[0].children!.length : 0)); assert(renderObject.rows == children.length); renderObject ..columnWidths = columnWidths ..defaultColumnWidth = defaultColumnWidth ..textDirection = textDirection ?? Directionality.of(context) ..border = border ..rowDecorations = _rowDecorations ..configuration = createLocalImageConfiguration(context) ..defaultVerticalAlignment = defaultVerticalAlignment ..textBaseline = textBaseline; } } class _TableElement extends RenderObjectElement { _TableElement(Table super.widget); @override RenderTable get renderObject => super.renderObject as RenderTable; List<_TableElementRow> _children = const<_TableElementRow>[]; bool _doingMountOrUpdate = false; @override void mount(Element? parent, Object? newSlot) { assert(!_doingMountOrUpdate); _doingMountOrUpdate = true; super.mount(parent, newSlot); int rowIndex = -1; _children = (widget as Table).children.map<_TableElementRow>((TableRow row) { int columnIndex = 0; rowIndex += 1; return _TableElementRow( key: row.key, children: row.children!.map<Element>((Widget child) { assert(child != null); return inflateWidget(child, _TableSlot(columnIndex++, rowIndex)); }).toList(growable: false), ); }).toList(growable: false); _updateRenderObjectChildren(); assert(_doingMountOrUpdate); _doingMountOrUpdate = false; } @override void insertRenderObjectChild(RenderBox child, _TableSlot slot) { renderObject.setupParentData(child); // Once [mount]/[update] are done, the children are getting set all at once // in [_updateRenderObjectChildren]. if (!_doingMountOrUpdate) { renderObject.setChild(slot.column, slot.row, child); } } @override void moveRenderObjectChild(RenderBox child, _TableSlot oldSlot, _TableSlot newSlot) { assert(_doingMountOrUpdate); // Child gets moved at the end of [update] in [_updateRenderObjectChildren]. } @override void removeRenderObjectChild(RenderBox child, _TableSlot slot) { renderObject.setChild(slot.column, slot.row, null); } final Set<Element> _forgottenChildren = HashSet<Element>(); @override void update(Table newWidget) { assert(!_doingMountOrUpdate); _doingMountOrUpdate = true; final Map<LocalKey, List<Element>> oldKeyedRows = <LocalKey, List<Element>>{}; for (final _TableElementRow row in _children) { if (row.key != null) { oldKeyedRows[row.key!] = row.children; } } final Iterator<_TableElementRow> oldUnkeyedRows = _children.where((_TableElementRow row) => row.key == null).iterator; final List<_TableElementRow> newChildren = <_TableElementRow>[]; final Set<List<Element>> taken = <List<Element>>{}; for (int rowIndex = 0; rowIndex < newWidget.children.length; rowIndex++) { final TableRow row = newWidget.children[rowIndex]; List<Element> oldChildren; if (row.key != null && oldKeyedRows.containsKey(row.key)) { oldChildren = oldKeyedRows[row.key]!; taken.add(oldChildren); } else if (row.key == null && oldUnkeyedRows.moveNext()) { oldChildren = oldUnkeyedRows.current.children; } else { oldChildren = const <Element>[]; } final List<_TableSlot> slots = List<_TableSlot>.generate( row.children!.length, (int columnIndex) => _TableSlot(columnIndex, rowIndex), ); newChildren.add(_TableElementRow( key: row.key, children: updateChildren(oldChildren, row.children!, forgottenChildren: _forgottenChildren, slots: slots), )); } while (oldUnkeyedRows.moveNext()) { updateChildren(oldUnkeyedRows.current.children, const <Widget>[], forgottenChildren: _forgottenChildren); } for (final List<Element> oldChildren in oldKeyedRows.values.where((List<Element> list) => !taken.contains(list))) { updateChildren(oldChildren, const <Widget>[], forgottenChildren: _forgottenChildren); } _children = newChildren; _updateRenderObjectChildren(); _forgottenChildren.clear(); super.update(newWidget); assert(widget == newWidget); assert(_doingMountOrUpdate); _doingMountOrUpdate = false; } void _updateRenderObjectChildren() { assert(renderObject != null); renderObject.setFlatChildren( _children.isNotEmpty ? _children[0].children.length : 0, _children.expand<RenderBox>((_TableElementRow row) { return row.children.map<RenderBox>((Element child) { final RenderBox box = child.renderObject! as RenderBox; return box; }); }).toList(), ); } @override void visitChildren(ElementVisitor visitor) { for (final Element child in _children.expand<Element>((_TableElementRow row) => row.children)) { if (!_forgottenChildren.contains(child)) { visitor(child); } } } @override bool forgetChild(Element child) { _forgottenChildren.add(child); super.forgetChild(child); return true; } } /// A widget that controls how a child of a [Table] is aligned. /// /// A [TableCell] widget must be a descendant of a [Table], and the path from /// the [TableCell] widget to its enclosing [Table] must contain only /// [TableRow]s, [StatelessWidget]s, or [StatefulWidget]s (not /// other kinds of widgets, like [RenderObjectWidget]s). class TableCell extends ParentDataWidget<TableCellParentData> { /// Creates a widget that controls how a child of a [Table] is aligned. const TableCell({ super.key, this.verticalAlignment, required super.child, }); /// How this cell is aligned vertically. final TableCellVerticalAlignment? verticalAlignment; @override void applyParentData(RenderObject renderObject) { final TableCellParentData parentData = renderObject.parentData! as TableCellParentData; if (parentData.verticalAlignment != verticalAlignment) { parentData.verticalAlignment = verticalAlignment; final AbstractNode? targetParent = renderObject.parent; if (targetParent is RenderObject) { targetParent.markNeedsLayout(); } } } @override Type get debugTypicalAncestorWidgetClass => Table; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(EnumProperty<TableCellVerticalAlignment>('verticalAlignment', verticalAlignment)); } } @immutable class _TableSlot with Diagnosticable { const _TableSlot(this.column, this.row); final int column; final int row; @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } return other is _TableSlot && column == other.column && row == other.row; } @override int get hashCode => Object.hash(column, row); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(IntProperty('x', column)); properties.add(IntProperty('y', row)); } }