1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
---|---|

2 | // Use of this source code is governed by a BSD-style license that can be |

3 | // found in the LICENSE file. |

4 | |

5 | import 'dart:collection'; |

6 | import 'dart:math' as math; |

7 | |

8 | import 'package:flutter/foundation.dart'; |

9 | |

10 | import 'box.dart'; |

11 | import 'object.dart'; |

12 | import 'table_border.dart'; |

13 | |

14 | /// Parent data used by [RenderTable] for its children. |

15 | class TableCellParentData extends BoxParentData { |

16 | /// Where this cell should be placed vertically. |

17 | /// |

18 | /// When using [TableCellVerticalAlignment.baseline], the text baseline must be set as well. |

19 | TableCellVerticalAlignment? verticalAlignment; |

20 | |

21 | /// The column that the child was in the last time it was laid out. |

22 | int? x; |

23 | |

24 | /// The row that the child was in the last time it was laid out. |

25 | int? y; |

26 | |

27 | @override |

28 | String toString() => '${ super.toString()};${verticalAlignment == null ? "default vertical alignment": "$verticalAlignment "} '; |

29 | } |

30 | |

31 | /// Base class to describe how wide a column in a [RenderTable] should be. |

32 | /// |

33 | /// To size a column to a specific number of pixels, use a [FixedColumnWidth]. |

34 | /// This is the cheapest way to size a column. |

35 | /// |

36 | /// Other algorithms that are relatively cheap include [FlexColumnWidth], which |

37 | /// distributes the space equally among the flexible columns, |

38 | /// [FractionColumnWidth], which sizes a column based on the size of the |

39 | /// table's container. |

40 | @immutable |

41 | abstract class TableColumnWidth { |

42 | /// Abstract const constructor. This constructor enables subclasses to provide |

43 | /// const constructors so that they can be used in const expressions. |

44 | const TableColumnWidth(); |

45 | |

46 | /// The smallest width that the column can have. |

47 | /// |

48 | /// The `cells` argument is an iterable that provides all the cells |

49 | /// in the table for this column. Walking the cells is by definition |

50 | /// O(N), so algorithms that do that should be considered expensive. |

51 | /// |

52 | /// The `containerWidth` argument is the `maxWidth` of the incoming |

53 | /// constraints for the table, and might be infinite. |

54 | double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth); |

55 | |

56 | /// The ideal width that the column should have. This must be equal |

57 | /// to or greater than the [minIntrinsicWidth]. The column might be |

58 | /// bigger than this width, e.g. if the column is flexible or if the |

59 | /// table's width ends up being forced to be bigger than the sum of |

60 | /// all the maxIntrinsicWidth values. |

61 | /// |

62 | /// The `cells` argument is an iterable that provides all the cells |

63 | /// in the table for this column. Walking the cells is by definition |

64 | /// O(N), so algorithms that do that should be considered expensive. |

65 | /// |

66 | /// The `containerWidth` argument is the `maxWidth` of the incoming |

67 | /// constraints for the table, and might be infinite. |

68 | double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth); |

69 | |

70 | /// The flex factor to apply to the cell if there is any room left |

71 | /// over when laying out the table. The remaining space is |

72 | /// distributed to any columns with flex in proportion to their flex |

73 | /// value (higher values get more space). |

74 | /// |

75 | /// The `cells` argument is an iterable that provides all the cells |

76 | /// in the table for this column. Walking the cells is by definition |

77 | /// O(N), so algorithms that do that should be considered expensive. |

78 | double? flex(Iterable<RenderBox> cells) => null; |

79 | |

80 | @override |

81 | String toString() => objectRuntimeType(this, 'TableColumnWidth'); |

82 | } |

83 | |

84 | /// Sizes the column according to the intrinsic dimensions of all the |

85 | /// cells in that column. |

86 | /// |

87 | /// This is a very expensive way to size a column. |

88 | /// |

89 | /// A flex value can be provided. If specified (and non-null), the |

90 | /// column will participate in the distribution of remaining space |

91 | /// once all the non-flexible columns have been sized. |

92 | class IntrinsicColumnWidth extends TableColumnWidth { |

93 | /// Creates a column width based on intrinsic sizing. |

94 | /// |

95 | /// This sizing algorithm is very expensive. |

96 | /// |

97 | /// The `flex` argument specifies the flex factor to apply to the column if |

98 | /// there is any room left over when laying out the table. If `flex` is |

99 | /// null (the default), the table will not distribute any extra space to the |

100 | /// column. |

101 | const IntrinsicColumnWidth({ double? flex }) : _flex = flex; |

102 | |

103 | @override |

104 | double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

105 | double result = 0.0; |

106 | for (final RenderBox cell in cells) { |

107 | result = math.max(result, cell.getMinIntrinsicWidth(double.infinity)); |

108 | } |

109 | return result; |

110 | } |

111 | |

112 | @override |

113 | double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

114 | double result = 0.0; |

115 | for (final RenderBox cell in cells) { |

116 | result = math.max(result, cell.getMaxIntrinsicWidth(double.infinity)); |

117 | } |

118 | return result; |

119 | } |

120 | |

121 | final double? _flex; |

122 | |

123 | @override |

124 | double? flex(Iterable<RenderBox> cells) => _flex; |

125 | |

126 | @override |

127 | String toString() => '${objectRuntimeType( this, 'IntrinsicColumnWidth')} (flex:${_flex?.toStringAsFixed( 1)})'; |

128 | } |

129 | |

130 | /// Sizes the column to a specific number of pixels. |

131 | /// |

132 | /// This is the cheapest way to size a column. |

133 | class FixedColumnWidth extends TableColumnWidth { |

134 | /// Creates a column width based on a fixed number of logical pixels. |

135 | const FixedColumnWidth(this.value); |

136 | |

137 | /// The width the column should occupy in logical pixels. |

138 | final double value; |

139 | |

140 | @override |

141 | double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

142 | return value; |

143 | } |

144 | |

145 | @override |

146 | double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

147 | return value; |

148 | } |

149 | |

150 | @override |

151 | String toString() => '${objectRuntimeType( this, 'FixedColumnWidth')} (${debugFormatDouble(value)} )'; |

152 | } |

153 | |

154 | /// Sizes the column to a fraction of the table's constraints' maxWidth. |

155 | /// |

156 | /// This is a cheap way to size a column. |

157 | class FractionColumnWidth extends TableColumnWidth { |

158 | /// Creates a column width based on a fraction of the table's constraints' |

159 | /// maxWidth. |

160 | const FractionColumnWidth(this.value); |

161 | |

162 | /// The fraction of the table's constraints' maxWidth that this column should |

163 | /// occupy. |

164 | final double value; |

165 | |

166 | @override |

167 | double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

168 | if (!containerWidth.isFinite) { |

169 | return 0.0; |

170 | } |

171 | return value * containerWidth; |

172 | } |

173 | |

174 | @override |

175 | double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

