Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion lib/features/workspace/result_grid_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,11 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
Timer? _autoScrollTimer;
List<double> _currentDisplayOffsets = const [0];
double _currentAvailableWidth = 0.0;

/// Widths and window the last build used, so a scroll tick can tell whether
/// the set of built columns actually changes before it asks for a rebuild.
List<double> _currentDisplayWidths = const [];
ResultGridColumnWindow? _currentWindow;
double _currentRowsViewportHeight = 0.0;

@override
Expand Down Expand Up @@ -976,11 +981,26 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
}
}

/// The scroll view already translates header and body together; the offset
/// only decides which columns are built (virtualized window). So rebuild only
/// when that window changes, not on every pan frame.
void _onHorizontalScroll() {
if (!_horizontalController.hasClients) return;
final offset = _horizontalController.offset;
if ((offset - _scrollOffset).abs() < 0.5) return;
setState(() => _scrollOffset = offset);
_scrollOffset = offset;

final current = _currentWindow;
if (current != null && _currentDisplayWidths.isNotEmpty) {
final next = computeVisibleColumnWindow(
columnWidths: _currentDisplayWidths,
columnOffsets: _currentDisplayOffsets,
scrollOffset: offset,
viewportWidth: _currentAvailableWidth,
);
if (next == current) return;
}
setState(() {});
}

void _onColumnResize(int index, double delta) {
Expand Down Expand Up @@ -2076,6 +2096,8 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
: computeResultGridColumnOffsets(displayWidths);

final window = _columnWindow(displayWidths, availableWidth);
_currentDisplayWidths = displayWidths;
_currentWindow = window;

return material.Scrollbar(
controller: _horizontalController,
Expand Down
70 changes: 70 additions & 0 deletions test/features/workspace/result_grid_sorting_model_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,74 @@ void main() {
expect(y(tester, 'Aaron') < y(tester, 'Alice'), isTrue);
});
});

group('VirtualResultGrid horizontal scroll rebuilds (#879)', () {
const colCount = 40;
final columns = [for (var c = 0; c < colCount; c++) 'column_$c'];
final rows = [
for (var r = 0; r < 5; r++) [for (var c = 0; c < colCount; c++) 'v${r}_$c'],
];

Future<void> pumpWide(WidgetTester tester) async {
await tester.pumpWidget(
_testShell(
child: material.SizedBox(
width: 600,
height: 400,
child: VirtualResultGrid(columns: columns, rows: rows),
),
),
);
await tester.pumpAndSettle();
}

Finder firstRow() => find.byKey(const material.ValueKey('result-row-0'));

Finder hScrollable() => find.byWidgetPredicate(
(w) => w is Scrollable && w.axis == Axis.horizontal,
);

testWidgets('a small pan inside the same column window does not rebuild rows',
(tester) async {
await pumpWide(tester);
final before = tester.widget(firstRow());
expect(find.text('column_0'), findsOneWidget);

// A few pixels stays inside the current (overscanned) window.
final position = tester.state<ScrollableState>(hScrollable()).position;
position.jumpTo(3);
await tester.pump();
expect(position.pixels, 3);

expect(identical(tester.widget(firstRow()), before), isTrue,
reason: 'rows must not be rebuilt for a pan that keeps the window');
await tester.pumpAndSettle(); // let the scrollbar fade timer finish
});

testWidgets('panning far enough builds the newly visible columns',
(tester) async {
await pumpWide(tester);
expect(find.text('column_39'), findsNothing);

final position = tester.state<ScrollableState>(hScrollable()).position;
position.jumpTo(position.maxScrollExtent);
await tester.pumpAndSettle();

expect(find.text('column_39'), findsOneWidget);
expect(find.text('v0_39'), findsOneWidget);
expect(find.text('column_0'), findsNothing);
});

testWidgets('scrolling back restores the first columns', (tester) async {
await pumpWide(tester);
final position = tester.state<ScrollableState>(hScrollable()).position;
position.jumpTo(position.maxScrollExtent);
await tester.pumpAndSettle();
position.jumpTo(0);
await tester.pumpAndSettle();

expect(find.text('column_0'), findsOneWidget);
expect(find.text('column_39'), findsNothing);
});
});
}
Loading