From 2af69541a214ad723fe44dfa0e3f9b1e2723943c Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Thu, 26 Oct 2023 17:23:47 -0500 Subject: [PATCH] [two_dimensional_scrollables] Add TableSpanPadding (#5039) --- Fixes https://github.com/flutter/flutter/issues/134453 Also related: https://github.com/flutter/flutter/issues/134655 This adds padding to TableSpans with TableSpanPadding. This affords folks a leading and trailing offset to pad rows and columns by. --- .../two_dimensional_scrollables/CHANGELOG.md | 4 + .../lib/src/table_view/table.dart | 114 ++++-- .../lib/src/table_view/table_span.dart | 95 ++++- .../two_dimensional_scrollables/pubspec.yaml | 2 +- .../tableSpanDecoration.defaultMainAxis.png | Bin 4914 -> 5003 bytes .../test/table_view/table_test.dart | 325 +++++++++++++++++- 6 files changed, 501 insertions(+), 39 deletions(-) diff --git a/packages/two_dimensional_scrollables/CHANGELOG.md b/packages/two_dimensional_scrollables/CHANGELOG.md index 570519e6898e..0a76c0b187e3 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.4 + +* Adds TableSpanPadding, TableSpan.padding, and TableSpanDecoration.consumeSpanPadding. + ## 0.0.3 * Fixes paint issue when axes are reversed and TableView has pinned rows and columns. 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 b8829f04ccfa..b6e249c2e026 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -308,6 +308,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ); } + // TODO(Piinks): Pinned rows/cols do not account for what is visible on the + // screen. Ostensibly, we would not want to have pinned rows/columns that + // extend beyond the viewport, we would never see them as they would never + // scroll into view. So this currently implementation is fairly assuming + // we will never have rows/cols that are outside of the viewport. We should + // maybe add an assertion for this during layout. + // https://github.com/flutter/flutter/issues/136833 int? get _lastPinnedRow => delegate.pinnedRowCount > 0 ? delegate.pinnedRowCount - 1 : null; int? get _lastPinnedColumn => @@ -667,12 +674,17 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { }) { // TODO(Piinks): Assert here or somewhere else merged cells cannot span // pinned and unpinned cells (for merged cell follow-up), https://github.com/flutter/flutter/issues/131224 + _Span colSpan, rowSpan; double yPaintOffset = -offset.dy; for (int row = start.row; row <= end.row; row += 1) { double xPaintOffset = -offset.dx; - final double rowHeight = _rowMetrics[row]!.extent; + rowSpan = _rowMetrics[row]!; + final double rowHeight = rowSpan.extent; + yPaintOffset += rowSpan.configuration.padding.leading; for (int column = start.column; column <= end.column; column += 1) { - final double columnWidth = _columnMetrics[column]!.extent; + colSpan = _columnMetrics[column]!; + final double columnWidth = colSpan.extent; + xPaintOffset += colSpan.configuration.padding.leading; final TableVicinity vicinity = TableVicinity(column: column, row: row); // TODO(Piinks): Add back merged cells, https://github.com/flutter/flutter/issues/131224 @@ -689,9 +701,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { cell.layout(cellConstraints); cellParentData.layoutOffset = Offset(xPaintOffset, yPaintOffset); } - xPaintOffset += columnWidth; + xPaintOffset += columnWidth + + _columnMetrics[column]!.configuration.padding.trailing; } - yPaintOffset += rowHeight; + yPaintOffset += + rowHeight + _rowMetrics[row]!.configuration.padding.trailing; } } @@ -836,10 +850,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final LinkedHashMap backgroundColumns = LinkedHashMap(); + final TableSpan rowSpan = _rowMetrics[leading.row]!.configuration; for (int column = leading.column; column <= trailing.column; column++) { - final _Span span = _columnMetrics[column]!; - if (span.configuration.backgroundDecoration != null || - span.configuration.foregroundDecoration != null) { + final TableSpan columnSpan = _columnMetrics[column]!.configuration; + if (columnSpan.backgroundDecoration != null || + columnSpan.foregroundDecoration != null) { final RenderBox leadingCell = getChildFor( TableVicinity(column: column, row: leading.row), )!; @@ -847,18 +862,33 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { TableVicinity(column: column, row: trailing.row), )!; - final Rect rect = Rect.fromPoints( - parentDataOf(leadingCell).paintOffset! + offset, - parentDataOf(trailingCell).paintOffset! + - Offset(trailingCell.size.width, trailingCell.size.height) + - offset, - ); + Rect getColumnRect(bool consumePadding) { + return Rect.fromPoints( + parentDataOf(leadingCell).paintOffset! + + offset - + Offset( + consumePadding ? columnSpan.padding.leading : 0.0, + rowSpan.padding.leading, + ), + parentDataOf(trailingCell).paintOffset! + + offset + + Offset(trailingCell.size.width, trailingCell.size.height) + + Offset( + consumePadding ? columnSpan.padding.trailing : 0.0, + rowSpan.padding.trailing, + ), + ); + } - if (span.configuration.backgroundDecoration != null) { - backgroundColumns[rect] = span.configuration.backgroundDecoration!; + if (columnSpan.backgroundDecoration != null) { + final Rect rect = getColumnRect( + columnSpan.backgroundDecoration!.consumeSpanPadding); + backgroundColumns[rect] = columnSpan.backgroundDecoration!; } - if (span.configuration.foregroundDecoration != null) { - foregroundColumns[rect] = span.configuration.foregroundDecoration!; + if (columnSpan.foregroundDecoration != null) { + final Rect rect = getColumnRect( + columnSpan.foregroundDecoration!.consumeSpanPadding); + foregroundColumns[rect] = columnSpan.foregroundDecoration!; } } } @@ -869,10 +899,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final LinkedHashMap backgroundRows = LinkedHashMap(); + final TableSpan columnSpan = _columnMetrics[leading.column]!.configuration; for (int row = leading.row; row <= trailing.row; row++) { - final _Span span = _rowMetrics[row]!; - if (span.configuration.backgroundDecoration != null || - span.configuration.foregroundDecoration != null) { + final TableSpan rowSpan = _rowMetrics[row]!.configuration; + if (rowSpan.backgroundDecoration != null || + rowSpan.foregroundDecoration != null) { final RenderBox leadingCell = getChildFor( TableVicinity(column: leading.column, row: row), )!; @@ -880,17 +911,33 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { TableVicinity(column: trailing.column, row: row), )!; - final Rect rect = Rect.fromPoints( - parentDataOf(leadingCell).paintOffset! + offset, - parentDataOf(trailingCell).paintOffset! + - Offset(trailingCell.size.width, trailingCell.size.height) + - offset, - ); - if (span.configuration.backgroundDecoration != null) { - backgroundRows[rect] = span.configuration.backgroundDecoration!; + Rect getRowRect(bool consumePadding) { + return Rect.fromPoints( + parentDataOf(leadingCell).paintOffset! + + offset - + Offset( + columnSpan.padding.leading, + consumePadding ? rowSpan.padding.leading : 0.0, + ), + parentDataOf(trailingCell).paintOffset! + + offset + + Offset(trailingCell.size.width, trailingCell.size.height) + + Offset( + columnSpan.padding.leading, + consumePadding ? rowSpan.padding.trailing : 0.0, + ), + ); + } + + if (rowSpan.backgroundDecoration != null) { + final Rect rect = + getRowRect(rowSpan.backgroundDecoration!.consumeSpanPadding); + backgroundRows[rect] = rowSpan.backgroundDecoration!; } - if (span.configuration.foregroundDecoration != null) { - foregroundRows[rect] = span.configuration.foregroundDecoration!; + if (rowSpan.foregroundDecoration != null) { + final Rect rect = + getRowRect(rowSpan.foregroundDecoration!.consumeSpanPadding); + foregroundRows[rect] = rowSpan.foregroundDecoration!; } } } @@ -1028,7 +1075,12 @@ class _Span bool get isPinned => _isPinned; late bool _isPinned; - double get trailingOffset => leadingOffset + extent; + double get trailingOffset { + return leadingOffset + + extent + + configuration.padding.leading + + configuration.padding.trailing; + } // ---- Span Management ---- diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart index fbfd9155a15a..fb6fb5c1f912 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_span.dart @@ -10,6 +10,42 @@ import 'package:flutter/widgets.dart'; import 'table.dart'; +/// Defines the leading and trailing padding values of a [TableSpan]. +class TableSpanPadding { + /// Creates a padding configuration for a [TableSpan]. + const TableSpanPadding({ + this.leading = 0.0, + this.trailing = 0.0, + }); + + /// Creates padding where both the [leading] and [trailing] are `value`. + const TableSpanPadding.all(double value) + : leading = value, + trailing = value; + + /// The leading amount of pixels to pad a [TableSpan] by. + /// + /// If the [TableSpan] is a row and the vertical [Axis] is not reversed, this + /// offset will be applied above the row. If the vertical [Axis] is reversed, + /// this will be applied below the row. + /// + /// If the [TableSpan] is a column and the horizontal [Axis] is not reversed, + /// this offset will be applied to the left the column. If the horizontal + /// [Axis] is reversed, this will be applied to the right of the column. + final double leading; + + /// The trailing amount of pixels to pad a [TableSpan] by. + /// + /// If the [TableSpan] is a row and the vertical [Axis] is not reversed, this + /// offset will be applied below the row. If the vertical [Axis] is reversed, + /// this will be applied above the row. + /// + /// If the [TableSpan] is a column and the horizontal [Axis] is not reversed, + /// this offset will be applied to the right the column. If the horizontal + /// [Axis] is reversed, this will be applied to the left of the column. + final double trailing; +} + /// Defines the extent, visual appearance, and gesture handling of a row or /// column in a [TableView]. /// @@ -20,13 +56,14 @@ class TableSpan { /// The [extent] argument must be provided. const TableSpan({ required this.extent, + TableSpanPadding? padding, this.recognizerFactories = const {}, this.onEnter, this.onExit, this.cursor = MouseCursor.defer, this.backgroundDecoration, this.foregroundDecoration, - }); + }) : padding = padding ?? const TableSpanPadding(); /// Defines the extent of the span. /// @@ -34,6 +71,11 @@ class TableSpan { /// represents a column, this is the width of the column. final TableSpanExtent extent; + /// Defines the leading and or trailing extent to pad the row or column by. + /// + /// Defaults to no padding. + final TableSpanPadding padding; + /// Factory for creating [GestureRecognizer]s that want to compete for /// gestures within the [extent] of the span. /// @@ -251,7 +293,11 @@ class MinTableSpanExtent extends CombiningTableSpanExtent { /// A decoration for a [TableSpan]. class TableSpanDecoration { /// Creates a [TableSpanDecoration]. - const TableSpanDecoration({this.border, this.color}); + const TableSpanDecoration({ + this.border, + this.color, + this.consumeSpanPadding = true, + }); /// The border drawn around the span. final TableSpanBorder? border; @@ -259,6 +305,51 @@ class TableSpanDecoration { /// The color to fill the bounds of the span with. final Color? color; + /// Whether or not the decoration should extend to fill the space created by + /// the [TableSpanPadding]. + /// + /// Defaults to true, meaning if a [TableSpan] is a row, the decoration will + /// apply to the full [TableSpanExtent], including the + /// [TableSpanPadding.leading] and [TableSpanPadding.trailing] for the row. + /// This same row decoration will consume any padding from the column spans so + /// as to decorate the row as one continuous span. + /// + /// {@tool snippet} + /// This example illustrates how [consumeSpanPadding] affects + /// [TableSpanDecoration.color]. By default, the color of the decoration + /// consumes the padding, coloring the row fully by including the padding + /// around the row. When [consumeSpanPadding] is false, the padded area of + /// the row is not decorated. + /// + /// ```dart + /// TableView.builder( + /// rowCount: 4, + /// columnCount: 4, + /// columnBuilder: (int index) => TableSpan( + /// extent: const FixedTableSpanExtent(150.0), + /// padding: const TableSpanPadding(trailing: 10), + /// ), + /// rowBuilder: (int index) => TableSpan( + /// extent: const FixedTableSpanExtent(150.0), + /// padding: TableSpanPadding(leading: 10, trailing: 10), + /// backgroundDecoration: TableSpanDecoration( + /// color: index.isOdd ? Colors.blue : Colors.green, + /// // The background color will not be applied to the padded area. + /// consumeSpanPadding: false, + /// ), + /// ), + /// cellBuilder: (_, TableVicinity vicinity) { + /// return Container( + /// height: 150, + /// width: 150, + /// child: const Center(child: FlutterLogo()), + /// ); + /// }, + /// ); + /// ``` + /// {@end-tool} + final bool consumeSpanPadding; + /// Called to draw the decoration around a span. /// /// The provided [TableSpanDecorationPaintDetails] describes the bounds and diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index bd30a0106dbc..9a392a8bf4b2 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.0.3 +version: 0.0.4 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+ diff --git a/packages/two_dimensional_scrollables/test/table_view/goldens/tableSpanDecoration.defaultMainAxis.png b/packages/two_dimensional_scrollables/test/table_view/goldens/tableSpanDecoration.defaultMainAxis.png index baf2cd358cf6de8fd3a82c2f09b8b32bcd6717af..44cb6497b63fbea7cc46d2de8579b4c4773e93c9 100644 GIT binary patch delta 2536 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 literal 4914 zcmeH~c~lcd9>*sL%ArKG4ns>NN`2i)hJ` zTDFxdpn{+r;SM!G00CL51tf%&BVfT22!t3y2;s_x-FElwKl}Etef#>Hf9B2a&HQG5 z-#I?Nx#NdI?$X_-3jo-4#_RN_0Bo}WK;we;cC{rp@kX4wf+T#3^Z?9WvsraRGr{AG zzqY!NwZFIxz@D))r$6#1=8FX}6+?1Lw^TydeKJm;C7AXJE593F>nUOpgEEi#q-UV< zdBbrhc=#7lArYcw1RC~pF*H|;JM>8;7v1ELH+mE8)&BS|l~OfT4*Tnt zTWDzL3fC3jIvwM4=2K{_PD}A;}n)3@T#QU|^Pum>V z;T-ztzz)O=D zXqy+VuP`h_m?FwnOjA^Oj<^lc+tdB7)0Jb6emq%YHwTVaOkSToQnwcZNiggc120|mxSnqGRDMzF?XXuqJA>gGp1_bJ^O zbdf`9%}m)9XoH;*c&q9#tT?Xr1wThVx_`IY;Sd_4<2<}V{Pm_0{~^we@>22EXNDGJ zZ3;`R{hXgpN=`O3TMHxabymj!8K^CqnzFXHtxSVq{3PD9=2G@QR^fo`>~8+9Nf=1j z|DB{ z9-qae)c$(K`g@LPp$TVUERbP{%&ERge0WVVKu=@^i0O2cuepoqKt8-u_z1)Jw5~h~ zQ_5A=d2Ty@uiklHlk4Kw(Q5Ak1Nr(#ENo60!+bGu!*OK)07u>vEH=~+%fwi^WmoGz z?~Zlj2o=x_dRbX-iQr?SRw=5*!VQE+f2rMawi5HStRVRkE^K7kqCf58s^Wc8V+A_t zv(W?qN_%y^UNFhxr|eYedM-!N*1i-a%15?_xswT4O3KLla)hMI##dW>P01dYIDSm% zY+tgOAT6fF*lx@(7DUdBvuE4kBl%EplAuwn7g`%n5VR6g1( zcg~(*GD#c)$+a=YS3JiE_33+1$4HVx2l=HxU5$6X%=fJ-g$G};_n_wihXP~dM^l5m(_6rL zbCS71Py4>d1rZg3tHdxh)h~qW1sa_?M-G)OB?V{evOCl%LF$C@k5Da;|0l?R)$i~> zfM8QssIRu5j?wgb@w?d)5&Xe#Ie~TI4c@n}CX2WZ1X*p`&CsU7B1~nJ$)18%t%({FrsqH2vxg=SdE>?0d|2&2cI!o#-02S(9ZQz>)(WvT`29B z2nL(-(#!NiW{9$$fCjk|#tXp( z@|Fu#Nu&-nMm?$c>su&ZGwQ3_bcKuNA3^zF|G5cQo%H1{RDr*2ZHLJ8;8!})GmH&` zft@Gh(|h}dzQ2btOL1dyYRg!yoD#7CgnHfr!S1v&4dS~_uv-d7S*6Qz6thIwKiu22 zxFlm6aeqaI|cq%3QWCJLH2*$%i!5biniuF childKeys = + {}; + const TableSpan columnSpan = TableSpan( + extent: FixedTableSpanExtent(200), + padding: TableSpanPadding( + leading: 10.0, + trailing: 20.0, + ), + ); + const TableSpan rowSpan = TableSpan( + extent: FixedTableSpanExtent(200), + padding: TableSpanPadding( + leading: 30.0, + trailing: 40.0, + ), + ); + TableView tableView = TableView.builder( + rowCount: 2, + columnCount: 2, + columnBuilder: (_) => columnSpan, + rowBuilder: (_) => rowSpan, + cellBuilder: (_, TableVicinity vicinity) { + childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); + return SizedBox.square(key: childKeys[vicinity], dimension: 200); + }, + ); + TableViewParentData parentDataOf(RenderBox child) { + return child.parentData! as TableViewParentData; + } + + await tester.pumpWidget(MaterialApp(home: tableView)); + await tester.pumpAndSettle(); + RenderTwoDimensionalViewport viewport = getViewport( + tester, + childKeys.values.first, + ); + // first child + TableVicinity vicinity = const TableVicinity(column: 0, row: 0); + TableViewParentData parentData = parentDataOf( + viewport.firstChild!, + ); + expect(parentData.vicinity, vicinity); + expect( + parentData.layoutOffset, + const Offset( + 10.0, // Leading 10 pixels before first column + 30.0, // leading 30 pixels before first row + ), + ); + // after first child + vicinity = const TableVicinity(column: 1, row: 0); + + parentData = parentDataOf( + viewport.childAfter(viewport.firstChild!)!, + ); + expect(parentData.vicinity, vicinity); + expect( + parentData.layoutOffset, + const Offset( + 240, // 10 leading + 200 first column + 20 trailing + 10 leading + 30.0, // leading 30 pixels before first row + ), + ); + + // last child + vicinity = const TableVicinity(column: 1, row: 1); + parentData = parentDataOf(viewport.lastChild!); + expect(parentData.vicinity, vicinity); + expect( + parentData.layoutOffset, + const Offset( + 240.0, // 10 leading + 200 first column + 20 trailing + 10 leading + 300.0, // 30 leading + 200 first row + 40 trailing + 30 leading + ), + ); + + // reverse + tableView = TableView.builder( + rowCount: 2, + columnCount: 2, + verticalDetails: const ScrollableDetails.vertical(reverse: true), + horizontalDetails: const ScrollableDetails.horizontal(reverse: true), + columnBuilder: (_) => columnSpan, + rowBuilder: (_) => rowSpan, + cellBuilder: (_, TableVicinity vicinity) { + childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); + return SizedBox.square(key: childKeys[vicinity], dimension: 200); + }, + ); + + await tester.pumpWidget(MaterialApp(home: tableView)); + await tester.pumpAndSettle(); + viewport = getViewport( + tester, + childKeys.values.first, + ); + // first child + vicinity = const TableVicinity(column: 0, row: 0); + parentData = parentDataOf( + viewport.firstChild!, + ); + expect(parentData.vicinity, vicinity); + // layoutOffset is later corrected for reverse in the paintOffset + expect(parentData.paintOffset, const Offset(590.0, 370.0)); + // after first child + vicinity = const TableVicinity(column: 1, row: 0); + + parentData = parentDataOf( + viewport.childAfter(viewport.firstChild!)!, + ); + expect(parentData.vicinity, vicinity); + expect(parentData.paintOffset, const Offset(360.0, 370.0)); + + // last child + vicinity = const TableVicinity(column: 1, row: 1); + parentData = parentDataOf(viewport.lastChild!); + expect(parentData.vicinity, vicinity); + expect(parentData.paintOffset, const Offset(360.0, 100.0)); + }); + testWidgets('TableSpan gesture hit testing', (WidgetTester tester) async { int tapCounter = 0; // Rows @@ -568,6 +689,190 @@ void main() { expect(rowExtent.delegate.viewportExtent, 600.0); }); + testWidgets('First row/column layout based on padding', + (WidgetTester tester) async { + // Huge padding, first span layout + // Column-wise + TableView tableView = TableView.builder( + rowCount: 50, + columnCount: 50, + columnBuilder: (_) => const TableSpan( + extent: FixedTableSpanExtent(100), + // This padding is so high, only the first column should be laid out. + padding: TableSpanPadding(leading: 2000), + ), + rowBuilder: (_) => span, + cellBuilder: (_, TableVicinity vicinity) { + return SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ); + }, + ); + + await tester.pumpWidget(MaterialApp(home: tableView)); + await tester.pumpAndSettle(); + // All of these children are so offset by the column padding that they are + // outside of the viewport and cache extent, so all but the very + // first column is laid out. This is so that the ability to scroll the + // table through means such as focus traversal are still accessible. + expect(find.text('Row: 0 Column: 0'), findsOneWidget); + expect(find.text('Row: 1 Column: 0'), findsOneWidget); + expect(find.text('Row: 0 Column: 1'), findsNothing); + expect(find.text('Row: 1 Column: 1'), findsNothing); + expect(find.text('Row: 0 Column: 2'), findsNothing); + expect(find.text('Row: 1 Column: 2'), findsNothing); + + // Row-wise + tableView = TableView.builder( + rowCount: 50, + columnCount: 50, + // This padding is so high, no children should be laid out. + rowBuilder: (_) => const TableSpan( + extent: FixedTableSpanExtent(100), + padding: TableSpanPadding(leading: 2000), + ), + columnBuilder: (_) => span, + cellBuilder: (_, TableVicinity vicinity) { + return SizedBox.square( + dimension: 100, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ); + }, + ); + + await tester.pumpWidget(MaterialApp(home: tableView)); + await tester.pumpAndSettle(); + // All of these children are so offset by the row padding that they are + // outside of the viewport and cache extent, so all but the very + // first row is laid out. This is so that the ability to scroll the + // table through means such as focus traversal are still accessible. + expect(find.text('Row: 0 Column: 0'), findsOneWidget); + expect(find.text('Row: 0 Column: 1'), findsOneWidget); + expect(find.text('Row: 1 Column: 0'), findsNothing); + expect(find.text('Row: 1 Column: 1'), findsNothing); + expect(find.text('Row: 2 Column: 0'), findsNothing); + expect(find.text('Row: 2 Column: 1'), findsNothing); + }); + + testWidgets('lazy layout accounts for gradually accrued padding', + (WidgetTester tester) async { + // Check with gradually accrued paddings + // Column-wise + TableView tableView = TableView.builder( + rowCount: 50, + columnCount: 50, + columnBuilder: (_) => const TableSpan( + extent: FixedTableSpanExtent(200), + ), + rowBuilder: (_) => span, + cellBuilder: (_, TableVicinity vicinity) { + return SizedBox.square( + dimension: 200, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ); + }, + ); + + await tester.pumpWidget(MaterialApp(home: tableView)); + await tester.pumpAndSettle(); + + // No padding here, check all lazily laid out columns in one row. + expect(find.text('Row: 0 Column: 0'), findsOneWidget); + expect(find.text('Row: 0 Column: 1'), findsOneWidget); + expect(find.text('Row: 0 Column: 2'), findsOneWidget); + expect(find.text('Row: 0 Column: 3'), findsOneWidget); + expect(find.text('Row: 0 Column: 4'), findsOneWidget); + expect(find.text('Row: 0 Column: 5'), findsOneWidget); + expect(find.text('Row: 0 Column: 6'), findsNothing); + + tableView = TableView.builder( + rowCount: 50, + columnCount: 50, + columnBuilder: (_) => const TableSpan( + extent: FixedTableSpanExtent(200), + padding: TableSpanPadding(trailing: 200), + ), + rowBuilder: (_) => span, + cellBuilder: (_, TableVicinity vicinity) { + return SizedBox.square( + dimension: 200, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ); + }, + ); + + await tester.pumpWidget(MaterialApp(home: tableView)); + await tester.pumpAndSettle(); + + // Fewer children laid out. + expect(find.text('Row: 0 Column: 0'), findsOneWidget); + expect(find.text('Row: 0 Column: 1'), findsOneWidget); + expect(find.text('Row: 0 Column: 2'), findsOneWidget); + expect(find.text('Row: 0 Column: 3'), findsNothing); + expect(find.text('Row: 0 Column: 4'), findsNothing); + expect(find.text('Row: 0 Column: 5'), findsNothing); + expect(find.text('Row: 0 Column: 6'), findsNothing); + + // Row-wise + tableView = TableView.builder( + rowCount: 50, + columnCount: 50, + rowBuilder: (_) => const TableSpan( + extent: FixedTableSpanExtent(200), + ), + columnBuilder: (_) => span, + cellBuilder: (_, TableVicinity vicinity) { + return SizedBox.square( + dimension: 200, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ); + }, + ); + + await tester.pumpWidget(MaterialApp(home: tableView)); + await tester.pumpAndSettle(); + + // No padding here, check all lazily laid out rows in one column. + expect(find.text('Row: 0 Column: 0'), findsOneWidget); + expect(find.text('Row: 1 Column: 0'), findsOneWidget); + expect(find.text('Row: 2 Column: 0'), findsOneWidget); + expect(find.text('Row: 3 Column: 0'), findsOneWidget); + expect(find.text('Row: 4 Column: 0'), findsOneWidget); + expect(find.text('Row: 5 Column: 0'), findsNothing); + + tableView = TableView.builder( + rowCount: 50, + columnCount: 50, + rowBuilder: (_) => const TableSpan( + extent: FixedTableSpanExtent(200), + padding: TableSpanPadding(trailing: 200), + ), + columnBuilder: (_) => span, + cellBuilder: (_, TableVicinity vicinity) { + return SizedBox.square( + dimension: 200, + child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), + ); + }, + ); + + await tester.pumpWidget(MaterialApp(home: tableView)); + await tester.pumpAndSettle(); + + // Fewer children laid out. + expect(find.text('Row: 0 Column: 0'), findsOneWidget); + expect(find.text('Row: 1 Column: 0'), findsOneWidget); + expect(find.text('Row: 2 Column: 0'), findsOneWidget); + expect(find.text('Row: 3 Column: 0'), findsNothing); + expect(find.text('Row: 4 Column: 0'), findsNothing); + expect(find.text('Row: 5 Column: 0'), findsNothing); + + // Check padding with pinned rows and columns + // TODO(Piinks): Pinned rows/columns are not lazily laid out, should check + // for assertions in this case. Will add in https://github.com/flutter/flutter/issues/136833 + }); + testWidgets('regular layout - no pinning', (WidgetTester tester) async { final ScrollController verticalController = ScrollController(); final ScrollController horizontalController = ScrollController(); @@ -950,13 +1255,17 @@ void main() { (WidgetTester tester) async { // TODO(Piinks): Rewrite this to remove golden files from this repo when // mock_canvas is public - https://github.com/flutter/flutter/pull/131631 - // foreground, background, and precedence per mainAxis + // * foreground, background, and precedence per mainAxis + // * Break out a separate test for padding decorations to validate paint + // rect calls TableView tableView = TableView.builder( rowCount: 2, columnCount: 2, columnBuilder: (int index) => TableSpan( extent: const FixedTableSpanExtent(200.0), + padding: index == 0 ? const TableSpanPadding(trailing: 10) : null, foregroundDecoration: const TableSpanDecoration( + consumeSpanPadding: false, border: TableSpanBorder( trailing: BorderSide( color: Colors.orange, @@ -965,12 +1274,15 @@ void main() { ), ), backgroundDecoration: TableSpanDecoration( + // consumePadding true by default color: index.isEven ? Colors.red : null, ), ), rowBuilder: (int index) => TableSpan( extent: const FixedTableSpanExtent(200.0), + padding: index == 1 ? const TableSpanPadding(leading: 10) : null, foregroundDecoration: const TableSpanDecoration( + // consumePadding true by default border: TableSpanBorder( leading: BorderSide( color: Colors.green, @@ -980,12 +1292,15 @@ void main() { ), backgroundDecoration: TableSpanDecoration( color: index.isOdd ? Colors.blue : null, + consumeSpanPadding: false, ), ), cellBuilder: (_, TableVicinity vicinity) { - return const SizedBox.square( - dimension: 200, - child: Center(child: FlutterLogo()), + return Container( + height: 200, + width: 200, + color: Colors.grey.withOpacity(0.5), + child: const Center(child: FlutterLogo()), ); }, ); @@ -1052,7 +1367,7 @@ void main() { (WidgetTester tester) async { // TODO(Piinks): Rewrite this to remove golden files from this repo when // mock_canvas is public - https://github.com/flutter/flutter/pull/131631 - // foreground, background, and precedence per mainAxis + // * foreground, background, and precedence per mainAxis final TableView tableView = TableView.builder( verticalDetails: const ScrollableDetails.vertical(reverse: true), horizontalDetails: const ScrollableDetails.horizontal(reverse: true),