176 | if (!containerWidth.isFinite) { |

177 | return 0.0; |

178 | } |

179 | return value * containerWidth; |

180 | } |

181 | |

182 | @override |

183 | String toString() => '${objectRuntimeType( this, 'FractionColumnWidth')} ($value )'; |

184 | } |

185 | |

186 | /// Sizes the column by taking a part of the remaining space once all |

187 | /// the other columns have been laid out. |

188 | /// |

189 | /// For example, if two columns have a [FlexColumnWidth], then half the |

190 | /// space will go to one and half the space will go to the other. |

191 | /// |

192 | /// This is a cheap way to size a column. |

193 | class FlexColumnWidth extends TableColumnWidth { |

194 | /// Creates a column width based on a fraction of the remaining space once all |

195 | /// the other columns have been laid out. |

196 | const FlexColumnWidth([this.value = 1.0]); |

197 | |

198 | /// The fraction of the remaining space once all the other columns have |

199 | /// been laid out that this column should occupy. |

200 | final double value; |

201 | |

202 | @override |

203 | double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

204 | return 0.0; |

205 | } |

206 | |

207 | @override |

208 | double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

209 | return 0.0; |

210 | } |

211 | |

212 | @override |

213 | double flex(Iterable<RenderBox> cells) { |

214 | return value; |

215 | } |

216 | |

217 | @override |

218 | String toString() => '${objectRuntimeType( this, 'FlexColumnWidth')} (${debugFormatDouble(value)} )'; |

219 | } |

220 | |

221 | /// Sizes the column such that it is the size that is the maximum of |

222 | /// two column width specifications. |

223 | /// |

224 | /// For example, to have a column be 10% of the container width or |

225 | /// 100px, whichever is bigger, you could use: |

226 | /// |

227 | /// const MaxColumnWidth(const FixedColumnWidth(100.0), FractionColumnWidth(0.1)) |

228 | /// |

229 | /// Both specifications are evaluated, so if either specification is |

230 | /// expensive, so is this. |

231 | class MaxColumnWidth extends TableColumnWidth { |

232 | /// Creates a column width that is the maximum of two other column widths. |

233 | const MaxColumnWidth(this.a, this.b); |

234 | |

235 | /// A lower bound for the width of this column. |

236 | final TableColumnWidth a; |

237 | |

238 | /// Another lower bound for the width of this column. |

239 | final TableColumnWidth b; |

240 | |

241 | @override |

242 | double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

243 | return math.max( |

244 | a.minIntrinsicWidth(cells, containerWidth), |

245 | b.minIntrinsicWidth(cells, containerWidth), |

246 | ); |

247 | } |

248 | |

249 | @override |

250 | double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

251 | return math.max( |

252 | a.maxIntrinsicWidth(cells, containerWidth), |

253 | b.maxIntrinsicWidth(cells, containerWidth), |

254 | ); |

255 | } |

256 | |

257 | @override |

258 | double? flex(Iterable<RenderBox> cells) { |

259 | final double? aFlex = a.flex(cells); |

260 | final double? bFlex = b.flex(cells); |

261 | if (aFlex == null) { |

262 | return bFlex; |

263 | } else if (bFlex == null) { |

264 | return aFlex; |

265 | } |

266 | return math.max(aFlex, bFlex); |

267 | } |

268 | |

269 | @override |

270 | String toString() => '${objectRuntimeType( this, 'MaxColumnWidth')} ($a ,$b )'; |

271 | } |

272 | |

273 | /// Sizes the column such that it is the size that is the minimum of |

274 | /// two column width specifications. |

275 | /// |

276 | /// For example, to have a column be 10% of the container width but |

277 | /// never bigger than 100px, you could use: |

278 | /// |

279 | /// const MinColumnWidth(const FixedColumnWidth(100.0), FractionColumnWidth(0.1)) |

280 | /// |

281 | /// Both specifications are evaluated, so if either specification is |

282 | /// expensive, so is this. |

283 | class MinColumnWidth extends TableColumnWidth { |

284 | /// Creates a column width that is the minimum of two other column widths. |

285 | const MinColumnWidth(this.a, this.b); |

286 | |

287 | /// An upper bound for the width of this column. |

288 | final TableColumnWidth a; |

289 | |

290 | /// Another upper bound for the width of this column. |

291 | final TableColumnWidth b; |

292 | |

293 | @override |

294 | double minIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

295 | return math.min( |

296 | a.minIntrinsicWidth(cells, containerWidth), |

297 | b.minIntrinsicWidth(cells, containerWidth), |

298 | ); |

299 | } |

300 | |

301 | @override |

302 | double maxIntrinsicWidth(Iterable<RenderBox> cells, double containerWidth) { |

303 | return math.min( |

304 | a.maxIntrinsicWidth(cells, containerWidth), |

305 | b.maxIntrinsicWidth(cells, containerWidth), |

306 | ); |

307 | } |

308 | |

309 | @override |

310 | double? flex(Iterable<RenderBox> cells) { |

311 | final double? aFlex = a.flex(cells); |

312 | final double? bFlex = b.flex(cells); |

313 | if (aFlex == null) { |

314 | return bFlex; |

315 | } else if (bFlex == null) { |

316 | return aFlex; |

317 | } |

318 | return math.min(aFlex, bFlex); |

319 | } |

320 | |

321 | @override |

322 | String toString() => '${objectRuntimeType( this, 'MinColumnWidth')} ($a ,$b )'; |

323 | } |

324 | |

325 | /// Vertical alignment options for cells in [RenderTable] objects. |

326 | /// |

327 | /// This is specified using [TableCellParentData] objects on the |

328 | /// [RenderObject.parentData] of the children of the [RenderTable]. |

329 | enum TableCellVerticalAlignment { |

330 | /// Cells with this alignment are placed with their top at the top of the row. |

331 | top, |

332 | |

333 | /// Cells with this alignment are vertically centered in the row. |

334 | middle, |

335 | |

336 | /// Cells with this alignment are placed with their bottom at the bottom of the row. |

337 | bottom, |

338 | |

339 | /// Cells with this alignment are aligned such that they all share the same |

340 | /// baseline. Cells with no baseline are top-aligned instead. The baseline |

341 | /// used is specified by [RenderTable.textBaseline]. It is not valid to use |

342 | /// the baseline value if [RenderTable.textBaseline] is not specified. |

343 | /// |

344 | /// This vertical alignment is relatively expensive because it causes the table |

345 | /// to compute the baseline for each cell in the row. |

346 | baseline, |

347 | |

348 | /// Cells with this alignment are sized to be as tall as the row, then made to fit the row. |

349 | /// If all the cells have this alignment, then the row will have zero height. |

350 | fill, |

351 | |

352 | /// Cells with this alignment are sized to be the same height as the tallest cell in the row. |

353 | intrinsicHeight |

354 | } |

355 | |

356 | /// A table where the columns and rows are sized to fit the contents of the cells. |

