From 161bfd8087ebaaa0f3c6d505090fe5e914a3667f Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 3 Jan 2024 17:08:47 -0600 Subject: [PATCH 01/21] Need to account for decorations. --- .../example/lib/main.dart | 42 +++- .../lib/src/table_view/table.dart | 137 ++++++++++-- .../lib/src/table_view/table_cell.dart | 156 ++++++++++++- .../lib/src/table_view/table_delegate.dart | 18 +- .../test/table_view/table_delegate_test.dart | 87 ++++---- .../test/table_view/table_test.dart | 209 +++++++++++------- 6 files changed, 495 insertions(+), 154 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index cd23c568ae62..d1571af92ab4 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -88,9 +88,27 @@ class _TableExampleState extends State { ); } - Widget _buildCell(BuildContext context, TableVicinity vicinity) { - return Center( - child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), + TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) { + final Set mergedCells = { + TableVicinity.zero.copyWith(row: 1), + TableVicinity.zero.copyWith(row: 2), + }; + + if (mergedCells.contains(vicinity)) { + print(vicinity); + return const TableViewCell( + rowMergeStart: 1, + rowMergeSpan: 2, + child: Center( + child: Text('Tile c: 0, r: 1'), + ), + ); + } + + return TableViewCell( + child: Center( + child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), + ), ); } @@ -106,7 +124,7 @@ class _TableExampleState extends State { return TableSpan( foregroundDecoration: decoration, extent: const FixedTableSpanExtent(100), - onEnter: (_) => print('Entered column $index'), + // onEnter: (_) => print('Entered column $index'), recognizerFactories: { TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( @@ -120,26 +138,26 @@ class _TableExampleState extends State { return TableSpan( foregroundDecoration: decoration, extent: const FractionalTableSpanExtent(0.5), - onEnter: (_) => print('Entered column $index'), + // onEnter: (_) => print('Entered column $index'), cursor: SystemMouseCursors.contextMenu, ); case 2: return TableSpan( foregroundDecoration: decoration, extent: const FixedTableSpanExtent(120), - onEnter: (_) => print('Entered column $index'), + // onEnter: (_) => print('Entered column $index'), ); case 3: return TableSpan( foregroundDecoration: decoration, extent: const FixedTableSpanExtent(145), - onEnter: (_) => print('Entered column $index'), + // onEnter: (_) => print('Entered column $index'), ); case 4: return TableSpan( foregroundDecoration: decoration, extent: const FixedTableSpanExtent(200), - onEnter: (_) => print('Entered column $index'), + // onEnter: (_) => print('Entered column $index'), ); } throw AssertionError( @@ -147,7 +165,7 @@ class _TableExampleState extends State { } TableSpan _buildRowSpan(int index) { - final TableSpanDecoration decoration = TableSpanDecoration( + final TableSpanDecoration decoration0 = TableSpanDecoration( color: index.isEven ? Colors.purple[100] : null, border: const TableSpanBorder( trailing: BorderSide( @@ -159,7 +177,7 @@ class _TableExampleState extends State { switch (index % 3) { case 0: return TableSpan( - backgroundDecoration: decoration, + backgroundDecoration: decoration0, extent: const FixedTableSpanExtent(50), recognizerFactories: { TapGestureRecognizer: @@ -172,13 +190,13 @@ class _TableExampleState extends State { ); case 1: return TableSpan( - backgroundDecoration: decoration, + backgroundDecoration: decoration0, extent: const FixedTableSpanExtent(65), cursor: SystemMouseCursors.click, ); case 2: return TableSpan( - backgroundDecoration: decoration, + backgroundDecoration: decoration0, extent: const FractionalTableSpanExtent(0.15), ); } diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index a8d9a0cc99d3..aac88ebc0561 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -169,7 +169,7 @@ class TableView extends TwoDimensionalScrollView { int pinnedColumnCount = 0, required TableSpanBuilder columnBuilder, required TableSpanBuilder rowBuilder, - List> cells = const >[], + List> cells = const >[], }) : assert(pinnedRowCount >= 0), assert(pinnedColumnCount >= 0), super( @@ -280,6 +280,18 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { super.delegate = value; } + // Skipped vicinities for the current frame based on merged cells. + // This prevents multiple build calls for the same cell that spans multiple + // vicinities. + // The key represents a skipped vicinity, the value is the resolved vicinity + // of the merged child. + final Map _mergedVicinities = + {}; + // Used to optimize decorating when there are no merged cells in a given + // frame. + bool _mergedRows = false; + bool _mergedColumns = false; + // Cached Table metrics Map _columnMetrics = {}; Map _rowMetrics = {}; @@ -595,6 +607,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { @override void layoutChildSequence() { + // Reset for a new frame + _mergedVicinities.clear(); + _mergedRows = false; + _mergedColumns = false; + if (needsDelegateRebuild || didResize) { // Recomputes the table metrics, invalidates any cached information. _updateAllMetrics(); @@ -624,7 +641,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (_lastPinnedRow != null && _lastPinnedColumn != null) { // Layout cells that are contained in both pinned rows and columns _layoutCells( - start: const TableVicinity(column: 0, row: 0), + start: TableVicinity.zero, end: TableVicinity(column: _lastPinnedColumn!, row: _lastPinnedRow!), offset: Offset.zero, ); @@ -665,19 +682,54 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } } + bool _debugCheckMergeBounds({ + required String spanOrientation, + required int currentSpan, + required int spanMergeStart, + required int spanMergeEnd, + required int spanCount, + required int pinnedSpanCount, + required TableVicinity currentVicinity, + }) { + final String lowerSpanOrientation = spanOrientation.toLowerCase(); + assert( + spanMergeStart <= currentSpan, + 'The ${lowerSpanOrientation}MergeStart of $spanMergeStart is greater ' + 'than the current $lowerSpanOrientation at $currentVicinity.', + ); + assert( + spanMergeEnd <= spanCount, + '$spanOrientation merge configuration exceeds number of ' + '${lowerSpanOrientation}s in the table. $spanOrientation merge ' + 'containing $currentVicinity starts at $spanMergeStart, and ends at ' + '$spanMergeEnd. The TableView contains $spanCount.', + ); + if (spanMergeStart < pinnedSpanCount) { + // Merged cells cannot span pinned and unpinned cells. + assert( + spanMergeEnd < pinnedSpanCount, + 'Merged cells cannot span pinned and unpinned cells. $spanOrientation ' + 'merge containing $currentVicinity starts at $spanMergeStart, and ends ' + 'at $spanMergeEnd. ${spanOrientation}s are currently pinned up to ' + '$lowerSpanOrientation ${pinnedSpanCount - 1}.', + ); + } + return true; + } + void _layoutCells({ required TableVicinity start, required TableVicinity end, required Offset offset, }) { - // TODO(Piinks): Assert here or somewhere else merged cells cannot span - // pinned and unpinned cells (for merged cell follow-up), https://github.com/flutter/flutter/issues/131224 _Span colSpan, rowSpan; double yPaintOffset = -offset.dy; for (int row = start.row; row <= end.row; row += 1) { double xPaintOffset = -offset.dx; rowSpan = _rowMetrics[row]!; - final double rowHeight = rowSpan.extent; + final double standardRowHeight = rowSpan.extent; + double? mergedRowHeight; + double? mergedYPaintOffset; yPaintOffset += rowSpan.configuration.padding.leading; for (int column = start.column; column <= end.column; column += 1) { colSpan = _columnMetrics[column]!; @@ -685,25 +737,64 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { xPaintOffset += colSpan.configuration.padding.leading; final TableVicinity vicinity = TableVicinity(column: column, row: row); - // TODO(Piinks): Add back merged cells, https://github.com/flutter/flutter/issues/131224 - - final RenderBox? cell = buildOrObtainChildFor(vicinity); + RenderBox? cell; + if (!_mergedVicinities.keys.contains(vicinity)) { + // We do not call build for vicinities that are already covered by a + // merged cell. + cell = buildOrObtainChildFor(vicinity); + } if (cell != null) { final TableViewParentData cellParentData = parentDataOf(cell); + // Merged cell handling + if (cellParentData.rowMergeStart != null) { + _mergedRows = true; + final int rowMergeStart = cellParentData.rowMergeStart!; + final int lastRow = + rowMergeStart + cellParentData.rowMergeSpan! - 1; + assert(_debugCheckMergeBounds( + spanOrientation: 'Row', + currentSpan: row, + spanMergeStart: rowMergeStart, + spanMergeEnd: lastRow, + spanCount: delegate.rowCount, + pinnedSpanCount: delegate.pinnedRowCount, + currentVicinity: vicinity, + )); + // Compute height and layout offset for merged rows. + final _Span firstRow = _rowMetrics[rowMergeStart]!; + mergedRowHeight = firstRow.extent; + mergedYPaintOffset = -verticalOffset.pixels + + firstRow.leadingOffset + + firstRow.configuration.padding.leading; + _mergedVicinities[vicinity.copyWith(row: rowMergeStart)] = vicinity; + int nextRow = rowMergeStart + 1; + while (nextRow <= lastRow) { + _mergedVicinities[vicinity.copyWith(row: nextRow)] = vicinity; + mergedRowHeight = mergedRowHeight! + _rowMetrics[nextRow]!.extent; + nextRow++; + } + } + // TODO(Piinks): Copy logic for merged columns + final BoxConstraints cellConstraints = BoxConstraints.tightFor( width: columnWidth, - height: rowHeight, + height: mergedRowHeight ?? standardRowHeight, ); cell.layout(cellConstraints); - cellParentData.layoutOffset = Offset(xPaintOffset, yPaintOffset); + cellParentData.layoutOffset = Offset( + xPaintOffset, + mergedYPaintOffset ?? yPaintOffset, + ); + mergedYPaintOffset = null; + mergedRowHeight = null; } xPaintOffset += columnWidth + _columnMetrics[column]!.configuration.padding.trailing; } yPaintOffset += - rowHeight + _rowMetrics[row]!.configuration.padding.trailing; + standardRowHeight + _rowMetrics[row]!.configuration.padding.trailing; } } @@ -829,13 +920,25 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _paintCells( context: context, offset: offset, - leading: const TableVicinity(column: 0, row: 0), + leading: TableVicinity.zero, trailing: TableVicinity(column: _lastPinnedColumn!, row: _lastPinnedRow!), ); } } + @override + RenderBox? getChildFor(ChildVicinity vicinity) { + final RenderBox? child = super.getChildFor(vicinity); + return child ?? _getMergedChildFor(vicinity as TableVicinity); + } + + RenderBox _getMergedChildFor(TableVicinity vicinity) { + assert(_mergedVicinities.keys.contains(vicinity)); + final TableVicinity mergedVicinity = _mergedVicinities[vicinity]!; + return getChildFor(mergedVicinity)!; + } + void _paintCells({ required PaintingContext context, required TableVicinity leading, @@ -1009,9 +1112,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // Cells for (int column = leading.column; column <= trailing.column; column++) { for (int row = leading.row; row <= trailing.row; row++) { - final RenderBox cell = getChildFor( - TableVicinity(column: column, row: row), - )!; + final TableVicinity vicinity = TableVicinity(column: column, row: row); + final RenderBox? cell = getChildFor(vicinity); + if (cell == null) { + // Covered by a merged cell + assert(_mergedVicinities.keys.contains(vicinity)); + continue; + } final TableViewParentData cellParentData = parentDataOf(cell); if (cellParentData.isVisible) { context.paintChild(cell, offset + cellParentData.paintOffset!); diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart index d071a2349675..69ab5ffe39fe 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'table.dart'; @@ -31,13 +32,166 @@ class TableVicinity extends ChildVicinity { /// Equivalent to the [xIndex]. int get column => xIndex; + /// + static const TableVicinity zero = TableVicinity(row: 0, column: 0); + + /// Returns a new TableVicinity, copying over the row and column fields with + /// those provided, or maintaining the original values. + TableVicinity copyWith({ + int? row, + int? column, + }) { + return TableVicinity( + row: row ?? this.row, + column: column ?? this.column, + ); + } + @override String toString() => '(row: $row, column: $column)'; } /// Parent data structure used by [RenderTableViewport]. class TableViewParentData extends TwoDimensionalViewportParentData { - // TODO(Piinks): Add back merged cells here, https://github.com/flutter/flutter/issues/131224 /// Converts the [ChildVicinity] to a [TableVicinity] for ease of use. TableVicinity get tableVicinity => vicinity as TableVicinity; + + /// + int? rowMergeStart; + + /// + int? rowMergeSpan; + + /// + int? columnMergeStart; + + /// + int? columnMergeSpan; +} + +/// +class TableViewCell extends StatelessWidget { + /// Creates a widget that controls how a child of a [TableView] spans across + /// multiple rows or columns. + const TableViewCell({ + super.key, + this.rowMergeStart, + this.rowMergeSpan, + this.columnMergeStart, + this.columnMergeSpan, + this.addRepaintBoundaries = true, + required this.child, + }) : assert( + (rowMergeStart == null && rowMergeSpan == null) || + (rowMergeStart != null && rowMergeSpan != null), + 'Row merge start and span must both be set, or both unset.', + ), + assert( + (columnMergeStart == null && columnMergeSpan == null) || + (columnMergeStart != null && columnMergeSpan != null), + 'Column merge start and span must both be set, or both unset.', + ); + + /// + final Widget child; + + /// + final int? rowMergeStart; + + /// + final int? rowMergeSpan; + + /// + final int? columnMergeStart; + + /// + final int? columnMergeSpan; + + /// + final bool addRepaintBoundaries; + + @override + Widget build(BuildContext context) { + Widget child = this.child; + + if (addRepaintBoundaries) { + child = RepaintBoundary(child: child); + } + + return _TableViewCell( + rowMergeStart: rowMergeStart, + rowMergeSpan: rowMergeSpan, + columnMergeStart: columnMergeStart, + columnMergeSpan: columnMergeSpan, + child: child, + ); + } +} + +class _TableViewCell extends ParentDataWidget { + const _TableViewCell({ + this.rowMergeStart, + this.rowMergeSpan, + this.columnMergeStart, + this.columnMergeSpan, + required super.child, + }) : assert( + (rowMergeStart == null && rowMergeSpan == null) || + (rowMergeStart != null && rowMergeSpan != null), + 'Row merge start and span must both be set, or both unset.', + ), + assert( + (columnMergeStart == null && columnMergeSpan == null) || + (columnMergeStart != null && columnMergeSpan != null), + 'Column merge start and span must both be set, or both unset.', + ); + + final int? rowMergeStart; + final int? rowMergeSpan; + final int? columnMergeStart; + final int? columnMergeSpan; + + @override + void applyParentData(RenderObject renderObject) { + final TableViewParentData parentData = + renderObject.parentData! as TableViewParentData; + bool needsLayout = false; + if (parentData.rowMergeStart != rowMergeStart) { + parentData.rowMergeStart = rowMergeStart; + needsLayout = true; + } + if (parentData.rowMergeSpan != rowMergeSpan) { + parentData.rowMergeSpan = rowMergeSpan; + needsLayout = true; + } + if (parentData.columnMergeStart != columnMergeStart) { + parentData.columnMergeStart = columnMergeStart; + needsLayout = true; + } + if (parentData.columnMergeSpan != columnMergeSpan) { + parentData.columnMergeSpan = columnMergeSpan; + needsLayout = true; + } + + final RenderObject? targetParent = renderObject.parent; + if (targetParent is RenderObject && needsLayout) { + targetParent.markNeedsLayout(); + } + } + + @override + Type get debugTypicalAncestorWidgetClass => TableViewport; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + if (rowMergeStart != null) { + properties.add(IntProperty('rowMergeStart', rowMergeStart)); + properties.add(IntProperty('rowMergeSpan', rowMergeSpan)); + } + if (columnMergeStart != null) { + properties.add(IntProperty('columnMergeStart', columnMergeStart)); + properties.add(IntProperty('columnMergeSpan', columnMergeSpan)); + } + } } diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart index 275bfb5bae48..de8453d0d0d1 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart @@ -21,7 +21,7 @@ typedef TableSpanBuilder = TableSpan Function(int index); /// /// Used by [TableCellBuilderDelegate.builder] to build cells on demand for the /// table. -typedef TableViewCellBuilder = Widget? Function( +typedef TableViewCellBuilder = TableViewCell? Function( BuildContext context, TableVicinity vicinity, ); @@ -124,7 +124,6 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate required int rowCount, int pinnedColumnCount = 0, int pinnedRowCount = 0, - super.addRepaintBoundaries, super.addAutomaticKeepAlives, required TableViewCellBuilder cellBuilder, required TableSpanBuilder columnBuilder, @@ -144,6 +143,7 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate cellBuilder(context, vicinity as TableVicinity), maxXIndex: columnCount - 1, maxYIndex: rowCount - 1, + addRepaintBoundaries: false, ); @override @@ -215,9 +215,8 @@ class TableCellListDelegate extends TwoDimensionalChildListDelegate TableCellListDelegate({ int pinnedColumnCount = 0, int pinnedRowCount = 0, - super.addRepaintBoundaries, super.addAutomaticKeepAlives, - required List> cells, + required List> cells, required TableSpanBuilder columnBuilder, required TableSpanBuilder rowBuilder, }) : assert(pinnedColumnCount >= 0), @@ -226,10 +225,15 @@ class TableCellListDelegate extends TwoDimensionalChildListDelegate _rowBuilder = rowBuilder, _pinnedColumnCount = pinnedColumnCount, _pinnedRowCount = pinnedRowCount, - super(children: cells) { + super( + children: cells, + addRepaintBoundaries: false, + ) { // Even if there are merged cells, they should be represented by the same - // child in each cell location. So all arrays of cells should have the same - // length. + // child in each cell location. This ensures that no matter which direction + // the merged cell scrolls into view from, we can build the correct child + // without having to explore all possible vicinities of the merged cell + // area. So all arrays of cells should have the same length. assert( children.map((List array) => array.length).toSet().length == 1, 'Each list of Widgets within cells must be of the same length.', diff --git a/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart index 5d919222f75d..f2c923e26b44 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart @@ -7,7 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; const TableSpan span = TableSpan(extent: FixedTableSpanExtent(50)); -const Widget cell = SizedBox.shrink(); +const TableViewCell cell = TableViewCell(child: SizedBox.shrink()); void main() { group('TableCellBuilderDelegate', () { @@ -173,7 +173,8 @@ void main() { int notified = 0; TableCellBuilderDelegate oldDelegate; TableSpan spanBuilder(int index) => span; - Widget cellBuilder(BuildContext context, TableVicinity vicinity) => cell; + TableViewCell cellBuilder(BuildContext context, TableVicinity vicinity) => + cell; final TableCellBuilderDelegate delegate = TableCellBuilderDelegate( cellBuilder: cellBuilder, columnBuilder: spanBuilder, @@ -219,7 +220,7 @@ void main() { group('TableCellListDelegate', () { test('exposes addAutomaticKeepAlives from super class', () { final TableCellListDelegate delegate = TableCellListDelegate( - cells: >[[]], + cells: >[[]], columnBuilder: (_) => span, rowBuilder: (_) => span, addAutomaticKeepAlives: false, @@ -232,7 +233,7 @@ void main() { expect( () { delegate = TableCellListDelegate( - cells: >[[]], + cells: >[[]], columnBuilder: (_) => span, rowBuilder: (_) => span, pinnedColumnCount: -1, // asserts @@ -249,7 +250,7 @@ void main() { expect( () { delegate = TableCellListDelegate( - cells: >[[]], + cells: >[[]], columnBuilder: (_) => span, rowBuilder: (_) => span, pinnedRowCount: -1, // asserts @@ -266,9 +267,9 @@ void main() { expect( () { delegate = TableCellListDelegate( - cells: >[ - [cell, cell], - [cell, cell], + cells: >[ + [cell, cell], + [cell, cell], ], columnBuilder: (_) => span, rowBuilder: (_) => span, @@ -286,9 +287,9 @@ void main() { expect( () { delegate = TableCellListDelegate( - cells: >[ - [cell, cell], - [cell, cell], + cells: >[ + [cell, cell], + [cell, cell], ], columnBuilder: (_) => span, rowBuilder: (_) => span, @@ -311,9 +312,9 @@ void main() { expect( () { delegate = TableCellListDelegate( - cells: >[ - [cell, cell], - [cell, cell, cell], + cells: >[ + [cell, cell], + [cell, cell, cell], ], columnBuilder: (_) => span, rowBuilder: (_) => span, @@ -336,9 +337,9 @@ void main() { TableCellListDelegate oldDelegate; TableSpan spanBuilder(int index) => span; TableCellListDelegate delegate = TableCellListDelegate( - cells: >[ - [cell, cell], - [cell, cell], + cells: >[ + [cell, cell], + [cell, cell], ], columnBuilder: spanBuilder, rowBuilder: spanBuilder, @@ -363,9 +364,9 @@ void main() { // columnCount oldDelegate = delegate; delegate = TableCellListDelegate( - cells: >[ - [cell, cell, cell], - [cell, cell, cell], + cells: >[ + [cell, cell, cell], + [cell, cell, cell], ], columnBuilder: spanBuilder, rowBuilder: spanBuilder, @@ -375,9 +376,9 @@ void main() { // columnBuilder oldDelegate = delegate; delegate = TableCellListDelegate( - cells: >[ - [cell, cell, cell], - [cell, cell, cell], + cells: >[ + [cell, cell, cell], + [cell, cell, cell], ], columnBuilder: (int index) => const TableSpan( extent: FixedTableSpanExtent(150), @@ -389,10 +390,10 @@ void main() { // rowCount oldDelegate = delegate; delegate = TableCellListDelegate( - cells: >[ - [cell, cell, cell], - [cell, cell, cell], - [cell, cell, cell], + cells: >[ + [cell, cell, cell], + [cell, cell, cell], + [cell, cell, cell], ], columnBuilder: (int index) => const TableSpan( extent: FixedTableSpanExtent(150), @@ -404,10 +405,10 @@ void main() { // rowBuilder oldDelegate = delegate; delegate = TableCellListDelegate( - cells: >[ - [cell, cell, cell], - [cell, cell, cell], - [cell, cell, cell], + cells: >[ + [cell, cell, cell], + [cell, cell, cell], + [cell, cell, cell], ], columnBuilder: (int index) => const TableSpan( extent: FixedTableSpanExtent(150), @@ -421,10 +422,10 @@ void main() { // pinned row count oldDelegate = delegate; delegate = TableCellListDelegate( - cells: >[ - [cell, cell, cell], - [cell, cell, cell], - [cell, cell, cell], + cells: >[ + [cell, cell, cell], + [cell, cell, cell], + [cell, cell, cell], ], columnBuilder: (int index) => const TableSpan( extent: FixedTableSpanExtent(150), @@ -439,10 +440,10 @@ void main() { // pinned column count oldDelegate = delegate; delegate = TableCellListDelegate( - cells: >[ - [cell, cell, cell], - [cell, cell, cell], - [cell, cell, cell], + cells: >[ + [cell, cell, cell], + [cell, cell, cell], + [cell, cell, cell], ], columnBuilder: (int index) => const TableSpan( extent: FixedTableSpanExtent(150), @@ -461,10 +462,10 @@ void main() { test('Changing pinned row and column counts asserts valid values', () { final TableCellListDelegate delegate = TableCellListDelegate( - cells: >[ - [cell, cell, cell], - [cell, cell, cell], - [cell, cell, cell], + cells: >[ + [cell, cell, cell], + [cell, cell, cell], + [cell, cell, cell], ], columnBuilder: (int index) => const TableSpan( extent: FixedTableSpanExtent(150), diff --git a/packages/two_dimensional_scrollables/test/table_view/table_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_test.dart index 670caf00b620..0e16a9a8a86d 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -10,7 +10,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; const TableSpan span = TableSpan(extent: FixedTableSpanExtent(100)); -const Widget cell = SizedBox.shrink(); +const TableViewCell cell = TableViewCell(child: SizedBox.shrink()); TableSpan getTappableSpan(int index, VoidCallback callback) { return TableSpan( @@ -188,9 +188,9 @@ void main() { final TableView tableView = TableView.list( rowBuilder: (_) => span, columnBuilder: (_) => span, - cells: const >[ - [cell, cell, cell], - [cell, cell, cell] + cells: const >[ + [cell, cell, cell], + [cell, cell, cell] ], ); final TableCellListDelegate delegate = @@ -209,8 +209,8 @@ void main() { expect( () { tableView = TableView.list( - cells: const >[ - [cell] + cells: const >[ + [cell] ], columnBuilder: (_) => span, rowBuilder: (_) => span, @@ -228,8 +228,8 @@ void main() { expect( () { tableView = TableView.list( - cells: const >[ - [cell] + cells: const >[ + [cell] ], columnBuilder: (_) => span, rowBuilder: (_) => span, @@ -261,7 +261,12 @@ void main() { rowBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); - return SizedBox.square(key: childKeys[vicinity], dimension: 200); + return TableViewCell( + child: SizedBox.square( + key: childKeys[vicinity], + dimension: 200, + ), + ); }, ); TableViewParentData parentDataOf(RenderBox child) { @@ -343,7 +348,12 @@ void main() { rowBuilder: (_) => rowSpan, cellBuilder: (_, TableVicinity vicinity) { childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); - return SizedBox.square(key: childKeys[vicinity], dimension: 200); + return TableViewCell( + child: SizedBox.square( + key: childKeys[vicinity], + dimension: 200, + ), + ); }, ); TableViewParentData parentDataOf(RenderBox child) { @@ -406,7 +416,12 @@ void main() { rowBuilder: (_) => rowSpan, cellBuilder: (_, TableVicinity vicinity) { childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); - return SizedBox.square(key: childKeys[vicinity], dimension: 200); + return TableViewCell( + child: SizedBox.square( + key: childKeys[vicinity], + dimension: 200, + ), + ); }, ); @@ -454,9 +469,11 @@ void main() { ) : span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, ); @@ -498,9 +515,11 @@ void main() { ) : span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, ); @@ -548,9 +567,11 @@ void main() { ) : span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, ); @@ -605,9 +626,11 @@ void main() { ) : span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, ); @@ -638,7 +661,9 @@ void main() { columnBuilder: (_) => TableSpan(extent: columnExtent), rowBuilder: (_) => TableSpan(extent: rowExtent), cellBuilder: (_, TableVicinity vicinity) { - return const SizedBox.square(dimension: 100); + return const TableViewCell( + child: SizedBox.square(dimension: 100), + ); }, verticalDetails: ScrollableDetails.vertical( controller: verticalController, @@ -693,9 +718,11 @@ void main() { ), rowBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, ); @@ -724,9 +751,11 @@ void main() { ), columnBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, ); @@ -757,9 +786,11 @@ void main() { ), rowBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 200, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 200, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, ); @@ -785,9 +816,11 @@ void main() { ), rowBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 200, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 200, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, ); @@ -813,9 +846,11 @@ void main() { ), columnBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 200, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 200, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, ); @@ -840,9 +875,11 @@ void main() { ), columnBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 200, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 200, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, ); @@ -872,9 +909,11 @@ void main() { columnBuilder: (_) => span, rowBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, verticalDetails: ScrollableDetails.vertical( @@ -950,9 +989,11 @@ void main() { columnBuilder: (_) => span, rowBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, verticalDetails: ScrollableDetails.vertical( @@ -1028,9 +1069,11 @@ void main() { columnBuilder: (_) => span, rowBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, verticalDetails: ScrollableDetails.vertical( @@ -1107,9 +1150,11 @@ void main() { columnBuilder: (_) => span, rowBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, verticalDetails: ScrollableDetails.vertical( @@ -1185,9 +1230,11 @@ void main() { columnBuilder: (_) => span, rowBuilder: (_) => span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, verticalDetails: ScrollableDetails.vertical( @@ -1285,10 +1332,12 @@ void main() { ), ), cellBuilder: (_, TableVicinity vicinity) { - return Container( - height: 200, - width: 200, - color: Colors.grey.withOpacity(0.5), + return TableViewCell( + child: Container( + height: 200, + width: 200, + color: Colors.grey.withOpacity(0.5), + ), ); }, ); @@ -1442,10 +1491,12 @@ void main() { ), ), cellBuilder: (_, TableVicinity vicinity) { - return Container( - height: 200, - width: 200, - color: Colors.grey.withOpacity(0.5), + return TableViewCell( + child: Container( + height: 200, + width: 200, + color: Colors.grey.withOpacity(0.5), + ), ); }, ); @@ -1541,10 +1592,12 @@ void main() { extent: FixedTableSpanExtent(200.0), ), cellBuilder: (_, TableVicinity vicinity) { - return Container( - height: 200, - width: 200, - color: Colors.grey.withOpacity(0.5), + return TableViewCell( + child: Container( + height: 200, + width: 200, + color: Colors.grey.withOpacity(0.5), + ), ); }, ); @@ -1587,10 +1640,12 @@ void main() { extent: FixedTableSpanExtent(200.0), ), cellBuilder: (_, TableVicinity vicinity) { - return Container( - height: 200, - width: 200, - color: Colors.grey.withOpacity(0.5), + return TableViewCell( + child: Container( + height: 200, + width: 200, + color: Colors.grey.withOpacity(0.5), + ), ); }, ); @@ -1634,9 +1689,11 @@ void main() { ) : span, cellBuilder: (_, TableVicinity vicinity) { - return SizedBox.square( - dimension: 100, - child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + return TableViewCell( + child: SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ), ); }, ); From 7245dcd5984ec5508aea9e7a9c13466cec826486 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Thu, 4 Jan 2024 13:23:28 -0600 Subject: [PATCH 02/21] Rows work with resolution for decorations, example app needs to be restored/updated --- .../example/lib/main.dart | 174 +++++------------- .../lib/src/table_view/table.dart | 97 ++++++---- 2 files changed, 107 insertions(+), 164 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index d1571af92ab4..713303a69694 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -41,8 +41,15 @@ class TableExample extends StatefulWidget { } class _TableExampleState extends State { - late final ScrollController _verticalController = ScrollController(); - int _rowCount = 20; + final Map mergedRows= { + // TableVicinity in merged cell : (start, span) + TableVicinity.zero : (0, 2), + TableVicinity.zero.copyWith(row: 1): (0, 2), + const TableVicinity(row: 1, column: 1) : (1, 2), + const TableVicinity(row: 2, column: 1) : (1, 2), + const TableVicinity(row: 2, column: 2) : (2, 2), + const TableVicinity(row: 3, column: 2) : (2, 2), + }; @override Widget build(BuildContext context) { @@ -50,57 +57,23 @@ class _TableExampleState extends State { appBar: AppBar( title: const Text('Table Example'), ), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 50), - child: TableView.builder( - verticalDetails: - ScrollableDetails.vertical(controller: _verticalController), - cellBuilder: _buildCell, - columnCount: 20, - columnBuilder: _buildColumnSpan, - rowCount: _rowCount, - rowBuilder: _buildRowSpan, - ), + body: TableView.builder( + cellBuilder: _buildCell, + columnCount: 4, + columnBuilder: _buildColumnSpan, + rowCount: 4, + rowBuilder: _buildRowSpan, ), - persistentFooterButtons: [ - TextButton( - onPressed: () { - _verticalController.jumpTo(0); - }, - child: const Text('Jump to Top'), - ), - TextButton( - onPressed: () { - _verticalController - .jumpTo(_verticalController.position.maxScrollExtent); - }, - child: const Text('Jump to Bottom'), - ), - TextButton( - onPressed: () { - setState(() { - _rowCount += 10; - }); - }, - child: const Text('Add 10 Rows'), - ), - ], ); } TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) { - final Set mergedCells = { - TableVicinity.zero.copyWith(row: 1), - TableVicinity.zero.copyWith(row: 2), - }; - - if (mergedCells.contains(vicinity)) { - print(vicinity); - return const TableViewCell( - rowMergeStart: 1, - rowMergeSpan: 2, - child: Center( - child: Text('Tile c: 0, r: 1'), + if (mergedRows.keys.contains(vicinity)) { + return TableViewCell( + rowMergeStart: mergedRows[vicinity]!.$1, + rowMergeSpan: mergedRows[vicinity]!.$2, + child: const Center( + child: Text('Merged'), ), ); } @@ -112,95 +85,30 @@ class _TableExampleState extends State { ); } - TableSpan _buildColumnSpan(int index) { - const TableSpanDecoration decoration = TableSpanDecoration( - border: TableSpanBorder( - trailing: BorderSide(), - ), - ); - - switch (index % 5) { - case 0: - return TableSpan( - foregroundDecoration: decoration, - extent: const FixedTableSpanExtent(100), - // onEnter: (_) => print('Entered column $index'), - recognizerFactories: { - TapGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(), - (TapGestureRecognizer t) => - t.onTap = () => print('Tap column $index'), - ), - }, - ); - case 1: - return TableSpan( - foregroundDecoration: decoration, - extent: const FractionalTableSpanExtent(0.5), - // onEnter: (_) => print('Entered column $index'), - cursor: SystemMouseCursors.contextMenu, - ); - case 2: - return TableSpan( - foregroundDecoration: decoration, - extent: const FixedTableSpanExtent(120), - // onEnter: (_) => print('Entered column $index'), - ); - case 3: - return TableSpan( - foregroundDecoration: decoration, - extent: const FixedTableSpanExtent(145), - // onEnter: (_) => print('Entered column $index'), - ); - case 4: - return TableSpan( - foregroundDecoration: decoration, - extent: const FixedTableSpanExtent(200), - // onEnter: (_) => print('Entered column $index'), - ); + TableSpan _buildRowSpan(int index) { + late final Color color; + switch(index) { + case 1: color = Colors.purple; + case 2: color = Colors.blue; + case 3: color = Colors.green; + default: color = Colors.transparent; } - throw AssertionError( - 'This should be unreachable, as every index is accounted for in the switch clauses.'); - } - TableSpan _buildRowSpan(int index) { - final TableSpanDecoration decoration0 = TableSpanDecoration( - color: index.isEven ? Colors.purple[100] : null, - border: const TableSpanBorder( - trailing: BorderSide( - width: 3, - ), + return TableSpan( + extent: const FixedTableSpanExtent(100.0), + backgroundDecoration: TableSpanDecoration( + color: color, + border: const TableSpanBorder(leading: BorderSide(), trailing: BorderSide(),), ), ); + } - switch (index % 3) { - case 0: - return TableSpan( - backgroundDecoration: decoration0, - extent: const FixedTableSpanExtent(50), - recognizerFactories: { - TapGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(), - (TapGestureRecognizer t) => - t.onTap = () => print('Tap row $index'), - ), - }, - ); - case 1: - return TableSpan( - backgroundDecoration: decoration0, - extent: const FixedTableSpanExtent(65), - cursor: SystemMouseCursors.click, - ); - case 2: - return TableSpan( - backgroundDecoration: decoration0, - extent: const FractionalTableSpanExtent(0.15), - ); - } - throw AssertionError( - 'This should be unreachable, as every index is accounted for in the switch clauses.'); + TableSpan _buildColumnSpan(int index) { + return const TableSpan( + extent: FixedTableSpanExtent(100.0), + // foregroundDecoration: TableSpanDecoration( + // border: const TableSpanBorder(leading: BorderSide(), trailing: BorderSide(),), + // ), + ); } } diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index aac88ebc0561..42a3a0a836a4 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -289,8 +289,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { {}; // Used to optimize decorating when there are no merged cells in a given // frame. - bool _mergedRows = false; - bool _mergedColumns = false; + final List _mergedRows = []; + // bool _mergedColumns = false; // Cached Table metrics Map _columnMetrics = {}; @@ -609,8 +609,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { void layoutChildSequence() { // Reset for a new frame _mergedVicinities.clear(); - _mergedRows = false; - _mergedColumns = false; + _mergedRows.clear(); + // _mergedColumns = false; if (needsDelegateRebuild || didResize) { // Recomputes the table metrics, invalidates any cached information. @@ -749,8 +749,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // Merged cell handling if (cellParentData.rowMergeStart != null) { - _mergedRows = true; final int rowMergeStart = cellParentData.rowMergeStart!; + _mergedRows.add(rowMergeStart); final int lastRow = rowMergeStart + cellParentData.rowMergeSpan! - 1; assert(_debugCheckMergeBounds( @@ -771,6 +771,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _mergedVicinities[vicinity.copyWith(row: rowMergeStart)] = vicinity; int nextRow = rowMergeStart + 1; while (nextRow <= lastRow) { + _mergedRows.add(nextRow); _mergedVicinities[vicinity.copyWith(row: nextRow)] = vicinity; mergedRowHeight = mergedRowHeight! + _rowMetrics[nextRow]!.extent; nextRow++; @@ -927,12 +928,6 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } } - @override - RenderBox? getChildFor(ChildVicinity vicinity) { - final RenderBox? child = super.getChildFor(vicinity); - return child ?? _getMergedChildFor(vicinity as TableVicinity); - } - RenderBox _getMergedChildFor(TableVicinity vicinity) { assert(_mergedVicinities.keys.contains(vicinity)); final TableVicinity mergedVicinity = _mergedVicinities[vicinity]!; @@ -1009,18 +1004,43 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { LinkedHashMap(); final LinkedHashMap backgroundRows = LinkedHashMap(); - final TableSpan columnSpan = _columnMetrics[leading.column]!.configuration; for (int row = leading.row; row <= trailing.row; row++) { - final TableSpan rowSpan = _rowMetrics[row]!.configuration; + TableSpan rowSpan = _rowMetrics[row]!.configuration; if (rowSpan.backgroundDecoration != null || - rowSpan.foregroundDecoration != null) { - final RenderBox leadingCell = getChildFor( - TableVicinity(column: leading.column, row: row), - )!; - final RenderBox trailingCell = getChildFor( - TableVicinity(column: trailing.column, row: row), - )!; + rowSpan.foregroundDecoration != null || _mergedRows.contains(row)) { + final List<(RenderBox, RenderBox)> decorationCells = <(RenderBox, RenderBox)>[]; + late RenderBox? leadingCell; + late RenderBox? trailingCell; + if (_mergedRows.isEmpty || !_mergedRows.contains(row)) { + // One decoration across the whole row. + decorationCells.add(( + getChildFor(TableVicinity(column: leading.column, row: row))!, // leading + getChildFor(TableVicinity(column: trailing.column, row: row))!, // trailing + )); + } else { + // Walk through the columns to separate merged rows for decorating. A + // merged row takes the decoration of its leading row. + int currentColumn = leading.column; + while (currentColumn <= trailing.column) { + TableVicinity vicinity = TableVicinity(column: currentColumn, row: row,); + leadingCell = getChildFor(vicinity) ?? _getMergedChildFor(vicinity); + if (parentDataOf(leadingCell).rowMergeStart != null) { + // Merged cell decorated individually. + decorationCells.add((leadingCell, leadingCell)); + currentColumn++; + continue; + } + RenderBox? nextCell = leadingCell; + while (nextCell != null && parentDataOf(nextCell).rowMergeStart == null) { + trailingCell = nextCell; + vicinity = vicinity.copyWith(column: currentColumn++); + nextCell = getChildFor(vicinity); + } + decorationCells.add((leadingCell, trailingCell!)); + currentColumn--; + } + } Rect getRowRect(bool consumePadding) { final ({double leading, double trailing}) offsetCorrection = @@ -1031,13 +1051,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ) : (leading: 0.0, trailing: 0.0); return Rect.fromPoints( - parentDataOf(leadingCell).paintOffset! + + parentDataOf(leadingCell!).paintOffset! + offset - Offset( columnSpan.padding.leading - offsetCorrection.leading, consumePadding ? rowSpan.padding.leading : 0.0, ), - parentDataOf(trailingCell).paintOffset! + + parentDataOf(trailingCell!).paintOffset! + offset + Offset(trailingCell.size.width, trailingCell.size.height) + Offset( @@ -1047,15 +1067,30 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ); } - if (rowSpan.backgroundDecoration != null) { - final Rect rect = - getRowRect(rowSpan.backgroundDecoration!.consumeSpanPadding); - backgroundRows[rect] = rowSpan.backgroundDecoration!; - } - if (rowSpan.foregroundDecoration != null) { - final Rect rect = - getRowRect(rowSpan.foregroundDecoration!.consumeSpanPadding); - foregroundRows[rect] = rowSpan.foregroundDecoration!; + for (final (RenderBox, RenderBox) span in decorationCells) { + (leadingCell, trailingCell) = span; + // If this was a merged cell, the decoration is defined by the leading + // cell, which may come from a different row. + final int rowIndex = parentDataOf(leadingCell).rowMergeStart ?? parentDataOf(leadingCell).tableVicinity.row; + rowSpan = _rowMetrics[rowIndex]!.configuration; + if (rowSpan.backgroundDecoration != null) { + final Rect rect = + getRowRect(rowSpan.backgroundDecoration!.consumeSpanPadding); + // We could have already added this rect if it came from a merged + // cell. + if (!backgroundRows.keys.contains(rect)) { + backgroundRows[rect] = rowSpan.backgroundDecoration!; + } + } + if (rowSpan.foregroundDecoration != null) { + final Rect rect = + getRowRect(rowSpan.foregroundDecoration!.consumeSpanPadding); + // We could have already added this rect if it came from a merged + // cell. + if (!foregroundRows.keys.contains(rect)) { + foregroundRows[rect] = rowSpan.foregroundDecoration!; + } + } } } } From 0d461c7be06884bd1c9d5be26b89a187c0483d84 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 10 Jan 2024 17:25:56 -0600 Subject: [PATCH 03/21] Stop before rework --- .../example/lib/main.dart | 61 ++++-- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../lib/src/table_view/table.dart | 182 ++++++++++++++---- .../test/table_view/table_test.dart | 8 +- 5 files changed, 189 insertions(+), 66 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index 713303a69694..aea6f87fb2ec 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -41,14 +41,22 @@ class TableExample extends StatefulWidget { } class _TableExampleState extends State { - final Map mergedRows= { + final Map mergedRows = { // TableVicinity in merged cell : (start, span) - TableVicinity.zero : (0, 2), + TableVicinity.zero: (0, 2), TableVicinity.zero.copyWith(row: 1): (0, 2), - const TableVicinity(row: 1, column: 1) : (1, 2), - const TableVicinity(row: 2, column: 1) : (1, 2), - const TableVicinity(row: 2, column: 2) : (2, 2), - const TableVicinity(row: 3, column: 2) : (2, 2), + const TableVicinity(row: 1, column: 1): (1, 2), + const TableVicinity(row: 2, column: 1): (1, 2), + const TableVicinity(row: 2, column: 2): (2, 2), + const TableVicinity(row: 3, column: 2): (2, 2), + }; + + final Map mergedColumns = { + // TableVicinity in merged cell : (start, span) + const TableVicinity(row: 0, column: 2) : (2, 2), + const TableVicinity(row: 0, column: 3) : (2, 2), + const TableVicinity(row: 3, column: 0) : (0, 2), + const TableVicinity(row: 3, column: 1) : (0, 2), }; @override @@ -68,10 +76,18 @@ class _TableExampleState extends State { } TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) { - if (mergedRows.keys.contains(vicinity)) { + if (mergedColumns.keys.contains(vicinity) || mergedRows.keys.contains(vicinity)) { + print('Vicinity $vicinity has: \n ' + '\t rowMergeStart: ${mergedRows[vicinity]?.$1}\n' + '\t rowMergeSpan: ${mergedRows[vicinity]?.$2}\n' + '\t columnMergeStart: ${mergedColumns[vicinity]?.$1}\n' + '\t columnMergeSpan: ${mergedColumns[vicinity]?.$2} ' + ); return TableViewCell( - rowMergeStart: mergedRows[vicinity]!.$1, - rowMergeSpan: mergedRows[vicinity]!.$2, + rowMergeStart: mergedRows[vicinity]?.$1, + rowMergeSpan: mergedRows[vicinity]?.$2, + // columnMergeStart: mergedColumns[vicinity]?.$1, + // columnMergeSpan: mergedColumns[vicinity]?.$2, child: const Center( child: Text('Merged'), ), @@ -87,19 +103,26 @@ class _TableExampleState extends State { TableSpan _buildRowSpan(int index) { late final Color color; - switch(index) { - case 1: color = Colors.purple; - case 2: color = Colors.blue; - case 3: color = Colors.green; - default: color = Colors.transparent; + switch (index) { + case 1: + color = Colors.purple; + case 2: + color = Colors.blue; + case 3: + color = Colors.green; + default: + color = Colors.transparent; } return TableSpan( extent: const FixedTableSpanExtent(100.0), - backgroundDecoration: TableSpanDecoration( - color: color, - border: const TableSpanBorder(leading: BorderSide(), trailing: BorderSide(),), - ), + // backgroundDecoration: TableSpanDecoration( + // color: color, + // border: const TableSpanBorder( + // leading: BorderSide(), + // trailing: BorderSide(), + // ), + // ), ); } @@ -107,7 +130,7 @@ class _TableExampleState extends State { return const TableSpan( extent: FixedTableSpanExtent(100.0), // foregroundDecoration: TableSpanDecoration( - // border: const TableSpanBorder(leading: BorderSide(), trailing: BorderSide(),), + // border: TableSpanBorder(leading: BorderSide(), trailing: BorderSide(),), // ), ); } diff --git a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj index 27e0f506b609..7d9ca676a43d 100644 --- a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj @@ -227,7 +227,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 397f3d339fde..15368eccb25a 100644 --- a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ _mergedRows = []; - // bool _mergedColumns = false; + final List _mergedColumns = []; // Cached Table metrics Map _columnMetrics = {}; @@ -610,7 +610,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // Reset for a new frame _mergedVicinities.clear(); _mergedRows.clear(); - // _mergedColumns = false; + _mergedColumns.clear(); if (needsDelegateRebuild || didResize) { // Recomputes the table metrics, invalidates any cached information. @@ -731,9 +731,12 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { double? mergedRowHeight; double? mergedYPaintOffset; yPaintOffset += rowSpan.configuration.padding.leading; + for (int column = start.column; column <= end.column; column += 1) { colSpan = _columnMetrics[column]!; - final double columnWidth = colSpan.extent; + final double standardColumnWidth = colSpan.extent; + double? mergedColumnWidth; + double? mergedXPaintOffset; xPaintOffset += colSpan.configuration.padding.leading; final TableVicinity vicinity = TableVicinity(column: column, row: row); @@ -747,7 +750,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (cell != null) { final TableViewParentData cellParentData = parentDataOf(cell); - // Merged cell handling + // Merged row handling if (cellParentData.rowMergeStart != null) { final int rowMergeStart = cellParentData.rowMergeStart!; _mergedRows.add(rowMergeStart); @@ -777,21 +780,56 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { nextRow++; } } - // TODO(Piinks): Copy logic for merged columns + + // Merged column handling. + if (cellParentData.columnMergeStart != null) { + final int columnMergeStart = cellParentData.columnMergeStart!; + _mergedColumns.add(columnMergeStart); + final int lastColumn = + columnMergeStart + cellParentData.columnMergeSpan! - 1; + assert(_debugCheckMergeBounds( + spanOrientation: 'Column', + currentSpan: column, + spanMergeStart: columnMergeStart, + spanMergeEnd: lastColumn, + spanCount: delegate.columnCount, + pinnedSpanCount: delegate.pinnedColumnCount, + currentVicinity: vicinity, + )); + // Compute width and layout offset for merged columns. + final _Span firstColumn = _columnMetrics[columnMergeStart]!; + mergedColumnWidth = firstColumn.extent; + mergedXPaintOffset = -horizontalOffset.pixels + + firstColumn.leadingOffset + + firstColumn.configuration.padding.leading; + _mergedVicinities[vicinity.copyWith(column: columnMergeStart)] = + vicinity; + int nextColumn = columnMergeStart + 1; + while (nextColumn <= lastColumn) { + _mergedColumns.add(nextColumn); + _mergedVicinities[vicinity.copyWith(column: nextColumn)] = + vicinity; + mergedColumnWidth = + mergedColumnWidth! + _columnMetrics[nextColumn]!.extent; + nextColumn++; + } + } final BoxConstraints cellConstraints = BoxConstraints.tightFor( - width: columnWidth, + width: mergedColumnWidth ?? standardColumnWidth, height: mergedRowHeight ?? standardRowHeight, ); cell.layout(cellConstraints); cellParentData.layoutOffset = Offset( - xPaintOffset, + mergedXPaintOffset ?? xPaintOffset, mergedYPaintOffset ?? yPaintOffset, ); mergedYPaintOffset = null; mergedRowHeight = null; + mergedXPaintOffset = null; + mergedColumnWidth = null; } - xPaintOffset += columnWidth + + xPaintOffset += standardColumnWidth + _columnMetrics[column]!.configuration.padding.trailing; } yPaintOffset += @@ -929,6 +967,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } RenderBox _getMergedChildFor(TableVicinity vicinity) { + // A merged cell spans multiple vicinities, but only lays out one child for + // the full area. Returns the child that has been laid out to span the given + // vicinity. assert(_mergedVicinities.keys.contains(vicinity)); final TableVicinity mergedVicinity = _mergedVicinities[vicinity]!; return getChildFor(mergedVicinity)!; @@ -940,6 +981,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { required TableVicinity trailing, required Offset offset, }) { + print('paint cells'); // Column decorations final LinkedHashMap foregroundColumns = LinkedHashMap(); @@ -948,15 +990,44 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final TableSpan rowSpan = _rowMetrics[leading.row]!.configuration; for (int column = leading.column; column <= trailing.column; column++) { - final TableSpan columnSpan = _columnMetrics[column]!.configuration; + TableSpan columnSpan = _columnMetrics[column]!.configuration; if (columnSpan.backgroundDecoration != null || - columnSpan.foregroundDecoration != null) { - final RenderBox leadingCell = getChildFor( - TableVicinity(column: column, row: leading.row), - )!; - final RenderBox trailingCell = getChildFor( - TableVicinity(column: column, row: trailing.row), - )!; + columnSpan.foregroundDecoration != null || _mergedColumns.contains(column)) { + final List<(RenderBox, RenderBox)> decorationCells = <(RenderBox, RenderBox)>[]; + late RenderBox? leadingCell; + late RenderBox? trailingCell; + if ((_mergedColumns.isEmpty || !_mergedColumns.contains(column)) && _mergedRows.isEmpty) { + // One decoration across the whole column. + decorationCells.add(( + getChildFor(TableVicinity(column: column, row: leading.row,))!, // leading + getChildFor(TableVicinity(column: column, row: trailing.row,))!, // trailing + )); + } else { + // Walk through the rows to separate merged cells for decorating. A + // merged column takes the decoration of its leading column. + int currentRow = leading.row; + print('column: $column trailing.row: ${trailing.row}'); + while (currentRow <= trailing.row) { + print('currentRow: $currentRow'); + TableVicinity vicinity = TableVicinity(column: column, row: currentRow,); + leadingCell = getChildFor(vicinity) ?? _getMergedChildFor(vicinity); + if (parentDataOf(leadingCell).columnMergeStart != null) { + // Merged cell decorated individually. + print('column $column is merged'); + decorationCells.add((leadingCell, leadingCell)); + currentRow ++; + continue; + } + RenderBox? nextCell = leadingCell; + while (nextCell != null && parentDataOf(nextCell).columnMergeStart == null) { + trailingCell = nextCell; + vicinity = vicinity.copyWith(row: currentRow++); + nextCell = getChildFor(vicinity); + } + decorationCells.add((leadingCell, trailingCell!)); + currentRow--; + } + } Rect getColumnRect(bool consumePadding) { final ({double leading, double trailing}) offsetCorrection = @@ -968,13 +1039,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { : (leading: 0.0, trailing: 0.0); return Rect.fromPoints( - parentDataOf(leadingCell).paintOffset! + + parentDataOf(leadingCell!).paintOffset! + offset - Offset( consumePadding ? columnSpan.padding.leading : 0.0, rowSpan.padding.leading - offsetCorrection.leading, ), - parentDataOf(trailingCell).paintOffset! + + parentDataOf(trailingCell!).paintOffset! + offset + Offset(trailingCell.size.width, trailingCell.size.height) + Offset( @@ -984,21 +1055,38 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ); } - if (columnSpan.backgroundDecoration != null) { - final Rect rect = getColumnRect( - columnSpan.backgroundDecoration!.consumeSpanPadding, - ); - backgroundColumns[rect] = columnSpan.backgroundDecoration!; - } - if (columnSpan.foregroundDecoration != null) { - final Rect rect = getColumnRect( - columnSpan.foregroundDecoration!.consumeSpanPadding, - ); - foregroundColumns[rect] = columnSpan.foregroundDecoration!; + for (final (RenderBox, RenderBox) span in decorationCells) { + (leadingCell, trailingCell) = span; + // If this was a merged cell, the decoration is defined by the leading + // cell, which may come from a different column. + final int columnIndex = parentDataOf(leadingCell).columnMergeStart ?? parentDataOf(leadingCell).tableVicinity.column; + columnSpan = _columnMetrics[columnIndex]!.configuration; + if (columnSpan.backgroundDecoration != null) { + final Rect rect = + getColumnRect(columnSpan.backgroundDecoration!.consumeSpanPadding); + // We could have already added this rect if it came from another + // vicinity contained by the merged + // cell. + if (!backgroundColumns.keys.contains(rect)) { + backgroundColumns[rect] = columnSpan.backgroundDecoration!; + } + } + if (columnSpan.foregroundDecoration != null) { + final Rect rect = + getColumnRect(columnSpan.foregroundDecoration!.consumeSpanPadding); + // We could have already added this rect if it came from another + // vicinity contained by the merged + // cell. + if (!foregroundColumns.keys.contains(rect)) { + foregroundColumns[rect] = columnSpan.foregroundDecoration!; + } + } } } } + print('columns sorted'); + // Row decorations final LinkedHashMap foregroundRows = LinkedHashMap(); @@ -1008,22 +1096,29 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { for (int row = leading.row; row <= trailing.row; row++) { TableSpan rowSpan = _rowMetrics[row]!.configuration; if (rowSpan.backgroundDecoration != null || - rowSpan.foregroundDecoration != null || _mergedRows.contains(row)) { - final List<(RenderBox, RenderBox)> decorationCells = <(RenderBox, RenderBox)>[]; + rowSpan.foregroundDecoration != null || + _mergedRows.contains(row)) { + final List<(RenderBox, RenderBox)> decorationCells = + <(RenderBox, RenderBox)>[]; late RenderBox? leadingCell; late RenderBox? trailingCell; if (_mergedRows.isEmpty || !_mergedRows.contains(row)) { // One decoration across the whole row. decorationCells.add(( - getChildFor(TableVicinity(column: leading.column, row: row))!, // leading - getChildFor(TableVicinity(column: trailing.column, row: row))!, // trailing + getChildFor( + TableVicinity(column: leading.column, row: row,))!, // leading + getChildFor( + TableVicinity(column: trailing.column, row: row,))!, // trailing )); } else { - // Walk through the columns to separate merged rows for decorating. A + // Walk through the columns to separate merged cells for decorating. A // merged row takes the decoration of its leading row. int currentColumn = leading.column; while (currentColumn <= trailing.column) { - TableVicinity vicinity = TableVicinity(column: currentColumn, row: row,); + TableVicinity vicinity = TableVicinity( + column: currentColumn, + row: row, + ); leadingCell = getChildFor(vicinity) ?? _getMergedChildFor(vicinity); if (parentDataOf(leadingCell).rowMergeStart != null) { // Merged cell decorated individually. @@ -1032,7 +1127,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { continue; } RenderBox? nextCell = leadingCell; - while (nextCell != null && parentDataOf(nextCell).rowMergeStart == null) { + while (nextCell != null && + parentDataOf(nextCell).rowMergeStart == null) { trailingCell = nextCell; vicinity = vicinity.copyWith(column: currentColumn++); nextCell = getChildFor(vicinity); @@ -1071,12 +1167,14 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { (leadingCell, trailingCell) = span; // If this was a merged cell, the decoration is defined by the leading // cell, which may come from a different row. - final int rowIndex = parentDataOf(leadingCell).rowMergeStart ?? parentDataOf(leadingCell).tableVicinity.row; + final int rowIndex = parentDataOf(leadingCell).rowMergeStart ?? + parentDataOf(leadingCell).tableVicinity.row; rowSpan = _rowMetrics[rowIndex]!.configuration; if (rowSpan.backgroundDecoration != null) { final Rect rect = - getRowRect(rowSpan.backgroundDecoration!.consumeSpanPadding); - // We could have already added this rect if it came from a merged + getRowRect(rowSpan.backgroundDecoration!.consumeSpanPadding); + // We could have already added this rect if it came from another + // vicinity contained by the merged // cell. if (!backgroundRows.keys.contains(rect)) { backgroundRows[rect] = rowSpan.backgroundDecoration!; @@ -1084,8 +1182,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } if (rowSpan.foregroundDecoration != null) { final Rect rect = - getRowRect(rowSpan.foregroundDecoration!.consumeSpanPadding); - // We could have already added this rect if it came from a merged + getRowRect(rowSpan.foregroundDecoration!.consumeSpanPadding); + // We could have already added this rect if it came from another + // vicinity contained by the merged // cell. if (!foregroundRows.keys.contains(rect)) { foregroundRows[rect] = rowSpan.foregroundDecoration!; @@ -1094,6 +1193,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } } } + print('rows sorted'); // Get to painting. // Painting is done in row or column major ordering according to the main diff --git a/packages/two_dimensional_scrollables/test/table_view/table_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_test.dart index 0e16a9a8a86d..7d59ad8aa573 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -59,7 +59,7 @@ void main() { expect( delegate.builder( _NullBuildContext(), - const TableVicinity(row: 0, column: 0), + TableVicinity.zero, ), cell, ); @@ -281,7 +281,7 @@ void main() { ); expect(viewport.mainAxis, Axis.vertical); // first child - TableVicinity vicinity = const TableVicinity(column: 0, row: 0); + TableVicinity vicinity = TableVicinity.zero; TableViewParentData parentData = parentDataOf( viewport.firstChild!, ); @@ -367,7 +367,7 @@ void main() { childKeys.values.first, ); // first child - TableVicinity vicinity = const TableVicinity(column: 0, row: 0); + TableVicinity vicinity = TableVicinity.zero; TableViewParentData parentData = parentDataOf( viewport.firstChild!, ); @@ -432,7 +432,7 @@ void main() { childKeys.values.first, ); // first child - vicinity = const TableVicinity(column: 0, row: 0); + vicinity = TableVicinity.zero; parentData = parentDataOf( viewport.firstChild!, ); From 3dd965869cf4a3937fac4182ff77fa4eb2ddbd26 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 10 Jan 2024 19:21:51 -0600 Subject: [PATCH 04/21] Got it. Need clean up and tests --- .../example/lib/main.dart | 62 +++++++++---------- .../lib/src/table_view/table.dart | 62 +++++++++++-------- .../lib/src/table_view/table_cell.dart | 15 +++++ 3 files changed, 81 insertions(+), 58 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index aea6f87fb2ec..0927c87f860d 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -43,20 +43,26 @@ class TableExample extends StatefulWidget { class _TableExampleState extends State { final Map mergedRows = { // TableVicinity in merged cell : (start, span) - TableVicinity.zero: (0, 2), - TableVicinity.zero.copyWith(row: 1): (0, 2), - const TableVicinity(row: 1, column: 1): (1, 2), - const TableVicinity(row: 2, column: 1): (1, 2), - const TableVicinity(row: 2, column: 2): (2, 2), - const TableVicinity(row: 3, column: 2): (2, 2), + // TableVicinity.zero : (0, 2), + // TableVicinity.zero.copyWith(row: 1) : (0, 2), + // const TableVicinity(row: 1, column: 1) : (1, 2), + // const TableVicinity(row: 2, column: 1) : (1, 2), + const TableVicinity(row: 2, column: 2) : (2, 2), + const TableVicinity(row: 2, column: 3) : (2, 2), + const TableVicinity(row: 3, column: 2) : (2, 2), + const TableVicinity(row: 3, column: 3) : (2, 2), }; final Map mergedColumns = { // TableVicinity in merged cell : (start, span) - const TableVicinity(row: 0, column: 2) : (2, 2), - const TableVicinity(row: 0, column: 3) : (2, 2), - const TableVicinity(row: 3, column: 0) : (0, 2), - const TableVicinity(row: 3, column: 1) : (0, 2), + // TableVicinity.zero.copyWith(column: 2) : (2, 2), + // TableVicinity.zero.copyWith(column: 3) : (2, 2), + // const TableVicinity(row: 3, column: 0) : (0, 2), + // const TableVicinity(row: 3, column: 1) : (0, 2), + const TableVicinity(row: 2, column: 2) : (2, 2), + const TableVicinity(row: 2, column: 3) : (2, 2), + const TableVicinity(row: 3, column: 2) : (2, 2), + const TableVicinity(row: 3, column: 3) : (2, 2), }; @override @@ -77,17 +83,11 @@ class _TableExampleState extends State { TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) { if (mergedColumns.keys.contains(vicinity) || mergedRows.keys.contains(vicinity)) { - print('Vicinity $vicinity has: \n ' - '\t rowMergeStart: ${mergedRows[vicinity]?.$1}\n' - '\t rowMergeSpan: ${mergedRows[vicinity]?.$2}\n' - '\t columnMergeStart: ${mergedColumns[vicinity]?.$1}\n' - '\t columnMergeSpan: ${mergedColumns[vicinity]?.$2} ' - ); return TableViewCell( rowMergeStart: mergedRows[vicinity]?.$1, rowMergeSpan: mergedRows[vicinity]?.$2, - // columnMergeStart: mergedColumns[vicinity]?.$1, - // columnMergeSpan: mergedColumns[vicinity]?.$2, + columnMergeStart: mergedColumns[vicinity]?.$1, + columnMergeSpan: mergedColumns[vicinity]?.$2, child: const Center( child: Text('Merged'), ), @@ -111,27 +111,27 @@ class _TableExampleState extends State { case 3: color = Colors.green; default: - color = Colors.transparent; + color = Colors.red; } return TableSpan( - extent: const FixedTableSpanExtent(100.0), - // backgroundDecoration: TableSpanDecoration( - // color: color, - // border: const TableSpanBorder( - // leading: BorderSide(), - // trailing: BorderSide(), - // ), - // ), + extent: const FixedTableSpanExtent(200.0), + backgroundDecoration: TableSpanDecoration( + color: color, + border: const TableSpanBorder( + leading: BorderSide(), + trailing: BorderSide(), + ), + ), ); } TableSpan _buildColumnSpan(int index) { return const TableSpan( - extent: FixedTableSpanExtent(100.0), - // foregroundDecoration: TableSpanDecoration( - // border: TableSpanBorder(leading: BorderSide(), trailing: BorderSide(),), - // ), + extent: FixedTableSpanExtent(200.0), + foregroundDecoration: TableSpanDecoration( + border: TableSpanBorder(leading: BorderSide(), trailing: BorderSide(),), + ), ); } } diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 9387caecbb32..1dcfafd8cd7a 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -966,6 +966,12 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } } + @override + RenderBox? getChildFor(ChildVicinity vicinity, { bool allowMerged = true, }) { + return super.getChildFor(vicinity) + ?? (allowMerged ? _getMergedChildFor(vicinity as TableVicinity) : null); + } + RenderBox _getMergedChildFor(TableVicinity vicinity) { // A merged cell spans multiple vicinities, but only lays out one child for // the full area. Returns the child that has been laid out to span the given @@ -981,7 +987,6 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { required TableVicinity trailing, required Offset offset, }) { - print('paint cells'); // Column decorations final LinkedHashMap foregroundColumns = LinkedHashMap(); @@ -996,7 +1001,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final List<(RenderBox, RenderBox)> decorationCells = <(RenderBox, RenderBox)>[]; late RenderBox? leadingCell; late RenderBox? trailingCell; - if ((_mergedColumns.isEmpty || !_mergedColumns.contains(column)) && _mergedRows.isEmpty) { + if (_mergedColumns.isEmpty || !_mergedColumns.contains(column)) { // One decoration across the whole column. decorationCells.add(( getChildFor(TableVicinity(column: column, row: leading.row,))!, // leading @@ -1006,26 +1011,29 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // Walk through the rows to separate merged cells for decorating. A // merged column takes the decoration of its leading column. int currentRow = leading.row; - print('column: $column trailing.row: ${trailing.row}'); while (currentRow <= trailing.row) { - print('currentRow: $currentRow'); TableVicinity vicinity = TableVicinity(column: column, row: currentRow,); - leadingCell = getChildFor(vicinity) ?? _getMergedChildFor(vicinity); - if (parentDataOf(leadingCell).columnMergeStart != null) { - // Merged cell decorated individually. - print('column $column is merged'); + leadingCell = getChildFor(vicinity); + if (parentDataOf(leadingCell!).columnMergeStart != null) { + // Merged portion decorated individually. decorationCells.add((leadingCell, leadingCell)); - currentRow ++; + currentRow++; continue; } RenderBox? nextCell = leadingCell; - while (nextCell != null && parentDataOf(nextCell).columnMergeStart == null) { + while (nextCell != null && + parentDataOf(nextCell).columnMergeStart == null) { + final TableViewParentData parentData = parentDataOf(nextCell); + if (parentData.rowMergeStart != null) { + currentRow = parentData.rowMergeStart! + parentData.rowMergeSpan!; + } else { + currentRow += 1; + } trailingCell = nextCell; - vicinity = vicinity.copyWith(row: currentRow++); - nextCell = getChildFor(vicinity); + vicinity = vicinity.copyWith(row: currentRow); + nextCell = getChildFor(vicinity, allowMerged: false); } decorationCells.add((leadingCell, trailingCell!)); - currentRow--; } } @@ -1085,8 +1093,6 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } } - print('columns sorted'); - // Row decorations final LinkedHashMap foregroundRows = LinkedHashMap(); @@ -1105,10 +1111,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (_mergedRows.isEmpty || !_mergedRows.contains(row)) { // One decoration across the whole row. decorationCells.add(( - getChildFor( - TableVicinity(column: leading.column, row: row,))!, // leading - getChildFor( - TableVicinity(column: trailing.column, row: row,))!, // trailing + getChildFor(TableVicinity(column: leading.column, row: row,))!, // leading + getChildFor(TableVicinity(column: trailing.column, row: row,))!, // trailing )); } else { // Walk through the columns to separate merged cells for decorating. A @@ -1119,9 +1123,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { column: currentColumn, row: row, ); - leadingCell = getChildFor(vicinity) ?? _getMergedChildFor(vicinity); - if (parentDataOf(leadingCell).rowMergeStart != null) { - // Merged cell decorated individually. + leadingCell = getChildFor(vicinity); + if (parentDataOf(leadingCell!).rowMergeStart != null) { + // Merged portion decorated individually. decorationCells.add((leadingCell, leadingCell)); currentColumn++; continue; @@ -1129,12 +1133,17 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { RenderBox? nextCell = leadingCell; while (nextCell != null && parentDataOf(nextCell).rowMergeStart == null) { + final TableViewParentData parentData = parentDataOf(nextCell); + if (parentData.columnMergeStart != null) { + currentColumn = parentData.columnMergeStart! + parentData.columnMergeSpan!; + } else { + currentColumn += 1; + } trailingCell = nextCell; - vicinity = vicinity.copyWith(column: currentColumn++); - nextCell = getChildFor(vicinity); + vicinity = vicinity.copyWith(column: currentColumn); + nextCell = getChildFor(vicinity, allowMerged: false); } decorationCells.add((leadingCell, trailingCell!)); - currentColumn--; } } @@ -1193,7 +1202,6 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } } } - print('rows sorted'); // Get to painting. // Painting is done in row or column major ordering according to the main @@ -1248,7 +1256,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { for (int column = leading.column; column <= trailing.column; column++) { for (int row = leading.row; row <= trailing.row; row++) { final TableVicinity vicinity = TableVicinity(column: column, row: row); - final RenderBox? cell = getChildFor(vicinity); + final RenderBox? cell = getChildFor(vicinity, allowMerged: false); if (cell == null) { // Covered by a merged cell assert(_mergedVicinities.keys.contains(vicinity)); diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart index 69ab5ffe39fe..f410759455f2 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart @@ -67,6 +67,21 @@ class TableViewParentData extends TwoDimensionalViewportParentData { /// int? columnMergeSpan; + + @override + String toString() { + String mergeDetails = ''; + if (rowMergeStart != null || columnMergeStart != null) { + mergeDetails += ', merged'; + } + if (rowMergeStart != null) { + mergeDetails += ', rowMergeStart=$rowMergeStart, rowMergeSpan=$rowMergeSpan'; + } + if (columnMergeStart != null) { + mergeDetails += ', columnMergeStart=$columnMergeStart, columnMergeSpan=$columnMergeSpan'; + } + return super.toString() + mergeDetails; + } } /// From 4960a25855afeb4dc23a843a0bf10249155814fe Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Fri, 12 Jan 2024 17:15:08 -0600 Subject: [PATCH 05/21] Optimize build calls --- .../example/.metadata | 25 +---- .../example/lib/main.dart | 51 +++++----- .../lib/src/table_view/table.dart | 93 ++++++++++--------- 3 files changed, 81 insertions(+), 88 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/.metadata b/packages/two_dimensional_scrollables/example/.metadata index 9b1076dd78ef..2835b141092e 100644 --- a/packages/two_dimensional_scrollables/example/.metadata +++ b/packages/two_dimensional_scrollables/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "f1fefa8315ccf7081343d50815809dc3c7d5f347" + revision: "3a4f5779c1372f29a2eb181aedd796dba7a720c8" channel: "[user-branch]" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - - platform: android - create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - - platform: ios - create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - - platform: linux - create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + create_revision: 3a4f5779c1372f29a2eb181aedd796dba7a720c8 + base_revision: 3a4f5779c1372f29a2eb181aedd796dba7a720c8 - platform: macos - create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - - platform: web - create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - - platform: windows - create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + create_revision: 3a4f5779c1372f29a2eb181aedd796dba7a720c8 + base_revision: 3a4f5779c1372f29a2eb181aedd796dba7a720c8 # User provided section diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index 0927c87f860d..ec8133306259 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -42,28 +42,28 @@ class TableExample extends StatefulWidget { class _TableExampleState extends State { final Map mergedRows = { - // TableVicinity in merged cell : (start, span) - // TableVicinity.zero : (0, 2), - // TableVicinity.zero.copyWith(row: 1) : (0, 2), - // const TableVicinity(row: 1, column: 1) : (1, 2), - // const TableVicinity(row: 2, column: 1) : (1, 2), - const TableVicinity(row: 2, column: 2) : (2, 2), - const TableVicinity(row: 2, column: 3) : (2, 2), - const TableVicinity(row: 3, column: 2) : (2, 2), - const TableVicinity(row: 3, column: 3) : (2, 2), + const TableVicinity(row: 0, column: 0) : (0, 3), + const TableVicinity(row: 1, column: 0) : (0, 3), + const TableVicinity(row: 2, column: 0) : (0, 3), + const TableVicinity(row: 0, column: 1) : (0, 3), + const TableVicinity(row: 1, column: 1) : (0, 3), + const TableVicinity(row: 2, column: 1) : (0, 3), + const TableVicinity(row: 0, column: 2) : (0, 3), + const TableVicinity(row: 1, column: 2) : (0, 3), + const TableVicinity(row: 2, column: 2) : (0, 3), }; - final Map mergedColumns = { - // TableVicinity in merged cell : (start, span) - // TableVicinity.zero.copyWith(column: 2) : (2, 2), - // TableVicinity.zero.copyWith(column: 3) : (2, 2), - // const TableVicinity(row: 3, column: 0) : (0, 2), - // const TableVicinity(row: 3, column: 1) : (0, 2), - const TableVicinity(row: 2, column: 2) : (2, 2), - const TableVicinity(row: 2, column: 3) : (2, 2), - const TableVicinity(row: 3, column: 2) : (2, 2), - const TableVicinity(row: 3, column: 3) : (2, 2), - }; + // final Map mergedColumns = { + // // TableVicinity in merged cell : (start, span) + // // TableVicinity.zero.copyWith(column: 2) : (2, 2), + // // TableVicinity.zero.copyWith(column: 3) : (2, 2), + // // const TableVicinity(row: 3, column: 0) : (0, 2), + // // const TableVicinity(row: 3, column: 1) : (0, 2), + // const TableVicinity(row: 2, column: 2) : (2, 2), + // const TableVicinity(row: 2, column: 3) : (2, 2), + // const TableVicinity(row: 3, column: 2) : (2, 2), + // const TableVicinity(row: 3, column: 3) : (2, 2), + // }; @override Widget build(BuildContext context) { @@ -73,21 +73,22 @@ class _TableExampleState extends State { ), body: TableView.builder( cellBuilder: _buildCell, - columnCount: 4, + columnCount: 3, columnBuilder: _buildColumnSpan, - rowCount: 4, + rowCount: 3, rowBuilder: _buildRowSpan, ), ); } TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) { - if (mergedColumns.keys.contains(vicinity) || mergedRows.keys.contains(vicinity)) { + print('build called'); + if (mergedRows.keys.contains(vicinity)) { // || mergedRows.keys.contains(vicinity)) { return TableViewCell( rowMergeStart: mergedRows[vicinity]?.$1, rowMergeSpan: mergedRows[vicinity]?.$2, - columnMergeStart: mergedColumns[vicinity]?.$1, - columnMergeSpan: mergedColumns[vicinity]?.$2, + columnMergeStart: mergedRows[vicinity]?.$1, + columnMergeSpan: mergedRows[vicinity]?.$2, child: const Center( child: Text('Merged'), ), diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 1dcfafd8cd7a..7e856ad59495 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -691,6 +691,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { required int pinnedSpanCount, required TableVicinity currentVicinity, }) { + if (spanMergeStart == spanMergeEnd) { + // Not merged + return true; + } + final String lowerSpanOrientation = spanOrientation.toLowerCase(); assert( spanMergeStart <= currentSpan, @@ -740,22 +745,23 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { xPaintOffset += colSpan.configuration.padding.leading; final TableVicinity vicinity = TableVicinity(column: column, row: row); - RenderBox? cell; - if (!_mergedVicinities.keys.contains(vicinity)) { - // We do not call build for vicinities that are already covered by a - // merged cell. - cell = buildOrObtainChildFor(vicinity); - } + final RenderBox? cell = _mergedVicinities.keys.contains(vicinity) ? null : buildOrObtainChildFor(vicinity); if (cell != null) { final TableViewParentData cellParentData = parentDataOf(cell); - // Merged row handling - if (cellParentData.rowMergeStart != null) { - final int rowMergeStart = cellParentData.rowMergeStart!; - _mergedRows.add(rowMergeStart); - final int lastRow = - rowMergeStart + cellParentData.rowMergeSpan! - 1; + // Merged cell handling + if (cellParentData.rowMergeStart != null || cellParentData.columnMergeStart != null) { + late final int rowMergeStart; + late final int lastRow; + if (cellParentData.rowMergeStart != null) { + rowMergeStart = cellParentData.rowMergeStart!; + lastRow = rowMergeStart + cellParentData.rowMergeSpan! - 1; + _mergedRows.add(rowMergeStart); + } else { + rowMergeStart = row; + lastRow = row; + } assert(_debugCheckMergeBounds( spanOrientation: 'Row', currentSpan: row, @@ -765,28 +771,17 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { pinnedSpanCount: delegate.pinnedRowCount, currentVicinity: vicinity, )); - // Compute height and layout offset for merged rows. - final _Span firstRow = _rowMetrics[rowMergeStart]!; - mergedRowHeight = firstRow.extent; - mergedYPaintOffset = -verticalOffset.pixels + - firstRow.leadingOffset + - firstRow.configuration.padding.leading; - _mergedVicinities[vicinity.copyWith(row: rowMergeStart)] = vicinity; - int nextRow = rowMergeStart + 1; - while (nextRow <= lastRow) { - _mergedRows.add(nextRow); - _mergedVicinities[vicinity.copyWith(row: nextRow)] = vicinity; - mergedRowHeight = mergedRowHeight! + _rowMetrics[nextRow]!.extent; - nextRow++; - } - } - // Merged column handling. - if (cellParentData.columnMergeStart != null) { - final int columnMergeStart = cellParentData.columnMergeStart!; - _mergedColumns.add(columnMergeStart); - final int lastColumn = - columnMergeStart + cellParentData.columnMergeSpan! - 1; + late final int columnMergeStart; + late final int lastColumn; + if (cellParentData.columnMergeStart != null) { + columnMergeStart = cellParentData.columnMergeStart!; + lastColumn = columnMergeStart + cellParentData.columnMergeSpan! - 1; + _mergedColumns.add(columnMergeStart); + } else { + columnMergeStart = column; + lastColumn = column; + } assert(_debugCheckMergeBounds( spanOrientation: 'Column', currentSpan: column, @@ -796,22 +791,34 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { pinnedSpanCount: delegate.pinnedColumnCount, currentVicinity: vicinity, )); + + // Compute height and layout offset for merged rows. + final _Span firstRow = _rowMetrics[rowMergeStart]!; + mergedRowHeight = firstRow.extent; + mergedYPaintOffset = -verticalOffset.pixels + + firstRow.leadingOffset + + firstRow.configuration.padding.leading; // Compute width and layout offset for merged columns. final _Span firstColumn = _columnMetrics[columnMergeStart]!; mergedColumnWidth = firstColumn.extent; mergedXPaintOffset = -horizontalOffset.pixels + firstColumn.leadingOffset + firstColumn.configuration.padding.leading; - _mergedVicinities[vicinity.copyWith(column: columnMergeStart)] = - vicinity; - int nextColumn = columnMergeStart + 1; - while (nextColumn <= lastColumn) { - _mergedColumns.add(nextColumn); - _mergedVicinities[vicinity.copyWith(column: nextColumn)] = - vicinity; - mergedColumnWidth = - mergedColumnWidth! + _columnMetrics[nextColumn]!.extent; - nextColumn++; + + int currentRow = rowMergeStart; + while (currentRow <= lastRow) { + _mergedRows.add(currentRow); + mergedRowHeight = mergedRowHeight! + _rowMetrics[currentRow]!.extent; + int currentColumn = columnMergeStart; + while (currentColumn <= lastColumn) { + _mergedColumns.add(currentColumn); + mergedColumnWidth = + mergedColumnWidth! + _columnMetrics[currentColumn]!.extent; + _mergedVicinities[TableVicinity(row: currentRow, column: currentColumn,)] = + vicinity; + currentColumn++; + } + currentRow++; } } From dd9f4c34c9fc560452dbc3291e4e8d163d9fc983 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Fri, 12 Jan 2024 17:28:08 -0600 Subject: [PATCH 06/21] ++ --- .../example/.metadata | 25 ++- .../example/lib/main.dart | 188 +++++++++++------- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../lib/src/table_view/table.dart | 76 ++++--- .../lib/src/table_view/table_cell.dart | 6 +- 6 files changed, 198 insertions(+), 101 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/.metadata b/packages/two_dimensional_scrollables/example/.metadata index 2835b141092e..9b1076dd78ef 100644 --- a/packages/two_dimensional_scrollables/example/.metadata +++ b/packages/two_dimensional_scrollables/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "3a4f5779c1372f29a2eb181aedd796dba7a720c8" + revision: "f1fefa8315ccf7081343d50815809dc3c7d5f347" channel: "[user-branch]" project_type: app @@ -13,11 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: 3a4f5779c1372f29a2eb181aedd796dba7a720c8 - base_revision: 3a4f5779c1372f29a2eb181aedd796dba7a720c8 + create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + - platform: android + create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + - platform: ios + create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + - platform: linux + create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 - platform: macos - create_revision: 3a4f5779c1372f29a2eb181aedd796dba7a720c8 - base_revision: 3a4f5779c1372f29a2eb181aedd796dba7a720c8 + create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + - platform: web + create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + - platform: windows + create_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 + base_revision: f1fefa8315ccf7081343d50815809dc3c7d5f347 # User provided section diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index ec8133306259..cd23c568ae62 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -41,29 +41,8 @@ class TableExample extends StatefulWidget { } class _TableExampleState extends State { - final Map mergedRows = { - const TableVicinity(row: 0, column: 0) : (0, 3), - const TableVicinity(row: 1, column: 0) : (0, 3), - const TableVicinity(row: 2, column: 0) : (0, 3), - const TableVicinity(row: 0, column: 1) : (0, 3), - const TableVicinity(row: 1, column: 1) : (0, 3), - const TableVicinity(row: 2, column: 1) : (0, 3), - const TableVicinity(row: 0, column: 2) : (0, 3), - const TableVicinity(row: 1, column: 2) : (0, 3), - const TableVicinity(row: 2, column: 2) : (0, 3), - }; - - // final Map mergedColumns = { - // // TableVicinity in merged cell : (start, span) - // // TableVicinity.zero.copyWith(column: 2) : (2, 2), - // // TableVicinity.zero.copyWith(column: 3) : (2, 2), - // // const TableVicinity(row: 3, column: 0) : (0, 2), - // // const TableVicinity(row: 3, column: 1) : (0, 2), - // const TableVicinity(row: 2, column: 2) : (2, 2), - // const TableVicinity(row: 2, column: 3) : (2, 2), - // const TableVicinity(row: 3, column: 2) : (2, 2), - // const TableVicinity(row: 3, column: 3) : (2, 2), - // }; + late final ScrollController _verticalController = ScrollController(); + int _rowCount = 20; @override Widget build(BuildContext context) { @@ -71,68 +50,139 @@ class _TableExampleState extends State { appBar: AppBar( title: const Text('Table Example'), ), - body: TableView.builder( - cellBuilder: _buildCell, - columnCount: 3, - columnBuilder: _buildColumnSpan, - rowCount: 3, - rowBuilder: _buildRowSpan, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 50), + child: TableView.builder( + verticalDetails: + ScrollableDetails.vertical(controller: _verticalController), + cellBuilder: _buildCell, + columnCount: 20, + columnBuilder: _buildColumnSpan, + rowCount: _rowCount, + rowBuilder: _buildRowSpan, + ), ), + persistentFooterButtons: [ + TextButton( + onPressed: () { + _verticalController.jumpTo(0); + }, + child: const Text('Jump to Top'), + ), + TextButton( + onPressed: () { + _verticalController + .jumpTo(_verticalController.position.maxScrollExtent); + }, + child: const Text('Jump to Bottom'), + ), + TextButton( + onPressed: () { + setState(() { + _rowCount += 10; + }); + }, + child: const Text('Add 10 Rows'), + ), + ], ); } - TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) { - print('build called'); - if (mergedRows.keys.contains(vicinity)) { // || mergedRows.keys.contains(vicinity)) { - return TableViewCell( - rowMergeStart: mergedRows[vicinity]?.$1, - rowMergeSpan: mergedRows[vicinity]?.$2, - columnMergeStart: mergedRows[vicinity]?.$1, - columnMergeSpan: mergedRows[vicinity]?.$2, - child: const Center( - child: Text('Merged'), - ), - ); - } + Widget _buildCell(BuildContext context, TableVicinity vicinity) { + return Center( + child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), + ); + } - return TableViewCell( - child: Center( - child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), + TableSpan _buildColumnSpan(int index) { + const TableSpanDecoration decoration = TableSpanDecoration( + border: TableSpanBorder( + trailing: BorderSide(), ), ); - } - TableSpan _buildRowSpan(int index) { - late final Color color; - switch (index) { + switch (index % 5) { + case 0: + return TableSpan( + foregroundDecoration: decoration, + extent: const FixedTableSpanExtent(100), + onEnter: (_) => print('Entered column $index'), + recognizerFactories: { + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (TapGestureRecognizer t) => + t.onTap = () => print('Tap column $index'), + ), + }, + ); case 1: - color = Colors.purple; + return TableSpan( + foregroundDecoration: decoration, + extent: const FractionalTableSpanExtent(0.5), + onEnter: (_) => print('Entered column $index'), + cursor: SystemMouseCursors.contextMenu, + ); case 2: - color = Colors.blue; + return TableSpan( + foregroundDecoration: decoration, + extent: const FixedTableSpanExtent(120), + onEnter: (_) => print('Entered column $index'), + ); case 3: - color = Colors.green; - default: - color = Colors.red; + return TableSpan( + foregroundDecoration: decoration, + extent: const FixedTableSpanExtent(145), + onEnter: (_) => print('Entered column $index'), + ); + case 4: + return TableSpan( + foregroundDecoration: decoration, + extent: const FixedTableSpanExtent(200), + onEnter: (_) => print('Entered column $index'), + ); } + throw AssertionError( + 'This should be unreachable, as every index is accounted for in the switch clauses.'); + } - return TableSpan( - extent: const FixedTableSpanExtent(200.0), - backgroundDecoration: TableSpanDecoration( - color: color, - border: const TableSpanBorder( - leading: BorderSide(), - trailing: BorderSide(), + TableSpan _buildRowSpan(int index) { + final TableSpanDecoration decoration = TableSpanDecoration( + color: index.isEven ? Colors.purple[100] : null, + border: const TableSpanBorder( + trailing: BorderSide( + width: 3, ), ), ); - } - TableSpan _buildColumnSpan(int index) { - return const TableSpan( - extent: FixedTableSpanExtent(200.0), - foregroundDecoration: TableSpanDecoration( - border: TableSpanBorder(leading: BorderSide(), trailing: BorderSide(),), - ), - ); + switch (index % 3) { + case 0: + return TableSpan( + backgroundDecoration: decoration, + extent: const FixedTableSpanExtent(50), + recognizerFactories: { + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (TapGestureRecognizer t) => + t.onTap = () => print('Tap row $index'), + ), + }, + ); + case 1: + return TableSpan( + backgroundDecoration: decoration, + extent: const FixedTableSpanExtent(65), + cursor: SystemMouseCursors.click, + ); + case 2: + return TableSpan( + backgroundDecoration: decoration, + extent: const FractionalTableSpanExtent(0.15), + ); + } + throw AssertionError( + 'This should be unreachable, as every index is accounted for in the switch clauses.'); } } diff --git a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj index 7d9ca676a43d..27e0f506b609 100644 --- a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj @@ -227,7 +227,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15368eccb25a..397f3d339fde 100644 --- a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ decorationCells = <(RenderBox, RenderBox)>[]; + columnSpan.foregroundDecoration != null || + _mergedColumns.contains(column)) { + final List<(RenderBox, RenderBox)> decorationCells = + <(RenderBox, RenderBox)>[]; late RenderBox? leadingCell; late RenderBox? trailingCell; if (_mergedColumns.isEmpty || !_mergedColumns.contains(column)) { // One decoration across the whole column. decorationCells.add(( - getChildFor(TableVicinity(column: column, row: leading.row,))!, // leading - getChildFor(TableVicinity(column: column, row: trailing.row,))!, // trailing + getChildFor(TableVicinity( + column: column, + row: leading.row, + ))!, // leading + getChildFor(TableVicinity( + column: column, + row: trailing.row, + ))!, // trailing )); } else { // Walk through the rows to separate merged cells for decorating. A // merged column takes the decoration of its leading column. int currentRow = leading.row; while (currentRow <= trailing.row) { - TableVicinity vicinity = TableVicinity(column: column, row: currentRow,); + TableVicinity vicinity = TableVicinity( + column: column, + row: currentRow, + ); leadingCell = getChildFor(vicinity); if (parentDataOf(leadingCell!).columnMergeStart != null) { // Merged portion decorated individually. @@ -1032,7 +1053,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { parentDataOf(nextCell).columnMergeStart == null) { final TableViewParentData parentData = parentDataOf(nextCell); if (parentData.rowMergeStart != null) { - currentRow = parentData.rowMergeStart! + parentData.rowMergeSpan!; + currentRow = + parentData.rowMergeStart! + parentData.rowMergeSpan!; } else { currentRow += 1; } @@ -1074,11 +1096,12 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { (leadingCell, trailingCell) = span; // If this was a merged cell, the decoration is defined by the leading // cell, which may come from a different column. - final int columnIndex = parentDataOf(leadingCell).columnMergeStart ?? parentDataOf(leadingCell).tableVicinity.column; + final int columnIndex = parentDataOf(leadingCell).columnMergeStart ?? + parentDataOf(leadingCell).tableVicinity.column; columnSpan = _columnMetrics[columnIndex]!.configuration; if (columnSpan.backgroundDecoration != null) { - final Rect rect = - getColumnRect(columnSpan.backgroundDecoration!.consumeSpanPadding); + final Rect rect = getColumnRect( + columnSpan.backgroundDecoration!.consumeSpanPadding); // We could have already added this rect if it came from another // vicinity contained by the merged // cell. @@ -1087,8 +1110,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } } if (columnSpan.foregroundDecoration != null) { - final Rect rect = - getColumnRect(columnSpan.foregroundDecoration!.consumeSpanPadding); + final Rect rect = getColumnRect( + columnSpan.foregroundDecoration!.consumeSpanPadding); // We could have already added this rect if it came from another // vicinity contained by the merged // cell. @@ -1118,8 +1141,14 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (_mergedRows.isEmpty || !_mergedRows.contains(row)) { // One decoration across the whole row. decorationCells.add(( - getChildFor(TableVicinity(column: leading.column, row: row,))!, // leading - getChildFor(TableVicinity(column: trailing.column, row: row,))!, // trailing + getChildFor(TableVicinity( + column: leading.column, + row: row, + ))!, // leading + getChildFor(TableVicinity( + column: trailing.column, + row: row, + ))!, // trailing )); } else { // Walk through the columns to separate merged cells for decorating. A @@ -1142,7 +1171,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { parentDataOf(nextCell).rowMergeStart == null) { final TableViewParentData parentData = parentDataOf(nextCell); if (parentData.columnMergeStart != null) { - currentColumn = parentData.columnMergeStart! + parentData.columnMergeSpan!; + currentColumn = + parentData.columnMergeStart! + parentData.columnMergeSpan!; } else { currentColumn += 1; } diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart index f410759455f2..9042d77d3b8b 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart @@ -75,10 +75,12 @@ class TableViewParentData extends TwoDimensionalViewportParentData { mergeDetails += ', merged'; } if (rowMergeStart != null) { - mergeDetails += ', rowMergeStart=$rowMergeStart, rowMergeSpan=$rowMergeSpan'; + mergeDetails += + ', rowMergeStart=$rowMergeStart, rowMergeSpan=$rowMergeSpan'; } if (columnMergeStart != null) { - mergeDetails += ', columnMergeStart=$columnMergeStart, columnMergeSpan=$columnMergeSpan'; + mergeDetails += + ', columnMergeStart=$columnMergeStart, columnMergeSpan=$columnMergeSpan'; } return super.toString() + mergeDetails; } From ab812f7726f847c433949dd1e1c0870dd354b01c Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Fri, 12 Jan 2024 18:16:44 -0600 Subject: [PATCH 07/21] ++ --- .../two_dimensional_scrollables/lib/src/table_view/table.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 76335d48938e..2ee1ea7e97e3 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -288,7 +288,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final Map _mergedVicinities = {}; // Used to optimize decorating when there are no merged cells in a given - // frame. + // span. final List _mergedRows = []; final List _mergedColumns = []; @@ -809,6 +809,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { firstColumn.leadingOffset + firstColumn.configuration.padding.leading; + // Collect all of the vicinities that will not need to be built now. int currentRow = rowMergeStart; while (currentRow <= lastRow) { _mergedRows.add(currentRow); From 629eb7f61d2aeaecf1527dc1c5947749591f58f8 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 16 Jan 2024 14:18:12 -0600 Subject: [PATCH 08/21] Update docs --- .../two_dimensional_scrollables/CHANGELOG.md | 4 ++ .../example/lib/main.dart | 8 ++- .../lib/src/table_view/table.dart | 56 +++++++++---------- .../lib/src/table_view/table_cell.dart | 42 ++++++++++++-- .../lib/src/table_view/table_delegate.dart | 14 ++++- .../lib/src/table_view/table_span.dart | 3 + .../two_dimensional_scrollables/pubspec.yaml | 2 +- 7 files changed, 90 insertions(+), 39 deletions(-) diff --git a/packages/two_dimensional_scrollables/CHANGELOG.md b/packages/two_dimensional_scrollables/CHANGELOG.md index 6b04d1bf63c3..52ea3b087a7f 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.0 + +* [Breaking change] Adds support for merged cells in the TableView. + ## 0.0.6 * Fixes an error in TableSpanDecoration when one or both axes are reversed. diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index cd23c568ae62..ff714af1e497 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -88,9 +88,11 @@ class _TableExampleState extends State { ); } - Widget _buildCell(BuildContext context, TableVicinity vicinity) { - return Center( - child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), + TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) { + return TableViewCell( + child: Center( + child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), + ), ); } diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 2ee1ea7e97e3..976c87b55042 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -1036,6 +1036,16 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } else { // Walk through the rows to separate merged cells for decorating. A // merged column takes the decoration of its leading column. + // +---------+-------+-------+ + // | leading | | | + // | 1 rect | | | + // +---------+-------+-------+ + // | merged | | + // | 1 rect | | + // +---------+-------+-------+ + // | 1 rect | | | + // | | | | + // +---------+-------+-------+ int currentRow = leading.row; while (currentRow <= trailing.row) { TableVicinity vicinity = TableVicinity( @@ -1102,23 +1112,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { columnSpan = _columnMetrics[columnIndex]!.configuration; if (columnSpan.backgroundDecoration != null) { final Rect rect = getColumnRect( - columnSpan.backgroundDecoration!.consumeSpanPadding); - // We could have already added this rect if it came from another - // vicinity contained by the merged - // cell. - if (!backgroundColumns.keys.contains(rect)) { - backgroundColumns[rect] = columnSpan.backgroundDecoration!; - } + columnSpan.backgroundDecoration!.consumeSpanPadding,); + backgroundColumns[rect] = columnSpan.backgroundDecoration!; } if (columnSpan.foregroundDecoration != null) { final Rect rect = getColumnRect( - columnSpan.foregroundDecoration!.consumeSpanPadding); - // We could have already added this rect if it came from another - // vicinity contained by the merged - // cell. - if (!foregroundColumns.keys.contains(rect)) { - foregroundColumns[rect] = columnSpan.foregroundDecoration!; - } + columnSpan.foregroundDecoration!.consumeSpanPadding,); + foregroundColumns[rect] = columnSpan.foregroundDecoration!; } } } @@ -1154,6 +1154,16 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } else { // Walk through the columns to separate merged cells for decorating. A // merged row takes the decoration of its leading row. + // +---------+--------+--------+ + // | leading | merged | 1 rect | + // | 1 rect | 1 rect | | + // +---------+ +--------+ + // | | | | + // | | | | + // +---------+--------+--------+ + // | | | | + // | | | | + // +---------+--------+--------+ int currentColumn = leading.column; while (currentColumn <= trailing.column) { TableVicinity vicinity = TableVicinity( @@ -1219,23 +1229,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { rowSpan = _rowMetrics[rowIndex]!.configuration; if (rowSpan.backgroundDecoration != null) { final Rect rect = - getRowRect(rowSpan.backgroundDecoration!.consumeSpanPadding); - // We could have already added this rect if it came from another - // vicinity contained by the merged - // cell. - if (!backgroundRows.keys.contains(rect)) { - backgroundRows[rect] = rowSpan.backgroundDecoration!; - } + getRowRect(rowSpan.backgroundDecoration!.consumeSpanPadding,); + backgroundRows[rect] = rowSpan.backgroundDecoration!; } if (rowSpan.foregroundDecoration != null) { final Rect rect = - getRowRect(rowSpan.foregroundDecoration!.consumeSpanPadding); - // We could have already added this rect if it came from another - // vicinity contained by the merged - // cell. - if (!foregroundRows.keys.contains(rect)) { - foregroundRows[rect] = rowSpan.foregroundDecoration!; - } + getRowRect(rowSpan.foregroundDecoration!.consumeSpanPadding,); + foregroundRows[rect] = rowSpan.foregroundDecoration!; } } } diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart index 9042d77d3b8b..5cb88e3a1e57 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart @@ -56,16 +56,28 @@ class TableViewParentData extends TwoDimensionalViewportParentData { /// Converts the [ChildVicinity] to a [TableVicinity] for ease of use. TableVicinity get tableVicinity => vicinity as TableVicinity; + /// Represents the row index where a merged cell in the table begins. /// + /// Defaults to null, meaning a non-merged cell. A value must be provided if + /// a value is provided for [rowMergeSpan]. int? rowMergeStart; + /// Represents the number of rows spanned by a merged cell. /// + /// Defaults to null, meaning the cell is not merged. A value must be provided + /// if a value is provided for [rowMergeStart]. int? rowMergeSpan; + /// Represents the column index where a merged cell in the table begins. /// + /// Defaults to null, meaning a non-merged cell. A value must be provided if + /// a value is provided for [columnMergeSpan]. int? columnMergeStart; + /// Represents the number of columns spanned by a merged cell. /// + /// Defaults to null, meaning the cell is not merged. A value must be provided + /// if a value is provided for [columnMergeStart]. int? columnMergeSpan; @override @@ -75,12 +87,12 @@ class TableViewParentData extends TwoDimensionalViewportParentData { mergeDetails += ', merged'; } if (rowMergeStart != null) { - mergeDetails += - ', rowMergeStart=$rowMergeStart, rowMergeSpan=$rowMergeSpan'; + mergeDetails += ', rowMergeStart=$rowMergeStart, ' + 'rowMergeSpan=$rowMergeSpan'; } if (columnMergeStart != null) { - mergeDetails += - ', columnMergeStart=$columnMergeStart, columnMergeSpan=$columnMergeSpan'; + mergeDetails += ', columnMergeStart=$columnMergeStart, ' + 'columnMergeSpan=$columnMergeSpan'; } return super.toString() + mergeDetails; } @@ -109,22 +121,42 @@ class TableViewCell extends StatelessWidget { 'Column merge start and span must both be set, or both unset.', ); - /// + /// The child contained in this cell of the [TableView]. final Widget child; + /// Represents the row index where a merged cell in the table begins. /// + /// Defaults to null, meaning a non-merged cell. A value must be provided if + /// a value is provided for [rowMergeSpan]. final int? rowMergeStart; + /// Represents the number of rows spanned by a merged cell. /// + /// Defaults to null, meaning the cell is not merged. A value must be provided + /// if a value is provided for [rowMergeStart]. final int? rowMergeSpan; + /// Represents the column index where a merged cell in the table begins. /// + /// Defaults to null, meaning a non-merged cell. A value must be provided if + /// a value is provided for [columnMergeSpan]. final int? columnMergeStart; + /// Represents the number of columns spanned by a merged cell. /// + /// Defaults to null, meaning the cell is not merged. A value must be provided + /// if a value is provided for [columnMergeStart]. final int? columnMergeSpan; + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and instead always repaint the children during scrolling. /// + /// Defaults to true. final bool addRepaintBoundaries; @override diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart index de8453d0d0d1..bca0f9a8f8dd 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart @@ -116,6 +116,11 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// A delegate that supplies children for a [TableViewport] on demand using a /// builder callback. +/// +/// The [addRepaintBoundaries] of the super class is overridden to false here. +/// This is handled by [TableViewCell.addRepaintBoundaries]. This allows +/// [ParentData] to be written to children of the [TableView] for features like +/// merged cells. class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate with TableCellDelegateMixin { /// Creates a lazy building delegate to use with a [TableView]. @@ -143,7 +148,7 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate cellBuilder(context, vicinity as TableVicinity), maxXIndex: columnCount - 1, maxYIndex: rowCount - 1, - addRepaintBoundaries: false, + addRepaintBoundaries: false, // repaintBoundaries handled by TableViewCell ); @override @@ -209,6 +214,11 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate /// The [children] are accessed for each [TableVicinity.row] and /// [TableVicinity.column] of the [TwoDimensionalViewport] as /// `children[vicinity.row][vicinity.column]`. +/// +/// The [addRepaintBoundaries] of the super class is overridden to false here. +/// This is handled by [TableViewCell.addRepaintBoundaries]. This allows +/// [ParentData] to be written to children of the [TableView] for features like +/// merged cells. class TableCellListDelegate extends TwoDimensionalChildListDelegate with TableCellDelegateMixin { /// Creates a delegate that supplies children for a [TableView]. @@ -227,7 +237,7 @@ class TableCellListDelegate extends TwoDimensionalChildListDelegate _pinnedRowCount = pinnedRowCount, super( children: cells, - addRepaintBoundaries: false, + addRepaintBoundaries: false, // repaintBoundaries handled by TableViewCell ) { // Even if there are merged cells, they should be represented by the same // child in each cell location. This ensures that no matter which direction diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart index faac24b22d7d..e19a0f899b00 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart @@ -291,6 +291,9 @@ class MinTableSpanExtent extends CombiningTableSpanExtent { } /// A decoration for a [TableSpan]. +/// +/// When decorating merged cells in the [TableView], a merged cell will take its +/// decoration from the leading cell of the merged span. class TableSpanDecoration { /// Creates a [TableSpanDecoration]. const TableSpanDecoration({ diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index 5d1ede3d73f5..e67277843dee 100644 --- a/packages/two_dimensional_scrollables/pubspec.yaml +++ b/packages/two_dimensional_scrollables/pubspec.yaml @@ -1,6 +1,6 @@ name: two_dimensional_scrollables description: Widgets that scroll using the two dimensional scrolling foundation. -version: 0.0.6 +version: 0.1.0 repository: https://github.com/flutter/packages/tree/main/packages/two_dimensional_scrollables issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+two_dimensional_scrollables%22+ From bd84020a1e57b4f36910ed5797d4646fc5d7d797 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 16 Jan 2024 18:15:54 -0600 Subject: [PATCH 09/21] Found bug, ugh --- .../lib/src/table_view/table.dart | 44 +- .../lib/src/table_view/table_cell.dart | 32 +- .../lib/src/table_view/table_delegate.dart | 6 +- .../test/table_view/table_cell_test.dart | 437 +++++++++++++++++- .../test/table_view/table_delegate_test.dart | 7 +- 5 files changed, 497 insertions(+), 29 deletions(-) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 976c87b55042..01c2eae61795 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -24,10 +24,16 @@ import 'table_span.dart'; /// vertically. If there is not enough space for all the columns, it will /// scroll horizontally. /// -/// Each child [Widget] can belong to exactly one row and one column as -/// represented by its [TableVicinity]. The table supports lazy rendering and -/// will only instantiate those cells that are currently visible in the table's -/// viewport and those that extend into the [cacheExtent]. +/// Each child [TableViewCell] can belong to exactly one row and one column as +/// represented by its [TableVicinity], or it can span multiple rows and columns +/// through merging. The table supports lazy rendering and will only instantiate +/// those cells that are currently visible in the table's viewport and those +/// that extend into the [cacheExtent]. Therefore, when merging cells in a +/// [TableView], the same child should be returned from every vicinity the +/// merged cell contains. The `build` method will only be called once for a +/// merged cell, but since the table's children are lazily laid out, returning +/// the same child ensures the merged cell can be built no matter which part of +/// it is visible. /// /// The layout of the table (e.g. how many rows/columns there are and their /// extents) as well as the content of the individual cells is defined by @@ -751,6 +757,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (cell != null) { final TableViewParentData cellParentData = parentDataOf(cell); + if (vicinity == TableVicinity.zero) { + print(cellParentData); + } // Merged cell handling if (cellParentData.rowMergeStart != null || @@ -817,6 +826,12 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { mergedRowHeight! + _rowMetrics[currentRow]!.extent; int currentColumn = columnMergeStart; while (currentColumn <= lastColumn) { + if (vicinity == TableVicinity.zero) { + print(TableVicinity( + row: currentRow, + column: currentColumn, + )); + } _mergedColumns.add(currentColumn); mergedColumnWidth = mergedColumnWidth! + _columnMetrics[currentColumn]!.extent; @@ -830,6 +845,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } } + if (vicinity == TableVicinity.zero) { + print('mergedColumnWidth $mergedColumnWidth'); + print('mergedRowHeight $mergedRowHeight'); + } + final BoxConstraints cellConstraints = BoxConstraints.tightFor( width: mergedColumnWidth ?? standardColumnWidth, height: mergedRowHeight ?? standardRowHeight, @@ -1112,12 +1132,14 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { columnSpan = _columnMetrics[columnIndex]!.configuration; if (columnSpan.backgroundDecoration != null) { final Rect rect = getColumnRect( - columnSpan.backgroundDecoration!.consumeSpanPadding,); + columnSpan.backgroundDecoration!.consumeSpanPadding, + ); backgroundColumns[rect] = columnSpan.backgroundDecoration!; } if (columnSpan.foregroundDecoration != null) { final Rect rect = getColumnRect( - columnSpan.foregroundDecoration!.consumeSpanPadding,); + columnSpan.foregroundDecoration!.consumeSpanPadding, + ); foregroundColumns[rect] = columnSpan.foregroundDecoration!; } } @@ -1228,13 +1250,15 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { parentDataOf(leadingCell).tableVicinity.row; rowSpan = _rowMetrics[rowIndex]!.configuration; if (rowSpan.backgroundDecoration != null) { - final Rect rect = - getRowRect(rowSpan.backgroundDecoration!.consumeSpanPadding,); + final Rect rect = getRowRect( + rowSpan.backgroundDecoration!.consumeSpanPadding, + ); backgroundRows[rect] = rowSpan.backgroundDecoration!; } if (rowSpan.foregroundDecoration != null) { - final Rect rect = - getRowRect(rowSpan.foregroundDecoration!.consumeSpanPadding,); + final Rect rect = getRowRect( + rowSpan.foregroundDecoration!.consumeSpanPadding, + ); foregroundRows[rect] = rowSpan.foregroundDecoration!; } } diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart index 5cb88e3a1e57..fcd4559f3eec 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart @@ -32,10 +32,10 @@ class TableVicinity extends ChildVicinity { /// Equivalent to the [xIndex]. int get column => xIndex; - /// + /// The origin vicinity of the [TableView], (0,0). static const TableVicinity zero = TableVicinity(row: 0, column: 0); - /// Returns a new TableVicinity, copying over the row and column fields with + /// Returns a new [TableVicinity], copying over the row and column fields with /// those provided, or maintaining the original values. TableVicinity copyWith({ int? row, @@ -98,7 +98,14 @@ class TableViewParentData extends TwoDimensionalViewportParentData { } } +/// Creates a cell of the [TableView], along with information regarding merged +/// cells and [RepaintBoundary]s. /// +/// When merging cells in a [TableView], the same child should be returned from +/// every vicinity the merged cell contains. The `build` method will only be +/// called once for a merged cell, but since the table's children are lazily +/// laid out, returning the same child ensures the merged cell can be built no +/// matter which part of it is visible. class TableViewCell extends StatelessWidget { /// Creates a widget that controls how a child of a [TableView] spans across /// multiple rows or columns. @@ -115,11 +122,15 @@ class TableViewCell extends StatelessWidget { (rowMergeStart != null && rowMergeSpan != null), 'Row merge start and span must both be set, or both unset.', ), + assert(rowMergeStart == null || rowMergeStart >= 0), + assert(rowMergeSpan == null || rowMergeSpan > 0), assert( (columnMergeStart == null && columnMergeSpan == null) || (columnMergeStart != null && columnMergeSpan != null), 'Column merge start and span must both be set, or both unset.', - ); + ), + assert(columnMergeStart == null || columnMergeStart >= 0), + assert(columnMergeSpan == null || columnMergeSpan > 0); /// The child contained in this cell of the [TableView]. final Widget child; @@ -184,16 +195,7 @@ class _TableViewCell extends ParentDataWidget { this.columnMergeStart, this.columnMergeSpan, required super.child, - }) : assert( - (rowMergeStart == null && rowMergeSpan == null) || - (rowMergeStart != null && rowMergeSpan != null), - 'Row merge start and span must both be set, or both unset.', - ), - assert( - (columnMergeStart == null && columnMergeSpan == null) || - (columnMergeStart != null && columnMergeSpan != null), - 'Column merge start and span must both be set, or both unset.', - ); + }); final int? rowMergeStart; final int? rowMergeSpan; @@ -206,18 +208,22 @@ class _TableViewCell extends ParentDataWidget { renderObject.parentData! as TableViewParentData; bool needsLayout = false; if (parentData.rowMergeStart != rowMergeStart) { + assert(rowMergeStart == null || rowMergeStart! >= 0); parentData.rowMergeStart = rowMergeStart; needsLayout = true; } if (parentData.rowMergeSpan != rowMergeSpan) { + assert(rowMergeSpan == null || rowMergeSpan! > 0); parentData.rowMergeSpan = rowMergeSpan; needsLayout = true; } if (parentData.columnMergeStart != columnMergeStart) { + assert(columnMergeStart == null || columnMergeStart! >= 0); parentData.columnMergeStart = columnMergeStart; needsLayout = true; } if (parentData.columnMergeSpan != columnMergeSpan) { + assert(columnMergeSpan == null || columnMergeSpan! > 0); parentData.columnMergeSpan = columnMergeSpan; needsLayout = true; } diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart index bca0f9a8f8dd..805876164527 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart @@ -148,7 +148,8 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate cellBuilder(context, vicinity as TableVicinity), maxXIndex: columnCount - 1, maxYIndex: rowCount - 1, - addRepaintBoundaries: false, // repaintBoundaries handled by TableViewCell + addRepaintBoundaries: + false, // repaintBoundaries handled by TableViewCell ); @override @@ -237,7 +238,8 @@ class TableCellListDelegate extends TwoDimensionalChildListDelegate _pinnedRowCount = pinnedRowCount, super( children: cells, - addRepaintBoundaries: false, // repaintBoundaries handled by TableViewCell + addRepaintBoundaries: + false, // repaintBoundaries handled by TableViewCell ) { // Even if there are merged cells, they should be represented by the same // child in each cell location. This ensures that no matter which direction diff --git a/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart index 2eace84b18ac..e2df6374a8fc 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart @@ -2,16 +2,451 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; +const TableSpan span = TableSpan(extent: FixedTableSpanExtent(100)); + void main() { test('TableVicinity converts ChildVicinity', () { const TableVicinity vicinity = TableVicinity(column: 5, row: 10); expect(vicinity.xIndex, 5); expect(vicinity.yIndex, 10); + expect(vicinity.row, 10); + expect(vicinity.column, 5); expect(vicinity.toString(), '(row: 10, column: 5)'); }); - // TODO(Piinks): TableViewCell tests for merged cells, follow up change. + test('TableVicinity.zero', () { + const TableVicinity vicinity = TableVicinity.zero; + expect(vicinity.xIndex, 0); + expect(vicinity.yIndex, 0); + expect(vicinity.row, 0); + expect(vicinity.column, 0); + expect(vicinity.toString(), '(row: 0, column: 0)'); + }); + + test('TableVicinity.copyWith', () { + TableVicinity vicinity = TableVicinity.zero; + vicinity = vicinity.copyWith(column: 10); + expect(vicinity.xIndex, 10); + expect(vicinity.yIndex, 0); + expect(vicinity.row, 0); + expect(vicinity.column, 10); + expect(vicinity.toString(), '(row: 0, column: 10)'); + vicinity = vicinity.copyWith(row: 20); + expect(vicinity.xIndex, 10); + expect(vicinity.yIndex, 20); + expect(vicinity.row, 20); + expect(vicinity.column, 10); + expect(vicinity.toString(), '(row: 20, column: 10)'); + }); + + group('Merged cells', () { + group('Valid merge assertions', () { + test('TableViewCell asserts nonsensical merge configurations', () { + TableViewCell? cell; + const Widget child = SizedBox.shrink(); + expect( + () { + cell = TableViewCell(rowMergeStart: 0, child: child); + }, + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'description', + contains( + 'Row merge start and span must both be set, or both unset.'), + ), + ), + ); + expect( + () { + cell = TableViewCell(rowMergeSpan: 0, child: child); + }, + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'description', + contains( + 'Row merge start and span must both be set, or both unset.'), + ), + ), + ); + expect( + () { + cell = TableViewCell( + rowMergeStart: -1, + rowMergeSpan: 2, + child: child, + ); + }, + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'description', + contains('rowMergeStart == null || rowMergeStart >= 0'), + ), + ), + ); + expect( + () { + cell = TableViewCell( + rowMergeStart: 0, + rowMergeSpan: 0, + child: child, + ); + }, + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'description', + contains('rowMergeSpan == null || rowMergeSpan > 0'), + ), + ), + ); + expect( + () { + cell = TableViewCell(columnMergeStart: 0, child: child); + }, + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'description', + contains( + 'Column merge start and span must both be set, or both unset.'), + ), + ), + ); + expect( + () { + cell = TableViewCell(columnMergeSpan: 0, child: child); + }, + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'description', + contains( + 'Column merge start and span must both be set, or both unset.'), + ), + ), + ); + expect( + () { + cell = TableViewCell( + columnMergeStart: -1, + columnMergeSpan: 2, + child: child, + ); + }, + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'description', + contains('columnMergeStart == null || columnMergeStart >= 0'), + ), + ), + ); + expect( + () { + cell = TableViewCell( + columnMergeStart: 0, + columnMergeSpan: 0, + child: child, + ); + }, + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'description', + contains('columnMergeSpan == null || columnMergeSpan > 0'), + ), + ), + ); + expect(cell, isNull); + }); + + testWidgets('Merge start cannot exceed current index', + (WidgetTester tester) async { + // Merge span start is greater than given index, ex: column 10 has merge + // start at 20. + final List exceptions = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Row + // +---------+ + // | X err | + // | | + // +---------+ + // | merge | + // | | + // + + + // | | + // | | + // +---------+ + // This cell should only be built for (0, 1) and (0, 2), not (0,0). + TableViewCell cell = const TableViewCell( + rowMergeStart: 1, + rowMergeSpan: 2, + child: SizedBox.shrink(), + ); + await tester.pumpWidget(TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 1, + rowCount: 3, + )); + FlutterError.onError = oldHandler; + expect(exceptions.length, 2); + expect( + exceptions.first.toString(), + contains('spanMergeStart <= currentSpan'), + ); + + await tester.pumpWidget(Container()); + exceptions.clear(); + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Column + // +---------+---------+---------+ + // | X err | merged | + // | | | + // +---------+---------+---------+ + // This cell should only be returned for (1, 0) and (2, 0), not (0,0). + cell = const TableViewCell( + columnMergeStart: 1, + columnMergeSpan: 2, + child: SizedBox.shrink(), + ); + await tester.pumpWidget(TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 3, + rowCount: 1, + )); + FlutterError.onError = oldHandler; + expect(exceptions.length, 2); + expect( + exceptions.first.toString(), + contains('spanMergeStart <= currentSpan'), + ); + }); + + testWidgets('Merge cannot exceed table contents', + (WidgetTester tester) async { + // Merge exceeds table content, ex: at column 10, cell spans 4 columns, + // but table only has 12 columns. + final List exceptions = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Row + TableViewCell cell = const TableViewCell( + rowMergeStart: 0, + rowMergeSpan: 10, // Exceeds the number of rows + child: SizedBox.shrink(), + ); + await tester.pumpWidget(TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 1, + rowCount: 3, + )); + FlutterError.onError = oldHandler; + expect(exceptions.length, 2); + expect( + exceptions.first.toString(), + contains('spanMergeEnd <= spanCount'), + ); + + await tester.pumpWidget(Container()); + exceptions.clear(); + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Column + cell = const TableViewCell( + columnMergeStart: 0, + columnMergeSpan: 10, // Exceeds the number of columns + child: SizedBox.shrink(), + ); + await tester.pumpWidget(TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 3, + rowCount: 1, + )); + FlutterError.onError = oldHandler; + expect(exceptions.length, 2); + expect( + exceptions.first.toString(), + contains('spanMergeEnd <= spanCount'), + ); + }); + + testWidgets('Merge cannot contain pinned and unpinned cells', + (WidgetTester tester) async { + // Merge spans pinned and unpinned cells, ex: column 0 is pinned, 0-2 + // expected merge. + final List exceptions = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Row + TableViewCell cell = const TableViewCell( + rowMergeStart: 0, + rowMergeSpan: 3, + child: SizedBox.shrink(), + ); + await tester.pumpWidget(TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 1, + rowCount: 3, + pinnedRowCount: 1, + )); + FlutterError.onError = oldHandler; + expect(exceptions.length, 2); + expect( + exceptions.first.toString(), + contains('spanMergeEnd < pinnedSpanCount'), + ); + + await tester.pumpWidget(Container()); + exceptions.clear(); + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Column + cell = const TableViewCell( + columnMergeStart: 0, + columnMergeSpan: 3, + child: SizedBox.shrink(), + ); + await tester.pumpWidget(TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 3, + rowCount: 1, + pinnedColumnCount: 1, + )); + FlutterError.onError = oldHandler; + expect(exceptions.length, 2); + expect( + exceptions.first.toString(), + contains('spanMergeEnd < pinnedSpanCount'), + ); + }); + }); + group('layout', () { + // Cluster of merged cells (M) surrounded by regular cells (...). + // +---------+--------+--------+ + // | M(0,0) | M(0, 1) | .... + // | | | + // + +--------+--------+ + // | | M(1,1) | .... + // | | | + // +---------+ + + // | (2,0) | | .... + // | | | + // +---------+--------+--------+ + // ... ... ... + final Map mergedColumns = + { + const TableVicinity(row: 0, column: 1): (1, 2), // M(0, 1) + const TableVicinity(row: 0, column: 2): (1, 2), // M(0, 1) + const TableVicinity(row: 1, column: 1): (1, 2), // M(1, 1) + const TableVicinity(row: 1, column: 2): (1, 2), // M(1, 1) + const TableVicinity(row: 2, column: 1): (1, 2), // M(1, 1) + const TableVicinity(row: 2, column: 2): (1, 2), // M(1, 1) + }; + final Map mergedRows = + { + TableVicinity.zero: (0, 2), // M(0, 0) + TableVicinity.zero.copyWith(row: 1): (0, 2), // M(0,0) + const TableVicinity(row: 1, column: 1): (1, 2), // M(1, 1) + const TableVicinity(row: 1, column: 2): (1, 2), // M(1, 1) + const TableVicinity(row: 2, column: 1): (1, 2), // M(1, 1) + const TableVicinity(row: 2, column: 2): (1, 2), // M(1, 1) + }; + + testWidgets('natural main axis and scroll directions', + (WidgetTester tester) async { + // Verifies the right constraints for merged cells, and that extra calls + // to build are not made for merged cells. + final Map layoutConstraints = {}; + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: TableView.builder( + cellBuilder: (_, TableVicinity vicinity) { + if (mergedColumns.keys.contains(vicinity) || + mergedRows.keys.contains(vicinity)) { + return TableViewCell( + rowMergeStart: mergedRows[vicinity]?.$1, + rowMergeSpan: mergedRows[vicinity]?.$2, + columnMergeStart: mergedColumns[vicinity]?.$1, + columnMergeSpan: mergedColumns[vicinity]?.$2, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + layoutConstraints[vicinity] = constraints; + return Text( + 'M(${mergedRows[vicinity]?.$1 ?? vicinity.row},' + '${mergedColumns[vicinity]?.$1 ?? vicinity.column})', + ); + } + ), + ); + } + return TableViewCell( + child: Text('M(${vicinity.row},${vicinity.column})'), + ); + }, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 10, + rowCount: 10, + ), + )); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect(layoutConstraints[const TableVicinity(row: 0, column: 2)], isNull,); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect(layoutConstraints[const TableVicinity(row: 1, column: 0)], isNull,); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect(layoutConstraints[const TableVicinity(row: 1, column: 2)], isNull,); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect(layoutConstraints[const TableVicinity(row: 2, column: 1)], isNull,); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect(layoutConstraints[const TableVicinity(row: 2, column: 2)], isNull,); + + expect(tester.getTopLeft(find.text('M(0,0)')), Offset.zero); + print(layoutConstraints[TableVicinity.zero]); + // expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); // 300? + expect(tester.getTopLeft(find.text('M(0,1)')), const Offset(100.0, 0.0),); + // expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); + expect(tester.getTopLeft(find.text('M(1,1)')), const Offset(100.0, 100.0),); + // expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); + expect(tester.getTopLeft(find.text('M(2,0)')), const Offset(0.0, 200.0),); + // expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + }); + }); + }); } diff --git a/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart index f2c923e26b44..adafb6b7278a 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart @@ -163,10 +163,11 @@ void main() { cellBuilder: (_, __) => cell, columnBuilder: (_) => span, rowBuilder: (_) => span, - columnCount: 5, - rowCount: 6, + columnCount: 1, + rowCount: 1, ); - expect(delegate.addRepaintBoundaries, isTrue); + expect(delegate.addRepaintBoundaries, isFalse); + expect(cell.addRepaintBoundaries, isTrue); }); test('Notifies listeners & rebuilds', () { From 9e9abaaf1af7cc0fa8ee5baa6d73a3f1fb7bd880 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 16 Jan 2024 19:34:43 -0600 Subject: [PATCH 10/21] Fixed it --- .../lib/src/table_view/table.dart | 116 ++++++++---------- 1 file changed, 50 insertions(+), 66 deletions(-) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 01c2eae61795..f67d9d3f849a 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -734,21 +734,21 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { required Offset offset, }) { _Span colSpan, rowSpan; - double yPaintOffset = -offset.dy; + double rowOffset = -offset.dy; for (int row = start.row; row <= end.row; row += 1) { - double xPaintOffset = -offset.dx; + double columnOffset = -offset.dx; rowSpan = _rowMetrics[row]!; final double standardRowHeight = rowSpan.extent; double? mergedRowHeight; - double? mergedYPaintOffset; - yPaintOffset += rowSpan.configuration.padding.leading; + double? mergedRowOffset; + rowOffset += rowSpan.configuration.padding.leading; for (int column = start.column; column <= end.column; column += 1) { colSpan = _columnMetrics[column]!; final double standardColumnWidth = colSpan.extent; double? mergedColumnWidth; - double? mergedXPaintOffset; - xPaintOffset += colSpan.configuration.padding.leading; + double? mergedColumnOffset; + columnOffset += colSpan.configuration.padding.leading; final TableVicinity vicinity = TableVicinity(column: column, row: row); final RenderBox? cell = _mergedVicinities.keys.contains(vicinity) @@ -757,84 +757,73 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (cell != null) { final TableViewParentData cellParentData = parentDataOf(cell); - if (vicinity == TableVicinity.zero) { - print(cellParentData); - } // Merged cell handling if (cellParentData.rowMergeStart != null || cellParentData.columnMergeStart != null) { - late final int rowMergeStart; - late final int lastRow; - if (cellParentData.rowMergeStart != null) { - rowMergeStart = cellParentData.rowMergeStart!; - lastRow = rowMergeStart + cellParentData.rowMergeSpan! - 1; - _mergedRows.add(rowMergeStart); - } else { - rowMergeStart = row; - lastRow = row; - } + + final int firstRow = cellParentData.rowMergeStart ?? row; + final int lastRow = cellParentData.rowMergeStart == null ? row : firstRow + cellParentData.rowMergeSpan! - 1; assert(_debugCheckMergeBounds( spanOrientation: 'Row', currentSpan: row, - spanMergeStart: rowMergeStart, + spanMergeStart: firstRow, spanMergeEnd: lastRow, spanCount: delegate.rowCount, pinnedSpanCount: delegate.pinnedRowCount, currentVicinity: vicinity, )); - late final int columnMergeStart; - late final int lastColumn; - if (cellParentData.columnMergeStart != null) { - columnMergeStart = cellParentData.columnMergeStart!; - lastColumn = - columnMergeStart + cellParentData.columnMergeSpan! - 1; - _mergedColumns.add(columnMergeStart); - } else { - columnMergeStart = column; - lastColumn = column; - } + final int firstColumn = cellParentData.columnMergeStart ?? column; + final int lastColumn = cellParentData.columnMergeStart == null ? column : firstColumn + cellParentData.columnMergeSpan! - 1; assert(_debugCheckMergeBounds( spanOrientation: 'Column', currentSpan: column, - spanMergeStart: columnMergeStart, + spanMergeStart: firstColumn, spanMergeEnd: lastColumn, spanCount: delegate.columnCount, pinnedSpanCount: delegate.pinnedColumnCount, currentVicinity: vicinity, )); + // Leading padding on the leading cell, and trailing padding on the + // trailing cell should be excluded. Interim leading/trailing + // paddings are consumed by the merged cell. + // Example: This is one whole cell spanning 2 merged columns. + // l indicates leading padding, t trailing padding + // +---------------------------------------------------------+ + // | l | column extent | t | l | column extent | t | + // +---------------------------------------------------------+ + // | <--------- extent of merged cell ---------> | + // Compute height and layout offset for merged rows. - final _Span firstRow = _rowMetrics[rowMergeStart]!; - mergedRowHeight = firstRow.extent; - mergedYPaintOffset = -verticalOffset.pixels + - firstRow.leadingOffset + - firstRow.configuration.padding.leading; + mergedRowOffset = -verticalOffset.pixels + + _rowMetrics[firstRow]!.leadingOffset + + _rowMetrics[firstRow]!.configuration.padding.leading; + mergedRowHeight = _rowMetrics[lastRow]!.trailingOffset - + _rowMetrics[firstRow]!.leadingOffset - + _rowMetrics[lastRow]!.configuration.padding.trailing - + _rowMetrics[firstRow]!.configuration.padding.leading; // Compute width and layout offset for merged columns. - final _Span firstColumn = _columnMetrics[columnMergeStart]!; - mergedColumnWidth = firstColumn.extent; - mergedXPaintOffset = -horizontalOffset.pixels + - firstColumn.leadingOffset + - firstColumn.configuration.padding.leading; + mergedColumnOffset = -horizontalOffset.pixels + + _columnMetrics[firstColumn]!.leadingOffset + + _columnMetrics[firstColumn]!.configuration.padding.leading; + mergedColumnWidth = _columnMetrics[lastColumn]!.trailingOffset - + _columnMetrics[firstColumn]!.leadingOffset - + _columnMetrics[lastColumn]!.configuration.padding.trailing - + _columnMetrics[firstColumn]!.configuration.padding.leading; // Collect all of the vicinities that will not need to be built now. - int currentRow = rowMergeStart; + int currentRow = firstRow; while (currentRow <= lastRow) { - _mergedRows.add(currentRow); - mergedRowHeight = - mergedRowHeight! + _rowMetrics[currentRow]!.extent; - int currentColumn = columnMergeStart; + if (cellParentData.rowMergeStart != null) { + _mergedRows.add(currentRow); + } + int currentColumn = firstColumn; while (currentColumn <= lastColumn) { - if (vicinity == TableVicinity.zero) { - print(TableVicinity( - row: currentRow, - column: currentColumn, - )); + if (cellParentData.columnMergeStart != null) { + _mergedColumns.add(currentColumn); } - _mergedColumns.add(currentColumn); - mergedColumnWidth = - mergedColumnWidth! + _columnMetrics[currentColumn]!.extent; _mergedVicinities[TableVicinity( row: currentRow, column: currentColumn, @@ -845,29 +834,24 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } } - if (vicinity == TableVicinity.zero) { - print('mergedColumnWidth $mergedColumnWidth'); - print('mergedRowHeight $mergedRowHeight'); - } - final BoxConstraints cellConstraints = BoxConstraints.tightFor( width: mergedColumnWidth ?? standardColumnWidth, height: mergedRowHeight ?? standardRowHeight, ); cell.layout(cellConstraints); cellParentData.layoutOffset = Offset( - mergedXPaintOffset ?? xPaintOffset, - mergedYPaintOffset ?? yPaintOffset, + mergedColumnOffset ?? columnOffset, + mergedRowOffset ?? rowOffset, ); - mergedYPaintOffset = null; + mergedRowOffset = null; mergedRowHeight = null; - mergedXPaintOffset = null; + mergedColumnOffset = null; mergedColumnWidth = null; } - xPaintOffset += standardColumnWidth + + columnOffset += standardColumnWidth + _columnMetrics[column]!.configuration.padding.trailing; } - yPaintOffset += + rowOffset += standardRowHeight + _rowMetrics[row]!.configuration.padding.trailing; } } From ae22375a35de9708337cec28b4b3c121ed409f5f Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 17 Jan 2024 12:18:32 -0600 Subject: [PATCH 11/21] Found deco bug, stop to figure --- .../lib/src/table_view/table.dart | 9 +- .../test/table_view/table_cell_test.dart | 1376 ++++++++++++++++- 2 files changed, 1341 insertions(+), 44 deletions(-) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index f67d9d3f849a..11cccf74a825 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -761,9 +761,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // Merged cell handling if (cellParentData.rowMergeStart != null || cellParentData.columnMergeStart != null) { - final int firstRow = cellParentData.rowMergeStart ?? row; - final int lastRow = cellParentData.rowMergeStart == null ? row : firstRow + cellParentData.rowMergeSpan! - 1; + final int lastRow = cellParentData.rowMergeStart == null + ? row + : firstRow + cellParentData.rowMergeSpan! - 1; assert(_debugCheckMergeBounds( spanOrientation: 'Row', currentSpan: row, @@ -775,7 +776,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { )); final int firstColumn = cellParentData.columnMergeStart ?? column; - final int lastColumn = cellParentData.columnMergeStart == null ? column : firstColumn + cellParentData.columnMergeSpan! - 1; + final int lastColumn = cellParentData.columnMergeStart == null + ? column + : firstColumn + cellParentData.columnMergeSpan! - 1; assert(_debugCheckMergeBounds( spanOrientation: 'Column', currentSpan: column, diff --git a/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart index e2df6374a8fc..a4169f4bc6eb 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart @@ -352,6 +352,19 @@ void main() { }); }); group('layout', () { + // For TableView.mainAxis vertical (default) and + // For TableView.mainAxis horizontal + // - natural scroll directions + // - vertical reversed + // - horizontal reversed + // - both reversed + + late ScrollController verticalController; + late ScrollController horizontalController; + // Verifies the right constraints for merged cells, and that extra calls + // to build are not made for merged cells. + late Map layoutConstraints; + // Cluster of merged cells (M) surrounded by regular cells (...). // +---------+--------+--------+ // | M(0,0) | M(0, 1) | .... @@ -383,38 +396,52 @@ void main() { const TableVicinity(row: 2, column: 2): (1, 2), // M(1, 1) }; - testWidgets('natural main axis and scroll directions', - (WidgetTester tester) async { - // Verifies the right constraints for merged cells, and that extra calls - // to build are not made for merged cells. - final Map layoutConstraints = {}; + TableViewCell cellBuilder(BuildContext context, TableVicinity vicinity) { + if (mergedColumns.keys.contains(vicinity) || + mergedRows.keys.contains(vicinity)) { + return TableViewCell( + rowMergeStart: mergedRows[vicinity]?.$1, + rowMergeSpan: mergedRows[vicinity]?.$2, + columnMergeStart: mergedColumns[vicinity]?.$1, + columnMergeSpan: mergedColumns[vicinity]?.$2, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + layoutConstraints[vicinity] = constraints; + return Text( + 'M(${mergedRows[vicinity]?.$1 ?? vicinity.row},' + '${mergedColumns[vicinity]?.$1 ?? vicinity.column})', + ); + }), + ); + } + return TableViewCell( + child: Text('M(${vicinity.row},${vicinity.column})'), + ); + } + + setUp(() { + verticalController = ScrollController(); + horizontalController = ScrollController(); + layoutConstraints = {}; + }); + + tearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + testWidgets('vertical main axis and natural scroll directions', + (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: TableView.builder( - cellBuilder: (_, TableVicinity vicinity) { - if (mergedColumns.keys.contains(vicinity) || - mergedRows.keys.contains(vicinity)) { - return TableViewCell( - rowMergeStart: mergedRows[vicinity]?.$1, - rowMergeSpan: mergedRows[vicinity]?.$2, - columnMergeStart: mergedColumns[vicinity]?.$1, - columnMergeSpan: mergedColumns[vicinity]?.$2, - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - layoutConstraints[vicinity] = constraints; - return Text( - 'M(${mergedRows[vicinity]?.$1 ?? vicinity.row},' - '${mergedColumns[vicinity]?.$1 ?? vicinity.column})', - ); - } - ), - ); - } - return TableViewCell( - child: Text('M(${vicinity.row},${vicinity.column})'), - ); - }, + verticalDetails: ScrollableDetails.vertical( + controller: verticalController, + ), + horizontalDetails: ScrollableDetails.horizontal( + controller: horizontalController, + ), + cellBuilder: cellBuilder, columnBuilder: (_) => span, rowBuilder: (_) => span, columnCount: 10, @@ -425,27 +452,1294 @@ void main() { expect(find.text('M(0,0)'), findsOneWidget); expect(find.text('M(0,1)'), findsOneWidget); expect(find.text('M(0,2)'), findsNothing); // Merged - expect(layoutConstraints[const TableVicinity(row: 0, column: 2)], isNull,); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); expect(find.text('M(1,0)'), findsNothing); // Merged - expect(layoutConstraints[const TableVicinity(row: 1, column: 0)], isNull,); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); expect(find.text('M(1,1)'), findsOneWidget); expect(find.text('M(1,2)'), findsNothing); // Merged - expect(layoutConstraints[const TableVicinity(row: 1, column: 2)], isNull,); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); expect(find.text('M(2,0)'), findsOneWidget); expect(find.text('M(2,1)'), findsNothing); // Merged - expect(layoutConstraints[const TableVicinity(row: 2, column: 1)], isNull,); + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); expect(find.text('M(2,2)'), findsNothing); // Merged - expect(layoutConstraints[const TableVicinity(row: 2, column: 2)], isNull,); + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); expect(tester.getTopLeft(find.text('M(0,0)')), Offset.zero); - print(layoutConstraints[TableVicinity.zero]); - // expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); // 300? - expect(tester.getTopLeft(find.text('M(0,1)')), const Offset(100.0, 0.0),); - // expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); - expect(tester.getTopLeft(find.text('M(1,1)')), const Offset(100.0, 100.0),); - // expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); - expect(tester.getTopLeft(find.text('M(2,0)')), const Offset(0.0, 200.0),); - // expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(100.0, 0.0), + ); + expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(100.0, 100.0), + ); + expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(0.0, 200.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + + // Let's scroll a bit and check the layout + verticalController.jumpTo(25.0); + horizontalController.jumpTo(30.0); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), const Offset(-30.0, -25.0)); + expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(70.0, -25.0), + ); + expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(70.0, 75.0), + ); + expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(-30.0, 175.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + }); + + testWidgets('vertical main axis, reversed vertical', + (WidgetTester tester) async { + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: TableView.builder( + verticalDetails: ScrollableDetails.vertical( + controller: verticalController, + reverse: true, + ), + horizontalDetails: ScrollableDetails.horizontal( + controller: horizontalController, + ), + cellBuilder: cellBuilder, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 10, + rowCount: 10, + ), + )); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), + const Offset(0.0, 400.0), + ); + expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(100.0, 500.0), + ); + expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(100.0, 300.0), + ); + expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(0.0, 300.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + + // Let's scroll a bit and check the layout + verticalController.jumpTo(25.0); + horizontalController.jumpTo(30.0); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), const Offset(-30.0, 425.0)); + expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(70.0, 525.0), + ); + expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(70.0, 325.0), + ); + expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(-30.0, 325.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + }); + + testWidgets('vertical main axis, reversed horizontal', + (WidgetTester tester) async { + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: TableView.builder( + verticalDetails: ScrollableDetails.vertical( + controller: verticalController, + ), + horizontalDetails: ScrollableDetails.horizontal( + controller: horizontalController, + reverse: true, + ), + cellBuilder: cellBuilder, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 10, + rowCount: 10, + ), + )); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), + const Offset(700.0, 0.0), + ); + expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(500.0, 0.0), + ); + expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(500.0, 100.0), + ); + expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(700.0, 200.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + + // Let's scroll a bit and check the layout + verticalController.jumpTo(25.0); + horizontalController.jumpTo(30.0); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), + const Offset(730.0, -25.0), + ); + expect( + tester.getSize(find.text('M(0,0)')), + const Size(100.0, 200.0), + ); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(530.0, -25.0), + ); + expect( + tester.getSize(find.text('M(0,1)')), + const Size(200.0, 100.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(530.0, 75.0), + ); + expect( + tester.getSize(find.text('M(1,1)')), + const Size(200.0, 200.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(730.0, 175.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + }); + + testWidgets('vertical main axis, both axes reversed', + (WidgetTester tester) async { + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: TableView.builder( + verticalDetails: ScrollableDetails.vertical( + controller: verticalController, + reverse: true, + ), + horizontalDetails: ScrollableDetails.horizontal( + controller: horizontalController, + reverse: true, + ), + cellBuilder: cellBuilder, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 10, + rowCount: 10, + ), + )); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), + const Offset(700.0, 400.0), + ); + expect( + tester.getSize(find.text('M(0,0)')), + const Size(100.0, 200.0), + ); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(500.0, 500.0), + ); + expect( + tester.getSize(find.text('M(0,1)')), + const Size(200.0, 100.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(500.0, 300.0), + ); + expect( + tester.getSize(find.text('M(1,1)')), + const Size(200.0, 200.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(700.0, 300.0), + ); + expect( + tester.getSize(find.text('M(2,0)')), + const Size(100.0, 100.0), + ); + + // Let's scroll a bit and check the layout + verticalController.jumpTo(25.0); + horizontalController.jumpTo(30.0); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), + const Offset(730.0, 425.0), + ); + expect( + tester.getSize(find.text('M(0,0)')), + const Size(100.0, 200.0), + ); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(530.0, 525.0), + ); + expect( + tester.getSize(find.text('M(0,1)')), + const Size(200.0, 100.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(530.0, 325.0), + ); + expect( + tester.getSize(find.text('M(1,1)')), + const Size(200.0, 200.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(730.0, 325.0), + ); + expect( + tester.getSize(find.text('M(2,0)')), + const Size(100.0, 100.0), + ); + }); + + testWidgets('horizontal main axis and natural scroll directions', + (WidgetTester tester) async { + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: TableView.builder( + mainAxis: Axis.horizontal, + verticalDetails: ScrollableDetails.vertical( + controller: verticalController, + ), + horizontalDetails: ScrollableDetails.horizontal( + controller: horizontalController, + ), + cellBuilder: cellBuilder, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 10, + rowCount: 10, + ), + )); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect(tester.getTopLeft(find.text('M(0,0)')), Offset.zero); + expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(100.0, 0.0), + ); + expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(100.0, 100.0), + ); + expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(0.0, 200.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + + // Let's scroll a bit and check the layout + verticalController.jumpTo(25.0); + horizontalController.jumpTo(30.0); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), const Offset(-30.0, -25.0)); + expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(70.0, -25.0), + ); + expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(70.0, 75.0), + ); + expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(-30.0, 175.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + }); + + testWidgets('horizontal main axis, reversed vertical', + (WidgetTester tester) async { + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: TableView.builder( + mainAxis: Axis.horizontal, + verticalDetails: ScrollableDetails.vertical( + controller: verticalController, + reverse: true, + ), + horizontalDetails: ScrollableDetails.horizontal( + controller: horizontalController, + ), + cellBuilder: cellBuilder, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 10, + rowCount: 10, + ), + )); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), + const Offset(0.0, 400.0), + ); + expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(100.0, 500.0), + ); + expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(100.0, 300.0), + ); + expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(0.0, 300.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + + // Let's scroll a bit and check the layout + verticalController.jumpTo(25.0); + horizontalController.jumpTo(30.0); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), const Offset(-30.0, 425.0)); + expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(70.0, 525.0), + ); + expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(70.0, 325.0), + ); + expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(-30.0, 325.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + }); + + testWidgets('horizontal main axis, reversed horizontal', + (WidgetTester tester) async { + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: TableView.builder( + mainAxis: Axis.horizontal, + verticalDetails: ScrollableDetails.vertical( + controller: verticalController, + ), + horizontalDetails: ScrollableDetails.horizontal( + controller: horizontalController, + reverse: true, + ), + cellBuilder: cellBuilder, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 10, + rowCount: 10, + ), + )); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), + const Offset(700.0, 0.0), + ); + expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(500.0, 0.0), + ); + expect(tester.getSize(find.text('M(0,1)')), const Size(200.0, 100.0)); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(500.0, 100.0), + ); + expect(tester.getSize(find.text('M(1,1)')), const Size(200.0, 200.0)); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(700.0, 200.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + + // Let's scroll a bit and check the layout + verticalController.jumpTo(25.0); + horizontalController.jumpTo(30.0); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), + const Offset(730.0, -25.0), + ); + expect( + tester.getSize(find.text('M(0,0)')), + const Size(100.0, 200.0), + ); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(530.0, -25.0), + ); + expect( + tester.getSize(find.text('M(0,1)')), + const Size(200.0, 100.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(530.0, 75.0), + ); + expect( + tester.getSize(find.text('M(1,1)')), + const Size(200.0, 200.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(730.0, 175.0), + ); + expect(tester.getSize(find.text('M(2,0)')), const Size(100.0, 100.0)); + }); + + testWidgets('horizontal main axis, both axes reversed', + (WidgetTester tester) async { + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: TableView.builder( + mainAxis: Axis.horizontal, + verticalDetails: ScrollableDetails.vertical( + controller: verticalController, + reverse: true, + ), + horizontalDetails: ScrollableDetails.horizontal( + controller: horizontalController, + reverse: true, + ), + cellBuilder: cellBuilder, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 10, + rowCount: 10, + ), + )); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), + const Offset(700.0, 400.0), + ); + expect( + tester.getSize(find.text('M(0,0)')), + const Size(100.0, 200.0), + ); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(500.0, 500.0), + ); + expect( + tester.getSize(find.text('M(0,1)')), + const Size(200.0, 100.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(500.0, 300.0), + ); + expect( + tester.getSize(find.text('M(1,1)')), + const Size(200.0, 200.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(700.0, 300.0), + ); + expect( + tester.getSize(find.text('M(2,0)')), + const Size(100.0, 100.0), + ); + + // Let's scroll a bit and check the layout + verticalController.jumpTo(25.0); + horizontalController.jumpTo(30.0); + await tester.pumpAndSettle(); + expect(find.text('M(0,0)'), findsOneWidget); + expect(find.text('M(0,1)'), findsOneWidget); + expect(find.text('M(0,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 0, column: 2)], + isNull, + ); + expect(find.text('M(1,0)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 0)], + isNull, + ); + expect(find.text('M(1,1)'), findsOneWidget); + expect(find.text('M(1,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 1, column: 2)], + isNull, + ); + expect(find.text('M(2,0)'), findsOneWidget); + expect(find.text('M(2,1)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 1)], + isNull, + ); + expect(find.text('M(2,2)'), findsNothing); // Merged + expect( + layoutConstraints[const TableVicinity(row: 2, column: 2)], + isNull, + ); + + expect( + tester.getTopLeft(find.text('M(0,0)')), + const Offset(730.0, 425.0), + ); + expect( + tester.getSize(find.text('M(0,0)')), + const Size(100.0, 200.0), + ); + expect( + layoutConstraints[TableVicinity.zero], + BoxConstraints.tight(const Size(100.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(0,1)')), + const Offset(530.0, 525.0), + ); + expect( + tester.getSize(find.text('M(0,1)')), + const Size(200.0, 100.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 0, column: 1)], + BoxConstraints.tight(const Size(200.0, 100.0)), + ); + + expect( + tester.getTopLeft(find.text('M(1,1)')), + const Offset(530.0, 325.0), + ); + expect( + tester.getSize(find.text('M(1,1)')), + const Size(200.0, 200.0), + ); + expect( + layoutConstraints[const TableVicinity(row: 1, column: 1)], + BoxConstraints.tight(const Size(200.0, 200.0)), + ); + + expect( + tester.getTopLeft(find.text('M(2,0)')), + const Offset(730.0, 325.0), + ); + expect( + tester.getSize(find.text('M(2,0)')), + const Size(100.0, 100.0), + ); }); }); }); From 5993e7d5009c1006b6627bd8c03ef5e4f0138768 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 17 Jan 2024 14:21:37 -0600 Subject: [PATCH 12/21] ++ --- .../test/table_view/table_span_test.dart | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart index b6b0db398e26..385dddaab2c4 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart @@ -192,8 +192,8 @@ void main() { horizontalController.dispose(); }); - Widget buildCell(BuildContext context, TableVicinity vicinity) { - return const SizedBox.shrink(); + TableViewCell buildCell(BuildContext context, TableVicinity vicinity) { + return const TableViewCell(child: SizedBox.shrink()); } TableSpan buildSpan(bool isColumn) { @@ -846,6 +846,58 @@ void main() { ); }); }); + + // TODO(Piinks): Add cases for reversed axes once + // https://github.com/flutter/flutter/issues/141704 is fixed. + testWidgets('merged cell decorations', (WidgetTester tester) async { + // Cluster of merged rows (M) surrounded by regular cells (...). + // This tiered scenario verifies that the correct decoration is applied + // from leading cells for column decorations. + // +---------+--------+--------+ + // | M(0,0)//|////////|////////| + // |/////////|////////|////////| + // +/////////+--------+--------+ + // |/////////| M(1,1) | | + // |/////////| | | + // +---------+ +--------+ + // | | | M(2,2) | + // | | | | + // +---------+--------+ + + // |*********|********| | + // |*********|********| | + // +---------+--------+--------+ + + // Cluster of merged cells (M) surrounded by regular cells (...). + // This tiered scenario verifies that the correct decoration is applied + // from leading cells for column decorations. + // +--------+--------+--------+--------+ + // | M(0,0)//////////|********| | + // |/////////////////|********| | + // +--------+--------+--------+--------+ + // |////////| M(1,1) | | + // |////////| | | + // +--------+--------+--------+--------+ + // |////////| |M(2,2)***********| + // |////////| |*****************| + // +--------+--------+--------+--------+ + + // Cluster of merged cells (M) surrounded by regular cells (...). + // This tiered scenario verifies that the correct decoration is applied + // from leading cells for column decorations. + // +--------+--------+--------+--------+ + // | M(0,0)//////////|////////|////////| + // |/////////////////|////////|////////| + // +/////////////////+--------+--------+ + // |/////////////////| M(1,2) | + // |/////////////////| | + // +--------+--------+ | + // |********|********| | + // |********|********| | + // +--------+--------+--------+--------+ + }); + + testWidgets('merged cells account for row/column padding', + (WidgetTester tester) async {}); } class TestCanvas implements Canvas { From b8fbc86552257b35c254139bafa5fda24a55edb6 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 17 Jan 2024 19:20:36 -0600 Subject: [PATCH 13/21] Fisnished tests --- .../lib/src/table_view/table.dart | 11 +- .../test/table_view/table_cell_test.dart | 4 +- .../test/table_view/table_span_test.dart | 788 +++++++++++++++++- 3 files changed, 781 insertions(+), 22 deletions(-) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 11cccf74a825..633526087aff 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -709,7 +709,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { 'than the current $lowerSpanOrientation at $currentVicinity.', ); assert( - spanMergeEnd <= spanCount, + spanMergeEnd < spanCount, '$spanOrientation merge configuration exceeds number of ' '${lowerSpanOrientation}s in the table. $spanOrientation merge ' 'containing $currentVicinity starts at $spanMergeStart, and ends at ' @@ -1088,11 +1088,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final ({double leading, double trailing}) offsetCorrection = axisDirectionIsReversed(verticalAxisDirection) ? ( - leading: leadingCell.size.height, - trailing: trailingCell.size.height, + leading: leadingCell!.size.height, + trailing: trailingCell!.size.height, ) : (leading: 0.0, trailing: 0.0); - return Rect.fromPoints( parentDataOf(leadingCell!).paintOffset! + offset - @@ -1208,8 +1207,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final ({double leading, double trailing}) offsetCorrection = axisDirectionIsReversed(horizontalAxisDirection) ? ( - leading: leadingCell.size.width, - trailing: trailingCell.size.width, + leading: leadingCell!.size.width, + trailing: trailingCell!.size.width, ) : (leading: 0.0, trailing: 0.0); return Rect.fromPoints( diff --git a/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart index a4169f4bc6eb..aaf452c995fb 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart @@ -265,7 +265,7 @@ void main() { expect(exceptions.length, 2); expect( exceptions.first.toString(), - contains('spanMergeEnd <= spanCount'), + contains('spanMergeEnd < spanCount'), ); await tester.pumpWidget(Container()); @@ -290,7 +290,7 @@ void main() { expect(exceptions.length, 2); expect( exceptions.first.toString(), - contains('spanMergeEnd <= spanCount'), + contains('spanMergeEnd < spanCount'), ); }); diff --git a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart index 385dddaab2c4..207676c13b0d 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; @@ -847,12 +848,20 @@ void main() { }); }); - // TODO(Piinks): Add cases for reversed axes once - // https://github.com/flutter/flutter/issues/141704 is fixed. - testWidgets('merged cell decorations', (WidgetTester tester) async { + group('merged cell decorations', () { + // Visualizing test cases in this group of tests ---------- + // For each test configuration, these 3 scenarios are validated. + // Each test represents a permutation of + // TableView.mainAxis vertical (default) and horizontal, with + // - natural scroll directions + // - vertical reversed + // - horizontal reversed + // - both reversed + + // Scenario 1 // Cluster of merged rows (M) surrounded by regular cells (...). // This tiered scenario verifies that the correct decoration is applied - // from leading cells for column decorations. + // for merged rows. // +---------+--------+--------+ // | M(0,0)//|////////|////////| // |/////////|////////|////////| @@ -866,10 +875,60 @@ void main() { // |*********|********| | // |*********|********| | // +---------+--------+--------+ + final Map scenario1MergedRows = + { + TableVicinity.zero: (0, 2), + TableVicinity.zero.copyWith(row: 1): (0, 2), + const TableVicinity(row: 1, column: 1): (1, 2), + const TableVicinity(row: 2, column: 1): (1, 2), + const TableVicinity(row: 2, column: 2): (2, 2), + const TableVicinity(row: 3, column: 2): (2, 2), + }; + TableView buildScenario1({ + bool reverseVertical = false, + bool reverseHorizontal = false, + }) { + return TableView.builder( + verticalDetails: ScrollableDetails.vertical( + reverse: reverseVertical, + ), + horizontalDetails: ScrollableDetails.horizontal( + reverse: reverseHorizontal, + ), + columnCount: 3, + rowCount: 4, + cellBuilder: (_, TableVicinity vicinity) { + return TableViewCell( + rowMergeStart: scenario1MergedRows[vicinity]?.$1, + rowMergeSpan: scenario1MergedRows[vicinity]?.$2, + child: const SizedBox.expand(), + ); + }, + columnBuilder: (_) { + return const TableSpan(extent: FixedTableSpanExtent(100.0)); + }, + rowBuilder: (int index) { + Color? color; + switch (index) { + case 0: + color = const Color(0xFF2196F3); + case 3: + color = const Color(0xFF4CAF50); + } + return TableSpan( + extent: const FixedTableSpanExtent(100.0), + backgroundDecoration: + color == null ? null : TableSpanDecoration(color: color), + ); + }, + ); + } + + // Scenario 2 // Cluster of merged cells (M) surrounded by regular cells (...). // This tiered scenario verifies that the correct decoration is applied - // from leading cells for column decorations. + // to merged columns. // +--------+--------+--------+--------+ // | M(0,0)//////////|********| | // |/////////////////|********| | @@ -880,24 +939,725 @@ void main() { // |////////| |M(2,2)***********| // |////////| |*****************| // +--------+--------+--------+--------+ + final Map scenario2MergedColumns = + { + TableVicinity.zero: (0, 2), + TableVicinity.zero.copyWith(column: 1): (0, 2), + const TableVicinity(row: 1, column: 1): (1, 2), + const TableVicinity(row: 1, column: 2): (1, 2), + const TableVicinity(row: 2, column: 2): (2, 2), + const TableVicinity(row: 2, column: 3): (2, 2), + }; + TableView buildScenario2({ + bool reverseVertical = false, + bool reverseHorizontal = false, + }) { + return TableView.builder( + verticalDetails: ScrollableDetails.vertical( + reverse: reverseVertical, + ), + horizontalDetails: ScrollableDetails.horizontal( + reverse: reverseHorizontal, + ), + columnCount: 4, + rowCount: 3, + cellBuilder: (_, TableVicinity vicinity) { + return TableViewCell( + columnMergeStart: scenario2MergedColumns[vicinity]?.$1, + columnMergeSpan: scenario2MergedColumns[vicinity]?.$2, + child: const SizedBox.expand(), + ); + }, + rowBuilder: (_) { + return const TableSpan(extent: FixedTableSpanExtent(100.0)); + }, + columnBuilder: (int index) { + Color? color; + switch (index) { + case 0: + color = const Color(0xFF2196F3); + case 2: + color = const Color(0xFF4CAF50); + } + return TableSpan( + extent: const FixedTableSpanExtent(100.0), + backgroundDecoration: + color == null ? null : TableSpanDecoration(color: color), + ); + }, + ); + } + + // Scenario 3 // Cluster of merged cells (M) surrounded by regular cells (...). // This tiered scenario verifies that the correct decoration is applied - // from leading cells for column decorations. + // for merged cells over both rows and columns. + // \\ = blue + // // = green + // XX = intersection // +--------+--------+--------+--------+ - // | M(0,0)//////////|////////|////////| - // |/////////////////|////////|////////| - // +/////////////////+--------+--------+ - // |/////////////////| M(1,2) | - // |/////////////////| | + // | M(0,0)XXXXXXXXXX|\\\\\\\\|XXXXXXXX| + // |XXXXXXXXXXXXXXXXX|\\\\\\\\|XXXXXXXX| + // +XXXXXXXXXXXXXXXXX+--------+--------+ + // |XXXXXXXXXXXXXXXXX| M(1,2) | + // |XXXXXXXXXXXXXXXXX| | // +--------+--------+ | - // |********|********| | - // |********|********| | + // |////////| | | + // |////////| | | // +--------+--------+--------+--------+ + final Map scenario3MergedRows = + { + TableVicinity.zero: (0, 2), + const TableVicinity(row: 1, column: 0): (0, 2), + const TableVicinity(row: 0, column: 1): (0, 2), + const TableVicinity(row: 1, column: 1): (0, 2), + const TableVicinity(row: 1, column: 2): (1, 2), + const TableVicinity(row: 2, column: 2): (1, 2), + const TableVicinity(row: 1, column: 3): (1, 2), + const TableVicinity(row: 2, column: 3): (1, 2), + }; + final Map scenario3MergedColumns = + { + TableVicinity.zero: (0, 2), + const TableVicinity(row: 1, column: 0): (0, 2), + const TableVicinity(row: 0, column: 1): (0, 2), + const TableVicinity(row: 1, column: 1): (0, 2), + const TableVicinity(row: 1, column: 2): (2, 2), + const TableVicinity(row: 2, column: 2): (2, 2), + const TableVicinity(row: 1, column: 3): (2, 2), + const TableVicinity(row: 2, column: 3): (2, 2), + }; + + TableView buildScenario3({ + Axis mainAxis = Axis.vertical, + bool reverseVertical = false, + bool reverseHorizontal = false, + }) { + return TableView.builder( + mainAxis: mainAxis, + verticalDetails: ScrollableDetails.vertical( + reverse: reverseVertical, + ), + horizontalDetails: ScrollableDetails.horizontal( + reverse: reverseHorizontal, + ), + columnCount: 4, + rowCount: 3, + cellBuilder: (_, TableVicinity vicinity) { + return TableViewCell( + columnMergeStart: scenario3MergedColumns[vicinity]?.$1, + columnMergeSpan: scenario3MergedColumns[vicinity]?.$2, + rowMergeStart: scenario3MergedRows[vicinity]?.$1, + rowMergeSpan: scenario3MergedRows[vicinity]?.$2, + child: const SizedBox.expand(), + ); + }, + rowBuilder: (int index) { + Color? color; + switch (index) { + case 0: + color = const Color(0xFF2196F3); + } + return TableSpan( + extent: const FixedTableSpanExtent(100.0), + backgroundDecoration: + color == null ? null : TableSpanDecoration(color: color), + ); + }, + columnBuilder: (int index) { + Color? color; + switch (index) { + case 0: + case 3: + color = const Color(0xFF4CAF50); + } + return TableSpan( + extent: const FixedTableSpanExtent(100.0), + backgroundDecoration: + color == null ? null : TableSpanDecoration(color: color), + ); + }, + ); + } + + testWidgets('Vertical main axis, natural scroll directions', + (WidgetTester tester) async { + // Scenario 1 + await tester.pumpWidget(buildScenario1()); + expect( + find.byType(TableViewport), + paints + // Top row decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 200.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(100.0, 0.0, 300.0, 100.0), + color: const Color(0xFF2196F3), + ) + // Bottom row decoration, does not extend into last column + ..rect( + rect: const Rect.fromLTRB(0.0, 300.0, 200.0, 400.0), + color: const Color(0xff4caf50), + )); + + // Scenario 2 + await tester.pumpWidget(buildScenario2()); + expect( + find.byType(TableViewport), + paints + // First column decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 0.0, 200.0, 100.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first column + rect: const Rect.fromLTRB(0.0, 100.0, 100.0, 300.0), + color: const Color(0xFF2196F3), + ) + // Third column decorations, does not extend into last column + ..rect( // Unmerged section + rect: const Rect.fromLTRB(200.0, 0.0, 300.0, 100.0), + color: const Color(0xff4caf50), + ) + ..rect( + rect: const Rect.fromLTRB(200.0, 200.0, 400.0, 300.0), // M(2,2) + color: const Color(0xff4caf50), + ), + ); + + // Scenario 3 + await tester.pumpWidget(buildScenario3()); + expect( + find.byType(TableViewport), + paints + // Row decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(200.0, 0.0, 400.0, 100.0), + color: const Color(0xFF2196F3), + ) + // Column decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), // M(0,0) + color: const Color(0xff4caf50), + ) + ..rect( // Rest of the first column + rect: const Rect.fromLTRB(0.0, 200.0, 100.0, 300.0), + color: const Color(0xff4caf50), + ) + ..rect( + rect: const Rect.fromLTRB(300.0, 0.0, 400.0, 100.0), // Last column + color: const Color(0xff4caf50), + ), + ); + }); + + testWidgets('Vertical main axis, vertical reversed', + (WidgetTester tester) async { + // Scenario 1 + await tester.pumpWidget(buildScenario1(reverseVertical: true)); + expect( + find.byType(TableViewport), + paints + // Bottom row decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 400.0, 100.0, 600.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(100.0, 500.0, 300.0, 600.0), + color: const Color(0xFF2196F3), + ) + // Top row decoration, does not extend into last column + ..rect( + rect: const Rect.fromLTRB(0.0, 200.0, 200.0, 300.0), + color: const Color(0xff4caf50), + ), + ); + + // Scenario 2 + await tester.pumpWidget(buildScenario2(reverseVertical: true)); + expect( + find.byType(TableViewport), + paints + // First column decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 500.0, 200.0, 600.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first column + rect: const Rect.fromLTRB(0.0, 300.0, 100.0, 500.0), + color: const Color(0xFF2196F3), + ) + // Third column decorations, does not extend into last column + ..rect( // Unmerged section + rect: const Rect.fromLTRB(200.0, 500.0, 300.0, 600.0), + color: const Color(0xff4caf50), + ) + ..rect( + rect: const Rect.fromLTRB(200.0, 300.0, 400.0, 400.0), // M(2,2) + color: const Color(0xff4caf50), + ), + ); + + // Scenario 3 + await tester.pumpWidget(buildScenario3(reverseVertical: true)); + expect( + find.byType(TableViewport), + paints + // Row decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 400.0, 200.0, 600.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(200.0, 500.0, 400.0, 600.0), + color: const Color(0xFF2196F3), + ) + // Column decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 400.0, 200.0, 600.0), // M(0,0) + color: const Color(0xff4caf50), + ) + ..rect( // Rest of the first column + rect: const Rect.fromLTRB(0.0, 300.0, 100.0, 400.0), + color: const Color(0xff4caf50), + ) + ..rect( + rect: const Rect.fromLTRB(300.0, 500.0, 400.0, 600.0), // Last column + color: const Color(0xff4caf50), + ), + ); + }); + + testWidgets('Vertical main axis, horizontal reversed', + (WidgetTester tester) async { + // Scenario 1 + await tester.pumpWidget(buildScenario1(reverseHorizontal: true)); + expect( + find.byType(TableViewport), + paints + // Top row decorations + ..rect( + rect: const Rect.fromLTRB(700.0, 0.0, 800.0, 200.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(500.0, 0.0, 700.0, 100.0), + color: const Color(0xFF2196F3), + ) + // Bottom row decoration, does not extend into last column + ..rect( + rect: const Rect.fromLTRB(600.0, 300.0, 800.0, 400.0), + color: const Color(0xff4caf50), + ), + ); + + // Scenario 2 + await tester.pumpWidget(buildScenario2(reverseHorizontal: true)); + expect( + find.byType(TableViewport), + paints + // First column decorations + ..rect( + rect: const Rect.fromLTRB(600.0, 0.0, 800.0, 100.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first column + rect: const Rect.fromLTRB(700.0, 100.0, 800.0, 300.0), + color: const Color(0xFF2196F3), + ) + // Third column decorations, does not extend into last column + ..rect( // Unmerged section + rect: const Rect.fromLTRB(500.0, 0.0, 600.0, 100.0), + color: const Color(0xff4caf50), + ) + ..rect( + rect: const Rect.fromLTRB(400.0, 200.0, 600.0, 300.0), // M(2,2) + color: const Color(0xff4caf50), + ), + ); + + // Scenario 3 + await tester.pumpWidget(buildScenario3(reverseHorizontal: true)); + expect( + find.byType(TableViewport), + paints + // Row decorations + ..rect( + rect: const Rect.fromLTRB(600.0, 0.0, 800.0, 200.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(400.0, 0.0, 600.0, 100.0), + color: const Color(0xFF2196F3), + ) + // Column decorations + ..rect( + rect: const Rect.fromLTRB(600.0, 0.0, 800.0, 200.0), // M(0,0) + color: const Color(0xff4caf50), + ) + ..rect( // Rest of the first column + rect: const Rect.fromLTRB(700.0, 200.0, 800.0, 300.0), + color: const Color(0xff4caf50), + ) + ..rect( + rect: const Rect.fromLTRB(400.0, 0.0, 500.0, 100.0), // Last column + color: const Color(0xff4caf50), + ), + ); + }); + + testWidgets('Vertical main axis, both reversed', + (WidgetTester tester) async { + // Scenario 1 + await tester.pumpWidget(buildScenario1( + reverseHorizontal: true, + reverseVertical: true, + )); + expect( + find.byType(TableViewport), + paints + // Top row decorations + ..rect( + rect: const Rect.fromLTRB(700.0, 400.0, 800.0, 600.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(500.0, 500.0, 700.0, 600.0), + color: const Color(0xFF2196F3), + ) + // Bottom row decoration, does not extend into last column + ..rect( + rect: const Rect.fromLTRB(600.0, 200.0, 800.0, 300.0), + color: const Color(0xff4caf50), + ), + ); + + // Scenario 2 + await tester.pumpWidget(buildScenario2( + reverseHorizontal: true, + reverseVertical: true, + )); + expect( + find.byType(TableViewport), + paints + // First column decorations + ..rect( + rect: const Rect.fromLTRB(600.0, 500.0, 800.0, 600.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first column + rect: const Rect.fromLTRB(700.0, 300.0, 800.0, 500.0), + color: const Color(0xFF2196F3), + ) + // Third column decorations, does not extend into last column + ..rect( // Unmerged section + rect: const Rect.fromLTRB(500.0, 500.0, 600.0, 600.0), + color: const Color(0xff4caf50), + ) + ..rect( + rect: const Rect.fromLTRB(400.0, 300.0, 600.0, 400.0), // M(2,2) + color: const Color(0xff4caf50), + ), + ); + + // Scenario 3 + await tester.pumpWidget(buildScenario3( + reverseHorizontal: true, + reverseVertical: true, + )); + expect( + find.byType(TableViewport), + paints + // Row decorations + ..rect( + rect: const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(400.0, 500.0, 600.0, 600.0), + color: const Color(0xFF2196F3), + ) + // Column decorations + ..rect( + rect: const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), // M(0,0) + color: const Color(0xff4caf50), + ) + ..rect( // Rest of the first column + rect: const Rect.fromLTRB(700.0, 300.0, 800.0, 400.0), + color: const Color(0xff4caf50), + ) + ..rect( // Last column + rect: const Rect.fromLTRB(400.0, 500.0, 500.0, 600.0), + color: const Color(0xff4caf50), + ), + ); + }); + + testWidgets('Horizontal main axis, natural scroll directions', + (WidgetTester tester) async { + // Scenarios 1 & 2 do not mix column and row decorations, so main axis + // does not affect them. + + // Scenario 3 + await tester.pumpWidget(buildScenario3(mainAxis: Axis.horizontal)); + expect( + find.byType(TableViewport), + paints + // Column decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), // M(0,0) + color: const Color(0xff4caf50), + ) + ..rect( // Rest of the first column + rect: const Rect.fromLTRB(0.0, 200.0, 100.0, 300.0), + color: const Color(0xff4caf50), + ) + ..rect( + rect: const Rect.fromLTRB(300.0, 0.0, 400.0, 100.0), // Last column + color: const Color(0xff4caf50), + ) + // Row decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(200.0, 0.0, 400.0, 100.0), + color: const Color(0xFF2196F3), + ), + ); + }); + + testWidgets('Horizontal main axis, vertical reversed', + (WidgetTester tester) async { + // Scenarios 1 & 2 do not mix column and row decorations, so main axis + // does not affect them. + + // Scenario 3 + await tester.pumpWidget(buildScenario3( + reverseVertical: true, + mainAxis: Axis.horizontal, + )); + expect( + find.byType(TableViewport), + paints + // Column decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 400.0, 200.0, 600.0), // M(0,0) + color: const Color(0xff4caf50), + ) + ..rect( // Rest of the first column + rect: const Rect.fromLTRB(0.0, 300.0, 100.0, 400.0), + color: const Color(0xff4caf50), + ) + ..rect( // Last column + rect: const Rect.fromLTRB(300.0, 500.0, 400.0, 600.0), + color: const Color(0xff4caf50), + ) + // Row decorations + ..rect( + rect: const Rect.fromLTRB(0.0, 400.0, 200.0, 600.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(200.0, 500.0, 400.0, 600.0), + color: const Color(0xFF2196F3), + ), + ); + }); + + testWidgets('Horizontal main axis, horizontal reversed', + (WidgetTester tester) async { + // Scenarios 1 & 2 do not mix column and row decorations, so main axis + // does not affect them. + + // Scenario 3 + await tester.pumpWidget(buildScenario3( + reverseHorizontal: true, + mainAxis: Axis.horizontal, + )); + expect( + find.byType(TableViewport), + paints + // Column decorations + ..rect( + rect: const Rect.fromLTRB(600.0, 0.0, 800.0, 200.0), // M(0,0) + color: const Color(0xff4caf50), + ) + ..rect( // Rest of the first column + rect: const Rect.fromLTRB(700.0, 200.0, 800.0, 300.0), + color: const Color(0xff4caf50), + ) + ..rect( + rect: const Rect.fromLTRB(400.0, 0.0, 500.0, 100.0), // Last column + color: const Color(0xff4caf50), + ) + // Row decorations + ..rect( + rect: const Rect.fromLTRB(600.0, 0.0, 800.0, 200.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(400.0, 0.0, 600.0, 100.0), + color: const Color(0xFF2196F3), + ), + ); + }); + + testWidgets('Horizontal main axis, both reversed', + (WidgetTester tester) async { + // Scenarios 1 & 2 do not mix column and row decorations, so main axis + // does not affect them. + + // Scenario 3 + await tester.pumpWidget(buildScenario3( + reverseHorizontal: true, + reverseVertical: true, + mainAxis: Axis.horizontal, + )); + expect( + find.byType(TableViewport), + paints + // Column decorations + ..rect( + rect: const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), // M(0,0) + color: const Color(0xff4caf50), + ) + ..rect( // Rest of the first column + rect: const Rect.fromLTRB(700.0, 300.0, 800.0, 400.0), + color: const Color(0xff4caf50), + ) + ..rect( + rect: + const Rect.fromLTRB(400.0, 500.0, 500.0, 600.0), // Last column + color: const Color(0xff4caf50), + ) + // Row decorations + ..rect( + rect: const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), // M(0,0) + color: const Color(0xFF2196F3), + ) + ..rect( // Rest of the unmerged first row + rect: const Rect.fromLTRB(400.0, 500.0, 600.0, 600.0), + color: const Color(0xFF2196F3), + ), + ); + }); }); testWidgets('merged cells account for row/column padding', - (WidgetTester tester) async {}); + (WidgetTester tester) async { + // Leading padding on the leading cell, and trailing padding on the + // trailing cell should be excluded. Interim leading/trailing + // paddings are consumed by the merged cell. + // Example: This is one whole cell spanning 2 merged columns. + // l indicates leading padding, t trailing padding + // +---------------------------------------------------------+ + // | l | column extent | t | l | column extent | t | + // +---------------------------------------------------------+ + // | <--------- extent of merged cell ---------> | + + // Merged Row + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: TableView.builder( + rowCount: 2, + columnCount: 1, + cellBuilder: (_, __) { + return const TableViewCell( + rowMergeStart: 0, + rowMergeSpan: 2, + child: Text('M(0,0)'), + ); + }, + columnBuilder: (_) => + const TableSpan(extent: FixedTableSpanExtent(100.0)), + rowBuilder: (_) { + return const TableSpan( + extent: FixedTableSpanExtent(100.0), + padding: TableSpanPadding(leading: 10.0, trailing: 15.0), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('M(0,0)')), const Offset(0.0, 10.0)); + expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 225.0)); + + // Merged Column + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: TableView.builder( + rowCount: 1, + columnCount: 2, + cellBuilder: (_, __) { + return const TableViewCell( + columnMergeStart: 0, + columnMergeSpan: 2, + child: Text('M(0,0)'), + ); + }, + rowBuilder: (_) => + const TableSpan(extent: FixedTableSpanExtent(100.0)), + columnBuilder: (_) { + return const TableSpan( + extent: FixedTableSpanExtent(100.0), + padding: TableSpanPadding(leading: 10.0, trailing: 15.0), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('M(0,0)')), const Offset(10.0, 0)); + expect(tester.getSize(find.text('M(0,0)')), const Size(225.0, 100.0)); + + // Merged Square + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: TableView.builder( + rowCount: 2, + columnCount: 2, + cellBuilder: (_, __) { + return const TableViewCell( + rowMergeStart: 0, + rowMergeSpan: 2, + columnMergeStart: 0, + columnMergeSpan: 2, + child: Text('M(0,0)'), + ); + }, + columnBuilder: (_) { + return const TableSpan( + extent: FixedTableSpanExtent(100.0), + padding: TableSpanPadding(leading: 10.0, trailing: 15.0), + ); + }, + rowBuilder: (_) { + return const TableSpan( + extent: FixedTableSpanExtent(100.0), + padding: TableSpanPadding(leading: 10.0, trailing: 15.0), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + expect(tester.getTopLeft(find.text('M(0,0)')), const Offset(10.0, 10.0)); + expect(tester.getSize(find.text('M(0,0)')), const Size(225.0, 225.0)); + }); } class TestCanvas implements Canvas { From 923ea8b92e63ca2e04ed360333da9c0f1c7a2f9a Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 23 Jan 2024 12:15:06 -0600 Subject: [PATCH 14/21] Clean up records in _paintCells --- .../lib/src/table_view/table.dart | 175 +++++++++++------- .../test/table_view/table_span_test.dart | 98 ++++++---- 2 files changed, 171 insertions(+), 102 deletions(-) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 633526087aff..760a644d5962 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -900,8 +900,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _paintCells( context: context, offset: offset, - leading: _firstNonPinnedCell!, - trailing: _lastNonPinnedCell!, + leadingVicinity: _firstNonPinnedCell!, + trailingVicinity: _lastNonPinnedCell!, ); }, clipBehavior: clipBehavior, @@ -931,8 +931,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _paintCells( context: context, offset: offset, - leading: TableVicinity(column: 0, row: _firstNonPinnedRow!), - trailing: TableVicinity( + leadingVicinity: TableVicinity(column: 0, row: _firstNonPinnedRow!), + trailingVicinity: TableVicinity( column: _lastPinnedColumn!, row: _lastNonPinnedRow!), ); }, @@ -963,8 +963,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _paintCells( context: context, offset: offset, - leading: TableVicinity(column: _firstNonPinnedColumn!, row: 0), - trailing: TableVicinity( + leadingVicinity: + TableVicinity(column: _firstNonPinnedColumn!, row: 0), + trailingVicinity: TableVicinity( column: _lastNonPinnedColumn!, row: _lastPinnedRow!), ); }, @@ -981,8 +982,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _paintCells( context: context, offset: offset, - leading: TableVicinity.zero, - trailing: + leadingVicinity: TableVicinity.zero, + trailingVicinity: TableVicinity(column: _lastPinnedColumn!, row: _lastPinnedRow!), ); } @@ -1008,8 +1009,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { void _paintCells({ required PaintingContext context, - required TableVicinity leading, - required TableVicinity trailing, + required TableVicinity leadingVicinity, + required TableVicinity trailingVicinity, required Offset offset, }) { // Column decorations @@ -1018,27 +1019,27 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final LinkedHashMap backgroundColumns = LinkedHashMap(); - final TableSpan rowSpan = _rowMetrics[leading.row]!.configuration; - for (int column = leading.column; column <= trailing.column; column++) { + final TableSpan rowSpan = _rowMetrics[leadingVicinity.row]!.configuration; + for (int column = leadingVicinity.column; + column <= trailingVicinity.column; + column++) { TableSpan columnSpan = _columnMetrics[column]!.configuration; if (columnSpan.backgroundDecoration != null || columnSpan.foregroundDecoration != null || _mergedColumns.contains(column)) { - final List<(RenderBox, RenderBox)> decorationCells = - <(RenderBox, RenderBox)>[]; - late RenderBox? leadingCell; - late RenderBox? trailingCell; + final List<({RenderBox leading, RenderBox trailing})> decorationCells = + <({RenderBox leading, RenderBox trailing})>[]; if (_mergedColumns.isEmpty || !_mergedColumns.contains(column)) { // One decoration across the whole column. decorationCells.add(( - getChildFor(TableVicinity( + leading: getChildFor(TableVicinity( column: column, - row: leading.row, - ))!, // leading - getChildFor(TableVicinity( + row: leadingVicinity.row, + ))!, + trailing: getChildFor(TableVicinity( column: column, - row: trailing.row, - ))!, // trailing + row: trailingVicinity.row, + ))!, )); } else { // Walk through the rows to separate merged cells for decorating. A @@ -1053,16 +1054,22 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // | 1 rect | | | // | | | | // +---------+-------+-------+ - int currentRow = leading.row; - while (currentRow <= trailing.row) { + late RenderBox leadingCell; + late RenderBox trailingCell; + int currentRow = leadingVicinity.row; + while (currentRow <= trailingVicinity.row) { TableVicinity vicinity = TableVicinity( column: column, row: currentRow, ); - leadingCell = getChildFor(vicinity); - if (parentDataOf(leadingCell!).columnMergeStart != null) { - // Merged portion decorated individually. - decorationCells.add((leadingCell, leadingCell)); + leadingCell = getChildFor(vicinity)!; + if (parentDataOf(leadingCell).columnMergeStart != null) { + // Merged portion decorated individually since it exceeds the + // current column. + decorationCells.add(( + leading: leadingCell, + trailing: leadingCell, + )); currentRow++; continue; } @@ -1080,26 +1087,33 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { vicinity = vicinity.copyWith(row: currentRow); nextCell = getChildFor(vicinity, allowMerged: false); } - decorationCells.add((leadingCell, trailingCell!)); + decorationCells.add(( + leading: leadingCell, + trailing: trailingCell, + )); } } - Rect getColumnRect(bool consumePadding) { + Rect getColumnRect({ + required RenderBox leadingCell, + required RenderBox trailingCell, + required bool consumePadding, + }) { final ({double leading, double trailing}) offsetCorrection = axisDirectionIsReversed(verticalAxisDirection) ? ( - leading: leadingCell!.size.height, - trailing: trailingCell!.size.height, + leading: leadingCell.size.height, + trailing: trailingCell.size.height, ) : (leading: 0.0, trailing: 0.0); return Rect.fromPoints( - parentDataOf(leadingCell!).paintOffset! + + parentDataOf(leadingCell).paintOffset! + offset - Offset( consumePadding ? columnSpan.padding.leading : 0.0, rowSpan.padding.leading - offsetCorrection.leading, ), - parentDataOf(trailingCell!).paintOffset! + + parentDataOf(trailingCell).paintOffset! + offset + Offset(trailingCell.size.width, trailingCell.size.height) + Offset( @@ -1109,22 +1123,28 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ); } - for (final (RenderBox, RenderBox) span in decorationCells) { - (leadingCell, trailingCell) = span; + for (final ({RenderBox leading, RenderBox trailing}) cell + in decorationCells) { // If this was a merged cell, the decoration is defined by the leading // cell, which may come from a different column. - final int columnIndex = parentDataOf(leadingCell).columnMergeStart ?? - parentDataOf(leadingCell).tableVicinity.column; + final int columnIndex = parentDataOf(cell.leading).columnMergeStart ?? + parentDataOf(cell.leading).tableVicinity.column; columnSpan = _columnMetrics[columnIndex]!.configuration; if (columnSpan.backgroundDecoration != null) { final Rect rect = getColumnRect( - columnSpan.backgroundDecoration!.consumeSpanPadding, + leadingCell: cell.leading, + trailingCell: cell.trailing, + consumePadding: + columnSpan.backgroundDecoration!.consumeSpanPadding, ); backgroundColumns[rect] = columnSpan.backgroundDecoration!; } if (columnSpan.foregroundDecoration != null) { final Rect rect = getColumnRect( - columnSpan.foregroundDecoration!.consumeSpanPadding, + leadingCell: cell.leading, + trailingCell: cell.trailing, + consumePadding: + columnSpan.foregroundDecoration!.consumeSpanPadding, ); foregroundColumns[rect] = columnSpan.foregroundDecoration!; } @@ -1137,25 +1157,24 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { LinkedHashMap(); final LinkedHashMap backgroundRows = LinkedHashMap(); - final TableSpan columnSpan = _columnMetrics[leading.column]!.configuration; - for (int row = leading.row; row <= trailing.row; row++) { + final TableSpan columnSpan = + _columnMetrics[leadingVicinity.column]!.configuration; + for (int row = leadingVicinity.row; row <= trailingVicinity.row; row++) { TableSpan rowSpan = _rowMetrics[row]!.configuration; if (rowSpan.backgroundDecoration != null || rowSpan.foregroundDecoration != null || _mergedRows.contains(row)) { - final List<(RenderBox, RenderBox)> decorationCells = - <(RenderBox, RenderBox)>[]; - late RenderBox? leadingCell; - late RenderBox? trailingCell; + final List<({RenderBox leading, RenderBox trailing})> decorationCells = + <({RenderBox leading, RenderBox trailing})>[]; if (_mergedRows.isEmpty || !_mergedRows.contains(row)) { // One decoration across the whole row. decorationCells.add(( - getChildFor(TableVicinity( - column: leading.column, + leading: getChildFor(TableVicinity( + column: leadingVicinity.column, row: row, ))!, // leading - getChildFor(TableVicinity( - column: trailing.column, + trailing: getChildFor(TableVicinity( + column: trailingVicinity.column, row: row, ))!, // trailing )); @@ -1172,16 +1191,21 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // | | | | // | | | | // +---------+--------+--------+ - int currentColumn = leading.column; - while (currentColumn <= trailing.column) { + late RenderBox leadingCell; + late RenderBox trailingCell; + int currentColumn = leadingVicinity.column; + while (currentColumn <= trailingVicinity.column) { TableVicinity vicinity = TableVicinity( column: currentColumn, row: row, ); - leadingCell = getChildFor(vicinity); - if (parentDataOf(leadingCell!).rowMergeStart != null) { + leadingCell = getChildFor(vicinity)!; + if (parentDataOf(leadingCell).rowMergeStart != null) { // Merged portion decorated individually. - decorationCells.add((leadingCell, leadingCell)); + decorationCells.add(( + leading: leadingCell, + trailing: leadingCell, + )); currentColumn++; continue; } @@ -1199,26 +1223,33 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { vicinity = vicinity.copyWith(column: currentColumn); nextCell = getChildFor(vicinity, allowMerged: false); } - decorationCells.add((leadingCell, trailingCell!)); + decorationCells.add(( + leading: leadingCell, + trailing: trailingCell, + )); } } - Rect getRowRect(bool consumePadding) { + Rect getRowRect({ + required RenderBox leadingCell, + required RenderBox trailingCell, + required bool consumePadding, + }) { final ({double leading, double trailing}) offsetCorrection = axisDirectionIsReversed(horizontalAxisDirection) ? ( - leading: leadingCell!.size.width, - trailing: trailingCell!.size.width, + leading: leadingCell.size.width, + trailing: trailingCell.size.width, ) : (leading: 0.0, trailing: 0.0); return Rect.fromPoints( - parentDataOf(leadingCell!).paintOffset! + + parentDataOf(leadingCell).paintOffset! + offset - Offset( columnSpan.padding.leading - offsetCorrection.leading, consumePadding ? rowSpan.padding.leading : 0.0, ), - parentDataOf(trailingCell!).paintOffset! + + parentDataOf(trailingCell).paintOffset! + offset + Offset(trailingCell.size.width, trailingCell.size.height) + Offset( @@ -1228,22 +1259,26 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ); } - for (final (RenderBox, RenderBox) span in decorationCells) { - (leadingCell, trailingCell) = span; + for (final ({RenderBox leading, RenderBox trailing}) cell + in decorationCells) { // If this was a merged cell, the decoration is defined by the leading // cell, which may come from a different row. - final int rowIndex = parentDataOf(leadingCell).rowMergeStart ?? - parentDataOf(leadingCell).tableVicinity.row; + final int rowIndex = parentDataOf(cell.leading).rowMergeStart ?? + parentDataOf(cell.trailing).tableVicinity.row; rowSpan = _rowMetrics[rowIndex]!.configuration; if (rowSpan.backgroundDecoration != null) { final Rect rect = getRowRect( - rowSpan.backgroundDecoration!.consumeSpanPadding, + leadingCell: cell.leading, + trailingCell: cell.trailing, + consumePadding: rowSpan.backgroundDecoration!.consumeSpanPadding, ); backgroundRows[rect] = rowSpan.backgroundDecoration!; } if (rowSpan.foregroundDecoration != null) { final Rect rect = getRowRect( - rowSpan.foregroundDecoration!.consumeSpanPadding, + leadingCell: cell.leading, + trailingCell: cell.trailing, + consumePadding: rowSpan.foregroundDecoration!.consumeSpanPadding, ); foregroundRows[rect] = rowSpan.foregroundDecoration!; } @@ -1301,8 +1336,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } // Cells - for (int column = leading.column; column <= trailing.column; column++) { - for (int row = leading.row; row <= trailing.row; row++) { + for (int column = leadingVicinity.column; + column <= trailingVicinity.column; + column++) { + for (int row = leadingVicinity.row; row <= trailingVicinity.row; row++) { final TableVicinity vicinity = TableVicinity(column: column, row: row); final RenderBox? cell = getChildFor(vicinity, allowMerged: false); if (cell == null) { diff --git a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart index 207676c13b0d..53fe69b816a4 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart @@ -1093,7 +1093,8 @@ void main() { rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 200.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(100.0, 0.0, 300.0, 100.0), color: const Color(0xFF2196F3), ) @@ -1113,12 +1114,14 @@ void main() { rect: const Rect.fromLTRB(0.0, 0.0, 200.0, 100.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first column + ..rect( + // Rest of the unmerged first column rect: const Rect.fromLTRB(0.0, 100.0, 100.0, 300.0), color: const Color(0xFF2196F3), ) // Third column decorations, does not extend into last column - ..rect( // Unmerged section + ..rect( + // Unmerged section rect: const Rect.fromLTRB(200.0, 0.0, 300.0, 100.0), color: const Color(0xff4caf50), ) @@ -1138,7 +1141,8 @@ void main() { rect: const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(200.0, 0.0, 400.0, 100.0), color: const Color(0xFF2196F3), ) @@ -1147,7 +1151,8 @@ void main() { rect: const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), // M(0,0) color: const Color(0xff4caf50), ) - ..rect( // Rest of the first column + ..rect( + // Rest of the first column rect: const Rect.fromLTRB(0.0, 200.0, 100.0, 300.0), color: const Color(0xff4caf50), ) @@ -1170,7 +1175,8 @@ void main() { rect: const Rect.fromLTRB(0.0, 400.0, 100.0, 600.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(100.0, 500.0, 300.0, 600.0), color: const Color(0xFF2196F3), ) @@ -1191,12 +1197,14 @@ void main() { rect: const Rect.fromLTRB(0.0, 500.0, 200.0, 600.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first column + ..rect( + // Rest of the unmerged first column rect: const Rect.fromLTRB(0.0, 300.0, 100.0, 500.0), color: const Color(0xFF2196F3), ) // Third column decorations, does not extend into last column - ..rect( // Unmerged section + ..rect( + // Unmerged section rect: const Rect.fromLTRB(200.0, 500.0, 300.0, 600.0), color: const Color(0xff4caf50), ) @@ -1216,7 +1224,8 @@ void main() { rect: const Rect.fromLTRB(0.0, 400.0, 200.0, 600.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(200.0, 500.0, 400.0, 600.0), color: const Color(0xFF2196F3), ) @@ -1225,12 +1234,14 @@ void main() { rect: const Rect.fromLTRB(0.0, 400.0, 200.0, 600.0), // M(0,0) color: const Color(0xff4caf50), ) - ..rect( // Rest of the first column + ..rect( + // Rest of the first column rect: const Rect.fromLTRB(0.0, 300.0, 100.0, 400.0), color: const Color(0xff4caf50), ) ..rect( - rect: const Rect.fromLTRB(300.0, 500.0, 400.0, 600.0), // Last column + rect: + const Rect.fromLTRB(300.0, 500.0, 400.0, 600.0), // Last column color: const Color(0xff4caf50), ), ); @@ -1248,7 +1259,8 @@ void main() { rect: const Rect.fromLTRB(700.0, 0.0, 800.0, 200.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(500.0, 0.0, 700.0, 100.0), color: const Color(0xFF2196F3), ) @@ -1269,12 +1281,14 @@ void main() { rect: const Rect.fromLTRB(600.0, 0.0, 800.0, 100.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first column + ..rect( + // Rest of the unmerged first column rect: const Rect.fromLTRB(700.0, 100.0, 800.0, 300.0), color: const Color(0xFF2196F3), ) // Third column decorations, does not extend into last column - ..rect( // Unmerged section + ..rect( + // Unmerged section rect: const Rect.fromLTRB(500.0, 0.0, 600.0, 100.0), color: const Color(0xff4caf50), ) @@ -1294,7 +1308,8 @@ void main() { rect: const Rect.fromLTRB(600.0, 0.0, 800.0, 200.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(400.0, 0.0, 600.0, 100.0), color: const Color(0xFF2196F3), ) @@ -1303,7 +1318,8 @@ void main() { rect: const Rect.fromLTRB(600.0, 0.0, 800.0, 200.0), // M(0,0) color: const Color(0xff4caf50), ) - ..rect( // Rest of the first column + ..rect( + // Rest of the first column rect: const Rect.fromLTRB(700.0, 200.0, 800.0, 300.0), color: const Color(0xff4caf50), ) @@ -1329,7 +1345,8 @@ void main() { rect: const Rect.fromLTRB(700.0, 400.0, 800.0, 600.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(500.0, 500.0, 700.0, 600.0), color: const Color(0xFF2196F3), ) @@ -1353,12 +1370,14 @@ void main() { rect: const Rect.fromLTRB(600.0, 500.0, 800.0, 600.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first column + ..rect( + // Rest of the unmerged first column rect: const Rect.fromLTRB(700.0, 300.0, 800.0, 500.0), color: const Color(0xFF2196F3), ) // Third column decorations, does not extend into last column - ..rect( // Unmerged section + ..rect( + // Unmerged section rect: const Rect.fromLTRB(500.0, 500.0, 600.0, 600.0), color: const Color(0xff4caf50), ) @@ -1381,7 +1400,8 @@ void main() { rect: const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(400.0, 500.0, 600.0, 600.0), color: const Color(0xFF2196F3), ) @@ -1390,11 +1410,13 @@ void main() { rect: const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), // M(0,0) color: const Color(0xff4caf50), ) - ..rect( // Rest of the first column + ..rect( + // Rest of the first column rect: const Rect.fromLTRB(700.0, 300.0, 800.0, 400.0), color: const Color(0xff4caf50), ) - ..rect( // Last column + ..rect( + // Last column rect: const Rect.fromLTRB(400.0, 500.0, 500.0, 600.0), color: const Color(0xff4caf50), ), @@ -1416,7 +1438,8 @@ void main() { rect: const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), // M(0,0) color: const Color(0xff4caf50), ) - ..rect( // Rest of the first column + ..rect( + // Rest of the first column rect: const Rect.fromLTRB(0.0, 200.0, 100.0, 300.0), color: const Color(0xff4caf50), ) @@ -1429,7 +1452,8 @@ void main() { rect: const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(200.0, 0.0, 400.0, 100.0), color: const Color(0xFF2196F3), ), @@ -1454,11 +1478,13 @@ void main() { rect: const Rect.fromLTRB(0.0, 400.0, 200.0, 600.0), // M(0,0) color: const Color(0xff4caf50), ) - ..rect( // Rest of the first column + ..rect( + // Rest of the first column rect: const Rect.fromLTRB(0.0, 300.0, 100.0, 400.0), color: const Color(0xff4caf50), ) - ..rect( // Last column + ..rect( + // Last column rect: const Rect.fromLTRB(300.0, 500.0, 400.0, 600.0), color: const Color(0xff4caf50), ) @@ -1467,7 +1493,8 @@ void main() { rect: const Rect.fromLTRB(0.0, 400.0, 200.0, 600.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(200.0, 500.0, 400.0, 600.0), color: const Color(0xFF2196F3), ), @@ -1492,7 +1519,8 @@ void main() { rect: const Rect.fromLTRB(600.0, 0.0, 800.0, 200.0), // M(0,0) color: const Color(0xff4caf50), ) - ..rect( // Rest of the first column + ..rect( + // Rest of the first column rect: const Rect.fromLTRB(700.0, 200.0, 800.0, 300.0), color: const Color(0xff4caf50), ) @@ -1505,7 +1533,8 @@ void main() { rect: const Rect.fromLTRB(600.0, 0.0, 800.0, 200.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(400.0, 0.0, 600.0, 100.0), color: const Color(0xFF2196F3), ), @@ -1531,7 +1560,8 @@ void main() { rect: const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), // M(0,0) color: const Color(0xff4caf50), ) - ..rect( // Rest of the first column + ..rect( + // Rest of the first column rect: const Rect.fromLTRB(700.0, 300.0, 800.0, 400.0), color: const Color(0xff4caf50), ) @@ -1545,7 +1575,8 @@ void main() { rect: const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), // M(0,0) color: const Color(0xFF2196F3), ) - ..rect( // Rest of the unmerged first row + ..rect( + // Rest of the unmerged first row rect: const Rect.fromLTRB(400.0, 500.0, 600.0, 600.0), color: const Color(0xFF2196F3), ), @@ -1608,8 +1639,9 @@ void main() { child: Text('M(0,0)'), ); }, - rowBuilder: (_) => - const TableSpan(extent: FixedTableSpanExtent(100.0)), + rowBuilder: (_) => const TableSpan( + extent: FixedTableSpanExtent(100.0), + ), columnBuilder: (_) { return const TableSpan( extent: FixedTableSpanExtent(100.0), From 693db591b8781549dce34daaeba9622c75db41b3 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 23 Jan 2024 12:17:17 -0600 Subject: [PATCH 15/21] ++ --- .../lib/src/table_view/table.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 760a644d5962..2c07c8ad0b88 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -1344,7 +1344,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final RenderBox? cell = getChildFor(vicinity, allowMerged: false); if (cell == null) { // Covered by a merged cell - assert(_mergedVicinities.keys.contains(vicinity)); + assert( + _mergedVicinities.keys.contains(vicinity), + 'TableViewCell for $vicinity could not be found. If merging ' + 'cells, the same TableViewCell must be returned for every ' + 'TableVicinity that is contained in the merged area of the ' + 'TableView.', + ); continue; } final TableViewParentData cellParentData = parentDataOf(cell); From f88dd70b6f7842b7e3977d432ef937f01e63ed87 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 23 Jan 2024 12:30:32 -0600 Subject: [PATCH 16/21] Finish self review --- .../lib/src/table_view/table.dart | 17 ++++++++++++----- .../lib/src/table_view/table_delegate.dart | 8 ++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 2c07c8ad0b88..ee0810b2fcc2 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -1045,7 +1045,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // Walk through the rows to separate merged cells for decorating. A // merged column takes the decoration of its leading column. // +---------+-------+-------+ - // | leading | | | + // | | | | // | 1 rect | | | // +---------+-------+-------+ // | merged | | @@ -1065,7 +1065,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { leadingCell = getChildFor(vicinity)!; if (parentDataOf(leadingCell).columnMergeStart != null) { // Merged portion decorated individually since it exceeds the - // current column. + // single column width. decorationCells.add(( leading: leadingCell, trailing: leadingCell, @@ -1073,6 +1073,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { currentRow++; continue; } + // If this is not a merged cell, collect up all of the cells leading + // up to, or following after, the merged cell so we can decorate + // efficiently with as few rects as possible. RenderBox? nextCell = leadingCell; while (nextCell != null && parentDataOf(nextCell).columnMergeStart == null) { @@ -1182,8 +1185,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // Walk through the columns to separate merged cells for decorating. A // merged row takes the decoration of its leading row. // +---------+--------+--------+ - // | leading | merged | 1 rect | - // | 1 rect | 1 rect | | + // | 1 rect | merged | 1 rect | + // | | 1 rect | | // +---------+ +--------+ // | | | | // | | | | @@ -1201,7 +1204,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ); leadingCell = getChildFor(vicinity)!; if (parentDataOf(leadingCell).rowMergeStart != null) { - // Merged portion decorated individually. + // Merged portion decorated individually since it exceeds the + // single row height. decorationCells.add(( leading: leadingCell, trailing: leadingCell, @@ -1209,6 +1213,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { currentColumn++; continue; } + // If this is not a merged cell, collect up all of the cells leading + // up to, or following after, the merged cell so we can decorate + // efficiently with as few rects as possible. RenderBox? nextCell = leadingCell; while (nextCell != null && parentDataOf(nextCell).rowMergeStart == null) { diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart index 805876164527..037f6ac8c4da 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart @@ -148,8 +148,8 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate cellBuilder(context, vicinity as TableVicinity), maxXIndex: columnCount - 1, maxYIndex: rowCount - 1, - addRepaintBoundaries: - false, // repaintBoundaries handled by TableViewCell + // repaintBoundaries handled by TableViewCell + addRepaintBoundaries: false, ); @override @@ -238,8 +238,8 @@ class TableCellListDelegate extends TwoDimensionalChildListDelegate _pinnedRowCount = pinnedRowCount, super( children: cells, - addRepaintBoundaries: - false, // repaintBoundaries handled by TableViewCell + // repaintBoundaries handled by TableViewCell + addRepaintBoundaries: false, ) { // Even if there are merged cells, they should be represented by the same // child in each cell location. This ensures that no matter which direction From 03838f17acb5243213e3de1c3430a3d075467525 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 30 Jan 2024 12:08:25 -0600 Subject: [PATCH 17/21] Update packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart Co-authored-by: Michael Goderbauer --- .../lib/src/table_view/table_cell.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart index fcd4559f3eec..3474c4874c1e 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_cell.dart @@ -228,9 +228,8 @@ class _TableViewCell extends ParentDataWidget { needsLayout = true; } - final RenderObject? targetParent = renderObject.parent; - if (targetParent is RenderObject && needsLayout) { - targetParent.markNeedsLayout(); + if (needsLayout) { + renderObject.parent?.markNeedsLayout(); } } From 6a3f661f7b5a33fa7019121cd32a27d41a935248 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 30 Jan 2024 12:29:29 -0600 Subject: [PATCH 18/21] Review feedback --- .../example/lib/main.dart | 75 ++++++++++++------- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../lib/src/table_view/table.dart | 10 ++- .../lib/src/table_view/table_delegate.dart | 16 ++-- .../test/table_view/table_cell_test.dart | 32 +++++--- .../test/table_view/table_delegate_test.dart | 6 +- 7 files changed, 86 insertions(+), 57 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index ff714af1e497..948f2a8a0b24 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -44,6 +44,28 @@ class _TableExampleState extends State { late final ScrollController _verticalController = ScrollController(); int _rowCount = 20; + final Map mergedRows = + { + TableVicinity.zero: (start: 0, span: 2), + TableVicinity.zero.copyWith(row: 1): (start: 0, span: 2), + }; + + final Map mergedColumns = + { + const TableVicinity(row: 0, column: 1): (start: 1, span: 2), + const TableVicinity(row: 0, column: 2): (start: 1, span: 2), + }; + + // If a merged square is along the identity matrix, the values are the same + // for row merge and column merge data. + final Map mergedIdentitySquares = + { + const TableVicinity(row: 1, column: 1): (start: 1, span: 2), + const TableVicinity(row: 1, column: 2): (start: 1, span: 2), + const TableVicinity(row: 2, column: 1): (start: 1, span: 2), + const TableVicinity(row: 2, column: 2): (start: 1, span: 2), + }; + @override Widget build(BuildContext context) { return Scaffold( @@ -53,45 +75,42 @@ class _TableExampleState extends State { body: Padding( padding: const EdgeInsets.symmetric(horizontal: 50), child: TableView.builder( - verticalDetails: - ScrollableDetails.vertical(controller: _verticalController), + verticalDetails: ScrollableDetails.vertical( + controller: _verticalController, + ), cellBuilder: _buildCell, columnCount: 20, - columnBuilder: _buildColumnSpan, rowCount: _rowCount, + columnBuilder: _buildColumnSpan, rowBuilder: _buildRowSpan, ), ), - persistentFooterButtons: [ - TextButton( - onPressed: () { - _verticalController.jumpTo(0); - }, - child: const Text('Jump to Top'), - ), - TextButton( - onPressed: () { - _verticalController - .jumpTo(_verticalController.position.maxScrollExtent); - }, - child: const Text('Jump to Bottom'), - ), - TextButton( - onPressed: () { - setState(() { - _rowCount += 10; - }); - }, - child: const Text('Add 10 Rows'), - ), - ], ); } TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) { + final bool mergedCell = mergedRows.keys.contains(vicinity) || + mergedColumns.keys.contains(vicinity) || + mergedIdentitySquares.keys.contains(vicinity); + if (mergedCell) { + return TableViewCell( + rowMergeStart: mergedIdentitySquares[vicinity]?.start ?? + mergedRows[vicinity]?.start, + rowMergeSpan: + mergedIdentitySquares[vicinity]?.span ?? mergedRows[vicinity]?.span, + columnMergeStart: mergedIdentitySquares[vicinity]?.start ?? + mergedColumns[vicinity]?.start, + columnMergeSpan: mergedIdentitySquares[vicinity]?.span ?? + mergedColumns[vicinity]?.span, + child: const Center( + child: Text('Merged'), + ), + ); + } + return TableViewCell( child: Center( - child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), + child: Text('(${vicinity.row}, ${vicinity.column})'), ), ); } @@ -150,7 +169,7 @@ class _TableExampleState extends State { TableSpan _buildRowSpan(int index) { final TableSpanDecoration decoration = TableSpanDecoration( - color: index.isEven ? Colors.purple[100] : null, + color: index.isEven ? Colors.blueAccent[100] : null, border: const TableSpanBorder( trailing: BorderSide( width: 3, diff --git a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj index 27e0f506b609..7d9ca676a43d 100644 --- a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj @@ -227,7 +227,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 397f3d339fde..15368eccb25a 100644 --- a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ _mergedVicinities = {}; - // Used to optimize decorating when there are no merged cells in a given - // span. + // These contain the indexes of rows/columns that contain merged cells to + // optimize decoration drawing for rows/columns that don't contain merged + // cells. final List _mergedRows = []; final List _mergedColumns = []; @@ -827,10 +828,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (cellParentData.columnMergeStart != null) { _mergedColumns.add(currentColumn); } - _mergedVicinities[TableVicinity( + final TableVicinity key = TableVicinity( row: currentRow, column: currentColumn, - )] = vicinity; + ); + _mergedVicinities[key] = vicinity; currentColumn++; } currentRow++; diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart index 037f6ac8c4da..f3497208182d 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart @@ -16,7 +16,7 @@ import 'table_span.dart'; /// [TableView]. typedef TableSpanBuilder = TableSpan Function(int index); -/// Signature for a function that creates a child [Widget] for a given +/// Signature for a function that creates a child [TableViewCell] for a given /// [TableVicinity] in a [TableView], but may return null. /// /// Used by [TableCellBuilderDelegate.builder] to build cells on demand for the @@ -117,10 +117,9 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// A delegate that supplies children for a [TableViewport] on demand using a /// builder callback. /// -/// The [addRepaintBoundaries] of the super class is overridden to false here. -/// This is handled by [TableViewCell.addRepaintBoundaries]. This allows -/// [ParentData] to be written to children of the [TableView] for features like -/// merged cells. +/// Unlike the base [TwoDimensionalChildBuilderDelegate] this delegate does not +/// automatically insert repaint boundaries. Instead, repaint boundaries are +/// controlled by [TableViewCell.addRepaintBoundaries]. class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate with TableCellDelegateMixin { /// Creates a lazy building delegate to use with a [TableView]. @@ -216,10 +215,9 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate /// [TableVicinity.column] of the [TwoDimensionalViewport] as /// `children[vicinity.row][vicinity.column]`. /// -/// The [addRepaintBoundaries] of the super class is overridden to false here. -/// This is handled by [TableViewCell.addRepaintBoundaries]. This allows -/// [ParentData] to be written to children of the [TableView] for features like -/// merged cells. +/// Unlike the base [TwoDimensionalChildBuilderDelegate] this delegate does not +/// automatically insert repaint boundaries. Instead, repaint boundaries are +/// controlled by [TableViewCell.addRepaintBoundaries]. class TableCellListDelegate extends TwoDimensionalChildListDelegate with TableCellDelegateMixin { /// Creates a delegate that supplies children for a [TableView]. diff --git a/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart index aaf452c995fb..f68004b8bc7c 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart @@ -58,7 +58,8 @@ void main() { (AssertionError error) => error.toString(), 'description', contains( - 'Row merge start and span must both be set, or both unset.'), + 'Row merge start and span must both be set, or both unset.', + ), ), ), ); @@ -71,7 +72,8 @@ void main() { (AssertionError error) => error.toString(), 'description', contains( - 'Row merge start and span must both be set, or both unset.'), + 'Row merge start and span must both be set, or both unset.', + ), ), ), ); @@ -116,7 +118,8 @@ void main() { (AssertionError error) => error.toString(), 'description', contains( - 'Column merge start and span must both be set, or both unset.'), + 'Column merge start and span must both be set, or both unset.', + ), ), ), ); @@ -129,7 +132,8 @@ void main() { (AssertionError error) => error.toString(), 'description', contains( - 'Column merge start and span must both be set, or both unset.'), + 'Column merge start and span must both be set, or both unset.', + ), ), ), ); @@ -351,6 +355,7 @@ void main() { ); }); }); + group('layout', () { // For TableView.mainAxis vertical (default) and // For TableView.mainAxis horizontal @@ -405,13 +410,14 @@ void main() { columnMergeStart: mergedColumns[vicinity]?.$1, columnMergeSpan: mergedColumns[vicinity]?.$2, child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - layoutConstraints[vicinity] = constraints; - return Text( - 'M(${mergedRows[vicinity]?.$1 ?? vicinity.row},' - '${mergedColumns[vicinity]?.$1 ?? vicinity.column})', - ); - }), + builder: (BuildContext context, BoxConstraints constraints) { + layoutConstraints[vicinity] = constraints; + return Text( + 'M(${mergedRows[vicinity]?.$1 ?? vicinity.row},' + '${mergedColumns[vicinity]?.$1 ?? vicinity.column})', + ); + }, + ), ); } return TableViewCell( @@ -702,7 +708,9 @@ void main() { ); expect( - tester.getTopLeft(find.text('M(0,0)')), const Offset(-30.0, 425.0)); + tester.getTopLeft(find.text('M(0,0)')), + const Offset(-30.0, 425.0), + ); expect(tester.getSize(find.text('M(0,0)')), const Size(100.0, 200.0)); expect( layoutConstraints[TableVicinity.zero], diff --git a/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart index adafb6b7278a..70f2b1a02594 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_delegate_test.dart @@ -174,8 +174,10 @@ void main() { int notified = 0; TableCellBuilderDelegate oldDelegate; TableSpan spanBuilder(int index) => span; - TableViewCell cellBuilder(BuildContext context, TableVicinity vicinity) => - cell; + TableViewCell cellBuilder(BuildContext context, TableVicinity vicinity) { + return cell; + } + final TableCellBuilderDelegate delegate = TableCellBuilderDelegate( cellBuilder: cellBuilder, columnBuilder: spanBuilder, From fab93be39f80991303739ca4dd7e73f5854cb677 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 31 Jan 2024 09:20:24 -0600 Subject: [PATCH 19/21] Review feedback, clean up example error --- .../example/lib/main.dart | 81 +++++++------------ .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../lib/src/table_view/table.dart | 27 +++++-- 4 files changed, 54 insertions(+), 58 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index 948f2a8a0b24..cd23c568ae62 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -44,28 +44,6 @@ class _TableExampleState extends State { late final ScrollController _verticalController = ScrollController(); int _rowCount = 20; - final Map mergedRows = - { - TableVicinity.zero: (start: 0, span: 2), - TableVicinity.zero.copyWith(row: 1): (start: 0, span: 2), - }; - - final Map mergedColumns = - { - const TableVicinity(row: 0, column: 1): (start: 1, span: 2), - const TableVicinity(row: 0, column: 2): (start: 1, span: 2), - }; - - // If a merged square is along the identity matrix, the values are the same - // for row merge and column merge data. - final Map mergedIdentitySquares = - { - const TableVicinity(row: 1, column: 1): (start: 1, span: 2), - const TableVicinity(row: 1, column: 2): (start: 1, span: 2), - const TableVicinity(row: 2, column: 1): (start: 1, span: 2), - const TableVicinity(row: 2, column: 2): (start: 1, span: 2), - }; - @override Widget build(BuildContext context) { return Scaffold( @@ -75,43 +53,44 @@ class _TableExampleState extends State { body: Padding( padding: const EdgeInsets.symmetric(horizontal: 50), child: TableView.builder( - verticalDetails: ScrollableDetails.vertical( - controller: _verticalController, - ), + verticalDetails: + ScrollableDetails.vertical(controller: _verticalController), cellBuilder: _buildCell, columnCount: 20, - rowCount: _rowCount, columnBuilder: _buildColumnSpan, + rowCount: _rowCount, rowBuilder: _buildRowSpan, ), ), + persistentFooterButtons: [ + TextButton( + onPressed: () { + _verticalController.jumpTo(0); + }, + child: const Text('Jump to Top'), + ), + TextButton( + onPressed: () { + _verticalController + .jumpTo(_verticalController.position.maxScrollExtent); + }, + child: const Text('Jump to Bottom'), + ), + TextButton( + onPressed: () { + setState(() { + _rowCount += 10; + }); + }, + child: const Text('Add 10 Rows'), + ), + ], ); } - TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) { - final bool mergedCell = mergedRows.keys.contains(vicinity) || - mergedColumns.keys.contains(vicinity) || - mergedIdentitySquares.keys.contains(vicinity); - if (mergedCell) { - return TableViewCell( - rowMergeStart: mergedIdentitySquares[vicinity]?.start ?? - mergedRows[vicinity]?.start, - rowMergeSpan: - mergedIdentitySquares[vicinity]?.span ?? mergedRows[vicinity]?.span, - columnMergeStart: mergedIdentitySquares[vicinity]?.start ?? - mergedColumns[vicinity]?.start, - columnMergeSpan: mergedIdentitySquares[vicinity]?.span ?? - mergedColumns[vicinity]?.span, - child: const Center( - child: Text('Merged'), - ), - ); - } - - return TableViewCell( - child: Center( - child: Text('(${vicinity.row}, ${vicinity.column})'), - ), + Widget _buildCell(BuildContext context, TableVicinity vicinity) { + return Center( + child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), ); } @@ -169,7 +148,7 @@ class _TableExampleState extends State { TableSpan _buildRowSpan(int index) { final TableSpanDecoration decoration = TableSpanDecoration( - color: index.isEven ? Colors.blueAccent[100] : null, + color: index.isEven ? Colors.purple[100] : null, border: const TableSpanBorder( trailing: BorderSide( width: 3, diff --git a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj index 7d9ca676a43d..27e0f506b609 100644 --- a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/project.pbxproj @@ -227,7 +227,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15368eccb25a..397f3d339fde 100644 --- a/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/two_dimensional_scrollables/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Wed, 31 Jan 2024 09:21:50 -0600 Subject: [PATCH 20/21] Analyzer therapy --- .../two_dimensional_scrollables/example/lib/main.dart | 8 +++++--- .../lib/src/table_view/table_delegate.dart | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index cd23c568ae62..ff714af1e497 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -88,9 +88,11 @@ class _TableExampleState extends State { ); } - Widget _buildCell(BuildContext context, TableVicinity vicinity) { - return Center( - child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), + TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) { + return TableViewCell( + child: Center( + child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), + ), ); } diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart index f3497208182d..39a8d676a549 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart @@ -21,7 +21,7 @@ typedef TableSpanBuilder = TableSpan Function(int index); /// /// Used by [TableCellBuilderDelegate.builder] to build cells on demand for the /// table. -typedef TableViewCellBuilder = TableViewCell? Function( +typedef TableViewCellBuilder = TableViewCell Function( BuildContext context, TableVicinity vicinity, ); From ac283048891ca7cfc87c78ba6788b302419c1b22 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 31 Jan 2024 13:57:49 -0600 Subject: [PATCH 21/21] Feedback --- .../lib/src/table_view/table.dart | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index d9115f888c33..5d1575b1f5af 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -24,12 +24,12 @@ import 'table_span.dart'; /// vertically. If there is not enough space for all the columns, it will /// scroll horizontally. /// -/// Each child [TableViewCell] can belong to exactly one row and one column as -/// represented by its [TableVicinity], or it can span multiple rows and columns -/// through merging. The table supports lazy rendering and will only instantiate -/// those cells that are currently visible in the table's viewport and those -/// that extend into the [cacheExtent]. Therefore, when merging cells in a -/// [TableView], the same child should be returned from every vicinity the +/// Each child [TableViewCell] can belong to either exactly one row and one +/// column as represented by its [TableVicinity], or it can span multiple rows +/// and columns through merging. The table supports lazy rendering and will only +/// instantiate those cells that are currently visible in the table's viewport +/// and those that extend into the [cacheExtent]. Therefore, when merging cells +/// in a [TableView], the same child must be returned from every vicinity the /// merged cell contains. The `build` method will only be called once for a /// merged cell, but since the table's children are lazily laid out, returning /// the same child ensures the merged cell can be built no matter which part of @@ -1014,7 +1014,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // vicinity. assert(_mergedVicinities.keys.contains(vicinity)); final TableVicinity mergedVicinity = _mergedVicinities[vicinity]!; - return getChildFor(mergedVicinity)!; + // This vicinity must resolve to a child, unless something has gone wrong! + return getChildFor( + mergedVicinity, + mapMergedVicinityToCanonicalChild: false, + )!; } void _paintCells({