Skip to content

Commit 8a42447

Browse files
authored
Table columns are resizable (#9485)
1 parent c958e13 commit 8a42447

File tree

5 files changed

+274
-123
lines changed

5 files changed

+274
-123
lines changed

packages/devtools_app/lib/src/shared/table/_flat_table.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ class FlatTableState<T> extends State<FlatTable<T>> with AutoDisposeMixin {
305305
fillWithEmptyRows: widget.fillWithEmptyRows,
306306
enableHoverHandling: widget.enableHoverHandling,
307307
);
308-
if (widget.sizeColumnsToFit || tableController.columnWidths == null) {
308+
if (tableController.columnWidths == null) {
309309
return LayoutBuilder(
310310
builder: (context, constraints) => buildTable(
311311
tableController.computeColumnWidthsSizeToFit(constraints.maxWidth),

packages/devtools_app/lib/src/shared/table/_table_row.dart

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class TableRow<T> extends StatefulWidget {
3737
sortDirection = null,
3838
secondarySortColumn = null,
3939
onSortChanged = null,
40+
onColumnResize = null,
4041
_rowType = _TableRowType.data,
4142
tall = false;
4243

@@ -58,6 +59,7 @@ class TableRow<T> extends StatefulWidget {
5859
sortDirection = null,
5960
secondarySortColumn = null,
6061
onSortChanged = null,
62+
onColumnResize = null,
6163
searchMatchesNotifier = null,
6264
activeSearchMatchNotifier = null,
6365
tall = false,
@@ -75,6 +77,7 @@ class TableRow<T> extends StatefulWidget {
7577
required this.sortColumn,
7678
required this.sortDirection,
7779
required this.onSortChanged,
80+
this.onColumnResize,
7881
this.secondarySortColumn,
7982
this.onPressed,
8083
this.tall = false,
@@ -100,6 +103,7 @@ class TableRow<T> extends StatefulWidget {
100103
required this.sortColumn,
101104
required this.sortDirection,
102105
required this.onSortChanged,
106+
this.onColumnResize,
103107
this.secondarySortColumn,
104108
this.onPressed,
105109
this.tall = false,
@@ -176,6 +180,8 @@ class TableRow<T> extends StatefulWidget {
176180
})?
177181
onSortChanged;
178182

183+
final void Function(int, double)? onColumnResize;
184+
179185
final ValueListenable<List<T>>? searchMatchesNotifier;
180186

181187
final ValueListenable<T?>? activeSearchMatchNotifier;
@@ -527,9 +533,32 @@ class _TableRowState<T> extends State<TableRow<T>>
527533
widget.columnWidths[index],
528534
);
529535
case _TableRowPartDisplayType.columnSpacer:
530-
return const SizedBox(
531-
width: columnSpacing,
532-
child: VerticalDivider(width: columnSpacing),
536+
final columnIndex = columnIndexMap[i - 1];
537+
final onColumnResize = widget.onColumnResize;
538+
final isResizable = columnIndex != null && onColumnResize != null;
539+
return MouseRegion(
540+
cursor: isResizable
541+
? SystemMouseCursors.resizeColumn
542+
: SystemMouseCursors.basic,
543+
child: GestureDetector(
544+
behavior: HitTestBehavior.opaque,
545+
onHorizontalDragUpdate: (details) {
546+
if (isResizable) {
547+
setState(() {
548+
final newWidth = _calculateNewColumnWidth(
549+
width: widget.columnWidths[columnIndex],
550+
delta: details.delta.dx,
551+
minWidth: widget.columns[columnIndex].minWidthPx,
552+
);
553+
onColumnResize(columnIndex, newWidth);
554+
});
555+
}
556+
},
557+
child: const SizedBox(
558+
width: columnSpacing,
559+
child: VerticalDivider(width: columnSpacing),
560+
),
561+
),
533562
);
534563
case _TableRowPartDisplayType.columnGroupSpacer:
535564
return const _ColumnGroupSpacer();
@@ -560,4 +589,13 @@ class _TableRowState<T> extends State<TableRow<T>>
560589

561590
@override
562591
bool shouldShow() => widget.isShown;
592+
593+
double _calculateNewColumnWidth({
594+
required double width,
595+
required double delta,
596+
double? minWidth,
597+
}) => (width + delta).clamp(
598+
minWidth ?? DevToolsTable.columnMinWidth,
599+
double.infinity,
600+
);
563601
}

packages/devtools_app/lib/src/shared/table/table.dart

Lines changed: 87 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -104,34 +104,40 @@ class DevToolsTable<T> extends StatefulWidget {
104104
final bool fillWithEmptyRows;
105105
final bool enableHoverHandling;
106106

107+
static const columnMinWidth = 50.0;
108+
107109
@override
108110
DevToolsTableState<T> createState() => DevToolsTableState<T>();
109111
}
110112

111113
@visibleForTesting
112114
class DevToolsTableState<T> extends State<DevToolsTable<T>>
113115
with AutoDisposeMixin {
116+
static const _resizingDebounceDuration = Duration(milliseconds: 200);
117+
114118
late ScrollController scrollController;
115119
late ScrollController pinnedScrollController;
116120
late ScrollController _horizontalScrollbarController;
117121

118122
late List<T> _data;
119123

120-
/// An adjusted copy of `widget.columnWidths` where any variable width columns
121-
/// may be increased so that the sum of all column widths equals the available
122-
/// screen space.
123-
///
124-
/// This must be calculated where we have access to the Flutter view
125-
/// constraints (e.g. the [LayoutBuilder] below).
124+
late Debouncer _resizingDebouncer;
125+
126126
@visibleForTesting
127-
late List<double> adjustedColumnWidths;
127+
List<double> get columnWidths => _columnWidths;
128+
129+
late List<double> _columnWidths;
130+
131+
double? _previousViewWidth;
128132

129133
@override
130134
void initState() {
131135
super.initState();
132136

133137
_initDataAndAddListeners();
134138

139+
_resizingDebouncer = Debouncer(duration: _resizingDebounceDuration);
140+
135141
final initialScrollOffset = widget.preserveVerticalScrollPosition
136142
? widget.tableController.tableUiState.scrollOffset
137143
: 0.0;
@@ -148,7 +154,7 @@ class DevToolsTableState<T> extends State<DevToolsTable<T>>
148154

149155
pinnedScrollController = ScrollController();
150156

151-
adjustedColumnWidths = List.of(widget.columnWidths);
157+
_columnWidths = List.of(widget.columnWidths);
152158
}
153159

154160
@override
@@ -166,7 +172,9 @@ class DevToolsTableState<T> extends State<DevToolsTable<T>>
166172
_initDataAndAddListeners();
167173
}
168174

169-
adjustedColumnWidths = List.of(widget.columnWidths);
175+
if (!collectionEquals(widget.columnWidths, oldWidget.columnWidths)) {
176+
_columnWidths = List.of(widget.columnWidths);
177+
}
170178
}
171179

172180
void _initDataAndAddListeners() {
@@ -252,93 +260,91 @@ class DevToolsTableState<T> extends State<DevToolsTable<T>>
252260
super.dispose();
253261
}
254262

263+
void _handleColumnResize(int columnIndex, double newWidth) {
264+
setState(() {
265+
_columnWidths[columnIndex] = newWidth;
266+
});
267+
}
268+
255269
/// The width of all columns in the table with additional padding.
256-
double get _tableWidthForOriginalColumns {
270+
double get _currentTableWidth {
257271
var tableWidth = 2 * defaultSpacing;
258272
final numColumnGroupSpacers =
259273
widget.tableController.columnGroups?.numSpacers ?? 0;
260274
final numColumnSpacers =
261275
widget.tableController.columns.numSpacers - numColumnGroupSpacers;
262276
tableWidth += numColumnSpacers * columnSpacing;
263277
tableWidth += numColumnGroupSpacers * columnGroupSpacingWithPadding;
264-
for (final columnWidth in widget.columnWidths) {
278+
for (final columnWidth in _columnWidths) {
265279
tableWidth += columnWidth;
266280
}
267281
return tableWidth;
268282
}
269283

270-
/// Modifies [adjustedColumnWidths] so that any available view space greater
271-
/// than [_tableWidthForOriginalColumns] is distributed evenly across variable
272-
/// width columns.
284+
/// Adjusts the column widths to fit the new [viewWidth].
285+
///
286+
/// This method will attempt to distribute any extra space (positive or
287+
/// negative) amongst the variable-width columns. If there are no
288+
/// variable-width columns, it will distribute the space amongst all columns.
273289
void _adjustColumnWidthsForViewSize(double viewWidth) {
274-
final extraSpace = viewWidth - _tableWidthForOriginalColumns;
275-
if (extraSpace <= 0) {
276-
adjustedColumnWidths = List.of(widget.columnWidths);
290+
final extraSpace = _currentTableWidth - viewWidth;
291+
if (extraSpace == 0) {
277292
return;
278293
}
279294

280-
final adjustedColumnWidthsByIndex = <int, double>{};
281-
282-
/// Helper method to evenly distribute [space] among the columns at
283-
/// [columnIndices].
284-
///
285-
/// This method stores the adjusted width values in
286-
/// [adjustedColumnWidthsByIndex].
287-
void evenlyDistributeColumnSizes(List<int> columnIndices, double space) {
288-
final targetSize = space / columnIndices.length;
289-
290-
var largestColumnIndex = -1;
291-
var largestColumnWidth = 0.0;
292-
for (final index in columnIndices) {
293-
final columnWidth = widget.columnWidths[index];
294-
if (columnWidth >= largestColumnWidth) {
295-
largestColumnIndex = index;
296-
largestColumnWidth = columnWidth;
297-
}
298-
}
299-
if (targetSize < largestColumnWidth) {
300-
// We do not have enough extra space to evenly distribute to all
301-
// columns. Remove the largest column and recurse.
302-
adjustedColumnWidthsByIndex[largestColumnIndex] = largestColumnWidth;
303-
final newColumnIndices = List.of(columnIndices)
304-
..remove(largestColumnIndex);
305-
return evenlyDistributeColumnSizes(
306-
newColumnIndices,
307-
space - largestColumnWidth,
308-
);
309-
}
310-
311-
for (int i = 0; i < columnIndices.length; i++) {
312-
final columnIndex = columnIndices[i];
313-
adjustedColumnWidthsByIndex[columnIndex] = targetSize;
314-
}
315-
}
316-
317-
final variableWidthColumnIndices = <int>[];
318-
var sumVariableWidthColumnSizes = 0.0;
295+
final variableWidthColumnIndices = <(int, double)>[];
319296
for (int i = 0; i < widget.tableController.columns.length; i++) {
320297
final column = widget.tableController.columns[i];
321298
if (column.fixedWidthPx == null) {
322-
variableWidthColumnIndices.add(i);
323-
sumVariableWidthColumnSizes += widget.columnWidths[i];
299+
variableWidthColumnIndices.add((i, _columnWidths[i]));
324300
}
325301
}
326-
final totalVariableWidthColumnSpace =
327-
sumVariableWidthColumnSizes + extraSpace;
328302

329-
evenlyDistributeColumnSizes(
330-
variableWidthColumnIndices,
331-
totalVariableWidthColumnSpace,
303+
// If the table contains variable width columns, then distribute the extra
304+
// space between them. Otherwise, distribute the extra space between all the
305+
// columns.
306+
_distributeExtraSpace(
307+
extraSpace,
308+
indexedColumns: variableWidthColumnIndices.isNotEmpty
309+
? variableWidthColumnIndices
310+
: _columnWidths.indexed,
332311
);
312+
}
333313

334-
adjustedColumnWidths.clear();
335-
for (int i = 0; i < widget.columnWidths.length; i++) {
336-
final originalWidth = widget.columnWidths[i];
337-
final isVariableWidthColumn = variableWidthColumnIndices.contains(i);
338-
adjustedColumnWidths.add(
339-
isVariableWidthColumn ? adjustedColumnWidthsByIndex[i]! : originalWidth,
314+
/// Distributes [extraSpace] evenly between the given [indexedColumns].
315+
///
316+
/// The [extraSpace] will be subtracted from each column's width. The
317+
/// remainder of the division is subtracted from the last column to ensure a
318+
/// perfect fit.
319+
///
320+
/// This method respects the `minWidthPx` of each column.
321+
void _distributeExtraSpace(
322+
double extraSpace, {
323+
required Iterable<(int, double)> indexedColumns,
324+
}) {
325+
final newWidths = List.of(_columnWidths);
326+
final delta = extraSpace / indexedColumns.length;
327+
final remainder = extraSpace % indexedColumns.length;
328+
329+
for (var i = 0; i < indexedColumns.length; i++) {
330+
final columnIndex = indexedColumns.elementAt(i).$1;
331+
var newWidth = indexedColumns.elementAt(i).$2;
332+
333+
newWidth -= delta;
334+
if (i == indexedColumns.length - 1) {
335+
newWidth -= remainder;
336+
}
337+
338+
final column = widget.tableController.columns[columnIndex];
339+
newWidths[columnIndex] = max(
340+
newWidth,
341+
column.minWidthPx ?? DevToolsTable.columnMinWidth,
340342
);
341343
}
344+
345+
setState(() {
346+
_columnWidths = newWidths;
347+
});
342348
}
343349

344350
double _pinnedDataHeight(BoxConstraints tableConstraints) => min(
@@ -372,7 +378,7 @@ class DevToolsTableState<T> extends State<DevToolsTable<T>>
372378
return widget.rowBuilder(
373379
context: context,
374380
index: index,
375-
columnWidths: adjustedColumnWidths,
381+
columnWidths: _columnWidths,
376382
isPinned: isPinned,
377383
enableHoverHandling: widget.enableHoverHandling,
378384
);
@@ -406,7 +412,12 @@ class DevToolsTableState<T> extends State<DevToolsTable<T>>
406412
return LayoutBuilder(
407413
builder: (context, constraints) {
408414
final viewWidth = constraints.maxWidth;
409-
_adjustColumnWidthsForViewSize(viewWidth);
415+
if (_previousViewWidth != null && viewWidth != _previousViewWidth) {
416+
_resizingDebouncer.run(
417+
() => _adjustColumnWidthsForViewSize(viewWidth),
418+
);
419+
}
420+
_previousViewWidth = viewWidth;
410421
return Scrollbar(
411422
controller: _horizontalScrollbarController,
412423
thumbVisibility: true,
@@ -415,14 +426,15 @@ class DevToolsTableState<T> extends State<DevToolsTable<T>>
415426
controller: _horizontalScrollbarController,
416427
child: SelectionArea(
417428
child: SizedBox(
418-
width: max(viewWidth, _tableWidthForOriginalColumns),
429+
width: max(viewWidth, _currentTableWidth),
419430
child: Column(
420431
crossAxisAlignment: CrossAxisAlignment.stretch,
421432
children: [
422433
if (showColumnGroupHeader)
423434
TableRow<T>.tableColumnGroupHeader(
424435
columnGroups: columnGroups,
425-
columnWidths: adjustedColumnWidths,
436+
columnWidths: _columnWidths,
437+
onColumnResize: _handleColumnResize,
426438
sortColumn: sortColumn,
427439
sortDirection: tableUiState.sortDirection,
428440
secondarySortColumn:
@@ -436,7 +448,8 @@ class DevToolsTableState<T> extends State<DevToolsTable<T>>
436448
key: const Key('Table header'),
437449
columns: widget.tableController.columns,
438450
columnGroups: columnGroups,
439-
columnWidths: adjustedColumnWidths,
451+
columnWidths: _columnWidths,
452+
onColumnResize: _handleColumnResize,
440453
sortColumn: sortColumn,
441454
sortDirection: tableUiState.sortDirection,
442455
secondarySortColumn:

packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ To learn more about DevTools, check out the
1717

1818
- Added a horizontal scrollbar to data tables to help with navigation. -
1919
[#9482](https://github.com/flutter/devtools/pull/9482)
20+
- Made it possible to resize data table columns by dragging the header separators. -
21+
[#9845](https://github.com/flutter/devtools/pull/9485)
2022

2123
## Inspector updates
2224

0 commit comments

Comments
 (0)