diff --git a/packages/devtools_app/lib/src/shared/table/_flat_table.dart b/packages/devtools_app/lib/src/shared/table/_flat_table.dart index 087148ca739..79bb3e7d15d 100644 --- a/packages/devtools_app/lib/src/shared/table/_flat_table.dart +++ b/packages/devtools_app/lib/src/shared/table/_flat_table.dart @@ -305,7 +305,7 @@ class FlatTableState extends State> with AutoDisposeMixin { fillWithEmptyRows: widget.fillWithEmptyRows, enableHoverHandling: widget.enableHoverHandling, ); - if (widget.sizeColumnsToFit || tableController.columnWidths == null) { + if (tableController.columnWidths == null) { return LayoutBuilder( builder: (context, constraints) => buildTable( tableController.computeColumnWidthsSizeToFit(constraints.maxWidth), diff --git a/packages/devtools_app/lib/src/shared/table/_table_row.dart b/packages/devtools_app/lib/src/shared/table/_table_row.dart index 9161ca180fe..b7c6a010b92 100644 --- a/packages/devtools_app/lib/src/shared/table/_table_row.dart +++ b/packages/devtools_app/lib/src/shared/table/_table_row.dart @@ -37,6 +37,7 @@ class TableRow extends StatefulWidget { sortDirection = null, secondarySortColumn = null, onSortChanged = null, + onColumnResize = null, _rowType = _TableRowType.data, tall = false; @@ -58,6 +59,7 @@ class TableRow extends StatefulWidget { sortDirection = null, secondarySortColumn = null, onSortChanged = null, + onColumnResize = null, searchMatchesNotifier = null, activeSearchMatchNotifier = null, tall = false, @@ -75,6 +77,7 @@ class TableRow extends StatefulWidget { required this.sortColumn, required this.sortDirection, required this.onSortChanged, + this.onColumnResize, this.secondarySortColumn, this.onPressed, this.tall = false, @@ -100,6 +103,7 @@ class TableRow extends StatefulWidget { required this.sortColumn, required this.sortDirection, required this.onSortChanged, + this.onColumnResize, this.secondarySortColumn, this.onPressed, this.tall = false, @@ -176,6 +180,8 @@ class TableRow extends StatefulWidget { })? onSortChanged; + final void Function(int, double)? onColumnResize; + final ValueListenable>? searchMatchesNotifier; final ValueListenable? activeSearchMatchNotifier; @@ -527,9 +533,32 @@ class _TableRowState extends State> widget.columnWidths[index], ); case _TableRowPartDisplayType.columnSpacer: - return const SizedBox( - width: columnSpacing, - child: VerticalDivider(width: columnSpacing), + final columnIndex = columnIndexMap[i - 1]; + final onColumnResize = widget.onColumnResize; + final isResizable = columnIndex != null && onColumnResize != null; + return MouseRegion( + cursor: isResizable + ? SystemMouseCursors.resizeColumn + : SystemMouseCursors.basic, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onHorizontalDragUpdate: (details) { + if (isResizable) { + setState(() { + final newWidth = _calculateNewColumnWidth( + width: widget.columnWidths[columnIndex], + delta: details.delta.dx, + minWidth: widget.columns[columnIndex].minWidthPx, + ); + onColumnResize(columnIndex, newWidth); + }); + } + }, + child: const SizedBox( + width: columnSpacing, + child: VerticalDivider(width: columnSpacing), + ), + ), ); case _TableRowPartDisplayType.columnGroupSpacer: return const _ColumnGroupSpacer(); @@ -560,4 +589,13 @@ class _TableRowState extends State> @override bool shouldShow() => widget.isShown; + + double _calculateNewColumnWidth({ + required double width, + required double delta, + double? minWidth, + }) => (width + delta).clamp( + minWidth ?? DevToolsTable.columnMinWidth, + double.infinity, + ); } diff --git a/packages/devtools_app/lib/src/shared/table/table.dart b/packages/devtools_app/lib/src/shared/table/table.dart index a7a1ce4a477..9e01cf2283b 100644 --- a/packages/devtools_app/lib/src/shared/table/table.dart +++ b/packages/devtools_app/lib/src/shared/table/table.dart @@ -104,6 +104,8 @@ class DevToolsTable extends StatefulWidget { final bool fillWithEmptyRows; final bool enableHoverHandling; + static const columnMinWidth = 50.0; + @override DevToolsTableState createState() => DevToolsTableState(); } @@ -111,20 +113,22 @@ class DevToolsTable extends StatefulWidget { @visibleForTesting class DevToolsTableState extends State> with AutoDisposeMixin { + static const _resizingDebounceDuration = Duration(milliseconds: 200); + late ScrollController scrollController; late ScrollController pinnedScrollController; late ScrollController _horizontalScrollbarController; late List _data; - /// An adjusted copy of `widget.columnWidths` where any variable width columns - /// may be increased so that the sum of all column widths equals the available - /// screen space. - /// - /// This must be calculated where we have access to the Flutter view - /// constraints (e.g. the [LayoutBuilder] below). + late Debouncer _resizingDebouncer; + @visibleForTesting - late List adjustedColumnWidths; + List get columnWidths => _columnWidths; + + late List _columnWidths; + + double? _previousViewWidth; @override void initState() { @@ -132,6 +136,8 @@ class DevToolsTableState extends State> _initDataAndAddListeners(); + _resizingDebouncer = Debouncer(duration: _resizingDebounceDuration); + final initialScrollOffset = widget.preserveVerticalScrollPosition ? widget.tableController.tableUiState.scrollOffset : 0.0; @@ -148,7 +154,7 @@ class DevToolsTableState extends State> pinnedScrollController = ScrollController(); - adjustedColumnWidths = List.of(widget.columnWidths); + _columnWidths = List.of(widget.columnWidths); } @override @@ -166,7 +172,9 @@ class DevToolsTableState extends State> _initDataAndAddListeners(); } - adjustedColumnWidths = List.of(widget.columnWidths); + if (!collectionEquals(widget.columnWidths, oldWidget.columnWidths)) { + _columnWidths = List.of(widget.columnWidths); + } } void _initDataAndAddListeners() { @@ -252,8 +260,14 @@ class DevToolsTableState extends State> super.dispose(); } + void _handleColumnResize(int columnIndex, double newWidth) { + setState(() { + _columnWidths[columnIndex] = newWidth; + }); + } + /// The width of all columns in the table with additional padding. - double get _tableWidthForOriginalColumns { + double get _currentTableWidth { var tableWidth = 2 * defaultSpacing; final numColumnGroupSpacers = widget.tableController.columnGroups?.numSpacers ?? 0; @@ -261,84 +275,76 @@ class DevToolsTableState extends State> widget.tableController.columns.numSpacers - numColumnGroupSpacers; tableWidth += numColumnSpacers * columnSpacing; tableWidth += numColumnGroupSpacers * columnGroupSpacingWithPadding; - for (final columnWidth in widget.columnWidths) { + for (final columnWidth in _columnWidths) { tableWidth += columnWidth; } return tableWidth; } - /// Modifies [adjustedColumnWidths] so that any available view space greater - /// than [_tableWidthForOriginalColumns] is distributed evenly across variable - /// width columns. + /// Adjusts the column widths to fit the new [viewWidth]. + /// + /// This method will attempt to distribute any extra space (positive or + /// negative) amongst the variable-width columns. If there are no + /// variable-width columns, it will distribute the space amongst all columns. void _adjustColumnWidthsForViewSize(double viewWidth) { - final extraSpace = viewWidth - _tableWidthForOriginalColumns; - if (extraSpace <= 0) { - adjustedColumnWidths = List.of(widget.columnWidths); + final extraSpace = _currentTableWidth - viewWidth; + if (extraSpace == 0) { return; } - final adjustedColumnWidthsByIndex = {}; - - /// Helper method to evenly distribute [space] among the columns at - /// [columnIndices]. - /// - /// This method stores the adjusted width values in - /// [adjustedColumnWidthsByIndex]. - void evenlyDistributeColumnSizes(List columnIndices, double space) { - final targetSize = space / columnIndices.length; - - var largestColumnIndex = -1; - var largestColumnWidth = 0.0; - for (final index in columnIndices) { - final columnWidth = widget.columnWidths[index]; - if (columnWidth >= largestColumnWidth) { - largestColumnIndex = index; - largestColumnWidth = columnWidth; - } - } - if (targetSize < largestColumnWidth) { - // We do not have enough extra space to evenly distribute to all - // columns. Remove the largest column and recurse. - adjustedColumnWidthsByIndex[largestColumnIndex] = largestColumnWidth; - final newColumnIndices = List.of(columnIndices) - ..remove(largestColumnIndex); - return evenlyDistributeColumnSizes( - newColumnIndices, - space - largestColumnWidth, - ); - } - - for (int i = 0; i < columnIndices.length; i++) { - final columnIndex = columnIndices[i]; - adjustedColumnWidthsByIndex[columnIndex] = targetSize; - } - } - - final variableWidthColumnIndices = []; - var sumVariableWidthColumnSizes = 0.0; + final variableWidthColumnIndices = <(int, double)>[]; for (int i = 0; i < widget.tableController.columns.length; i++) { final column = widget.tableController.columns[i]; if (column.fixedWidthPx == null) { - variableWidthColumnIndices.add(i); - sumVariableWidthColumnSizes += widget.columnWidths[i]; + variableWidthColumnIndices.add((i, _columnWidths[i])); } } - final totalVariableWidthColumnSpace = - sumVariableWidthColumnSizes + extraSpace; - evenlyDistributeColumnSizes( - variableWidthColumnIndices, - totalVariableWidthColumnSpace, + // If the table contains variable width columns, then distribute the extra + // space between them. Otherwise, distribute the extra space between all the + // columns. + _distributeExtraSpace( + extraSpace, + indexedColumns: variableWidthColumnIndices.isNotEmpty + ? variableWidthColumnIndices + : _columnWidths.indexed, ); + } - adjustedColumnWidths.clear(); - for (int i = 0; i < widget.columnWidths.length; i++) { - final originalWidth = widget.columnWidths[i]; - final isVariableWidthColumn = variableWidthColumnIndices.contains(i); - adjustedColumnWidths.add( - isVariableWidthColumn ? adjustedColumnWidthsByIndex[i]! : originalWidth, + /// Distributes [extraSpace] evenly between the given [indexedColumns]. + /// + /// The [extraSpace] will be subtracted from each column's width. The + /// remainder of the division is subtracted from the last column to ensure a + /// perfect fit. + /// + /// This method respects the `minWidthPx` of each column. + void _distributeExtraSpace( + double extraSpace, { + required Iterable<(int, double)> indexedColumns, + }) { + final newWidths = List.of(_columnWidths); + final delta = extraSpace / indexedColumns.length; + final remainder = extraSpace % indexedColumns.length; + + for (var i = 0; i < indexedColumns.length; i++) { + final columnIndex = indexedColumns.elementAt(i).$1; + var newWidth = indexedColumns.elementAt(i).$2; + + newWidth -= delta; + if (i == indexedColumns.length - 1) { + newWidth -= remainder; + } + + final column = widget.tableController.columns[columnIndex]; + newWidths[columnIndex] = max( + newWidth, + column.minWidthPx ?? DevToolsTable.columnMinWidth, ); } + + setState(() { + _columnWidths = newWidths; + }); } double _pinnedDataHeight(BoxConstraints tableConstraints) => min( @@ -372,7 +378,7 @@ class DevToolsTableState extends State> return widget.rowBuilder( context: context, index: index, - columnWidths: adjustedColumnWidths, + columnWidths: _columnWidths, isPinned: isPinned, enableHoverHandling: widget.enableHoverHandling, ); @@ -406,7 +412,12 @@ class DevToolsTableState extends State> return LayoutBuilder( builder: (context, constraints) { final viewWidth = constraints.maxWidth; - _adjustColumnWidthsForViewSize(viewWidth); + if (_previousViewWidth != null && viewWidth != _previousViewWidth) { + _resizingDebouncer.run( + () => _adjustColumnWidthsForViewSize(viewWidth), + ); + } + _previousViewWidth = viewWidth; return Scrollbar( controller: _horizontalScrollbarController, thumbVisibility: true, @@ -415,14 +426,15 @@ class DevToolsTableState extends State> controller: _horizontalScrollbarController, child: SelectionArea( child: SizedBox( - width: max(viewWidth, _tableWidthForOriginalColumns), + width: max(viewWidth, _currentTableWidth), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (showColumnGroupHeader) TableRow.tableColumnGroupHeader( columnGroups: columnGroups, - columnWidths: adjustedColumnWidths, + columnWidths: _columnWidths, + onColumnResize: _handleColumnResize, sortColumn: sortColumn, sortDirection: tableUiState.sortDirection, secondarySortColumn: @@ -436,7 +448,8 @@ class DevToolsTableState extends State> key: const Key('Table header'), columns: widget.tableController.columns, columnGroups: columnGroups, - columnWidths: adjustedColumnWidths, + columnWidths: _columnWidths, + onColumnResize: _handleColumnResize, sortColumn: sortColumn, sortDirection: tableUiState.sortDirection, secondarySortColumn: diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 76c08acbcc2..6b91b8769c8 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -17,6 +17,8 @@ To learn more about DevTools, check out the - Added a horizontal scrollbar to data tables to help with navigation. - [#9482](https://github.com/flutter/devtools/pull/9482) +- Made it possible to resize data table columns by dragging the header separators. - + [#9845](https://github.com/flutter/devtools/pull/9485) ## Inspector updates diff --git a/packages/devtools_app/test/shared/table/table_test.dart b/packages/devtools_app/test/shared/table/table_test.dart index b2303f43dcd..dd2dcd97446 100644 --- a/packages/devtools_app/test/shared/table/table_test.dart +++ b/packages/devtools_app/test/shared/table/table_test.dart @@ -411,7 +411,6 @@ void main() { expect(data[8].name, 'Snap'); } }); - // TODO(jacobr): add a golden image tests for column width tests. testWidgets('displays with many columns', (WidgetTester tester) async { final table = FlatTable( @@ -464,6 +463,7 @@ void main() { ), ), ); + await _waitForResizingDebouncer(tester); } group('size to fit', () { @@ -484,7 +484,7 @@ void main() { find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 3); expect(columnWidths[0], 300.0); // Fixed width column. expect(columnWidths[1], 400.0); // Fixed width column. @@ -495,15 +495,14 @@ void main() { expect(adjustedColumnWidths[1], 400.0); expect(adjustedColumnWidths[2], 3252.0); } - viewSize.value = const Size(800.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 3); expect(columnWidths[0], 300.0); expect(columnWidths[1], 400.0); @@ -516,13 +515,13 @@ void main() { } viewSize.value = const Size(200.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 3); expect(columnWidths[0], 300.0); // Fixed width column. expect(columnWidths[1], 400.0); // Fixed width column. @@ -531,7 +530,7 @@ void main() { expect(adjustedColumnWidths.length, 3); expect(adjustedColumnWidths[0], 300.0); expect(adjustedColumnWidths[1], 400.0); - expect(adjustedColumnWidths[2], 0.0); + expect(adjustedColumnWidths[2], 50.0); } }); @@ -557,7 +556,7 @@ void main() { find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 4); expect(columnWidths[0], 300.0); // Fixed width column. expect(columnWidths[1], 1620.0); // Min width wide column @@ -572,13 +571,13 @@ void main() { } viewSize.value = const Size(1000.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 4); expect(columnWidths[0], 300.0); // Fixed width column. expect(columnWidths[1], 120.0); // Min width wide column @@ -593,13 +592,13 @@ void main() { } viewSize.value = const Size(200.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 4); expect(columnWidths[0], 300.0); // Fixed width column. expect(columnWidths[1], 100.0); // Min width wide column @@ -610,7 +609,7 @@ void main() { expect(adjustedColumnWidths[0], 300.0); expect(adjustedColumnWidths[1], 100.0); expect(adjustedColumnWidths[2], 400.0); - expect(adjustedColumnWidths[3], 0.0); + expect(adjustedColumnWidths[3], 50.0); } }); @@ -638,7 +637,7 @@ void main() { find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 5); expect(columnWidths[0], 300.0); // Fixed width column expect(columnWidths[1], 1076.0); // Min width wide column @@ -656,13 +655,13 @@ void main() { } viewSize.value = const Size(1501.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 5); expect(columnWidths[0], 300.0); // Fixed width column expect(columnWidths[1], 243.0); // Min width wide column @@ -680,13 +679,13 @@ void main() { } viewSize.value = const Size(1200.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 5); expect(columnWidths[0], 300.0); // Fixed width column expect(columnWidths[1], 134.0); // Min width wide column @@ -704,13 +703,13 @@ void main() { } viewSize.value = const Size(1000.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 5); expect(columnWidths[0], 300.0); // Fixed width column expect(columnWidths[1], 100.0); // Min width wide column @@ -724,7 +723,7 @@ void main() { expect(adjustedColumnWidths[1], 100.0); expect(adjustedColumnWidths[2], 160.0); expect(adjustedColumnWidths[3], 400.0); - expect(adjustedColumnWidths[4], 0.0); + expect(adjustedColumnWidths[4], 50.0); } }, ); @@ -748,7 +747,7 @@ void main() { find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 3); expect(columnWidths[0], 300.0); // Fixed width column. expect(columnWidths[1], 400.0); // Fixed width column. @@ -757,17 +756,17 @@ void main() { expect(adjustedColumnWidths.length, 3); expect(adjustedColumnWidths[0], 300.0); expect(adjustedColumnWidths[1], 400.0); - expect(adjustedColumnWidths[2], 3252.0); + expect(adjustedColumnWidths[2], 1200.0); } viewSize.value = const Size(800.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 3); expect(columnWidths[0], 300.0); // Fixed width column. expect(columnWidths[1], 400.0); // Fixed width column. @@ -776,17 +775,17 @@ void main() { expect(adjustedColumnWidths.length, 3); expect(adjustedColumnWidths[0], 300.0); expect(adjustedColumnWidths[1], 400.0); - expect(adjustedColumnWidths[2], 1200.0); + expect(adjustedColumnWidths[2], 52.0); } viewSize.value = const Size(200.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 3); expect(columnWidths[0], 300.0); // Fixed width column. expect(columnWidths[1], 400.0); // Fixed width column. @@ -795,7 +794,7 @@ void main() { expect(adjustedColumnWidths.length, 3); expect(adjustedColumnWidths[0], 300.0); expect(adjustedColumnWidths[1], 400.0); - expect(adjustedColumnWidths[2], 1200.0); + expect(adjustedColumnWidths[2], 50.0); } }); @@ -821,7 +820,7 @@ void main() { find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 4); expect(columnWidths[0], 300.0); // Fixed width column. expect(columnWidths[1], 100.0); // Min width wide column @@ -830,19 +829,19 @@ void main() { expect(adjustedColumnWidths.length, 4); expect(adjustedColumnWidths[0], 300.0); - expect(adjustedColumnWidths[1], 1620.0); + expect(adjustedColumnWidths[1], 100.0); expect(adjustedColumnWidths[2], 400.0); - expect(adjustedColumnWidths[3], 1620.0); + expect(adjustedColumnWidths[3], 1200.0); } viewSize.value = const Size(1000.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 4); expect(columnWidths[0], 300.0); // Fixed width column. expect(columnWidths[1], 100.0); // Min width wide column @@ -853,17 +852,17 @@ void main() { expect(adjustedColumnWidths[0], 300.0); expect(adjustedColumnWidths[1], 100.0); expect(adjustedColumnWidths[2], 400.0); - expect(adjustedColumnWidths[3], 1200.0); + expect(adjustedColumnWidths[3], 670.0); } viewSize.value = const Size(200.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 4); expect(columnWidths[0], 300.0); // Fixed width column. expect(columnWidths[1], 100.0); // Min width wide column @@ -874,7 +873,7 @@ void main() { expect(adjustedColumnWidths[0], 300.0); expect(adjustedColumnWidths[1], 100.0); expect(adjustedColumnWidths[2], 400.0); - expect(adjustedColumnWidths[3], 1200.0); + expect(adjustedColumnWidths[3], 50.0); } }); @@ -902,7 +901,7 @@ void main() { find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths.length, 5); expect(columnWidths[0], 300.0); // Fixed width column expect(columnWidths[1], 100.0); // Min width wide column @@ -912,20 +911,20 @@ void main() { expect(adjustedColumnWidths.length, 5); expect(adjustedColumnWidths[0], 300.0); - expect(adjustedColumnWidths[1], 1014.0); - expect(adjustedColumnWidths[2], 1014.0); + expect(adjustedColumnWidths[1], 100.0); + expect(adjustedColumnWidths[2], 160.0); expect(adjustedColumnWidths[3], 400.0); expect(adjustedColumnWidths[4], 1200.0); } viewSize.value = const Size(1200.0, 200.0); - await tester.pumpAndSettle(); + await _waitForResizingDebouncer(tester); { final DevToolsTableState tableState = tester.state( find.byType(DevToolsTable), ); final columnWidths = tableState.widget.columnWidths; - final adjustedColumnWidths = tableState.adjustedColumnWidths; + final adjustedColumnWidths = tableState.columnWidths; expect(columnWidths[0], 300.0); // Fixed width column expect(columnWidths[1], 100.0); // Min width wide column expect(columnWidths[2], 160.0); // Very wide min width wide column @@ -937,11 +936,102 @@ void main() { expect(adjustedColumnWidths[1], 100.0); expect(adjustedColumnWidths[2], 160.0); expect(adjustedColumnWidths[3], 400.0); - expect(adjustedColumnWidths[4], 1200.0); + expect(adjustedColumnWidths[4], 856.0); } }, ); }); + + group('resizing columns', () { + late FlatTable table; + + Finder getTableHeaderFinder() => find.byKey(const Key('Table header')); + + Finder getResizerFinder() => find.descendant( + of: getTableHeaderFinder(), + matching: find.byWidgetPredicate( + (widget) => + widget is GestureDetector && + widget.child is SizedBox && + (widget.child as SizedBox).width == columnSpacing, + ), + ); + + setUp(() { + table = FlatTable( + columns: [flatNameColumn, _NumberColumn()], + data: flatData, + dataKey: 'test-data', + keyFactory: (d) => Key(d.name), + defaultSortColumn: flatNameColumn, + defaultSortDirection: SortDirection.ascending, + ); + }); + + testWidgets('resize with right to left drag', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(wrap(table)); + + final DevToolsTableState tableState = tester.state( + find.byType(DevToolsTable), + ); + + final initialWidths = List.of(tableState.columnWidths); + expect(initialWidths, orderedEquals([300.0, 400.0])); + + final resizerFinder = getResizerFinder(); + expect(resizerFinder, findsOneWidget); + + double dragX = 100.0; + const dragOffset = 20.0; + await tester.drag(resizerFinder, Offset(dragX, 0)); + await _waitForResizingDebouncer(tester); + + final widthsAfterDrag1 = List.of(tableState.columnWidths); + expect(widthsAfterDrag1[0], initialWidths[0] + dragX - dragOffset); + expect(widthsAfterDrag1[1], initialWidths[1]); + + dragX = 50.0; + await tester.drag(resizerFinder, Offset(0 - dragX, 0)); + await _waitForResizingDebouncer(tester); + + final widthsAfterDrag2 = tableState.columnWidths; + expect(widthsAfterDrag2[0], widthsAfterDrag1[0] - dragX + dragOffset); + }); + + testWidgets('resize with left to right drag', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(wrap(table)); + + final DevToolsTableState tableState = tester.state( + find.byType(DevToolsTable), + ); + + final initialWidths = List.of(tableState.columnWidths); + expect(initialWidths, orderedEquals([300.0, 400.0])); + + final resizerFinder = getResizerFinder(); + expect(resizerFinder, findsOneWidget); + + double dragX = 50.0; + const dragOffset = 20.0; + await tester.drag(resizerFinder, Offset(0 - dragX, 0)); + await _waitForResizingDebouncer(tester); + + final widthsAfterDrag1 = List.of(tableState.columnWidths); + expect(widthsAfterDrag1[0], initialWidths[0] - dragX + dragOffset); + expect(widthsAfterDrag1[1], initialWidths[1]); + + dragX = 100.0; + await tester.drag(resizerFinder, Offset(dragX, 0)); + await _waitForResizingDebouncer(tester); + + final widthsAfterDrag2 = tableState.columnWidths; + expect(widthsAfterDrag2[0], widthsAfterDrag1[0] + dragX - dragOffset); + }); + }); }); testWidgets('can select an item', (WidgetTester tester) async { @@ -1669,6 +1759,14 @@ void main() { }); } +const resizingDebounceDurationInMs = 200; + +Future _waitForResizingDebouncer(WidgetTester tester) async { + await tester.pumpAndSettle( + const Duration(milliseconds: resizingDebounceDurationInMs * 2), + ); +} + class TestData extends TreeNode { TestData(this.name, this.number);