1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:collection';
6
7import 'package:flutter/foundation.dart';
8import 'package:flutter/rendering.dart';
9
10import 'basic.dart';
11import 'debug.dart';
12import 'framework.dart';
13import 'image.dart';
14
15export 'package:flutter/rendering.dart' show
16 FixedColumnWidth,
17 FlexColumnWidth,
18 FractionColumnWidth,
19 IntrinsicColumnWidth,
20 MaxColumnWidth,
21 MinColumnWidth,
22 TableBorder,
23 TableCellVerticalAlignment,
24 TableColumnWidth;
25
26/// A horizontal group of cells in a [Table].
27///
28/// Every row in a table must have the same number of children.
29///
30/// The alignment of individual cells in a row can be controlled using a
31/// [TableCell].
32@immutable
33class TableRow {
34 /// Creates a row in a [Table].
35 const TableRow({ this.key, this.decoration, this.children = const <Widget>[]});
36
37 /// An identifier for the row.
38 final LocalKey? key;
39
40 /// A decoration to paint behind this row.
41 ///
42 /// Row decorations fill the horizontal and vertical extent of each row in
43 /// the table, unlike decorations for individual cells, which might not fill
44 /// either.
45 final Decoration? decoration;
46
47 /// The widgets that comprise the cells in this row.
48 ///
49 /// Children may be wrapped in [TableCell] widgets to provide per-cell
50 /// configuration to the [Table], but children are not required to be wrapped
51 /// in [TableCell] widgets.
52 final List<Widget> children;
53
54 @override
55 String toString() {
56 final StringBuffer result = StringBuffer();
57 result.write('TableRow(');
58 if (key != null) {
59 result.write('$key, ');
60 }
61 if (decoration != null) {
62 result.write('$decoration, ');
63 }
64 if (children.isEmpty) {
65 result.write('no children');
66 } else {
67 result.write('$children');
68 }
69 result.write(')');
70 return result.toString();
71 }
72}
73
74class _TableElementRow {
75 const _TableElementRow({ this.key, required this.children });
76 final LocalKey? key;
77 final List<Element> children;
78}
79
80/// A widget that uses the table layout algorithm for its children.
81///
82/// {@youtube 560 315 https://www.youtube.com/watch?v=_lbE0wsVZSw}
83///
84/// {@tool dartpad}
85/// This sample shows a [Table] with borders, multiple types of column widths
86/// and different vertical cell alignments.
87///
88/// ** See code in examples/api/lib/widgets/table/table.0.dart **
89/// {@end-tool}
90///
91/// If you only have one row, the [Row] widget is more appropriate. If you only
92/// have one column, the [SliverList] or [Column] widgets will be more
93/// appropriate.
94///
95/// Rows size vertically based on their contents. To control the individual
96/// column widths, use the [columnWidths] property to specify a
97/// [TableColumnWidth] for each column. If [columnWidths] is null, or there is a
98/// null entry for a given column in [columnWidths], the table uses the
99/// [defaultColumnWidth] instead.
100///
101/// By default, [defaultColumnWidth] is a [FlexColumnWidth]. This
102/// [TableColumnWidth] divides up the remaining space in the horizontal axis to
103/// determine the column width. If wrapping a [Table] in a horizontal
104/// [ScrollView], choose a different [TableColumnWidth], such as
105/// [FixedColumnWidth].
106///
107/// For more details about the table layout algorithm, see [RenderTable].
108/// To control the alignment of children, see [TableCell].
109///
110/// See also:
111///
112/// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/).
113class Table extends RenderObjectWidget {
114 /// Creates a table.
115 Table({
116 super.key,
117 this.children = const <TableRow>[],
118 this.columnWidths,
119 this.defaultColumnWidth = const FlexColumnWidth(),
120 this.textDirection,
121 this.border,
122 this.defaultVerticalAlignment = TableCellVerticalAlignment.top,
123 this.textBaseline, // NO DEFAULT: we don't know what the text's baseline should be
124 }) : assert(defaultVerticalAlignment != TableCellVerticalAlignment.baseline || textBaseline != null, 'textBaseline is required if you specify the defaultVerticalAlignment with TableCellVerticalAlignment.baseline'),
125 assert(() {
126 if (children.any((TableRow row1) => row1.key != null && children.any((TableRow row2) => row1 != row2 && row1.key == row2.key))) {
127 throw FlutterError(
128 'Two or more TableRow children of this Table had the same key.\n'
129 'All the keyed TableRow children of a Table must have different Keys.',
130 );
131 }
132 return true;
133 }()),
134 assert(() {
135 if (children.isNotEmpty) {
136 final int cellCount = children.first.children.length;
137 if (children.any((TableRow row) => row.children.length != cellCount)) {
138 throw FlutterError(
139 'Table contains irregular row lengths.\n'
140 'Every TableRow in a Table must have the same number of children, so that every cell is filled. '
141 'Otherwise, the table will contain holes.',
142 );
143 }
144 if (children.any((TableRow row) => row.children.isEmpty)) {
145 throw FlutterError(
146 'One or more TableRow have no children.\n'
147 'Every TableRow in a Table must have at least one child, so there is no empty row. ',
148 );
149 }
150 }
151 return true;
152 }()),
153 _rowDecorations = children.any((TableRow row) => row.decoration != null)
154 ? children.map<Decoration?>((TableRow row) => row.decoration).toList(growable: false)
155 : null {
156 assert(() {
157 final List<Widget> flatChildren = children.expand<Widget>((TableRow row) => row.children).toList(growable: false);
158 return !debugChildrenHaveDuplicateKeys(this, flatChildren, message:
159 'Two or more cells in this Table contain widgets with the same key.\n'
160 'Every widget child of every TableRow in a Table must have different keys. The cells of a Table are '
161 'flattened out for processing, so separate cells cannot have duplicate keys even if they are in '
162 'different rows.',
163 );
164 }());
165 }
166
167 /// The rows of the table.
168 ///
169 /// Every row in a table must have the same number of children.
170 final List<TableRow> children;
171
172 /// How the horizontal extents of the columns of this table should be determined.
173 ///
174 /// If the [Map] has a null entry for a given column, the table uses the
175 /// [defaultColumnWidth] instead. By default, that uses flex sizing to
176 /// distribute free space equally among the columns.
177 ///
178 /// The [FixedColumnWidth] class can be used to specify a specific width in
179 /// pixels. That is the cheapest way to size a table's columns.
180 ///
181 /// The layout performance of the table depends critically on which column
182 /// sizing algorithms are used here. In particular, [IntrinsicColumnWidth] is
183 /// quite expensive because it needs to measure each cell in the column to
184 /// determine the intrinsic size of the column.
185 ///
186 /// The keys of this map (column indexes) are zero-based.
187 ///
188 /// If this is set to null, then an empty map is assumed.
189 final Map<int, TableColumnWidth>? columnWidths;
190
191 /// How to determine with widths of columns that don't have an explicit sizing
192 /// algorithm.
193 ///
194 /// Specifically, the [defaultColumnWidth] is used for column `i` if
195 /// `columnWidths[i]` is null. Defaults to [FlexColumnWidth], which will
196 /// divide the remaining horizontal space up evenly between columns of the
197 /// same type [TableColumnWidth].
198 ///
199 /// A [Table] in a horizontal [ScrollView] must use a [FixedColumnWidth], or
200 /// an [IntrinsicColumnWidth] as the horizontal space is infinite.
201 final TableColumnWidth defaultColumnWidth;
202
203 /// The direction in which the columns are ordered.
204 ///
205 /// Defaults to the ambient [Directionality].
206 final TextDirection? textDirection;
207
208 /// The style to use when painting the boundary and interior divisions of the table.
209 final TableBorder? border;
210
211 /// How cells that do not explicitly specify a vertical alignment are aligned vertically.
212 ///
213 /// Cells may specify a vertical alignment by wrapping their contents in a
214 /// [TableCell] widget.
215 final TableCellVerticalAlignment defaultVerticalAlignment;
216
217 /// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline].
218 ///
219 /// This must be set if using baseline alignment. There is no default because there is no
220 /// way for the framework to know the correct baseline _a priori_.
221 final TextBaseline? textBaseline;
222
223 final List<Decoration?>? _rowDecorations;
224
225 @override
226 RenderObjectElement createElement() => _TableElement(this);
227
228 @override
229 RenderTable createRenderObject(BuildContext context) {
230 assert(debugCheckHasDirectionality(context));
231 return RenderTable(
232 columns: children.isNotEmpty ? children[0].children.length : 0,
233 rows: children.length,
234 columnWidths: columnWidths,
235 defaultColumnWidth: defaultColumnWidth,
236 textDirection: textDirection ?? Directionality.of(context),
237 border: border,
238 rowDecorations: _rowDecorations,
239 configuration: createLocalImageConfiguration(context),
240 defaultVerticalAlignment: defaultVerticalAlignment,
241 textBaseline: textBaseline,
242 );
243 }
244
245 @override
246 void updateRenderObject(BuildContext context, RenderTable renderObject) {
247 assert(debugCheckHasDirectionality(context));
248 assert(renderObject.columns == (children.isNotEmpty ? children[0].children.length : 0));
249 assert(renderObject.rows == children.length);
250 renderObject
251 ..columnWidths = columnWidths
252 ..defaultColumnWidth = defaultColumnWidth
253 ..textDirection = textDirection ?? Directionality.of(context)
254 ..border = border
255 ..rowDecorations = _rowDecorations
256 ..configuration = createLocalImageConfiguration(context)
257 ..defaultVerticalAlignment = defaultVerticalAlignment
258 ..textBaseline = textBaseline;
259 }
260}
261
262class _TableElement extends RenderObjectElement {
263 _TableElement(Table super.widget);
264
265 @override
266 RenderTable get renderObject => super.renderObject as RenderTable;
267
268 List<_TableElementRow> _children = const<_TableElementRow>[];
269
270 bool _doingMountOrUpdate = false;
271
272 @override
273 void mount(Element? parent, Object? newSlot) {
274 assert(!_doingMountOrUpdate);
275 _doingMountOrUpdate = true;
276 super.mount(parent, newSlot);
277 int rowIndex = -1;
278 _children = (widget as Table).children.map<_TableElementRow>((TableRow row) {
279 int columnIndex = 0;
280 rowIndex += 1;
281 return _TableElementRow(
282 key: row.key,
283 children: row.children.map<Element>((Widget child) {
284 return inflateWidget(child, _TableSlot(columnIndex++, rowIndex));
285 }).toList(growable: false),
286 );
287 }).toList(growable: false);
288 _updateRenderObjectChildren();
289 assert(_doingMountOrUpdate);
290 _doingMountOrUpdate = false;
291 }
292
293 @override
294 void insertRenderObjectChild(RenderBox child, _TableSlot slot) {
295 renderObject.setupParentData(child);
296 // Once [mount]/[update] are done, the children are getting set all at once
297 // in [_updateRenderObjectChildren].
298 if (!_doingMountOrUpdate) {
299 renderObject.setChild(slot.column, slot.row, child);
300 }
301 }
302
303 @override
304 void moveRenderObjectChild(RenderBox child, _TableSlot oldSlot, _TableSlot newSlot) {
305 assert(_doingMountOrUpdate);
306 // Child gets moved at the end of [update] in [_updateRenderObjectChildren].
307 }
308
309 @override
310 void removeRenderObjectChild(RenderBox child, _TableSlot slot) {
311 renderObject.setChild(slot.column, slot.row, null);
312 }
313
314 final Set<Element> _forgottenChildren = HashSet<Element>();
315
316 @override
317 void update(Table newWidget) {
318 assert(!_doingMountOrUpdate);
319 _doingMountOrUpdate = true;
320 final Map<LocalKey, List<Element>> oldKeyedRows = <LocalKey, List<Element>>{};
321 for (final _TableElementRow row in _children) {
322 if (row.key != null) {
323 oldKeyedRows[row.key!] = row.children;
324 }
325 }
326 final Iterator<_TableElementRow> oldUnkeyedRows = _children.where((_TableElementRow row) => row.key == null).iterator;
327 final List<_TableElementRow> newChildren = <_TableElementRow>[];
328 final Set<List<Element>> taken = <List<Element>>{};
329 for (int rowIndex = 0; rowIndex < newWidget.children.length; rowIndex++) {
330 final TableRow row = newWidget.children[rowIndex];
331 List<Element> oldChildren;
332 if (row.key != null && oldKeyedRows.containsKey(row.key)) {
333 oldChildren = oldKeyedRows[row.key]!;
334 taken.add(oldChildren);
335 } else if (row.key == null && oldUnkeyedRows.moveNext()) {
336 oldChildren = oldUnkeyedRows.current.children;
337 } else {
338 oldChildren = const <Element>[];
339 }
340 final List<_TableSlot> slots = List<_TableSlot>.generate(
341 row.children.length,
342 (int columnIndex) => _TableSlot(columnIndex, rowIndex),
343 );
344 newChildren.add(_TableElementRow(
345 key: row.key,
346 children: updateChildren(oldChildren, row.children, forgottenChildren: _forgottenChildren, slots: slots),
347 ));
348 }
349 while (oldUnkeyedRows.moveNext()) {
350 updateChildren(oldUnkeyedRows.current.children, const <Widget>[], forgottenChildren: _forgottenChildren);
351 }
352 for (final List<Element> oldChildren in oldKeyedRows.values.where((List<Element> list) => !taken.contains(list))) {
353 updateChildren(oldChildren, const <Widget>[], forgottenChildren: _forgottenChildren);
354 }
355
356 _children = newChildren;
357 _updateRenderObjectChildren();
358 _forgottenChildren.clear();
359 super.update(newWidget);
360 assert(widget == newWidget);
361 assert(_doingMountOrUpdate);
362 _doingMountOrUpdate = false;
363 }
364
365 void _updateRenderObjectChildren() {
366 renderObject.setFlatChildren(
367 _children.isNotEmpty ? _children[0].children.length : 0,
368 _children.expand<RenderBox>((_TableElementRow row) {
369 return row.children.map<RenderBox>((Element child) {
370 final RenderBox box = child.renderObject! as RenderBox;
371 return box;
372 });
373 }).toList(),
374 );
375 }
376
377 @override
378 void visitChildren(ElementVisitor visitor) {
379 for (final Element child in _children.expand<Element>((_TableElementRow row) => row.children)) {
380 if (!_forgottenChildren.contains(child)) {
381 visitor(child);
382 }
383 }
384 }
385
386 @override
387 bool forgetChild(Element child) {
388 _forgottenChildren.add(child);
389 super.forgetChild(child);
390 return true;
391 }
392}
393
394/// A widget that controls how a child of a [Table] is aligned.
395///
396/// A [TableCell] widget must be a descendant of a [Table], and the path from
397/// the [TableCell] widget to its enclosing [Table] must contain only
398/// [TableRow]s, [StatelessWidget]s, or [StatefulWidget]s (not
399/// other kinds of widgets, like [RenderObjectWidget]s).
400///
401/// To create an empty [TableCell], provide a [SizedBox.shrink]
402/// as the [child].
403class TableCell extends ParentDataWidget<TableCellParentData> {
404 /// Creates a widget that controls how a child of a [Table] is aligned.
405 const TableCell({
406 super.key,
407 this.verticalAlignment,
408 required super.child,
409 });
410
411 /// How this cell is aligned vertically.
412 final TableCellVerticalAlignment? verticalAlignment;
413
414 @override
415 void applyParentData(RenderObject renderObject) {
416 final TableCellParentData parentData = renderObject.parentData! as TableCellParentData;
417 if (parentData.verticalAlignment != verticalAlignment) {
418 parentData.verticalAlignment = verticalAlignment;
419 final RenderObject? targetParent = renderObject.parent;
420 if (targetParent is RenderObject) {
421 targetParent.markNeedsLayout();
422 }
423 }
424 }
425
426 @override
427 Type get debugTypicalAncestorWidgetClass => Table;
428
429 @override
430 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
431 super.debugFillProperties(properties);
432 properties.add(EnumProperty<TableCellVerticalAlignment>('verticalAlignment', verticalAlignment));
433 }
434}
435
436@immutable
437class _TableSlot with Diagnosticable {
438 const _TableSlot(this.column, this.row);
439
440 final int column;
441 final int row;
442
443 @override
444 bool operator ==(Object other) {
445 if (other.runtimeType != runtimeType) {
446 return false;
447 }
448 return other is _TableSlot
449 && column == other.column
450 && row == other.row;
451 }
452
453 @override
454 int get hashCode => Object.hash(column, row);
455
456 @override
457 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
458 super.debugFillProperties(properties);
459 properties.add(IntProperty('x', column));
460 properties.add(IntProperty('y', row));
461 }
462}
463