From c59808e3e5a2954dcc0db78bebd4ed486e3ebf0c Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Fri, 22 Mar 2024 15:20:12 -0500 Subject: [PATCH 01/17] Pausing for other change --- .../lib/src/table_view/table.dart | 110 ++++++++++++------ .../lib/src/table_view/table_delegate.dart | 65 ++++++----- 2 files changed, 113 insertions(+), 62 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 506204f917a7..bfbbadb595ff 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -309,6 +309,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { int? _lastNonPinnedRow; int? _lastNonPinnedColumn; + // How far columns should be laid out in a given frame. + double get _targetColumnPixel { + return cacheExtent + horizontalOffset.pixels + viewportDimension.width - _pinnedColumnsExtent; + } + TableVicinity? get _firstNonPinnedCell { if (_firstNonPinnedRow == null || _firstNonPinnedColumn == null) { return null; @@ -406,31 +411,52 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { return false; } - // Updates the cached metrics for the table. - // - // Will iterate through all columns and rows to define the layout pattern of - // the cells of the table. - // - // TODO(Piinks): Add back infinite separately for easier review, https://github.com/flutter/flutter/issues/131226 - // Only relevant when the number of rows and columns is finite - void _updateAllMetrics() { - assert(needsDelegateRebuild || didResize); - + // Updates the cached column metrics for the table. + void _updateColumnMetrics({int fromColumnIndex = 0, int? toColumnIndex,}) { _firstNonPinnedColumn = null; _lastNonPinnedColumn = null; double startOfRegularColumn = 0; double startOfPinnedColumn = 0; - final Map newColumnMetrics = {}; - for (int column = 0; column < delegate.columnCount; column++) { + int column = fromColumnIndex; + + bool reachedColumnEnd() { + if (toColumnIndex != null) { + // Column metrics should be computed up to the provided index. + // Only relevant when we are filling in missing column metrics in an + //infinite context. + assert(delegate.columnCount == null); + return newColumnMetrics.length > toColumnIndex; + } else if (delegate.columnCount == null) { + // There are infinite columns, and no target index, computing metrics + // up to what is visible and in the cache extent. + return _lastNonPinnedColumn != null; + } + // Infinite columns would be satisfied by cases above. + assert(delegate.columnCount != null); + // Compute all the metrics if the columns are finite. + return column < delegate.columnCount!; + } + + if (fromColumnIndex != 0) { + // If fromColumnIndex is provided, we are only adding to the metrics we + // already know, since we lazily compile metrics. Copy over cached column + // metrics we aren't updating. This should only be the case when the + // number of columns is infinite. + assert(delegate.columnCount == null); + for (int cachedColumn = 0; cachedColumn < fromColumnIndex; cachedColumn++) { + newColumnMetrics[cachedColumn] = _columnMetrics[cachedColumn]!; + } + } + + while (!reachedColumnEnd()) { final bool isPinned = column < delegate.pinnedColumnCount; final double leadingOffset = isPinned ? startOfPinnedColumn : startOfRegularColumn; _Span? span = _columnMetrics.remove(column); - assert(needsDelegateRebuild || span != null); - final TableSpan configuration = needsDelegateRebuild + final TableSpan configuration = needsDelegateRebuild || span == null ? delegate.buildColumn(column) - : span!.configuration; + : span.configuration; span ??= _Span(); span.update( isPinned: isPinned, @@ -449,11 +475,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _firstNonPinnedColumn == null) { _firstNonPinnedColumn = column; } - final double targetColumnPixel = cacheExtent + - horizontalOffset.pixels + - viewportDimension.width - - startOfPinnedColumn; - if (span.trailingOffset >= targetColumnPixel && + if (span.trailingOffset >= _targetColumnPixel && _lastNonPinnedColumn == null) { _lastNonPinnedColumn = column; } @@ -461,13 +483,17 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } else { startOfPinnedColumn = span.trailingOffset; } + column++; } assert(newColumnMetrics.length >= delegate.pinnedColumnCount); for (final _Span span in _columnMetrics.values) { span.dispose(); } _columnMetrics = newColumnMetrics; + } + // Updates the cached row metrics for the table. + void _updateRowMetrics({int fromRowIndex = 0, int? toRowIndex,}) { _firstNonPinnedRow = null; _lastNonPinnedRow = null; double startOfRegularRow = 0; @@ -518,9 +544,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { span.dispose(); } _rowMetrics = newRowMetrics; + } + void _updateScrollBounds() { final double maxVerticalScrollExtent; - if (_rowMetrics.length <= delegate.pinnedRowCount) { + if (delegate.rowCount == null) { + maxVerticalScrollExtent = double.infinity; + } else if (_rowMetrics.length <= delegate.pinnedRowCount) { assert(_firstNonPinnedRow == null && _lastNonPinnedRow == null); maxVerticalScrollExtent = 0.0; } else { @@ -532,12 +562,14 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { 0.0, _rowMetrics[lastRow]!.trailingOffset - viewportDimension.height + - startOfPinnedRow, + _pinnedRowsExtent, ); } final double maxHorizontalScrollExtent; - if (_columnMetrics.length <= delegate.pinnedColumnCount) { + if (delegate.columnCount == null) { + maxHorizontalScrollExtent = double.infinity; + } else if (_columnMetrics.length <= delegate.pinnedColumnCount) { assert(_firstNonPinnedColumn == null && _lastNonPinnedColumn == null); maxHorizontalScrollExtent = 0.0; } else { @@ -549,7 +581,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { 0.0, _columnMetrics[lastColumn]!.trailingOffset - viewportDimension.width + - startOfPinnedColumn, + _pinnedColumnsExtent, ); } @@ -561,17 +593,21 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } } - // Uses the cached metrics to update the currently visible cells - // - // TODO(Piinks): Add back infinite separately for easier review, https://github.com/flutter/flutter/issues/131226 - // Only relevant when the number of rows and columns is finite + // Uses the cached metrics to update the currently visible cells. void _updateFirstAndLastVisibleCell() { _firstNonPinnedColumn = null; _lastNonPinnedColumn = null; - final double targetColumnPixel = cacheExtent + - horizontalOffset.pixels + - viewportDimension.width - - _pinnedColumnsExtent; + if (_columnMetrics.isNotEmpty) { + _Span lastKnownColumn = _columnMetrics[_columnMetrics.length - 1]!; + if (_columnMetrics.length != delegate.columnCount || lastKnownColumn.trailingOffset < _targetColumnPixel) { + // This will add the column metrics we do not know about up to the + // _targetColumnPixel, while keeping the ones we already know about. + _updateColumnMetrics(fromColumnIndex: _columnMetrics.length); + assert(_columnMetrics.length == delegate.columnCount || lastKnownColumn.trailingOffset >= _targetColumnPixel); + } + lastKnownColumn = _columnMetrics[_columnMetrics.length - 1]!; + } + for (int column = 0; column < _columnMetrics.length; column++) { if (_columnMetrics[column]!.isPinned) { continue; @@ -581,7 +617,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _firstNonPinnedColumn == null) { _firstNonPinnedColumn = column; } - if (endOfColumn >= targetColumnPixel && _lastNonPinnedColumn == null) { + if (endOfColumn >= _targetColumnPixel && _lastNonPinnedColumn == null) { _lastNonPinnedColumn = column; break; } @@ -623,7 +659,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (needsDelegateRebuild || didResize) { // Recomputes the table metrics, invalidates any cached information. - _updateAllMetrics(); + _updateColumnMetrics(); + _updateRowMetrics(); + _updateScrollBounds(); } else { // Updates the visible cells based on cached table metrics. _updateFirstAndLastVisibleCell(); @@ -696,7 +734,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { required int currentSpan, required int spanMergeStart, required int spanMergeEnd, - required int spanCount, + required int? spanCount, required int pinnedSpanCount, required TableVicinity currentVicinity, }) { @@ -712,7 +750,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { 'than the current $lowerSpanOrientation at $currentVicinity.', ); assert( - spanMergeEnd < spanCount, + spanCount == null || spanMergeEnd < spanCount, '$spanOrientation merge configuration exceeds number of ' '${lowerSpanOrientation}s in the table. $spanOrientation merge ' 'containing $currentVicinity starts at $spanMergeStart, and ends at ' 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 39a8d676a549..1f658dc8d24c 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 @@ -33,9 +33,8 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// /// The [buildColumn] method will be called for indices smaller than the value /// provided here to learn more about the extent and visual appearance of a - /// particular column. - // TODO(Piinks): land infinite separately, https://github.com/flutter/flutter/issues/131226 - // If null, the table will have an infinite number of columns. + /// particular column. If null, the table will have an infinite number of + /// columns. /// /// The value returned by this getter may be an estimate of the total /// available columns, but [buildColumn] method must provide a valid @@ -46,15 +45,13 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// /// If the value returned by this getter changes throughout the lifetime of /// the delegate object, [notifyListeners] must be called. - int get columnCount; + int? get columnCount; /// The number of rows that the table has content for. /// /// The [buildRow] method will be called for indices smaller than the value /// provided here to learn more about the extent and visual appearance of a - /// particular row. - // TODO(Piinks): land infinite separately, https://github.com/flutter/flutter/issues/131226 - // If null, the table will have an infinite number of rows. + /// particular row. If null, the table will have an infinite number of rows. /// /// The value returned by this getter may be an estimate of the total /// available rows, but [buildRow] method must provide a valid @@ -65,7 +62,7 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// /// If the value returned by this getter changes throughout the lifetime of /// the delegate object, [notifyListeners] must be called. - int get rowCount; + int? get rowCount; /// The number of columns that are permanently shown on the leading vertical /// edge of the viewport. @@ -124,8 +121,8 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate with TableCellDelegateMixin { /// Creates a lazy building delegate to use with a [TableView]. TableCellBuilderDelegate({ - required int columnCount, - required int rowCount, + int? columnCount, + int? rowCount, int pinnedColumnCount = 0, int pinnedRowCount = 0, super.addAutomaticKeepAlives, @@ -134,10 +131,10 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate required TableSpanBuilder rowBuilder, }) : assert(pinnedColumnCount >= 0), assert(pinnedRowCount >= 0), - assert(rowCount >= 0), - assert(columnCount >= 0), - assert(pinnedColumnCount <= columnCount), - assert(pinnedRowCount <= rowCount), + assert(rowCount == null || rowCount >= 0), + assert(columnCount == null || columnCount >= 0), + assert(columnCount == null || pinnedColumnCount <= columnCount), + assert(rowCount == null || pinnedRowCount <= rowCount), _rowBuilder = rowBuilder, _columnBuilder = columnBuilder, _pinnedColumnCount = pinnedColumnCount, @@ -145,17 +142,25 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate super( builder: (BuildContext context, ChildVicinity vicinity) => cellBuilder(context, vicinity as TableVicinity), - maxXIndex: columnCount - 1, - maxYIndex: rowCount - 1, + maxXIndex: columnCount == null ? columnCount : columnCount - 1, + maxYIndex: rowCount == null ? rowCount : rowCount - 1, // repaintBoundaries handled by TableViewCell addRepaintBoundaries: false, ); @override - int get columnCount => maxXIndex! + 1; - set columnCount(int value) { - assert(pinnedColumnCount <= value); - maxXIndex = value - 1; + int? get columnCount { + return switch (maxXIndex) { + null => maxXIndex, + _ => maxXIndex! + 1, + }; + } + set columnCount(int? value) { + assert(value == null || pinnedColumnCount <= value); + maxXIndex = switch (value) { + null => value, + _ => value - 1, + }; } /// Builds the [TableSpan] that describes the column at the provided index. @@ -171,7 +176,7 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate int _pinnedColumnCount; set pinnedColumnCount(int value) { assert(value >= 0); - assert(value <= columnCount); + assert(columnCount == null || value <= columnCount!); if (pinnedColumnCount == value) { return; } @@ -180,10 +185,18 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate } @override - int get rowCount => maxYIndex! + 1; - set rowCount(int value) { - assert(pinnedRowCount <= value); - maxYIndex = value - 1; + int? get rowCount { + return switch (maxYIndex) { + null => maxYIndex, + _ => maxYIndex! + 1, + }; + } + set rowCount(int? value) { + assert(value == null || pinnedRowCount <= value); + maxYIndex = switch (value) { + null => value, + _ => value -1, + }; } /// Builds the [TableSpan] that describes the row at the provided index. @@ -199,7 +212,7 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate int _pinnedRowCount; set pinnedRowCount(int value) { assert(value >= 0); - assert(value <= rowCount); + assert(rowCount == null || value <= rowCount!); if (pinnedRowCount == value) { return; } From 3859c27db606200b7f70cd9ed4c42a158a417097 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Mon, 25 Mar 2024 14:24:21 -0500 Subject: [PATCH 02/17] ++ --- .../example/lib/main.dart | 4 +- .../lib/src/table_view/table.dart | 41 +++++++++++++------ .../lib/src/table_view/table_delegate.dart | 29 +++++++++---- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index ff714af1e497..47a9f6a6beb2 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -56,9 +56,9 @@ class _TableExampleState extends State { verticalDetails: ScrollableDetails.vertical(controller: _verticalController), cellBuilder: _buildCell, - columnCount: 20, + // columnCount: 20, columnBuilder: _buildColumnSpan, - rowCount: _rowCount, + // rowCount: _rowCount, rowBuilder: _buildRowSpan, ), ), 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 bfbbadb595ff..f76b7d7d6259 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -129,17 +129,17 @@ class TableView extends TwoDimensionalScrollView { super.clipBehavior, int pinnedRowCount = 0, int pinnedColumnCount = 0, - required int columnCount, - required int rowCount, + int? columnCount, + int? rowCount, required TableSpanBuilder columnBuilder, required TableSpanBuilder rowBuilder, required TableViewCellBuilder cellBuilder, }) : assert(pinnedRowCount >= 0), - assert(rowCount >= 0), - assert(rowCount >= pinnedRowCount), - assert(columnCount >= 0), + assert(rowCount == null || rowCount >= 0), + assert(rowCount == null || rowCount >= pinnedRowCount), + assert(columnCount == null || columnCount >= 0), assert(pinnedColumnCount >= 0), - assert(columnCount >= pinnedColumnCount), + assert(columnCount == null || columnCount >= pinnedColumnCount), super( delegate: TableCellBuilderDelegate( columnCount: columnCount, @@ -412,7 +412,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } // Updates the cached column metrics for the table. - void _updateColumnMetrics({int fromColumnIndex = 0, int? toColumnIndex,}) { + void _updateColumnMetrics({int fromColumnIndex = 0, int? toColumnIndex}) { _firstNonPinnedColumn = null; _lastNonPinnedColumn = null; double startOfRegularColumn = 0; @@ -424,11 +424,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (toColumnIndex != null) { // Column metrics should be computed up to the provided index. // Only relevant when we are filling in missing column metrics in an - //infinite context. + // infinite context. assert(delegate.columnCount == null); return newColumnMetrics.length > toColumnIndex; } else if (delegate.columnCount == null) { - // There are infinite columns, and no target index, computing metrics + // There are infinite columns, and no target index, compute metrics // up to what is visible and in the cache extent. return _lastNonPinnedColumn != null; } @@ -454,9 +454,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final double leadingOffset = isPinned ? startOfPinnedColumn : startOfRegularColumn; _Span? span = _columnMetrics.remove(column); - final TableSpan configuration = needsDelegateRebuild || span == null + final TableSpan? configuration = needsDelegateRebuild ? delegate.buildColumn(column) - : span.configuration; + : span?.configuration; + if (configuration == null) { + // We have reached the end of columns. + break; + } span ??= _Span(); span.update( isPinned: isPinned, @@ -485,6 +489,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } column++; } + assert(newColumnMetrics.length >= delegate.pinnedColumnCount); for (final _Span span in _columnMetrics.values) { span.dispose(); @@ -493,7 +498,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } // Updates the cached row metrics for the table. - void _updateRowMetrics({int fromRowIndex = 0, int? toRowIndex,}) { + void _updateRowMetrics({int fromRowIndex = 0, int? toRowIndex}) { _firstNonPinnedRow = null; _lastNonPinnedRow = null; double startOfRegularRow = 0; @@ -507,7 +512,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _Span? span = _rowMetrics.remove(row); assert(needsDelegateRebuild || span != null); final TableSpan configuration = - needsDelegateRebuild ? delegate.buildRow(row) : span!.configuration; + needsDelegateRebuild ? delegate.buildRow(row) : span.configuration; span ??= _Span(); span.update( isPinned: isPinned, @@ -878,6 +883,16 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { mergedColumnOffset = baseColumnOffset + _columnMetrics[firstColumn]!.leadingOffset + _columnMetrics[firstColumn]!.configuration.padding.leading; + + if (delegate.columnCount == null && _columnMetrics[lastColumn] == null) { + // The number of columns is infinte, and we have not calculated + // the metrics to the full extent of the merged cell. Update the + // metrics so we have all the information for the merged area. + _updateColumnMetrics( + fromColumnIndex: _columnMetrics.length, + toColumnIndex: lastColumn, + ); + } mergedColumnWidth = _columnMetrics[lastColumn]!.trailingOffset - _columnMetrics[firstColumn]!.leadingOffset - _columnMetrics[lastColumn]!.configuration.padding.trailing - 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 1f658dc8d24c..a8e23626efda 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 @@ -14,14 +14,14 @@ import 'table_span.dart'; /// Used by the [TableCellDelegateMixin.buildColumn] and /// [TableCellDelegateMixin.buildRow] to configure rows and columns in the /// [TableView]. -typedef TableSpanBuilder = TableSpan Function(int index); +typedef TableSpanBuilder = TableSpan? Function(int index); /// 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 /// table. -typedef TableViewCellBuilder = TableViewCell Function( +typedef TableViewCellBuilder = TableViewCell? Function( BuildContext context, TableVicinity vicinity, ); @@ -45,6 +45,7 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// /// If the value returned by this getter changes throughout the lifetime of /// the delegate object, [notifyListeners] must be called. + // TODO(Piinks): Update all docs where making nullable int? get columnCount; /// The number of rows that the table has content for. @@ -102,13 +103,13 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// /// The builder must return a valid [TableSpan] for all indices smaller than /// [columnCount]. - TableSpan buildColumn(int index); + TableSpan? buildColumn(int index); /// Builds the [TableSpan] that describe the row at the provided index. /// /// The builder must return a valid [TableSpan] for all indices smaller than /// [rowCount]. - TableSpan buildRow(int index); + TableSpan? buildRow(int index); } /// A delegate that supplies children for a [TableViewport] on demand using a @@ -169,7 +170,7 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate /// [columnCount]. final TableSpanBuilder _columnBuilder; @override - TableSpan buildColumn(int index) => _columnBuilder(index); + TableSpan? buildColumn(int index) => _columnBuilder(index); @override int get pinnedColumnCount => _pinnedColumnCount; @@ -205,7 +206,7 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate /// [rowCount]. final TableSpanBuilder _rowBuilder; @override - TableSpan buildRow(int index) => _rowBuilder(index); + TableSpan? buildRow(int index) => _rowBuilder(index); @override int get pinnedRowCount => _pinnedRowCount; @@ -274,7 +275,13 @@ class TableCellListDelegate extends TwoDimensionalChildListDelegate /// [columnCount]. final TableSpanBuilder _columnBuilder; @override - TableSpan buildColumn(int index) => _columnBuilder(index); + TableSpan? buildColumn(int index) { + if (index >= columnCount) { + // The list delegate has a finite number of columns. + return null; + } + return _columnBuilder(index); + } @override int get pinnedColumnCount => _pinnedColumnCount; @@ -298,7 +305,13 @@ class TableCellListDelegate extends TwoDimensionalChildListDelegate /// [rowCount]. final TableSpanBuilder _rowBuilder; @override - TableSpan buildRow(int index) => _rowBuilder(index); + TableSpan? buildRow(int index) { + if (index >= rowCount) { + // The list deleagte has a finite number of rows. + return null; + } + return _rowBuilder(index); + } @override int get pinnedRowCount => _pinnedRowCount; From 02167b435fb920ddf30804b50b1399876a44bcc9 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Mon, 25 Mar 2024 17:10:48 -0500 Subject: [PATCH 03/17] +++ --- .../example/lib/main.dart | 28 +-- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../lib/src/table_view/table.dart | 218 ++++++++++++------ .../lib/src/table_view/table_delegate.dart | 37 ++- 5 files changed, 193 insertions(+), 94 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index 47a9f6a6beb2..8a3d60eaa8f1 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -56,9 +56,9 @@ class _TableExampleState extends State { verticalDetails: ScrollableDetails.vertical(controller: _verticalController), cellBuilder: _buildCell, - // columnCount: 20, + columnCount: 20, columnBuilder: _buildColumnSpan, - // rowCount: _rowCount, + rowCount: _rowCount, rowBuilder: _buildRowSpan, ), ), @@ -108,7 +108,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( @@ -121,27 +121,27 @@ class _TableExampleState extends State { case 1: return TableSpan( foregroundDecoration: decoration, - extent: const FractionalTableSpanExtent(0.5), - onEnter: (_) => print('Entered column $index'), + extent: const FixedTableSpanExtent(100), + // onEnter: (_) => print('Entered column $index'), cursor: SystemMouseCursors.contextMenu, ); case 2: return TableSpan( foregroundDecoration: decoration, - extent: const FixedTableSpanExtent(120), - onEnter: (_) => print('Entered column $index'), + extent: const FixedTableSpanExtent(100), + // onEnter: (_) => print('Entered column $index'), ); case 3: return TableSpan( foregroundDecoration: decoration, - extent: const FixedTableSpanExtent(145), - onEnter: (_) => print('Entered column $index'), + extent: const FixedTableSpanExtent(100), + // onEnter: (_) => print('Entered column $index'), ); case 4: return TableSpan( foregroundDecoration: decoration, - extent: const FixedTableSpanExtent(200), - onEnter: (_) => print('Entered column $index'), + extent: const FixedTableSpanExtent(100), + // onEnter: (_) => print('Entered column $index'), ); } throw AssertionError( @@ -162,7 +162,7 @@ class _TableExampleState extends State { case 0: return TableSpan( backgroundDecoration: decoration, - extent: const FixedTableSpanExtent(50), + extent: const FixedTableSpanExtent(100), recognizerFactories: { TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( @@ -175,13 +175,13 @@ class _TableExampleState extends State { case 1: return TableSpan( backgroundDecoration: decoration, - extent: const FixedTableSpanExtent(65), + extent: const FixedTableSpanExtent(100), cursor: SystemMouseCursors.click, ); case 2: return TableSpan( backgroundDecoration: decoration, - extent: const FractionalTableSpanExtent(0.15), + extent: const FixedTableSpanExtent(100), ); } throw AssertionError( 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 @@ delegate.columnCount == null; // How far columns should be laid out in a given frame. double get _targetColumnPixel { - return cacheExtent + horizontalOffset.pixels + viewportDimension.width - _pinnedColumnsExtent; + return cacheExtent + + horizontalOffset.pixels + + viewportDimension.width - + _pinnedColumnsExtent; + } + + bool _rowNullTerminated = false; + bool get _rowsAreInfinite => delegate.rowCount == null; + // How far rows should be laid out in a given frame. + double get _targetRowPixel { + return cacheExtent + + verticalOffset.pixels + + viewportDimension.height - + _pinnedRowsExtent; } TableVicinity? get _firstNonPinnedCell { @@ -412,53 +427,62 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } // Updates the cached column metrics for the table. - void _updateColumnMetrics({int fromColumnIndex = 0, int? toColumnIndex}) { - _firstNonPinnedColumn = null; - _lastNonPinnedColumn = null; + void _updateColumnMetrics({bool appendColumns = false, int? toColumnIndex}) { + final Map oldColumnMetrics = _columnMetrics; + _columnMetrics.clear(); double startOfRegularColumn = 0; double startOfPinnedColumn = 0; - final Map newColumnMetrics = {}; - int column = fromColumnIndex; + if (appendColumns) { + // If fromColumnIndex is provided, we are only adding to the metrics we + // already know, since we lazily compile metrics. Copy over cached column + // metrics we aren't updating. This should only be the case when the + // number of columns is infinite, and saves us going through all the + // columns we already know about. + assert(_columnsAreInfinite); + assert(oldColumnMetrics.isNotEmpty); + for (int cachedColumn = 0; cachedColumn < oldColumnMetrics.length; cachedColumn++) { + _columnMetrics[cachedColumn] = oldColumnMetrics[cachedColumn]!; + } + if (_firstNonPinnedColumn != 0) { + startOfPinnedColumn = oldColumnMetrics[_firstNonPinnedColumn]!.trailingOffset; + } + startOfRegularColumn = oldColumnMetrics[_lastNonPinnedColumn]!.trailingOffset; + } + _firstNonPinnedColumn = null; + _lastNonPinnedColumn = null; + _columnNullTerminated = false; + int column = appendColumns ? oldColumnMetrics.length : 0; bool reachedColumnEnd() { - if (toColumnIndex != null) { - // Column metrics should be computed up to the provided index. - // Only relevant when we are filling in missing column metrics in an - // infinite context. - assert(delegate.columnCount == null); - return newColumnMetrics.length > toColumnIndex; - } else if (delegate.columnCount == null) { + if (_columnsAreInfinite) { + if (toColumnIndex != null) { + // Column metrics should be computed up to the provided index. + // Only relevant when we are filling in missing column metrics in an + // infinite context. + return _columnMetrics.length > toColumnIndex; + } // There are infinite columns, and no target index, compute metrics // up to what is visible and in the cache extent. return _lastNonPinnedColumn != null; } - // Infinite columns would be satisfied by cases above. - assert(delegate.columnCount != null); // Compute all the metrics if the columns are finite. - return column < delegate.columnCount!; - } - - if (fromColumnIndex != 0) { - // If fromColumnIndex is provided, we are only adding to the metrics we - // already know, since we lazily compile metrics. Copy over cached column - // metrics we aren't updating. This should only be the case when the - // number of columns is infinite. - assert(delegate.columnCount == null); - for (int cachedColumn = 0; cachedColumn < fromColumnIndex; cachedColumn++) { - newColumnMetrics[cachedColumn] = _columnMetrics[cachedColumn]!; - } + return column == delegate.columnCount!; } while (!reachedColumnEnd()) { final bool isPinned = column < delegate.pinnedColumnCount; final double leadingOffset = isPinned ? startOfPinnedColumn : startOfRegularColumn; - _Span? span = _columnMetrics.remove(column); - final TableSpan? configuration = needsDelegateRebuild + _Span? span = oldColumnMetrics.remove(column); + final TableSpan? configuration = needsDelegateRebuild || span == null ? delegate.buildColumn(column) - : span?.configuration; + : span.configuration; if (configuration == null) { - // We have reached the end of columns. + // We have reached the end of rows based on a null termination. This + // This happens when a row count has not been specified, but we have + // reached the end. + assert(_columnsAreInfinite); + _columnNullTerminated = true; break; } span ??= _Span(); @@ -473,7 +497,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ), ), ); - newColumnMetrics[column] = span; + _columnMetrics[column] = span; if (!isPinned) { if (span.trailingOffset >= horizontalOffset.pixels && _firstNonPinnedColumn == null) { @@ -490,29 +514,68 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { column++; } - assert(newColumnMetrics.length >= delegate.pinnedColumnCount); - for (final _Span span in _columnMetrics.values) { + assert(_columnMetrics.length >= delegate.pinnedColumnCount); + for (final _Span span in oldColumnMetrics.values) { span.dispose(); } - _columnMetrics = newColumnMetrics; } // Updates the cached row metrics for the table. - void _updateRowMetrics({int fromRowIndex = 0, int? toRowIndex}) { - _firstNonPinnedRow = null; - _lastNonPinnedRow = null; + void _updateRowMetrics({bool appendRows = false, int? toRowIndex}) { + final Map oldRowMetrics = _rowMetrics; + _rowMetrics.clear(); double startOfRegularRow = 0; double startOfPinnedRow = 0; + if (appendRows) { + // We are only adding to the metrics we already know, since we lazily + // compile metrics when we can. Copy over cached row metrics we aren't + // updating. This should only be the case when the number of rows is + // infinite. + assert(_rowsAreInfinite); + assert(oldRowMetrics.isNotEmpty); + for (int cachedRow = 0; cachedRow < oldRowMetrics.length; cachedRow++) { + _rowMetrics[cachedRow] = oldRowMetrics[cachedRow]!; + } + if (_firstNonPinnedRow != 0) { + startOfPinnedRow = oldRowMetrics[_firstNonPinnedRow]!.trailingOffset; + } + startOfRegularRow = oldRowMetrics[_lastNonPinnedRow]!.trailingOffset; + } + _firstNonPinnedRow = null; + _lastNonPinnedRow = null; + _rowNullTerminated = false; + int row = appendRows ? oldRowMetrics.length : 0; + + bool reachedRowEnd() { + if (_rowsAreInfinite) { + if (toRowIndex != null) { + // Row metrics should be computed up to the provided index. + // Only relevant when we are filling in missing column metrics in an + // infinite context. + return _rowMetrics.length > toRowIndex; + } + // There are infinite row, and no target index, compute metrics + // up to what is visible and in the cache extent. + return _lastNonPinnedRow != null; + } + // Compute all the metrics if the rows are finite. + return row == delegate.rowCount!; + } - final Map newRowMetrics = {}; - for (int row = 0; row < delegate.rowCount; row++) { + while (!reachedRowEnd()) { final bool isPinned = row < delegate.pinnedRowCount; final double leadingOffset = isPinned ? startOfPinnedRow : startOfRegularRow; - _Span? span = _rowMetrics.remove(row); - assert(needsDelegateRebuild || span != null); - final TableSpan configuration = - needsDelegateRebuild ? delegate.buildRow(row) : span.configuration; + _Span? span = oldRowMetrics.remove(row); + final TableSpan? configuration = needsDelegateRebuild || span == null ? delegate.buildRow(row) : span.configuration; + if (configuration == null) { + // We have reached the end of rows based on a null termination. This + // This happens when a row count has not been specified, but we have + // reached the end. + assert(_rowsAreInfinite); + _rowNullTerminated = true; + break; + } span ??= _Span(); span.update( isPinned: isPinned, @@ -525,17 +588,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ), ), ); - newRowMetrics[row] = span; + _rowMetrics[row] = span; if (!isPinned) { if (span.trailingOffset >= verticalOffset.pixels && _firstNonPinnedRow == null) { _firstNonPinnedRow = row; } - final double targetRowPixel = cacheExtent + - verticalOffset.pixels + - viewportDimension.height - - startOfPinnedRow; - if (span.trailingOffset >= targetRowPixel && + if (span.trailingOffset > _targetRowPixel && _lastNonPinnedRow == null) { _lastNonPinnedRow = row; } @@ -543,19 +602,20 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } else { startOfPinnedRow = span.trailingOffset; } + row++; } - assert(newRowMetrics.length >= delegate.pinnedRowCount); - for (final _Span span in _rowMetrics.values) { + + assert(_rowMetrics.length >= delegate.pinnedRowCount); + for (final _Span span in oldRowMetrics.values) { span.dispose(); } - _rowMetrics = newRowMetrics; } void _updateScrollBounds() { final double maxVerticalScrollExtent; - if (delegate.rowCount == null) { + if (_rowsAreInfinite && !_rowNullTerminated) { maxVerticalScrollExtent = double.infinity; - } else if (_rowMetrics.length <= delegate.pinnedRowCount) { + } else if (!_rowsAreInfinite && _rowMetrics.length <= delegate.pinnedRowCount) { assert(_firstNonPinnedRow == null && _lastNonPinnedRow == null); maxVerticalScrollExtent = 0.0; } else { @@ -572,9 +632,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } final double maxHorizontalScrollExtent; - if (delegate.columnCount == null) { + if (_columnsAreInfinite && !_columnNullTerminated) { maxHorizontalScrollExtent = double.infinity; - } else if (_columnMetrics.length <= delegate.pinnedColumnCount) { + } else if (!_columnsAreInfinite && + _columnMetrics.length <= delegate.pinnedColumnCount) { assert(_firstNonPinnedColumn == null && _lastNonPinnedColumn == null); maxHorizontalScrollExtent = 0.0; } else { @@ -600,19 +661,19 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // Uses the cached metrics to update the currently visible cells. void _updateFirstAndLastVisibleCell() { - _firstNonPinnedColumn = null; - _lastNonPinnedColumn = null; if (_columnMetrics.isNotEmpty) { _Span lastKnownColumn = _columnMetrics[_columnMetrics.length - 1]!; - if (_columnMetrics.length != delegate.columnCount || lastKnownColumn.trailingOffset < _targetColumnPixel) { + if (_columnsAreInfinite && lastKnownColumn.trailingOffset < _targetColumnPixel) { // This will add the column metrics we do not know about up to the // _targetColumnPixel, while keeping the ones we already know about. - _updateColumnMetrics(fromColumnIndex: _columnMetrics.length); - assert(_columnMetrics.length == delegate.columnCount || lastKnownColumn.trailingOffset >= _targetColumnPixel); - } + _updateColumnMetrics(appendColumns: true); lastKnownColumn = _columnMetrics[_columnMetrics.length - 1]!; + assert(_columnMetrics.length == delegate.columnCount || + lastKnownColumn.trailingOffset >= _targetColumnPixel); + } } - + _firstNonPinnedColumn = null; + _lastNonPinnedColumn = null; for (int column = 0; column < _columnMetrics.length; column++) { if (_columnMetrics[column]!.isPinned) { continue; @@ -631,12 +692,19 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _lastNonPinnedColumn ??= _columnMetrics.length - 1; } + if (_rowMetrics.isNotEmpty) { + _Span lastKnownRow = _rowMetrics[_rowMetrics.length - 1]!; + if (_rowsAreInfinite && lastKnownRow.trailingOffset < _targetRowPixel) { + // This will add the row metrics we do not know about up to the + // _targetRowPixel, while keeping the ones we already know about. + _updateRowMetrics(appendRows: true); + lastKnownRow = _rowMetrics[_rowMetrics.length - 1]!; + assert(_rowMetrics.length == delegate.rowCount || + lastKnownRow.trailingOffset >= _targetRowPixel); + } + } _firstNonPinnedRow = null; _lastNonPinnedRow = null; - final double targetRowPixel = cacheExtent + - verticalOffset.pixels + - viewportDimension.height - - _pinnedRowsExtent; for (int row = 0; row < _rowMetrics.length; row++) { if (_rowMetrics[row]!.isPinned) { continue; @@ -645,7 +713,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (endOfRow >= verticalOffset.pixels && _firstNonPinnedRow == null) { _firstNonPinnedRow = row; } - if (endOfRow >= targetRowPixel && _lastNonPinnedRow == null) { + if (endOfRow >= _targetRowPixel && _lastNonPinnedRow == null) { _lastNonPinnedRow = row; break; } @@ -783,6 +851,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { double rowOffset = -offset.dy; for (int row = start.row; row <= end.row; row += 1) { double columnOffset = -offset.dx; + assert(row < _rowMetrics.length); rowSpan = _rowMetrics[row]!; final double standardRowHeight = rowSpan.extent; double? mergedRowHeight; @@ -790,6 +859,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { rowOffset += rowSpan.configuration.padding.leading; for (int column = start.column; column <= end.column; column += 1) { + assert(column < _columnMetrics.length); colSpan = _columnMetrics[column]!; final double standardColumnWidth = colSpan.extent; double? mergedColumnWidth; @@ -862,6 +932,12 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { mergedRowOffset = baseRowOffset + _rowMetrics[firstRow]!.leadingOffset + _rowMetrics[firstRow]!.configuration.padding.leading; + if (_rowsAreInfinite && _rowMetrics[lastRow] == null) { + // The number of rows is infinte, and we have not calculated + // the metrics to the full extent of the merged cell. Update the + // metrics so we have all the information for the merged area. + _updateRowMetrics(appendRows: true, toRowIndex: lastRow); + } mergedRowHeight = _rowMetrics[lastRow]!.trailingOffset - _rowMetrics[firstRow]!.leadingOffset - _rowMetrics[lastRow]!.configuration.padding.trailing - @@ -884,12 +960,12 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _columnMetrics[firstColumn]!.leadingOffset + _columnMetrics[firstColumn]!.configuration.padding.leading; - if (delegate.columnCount == null && _columnMetrics[lastColumn] == null) { + if (_columnsAreInfinite && _columnMetrics[lastColumn] == null) { // The number of columns is infinte, and we have not calculated // the metrics to the full extent of the merged cell. Update the // metrics so we have all the information for the merged area. _updateColumnMetrics( - fromColumnIndex: _columnMetrics.length, + appendColumns: true, toColumnIndex: lastColumn, ); } 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 a8e23626efda..f2be9f9a44c0 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 @@ -14,6 +14,9 @@ import 'table_span.dart'; /// Used by the [TableCellDelegateMixin.buildColumn] and /// [TableCellDelegateMixin.buildRow] to configure rows and columns in the /// [TableView]. +/// +/// Returning null from this builder signifies the end of rows or columns being +/// built if a row or column count has not been specified for the table. typedef TableSpanBuilder = TableSpan? Function(int index); /// Signature for a function that creates a child [TableViewCell] for a given @@ -21,7 +24,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, ); @@ -45,7 +48,10 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// /// If the value returned by this getter changes throughout the lifetime of /// the delegate object, [notifyListeners] must be called. - // TODO(Piinks): Update all docs where making nullable + /// + /// When null, the number of columns will be infinite in number, unless null + /// is returned from [TableCellBuilderDelegate.columnBuilder]. The + /// [TableCellListDelegate] does not support an infinite number of columns. int? get columnCount; /// The number of rows that the table has content for. @@ -63,6 +69,10 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// /// If the value returned by this getter changes throughout the lifetime of /// the delegate object, [notifyListeners] must be called. + /// + /// When null, the number of rows will be infinite in number, unless null + /// is returned from [TableCellBuilderDelegate.rowBuilder]. The + /// [TableCellListDelegate] does not support an infinite number of rows. int? get rowCount; /// The number of columns that are permanently shown on the leading vertical @@ -102,13 +112,17 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// Builds the [TableSpan] that describes the column at the provided index. /// /// The builder must return a valid [TableSpan] for all indices smaller than - /// [columnCount]. + /// [columnCount]. If [columnCount] is null, the number of columns will be + /// infinite, unless this builder returns null to signal the end of the + /// columns. TableSpan? buildColumn(int index); /// Builds the [TableSpan] that describe the row at the provided index. /// /// The builder must return a valid [TableSpan] for all indices smaller than - /// [rowCount]. + /// [rowCount]. If [rowCount] is null, the number of rows will be + /// infinite, unless this builder returns null to signal the end of the + /// columns. TableSpan? buildRow(int index); } @@ -118,6 +132,11 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// Unlike the base [TwoDimensionalChildBuilderDelegate] this delegate does not /// automatically insert repaint boundaries. Instead, repaint boundaries are /// controlled by [TableViewCell.addRepaintBoundaries]. +/// +/// If the [rowCount] or [columnCount] is not provided, the number of rows +/// and/or columns will be infinite. Returning null from the [columnBuilder] +/// and/or [rowBuilder] in this case can terminate the number of rows and +/// columns at the given index. class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate with TableCellDelegateMixin { /// Creates a lazy building delegate to use with a [TableView]. @@ -167,7 +186,9 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate /// Builds the [TableSpan] that describes the column at the provided index. /// /// The builder must return a valid [TableSpan] for all indices smaller than - /// [columnCount]. + /// [columnCount]. If [columnCount] is null, the number of columns will be + /// infinite, unless this builder returns null to signal the end of the + /// columns. final TableSpanBuilder _columnBuilder; @override TableSpan? buildColumn(int index) => _columnBuilder(index); @@ -196,14 +217,16 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate assert(value == null || pinnedRowCount <= value); maxYIndex = switch (value) { null => value, - _ => value -1, + _ => value - 1, }; } /// Builds the [TableSpan] that describes the row at the provided index. /// /// The builder must return a valid [TableSpan] for all indices smaller than - /// [rowCount]. + /// [rowCount]. If [rowCount] is null, the number of rows will be + /// infinite, unless this builder returns null to signal the end of the + /// rows. final TableSpanBuilder _rowBuilder; @override TableSpan? buildRow(int index) => _rowBuilder(index); From ca141cd009d52ef86f5c823d546f9d44921ad6a5 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Mon, 25 Mar 2024 17:26:04 -0500 Subject: [PATCH 04/17] Docs --- .../lib/src/table_view/table.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 6cbcc87ff59d..feb269569073 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -41,6 +41,17 @@ import 'table_span.dart'; /// the [TableCellDelegateMixin]. The [TableView.builder] and [TableView.list] /// constructors create their own delegate. /// +/// Using a [TableCellBuilderDelegate], or the [TableView.builder] constructor +/// will allow for infinite rows and columns. Returning null from the +/// [columnBuilder] or [rowBuilder] in this case will terminate +/// the row or column at that index, representing the end of the table in that +/// axis. In this scenario, until the potential end of the table in either +/// dimension is reached by returning null, the +/// [ScrollPosition.maxScrollExtent] will reflect [double.infinity]. This is +/// because as the table is built lazily, it will not know the end has been +/// reached until the [ScrollPosition] arrives there. This is similar to +/// returning null from [ListView.builder] to signify the end of the list. +/// /// This example shows a TableView of 100 children, all sized 100 by 100 /// pixels with a few [TableSpanDecoration]s like background colors and borders. /// The `builder` constructor is called on demand for the cells that are visible @@ -116,6 +127,16 @@ class TableView extends TwoDimensionalScrollView { /// This constructor generates a [TableCellBuilderDelegate] for building /// children on demand using the required [cellBuilder], /// [columnBuilder], and [rowBuilder]. + /// + /// For infinite rows and columns, omit providing [columnCount] or [rowCount]. + /// Returning null from the [columnBuilder] or [rowBuilder] will terminate + /// the row or column at that index, representing the end of the table in that + /// axis. In this scenario, until the potential end of the table in either + /// dimension is reached by returning null, the + /// [ScrollPosition.maxScrollExtent] will reflect [double.infinity]. This is + /// because as the table is built lazily, it will not know the end has been + /// reached until the [ScrollPosition] arrives there. This is similar to + /// returning null from [ListView.builder] to signify the end of the list. TableView.builder({ super.key, super.primary, From fbfaf59793cf5747b0d0e02e610bcce7cb3bb66a Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Mon, 25 Mar 2024 17:54:16 -0500 Subject: [PATCH 05/17] Finished? --- .../example/lib/main.dart | 4 +- .../lib/src/table_view/table.dart | 72 +++++++++---------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index 8a3d60eaa8f1..d95554fd95ec 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -96,7 +96,7 @@ class _TableExampleState extends State { ); } - TableSpan _buildColumnSpan(int index) { + TableSpan? _buildColumnSpan(int index) { const TableSpanDecoration decoration = TableSpanDecoration( border: TableSpanBorder( trailing: BorderSide(), @@ -148,7 +148,7 @@ class _TableExampleState extends State { 'This should be unreachable, as every index is accounted for in the switch clauses.'); } - TableSpan _buildRowSpan(int index) { + TableSpan? _buildRowSpan(int index) { final TableSpanDecoration decoration = TableSpanDecoration( color: index.isEven ? Colors.purple[100] : null, border: const TableSpanBorder( 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 feb269569073..4059701637b1 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -330,7 +330,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { int? _lastNonPinnedRow; int? _lastNonPinnedColumn; - bool _columnNullTerminated = false; + int? _columnNullTerminated; bool get _columnsAreInfinite => delegate.columnCount == null; // How far columns should be laid out in a given frame. double get _targetColumnPixel { @@ -340,7 +340,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _pinnedColumnsExtent; } - bool _rowNullTerminated = false; + int? _rowNullTerminated; bool get _rowsAreInfinite => delegate.rowCount == null; // How far rows should be laid out in a given frame. double get _targetRowPixel { @@ -449,8 +449,6 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // Updates the cached column metrics for the table. void _updateColumnMetrics({bool appendColumns = false, int? toColumnIndex}) { - final Map oldColumnMetrics = _columnMetrics; - _columnMetrics.clear(); double startOfRegularColumn = 0; double startOfPinnedColumn = 0; if (appendColumns) { @@ -460,22 +458,23 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // number of columns is infinite, and saves us going through all the // columns we already know about. assert(_columnsAreInfinite); - assert(oldColumnMetrics.isNotEmpty); - for (int cachedColumn = 0; cachedColumn < oldColumnMetrics.length; cachedColumn++) { - _columnMetrics[cachedColumn] = oldColumnMetrics[cachedColumn]!; - } + assert(_columnMetrics.isNotEmpty); if (_firstNonPinnedColumn != 0) { - startOfPinnedColumn = oldColumnMetrics[_firstNonPinnedColumn]!.trailingOffset; + startOfPinnedColumn = _columnMetrics[_firstNonPinnedColumn]!.trailingOffset; } - startOfRegularColumn = oldColumnMetrics[_lastNonPinnedColumn]!.trailingOffset; + startOfRegularColumn = _columnMetrics[_lastNonPinnedColumn]!.trailingOffset; } _firstNonPinnedColumn = null; _lastNonPinnedColumn = null; - _columnNullTerminated = false; - int column = appendColumns ? oldColumnMetrics.length : 0; + _columnNullTerminated = needsDelegateRebuild ? null : _columnNullTerminated; + int column = appendColumns ? _columnMetrics.length : 0; bool reachedColumnEnd() { if (_columnsAreInfinite) { + if (_columnNullTerminated != null) { + _lastNonPinnedColumn ??= _columnMetrics.length - 1; + return true; + } if (toColumnIndex != null) { // Column metrics should be computed up to the provided index. // Only relevant when we are filling in missing column metrics in an @@ -494,7 +493,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final bool isPinned = column < delegate.pinnedColumnCount; final double leadingOffset = isPinned ? startOfPinnedColumn : startOfRegularColumn; - _Span? span = oldColumnMetrics.remove(column); + _Span? span = _columnMetrics.remove(column); final TableSpan? configuration = needsDelegateRebuild || span == null ? delegate.buildColumn(column) : span.configuration; @@ -503,7 +502,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // This happens when a row count has not been specified, but we have // reached the end. assert(_columnsAreInfinite); - _columnNullTerminated = true; + _columnNullTerminated = column; + _updateScrollBounds(); break; } span ??= _Span(); @@ -536,15 +536,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } assert(_columnMetrics.length >= delegate.pinnedColumnCount); - for (final _Span span in oldColumnMetrics.values) { - span.dispose(); - } } // Updates the cached row metrics for the table. void _updateRowMetrics({bool appendRows = false, int? toRowIndex}) { - final Map oldRowMetrics = _rowMetrics; - _rowMetrics.clear(); double startOfRegularRow = 0; double startOfPinnedRow = 0; if (appendRows) { @@ -553,22 +548,23 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // updating. This should only be the case when the number of rows is // infinite. assert(_rowsAreInfinite); - assert(oldRowMetrics.isNotEmpty); - for (int cachedRow = 0; cachedRow < oldRowMetrics.length; cachedRow++) { - _rowMetrics[cachedRow] = oldRowMetrics[cachedRow]!; - } + assert(_rowMetrics.isNotEmpty); if (_firstNonPinnedRow != 0) { - startOfPinnedRow = oldRowMetrics[_firstNonPinnedRow]!.trailingOffset; + startOfPinnedRow = _rowMetrics[_firstNonPinnedRow]!.trailingOffset; } - startOfRegularRow = oldRowMetrics[_lastNonPinnedRow]!.trailingOffset; + startOfRegularRow = _rowMetrics[_lastNonPinnedRow]!.trailingOffset; } _firstNonPinnedRow = null; _lastNonPinnedRow = null; - _rowNullTerminated = false; - int row = appendRows ? oldRowMetrics.length : 0; + _rowNullTerminated = needsDelegateRebuild ? null : _rowNullTerminated; + int row = appendRows ? _rowMetrics.length : 0; bool reachedRowEnd() { if (_rowsAreInfinite) { + if (_rowNullTerminated != null) { + _lastNonPinnedRow ??= _rowMetrics.length - 1; + return true; + } if (toRowIndex != null) { // Row metrics should be computed up to the provided index. // Only relevant when we are filling in missing column metrics in an @@ -587,14 +583,15 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final bool isPinned = row < delegate.pinnedRowCount; final double leadingOffset = isPinned ? startOfPinnedRow : startOfRegularRow; - _Span? span = oldRowMetrics.remove(row); + _Span? span = _rowMetrics.remove(row); final TableSpan? configuration = needsDelegateRebuild || span == null ? delegate.buildRow(row) : span.configuration; if (configuration == null) { // We have reached the end of rows based on a null termination. This // This happens when a row count has not been specified, but we have // reached the end. assert(_rowsAreInfinite); - _rowNullTerminated = true; + _rowNullTerminated = row; + _updateScrollBounds(); break; } span ??= _Span(); @@ -627,14 +624,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } assert(_rowMetrics.length >= delegate.pinnedRowCount); - for (final _Span span in oldRowMetrics.values) { - span.dispose(); - } } void _updateScrollBounds() { final double maxVerticalScrollExtent; - if (_rowsAreInfinite && !_rowNullTerminated) { + if (_rowsAreInfinite && _rowNullTerminated == null) { maxVerticalScrollExtent = double.infinity; } else if (!_rowsAreInfinite && _rowMetrics.length <= delegate.pinnedRowCount) { assert(_firstNonPinnedRow == null && _lastNonPinnedRow == null); @@ -653,7 +647,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } final double maxHorizontalScrollExtent; - if (_columnsAreInfinite && !_columnNullTerminated) { + if (_columnsAreInfinite && _columnNullTerminated == null) { maxHorizontalScrollExtent = double.infinity; } else if (!_columnsAreInfinite && _columnMetrics.length <= delegate.pinnedColumnCount) { @@ -690,7 +684,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _updateColumnMetrics(appendColumns: true); lastKnownColumn = _columnMetrics[_columnMetrics.length - 1]!; assert(_columnMetrics.length == delegate.columnCount || - lastKnownColumn.trailingOffset >= _targetColumnPixel); + lastKnownColumn.trailingOffset >= _targetColumnPixel || _columnNullTerminated != null); } } _firstNonPinnedColumn = null; @@ -721,7 +715,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _updateRowMetrics(appendRows: true); lastKnownRow = _rowMetrics[_rowMetrics.length - 1]!; assert(_rowMetrics.length == delegate.rowCount || - lastKnownRow.trailingOffset >= _targetRowPixel); + lastKnownRow.trailingOffset >= _targetRowPixel || _rowNullTerminated != null); } } _firstNonPinnedRow = null; @@ -1618,6 +1612,12 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _clipPinnedRowsHandle.layer = null; _clipPinnedColumnsHandle.layer = null; _clipCellsHandle.layer = null; + for (final _Span span in _rowMetrics.values) { + span.dispose(); + } + for (final _Span span in _columnMetrics.values) { + span.dispose(); + } super.dispose(); } } From 88d0757bdd1594ae25bf40b525cc9973a8867ee8 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 26 Mar 2024 11:47:59 -0500 Subject: [PATCH 06/17] Unrelated changes --- .../example/lib/main.dart | 28 +++++++++---------- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index d95554fd95ec..ff714af1e497 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -96,7 +96,7 @@ class _TableExampleState extends State { ); } - TableSpan? _buildColumnSpan(int index) { + TableSpan _buildColumnSpan(int index) { const TableSpanDecoration decoration = TableSpanDecoration( border: TableSpanBorder( trailing: BorderSide(), @@ -108,7 +108,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( @@ -121,34 +121,34 @@ class _TableExampleState extends State { case 1: return TableSpan( foregroundDecoration: decoration, - extent: const FixedTableSpanExtent(100), - // onEnter: (_) => print('Entered column $index'), + extent: const FractionalTableSpanExtent(0.5), + onEnter: (_) => print('Entered column $index'), cursor: SystemMouseCursors.contextMenu, ); case 2: return TableSpan( foregroundDecoration: decoration, - extent: const FixedTableSpanExtent(100), - // onEnter: (_) => print('Entered column $index'), + extent: const FixedTableSpanExtent(120), + onEnter: (_) => print('Entered column $index'), ); case 3: return TableSpan( foregroundDecoration: decoration, - extent: const FixedTableSpanExtent(100), - // onEnter: (_) => print('Entered column $index'), + extent: const FixedTableSpanExtent(145), + onEnter: (_) => print('Entered column $index'), ); case 4: return TableSpan( foregroundDecoration: decoration, - extent: const FixedTableSpanExtent(100), - // onEnter: (_) => print('Entered column $index'), + 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.'); } - TableSpan? _buildRowSpan(int index) { + TableSpan _buildRowSpan(int index) { final TableSpanDecoration decoration = TableSpanDecoration( color: index.isEven ? Colors.purple[100] : null, border: const TableSpanBorder( @@ -162,7 +162,7 @@ class _TableExampleState extends State { case 0: return TableSpan( backgroundDecoration: decoration, - extent: const FixedTableSpanExtent(100), + extent: const FixedTableSpanExtent(50), recognizerFactories: { TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( @@ -175,13 +175,13 @@ class _TableExampleState extends State { case 1: return TableSpan( backgroundDecoration: decoration, - extent: const FixedTableSpanExtent(100), + extent: const FixedTableSpanExtent(65), cursor: SystemMouseCursors.click, ); case 2: return TableSpan( backgroundDecoration: decoration, - extent: const FixedTableSpanExtent(100), + extent: const FractionalTableSpanExtent(0.15), ); } throw AssertionError( 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: Tue, 26 Mar 2024 18:01:14 -0500 Subject: [PATCH 07/17] Finsihed tests, need self review --- .../lib/src/table_view/table.dart | 60 +- .../test/table_view/table_test.dart | 1570 +++++++++++++++++ 2 files changed, 1618 insertions(+), 12 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 4059701637b1..fed9d3a426a7 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -449,6 +449,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // Updates the cached column metrics for the table. void _updateColumnMetrics({bool appendColumns = false, int? toColumnIndex}) { + assert(() { + if (toColumnIndex != null) { + // If we are computing up to an index, we must be appending. + return appendColumns; + } + return true; + }()); double startOfRegularColumn = 0; double startOfPinnedColumn = 0; if (appendColumns) { @@ -460,12 +467,14 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { assert(_columnsAreInfinite); assert(_columnMetrics.isNotEmpty); if (_firstNonPinnedColumn != 0) { - startOfPinnedColumn = _columnMetrics[_firstNonPinnedColumn]!.trailingOffset; + startOfPinnedColumn = + _columnMetrics[_firstNonPinnedColumn]!.trailingOffset; } - startOfRegularColumn = _columnMetrics[_lastNonPinnedColumn]!.trailingOffset; + startOfRegularColumn = + _columnMetrics[_lastNonPinnedColumn]!.trailingOffset; } - _firstNonPinnedColumn = null; - _lastNonPinnedColumn = null; + _firstNonPinnedColumn = toColumnIndex == null ? null : _firstNonPinnedColumn; + _lastNonPinnedColumn = toColumnIndex == null ? null : _lastNonPinnedColumn; _columnNullTerminated = needsDelegateRebuild ? null : _columnNullTerminated; int column = appendColumns ? _columnMetrics.length : 0; @@ -540,6 +549,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // Updates the cached row metrics for the table. void _updateRowMetrics({bool appendRows = false, int? toRowIndex}) { + assert(() { + if (toRowIndex != null) { + // If we are computing up to an index, we must be appending. + return appendRows; + } + return true; + }()); double startOfRegularRow = 0; double startOfPinnedRow = 0; if (appendRows) { @@ -554,8 +570,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } startOfRegularRow = _rowMetrics[_lastNonPinnedRow]!.trailingOffset; } - _firstNonPinnedRow = null; - _lastNonPinnedRow = null; + _firstNonPinnedRow = toRowIndex == null ? null : _firstNonPinnedRow; + _lastNonPinnedRow = toRowIndex == null ? null : _lastNonPinnedRow; _rowNullTerminated = needsDelegateRebuild ? null : _rowNullTerminated; int row = appendRows ? _rowMetrics.length : 0; @@ -584,7 +600,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final double leadingOffset = isPinned ? startOfPinnedRow : startOfRegularRow; _Span? span = _rowMetrics.remove(row); - final TableSpan? configuration = needsDelegateRebuild || span == null ? delegate.buildRow(row) : span.configuration; + final TableSpan? configuration = needsDelegateRebuild || span == null + ? delegate.buildRow(row) + : span.configuration; if (configuration == null) { // We have reached the end of rows based on a null termination. This // This happens when a row count has not been specified, but we have @@ -630,7 +648,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final double maxVerticalScrollExtent; if (_rowsAreInfinite && _rowNullTerminated == null) { maxVerticalScrollExtent = double.infinity; - } else if (!_rowsAreInfinite && _rowMetrics.length <= delegate.pinnedRowCount) { + } else if (!_rowsAreInfinite && + _rowMetrics.length <= delegate.pinnedRowCount) { assert(_firstNonPinnedRow == null && _lastNonPinnedRow == null); maxVerticalScrollExtent = 0.0; } else { @@ -678,13 +697,15 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { void _updateFirstAndLastVisibleCell() { if (_columnMetrics.isNotEmpty) { _Span lastKnownColumn = _columnMetrics[_columnMetrics.length - 1]!; - if (_columnsAreInfinite && lastKnownColumn.trailingOffset < _targetColumnPixel) { + if (_columnsAreInfinite && + lastKnownColumn.trailingOffset < _targetColumnPixel) { // This will add the column metrics we do not know about up to the // _targetColumnPixel, while keeping the ones we already know about. _updateColumnMetrics(appendColumns: true); - lastKnownColumn = _columnMetrics[_columnMetrics.length - 1]!; + lastKnownColumn = _columnMetrics[_columnMetrics.length - 1]!; assert(_columnMetrics.length == delegate.columnCount || - lastKnownColumn.trailingOffset >= _targetColumnPixel || _columnNullTerminated != null); + lastKnownColumn.trailingOffset >= _targetColumnPixel || + _columnNullTerminated != null); } } _firstNonPinnedColumn = null; @@ -715,7 +736,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _updateRowMetrics(appendRows: true); lastKnownRow = _rowMetrics[_rowMetrics.length - 1]!; assert(_rowMetrics.length == delegate.rowCount || - lastKnownRow.trailingOffset >= _targetRowPixel || _rowNullTerminated != null); + lastKnownRow.trailingOffset >= _targetRowPixel || + _rowNullTerminated != null); } } _firstNonPinnedRow = null; @@ -953,6 +975,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // metrics so we have all the information for the merged area. _updateRowMetrics(appendRows: true, toRowIndex: lastRow); } + assert( + _rowMetrics[lastRow] != null, + 'The merged cell containing $vicinity is missing TableSpan ' + 'information necessary for layout. The rowBuilder returned ' + 'null, signifying the end, at row $_rowNullTerminated but the ' + 'merged cell is configured to end with row $lastRow.', + ); mergedRowHeight = _rowMetrics[lastRow]!.trailingOffset - _rowMetrics[firstRow]!.leadingOffset - _rowMetrics[lastRow]!.configuration.padding.trailing - @@ -984,6 +1013,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { toColumnIndex: lastColumn, ); } + assert( + _columnMetrics[lastColumn] != null, + 'The merged cell containing $vicinity is missing TableSpan ' + 'information necessary for layout. The columnBuilder returned ' + 'null, signifying the end, at column $_columnNullTerminated but ' + 'the merged cell is configured to end with column $lastColumn.', + ); mergedColumnWidth = _columnMetrics[lastColumn]!.trailingOffset - _columnMetrics[firstColumn]!.leadingOffset - _columnMetrics[lastColumn]!.configuration.padding.trailing - 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 de08873cfb2a..e9ceff218ca3 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_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/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -181,6 +182,1575 @@ void main() { ); expect(tableView, isNull); }); + + group('Infinite spans - ', () { + late ScrollController verticalController; + late ScrollController horizontalController; + const TableSpan largeSpan = TableSpan(extent: FixedTableSpanExtent(200)); + + setUp(() { + verticalController = ScrollController(); + horizontalController = ScrollController(); + }); + + tearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + + TableView getTableView({ + int? columnCount, + int? rowCount, + TableSpanBuilder? columnBuilder, + TableSpanBuilder? rowBuilder, + TableViewCellBuilder? cellBuilder, + int pinnedColumnCount = 0, + int pinnedRowCount = 0, + }) { + return TableView.builder( + verticalDetails: ScrollableDetails.vertical( + controller: verticalController, + ), + horizontalDetails: ScrollableDetails.horizontal( + controller: horizontalController, + ), + columnCount: columnCount, + pinnedColumnCount: pinnedColumnCount, + columnBuilder: columnBuilder ?? (_) => largeSpan, + rowCount: rowCount, + pinnedRowCount: pinnedRowCount, + rowBuilder: rowBuilder ?? (_) => largeSpan, + cellBuilder: cellBuilder ?? (_, TableVicinity vicinity) { + return TableViewCell( + child: Text('R${vicinity.row}:C${vicinity.column}'), + ); + }, + ); + } + + testWidgets('infinte rows, columns are finite', + (WidgetTester tester) async { + // Nothing pinned --- + await tester.pumpWidget(MaterialApp( + home: getTableView(columnCount: 10), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the vetical scroll offset, valdiate more rows were populated. + verticalController.jumpTo(1000.0); + await tester.pump(); + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R9:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R9:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out before row 5, or after row 9. + expect(find.text('R0:C0'), findsNothing); + expect(find.text('R10:C0'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + columnCount: 10, + pinnedColumnCount: 1, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the vetical scroll offset, valdiate more rows were populated. + verticalController.jumpTo(1000.0); + await tester.pump(); + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R9:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R9:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out before row 5, or after row 9. + expect(find.text('R0:C0'), findsNothing); + expect(find.text('R10:C0'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + columnCount: 10, + pinnedRowCount: 1, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the vetical scroll offset, valdiate more rows were populated. + verticalController.jumpTo(1000.0); + await tester.pump(); + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R9:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R9:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out before row 5, or after row 9, except for pinned row 0. + expect(find.text('R0:C0'), findsOneWidget); + expect(find.text('R1:C0'), findsNothing); + expect(find.text('R10:C0'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns and rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + columnCount: 10, + pinnedColumnCount: 1, + pinnedRowCount: 1, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the vetical scroll offset, valdiate more rows were populated. + verticalController.jumpTo(1000.0); + await tester.pump(); + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R9:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R9:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out before row 5, or after row 9, except for pinned row 0. + expect(find.text('R0:C0'), findsOneWidget); + expect(find.text('R1:C0'), findsNothing); + expect(find.text('R10:C0'), findsNothing); + }); + + testWidgets('infinite columns, rows are finite', + (WidgetTester tester) async { + // Nothing pinned --- + await tester.pumpWidget(MaterialApp( + home: getTableView(rowCount: 10), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // Change the horizontal scroll offset, validate more columns were populated. + horizontalController.jumpTo(1200.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 1200.0); + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C11'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C11')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out before column 5, or after column 12. + expect(find.text('R0:C4'), findsNothing); + expect(find.text('R0:C12'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns --- + await tester.pumpWidget(MaterialApp( + home: getTableView(rowCount: 10, pinnedColumnCount: 1), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // Change the horizontal scroll offset, validate more columns were populated. + horizontalController.jumpTo(1200.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 1200.0); + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C11'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C11')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out before column 5, or after column 12, except for pinned column. + expect(find.text('R0:C0'), findsOneWidget); + expect(find.text('R0:C4'), findsNothing); + expect(find.text('R0:C12'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView(rowCount: 10, pinnedRowCount: 1), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // Change the horizontal scroll offset, validate more columns were populated. + horizontalController.jumpTo(1200.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 1200.0); + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C11'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C11')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out before column 5, or after column 12. + expect(find.text('R0:C4'), findsNothing); + expect(find.text('R0:C12'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns and rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowCount: 10, + pinnedRowCount: 1, + pinnedColumnCount: 1, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // Change the horizontal scroll offset, validate more columns were populated. + horizontalController.jumpTo(1200.0); + await tester.pump(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 1200.0); + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C11'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C11')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out before column 5, or after column 12, except for pinned column. + expect(find.text('R0:C0'), findsOneWidget); + expect(find.text('R0:C4'), findsNothing); + expect(find.text('R0:C12'), findsNothing); + }); + + testWidgets('infinite rows & columns', (WidgetTester tester) async { + // No pinned --- + await tester.pumpWidget(MaterialApp( + home: getTableView(), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5, no rows beyond row 4. + expect(find.text('R0:C6'), findsNothing); + expect(find.text('R5:C0'), findsNothing); + // Change both scroll offsets, validate more columns and rows were populated. + horizontalController.jumpTo(1200.0); + verticalController.jumpTo(1000.0); + await tester.pump(); + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 1200.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R5:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R9:C11'), findsOneWidget); + expect( + tester.getRect(find.text('R9:C11')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out before column 5, or after column 12. + expect(find.text('R5:C4'), findsNothing); + expect(find.text('R5:C12'), findsNothing); + // No rows laid out before row 4, or after row 9. + expect(find.text('R3:C6'), findsNothing); + expect(find.text('R10:C6'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns --- + await tester.pumpWidget(MaterialApp( + home: getTableView(pinnedColumnCount: 1), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5, no rows beyond row 4. + expect(find.text('R0:C6'), findsNothing); + expect(find.text('R5:C0'), findsNothing); + // Change both scroll offsets, validate more columns and rows were populated. + horizontalController.jumpTo(1200.0); + verticalController.jumpTo(1000.0); + await tester.pump(); + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 1200.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R5:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R9:C11'), findsOneWidget); + expect( + tester.getRect(find.text('R9:C11')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out before column 5, or after column 12, except for pinned first column. + expect(find.text('R5:C0'), findsOneWidget); + expect(find.text('R5:C4'), findsNothing); + expect(find.text('R5:C12'), findsNothing); + // No rows laid out before row 4, or after row 9. + expect(find.text('R3:C6'), findsNothing); + expect(find.text('R10:C6'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned Rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView(pinnedRowCount: 1), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5, no rows beyond row 4. + expect(find.text('R0:C6'), findsNothing); + expect(find.text('R5:C0'), findsNothing); + // Change both scroll offsets, validate more columns and rows were populated. + horizontalController.jumpTo(1200.0); + verticalController.jumpTo(1000.0); + await tester.pump(); + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 1200.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R5:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R9:C11'), findsOneWidget); + expect( + tester.getRect(find.text('R9:C11')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out before column 5, or after column 12. + expect(find.text('R5:C4'), findsNothing); + expect(find.text('R5:C12'), findsNothing); + // No rows laid out before row 4, or after row 9, except for pinned first row. + expect(find.text('R0:C6'), findsOneWidget); + expect(find.text('R3:C6'), findsNothing); + expect(find.text('R10:C6'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns and rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + pinnedRowCount: 1, + pinnedColumnCount: 1, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5, no rows beyond row 4. + expect(find.text('R0:C6'), findsNothing); + expect(find.text('R5:C0'), findsNothing); + // Change both scroll offsets, validate more columns and rows were populated. + horizontalController.jumpTo(1200.0); + verticalController.jumpTo(1000.0); + await tester.pump(); + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 1200.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R5:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R9:C11'), findsOneWidget); + expect( + tester.getRect(find.text('R9:C11')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out before column 5, or after column 12, except for pinned first column. + expect(find.text('R5:C0'), findsOneWidget); + expect(find.text('R5:C4'), findsNothing); + expect(find.text('R5:C12'), findsNothing); + // No rows laid out before row 4, or after row 9, except for pinned first row. + expect(find.text('R0:C6'), findsOneWidget); + expect(find.text('R3:C6'), findsNothing); + expect(find.text('R10:C6'), findsNothing); + }); + + testWidgets('infinite rows can null terminate', + (WidgetTester tester) async { + // Nothing pinned --- + bool calledOutOfBounds = false; + await tester.pumpWidget(MaterialApp( + home: getTableView( + columnCount: 10, + rowBuilder: (int index) { + // There will only be 8 rows. + if (index == 8) { + return null; + } + if (index > 8) { + calledOutOfBounds = true; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the vetical scroll offset, valdiate more rows were populated. + // This exceeds the bounds of the scroll view once the rows have been + // null terminated. + verticalController.jumpTo(1200.0); + await tester.pumpAndSettle(); + // Position was corrected. + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 0.0); + // After null terminating, the builder was not called further. + expect(calledOutOfBounds, isFalse); + // Max scroll extent was updated to reflect reaching the end of the rows + // after returning null. + expect(verticalController.position.maxScrollExtent, 1000.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R7:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R7:C4')), + const Rect.fromLTRB(800.0, 400.0, 1000.0, 600.0), + ); + // No rows laid out before row 5, or after row 7. + expect(find.text('R0:C0'), findsNothing); + expect(find.text('R8:C0'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + columnCount: 10, + pinnedColumnCount: 1, + rowBuilder: (int index) { + // There will only be 8 rows. + if (index == 8) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the vetical scroll offset, valdiate more rows were populated. + // This exceeds the bounds of the scroll view once the rows have been + // null terminated. + verticalController.jumpTo(1200.0); + await tester.pumpAndSettle(); + // Position was corrected. + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 0.0); + // Max scroll extent was updated to reflect reaching the end of the rows + // after returning null. + expect(verticalController.position.maxScrollExtent, 1000.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R7:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R7:C4')), + const Rect.fromLTRB(800.0, 400.0, 1000.0, 600.0), + ); + // No rows laid out before row 5, or after row 7. + expect(find.text('R0:C0'), findsNothing); + expect(find.text('R8:C0'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + columnCount: 10, + pinnedRowCount: 1, + rowBuilder: (int index) { + // There will only be 8 rows. + if (index == 8) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the vetical scroll offset, valdiate more rows were populated. + // This exceeds the bounds of the scroll view once the rows have been + // null terminated. + verticalController.jumpTo(1200.0); + await tester.pumpAndSettle(); + // Position was corrected. + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 0.0); + // Max scroll extent was updated to reflect reaching the end of the rows + // after returning null. + expect(verticalController.position.maxScrollExtent, 1000.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R7:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R7:C4')), + const Rect.fromLTRB(800.0, 400.0, 1000.0, 600.0), + ); + // No rows laid out before row 5, or after row 7, except for the first pinned row. + expect(find.text('R0:C0'), findsOneWidget); + expect(find.text('R1:C0'), findsNothing); + expect(find.text('R8:C0'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns and rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + columnCount: 10, + pinnedColumnCount: 1, + pinnedRowCount: 1, + rowBuilder: (int index) { + // There will only be 8 rows. + if (index == 8) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C4')), + const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), + ); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the vetical scroll offset, valdiate more rows were populated. + // This exceeds the bounds of the scroll view once the rows have been + // null terminated. + verticalController.jumpTo(1200.0); + await tester.pumpAndSettle(); + // Position was corrected. + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 0.0); + // Max scroll extent was updated to reflect reaching the end of the rows + // after returning null. + expect(verticalController.position.maxScrollExtent, 1000.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R7:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R7:C4')), + const Rect.fromLTRB(800.0, 400.0, 1000.0, 600.0), + ); + // No rows laid out before row 5, or after row 7, except for the first pinned row. + expect(find.text('R0:C0'), findsOneWidget); + expect(find.text('R1:C0'), findsNothing); + expect(find.text('R8:C0'), findsNothing); + }); + + testWidgets('infinite columns can null terminate', + (WidgetTester tester) async { + // Nothing pinned --- + bool calledOutOfBounds = false; + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowCount: 10, + columnBuilder: (int index) { + // There will only be 10 columns. + if (index == 10) { + return null; + } + if (index > 10) { + calledOutOfBounds = true; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, 1400.00); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // Change the horizontal scroll offset, valdiate more columns were populated. + // This exceeds the bounds of the scroll view once the columns have been + // null terminated. + horizontalController.jumpTo(1400.0); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + // Position was corrected. + expect(horizontalController.position.pixels, 1200.0); + // After null terminating, the builder was not called further. + expect(calledOutOfBounds, isFalse); + // Max scroll extent was updated to reflect reaching the end of the + // columns after returning null. + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C9'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C9')), + const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), + ); + // No rows laid out before column 5, or after column 9. + expect(find.text('R0:C4'), findsNothing); + expect(find.text('R0:C10'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowCount: 10, + pinnedColumnCount: 1, + columnBuilder: (int index) { + // There will only be 10 columns. + if (index == 10) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, 1400.00); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // Change the horizontal scroll offset, valdiate more columns were populated. + // This exceeds the bounds of the scroll view once the columns have been + // null terminated. + horizontalController.jumpTo(1400.0); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + // Position was corrected. + expect(horizontalController.position.pixels, 1200.0); + // Max scroll extent was updated to reflect reaching the end of the + // columns after returning null. + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C9'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C9')), + const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), + ); + // No rows laid out before column 5, or after column 9, except for the pinned first column. + expect(find.text('R0:C0'), findsOneWidget); + expect(find.text('R0:C4'), findsNothing); + expect(find.text('R0:C10'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowCount: 10, + pinnedRowCount: 1, + columnBuilder: (int index) { + // There will only be 10 columns. + if (index == 10) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, 1400.00); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // Change the horizontal scroll offset, valdiate more columns were populated. + // This exceeds the bounds of the scroll view once the columns have been + // null terminated. + horizontalController.jumpTo(1400.0); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + // Position was corrected. + expect(horizontalController.position.pixels, 1200.0); + // Max scroll extent was updated to reflect reaching the end of the + // columns after returning null. + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C9'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C9')), + const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), + ); + // No rows laid out before column 5, or after column 9. + expect(find.text('R0:C4'), findsNothing); + expect(find.text('R0:C10'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns and rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowCount: 10, + pinnedRowCount: 1, + pinnedColumnCount: 1, + columnBuilder: (int index) { + // There will only be 10 columns. + if (index == 10) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, 1400.00); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // Change the horizontal scroll offset, valdiate more columns were populated. + // This exceeds the bounds of the scroll view once the columns have been + // null terminated. + horizontalController.jumpTo(1400.0); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + // Position was corrected. + expect(horizontalController.position.pixels, 1200.0); + // Max scroll extent was updated to reflect reaching the end of the + // columns after returning null. + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R0:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C9'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C9')), + const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), + ); + // No rows laid out before column 5, or after column 9, except for the pinned first column. + expect(find.text('R0:C0'), findsOneWidget); + expect(find.text('R0:C4'), findsNothing); + expect(find.text('R0:C10'), findsNothing); + }); + testWidgets('infinite rows & columns can null terminate', + (WidgetTester tester) async { + // Nothing pinned --- + bool calledRowOutOfBounds = false; + bool calledColumnOutOfBounds = false; + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowBuilder: (int index) { + // There will only be 8 rows. + if (index == 8) { + return null; + } + if (index > 8) { + calledRowOutOfBounds = true; + } + return largeSpan; + }, + columnBuilder: (int index) { + // There will only be 10 columns. + if (index == 10) { + return null; + } + if (index > 10) { + calledColumnOutOfBounds = true; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the scroll offsets, valdiate more columns and rows were + // populated. + // These exceed the bounds of the scroll view once the column and row + // builders have null terminated. + horizontalController.jumpTo(1400.0); + verticalController.jumpTo(1200.0); + await tester.pumpAndSettle(); + // Position was corrected for both. + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 1200.0); + // After null terminating, the builders were not called further. + expect(calledRowOutOfBounds, isFalse); + expect(calledColumnOutOfBounds, isFalse); + // Max scroll extents were updated to reflect reaching the ends after + // returning null. + expect(verticalController.position.maxScrollExtent, 1000.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R7:C9'), findsOneWidget); + expect( + tester.getRect(find.text('R7:C9')), + const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), + ); + // No columns laid out before column 5, or after column 9. + expect(find.text('R5:C4'), findsNothing); + expect(find.text('R5:C10'), findsNothing); + // No rows laid out before row 4, or after row 7. + expect(find.text('R3:C6'), findsNothing); + expect(find.text('R8:C6'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowBuilder: (int index) { + // There will only be 8 rows. + if (index == 8) { + return null; + } + return largeSpan; + }, + pinnedColumnCount: 1, + columnBuilder: (int index) { + // There will only be 10 columns. + if (index == 10) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the scroll offsets, valdiate more columns and rows were + // populated. + // These exceed the bounds of the scroll view once the column and row + // builders have null terminated. + horizontalController.jumpTo(1400.0); + verticalController.jumpTo(1200.0); + await tester.pumpAndSettle(); + // Position was corrected for both. + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 1200.0); + // Max scroll extents were updated to reflect reaching the ends after + // returning null. + expect(verticalController.position.maxScrollExtent, 1000.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R7:C9'), findsOneWidget); + expect( + tester.getRect(find.text('R7:C9')), + const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), + ); + // No columns laid out before column 5, or after column 9, except for + // the pinned first column. + expect(find.text('R5:C0'), findsOneWidget); + expect(find.text('R5:C4'), findsNothing); + expect(find.text('R5:C10'), findsNothing); + // No rows laid out before row 4, or after row 7. + expect(find.text('R3:C6'), findsNothing); + expect(find.text('R8:C6'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowBuilder: (int index) { + // There will only be 8 rows. + if (index == 8) { + return null; + } + return largeSpan; + }, + pinnedRowCount: 1, + columnBuilder: (int index) { + // There will only be 10 columns. + if (index == 10) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the scroll offsets, valdiate more columns and rows were + // populated. + // These exceed the bounds of the scroll view once the column and row + // builders have null terminated. + horizontalController.jumpTo(1400.0); + verticalController.jumpTo(1200.0); + await tester.pumpAndSettle(); + // Position was corrected for both. + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 1200.0); + // Max scroll extents were updated to reflect reaching the ends after + // returning null. + expect(verticalController.position.maxScrollExtent, 1000.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R7:C9'), findsOneWidget); + expect( + tester.getRect(find.text('R7:C9')), + const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), + ); + // No columns laid out before column 5, or after column 9. + expect(find.text('R5:C4'), findsNothing); + expect(find.text('R5:C10'), findsNothing); + // No rows laid out before row 4, or after row 7, except for first + // pinned row. + expect(find.text('R0:C6'), findsOneWidget); + expect(find.text('R3:C6'), findsNothing); + expect(find.text('R8:C6'), findsNothing); + + await tester.pumpWidget(Container()); + + // Pinned columns and rows --- + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowBuilder: (int index) { + // There will only be 8 rows. + if (index == 8) { + return null; + } + return largeSpan; + }, + pinnedRowCount: 1, + pinnedColumnCount: 1, + columnBuilder: (int index) { + // There will only be 10 columns. + if (index == 10) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C5')), + const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), + ); + // No columns laid out beyond column 5. + expect(find.text('R0:C6'), findsNothing); + // No rows laid out beyond row 4. + expect(find.text('R5:C0'), findsNothing); + // Change the scroll offsets, valdiate more columns and rows were + // populated. + // These exceed the bounds of the scroll view once the column and row + // builders have null terminated. + horizontalController.jumpTo(1400.0); + verticalController.jumpTo(1200.0); + await tester.pumpAndSettle(); + // Position was corrected for both. + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 1200.0); + // Max scroll extents were updated to reflect reaching the ends after + // returning null. + expect(verticalController.position.maxScrollExtent, 1000.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C6'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C6')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R7:C9'), findsOneWidget); + expect( + tester.getRect(find.text('R7:C9')), + const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0), + ); + // No columns laid out before column 5, or after column 9, except for + //the first pinned column. + expect(find.text('R5:C0'), findsOneWidget); + expect(find.text('R5:C4'), findsNothing); + expect(find.text('R5:C10'), findsNothing); + // No rows laid out before row 4, or after row 7, except for first + // pinned row. + expect(find.text('R0:C6'), findsOneWidget); + expect(find.text('R3:C6'), findsNothing); + expect(find.text('R8:C6'), findsNothing); + }); + testWidgets('merged cells work with lazy layout computation', + (WidgetTester tester) async { + // When columns and rows are finite, the layout is eagerly computed and + // the children are lazily laid out. This makes computing merged cell + // layouts easy. In an infinite world, not all of the information may be + // available for a merged cell if it extends into an area we have not + // computed the layout for yet. + const ({int start, int span}) rowConfig = (start: 0, span: 10); + final List mergedRows = List.generate(10, (int index) => index,); + const ({int start, int span}) columnConfig = (start: 1, span: 10); + final List mergedColumns = List.generate(10, (int index) => index + 1,); + await tester.pumpWidget(MaterialApp( + home: getTableView( + cellBuilder: (_, TableVicinity vicinity) { + // Merged row + if (mergedRows.contains(vicinity.row) && vicinity.column == 0) { + return TableViewCell( + rowMergeStart: rowConfig.start, + rowMergeSpan: rowConfig.span, + child: const Text('R0:C0'), + ); + } + // Merged column + if (mergedColumns.contains(vicinity.column) && vicinity.row == 0) { + return TableViewCell( + columnMergeStart: columnConfig.start, + columnMergeSpan: columnConfig.span, + child: const Text('R0:C1'), + ); + } + return TableViewCell( + child: Text('R${vicinity.row}:C${vicinity.column}'), + ); + }, + ), + )); + await tester.pumpAndSettle(); + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 0.0); + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, double.infinity); + expect(find.text('R0:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 2000.0), + ); + expect(find.text('R0:C1'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C1')), + const Rect.fromLTRB(200.0, 0.0, 2200.0, 200.0), + ); + expect(find.text('R1:C1'), findsOneWidget); + expect( + tester.getRect(find.text('R1:C1')), + const Rect.fromLTRB(200.0, 200.0, 400.0, 400.0), + ); + }); + + testWidgets('merged column that exceeds metrics will assert', + (WidgetTester tester) async { + final List exceptions = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + const ({int start, int span}) columnConfig = (start: 1, span: 10); + final List mergedColumns = List.generate(10, (int index) => index + 1,); + await tester.pumpWidget(MaterialApp( + home: getTableView( + columnBuilder: (int index) { + // There will only be 8 columns, but the merge is set up for 10. + if (index == 8) { + return null; + } + return largeSpan; + }, + cellBuilder: (_, TableVicinity vicinity) { + // Merged column + if (mergedColumns.contains(vicinity.column) && vicinity.row == 0) { + return TableViewCell( + columnMergeStart: columnConfig.start, + columnMergeSpan: columnConfig.span, + child: const Text('R0:C1'), + ); + } + return TableViewCell( + child: Text('R${vicinity.row}:C${vicinity.column}'), + ); + }, + ), + )); + await tester.pumpWidget(Container()); + FlutterError.onError = oldHandler; + expect(exceptions.length, 3); + expect( + exceptions.first.toString(), + contains( + 'The merged cell containing (row: 0, column: 1) is ' + 'missing TableSpan information necessary for layout. The ' + 'columnBuilder returned null, signifying the end, at column 8 but ' + 'the merged cell is configured to end with column 10.', + ), + ); + }); + + testWidgets('merged row that exceeds metrics will assert', + (WidgetTester tester) async { + final List exceptions = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + const ({int start, int span}) rowConfig = (start: 0, span: 10); + final List mergedRows = List.generate(10, (int index) => index,); + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowBuilder: (int index) { + // There will only be 8 rows, but the merge is set up for 9. + if (index == 8) { + return null; + } + return largeSpan; + }, + cellBuilder: (_, TableVicinity vicinity) { + // Merged column + if (mergedRows.contains(vicinity.row) && vicinity.column == 0) { + return TableViewCell( + rowMergeStart: rowConfig.start, + rowMergeSpan: rowConfig.span, + child: const Text('R0:C0'), + ); + } + return TableViewCell( + child: Text('R${vicinity.row}:C${vicinity.column}'), + ); + }, + ), + )); + await tester.pumpWidget(Container()); + FlutterError.onError = oldHandler; + expect(exceptions.length, 3); + expect( + exceptions.first.toString(), + contains( + 'The merged cell containing (row: 0, column: 0) is ' + 'missing TableSpan information necessary for layout. The ' + 'rowBuilder returned null, signifying the end, at row 8 but ' + 'the merged cell is configured to end with row 9.', + ), + ); + }); + }); }); group('TableView.list', () { From bbb3520dd16638b7651884e46eb1c39b05a54d1b Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 26 Mar 2024 18:03:56 -0500 Subject: [PATCH 08/17] Broke some stuff, investigating --- .../lib/src/table_view/table.dart | 7 +- .../lib/src/table_view/table_delegate.dart | 2 + .../test/table_view/table_test.dart | 113 ++++++++++-------- 3 files changed, 70 insertions(+), 52 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 fed9d3a426a7..ca998bf60040 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -323,8 +323,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final List _mergedColumns = []; // Cached Table metrics - Map _columnMetrics = {}; - Map _rowMetrics = {}; + final Map _columnMetrics = {}; + final Map _rowMetrics = {}; int? _firstNonPinnedRow; int? _firstNonPinnedColumn; int? _lastNonPinnedRow; @@ -473,7 +473,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { startOfRegularColumn = _columnMetrics[_lastNonPinnedColumn]!.trailingOffset; } - _firstNonPinnedColumn = toColumnIndex == null ? null : _firstNonPinnedColumn; + _firstNonPinnedColumn = + toColumnIndex == null ? null : _firstNonPinnedColumn; _lastNonPinnedColumn = toColumnIndex == null ? null : _lastNonPinnedColumn; _columnNullTerminated = needsDelegateRebuild ? null : _columnNullTerminated; int column = appendColumns ? _columnMetrics.length : 0; 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 f2be9f9a44c0..fe5cfd685f41 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 @@ -175,6 +175,7 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate _ => maxXIndex! + 1, }; } + set columnCount(int? value) { assert(value == null || pinnedColumnCount <= value); maxXIndex = switch (value) { @@ -213,6 +214,7 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate _ => maxYIndex! + 1, }; } + set rowCount(int? value) { assert(value == null || pinnedRowCount <= value); maxYIndex = switch (value) { 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 e9ceff218ca3..63788b59ae5f 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -220,11 +220,12 @@ void main() { rowCount: rowCount, pinnedRowCount: pinnedRowCount, rowBuilder: rowBuilder ?? (_) => largeSpan, - cellBuilder: cellBuilder ?? (_, TableVicinity vicinity) { - return TableViewCell( - child: Text('R${vicinity.row}:C${vicinity.column}'), - ); - }, + cellBuilder: cellBuilder ?? + (_, TableVicinity vicinity) { + return TableViewCell( + child: Text('R${vicinity.row}:C${vicinity.column}'), + ); + }, ); } @@ -1607,32 +1608,39 @@ void main() { // available for a merged cell if it extends into an area we have not // computed the layout for yet. const ({int start, int span}) rowConfig = (start: 0, span: 10); - final List mergedRows = List.generate(10, (int index) => index,); + final List mergedRows = List.generate( + 10, + (int index) => index, + ); const ({int start, int span}) columnConfig = (start: 1, span: 10); - final List mergedColumns = List.generate(10, (int index) => index + 1,); + final List mergedColumns = List.generate( + 10, + (int index) => index + 1, + ); await tester.pumpWidget(MaterialApp( home: getTableView( cellBuilder: (_, TableVicinity vicinity) { - // Merged row - if (mergedRows.contains(vicinity.row) && vicinity.column == 0) { - return TableViewCell( - rowMergeStart: rowConfig.start, - rowMergeSpan: rowConfig.span, - child: const Text('R0:C0'), - ); - } - // Merged column - if (mergedColumns.contains(vicinity.column) && vicinity.row == 0) { + // Merged row + if (mergedRows.contains(vicinity.row) && vicinity.column == 0) { + return TableViewCell( + rowMergeStart: rowConfig.start, + rowMergeSpan: rowConfig.span, + child: const Text('R0:C0'), + ); + } + // Merged column + if (mergedColumns.contains(vicinity.column) && + vicinity.row == 0) { + return TableViewCell( + columnMergeStart: columnConfig.start, + columnMergeSpan: columnConfig.span, + child: const Text('R0:C1'), + ); + } return TableViewCell( - columnMergeStart: columnConfig.start, - columnMergeSpan: columnConfig.span, - child: const Text('R0:C1'), + child: Text('R${vicinity.row}:C${vicinity.column}'), ); - } - return TableViewCell( - child: Text('R${vicinity.row}:C${vicinity.column}'), - ); - }, + }, ), )); await tester.pumpAndSettle(); @@ -1659,13 +1667,16 @@ void main() { testWidgets('merged column that exceeds metrics will assert', (WidgetTester tester) async { - final List exceptions = []; + final List exceptions = []; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails details) { exceptions.add(details.exception); }; const ({int start, int span}) columnConfig = (start: 1, span: 10); - final List mergedColumns = List.generate(10, (int index) => index + 1,); + final List mergedColumns = List.generate( + 10, + (int index) => index + 1, + ); await tester.pumpWidget(MaterialApp( home: getTableView( columnBuilder: (int index) { @@ -1676,18 +1687,19 @@ void main() { return largeSpan; }, cellBuilder: (_, TableVicinity vicinity) { - // Merged column - if (mergedColumns.contains(vicinity.column) && vicinity.row == 0) { + // Merged column + if (mergedColumns.contains(vicinity.column) && + vicinity.row == 0) { + return TableViewCell( + columnMergeStart: columnConfig.start, + columnMergeSpan: columnConfig.span, + child: const Text('R0:C1'), + ); + } return TableViewCell( - columnMergeStart: columnConfig.start, - columnMergeSpan: columnConfig.span, - child: const Text('R0:C1'), + child: Text('R${vicinity.row}:C${vicinity.column}'), ); - } - return TableViewCell( - child: Text('R${vicinity.row}:C${vicinity.column}'), - ); - }, + }, ), )); await tester.pumpWidget(Container()); @@ -1706,13 +1718,16 @@ void main() { testWidgets('merged row that exceeds metrics will assert', (WidgetTester tester) async { - final List exceptions = []; + final List exceptions = []; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails details) { exceptions.add(details.exception); }; const ({int start, int span}) rowConfig = (start: 0, span: 10); - final List mergedRows = List.generate(10, (int index) => index,); + final List mergedRows = List.generate( + 10, + (int index) => index, + ); await tester.pumpWidget(MaterialApp( home: getTableView( rowBuilder: (int index) { @@ -1723,18 +1738,18 @@ void main() { return largeSpan; }, cellBuilder: (_, TableVicinity vicinity) { - // Merged column - if (mergedRows.contains(vicinity.row) && vicinity.column == 0) { + // Merged column + if (mergedRows.contains(vicinity.row) && vicinity.column == 0) { + return TableViewCell( + rowMergeStart: rowConfig.start, + rowMergeSpan: rowConfig.span, + child: const Text('R0:C0'), + ); + } return TableViewCell( - rowMergeStart: rowConfig.start, - rowMergeSpan: rowConfig.span, - child: const Text('R0:C0'), + child: Text('R${vicinity.row}:C${vicinity.column}'), ); - } - return TableViewCell( - child: Text('R${vicinity.row}:C${vicinity.column}'), - ); - }, + }, ), )); await tester.pumpWidget(Container()); From 91c282e941be27377c299ffebdccb4a7d8750119 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 26 Mar 2024 18:58:16 -0500 Subject: [PATCH 09/17] Fixed, still needs self review --- .../lib/src/table_view/table.dart | 7 ++++++- 1 file changed, 6 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 ca998bf60040..45d973f9afc3 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -770,6 +770,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (needsDelegateRebuild || didResize) { // Recomputes the table metrics, invalidates any cached information. + _columnMetrics.clear(); + _rowMetrics.clear(); _updateColumnMetrics(); _updateRowMetrics(); _updateScrollBounds(); @@ -1221,7 +1223,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // 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)); + assert( + _mergedVicinities.keys.contains(vicinity), + 'The vicinity $vicinity is not accounted for as covered by a merged cell.', + ); final TableVicinity mergedVicinity = _mergedVicinities[vicinity]!; // This vicinity must resolve to a child, unless something has gone wrong! return getChildFor( From a9f18d3e3d89d4fe5c2160a9b10a958ce3e765d2 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 27 Mar 2024 12:59:29 -0500 Subject: [PATCH 10/17] Self review --- .../lib/src/table_view/table.dart | 66 +++++----- .../test/table_view/table_test.dart | 123 ++++++++++-------- 2 files changed, 106 insertions(+), 83 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 45d973f9afc3..893c93532ceb 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -41,8 +41,9 @@ import 'table_span.dart'; /// the [TableCellDelegateMixin]. The [TableView.builder] and [TableView.list] /// constructors create their own delegate. /// -/// Using a [TableCellBuilderDelegate], or the [TableView.builder] constructor -/// will allow for infinite rows and columns. Returning null from the +/// A table with infinite rows and columns can be made by using a +/// [TableCellBuilderDelegate], or the [TableView.builder] constructor, and +/// omitting the row or column count. Returning null from the /// [columnBuilder] or [rowBuilder] in this case will terminate /// the row or column at that index, representing the end of the table in that /// axis. In this scenario, until the potential end of the table in either @@ -456,22 +457,19 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } return true; }()); - double startOfRegularColumn = 0; - double startOfPinnedColumn = 0; + double startOfRegularColumn = 0.0; + double startOfPinnedColumn = 0.0; if (appendColumns) { - // If fromColumnIndex is provided, we are only adding to the metrics we - // already know, since we lazily compile metrics. Copy over cached column - // metrics we aren't updating. This should only be the case when the + // We are only adding to the metrics we already know, since we are lazily + // compiling metrics. This should only be the case when the // number of columns is infinite, and saves us going through all the // columns we already know about. assert(_columnsAreInfinite); assert(_columnMetrics.isNotEmpty); - if (_firstNonPinnedColumn != 0) { - startOfPinnedColumn = - _columnMetrics[_firstNonPinnedColumn]!.trailingOffset; - } + startOfPinnedColumn = + _columnMetrics[_firstNonPinnedColumn]?.trailingOffset ?? 0.0; startOfRegularColumn = - _columnMetrics[_lastNonPinnedColumn]!.trailingOffset; + _columnMetrics[_lastNonPinnedColumn]?.trailingOffset ?? 0.0; } _firstNonPinnedColumn = toColumnIndex == null ? null : _firstNonPinnedColumn; @@ -504,13 +502,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final double leadingOffset = isPinned ? startOfPinnedColumn : startOfRegularColumn; _Span? span = _columnMetrics.remove(column); - final TableSpan? configuration = needsDelegateRebuild || span == null - ? delegate.buildColumn(column) - : span.configuration; + final TableSpan? configuration = + span?.configuration ?? delegate.buildColumn(column); if (configuration == null) { - // We have reached the end of rows based on a null termination. This - // This happens when a row count has not been specified, but we have - // reached the end. + // We have reached the end of columns based on a null termination. This + // This happens when a column count has not been specified. assert(_columnsAreInfinite); _columnNullTerminated = column; _updateScrollBounds(); @@ -557,19 +553,17 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } return true; }()); - double startOfRegularRow = 0; - double startOfPinnedRow = 0; + double startOfRegularRow = 0.0; + double startOfPinnedRow = 0.0; if (appendRows) { - // We are only adding to the metrics we already know, since we lazily - // compile metrics when we can. Copy over cached row metrics we aren't - // updating. This should only be the case when the number of rows is - // infinite. + // We are only adding to the metrics we already know, since we are lazily + // compiling metrics. This should only be the case when the + // number of rows is infinite, and saves us going through all the + // rows we already know about. assert(_rowsAreInfinite); assert(_rowMetrics.isNotEmpty); - if (_firstNonPinnedRow != 0) { - startOfPinnedRow = _rowMetrics[_firstNonPinnedRow]!.trailingOffset; - } - startOfRegularRow = _rowMetrics[_lastNonPinnedRow]!.trailingOffset; + startOfPinnedRow = _rowMetrics[_firstNonPinnedRow]?.trailingOffset ?? 0.0; + startOfRegularRow = _rowMetrics[_lastNonPinnedRow]?.trailingOffset ?? 0.0; } _firstNonPinnedRow = toRowIndex == null ? null : _firstNonPinnedRow; _lastNonPinnedRow = toRowIndex == null ? null : _lastNonPinnedRow; @@ -601,9 +595,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final double leadingOffset = isPinned ? startOfPinnedRow : startOfRegularRow; _Span? span = _rowMetrics.remove(row); - final TableSpan? configuration = needsDelegateRebuild || span == null - ? delegate.buildRow(row) - : span.configuration; + final TableSpan? configuration = + span?.configuration ?? delegate.buildRow(row); if (configuration == null) { // We have reached the end of rows based on a null termination. This // This happens when a row count has not been specified, but we have @@ -694,7 +687,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } } - // Uses the cached metrics to update the currently visible cells. + // Uses the cached metrics to update the currently visible cells. If the + // number of rows or columns are infinte, the layout is computed lazily, so + // this will call for an update to the metrics if we have scrolled beyond the + // layout portion we know about. void _updateFirstAndLastVisibleCell() { if (_columnMetrics.isNotEmpty) { _Span lastKnownColumn = _columnMetrics[_columnMetrics.length - 1]!; @@ -770,7 +766,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (needsDelegateRebuild || didResize) { // Recomputes the table metrics, invalidates any cached information. + for (final _Span span in _columnMetrics.values) { + span.dispose(); + } _columnMetrics.clear(); + for (final _Span span in _rowMetrics.values) { + span.dispose(); + } _rowMetrics.clear(); _updateColumnMetrics(); _updateRowMetrics(); 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 63788b59ae5f..873947e44f15 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -229,7 +229,7 @@ void main() { ); } - testWidgets('infinte rows, columns are finite', + testWidgets('infinite rows, columns are finite', (WidgetTester tester) async { // Nothing pinned --- await tester.pumpWidget(MaterialApp( @@ -252,7 +252,7 @@ void main() { ); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the vetical scroll offset, valdiate more rows were populated. + // Change the vertical scroll offset, validate more rows were populated. verticalController.jumpTo(1000.0); await tester.pump(); expect(verticalController.position.pixels, 1000.0); @@ -299,7 +299,7 @@ void main() { ); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the vetical scroll offset, valdiate more rows were populated. + // Change the vertical scroll offset, validate more rows were populated. verticalController.jumpTo(1000.0); await tester.pump(); expect(verticalController.position.pixels, 1000.0); @@ -346,7 +346,7 @@ void main() { ); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the vetical scroll offset, valdiate more rows were populated. + // Change the vertical scroll offset, validate more rows were populated. verticalController.jumpTo(1000.0); await tester.pump(); expect(verticalController.position.pixels, 1000.0); @@ -363,7 +363,8 @@ void main() { tester.getRect(find.text('R9:C4')), const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), ); - // No rows laid out before row 5, or after row 9, except for pinned row 0. + // No rows laid out before row 5, or after row 9, except for pinned row + // 0. expect(find.text('R0:C0'), findsOneWidget); expect(find.text('R1:C0'), findsNothing); expect(find.text('R10:C0'), findsNothing); @@ -395,7 +396,7 @@ void main() { ); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the vetical scroll offset, valdiate more rows were populated. + // Change the vertical scroll offset, validate more rows were populated. verticalController.jumpTo(1000.0); await tester.pump(); expect(verticalController.position.pixels, 1000.0); @@ -412,7 +413,8 @@ void main() { tester.getRect(find.text('R9:C4')), const Rect.fromLTRB(800.0, 800.0, 1000.0, 1000.0), ); - // No rows laid out before row 5, or after row 9, except for pinned row 0. + // No rows laid out before row 5, or after row 9, except for pinned row + // 0. expect(find.text('R0:C0'), findsOneWidget); expect(find.text('R1:C0'), findsNothing); expect(find.text('R10:C0'), findsNothing); @@ -441,7 +443,8 @@ void main() { ); // No columns laid out beyond column 5. expect(find.text('R0:C6'), findsNothing); - // Change the horizontal scroll offset, validate more columns were populated. + // Change the horizontal scroll offset, validate more columns were + // populated. horizontalController.jumpTo(1200.0); await tester.pump(); expect(verticalController.position.pixels, 0.0); @@ -485,7 +488,8 @@ void main() { ); // No columns laid out beyond column 5. expect(find.text('R0:C6'), findsNothing); - // Change the horizontal scroll offset, validate more columns were populated. + // Change the horizontal scroll offset, validate more columns were + // populated. horizontalController.jumpTo(1200.0); await tester.pump(); expect(verticalController.position.pixels, 0.0); @@ -502,7 +506,8 @@ void main() { tester.getRect(find.text('R4:C11')), const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), ); - // No columns laid out before column 5, or after column 12, except for pinned column. + // No columns laid out before column 5, or after column 12, except for + // pinned column. expect(find.text('R0:C0'), findsOneWidget); expect(find.text('R0:C4'), findsNothing); expect(find.text('R0:C12'), findsNothing); @@ -530,7 +535,8 @@ void main() { ); // No columns laid out beyond column 5. expect(find.text('R0:C6'), findsNothing); - // Change the horizontal scroll offset, validate more columns were populated. + // Change the horizontal scroll offset, validate more columns were + // populated. horizontalController.jumpTo(1200.0); await tester.pump(); expect(verticalController.position.pixels, 0.0); @@ -578,7 +584,8 @@ void main() { ); // No columns laid out beyond column 5. expect(find.text('R0:C6'), findsNothing); - // Change the horizontal scroll offset, validate more columns were populated. + // Change the horizontal scroll offset, validate more columns were + // populated. horizontalController.jumpTo(1200.0); await tester.pump(); expect(verticalController.position.pixels, 0.0); @@ -595,7 +602,8 @@ void main() { tester.getRect(find.text('R4:C11')), const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), ); - // No columns laid out before column 5, or after column 12, except for pinned column. + // No columns laid out before column 5, or after column 12, except for + // pinned column. expect(find.text('R0:C0'), findsOneWidget); expect(find.text('R0:C4'), findsNothing); expect(find.text('R0:C12'), findsNothing); @@ -624,7 +632,8 @@ void main() { // No columns laid out beyond column 5, no rows beyond row 4. expect(find.text('R0:C6'), findsNothing); expect(find.text('R5:C0'), findsNothing); - // Change both scroll offsets, validate more columns and rows were populated. + // Change both scroll offsets, validate more columns and rows were + // populated. horizontalController.jumpTo(1200.0); verticalController.jumpTo(1000.0); await tester.pump(); @@ -673,7 +682,8 @@ void main() { // No columns laid out beyond column 5, no rows beyond row 4. expect(find.text('R0:C6'), findsNothing); expect(find.text('R5:C0'), findsNothing); - // Change both scroll offsets, validate more columns and rows were populated. + // Change both scroll offsets, validate more columns and rows were + // populated. horizontalController.jumpTo(1200.0); verticalController.jumpTo(1000.0); await tester.pump(); @@ -691,7 +701,8 @@ void main() { tester.getRect(find.text('R9:C11')), const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), ); - // No columns laid out before column 5, or after column 12, except for pinned first column. + // No columns laid out before column 5, or after column 12, except for + // pinned first column. expect(find.text('R5:C0'), findsOneWidget); expect(find.text('R5:C4'), findsNothing); expect(find.text('R5:C12'), findsNothing); @@ -723,7 +734,8 @@ void main() { // No columns laid out beyond column 5, no rows beyond row 4. expect(find.text('R0:C6'), findsNothing); expect(find.text('R5:C0'), findsNothing); - // Change both scroll offsets, validate more columns and rows were populated. + // Change both scroll offsets, validate more columns and rows were + // populated. horizontalController.jumpTo(1200.0); verticalController.jumpTo(1000.0); await tester.pump(); @@ -744,7 +756,8 @@ void main() { // No columns laid out before column 5, or after column 12. expect(find.text('R5:C4'), findsNothing); expect(find.text('R5:C12'), findsNothing); - // No rows laid out before row 4, or after row 9, except for pinned first row. + // No rows laid out before row 4, or after row 9, except for pinned + // first row. expect(find.text('R0:C6'), findsOneWidget); expect(find.text('R3:C6'), findsNothing); expect(find.text('R10:C6'), findsNothing); @@ -776,7 +789,8 @@ void main() { // No columns laid out beyond column 5, no rows beyond row 4. expect(find.text('R0:C6'), findsNothing); expect(find.text('R5:C0'), findsNothing); - // Change both scroll offsets, validate more columns and rows were populated. + // Change both scroll offsets, validate more columns and rows were + // populated. horizontalController.jumpTo(1200.0); verticalController.jumpTo(1000.0); await tester.pump(); @@ -794,11 +808,13 @@ void main() { tester.getRect(find.text('R9:C11')), const Rect.fromLTRB(1000.0, 800.0, 1200.0, 1000.0), ); - // No columns laid out before column 5, or after column 12, except for pinned first column. + // No columns laid out before column 5, or after column 12, except for + // pinned first column. expect(find.text('R5:C0'), findsOneWidget); expect(find.text('R5:C4'), findsNothing); expect(find.text('R5:C12'), findsNothing); - // No rows laid out before row 4, or after row 9, except for pinned first row. + // No rows laid out before row 4, or after row 9, except for pinned + // first row. expect(find.text('R0:C6'), findsOneWidget); expect(find.text('R3:C6'), findsNothing); expect(find.text('R10:C6'), findsNothing); @@ -840,7 +856,7 @@ void main() { ); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the vetical scroll offset, valdiate more rows were populated. + // Change the vertical scroll offset, validate more rows were populated. // This exceeds the bounds of the scroll view once the rows have been // null terminated. verticalController.jumpTo(1200.0); @@ -901,7 +917,7 @@ void main() { ); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the vetical scroll offset, valdiate more rows were populated. + // Change the vertical scroll offset, validate more rows were populated. // This exceeds the bounds of the scroll view once the rows have been // null terminated. verticalController.jumpTo(1200.0); @@ -960,7 +976,7 @@ void main() { ); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the vetical scroll offset, valdiate more rows were populated. + // Change the vertical scroll offset, validate more rows were populated. // This exceeds the bounds of the scroll view once the rows have been // null terminated. verticalController.jumpTo(1200.0); @@ -982,7 +998,8 @@ void main() { tester.getRect(find.text('R7:C4')), const Rect.fromLTRB(800.0, 400.0, 1000.0, 600.0), ); - // No rows laid out before row 5, or after row 7, except for the first pinned row. + // No rows laid out before row 5, or after row 7, except for the first + // pinned row. expect(find.text('R0:C0'), findsOneWidget); expect(find.text('R1:C0'), findsNothing); expect(find.text('R8:C0'), findsNothing); @@ -1021,7 +1038,7 @@ void main() { ); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the vetical scroll offset, valdiate more rows were populated. + // Change the vertical scroll offset, validate more rows were populated. // This exceeds the bounds of the scroll view once the rows have been // null terminated. verticalController.jumpTo(1200.0); @@ -1043,7 +1060,8 @@ void main() { tester.getRect(find.text('R7:C4')), const Rect.fromLTRB(800.0, 400.0, 1000.0, 600.0), ); - // No rows laid out before row 5, or after row 7, except for the first pinned row. + // No rows laid out before row 5, or after row 7, except for the first + // pinned row. expect(find.text('R0:C0'), findsOneWidget); expect(find.text('R1:C0'), findsNothing); expect(find.text('R8:C0'), findsNothing); @@ -1085,9 +1103,9 @@ void main() { ); // No columns laid out beyond column 5. expect(find.text('R0:C6'), findsNothing); - // Change the horizontal scroll offset, valdiate more columns were populated. - // This exceeds the bounds of the scroll view once the columns have been - // null terminated. + // Change the horizontal scroll offset, validate more columns were + // populated. This exceeds the bounds of the scroll view once the + // columns have been null terminated. horizontalController.jumpTo(1400.0); await tester.pumpAndSettle(); expect(verticalController.position.pixels, 0.0); @@ -1109,7 +1127,7 @@ void main() { tester.getRect(find.text('R4:C9')), const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), ); - // No rows laid out before column 5, or after column 9. + // No columns laid out before column 5, or after column 9. expect(find.text('R0:C4'), findsNothing); expect(find.text('R0:C10'), findsNothing); @@ -1146,9 +1164,9 @@ void main() { ); // No columns laid out beyond column 5. expect(find.text('R0:C6'), findsNothing); - // Change the horizontal scroll offset, valdiate more columns were populated. - // This exceeds the bounds of the scroll view once the columns have been - // null terminated. + // Change the horizontal scroll offset, validate more columns were + // populated. This exceeds the bounds of the scroll view once the + // columns have been null terminated. horizontalController.jumpTo(1400.0); await tester.pumpAndSettle(); expect(verticalController.position.pixels, 0.0); @@ -1168,7 +1186,8 @@ void main() { tester.getRect(find.text('R4:C9')), const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), ); - // No rows laid out before column 5, or after column 9, except for the pinned first column. + // No columns laid out before column 5, or after column 9, except for + // the pinned first column. expect(find.text('R0:C0'), findsOneWidget); expect(find.text('R0:C4'), findsNothing); expect(find.text('R0:C10'), findsNothing); @@ -1206,9 +1225,9 @@ void main() { ); // No columns laid out beyond column 5. expect(find.text('R0:C6'), findsNothing); - // Change the horizontal scroll offset, valdiate more columns were populated. - // This exceeds the bounds of the scroll view once the columns have been - // null terminated. + // Change the horizontal scroll offset, validate more columns were + // populated. This exceeds the bounds of the scroll view once the + // columns have been null terminated. horizontalController.jumpTo(1400.0); await tester.pumpAndSettle(); expect(verticalController.position.pixels, 0.0); @@ -1228,7 +1247,7 @@ void main() { tester.getRect(find.text('R4:C9')), const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), ); - // No rows laid out before column 5, or after column 9. + // No columns laid out before column 5, or after column 9. expect(find.text('R0:C4'), findsNothing); expect(find.text('R0:C10'), findsNothing); @@ -1266,9 +1285,9 @@ void main() { ); // No columns laid out beyond column 5. expect(find.text('R0:C6'), findsNothing); - // Change the horizontal scroll offset, valdiate more columns were populated. - // This exceeds the bounds of the scroll view once the columns have been - // null terminated. + // Change the horizontal scroll offset, validate more columns were + // populated. This exceeds the bounds of the scroll view once the + // columns have been null terminated. horizontalController.jumpTo(1400.0); await tester.pumpAndSettle(); expect(verticalController.position.pixels, 0.0); @@ -1288,7 +1307,8 @@ void main() { tester.getRect(find.text('R4:C9')), const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), ); - // No rows laid out before column 5, or after column 9, except for the pinned first column. + // No columns laid out before column 5, or after column 9, except for + // the pinned first column. expect(find.text('R0:C0'), findsOneWidget); expect(find.text('R0:C4'), findsNothing); expect(find.text('R0:C10'), findsNothing); @@ -1341,7 +1361,7 @@ void main() { expect(find.text('R0:C6'), findsNothing); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the scroll offsets, valdiate more columns and rows were + // Change the scroll offsets, validate more columns and rows were // populated. // These exceed the bounds of the scroll view once the column and row // builders have null terminated. @@ -1416,7 +1436,7 @@ void main() { expect(find.text('R0:C6'), findsNothing); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the scroll offsets, valdiate more columns and rows were + // Change the scroll offsets, validate more columns and rows were // populated. // These exceed the bounds of the scroll view once the column and row // builders have null terminated. @@ -1490,7 +1510,7 @@ void main() { expect(find.text('R0:C6'), findsNothing); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the scroll offsets, valdiate more columns and rows were + // Change the scroll offsets, validate more columns and rows were // populated. // These exceed the bounds of the scroll view once the column and row // builders have null terminated. @@ -1565,7 +1585,7 @@ void main() { expect(find.text('R0:C6'), findsNothing); // No rows laid out beyond row 4. expect(find.text('R5:C0'), findsNothing); - // Change the scroll offsets, valdiate more columns and rows were + // Change the scroll offsets, validate more columns and rows were // populated. // These exceed the bounds of the scroll view once the column and row // builders have null terminated. @@ -1604,9 +1624,10 @@ void main() { (WidgetTester tester) async { // When columns and rows are finite, the layout is eagerly computed and // the children are lazily laid out. This makes computing merged cell - // layouts easy. In an infinite world, not all of the information may be - // available for a merged cell if it extends into an area we have not - // computed the layout for yet. + // layouts easy. In an infinite world, the layout is also lazily + // computed, so not all of the information may be available for a merged + // cell if it extends into an area we have not computed the layout for + // yet. const ({int start, int span}) rowConfig = (start: 0, span: 10); final List mergedRows = List.generate( 10, @@ -2165,7 +2186,7 @@ void main() { await tester.pumpAndSettle(); // Even columns and rows are set up for taps, mainAxis is vertical by - // default, mening row major order. Rows should take precedence where they + // default, meaning row major order. Rows should take precedence where they // intersect at even indices. expect(columnTapCounter, 0); expect(rowTapCounter, 0); From 7ed44342c35a0292645a5b34905efa2339a7662b Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 27 Mar 2024 13:10:54 -0500 Subject: [PATCH 11/17] ++ --- packages/two_dimensional_scrollables/CHANGELOG.md | 4 ++++ packages/two_dimensional_scrollables/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/two_dimensional_scrollables/CHANGELOG.md b/packages/two_dimensional_scrollables/CHANGELOG.md index 222468fa1776..e1541589d45f 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.0 + +* Adds support for infinite rows and columns in TableView. + ## 0.1.2 * Fixes a layout issue for unpinned merged cells that follow pinned table spans. diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index 45f1d77ea6e1..fb9dc192f702 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.1.2 +version: 0.2.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 f0f9e303f10af1d20288a4d5c3d759420df2358d Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Thu, 28 Mar 2024 19:25:19 -0500 Subject: [PATCH 12/17] Review feedback --- .../example/lib/main.dart | 140 +++------- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../lib/src/table_view/table.dart | 104 +++++--- .../lib/src/table_view/table_delegate.dart | 29 +- .../test/table_view/table_test.dart | 250 ++++++++++++++++++ 6 files changed, 358 insertions(+), 169 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index ff714af1e497..4663559b2364 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -41,54 +41,42 @@ class TableExample extends StatefulWidget { } class _TableExampleState extends State { - late final ScrollController _verticalController = ScrollController(); - int _rowCount = 20; + int counter = 0; + int get _columnCount { + return switch (counter % 3) { + 0 => 20, + 1 => 30, + 2 => 10, + _ => 500, + }; + } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Table Example'), + title: Text('Row count $_columnCount'), ), - 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: 20, + columnBuilder: _buildColumnSpan, + // rowCount: _rowCount, + rowBuilder: _buildRowSpan, + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + setState(() { + print('press'); + counter++; + }); + }, ), - 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(vicinity); return TableViewCell( child: Center( child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), @@ -96,59 +84,24 @@ class _TableExampleState extends State { ); } - TableSpan _buildColumnSpan(int index) { + TableSpan? _buildColumnSpan(int index) { + if (index > _columnCount) { + return null; + } 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( + return const TableSpan( foregroundDecoration: decoration, - extent: const FractionalTableSpanExtent(0.5), - onEnter: (_) => print('Entered column $index'), - cursor: SystemMouseCursors.contextMenu, + extent: FixedTableSpanExtent(100), ); - 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'), - ); - } - throw AssertionError( - 'This should be unreachable, as every index is accounted for in the switch clauses.'); } - TableSpan _buildRowSpan(int index) { + TableSpan? _buildRowSpan(int index) { + final TableSpanDecoration decoration = TableSpanDecoration( color: index.isEven ? Colors.purple[100] : null, border: const TableSpanBorder( @@ -157,34 +110,9 @@ class _TableExampleState extends State { ), ), ); - - switch (index % 3) { - case 0: - return TableSpan( + 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 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 @@ delegate.columnCount == null; // How far columns should be laid out in a given frame. double get _targetColumnPixel { @@ -341,7 +341,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _pinnedColumnsExtent; } - int? _rowNullTerminated; + int? _rowNullTerminatedIndex; bool get _rowsAreInfinite => delegate.rowCount == null; // How far rows should be laid out in a given frame. double get _targetRowPixel { @@ -449,6 +449,12 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } // Updates the cached column metrics for the table. + // + // By default, existing column metrics will be updated if they have changed. + // Setting `appendColumns` to false will preserve existing metrics, adding + // additional metrics up to the visible+cacheExtent or up to a provided index, + // `toColumnIndex`. Appending is only relevant when the number of columns is + // infinite. void _updateColumnMetrics({bool appendColumns = false, int? toColumnIndex}) { assert(() { if (toColumnIndex != null) { @@ -474,12 +480,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _firstNonPinnedColumn = toColumnIndex == null ? null : _firstNonPinnedColumn; _lastNonPinnedColumn = toColumnIndex == null ? null : _lastNonPinnedColumn; - _columnNullTerminated = needsDelegateRebuild ? null : _columnNullTerminated; int column = appendColumns ? _columnMetrics.length : 0; bool reachedColumnEnd() { if (_columnsAreInfinite) { - if (_columnNullTerminated != null) { + if (_columnNullTerminatedIndex != null) { _lastNonPinnedColumn ??= _columnMetrics.length - 1; return true; } @@ -508,8 +513,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // We have reached the end of columns based on a null termination. This // This happens when a column count has not been specified. assert(_columnsAreInfinite); - _columnNullTerminated = column; - _updateScrollBounds(); + _columnNullTerminatedIndex = column; + final bool acceptedDimension = _updateHorizontalScrollBounds(); + if (!acceptedDimension) { + _updateFirstAndLastVisibleCell(); + } break; } span ??= _Span(); @@ -545,6 +553,12 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } // Updates the cached row metrics for the table. + // + // By default, existing row metrics will be updated if they have changed. + // Setting `appendRows` to false will preserve existing metrics, adding + // additional metrics up to the visible+cacheExtent or up to a provided index, + // `toRowIndex`. Appending is only relevant when the number of rows is + // infinite. void _updateRowMetrics({bool appendRows = false, int? toRowIndex}) { assert(() { if (toRowIndex != null) { @@ -567,12 +581,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } _firstNonPinnedRow = toRowIndex == null ? null : _firstNonPinnedRow; _lastNonPinnedRow = toRowIndex == null ? null : _lastNonPinnedRow; - _rowNullTerminated = needsDelegateRebuild ? null : _rowNullTerminated; int row = appendRows ? _rowMetrics.length : 0; bool reachedRowEnd() { if (_rowsAreInfinite) { - if (_rowNullTerminated != null) { + if (_rowNullTerminatedIndex != null) { _lastNonPinnedRow ??= _rowMetrics.length - 1; return true; } @@ -602,8 +615,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // This happens when a row count has not been specified, but we have // reached the end. assert(_rowsAreInfinite); - _rowNullTerminated = row; - _updateScrollBounds(); + _rowNullTerminatedIndex = row; + final bool acceptedDimension = _updateVerticalScrollBounds(); + if (!acceptedDimension) { + _updateFirstAndLastVisibleCell(); + } break; } span ??= _Span(); @@ -639,28 +655,16 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } void _updateScrollBounds() { - final double maxVerticalScrollExtent; - if (_rowsAreInfinite && _rowNullTerminated == null) { - maxVerticalScrollExtent = double.infinity; - } else if (!_rowsAreInfinite && - _rowMetrics.length <= delegate.pinnedRowCount) { - assert(_firstNonPinnedRow == null && _lastNonPinnedRow == null); - maxVerticalScrollExtent = 0.0; - } else { - final int lastRow = _rowMetrics.length - 1; - if (_firstNonPinnedRow != null) { - _lastNonPinnedRow ??= lastRow; - } - maxVerticalScrollExtent = math.max( - 0.0, - _rowMetrics[lastRow]!.trailingOffset - - viewportDimension.height + - _pinnedRowsExtent, - ); + final bool acceptedDimension = _updateHorizontalScrollBounds() && + _updateVerticalScrollBounds(); + if (!acceptedDimension) { + _updateFirstAndLastVisibleCell(); } + } + bool _updateHorizontalScrollBounds() { final double maxHorizontalScrollExtent; - if (_columnsAreInfinite && _columnNullTerminated == null) { + if (_columnsAreInfinite && _columnNullTerminatedIndex == null) { maxHorizontalScrollExtent = double.infinity; } else if (!_columnsAreInfinite && _columnMetrics.length <= delegate.pinnedColumnCount) { @@ -678,17 +682,35 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _pinnedColumnsExtent, ); } + return horizontalOffset.applyContentDimensions( + 0.0, maxHorizontalScrollExtent,); + } - final bool acceptedDimension = horizontalOffset.applyContentDimensions( - 0.0, maxHorizontalScrollExtent) && - verticalOffset.applyContentDimensions(0.0, maxVerticalScrollExtent); - if (!acceptedDimension) { - _updateFirstAndLastVisibleCell(); + bool _updateVerticalScrollBounds() { + final double maxVerticalScrollExtent; + if (_rowsAreInfinite && _rowNullTerminatedIndex == null) { + maxVerticalScrollExtent = double.infinity; + } else if (!_rowsAreInfinite && + _rowMetrics.length <= delegate.pinnedRowCount) { + assert(_firstNonPinnedRow == null && _lastNonPinnedRow == null); + maxVerticalScrollExtent = 0.0; + } else { + final int lastRow = _rowMetrics.length - 1; + if (_firstNonPinnedRow != null) { + _lastNonPinnedRow ??= lastRow; + } + maxVerticalScrollExtent = math.max( + 0.0, + _rowMetrics[lastRow]!.trailingOffset - + viewportDimension.height + + _pinnedRowsExtent, + ); } + return verticalOffset.applyContentDimensions(0.0, maxVerticalScrollExtent,); } // Uses the cached metrics to update the currently visible cells. If the - // number of rows or columns are infinte, the layout is computed lazily, so + // number of rows or columns are infinite, the layout is computed lazily, so // this will call for an update to the metrics if we have scrolled beyond the // layout portion we know about. void _updateFirstAndLastVisibleCell() { @@ -702,7 +724,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { lastKnownColumn = _columnMetrics[_columnMetrics.length - 1]!; assert(_columnMetrics.length == delegate.columnCount || lastKnownColumn.trailingOffset >= _targetColumnPixel || - _columnNullTerminated != null); + _columnNullTerminatedIndex != null); } } _firstNonPinnedColumn = null; @@ -734,7 +756,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { lastKnownRow = _rowMetrics[_rowMetrics.length - 1]!; assert(_rowMetrics.length == delegate.rowCount || lastKnownRow.trailingOffset >= _targetRowPixel || - _rowNullTerminated != null); + _rowNullTerminatedIndex != null); } } _firstNonPinnedRow = null; @@ -760,6 +782,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { @override void layoutChildSequence() { // Reset for a new frame + // We always reset the null terminating indices in case rows or columns have + // been added or removed. + _rowNullTerminatedIndex = null; + _columnNullTerminatedIndex = null; _mergedVicinities.clear(); _mergedRows.clear(); _mergedColumns.clear(); @@ -984,7 +1010,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _rowMetrics[lastRow] != null, 'The merged cell containing $vicinity is missing TableSpan ' 'information necessary for layout. The rowBuilder returned ' - 'null, signifying the end, at row $_rowNullTerminated but the ' + 'null, signifying the end, at row $_rowNullTerminatedIndex but the ' 'merged cell is configured to end with row $lastRow.', ); mergedRowHeight = _rowMetrics[lastRow]!.trailingOffset - @@ -1022,7 +1048,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _columnMetrics[lastColumn] != null, 'The merged cell containing $vicinity is missing TableSpan ' 'information necessary for layout. The columnBuilder returned ' - 'null, signifying the end, at column $_columnNullTerminated but ' + 'null, signifying the end, at column $_columnNullTerminatedIndex but ' 'the merged cell is configured to end with column $lastColumn.', ); mergedColumnWidth = _columnMetrics[lastColumn]!.trailingOffset - 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 fe5cfd685f41..47a5f8b8a7a3 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 @@ -37,7 +37,7 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// The [buildColumn] method will be called for indices smaller than the value /// provided here to learn more about the extent and visual appearance of a /// particular column. If null, the table will have an infinite number of - /// columns. + /// columns, unless [buildColumn] returns null to signify the end. /// /// The value returned by this getter may be an estimate of the total /// available columns, but [buildColumn] method must provide a valid @@ -58,7 +58,8 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// /// The [buildRow] method will be called for indices smaller than the value /// provided here to learn more about the extent and visual appearance of a - /// particular row. If null, the table will have an infinite number of rows. + /// particular row. If null, the table will have an infinite number of rows, + /// unless [buildColumn] returns null to signify the end. /// /// The value returned by this getter may be an estimate of the total /// available rows, but [buildRow] method must provide a valid @@ -169,19 +170,11 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate ); @override - int? get columnCount { - return switch (maxXIndex) { - null => maxXIndex, - _ => maxXIndex! + 1, - }; - } + int? get columnCount => maxXIndex == null ? null : maxXIndex! + 1; set columnCount(int? value) { assert(value == null || pinnedColumnCount <= value); - maxXIndex = switch (value) { - null => value, - _ => value - 1, - }; + maxXIndex = value == null ? null : value - 1; } /// Builds the [TableSpan] that describes the column at the provided index. @@ -208,19 +201,11 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate } @override - int? get rowCount { - return switch (maxYIndex) { - null => maxYIndex, - _ => maxYIndex! + 1, - }; - } + int? get rowCount => maxYIndex == null ? null : maxYIndex! + 1; set rowCount(int? value) { assert(value == null || pinnedRowCount <= value); - maxYIndex = switch (value) { - null => value, - _ => value - 1, - }; + maxYIndex = value == null ? null : value - 1; } /// Builds the [TableSpan] that describes the row at the provided index. 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 873947e44f15..ce879b75b277 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -1067,6 +1067,256 @@ void main() { expect(find.text('R8:C0'), findsNothing); }); + testWidgets('Null terminated rows will update', + (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: getTableView( + columnCount: 10, + rowBuilder: (int index) { + // There will only be 8 rows. + if (index == 8) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + // Change the vertical scroll offset, validate more rows were populated. + // This exceeds the bounds of the scroll view once the rows have been + // null terminated. + verticalController.jumpTo(1200.0); + await tester.pumpAndSettle(); + // Position was corrected. + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 0.0); + // Max scroll extent was updated to reflect reaching the end of the rows + // after returning null. + expect(verticalController.position.maxScrollExtent, 1000.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + expect(find.text('R5:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R7:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R7:C4')), + const Rect.fromLTRB(800.0, 400.0, 1000.0, 600.0), + ); + // No rows laid out before row 5, or after row 7. + expect(find.text('R0:C0'), findsNothing); + expect(find.text('R8:C0'), findsNothing); + + // Increase the number of rows + await tester.pumpWidget(MaterialApp( + home: getTableView( + columnCount: 10, + rowBuilder: (int index) { + // There will only be 16 rows. + if (index == 16) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + + // The position should not have changed. + expect(verticalController.position.pixels, 1000.0); + expect(horizontalController.position.pixels, 0.0); + // Max scroll extent was updated to reflect we no longer know where the + // end is, until the rowBuilder returns null again. + expect(verticalController.position.maxScrollExtent, double.infinity); + expect(horizontalController.position.maxScrollExtent, 1200.0); + // The layout should not have changed. + expect(find.text('R5:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R5:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R7:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R7:C4')), + const Rect.fromLTRB(800.0, 400.0, 1000.0, 600.0), + ); + // No rows laid out before row 5, but more rows were laid out into the + // newly updated cacheExtent (row 8). + expect(find.text('R0:C0'), findsNothing); + expect(find.text('R8:C0'), findsOneWidget); + // This exceeds the new bounds. + verticalController.jumpTo(3200.0); + await tester.pumpAndSettle(); + // Position was corrected. + expect(verticalController.position.pixels, 2600.0); + expect(horizontalController.position.pixels, 0.0); + // Max scroll extent was updated to reflect reaching the end of the rows + // after returning null again at the new index. + expect(verticalController.position.maxScrollExtent, 2600.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + + // Decrease the number of rows + await tester.pumpWidget(MaterialApp( + home: getTableView( + columnCount: 10, + rowBuilder: (int index) { + // There will only be 5 rows. + if (index == 5) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + + // The position should have changed. + expect(verticalController.position.pixels, 400.0); + expect(horizontalController.position.pixels, 0.0); + // Max scroll extent was updated to the new end we have corrected to. + expect(verticalController.position.maxScrollExtent, 400.0); + expect(horizontalController.position.maxScrollExtent, 1200.0); + // The layout updated. + expect(find.text('R2:C0'), findsOneWidget); + expect( + tester.getRect(find.text('R2:C0')), + const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0), + ); + expect(find.text('R4:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C4')), + const Rect.fromLTRB(800.0, 400.0, 1000.0, 600.0), + ); + // No rows laid out after row 5. + expect(find.text('R5:C0'), findsNothing); + }); + + testWidgets('Null terminated columns will update', + (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowCount: 10, + columnBuilder: (int index) { + // There will only be 8 columns. + if (index == 8) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + // Change the horizontal scroll offset, validate more columns were + // populated. This exceeds the bounds of the scroll view once the + // columns have been null terminated. + horizontalController.jumpTo(1400.0); + await tester.pumpAndSettle(); + // Position was corrected. + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 800.0); + // Max scroll extent was updated to reflect reaching the end of the + // columns after returning null. + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, 800.0); + expect(find.text('R0:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C5')), + const Rect.fromLTRB(200.0, 0.0, 400.0, 200.0), + ); + expect(find.text('R4:C7'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C7')), + const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), + ); + // No columns laid out before column 3, or after column 7. + expect(find.text('R0:C2'), findsNothing); + expect(find.text('R0:C8'), findsNothing); + + // Increase the number of rows + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowCount: 10, + columnBuilder: (int index) { + // There will only be 16 column. + if (index == 16) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + + // The position should not have changed. + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 800.0); + // Max scroll extent was updated to reflect we no longer know where the + // end is, until the rowBuilder returns null again. + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, double.infinity); + // The layout should not have changed. + expect(find.text('R0:C5'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C5')), + const Rect.fromLTRB(200.0, 0.0, 400.0, 200.0), + ); + expect(find.text('R4:C7'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C7')), + const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), + ); + // No columns laid out before column 3, but after column 7 we have added + // new columns. + expect(find.text('R0:C2'), findsNothing); + expect(find.text('R0:C8'), findsOneWidget); + // This exceeds the new bounds. + horizontalController.jumpTo(3200.0); + await tester.pumpAndSettle(); + // Position was corrected. + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 2400.0); + // Max scroll extent was updated to reflect reaching the end of the + // columns after returning null again at the new index. + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, 2400.0); + + // Decrease the number of columns + await tester.pumpWidget(MaterialApp( + home: getTableView( + rowCount: 10, + columnBuilder: (int index) { + // There will only be 5 columns. + if (index == 5) { + return null; + } + return largeSpan; + }, + ), + )); + await tester.pumpAndSettle(); + + // The position should have changed. + expect(verticalController.position.pixels, 0.0); + expect(horizontalController.position.pixels, 200.0); + // Max scroll extent was updated to the new end we have corrected to. + expect(verticalController.position.maxScrollExtent, 1400.0); + expect(horizontalController.position.maxScrollExtent, 200.0); + // The layout updated. + expect(find.text('R0:C2'), findsOneWidget); + expect( + tester.getRect(find.text('R0:C2')), + const Rect.fromLTRB(200.0, 0.0, 400.0, 200.0), + ); + expect(find.text('R4:C4'), findsOneWidget); + expect( + tester.getRect(find.text('R4:C4')), + const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), + ); + // No columns laid out after column 5. + expect(find.text('R0:C5'), findsNothing); + }); + testWidgets('infinite columns can null terminate', (WidgetTester tester) async { // Nothing pinned --- From 3b6d7ce7b54f3de6fe681df4ac29d0f240aae7b2 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Thu, 28 Mar 2024 19:27:02 -0500 Subject: [PATCH 13/17] Unintended changes --- .../example/lib/main.dart | 140 +++++++++++++----- 1 file changed, 106 insertions(+), 34 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/lib/main.dart b/packages/two_dimensional_scrollables/example/lib/main.dart index 4663559b2364..ff714af1e497 100644 --- a/packages/two_dimensional_scrollables/example/lib/main.dart +++ b/packages/two_dimensional_scrollables/example/lib/main.dart @@ -41,42 +41,54 @@ class TableExample extends StatefulWidget { } class _TableExampleState extends State { - int counter = 0; - int get _columnCount { - return switch (counter % 3) { - 0 => 20, - 1 => 30, - 2 => 10, - _ => 500, - }; - } + late final ScrollController _verticalController = ScrollController(); + int _rowCount = 20; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Row count $_columnCount'), - ), - body: TableView.builder( - cellBuilder: _buildCell, - // columnCount: 20, - columnBuilder: _buildColumnSpan, - // rowCount: _rowCount, - rowBuilder: _buildRowSpan, + title: const Text('Table Example'), ), - floatingActionButton: FloatingActionButton( - onPressed: () { - setState(() { - print('press'); - counter++; - }); - }, + 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(vicinity); return TableViewCell( child: Center( child: Text('Tile c: ${vicinity.column}, r: ${vicinity.row}'), @@ -84,24 +96,59 @@ class _TableExampleState extends State { ); } - TableSpan? _buildColumnSpan(int index) { - if (index > _columnCount) { - return null; - } + TableSpan _buildColumnSpan(int index) { const TableSpanDecoration decoration = TableSpanDecoration( border: TableSpanBorder( trailing: BorderSide(), ), ); - return const TableSpan( + 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: FixedTableSpanExtent(100), + 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'), + ); + } + throw AssertionError( + 'This should be unreachable, as every index is accounted for in the switch clauses.'); } - TableSpan? _buildRowSpan(int index) { - + TableSpan _buildRowSpan(int index) { final TableSpanDecoration decoration = TableSpanDecoration( color: index.isEven ? Colors.purple[100] : null, border: const TableSpanBorder( @@ -110,9 +157,34 @@ class _TableExampleState extends State { ), ), ); - return TableSpan( + + 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.'); } } From a825b065b5befca54faa2af7395b4f2c4e540af3 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Thu, 28 Mar 2024 19:34:24 -0500 Subject: [PATCH 14/17] Self review --- .../example/macos/Runner.xcodeproj/project.pbxproj | 2 +- .../Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../two_dimensional_scrollables/lib/src/table_view/table.dart | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) 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: Thu, 28 Mar 2024 19:40:16 -0500 Subject: [PATCH 15/17] Little more cleanup --- .../lib/src/table_view/table.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 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 6101183dd86c..7fd46758994b 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -477,6 +477,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { startOfRegularColumn = _columnMetrics[_lastNonPinnedColumn]?.trailingOffset ?? 0.0; } + // If we are computing up to a specific index, we are getting info for a + // merged cell, do not change the visible cells. _firstNonPinnedColumn = toColumnIndex == null ? null : _firstNonPinnedColumn; _lastNonPinnedColumn = toColumnIndex == null ? null : _lastNonPinnedColumn; @@ -484,9 +486,6 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { bool reachedColumnEnd() { if (_columnsAreInfinite) { - if (_columnNullTerminatedIndex != null) { - return true; - } if (toColumnIndex != null) { // Column metrics should be computed up to the provided index. // Only relevant when we are filling in missing column metrics in an @@ -494,8 +493,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { return _columnMetrics.length > toColumnIndex; } // There are infinite columns, and no target index, compute metrics - // up to what is visible and in the cache extent. - return _lastNonPinnedColumn != null; + // up to what is visible and in the cache extent, or the index that null + // terminates. + return _lastNonPinnedColumn != null || _columnNullTerminatedIndex != null; } // Compute all the metrics if the columns are finite. return column == delegate.columnCount!; @@ -579,15 +579,14 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { startOfPinnedRow = _rowMetrics[_firstNonPinnedRow]?.trailingOffset ?? 0.0; startOfRegularRow = _rowMetrics[_lastNonPinnedRow]?.trailingOffset ?? 0.0; } + // If we are computing up to a specific index, we are getting info for a + // merged cell, do not change the visible cells. _firstNonPinnedRow = toRowIndex == null ? null : _firstNonPinnedRow; _lastNonPinnedRow = toRowIndex == null ? null : _lastNonPinnedRow; int row = appendRows ? _rowMetrics.length : 0; bool reachedRowEnd() { if (_rowsAreInfinite) { - if (_rowNullTerminatedIndex != null) { - return true; - } if (toRowIndex != null) { // Row metrics should be computed up to the provided index. // Only relevant when we are filling in missing column metrics in an @@ -595,8 +594,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { return _rowMetrics.length > toRowIndex; } // There are infinite row, and no target index, compute metrics - // up to what is visible and in the cache extent. - return _lastNonPinnedRow != null; + // up to what is visible and in the cache extent, or the index that null + // terminates. + return _lastNonPinnedRow != null || _rowNullTerminatedIndex != null; } // Compute all the metrics if the rows are finite. return row == delegate.rowCount!; From 9798f0610ff40f4d702e7494801037998ebfe304 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Thu, 28 Mar 2024 19:43:17 -0500 Subject: [PATCH 16/17] Update packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart --- .../lib/src/table_view/table_delegate.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 47a5f8b8a7a3..aef785bf63fd 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 @@ -59,7 +59,7 @@ mixin TableCellDelegateMixin on TwoDimensionalChildDelegate { /// The [buildRow] method will be called for indices smaller than the value /// provided here to learn more about the extent and visual appearance of a /// particular row. If null, the table will have an infinite number of rows, - /// unless [buildColumn] returns null to signify the end. + /// unless [buildRow] returns null to signify the end. /// /// The value returned by this getter may be an estimate of the total /// available rows, but [buildRow] method must provide a valid From 7b4b3d6d79a9bfdde4330461fa49dca32588afc6 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Thu, 28 Mar 2024 19:45:04 -0500 Subject: [PATCH 17/17] Format --- .../lib/src/table_view/table.dart | 16 +++++++++++----- .../lib/src/table_view/table_delegate.dart | 2 +- 2 files changed, 12 insertions(+), 6 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 7fd46758994b..9f2caa667d4a 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -495,7 +495,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // There are infinite columns, and no target index, compute metrics // up to what is visible and in the cache extent, or the index that null // terminates. - return _lastNonPinnedColumn != null || _columnNullTerminatedIndex != null; + return _lastNonPinnedColumn != null || + _columnNullTerminatedIndex != null; } // Compute all the metrics if the columns are finite. return column == delegate.columnCount!; @@ -655,8 +656,8 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { } void _updateScrollBounds() { - final bool acceptedDimension = _updateHorizontalScrollBounds() && - _updateVerticalScrollBounds(); + final bool acceptedDimension = + _updateHorizontalScrollBounds() && _updateVerticalScrollBounds(); if (!acceptedDimension) { _updateFirstAndLastVisibleCell(); } @@ -683,7 +684,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ); } return horizontalOffset.applyContentDimensions( - 0.0, maxHorizontalScrollExtent,); + 0.0, + maxHorizontalScrollExtent, + ); } bool _updateVerticalScrollBounds() { @@ -706,7 +709,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _pinnedRowsExtent, ); } - return verticalOffset.applyContentDimensions(0.0, maxVerticalScrollExtent,); + return verticalOffset.applyContentDimensions( + 0.0, + maxVerticalScrollExtent, + ); } // Uses the cached metrics to update the currently visible cells. If the 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 aef785bf63fd..11ea2459ea08 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 @@ -201,7 +201,7 @@ class TableCellBuilderDelegate extends TwoDimensionalChildBuilderDelegate } @override - int? get rowCount => maxYIndex == null ? null : maxYIndex! + 1; + int? get rowCount => maxYIndex == null ? null : maxYIndex! + 1; set rowCount(int? value) { assert(value == null || pinnedRowCount <= value);