From baefef5cd585490e469caf698b52837fd63027d1 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 16 Jul 2025 13:54:49 -0500 Subject: [PATCH 1/3] Fix missing leading cache extent for TableView --- .../lib/src/table_view/table.dart | 49 +++-- .../test/table_view/table_test.dart | 183 ++++++++++++------ 2 files changed, 158 insertions(+), 74 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 f2c88955605..0b3575c368b 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -333,8 +333,17 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { int? _columnNullTerminatedIndex; bool get _columnsAreInfinite => delegate.columnCount == null; + // Where column layout begins, potentially outside of the visible area. + double get _targetLeadingColumnPixel { + return clampDouble( + horizontalOffset.pixels - cacheExtent, + 0, + double.infinity, + ); + } + // How far columns should be laid out in a given frame. - double get _targetColumnPixel { + double get _targetTrailingColumnPixel { return cacheExtent + horizontalOffset.pixels + viewportDimension.width - @@ -343,8 +352,17 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { int? _rowNullTerminatedIndex; bool get _rowsAreInfinite => delegate.rowCount == null; + // Where row layout begins, potentially outside of the visible area. + double get _targetLeadingRowPixel { + return clampDouble( + verticalOffset.pixels - cacheExtent, + 0, + double.infinity, + ); + } + // How far rows should be laid out in a given frame. - double get _targetRowPixel { + double get _targetTrailingRowPixel { return cacheExtent + verticalOffset.pixels + viewportDimension.height - @@ -535,11 +553,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ); _columnMetrics[column] = span; if (!isPinned) { - if (span.trailingOffset >= horizontalOffset.pixels && + if (span.trailingOffset >= _targetLeadingColumnPixel && _firstNonPinnedColumn == null) { _firstNonPinnedColumn = column; } - if (span.trailingOffset >= _targetColumnPixel && + if (span.trailingOffset >= _targetTrailingColumnPixel && _lastNonPinnedColumn == null) { _lastNonPinnedColumn = column; } @@ -637,11 +655,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ); _rowMetrics[row] = span; if (!isPinned) { - if (span.trailingOffset >= verticalOffset.pixels && + if (span.trailingOffset >= _targetLeadingRowPixel && _firstNonPinnedRow == null) { _firstNonPinnedRow = row; } - if (span.trailingOffset > _targetRowPixel && + if (span.trailingOffset > _targetTrailingRowPixel && _lastNonPinnedRow == null) { _lastNonPinnedRow = row; } @@ -723,13 +741,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (_columnMetrics.isNotEmpty) { _Span lastKnownColumn = _columnMetrics[_columnMetrics.length - 1]!; if (_columnsAreInfinite && - lastKnownColumn.trailingOffset < _targetColumnPixel) { + lastKnownColumn.trailingOffset < _targetTrailingColumnPixel) { // 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]!; assert(_columnMetrics.length == delegate.columnCount || - lastKnownColumn.trailingOffset >= _targetColumnPixel || + lastKnownColumn.trailingOffset >= _targetTrailingColumnPixel || _columnNullTerminatedIndex != null); } } @@ -740,11 +758,12 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { continue; } final double endOfColumn = _columnMetrics[column]!.trailingOffset; - if (endOfColumn >= horizontalOffset.pixels && + if (endOfColumn >= _targetLeadingColumnPixel && _firstNonPinnedColumn == null) { _firstNonPinnedColumn = column; } - if (endOfColumn >= _targetColumnPixel && _lastNonPinnedColumn == null) { + if (endOfColumn >= _targetTrailingColumnPixel && + _lastNonPinnedColumn == null) { _lastNonPinnedColumn = column; break; } @@ -755,13 +774,14 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { if (_rowMetrics.isNotEmpty) { _Span lastKnownRow = _rowMetrics[_rowMetrics.length - 1]!; - if (_rowsAreInfinite && lastKnownRow.trailingOffset < _targetRowPixel) { + if (_rowsAreInfinite && + lastKnownRow.trailingOffset < _targetTrailingRowPixel) { // 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 || + lastKnownRow.trailingOffset >= _targetTrailingRowPixel || _rowNullTerminatedIndex != null); } } @@ -772,10 +792,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { continue; } final double endOfRow = _rowMetrics[row]!.trailingOffset; - if (endOfRow >= verticalOffset.pixels && _firstNonPinnedRow == null) { + if (endOfRow >= _targetLeadingRowPixel && _firstNonPinnedRow == null) { _firstNonPinnedRow = row; } - if (endOfRow >= _targetRowPixel && _lastNonPinnedRow == null) { + if (endOfRow >= _targetTrailingRowPixel && _lastNonPinnedRow == null) { _lastNonPinnedRow = row; break; } @@ -831,7 +851,6 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _rowMetrics[_firstNonPinnedRow]!.leadingOffset - _pinnedRowsExtent : null; - if (_lastPinnedRow != null && _lastPinnedColumn != null) { // Layout cells that are contained in both pinned rows and columns _layoutCells( 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 94955c14936..cc1f28750b8 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -461,9 +461,11 @@ 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. - expect(find.text('R0:C4'), findsNothing); - expect(find.text('R0:C12'), findsNothing); + // No columns laid out before column 4, or after column 11. + expect(find.text('R0:C3'), findsNothing); // Not laid out + expect(find.text('R0:C4'), findsOneWidget); // leading cache extent + expect(find.text('R0:C11'), findsOneWidget); // trailing cache extent + expect(find.text('R0:C12'), findsNothing); // Not laid out await tester.pumpWidget(Container()); @@ -553,9 +555,11 @@ 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. - expect(find.text('R0:C4'), findsNothing); - expect(find.text('R0:C12'), findsNothing); + // No columns laid out before column 4, or after column 11. + expect(find.text('R0:C3'), findsNothing); // Not laid out + expect(find.text('R0:C4'), findsOneWidget); // leading cache extent + expect(find.text('R0:C11'), findsOneWidget); // trailing cache extent + expect(find.text('R0:C12'), findsNothing); // Not laid out await tester.pumpWidget(Container()); @@ -651,12 +655,18 @@ 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. - 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); + // No Columns laid out before/after cache extent. + expect(find.text('R3:C3'), findsNothing); + expect(find.text('R3:C12'), findsNothing); + // Columns in the cache extent. + expect(find.text('R4:C4'), findsOneWidget); // leading + expect(find.text('R4:C11'), findsOneWidget); // trailing + // No rows laid out before/after cache extent. + expect(find.text('R2:C4'), findsNothing); + expect(find.text('R10:C9'), findsNothing); + // Rows in the cache extent. + expect(find.text('R3:C6'), findsOneWidget); // leading + expect(find.text('R9:C6'), findsOneWidget); // trailing await tester.pumpWidget(Container()); @@ -701,14 +711,20 @@ 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. - 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); + // Pinned column + expect(find.text('R4:C0'), findsOneWidget); + // No Columns laid out before/after cache extent. + expect(find.text('R3:C4'), findsNothing); + expect(find.text('R3:C12'), findsNothing); + // Columns in the cache extent. + expect(find.text('R4:C5'), findsOneWidget); // leading + expect(find.text('R4:C11'), findsOneWidget); // trailing + // No rows laid out before/after cache extent. + expect(find.text('R2:C4'), findsNothing); + expect(find.text('R10:C9'), findsNothing); + // Rows in the cache extent. + expect(find.text('R3:C6'), findsOneWidget); // leading + expect(find.text('R9:C6'), findsOneWidget); // trailing await tester.pumpWidget(Container()); @@ -753,14 +769,20 @@ 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. - 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); + // Pinned row + expect(find.text('R0:C4'), findsOneWidget); + // No Columns laid out before/after cache extent. + expect(find.text('R4:C3'), findsNothing); + expect(find.text('R4:C12'), findsNothing); + // Columns in the cache extent. + expect(find.text('R4:C4'), findsOneWidget); // leading + expect(find.text('R4:C11'), findsOneWidget); // trailing + // No rows laid out before/after cache extent. + expect(find.text('R3:C4'), findsNothing); + expect(find.text('R10:C9'), findsNothing); + // Rows in the cache extent. + expect(find.text('R4:C6'), findsOneWidget); // leading + expect(find.text('R9:C6'), findsOneWidget); // trailing await tester.pumpWidget(Container()); @@ -1229,9 +1251,16 @@ void main() { 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); + // No Columns laid out before/after cache extent. + expect(find.text('R0:C1'), findsNothing); expect(find.text('R0:C8'), findsNothing); + // Columns in the cache extent. + expect(find.text('R3:C2'), findsOneWidget); // leading + expect(find.text('R3:C7'), findsOneWidget); // trailing + // No rows laid out after cache extent. + expect(find.text('R5:C5'), findsNothing); + // Rows in the cache extent. + expect(find.text('R4:C5'), findsOneWidget); // trailing // Increase the number of rows await tester.pumpWidget(MaterialApp( @@ -1266,10 +1295,16 @@ void main() { 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); + // No Columns laid out before/after cache extent. + expect(find.text('R0:C1'), findsNothing); + expect(find.text('R0:C10'), findsNothing); + // Columns in the cache extent. + expect(find.text('R3:C2'), findsOneWidget); // leading + expect(find.text('R3:C9'), findsOneWidget); // trailing + // No rows laid out after cache extent. + expect(find.text('R5:C5'), findsNothing); + // Rows in the cache extent. + expect(find.text('R4:C5'), findsOneWidget); // trailing // This exceeds the new bounds. horizontalController.jumpTo(3200.0); await tester.pumpAndSettle(); @@ -1377,9 +1412,16 @@ void main() { tester.getRect(find.text('R4:C9')), const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), ); - // No columns laid out before column 5, or after column 9. - expect(find.text('R0:C4'), findsNothing); + // No Columns laid out before/after cache extent. + expect(find.text('R0:C3'), findsNothing); expect(find.text('R0:C10'), findsNothing); + // Columns in the cache extent. + expect(find.text('R3:C4'), findsOneWidget); // leading + expect(find.text('R3:C9'), findsOneWidget); // trailing + // No rows laid out after cache extent. + expect(find.text('R5:C5'), findsNothing); + // Rows in the cache extent. + expect(find.text('R4:C5'), findsOneWidget); // trailing await tester.pumpWidget(Container()); @@ -1497,9 +1539,16 @@ void main() { tester.getRect(find.text('R4:C9')), const Rect.fromLTRB(600.0, 800.0, 800.0, 1000.0), ); - // No columns laid out before column 5, or after column 9. - expect(find.text('R0:C4'), findsNothing); + // No Columns laid out before/after cache extent. + expect(find.text('R0:C3'), findsNothing); expect(find.text('R0:C10'), findsNothing); + // Columns in the cache extent. + expect(find.text('R3:C4'), findsOneWidget); // leading + expect(find.text('R3:C9'), findsOneWidget); // trailing + // No rows laid out after cache extent. + expect(find.text('R5:C5'), findsNothing); + // Rows in the cache extent. + expect(find.text('R4:C5'), findsOneWidget); // trailing await tester.pumpWidget(Container()); @@ -1638,12 +1687,18 @@ void main() { 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); + // No Columns laid out before/after cache extent. + expect(find.text('R3:C0'), findsNothing); + expect(find.text('R7:C3'), findsNothing); + // Columns in the cache extent. + expect(find.text('R4:C4'), findsOneWidget); // leading + expect(find.text('R4:C9'), findsOneWidget); // trailing + // No rows laid out before/after cache extent. + expect(find.text('R0:C4'), findsNothing); + expect(find.text('R2:C9'), findsNothing); + // Rows in the cache extent. + expect(find.text('R3:C6'), findsOneWidget); // leading + expect(find.text('R7:C6'), findsOneWidget); // trailing await tester.pumpWidget(Container()); @@ -1710,14 +1765,18 @@ void main() { 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); + // No Columns laid out before/after cache extent. + expect(find.text('R3:C1'), findsNothing); + expect(find.text('R3:C2'), findsNothing); + // Columns in the cache extent. + expect(find.text('R3:C5'), findsOneWidget); // leading + expect(find.text('R3:C9'), findsOneWidget); // trailing + // No rows laid out before/after cache extent. + expect(find.text('R0:C5'), findsNothing); + expect(find.text('R2:C5'), findsNothing); + // Rows in the cache extent. + expect(find.text('R3:C6'), findsOneWidget); // leading + expect(find.text('R7:C6'), findsOneWidget); // trailing await tester.pumpWidget(Container()); @@ -1784,14 +1843,20 @@ void main() { 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. + // First pinned row. expect(find.text('R0:C6'), findsOneWidget); - expect(find.text('R3:C6'), findsNothing); - expect(find.text('R8:C6'), findsNothing); + // No Columns laid out before/after cache extent. + expect(find.text('R5:C0'), findsNothing); + expect(find.text('R5:C1'), findsNothing); + // Columns in the cache extent. + expect(find.text('R5:C4'), findsOneWidget); // leading + expect(find.text('R5:C9'), findsOneWidget); // trailing + // No rows laid out before/after cache extent. + expect(find.text('R1:C5'), findsNothing); + expect(find.text('R2:C5'), findsNothing); + // Rows in the cache extent. + expect(find.text('R4:C6'), findsOneWidget); // leading + expect(find.text('R7:C6'), findsOneWidget); // trailing await tester.pumpWidget(Container()); From 6cd0c160525c69fb4d3315e8ea1403e5ac35ca62 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 16 Jul 2025 13:57:31 -0500 Subject: [PATCH 2/3] Release infos --- 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 db1a1badadf..321bbbfdfd2 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.7 + +* Fixes missing leading cache extent in TableView. + ## 0.3.6 * Fixes typo in API docs. diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index 63f2ee628f5..d116e0faee3 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.3.6 +version: 0.3.7 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 28cdd46ee9116ca52d4f81c1015c99620eda97b7 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Thu, 17 Jul 2025 11:34:57 -0500 Subject: [PATCH 3/3] Update packages/two_dimensional_scrollables/lib/src/table_view/table.dart --- .../two_dimensional_scrollables/lib/src/table_view/table.dart | 2 +- 1 file changed, 1 insertion(+), 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 0b3575c368b..7cf1e960c7c 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -659,7 +659,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _firstNonPinnedRow == null) { _firstNonPinnedRow = row; } - if (span.trailingOffset > _targetTrailingRowPixel && + if (span.trailingOffset >= _targetTrailingRowPixel && _lastNonPinnedRow == null) { _lastNonPinnedRow = row; }