357 | class RenderTable extends RenderBox { |

358 | /// Creates a table render object. |

359 | /// |

360 | /// * `columns` must either be null or non-negative. If `columns` is null, |

361 | /// the number of columns will be inferred from length of the first sublist |

362 | /// of `children`. |

363 | /// * `rows` must either be null or non-negative. If `rows` is null, the |

364 | /// number of rows will be inferred from the `children`. If `rows` is not |

365 | /// null, then `children` must be null. |

366 | /// * `children` must either be null or contain lists of all the same length. |

367 | /// if `children` is not null, then `rows` must be null. |

368 | /// * [columnWidths] may be null, in which case it defaults to an empty map. |

369 | RenderTable({ |

370 | int? columns, |

371 | int? rows, |

372 | Map<int, TableColumnWidth>? columnWidths, |

373 | TableColumnWidth defaultColumnWidth = const FlexColumnWidth(), |

374 | required TextDirection textDirection, |

375 | TableBorder? border, |

376 | List<Decoration?>? rowDecorations, |

377 | ImageConfiguration configuration = ImageConfiguration.empty, |

378 | TableCellVerticalAlignment defaultVerticalAlignment = TableCellVerticalAlignment.top, |

379 | TextBaseline? textBaseline, |

380 | List<List<RenderBox>>? children, |

381 | }) : assert(columns == null || columns >= 0), |

382 | assert(rows == null || rows >= 0), |

383 | assert(rows == null || children == null), |

384 | _textDirection = textDirection, |

385 | _columns = columns ?? (children != null && children.isNotEmpty ? children.first.length : 0), |

386 | _rows = rows ?? 0, |

387 | _columnWidths = columnWidths ?? HashMap<int, TableColumnWidth>(), |

388 | _defaultColumnWidth = defaultColumnWidth, |

389 | _border = border, |

390 | _textBaseline = textBaseline, |

391 | _defaultVerticalAlignment = defaultVerticalAlignment, |

392 | _configuration = configuration { |

393 | _children = <RenderBox?>[]..length = _columns * _rows; |

394 | this.rowDecorations = rowDecorations; // must use setter to initialize box painters array |

395 | children?.forEach(addRow); |

396 | } |

397 | |

398 | // Children are stored in row-major order. |

399 | // _children.length must be rows * columns |

400 | List<RenderBox?> _children = const <RenderBox?>[]; |

401 | |

402 | /// The number of vertical alignment lines in this table. |

403 | /// |

404 | /// Changing the number of columns will remove any children that no longer fit |

405 | /// in the table. |

406 | /// |

407 | /// Changing the number of columns is an expensive operation because the table |

408 | /// needs to rearrange its internal representation. |

409 | int get columns => _columns; |

410 | int _columns; |

411 | set columns(int value) { |

412 | assert(value >= 0); |

413 | if (value == columns) { |

414 | return; |

415 | } |

416 | final int oldColumns = columns; |

417 | final List<RenderBox?> oldChildren = _children; |

418 | _columns = value; |

419 | _children = List<RenderBox?>.filled(columns * rows, null); |

420 | final int columnsToCopy = math.min(columns, oldColumns); |

421 | for (int y = 0; y < rows; y += 1) { |

422 | for (int x = 0; x < columnsToCopy; x += 1) { |

423 | _children[x + y * columns] = oldChildren[x + y * oldColumns]; |

424 | } |

425 | } |

426 | if (oldColumns > columns) { |

427 | for (int y = 0; y < rows; y += 1) { |

428 | for (int x = columns; x < oldColumns; x += 1) { |

429 | final int xy = x + y * oldColumns; |

430 | if (oldChildren[xy] != null) { |

431 | dropChild(oldChildren[xy]!); |

432 | } |

433 | } |

434 | } |

435 | } |

436 | markNeedsLayout(); |

437 | } |

438 | |

439 | /// The number of horizontal alignment lines in this table. |

440 | /// |

441 | /// Changing the number of rows will remove any children that no longer fit |

442 | /// in the table. |

443 | int get rows => _rows; |

444 | int _rows; |

445 | set rows(int value) { |

446 | assert(value >= 0); |

447 | if (value == rows) { |

448 | return; |

449 | } |

450 | if (_rows > value) { |

451 | for (int xy = columns * value; xy < _children.length; xy += 1) { |

452 | if (_children[xy] != null) { |

453 | dropChild(_children[xy]!); |

454 | } |

455 | } |

456 | } |

457 | _rows = value; |

458 | _children.length = columns * rows; |

459 | markNeedsLayout(); |

460 | } |

461 | |

462 | /// How the horizontal extents of the columns of this table should be determined. |

463 | /// |

464 | /// If the [Map] has a null entry for a given column, the table uses the |

465 | /// [defaultColumnWidth] instead. |

466 | /// |

467 | /// The layout performance of the table depends critically on which column |

468 | /// sizing algorithms are used here. In particular, [IntrinsicColumnWidth] is |

469 | /// quite expensive because it needs to measure each cell in the column to |

470 | /// determine the intrinsic size of the column. |

471 | /// |

472 | /// This property can never return null. If it is set to null, and the existing |

473 | /// map is not empty, then the value is replaced by an empty map. (If it is set |

474 | /// to null while the current value is an empty map, the value is not changed.) |

475 | Map<int, TableColumnWidth>? get columnWidths => Map<int, TableColumnWidth>.unmodifiable(_columnWidths); |

476 | Map<int, TableColumnWidth> _columnWidths; |

477 | set columnWidths(Map<int, TableColumnWidth>? value) { |

478 | if (_columnWidths == value) { |

479 | return; |

480 | } |

481 | if (_columnWidths.isEmpty && value == null) { |

482 | return; |

483 | } |

484 | _columnWidths = value ?? HashMap<int, TableColumnWidth>(); |

485 | markNeedsLayout(); |

486 | } |

487 | |

488 | /// Determines how the width of column with the given index is determined. |

489 | void setColumnWidth(int column, TableColumnWidth value) { |

490 | if (_columnWidths[column] == value) { |

491 | return; |

492 | } |

493 | _columnWidths[column] = value; |

494 | markNeedsLayout(); |

495 | } |

496 | |

497 | /// How to determine with widths of columns that don't have an explicit sizing algorithm. |

498 | /// |

499 | /// Specifically, the [defaultColumnWidth] is used for column `i` if |

500 | /// `columnWidths[i]` is null. |

501 | TableColumnWidth get defaultColumnWidth => _defaultColumnWidth; |

502 | TableColumnWidth _defaultColumnWidth; |

503 | set defaultColumnWidth(TableColumnWidth value) { |

504 | if (defaultColumnWidth == value) { |

505 | return; |

506 | } |

507 | _defaultColumnWidth = value; |

508 | markNeedsLayout(); |

509 | } |

510 | |

511 | /// The direction in which the columns are ordered. |

512 | TextDirection get textDirection => _textDirection; |

513 | TextDirection _textDirection; |

514 | set textDirection(TextDirection value) { |

515 | if (_textDirection == value) { |

516 | return; |

517 | } |

518 | _textDirection = value; |

519 | markNeedsLayout(); |

520 | } |

521 | |

522 | /// The style to use when painting the boundary and interior divisions of the table. |

523 | TableBorder? get border => _border; |

524 | TableBorder? _border; |

525 | set border(TableBorder? value) { |

526 | if (border == value) { |

527 | return; |

528 | } |

529 | _border = value; |

530 | markNeedsPaint(); |

531 | } |

532 | |

533 | /// The decorations to use for each row of the table. |

534 | /// |

535 | /// Row decorations fill the horizontal and vertical extent of each row in |

536 | /// the table, unlike decorations for individual cells, which might not fill |

537 | /// either. |

538 | List<Decoration> get rowDecorations => List<Decoration>.unmodifiable(_rowDecorations ?? const <Decoration>[]); |

539 | // _rowDecorations and _rowDecorationPainters need to be in sync. They have to |

540 | // either both be null or have same length. |

541 | List<Decoration?>? _rowDecorations; |

542 | List<BoxPainter?>? _rowDecorationPainters; |

543 | set rowDecorations(List<Decoration?>? value) { |

544 | if (_rowDecorations == value) { |

545 | return; |

546 | } |

547 | _rowDecorations = value; |

548 | if (_rowDecorationPainters != null) { |

549 | for (final BoxPainter? painter in _rowDecorationPainters!) { |

550 | painter?.dispose(); |

551 | } |

552 | } |

553 | _rowDecorationPainters = _rowDecorations != null ? List<BoxPainter?>.filled(_rowDecorations!.length, null) : null; |

554 | } |

555 | |

556 | /// The settings to pass to the [rowDecorations] when painting, so that they |

557 | /// can resolve images appropriately. See [ImageProvider.resolve] and |

558 | /// [BoxPainter.paint]. |

559 | ImageConfiguration get configuration => _configuration; |

560 | ImageConfiguration _configuration; |

561 | set configuration(ImageConfiguration value) { |

562 | if (value == _configuration) { |

563 | return; |

564 | } |

565 | _configuration = value; |

566 | markNeedsPaint(); |

567 | } |

568 | |

569 | /// How cells that do not explicitly specify a vertical alignment are aligned vertically. |

570 | TableCellVerticalAlignment get defaultVerticalAlignment => _defaultVerticalAlignment; |

571 | TableCellVerticalAlignment _defaultVerticalAlignment; |

572 | set defaultVerticalAlignment(TableCellVerticalAlignment value) { |

573 | if (_defaultVerticalAlignment == value) { |

574 | return; |

575 | } |

576 | _defaultVerticalAlignment = value; |

577 | markNeedsLayout(); |

578 | } |

579 | |

580 | /// The text baseline to use when aligning rows using [TableCellVerticalAlignment.baseline]. |

581 | TextBaseline? get textBaseline => _textBaseline; |

582 | TextBaseline? _textBaseline; |

583 | set textBaseline(TextBaseline? value) { |

584 | if (_textBaseline == value) { |

585 | return; |

586 | } |

587 | _textBaseline = value; |

588 | markNeedsLayout(); |

589 | } |

590 | |

591 | @override |

592 | void setupParentData(RenderObject child) { |

593 | if (child.parentData is! TableCellParentData) { |

594 | child.parentData = TableCellParentData(); |

595 | } |

596 | } |

597 | |

598 | /// Replaces the children of this table with the given cells. |

599 | /// |

600 | /// The cells are divided into the specified number of columns before |

601 | /// replacing the existing children. |

602 | /// |

603 | /// If the new cells contain any existing children of the table, those |

604 | /// children are moved to their new location in the table rather than |

605 | /// removed from the table and re-added. |

606 | void setFlatChildren(int columns, List<RenderBox?> cells) { |

607 | if (cells == _children && columns == _columns) { |

608 | return; |

609 | } |

610 | assert(columns >= 0); |

611 | // consider the case of a newly empty table |

612 | if (columns == 0 || cells.isEmpty) { |

613 | assert(cells.isEmpty); |

614 | _columns = columns; |

615 | if (_children.isEmpty) { |

616 | assert(_rows == 0); |

617 | return; |

618 | } |

619 | for (final RenderBox? oldChild in _children) { |

620 | if (oldChild != null) { |

621 | dropChild(oldChild); |

622 | } |

623 | } |

624 | _rows = 0; |

625 | _children.clear(); |

626 | markNeedsLayout(); |

627 | return; |

628 | } |

629 | assert(cells.length % columns == 0); |

630 | // fill a set with the cells that are moving (it's important not |

631 | // to dropChild a child that's remaining with us, because that |

632 | // would clear their parentData field) |

633 | final Set<RenderBox> lostChildren = HashSet<RenderBox>(); |

634 | for (int y = 0; y < _rows; y += 1) { |

635 | for (int x = 0; x < _columns; x += 1) { |

636 | final int xyOld = x + y * _columns; |

637 | final int xyNew = x + y * columns; |

638 | if (_children[xyOld] != null && (x >= columns || xyNew >= cells.length || _children[xyOld] != cells[xyNew])) { |

639 | lostChildren.add(_children[xyOld]!); |

640 | } |

641 | } |

642 | } |

643 | // adopt cells that are arriving, and cross cells that are just moving off our list of lostChildren |

644 | int y = 0; |

645 | while (y * columns < cells.length) { |

646 | for (int x = 0; x < columns; x += 1) { |

647 | final int xyNew = x + y * columns; |

648 | final int xyOld = x + y * _columns; |

649 | if (cells[xyNew] != null && (x >= _columns || y >= _rows || _children[xyOld] != cells[xyNew])) { |

650 | if (!lostChildren.remove(cells[xyNew])) { |

651 | adoptChild(cells[xyNew]!); |

652 | } |

653 | } |

654 | } |

655 | y += 1; |

656 | } |

657 | // drop all the lost children |

658 | lostChildren.forEach(dropChild); |

659 | // update our internal values |

660 | _columns = columns; |

661 | _rows = cells.length ~/ columns; |

662 | _children = List<RenderBox?>.of(cells); |

663 | assert(_children.length == rows * columns); |

664 | markNeedsLayout(); |

665 | } |

666 | |

667 | /// Replaces the children of this table with the given cells. |

668 | void setChildren(List<List<RenderBox>>? cells) { |

669 | // TODO(ianh): Make this smarter, like setFlatChildren |

670 | if (cells == null) { |

671 | setFlatChildren(0, const <RenderBox?>[]); |

672 | return; |

673 | } |

674 | for (final RenderBox? oldChild in _children) { |

675 | if (oldChild != null) { |

676 | dropChild(oldChild); |

677 | } |

678 | } |

679 | _children.clear(); |

680 | _columns = cells.isNotEmpty ? cells.first.length : 0; |

681 | _rows = 0; |

682 | cells.forEach(addRow); |

683 | assert(_children.length == rows * columns); |

684 | } |

685 | |

686 | /// Adds a row to the end of the table. |

687 | /// |

688 | /// The newly added children must not already have parents. |

689 | void addRow(List<RenderBox?> cells) { |

690 | assert(cells.length == columns); |

691 | assert(_children.length == rows * columns); |

692 | _rows += 1; |

693 | _children.addAll(cells); |

694 | for (final RenderBox? cell in cells) { |

695 | if (cell != null) { |

696 | adoptChild(cell); |

697 | } |

698 | } |

699 | markNeedsLayout(); |

700 | } |

701 | |

702 | /// Replaces the child at the given position with the given child. |

703 | /// |

704 | /// If the given child is already located at the given position, this function |

705 | /// does not modify the table. Otherwise, the given child must not already |

706 | /// have a parent. |

707 | void setChild(int x, int y, RenderBox? value) { |

708 | assert(x >= 0 && x < columns && y >= 0 && y < rows); |

709 | assert(_children.length == rows * columns); |

710 | final int xy = x + y * columns; |

711 | final RenderBox? oldChild = _children[xy]; |

712 | if (oldChild == value) { |

713 | return; |

714 | } |

715 | if (oldChild != null) { |

716 | dropChild(oldChild); |

717 | } |

718 | _children[xy] = value; |

719 | if (value != null) { |

720 | adoptChild(value); |

721 | } |

722 | } |

723 | |

724 | @override |

725 | void attach(PipelineOwner owner) { |

726 | super.attach(owner); |

727 | for (final RenderBox? child in _children) { |

728 | child?.attach(owner); |

729 | } |

730 | } |

731 | |

732 | @override |

733 | void detach() { |

734 | super.detach(); |

735 | if (_rowDecorationPainters != null) { |

736 | for (final BoxPainter? painter in _rowDecorationPainters!) { |

737 | painter?.dispose(); |

738 | } |

739 | _rowDecorationPainters = List<BoxPainter?>.filled(_rowDecorations!.length, null); |

740 | } |

741 | for (final RenderBox? child in _children) { |

742 | child?.detach(); |

743 | } |

744 | } |

745 | |

746 | @override |

747 | void visitChildren(RenderObjectVisitor visitor) { |

748 | assert(_children.length == rows * columns); |

749 | for (final RenderBox? child in _children) { |

750 | if (child != null) { |

751 | visitor(child); |

752 | } |

753 | } |

754 | } |

755 | |

756 | @override |

757 | double computeMinIntrinsicWidth(double height) { |

758 | assert(_children.length == rows * columns); |

759 | double totalMinWidth = 0.0; |

760 | for (int x = 0; x < columns; x += 1) { |

761 | final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth; |

762 | final Iterable<RenderBox> columnCells = column(x); |

763 | totalMinWidth += columnWidth.minIntrinsicWidth(columnCells, double.infinity); |

764 | } |

765 | return totalMinWidth; |

766 | } |

767 | |

768 | @override |

769 | double computeMaxIntrinsicWidth(double height) { |

770 | assert(_children.length == rows * columns); |

771 | double totalMaxWidth = 0.0; |

772 | for (int x = 0; x < columns; x += 1) { |

773 | final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth; |

774 | final Iterable<RenderBox> columnCells = column(x); |

775 | totalMaxWidth += columnWidth.maxIntrinsicWidth(columnCells, double.infinity); |

776 | } |

777 | return totalMaxWidth; |

778 | } |

779 | |

780 | @override |

781 | double computeMinIntrinsicHeight(double width) { |

782 | // winner of the 2016 world's most expensive intrinsic dimension function award |

783 | // honorable mention, most likely to improve if taught about memoization award |

784 | assert(_children.length == rows * columns); |

785 | final List<double> widths = _computeColumnWidths(BoxConstraints.tightForFinite(width: width)); |

786 | double rowTop = 0.0; |

787 | for (int y = 0; y < rows; y += 1) { |

788 | double rowHeight = 0.0; |

789 | for (int x = 0; x < columns; x += 1) { |

790 | final int xy = x + y * columns; |

791 | final RenderBox? child = _children[xy]; |

792 | if (child != null) { |

793 | rowHeight = math.max(rowHeight, child.getMaxIntrinsicHeight(widths[x])); |

794 | } |

795 | } |

796 | rowTop += rowHeight; |

797 | } |

798 | return rowTop; |

799 | } |

800 | |

801 | @override |

802 | double computeMaxIntrinsicHeight(double width) { |

803 | return computeMinIntrinsicHeight(width); |

804 | } |

805 | |

806 | double? _baselineDistance; |

807 | @override |

808 | double? computeDistanceToActualBaseline(TextBaseline baseline) { |

809 | // returns the baseline of the first cell that has a baseline in the first row |

810 | assert(!debugNeedsLayout); |

811 | return _baselineDistance; |

812 | } |

813 | |

814 | /// Returns the list of [RenderBox] objects that are in the given |

815 | /// column, in row order, starting from the first row. |

816 | /// |

817 | /// This is a lazily-evaluated iterable. |

818 | // The following uses sync* because it is public API documented to return a |

819 | // lazy iterable. |

820 | Iterable<RenderBox> column(int x) sync* { |

821 | for (int y = 0; y < rows; y += 1) { |

822 | final int xy = x + y * columns; |

823 | final RenderBox? child = _children[xy]; |

824 | if (child != null) { |

825 | yield child; |

826 | } |

827 | } |

828 | } |

829 | |

830 | /// Returns the list of [RenderBox] objects that are on the given |

831 | /// row, in column order, starting with the first column. |

832 | /// |

833 | /// This is a lazily-evaluated iterable. |

834 | // The following uses sync* because it is public API documented to return a |

835 | // lazy iterable. |

836 | Iterable<RenderBox> row(int y) sync* { |

837 | final int start = y * columns; |

838 | final int end = (y + 1) * columns; |

839 | for (int xy = start; xy < end; xy += 1) { |

840 | final RenderBox? child = _children[xy]; |

841 | if (child != null) { |

842 | yield child; |

843 | } |

844 | } |

845 | } |

846 | |

847 | List<double> _computeColumnWidths(BoxConstraints constraints) { |

848 | assert(_children.length == rows * columns); |

849 | // We apply the constraints to the column widths in the order of |

850 | // least important to most important: |

851 | // 1. apply the ideal widths (maxIntrinsicWidth) |

852 | // 2. grow the flex columns so that the table has the maxWidth (if |

853 | // finite) or the minWidth (if not) |

854 | // 3. if there were no flex columns, then grow the table to the |

855 | // minWidth. |

856 | // 4. apply the maximum width of the table, shrinking columns as |

857 | // necessary, applying minimum column widths as we go |

858 | |

859 | // 1. apply ideal widths, and collect information we'll need later |

860 | final List<double> widths = List<double>.filled(columns, 0.0); |

861 | final List<double> minWidths = List<double>.filled(columns, 0.0); |

862 | final List<double?> flexes = List<double?>.filled(columns, null); |

863 | double tableWidth = 0.0; // running tally of the sum of widths[x] for all x |

864 | double unflexedTableWidth = 0.0; // sum of the maxIntrinsicWidths of any column that has null flex |

865 | double totalFlex = 0.0; |

866 | for (int x = 0; x < columns; x += 1) { |

867 | final TableColumnWidth columnWidth = _columnWidths[x] ?? defaultColumnWidth; |

868 | final Iterable<RenderBox> columnCells = column(x); |

869 | // apply ideal width (maxIntrinsicWidth) |

870 | final double maxIntrinsicWidth = columnWidth.maxIntrinsicWidth(columnCells, constraints.maxWidth); |

871 | assert(maxIntrinsicWidth.isFinite); |

872 | assert(maxIntrinsicWidth >= 0.0); |

873 | widths[x] = maxIntrinsicWidth; |

874 | tableWidth += maxIntrinsicWidth; |

875 | // collect min width information while we're at it |

876 | final double minIntrinsicWidth = columnWidth.minIntrinsicWidth(columnCells, constraints.maxWidth); |

877 | assert(minIntrinsicWidth.isFinite); |

878 | assert(minIntrinsicWidth >= 0.0); |

879 | minWidths[x] = minIntrinsicWidth; |

880 | assert(maxIntrinsicWidth >= minIntrinsicWidth); |

881 | // collect flex information while we're at it |

882 | final double? flex = columnWidth.flex(columnCells); |

883 | if (flex != null) { |

884 | assert(flex.isFinite); |

885 | assert(flex > 0.0); |

886 | flexes[x] = flex; |

887 | totalFlex += flex; |

888 | } else { |

889 | unflexedTableWidth = unflexedTableWidth + maxIntrinsicWidth; |

890 | } |

891 | } |

892 | final double maxWidthConstraint = constraints.maxWidth; |

893 | final double minWidthConstraint = constraints.minWidth; |

894 | |

895 | // 2. grow the flex columns so that the table has the maxWidth (if |

896 | // finite) or the minWidth (if not) |

897 | if (totalFlex > 0.0) { |

898 | // this can only grow the table, but it _will_ grow the table at |

899 | // least as big as the target width. |

900 | final double targetWidth; |

901 | if (maxWidthConstraint.isFinite) { |

902 | targetWidth = maxWidthConstraint; |

903 | } else { |

904 | targetWidth = minWidthConstraint; |

905 | } |

906 | if (tableWidth < targetWidth) { |

907 | final double remainingWidth = targetWidth - unflexedTableWidth; |

908 | assert(remainingWidth.isFinite); |

909 | assert(remainingWidth >= 0.0); |

910 | for (int x = 0; x < columns; x += 1) { |

911 | if (flexes[x] != null) { |

912 | final double flexedWidth = remainingWidth * flexes[x]! / totalFlex; |

913 | assert(flexedWidth.isFinite); |

914 | assert(flexedWidth >= 0.0); |

915 | if (widths[x] < flexedWidth) { |

916 | final double delta = flexedWidth - widths[x]; |

917 | tableWidth += delta; |

918 | widths[x] = flexedWidth; |

919 | } |

920 | } |

921 | } |

922 | assert(tableWidth + precisionErrorTolerance >= targetWidth); |

923 | } |

924 | } // step 2 and 3 are mutually exclusive |

925 | |

926 | // 3. if there were no flex columns, then grow the table to the |

927 | // minWidth. |

928 | else if (tableWidth < minWidthConstraint) { |

929 | final double delta = (minWidthConstraint - tableWidth) / columns; |

930 | for (int x = 0; x < columns; x += 1) { |

931 | widths[x] = widths[x] + delta; |

932 | } |

933 | tableWidth = minWidthConstraint; |

934 | } |

935 | |

936 | // beyond this point, unflexedTableWidth is no longer valid |

937 | |

938 | // 4. apply the maximum width of the table, shrinking columns as |

939 | // necessary, applying minimum column widths as we go |

940 | if (tableWidth > maxWidthConstraint) { |

941 | double deficit = tableWidth - maxWidthConstraint; |

942 | // Some columns may have low flex but have all the free space. |

943 | // (Consider a case with a 1px wide column of flex 1000.0 and |

944 | // a 1000px wide column of flex 1.0; the sizes coming from the |

945 | // maxIntrinsicWidths. If the maximum table width is 2px, then |

946 | // just applying the flexes to the deficit would result in a |

947 | // table with one column at -998px and one column at 990px, |

948 | // which is wildly unhelpful.) |

949 | // Similarly, some columns may be flexible, but not actually |

950 | // be shrinkable due to a large minimum width. (Consider a |

951 | // case with two columns, one is flex and one isn't, both have |

952 | // 1000px maxIntrinsicWidths, but the flex one has 1000px |

953 | // minIntrinsicWidth also. The whole deficit will have to come |

954 | // from the non-flex column.) |

955 | // So what we do is we repeatedly iterate through the flexible |

956 | // columns shrinking them proportionally until we have no |

957 | // available columns, then do the same to the non-flexible ones. |

958 | int availableColumns = columns; |

959 | while (deficit > precisionErrorTolerance && totalFlex > precisionErrorTolerance) { |

960 | double newTotalFlex = 0.0; |

961 | for (int x = 0; x < columns; x += 1) { |

962 | if (flexes[x] != null) { |

963 | final double newWidth = widths[x] - deficit * flexes[x]! / totalFlex; |

964 | assert(newWidth.isFinite); |

965 | if (newWidth <= minWidths[x]) { |

966 | // shrank to minimum |

967 | deficit -= widths[x] - minWidths[x]; |

968 | widths[x] = minWidths[x]; |

969 | flexes[x] = null; |

970 | availableColumns -= 1; |

971 | } else { |

972 | deficit -= widths[x] - newWidth; |

973 | widths[x] = newWidth; |

974 | newTotalFlex += flexes[x]!; |

975 | } |

976 | assert(widths[x] >= 0.0); |

977 | } |

978 | } |

979 | totalFlex = newTotalFlex; |

980 | } |

981 | while (deficit > precisionErrorTolerance && availableColumns > 0) { |

982 | // Now we have to take out the remaining space from the |

983 | // columns that aren't minimum sized. |

984 | // To make this fair, we repeatedly remove equal amounts from |

985 | // each column, clamped to the minimum width, until we run out |

986 | // of columns that aren't at their minWidth. |

987 | final double delta = deficit / availableColumns; |

988 | assert(delta != 0); |

989 | int newAvailableColumns = 0; |

990 | for (int x = 0; x < columns; x += 1) { |

991 | final double availableDelta = widths[x] - minWidths[x]; |

992 | if (availableDelta > 0.0) { |

993 | if (availableDelta <= delta) { |

994 | // shrank to minimum |

995 | deficit -= widths[x] - minWidths[x]; |

996 | widths[x] = minWidths[x]; |

997 | } else { |

998 | deficit -= delta; |

999 | widths[x] = widths[x] - delta; |

1000 | newAvailableColumns += 1; |

1001 | } |

1002 | } |

1003 | } |

1004 | availableColumns = newAvailableColumns; |

1005 | } |

1006 | } |

1007 | return widths; |

1008 | } |

1009 | |

1010 | // cache the table geometry for painting purposes |

1011 | final List<double> _rowTops = <double>[]; |

1012 | Iterable<double>? _columnLefts; |

1013 | late double _tableWidth; |

1014 | |

1015 | /// Returns the position and dimensions of the box that the given |

1016 | /// row covers, in this render object's coordinate space (so the |

1017 | /// left coordinate is always 0.0). |

1018 | /// |

1019 | /// The row being queried must exist. |

1020 | /// |

1021 | /// This is only valid after layout. |

1022 | Rect getRowBox(int row) { |

1023 | assert(row >= 0); |

1024 | assert(row < rows); |

1025 | assert(!debugNeedsLayout); |

1026 | return Rect.fromLTRB(0.0, _rowTops[row], size.width, _rowTops[row + 1]); |

1027 | } |

1028 | |

1029 | @override |

1030 | @protected |

1031 | Size computeDryLayout(covariant BoxConstraints constraints) { |

1032 | if (rows * columns == 0) { |

1033 | return constraints.constrain(Size.zero); |

1034 | } |

1035 | final List<double> widths = _computeColumnWidths(constraints); |

1036 | final double tableWidth = widths.fold(0.0, (double a, double b) => a + b); |

1037 | double rowTop = 0.0; |

1038 | for (int y = 0; y < rows; y += 1) { |

1039 | double rowHeight = 0.0; |

1040 | for (int x = 0; x < columns; x += 1) { |

1041 | final int xy = x + y * columns; |

1042 | final RenderBox? child = _children[xy]; |

1043 | if (child != null) { |

1044 | final TableCellParentData childParentData = child.parentData! as TableCellParentData; |

1045 | switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { |

1046 | case TableCellVerticalAlignment.baseline: |

1047 | assert(debugCannotComputeDryLayout( |

1048 | reason: 'TableCellVerticalAlignment.baseline requires a full layout for baseline metrics to be available.', |

1049 | )); |

1050 | return Size.zero; |

1051 | case TableCellVerticalAlignment.top: |

1052 | case TableCellVerticalAlignment.middle: |

1053 | case TableCellVerticalAlignment.bottom: |

1054 | case TableCellVerticalAlignment.intrinsicHeight: |

1055 | final Size childSize = child.getDryLayout(BoxConstraints.tightFor(width: widths[x])); |

1056 | rowHeight = math.max(rowHeight, childSize.height); |

1057 | case TableCellVerticalAlignment.fill: |

1058 | break; |

1059 | } |

1060 | } |

1061 | } |

1062 | rowTop += rowHeight; |

1063 | } |

1064 | return constraints.constrain(Size(tableWidth, rowTop)); |

1065 | } |

1066 | |

1067 | @override |

1068 | void performLayout() { |

1069 | final BoxConstraints constraints = this.constraints; |

1070 | final int rows = this.rows; |

1071 | final int columns = this.columns; |

1072 | assert(_children.length == rows * columns); |

1073 | if (rows * columns == 0) { |

1074 | // TODO(ianh): if columns is zero, this should be zero width |

1075 | // TODO(ianh): if columns is not zero, this should be based on the column width specifications |

1076 | _tableWidth = 0.0; |

1077 | size = constraints.constrain(Size.zero); |

1078 | return; |

1079 | } |

1080 | final List<double> widths = _computeColumnWidths(constraints); |

1081 | final List<double> positions = List<double>.filled(columns, 0.0); |

1082 | switch (textDirection) { |

1083 | case TextDirection.rtl: |

1084 | positions[columns - 1] = 0.0; |

1085 | for (int x = columns - 2; x >= 0; x -= 1) { |

1086 | positions[x] = positions[x+1] + widths[x+1]; |

1087 | } |

1088 | _columnLefts = positions.reversed; |

1089 | _tableWidth = positions.first + widths.first; |

1090 | case TextDirection.ltr: |

1091 | positions[0] = 0.0; |

1092 | for (int x = 1; x < columns; x += 1) { |

1093 | positions[x] = positions[x-1] + widths[x-1]; |

1094 | } |

1095 | _columnLefts = positions; |

1096 | _tableWidth = positions.last + widths.last; |

1097 | } |

1098 | _rowTops.clear(); |

1099 | _baselineDistance = null; |

1100 | // then, lay out each row |

1101 | double rowTop = 0.0; |

1102 | for (int y = 0; y < rows; y += 1) { |

1103 | _rowTops.add(rowTop); |

1104 | double rowHeight = 0.0; |

1105 | bool haveBaseline = false; |

1106 | double beforeBaselineDistance = 0.0; |

1107 | double afterBaselineDistance = 0.0; |

1108 | final List<double> baselines = List<double>.filled(columns, 0.0); |

1109 | for (int x = 0; x < columns; x += 1) { |

1110 | final int xy = x + y * columns; |

1111 | final RenderBox? child = _children[xy]; |

1112 | if (child != null) { |

1113 | final TableCellParentData childParentData = child.parentData! as TableCellParentData; |

1114 | childParentData.x = x; |

1115 | childParentData.y = y; |

1116 | switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { |

1117 | case TableCellVerticalAlignment.baseline: |

1118 | assert(textBaseline != null, 'An explicit textBaseline is required when using baseline alignment.'); |

1119 | child.layout(BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true); |

1120 | final double? childBaseline = child.getDistanceToBaseline(textBaseline!, onlyReal: true); |

1121 | if (childBaseline != null) { |

1122 | beforeBaselineDistance = math.max(beforeBaselineDistance, childBaseline); |

1123 | afterBaselineDistance = math.max(afterBaselineDistance, child.size.height - childBaseline); |

1124 | baselines[x] = childBaseline; |

1125 | haveBaseline = true; |

1126 | } else { |

1127 | rowHeight = math.max(rowHeight, child.size.height); |

1128 | childParentData.offset = Offset(positions[x], rowTop); |

1129 | } |

1130 | case TableCellVerticalAlignment.top: |

1131 | case TableCellVerticalAlignment.middle: |

1132 | case TableCellVerticalAlignment.bottom: |

1133 | case TableCellVerticalAlignment.intrinsicHeight: |

1134 | child.layout(BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true); |

1135 | rowHeight = math.max(rowHeight, child.size.height); |

1136 | case TableCellVerticalAlignment.fill: |

1137 | break; |

1138 | } |

1139 | } |

1140 | } |

1141 | if (haveBaseline) { |

1142 | if (y == 0) { |

1143 | _baselineDistance = beforeBaselineDistance; |

1144 | } |

1145 | rowHeight = math.max(rowHeight, beforeBaselineDistance + afterBaselineDistance); |

1146 | } |

1147 | for (int x = 0; x < columns; x += 1) { |

1148 | final int xy = x + y * columns; |

1149 | final RenderBox? child = _children[xy]; |

1150 | if (child != null) { |

1151 | final TableCellParentData childParentData = child.parentData! as TableCellParentData; |

1152 | switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { |

1153 | case TableCellVerticalAlignment.baseline: |

1154 | childParentData.offset = Offset(positions[x], rowTop + beforeBaselineDistance - baselines[x]); |

1155 | case TableCellVerticalAlignment.top: |

1156 | childParentData.offset = Offset(positions[x], rowTop); |

1157 | case TableCellVerticalAlignment.middle: |

1158 | childParentData.offset = Offset(positions[x], rowTop + (rowHeight - child.size.height) / 2.0); |

1159 | case TableCellVerticalAlignment.bottom: |

1160 | childParentData.offset = Offset(positions[x], rowTop + rowHeight - child.size.height); |

1161 | case TableCellVerticalAlignment.fill: |

1162 | case TableCellVerticalAlignment.intrinsicHeight: |

1163 | child.layout(BoxConstraints.tightFor(width: widths[x], height: rowHeight)); |

1164 | childParentData.offset = Offset(positions[x], rowTop); |

1165 | } |

1166 | } |

1167 | } |

1168 | rowTop += rowHeight; |

1169 | } |

1170 | _rowTops.add(rowTop); |

1171 | size = constraints.constrain(Size(_tableWidth, rowTop)); |

1172 | assert(_rowTops.length == rows + 1); |

1173 | } |

1174 | |

1175 | @override |

1176 | bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { |

1177 | assert(_children.length == rows * columns); |

1178 | for (int index = _children.length - 1; index >= 0; index -= 1) { |

1179 | final RenderBox? child = _children[index]; |

1180 | if (child != null) { |

1181 | final BoxParentData childParentData = child.parentData! as BoxParentData; |

1182 | final bool isHit = result.addWithPaintOffset( |

1183 | offset: childParentData.offset, |

1184 | position: position, |

1185 | hitTest: (BoxHitTestResult result, Offset transformed) { |

1186 | assert(transformed == position - childParentData.offset); |

1187 | return child.hitTest(result, position: transformed); |

1188 | }, |

1189 | ); |

1190 | if (isHit) { |

1191 | return true; |

1192 | } |

1193 | } |

1194 | } |

1195 | return false; |

1196 | } |

1197 | |

1198 | @override |

1199 | void paint(PaintingContext context, Offset offset) { |

1200 | assert(_children.length == rows * columns); |

1201 | if (rows * columns == 0) { |

1202 | if (border != null) { |

1203 | final Rect borderRect = Rect.fromLTWH(offset.dx, offset.dy, _tableWidth, 0.0); |

1204 | border!.paint(context.canvas, borderRect, rows: const <double>[], columns: const <double>[]); |

1205 | } |

1206 | return; |

1207 | } |

1208 | assert(_rowTops.length == rows + 1); |

1209 | if (_rowDecorations != null) { |

1210 | assert(_rowDecorations!.length == _rowDecorationPainters!.length); |

1211 | final Canvas canvas = context.canvas; |

1212 | for (int y = 0; y < rows; y += 1) { |

1213 | if (_rowDecorations!.length <= y) { |

1214 | break; |

1215 | } |

1216 | if (_rowDecorations![y] != null) { |

1217 | _rowDecorationPainters![y] ??= _rowDecorations![y]!.createBoxPainter(markNeedsPaint); |

1218 | _rowDecorationPainters![y]!.paint( |

1219 | canvas, |

1220 | Offset(offset.dx, offset.dy + _rowTops[y]), |

1221 | configuration.copyWith(size: Size(size.width, _rowTops[y+1] - _rowTops[y])), |

1222 | ); |

1223 | } |

1224 | } |

1225 | } |

1226 | for (int index = 0; index < _children.length; index += 1) { |

1227 | final RenderBox? child = _children[index]; |

1228 | if (child != null) { |

1229 | final BoxParentData childParentData = child.parentData! as BoxParentData; |

1230 | context.paintChild(child, childParentData.offset + offset); |

1231 | } |

1232 | } |

1233 | assert(_rows == _rowTops.length - 1); |

1234 | assert(_columns == _columnLefts!.length); |

1235 | if (border != null) { |

1236 | // The border rect might not fill the entire height of this render object |

1237 | // if the rows underflow. We always force the columns to fill the width of |

1238 | // the render object, which means the columns cannot underflow. |

1239 | final Rect borderRect = Rect.fromLTWH(offset.dx, offset.dy, _tableWidth, _rowTops.last); |

1240 | final Iterable<double> rows = _rowTops.getRange(1, _rowTops.length - 1); |

1241 | final Iterable<double> columns = _columnLefts!.skip(1); |

1242 | border!.paint(context.canvas, borderRect, rows: rows, columns: columns); |

1243 | } |

1244 | } |

1245 | |

1246 | @override |

1247 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |

1248 | super.debugFillProperties(properties); |

1249 | properties.add(DiagnosticsProperty<TableBorder>('border', border, defaultValue: null)); |

1250 | properties.add(DiagnosticsProperty<Map<int, TableColumnWidth>>('specified column widths', _columnWidths, level: _columnWidths.isEmpty ? DiagnosticLevel.hidden : DiagnosticLevel.info)); |

1251 | properties.add(DiagnosticsProperty<TableColumnWidth>('default column width', defaultColumnWidth)); |

1252 | properties.add(MessageProperty('table size', '$columns \u00D7$rows ')); |

1253 | properties.add(IterableProperty<String>('column offsets', _columnLefts?.map(debugFormatDouble), ifNull: 'unknown')); |

1254 | properties.add(IterableProperty<String>('row offsets', _rowTops.map(debugFormatDouble), ifNull: 'unknown')); |

1255 | } |

1256 | |

1257 | @override |

1258 | List<DiagnosticsNode> debugDescribeChildren() { |

1259 | if (_children.isEmpty) { |

1260 | return <DiagnosticsNode>[DiagnosticsNode.message('table is empty')]; |

1261 | } |

1262 | |

1263 | final List<DiagnosticsNode> children = <DiagnosticsNode>[]; |

1264 | for (int y = 0; y < rows; y += 1) { |

1265 | for (int x = 0; x < columns; x += 1) { |

1266 | final int xy = x + y * columns; |

1267 | final RenderBox? child = _children[xy]; |

1268 | final String name = 'child ($x ,$y )'; |

1269 | if (child != null) { |

1270 | children.add(child.toDiagnosticsNode(name: name)); |

1271 | } else { |

1272 | children.add(DiagnosticsProperty<Object>(name, null, ifNull: 'is null', showSeparator: false)); |

1273 | } |

1274 | } |

1275 | } |

1276 | return children; |

1277 | } |

1278 | } |

1279 |