1+ import 'package:flutter/foundation.dart' ;
12import 'package:flutter/material.dart' as material;
23import 'package:flutter/services.dart' show Clipboard, ClipboardData;
34import 'package:querya_desktop/core/layout/ui_scale.dart' ;
@@ -11,6 +12,56 @@ abstract final class ResultGridMetrics {
1112 static const double maxColumnWidth = 280 ;
1213 static const int columnWidthSampleRows = 40 ;
1314 static const int tooltipMinLength = 48 ;
15+
16+ /// Extra columns built beyond the viewport to reduce scroll flicker.
17+ static const int columnOverscan = 2 ;
18+ }
19+
20+ /// Inclusive visible column window with spacer widths for off-screen columns.
21+ @immutable
22+ class ResultGridColumnWindow {
23+ const ResultGridColumnWindow ({
24+ required this .first,
25+ required this .last,
26+ required this .leadingWidth,
27+ required this .trailingWidth,
28+ });
29+
30+ /// Empty window (no columns).
31+ static const empty = ResultGridColumnWindow (
32+ first: 0 ,
33+ last: - 1 ,
34+ leadingWidth: 0 ,
35+ trailingWidth: 0 ,
36+ );
37+
38+ /// Inclusive first visible (or overscanned) column index.
39+ final int first;
40+
41+ /// Inclusive last visible (or overscanned) column index.
42+ final int last;
43+
44+ /// Width of columns strictly before [first] (left spacer).
45+ final double leadingWidth;
46+
47+ /// Width of columns strictly after [last] (right spacer).
48+ final double trailingWidth;
49+
50+ bool get isEmpty => last < first;
51+
52+ int get columnCount => isEmpty ? 0 : last - first + 1 ;
53+
54+ @override
55+ bool operator == (Object other) =>
56+ identical (this , other) ||
57+ other is ResultGridColumnWindow &&
58+ first == other.first &&
59+ last == other.last &&
60+ leadingWidth == other.leadingWidth &&
61+ trailingWidth == other.trailingWidth;
62+
63+ @override
64+ int get hashCode => Object .hash (first, last, leadingWidth, trailingWidth);
1465}
1566
1667/// Computes fixed column widths from headers and a sample of [rows] .
@@ -38,7 +89,68 @@ List<double> computeResultGridColumnWidths({
3889 return widths;
3990}
4091
41- /// Virtualized read-only grid for SQL query results.
92+ /// Prefix sums: `offsets[i]` = sum of widths `[0, i)` .
93+ @visibleForTesting
94+ List <double > computeResultGridColumnOffsets (List <double > columnWidths) {
95+ final offsets = List <double >.filled (columnWidths.length + 1 , 0 );
96+ for (var i = 0 ; i < columnWidths.length; i++ ) {
97+ offsets[i + 1 ] = offsets[i] + columnWidths[i];
98+ }
99+ return offsets;
100+ }
101+
102+ /// Visible column range for a horizontal viewport (with overscan).
103+ @visibleForTesting
104+ ResultGridColumnWindow computeVisibleColumnWindow ({
105+ required List <double > columnWidths,
106+ required List <double > columnOffsets,
107+ required double scrollOffset,
108+ required double viewportWidth,
109+ int overscanColumns = ResultGridMetrics .columnOverscan,
110+ }) {
111+ final n = columnWidths.length;
112+ if (n == 0 ) return ResultGridColumnWindow .empty;
113+ assert (columnOffsets.length == n + 1 );
114+
115+ final total = columnOffsets[n];
116+ if (viewportWidth <= 0 ) {
117+ return ResultGridColumnWindow (
118+ first: 0 ,
119+ last: n - 1 ,
120+ leadingWidth: 0 ,
121+ trailingWidth: 0 ,
122+ );
123+ }
124+
125+ final start = scrollOffset.clamp (0.0 , total);
126+ final end = (scrollOffset + viewportWidth).clamp (0.0 , total);
127+
128+ // First column with any pixel past [start].
129+ var first = 0 ;
130+ while (first < n && columnOffsets[first + 1 ] <= start) {
131+ first++ ;
132+ }
133+ // Last column with any pixel before [end].
134+ var last = n - 1 ;
135+ while (last > 0 && columnOffsets[last] >= end) {
136+ last-- ;
137+ }
138+ if (first > last) {
139+ first = last.clamp (0 , n - 1 );
140+ }
141+
142+ first = (first - overscanColumns).clamp (0 , n - 1 );
143+ last = (last + overscanColumns).clamp (0 , n - 1 );
144+
145+ return ResultGridColumnWindow (
146+ first: first,
147+ last: last,
148+ leadingWidth: columnOffsets[first],
149+ trailingWidth: total - columnOffsets[last + 1 ],
150+ );
151+ }
152+
153+ /// Virtualized read-only grid for SQL query results (rows + columns).
42154class VirtualResultGrid extends material.StatefulWidget {
43155 const VirtualResultGrid ({
44156 super .key,
@@ -58,7 +170,15 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
58170 final _verticalController = material.ScrollController ();
59171
60172 List <double > _columnWidths = const [];
173+ List <double > _columnOffsets = const [0 ];
61174 bool _widthsNeedUpdate = true ;
175+ double _scrollOffset = 0 ;
176+
177+ @override
178+ void initState () {
179+ super .initState ();
180+ _horizontalController.addListener (_onHorizontalScroll);
181+ }
62182
63183 @override
64184 void didChangeDependencies () {
@@ -76,11 +196,19 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
76196
77197 @override
78198 void dispose () {
199+ _horizontalController.removeListener (_onHorizontalScroll);
79200 _horizontalController.dispose ();
80201 _verticalController.dispose ();
81202 super .dispose ();
82203 }
83204
205+ void _onHorizontalScroll () {
206+ if (! _horizontalController.hasClients) return ;
207+ final offset = _horizontalController.offset;
208+ if ((offset - _scrollOffset).abs () < 0.5 ) return ;
209+ setState (() => _scrollOffset = offset);
210+ }
211+
84212 List <double > _computeColumnWidths () {
85213 return computeResultGridColumnWidths (
86214 columns: widget.columns,
@@ -92,7 +220,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
92220
93221 double get _tableWidth {
94222 if (_columnWidths.isEmpty) return 0 ;
95- return _columnWidths.reduce ((a, b) => a + b) ;
223+ return _columnOffsets[ _columnWidths.length] ;
96224 }
97225
98226 double _scaledRowHeight (material.BuildContext context) =>
@@ -101,21 +229,37 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
101229 double _scaledHeaderHeight (material.BuildContext context) =>
102230 context.scaled (ResultGridMetrics .headerHeight);
103231
232+ ResultGridColumnWindow _columnWindow (
233+ List <double > displayWidths,
234+ double viewportWidth,
235+ ) {
236+ final offsets = identical (displayWidths, _columnWidths)
237+ ? _columnOffsets
238+ : computeResultGridColumnOffsets (displayWidths);
239+ return computeVisibleColumnWindow (
240+ columnWidths: displayWidths,
241+ columnOffsets: offsets,
242+ scrollOffset: _scrollOffset,
243+ viewportWidth: viewportWidth,
244+ );
245+ }
246+
104247 @override
105248 material.Widget build (material.BuildContext context) {
106249 if (_widthsNeedUpdate) {
107250 _columnWidths = _computeColumnWidths ();
251+ _columnOffsets = computeResultGridColumnOffsets (_columnWidths);
108252 _widthsNeedUpdate = false ;
109253 }
110254 final cs = Theme .of (context).colorScheme;
111- final colCount = widget.columns.length;
112255 final rowHeight = _scaledRowHeight (context);
113256 final headerHeight = _scaledHeaderHeight (context);
114257
115258 return material.RepaintBoundary (
116259 child: material.LayoutBuilder (
117260 builder: (context, constraints) {
118261 final availableWidth = constraints.maxWidth;
262+
119263 var displayWidths = _columnWidths;
120264 var tableWidth = _tableWidth;
121265 if (tableWidth < availableWidth && _columnWidths.isNotEmpty) {
@@ -129,6 +273,8 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
129273 tableWidth = availableWidth;
130274 }
131275
276+ final window = _columnWindow (displayWidths, availableWidth);
277+
132278 return material.Scrollbar (
133279 controller: _horizontalController,
134280 thumbVisibility: true ,
@@ -144,6 +290,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
144290 _HeaderRow (
145291 columns: widget.columns,
146292 columnWidths: displayWidths,
293+ window: window,
147294 height: headerHeight,
148295 colorScheme: cs,
149296 ),
@@ -162,7 +309,7 @@ class _VirtualResultGridState extends material.State<VirtualResultGrid> {
162309 key: ValueKey ('result-row-$rowIndex ' ),
163310 row: row,
164311 columnWidths: displayWidths,
165- columnCount : colCount ,
312+ window : window ,
166313 height: rowHeight,
167314 colorScheme: cs,
168315 striped: ! isEven,
@@ -186,12 +333,14 @@ class _HeaderRow extends material.StatelessWidget {
186333 const _HeaderRow ({
187334 required this .columns,
188335 required this .columnWidths,
336+ required this .window,
189337 required this .height,
190338 required this .colorScheme,
191339 });
192340
193341 final List <String > columns;
194342 final List <double > columnWidths;
343+ final ResultGridColumnWindow window;
195344 final double height;
196345 final ColorScheme colorScheme;
197346
@@ -209,13 +358,17 @@ class _HeaderRow extends material.StatelessWidget {
209358 ),
210359 child: material.Row (
211360 children: [
212- for (var i = 0 ; i < columns.length; i++ )
361+ if (window.leadingWidth > 0 )
362+ material.SizedBox (width: window.leadingWidth),
363+ for (var i = window.first; i <= window.last; i++ )
213364 _GridCell (
214365 text: columns[i],
215366 width: columnWidths[i],
216367 isHeader: true ,
217368 colorScheme: colorScheme,
218369 ),
370+ if (window.trailingWidth > 0 )
371+ material.SizedBox (width: window.trailingWidth),
219372 ],
220373 ),
221374 );
@@ -227,15 +380,15 @@ class _DataRow extends material.StatelessWidget {
227380 super .key,
228381 required this .row,
229382 required this .columnWidths,
230- required this .columnCount ,
383+ required this .window ,
231384 required this .height,
232385 required this .colorScheme,
233386 required this .striped,
234387 });
235388
236389 final List <String > row;
237390 final List <double > columnWidths;
238- final int columnCount ;
391+ final ResultGridColumnWindow window ;
239392 final double height;
240393 final ColorScheme colorScheme;
241394 final bool striped;
@@ -257,12 +410,16 @@ class _DataRow extends material.StatelessWidget {
257410 ),
258411 child: material.Row (
259412 children: [
260- for (var c = 0 ; c < columnCount; c++ )
413+ if (window.leadingWidth > 0 )
414+ material.SizedBox (width: window.leadingWidth),
415+ for (var c = window.first; c <= window.last; c++ )
261416 _GridCell (
262417 text: c < row.length ? row[c] : '' ,
263418 width: columnWidths[c],
264419 colorScheme: colorScheme,
265420 ),
421+ if (window.trailingWidth > 0 )
422+ material.SizedBox (width: window.trailingWidth),
266423 ],
267424 ),
268425 ),
0 commit comments