diff --git a/packages/two_dimensional_scrollables/CHANGELOG.md b/packages/two_dimensional_scrollables/CHANGELOG.md index e1541589d45f..20fe5bf78407 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.1 + +* Refactors TableSpans to use basic Span classes. Clean up for incoming TreeView. + ## 0.2.0 * Adds support for infinite rows and columns in TableView. diff --git a/packages/two_dimensional_scrollables/lib/src/common/span.dart b/packages/two_dimensional_scrollables/lib/src/common/span.dart new file mode 100644 index 000000000000..d2e88fc4aca9 --- /dev/null +++ b/packages/two_dimensional_scrollables/lib/src/common/span.dart @@ -0,0 +1,493 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// Defines the leading and trailing padding values of a [Span]. +class SpanPadding { + /// Creates a padding configuration for a [Span]. + const SpanPadding({ + this.leading = 0.0, + this.trailing = 0.0, + }); + + /// Creates padding where both the [leading] and [trailing] are `value`. + const SpanPadding.all(double value) + : leading = value, + trailing = value; + + /// The leading amount of pixels to pad a [Span] by. + /// + /// If the [Span] 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 [Span] 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 [Span] by. + /// + /// If the [Span] 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 [Span] 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. +/// +/// A span refers to either a column or a row. +class Span { + /// Creates a [Span]. + /// + /// The [extent] argument must be provided. + const Span({ + required this.extent, + SpanPadding? padding, + this.recognizerFactories = const {}, + this.onEnter, + this.onExit, + this.cursor = MouseCursor.defer, + this.backgroundDecoration, + this.foregroundDecoration, + }) : padding = padding ?? const SpanPadding(); + + /// Defines the extent of the span. + /// + /// If the span represents a row, this is the height of the row. If it + /// represents a column, this is the width of the column. + final SpanExtent extent; + + /// Defines the leading and or trailing extent to pad the row or column by. + /// + /// Defaults to no padding. + final SpanPadding padding; + + /// Factory for creating [GestureRecognizer]s that want to compete for + /// gestures within the [extent] of the span. + /// + /// If this span represents a row, a factory for a [TapGestureRecognizer] + /// could for example be provided here to recognize taps within the bounds + /// of the row. + /// + /// The content of a cell takes precedence in handling pointer events. Next, + /// the recognizers defined for the [TableView.mainAxis], followed by the + /// other [Axis]. + final Map recognizerFactories; + + /// Triggers when a mouse pointer, with or without buttons pressed, has + /// entered the region encompassing the row or column described by this span. + /// + /// This callback is triggered when the pointer has started to be contained by + /// the region, either due to a pointer event, or due to the movement or + /// appearance of the region. This method is always matched by a later + /// [onExit] call. + final PointerEnterEventListener? onEnter; + + /// Triggered when a mouse pointer, with or without buttons pressed, has + /// exited the region encompassing the row or column described by this span. + /// + /// This callback is triggered when the pointer has stopped being contained + /// by the region, either due to a pointer event, or due to the movement or + /// disappearance of the region. This method always matches an earlier + /// [onEnter] call. + final PointerExitEventListener? onExit; + + /// Mouse cursor to show when the mouse hovers over this span. + /// + /// Defaults to [MouseCursor.defer]. + final MouseCursor cursor; + + /// The [SpanDecoration] to paint behind the content of this span. + /// + /// The [backgroundDecoration]s of the [TableView.mainAxis] are painted after + /// the [backgroundDecoration]s of the other [Axis]. On top of that, + /// the content of the individual cells in this span are painted, followed by + /// any specified [foregroundDecoration]. + /// + /// The decorations of pinned rows and columns are painted separately from + /// the decorations of unpinned rows and columns, with the unpinned rows and + /// columns being painted first to account for overlap from pinned rows or + /// columns. + final SpanDecoration? backgroundDecoration; + + /// The [SpanDecoration] to paint in front of the content of this span. + /// + /// After painting any [backgroundDecoration]s, and the content of the + /// individual cells, the [foregroundDecoration] of the [TableView.mainAxis] + /// are painted after the [foregroundDecoration]s of the other [Axis] + /// + /// The decorations of pinned rows and columns are painted separately from + /// the decorations of unpinned rows and columns, with the unpinned rows and + /// columns being painted first to account for overlap from pinned rows or + /// columns. + final SpanDecoration? foregroundDecoration; +} + +/// Delegate passed to [SpanExtent.calculateExtent] from the +/// [RenderTableViewport] during layout. +/// +/// Provides access to metrics from the [TableView] that a [SpanExtent] may +/// need to calculate its extent. +/// +/// Extents will not be computed for every frame unless the delegate has been +/// updated. Otherwise, after the extents are computed during the first layout +/// passed, they are cached and reused in subsequent frames. +class SpanExtentDelegate { + /// Creates a [SpanExtentDelegate]. + /// + /// Usually, only [TableView]s need to create instances of this class. + const SpanExtentDelegate({ + required this.viewportExtent, + required this.precedingExtent, + }); + + /// The size of the viewport in the axis-direction of the span. + /// + /// If the [SpanExtent] calculates the extent of a row, this is the + /// height of the viewport. If it calculates the extent of a column, this + /// is the width of the viewport. + final double viewportExtent; + + /// The scroll extent that has already been used up by previous spans. + /// + /// If the [SpanExtent] calculates the extent of a row, this is the + /// sum of all row extents prior to this row. If it calculates the extent + /// of a column, this is the sum of all previous columns. + final double precedingExtent; +} + +/// Defines the extent of a [Span]. +/// +/// If the span is a row, its extent is the height of the row. If the span is +/// a column, it's the width of that column. +abstract class SpanExtent { + /// Creates a [SpanExtent]. + const SpanExtent(); + + /// Calculates the actual extent of the span in pixels. + /// + /// To assist with the calculation, span metrics obtained from the provided + /// [SpanExtentDelegate] may be used. + double calculateExtent(SpanExtentDelegate delegate); +} + +/// A span extent with a fixed [pixels] value. +class FixedSpanExtent extends SpanExtent { + /// Creates a [FixedSpanExtent]. + /// + /// The provided [pixels] value must be equal to or greater then zero. + const FixedSpanExtent(this.pixels) : assert(pixels >= 0.0); + + /// The extent of the span in pixels. + final double pixels; + + @override + double calculateExtent(SpanExtentDelegate delegate) => pixels; +} + +/// Specified the span extent as a fraction of the viewport extent. +/// +/// For example, a column with a 1.0 as [fraction] will be as wide as the +/// viewport. +class FractionalSpanExtent extends SpanExtent { + /// Creates a [FractionalSpanExtent]. + /// + /// The provided [fraction] value must be equal to or greater than zero. + const FractionalSpanExtent( + this.fraction, + ) : assert(fraction >= 0.0); + + /// The fraction of the [SpanExtentDelegate.viewportExtent] that the + /// span should occupy. + /// + /// The provided [fraction] value must be equal to or greater than zero. + final double fraction; + + @override + double calculateExtent(SpanExtentDelegate delegate) => + delegate.viewportExtent * fraction; +} + +/// Specifies that the span should occupy the remaining space in the viewport. +/// +/// If the previous [Span]s can already fill out the viewport, this will +/// evaluate the span's extent to zero. If the previous spans cannot fill out the +/// viewport, this span's extent will be whatever space is left to fill out the +/// viewport. +/// +/// To avoid that the span's extent evaluates to zero, consider combining this +/// extent with another extent. The following example will make sure that the +/// span's extent is at least 200 pixels, but if there's more than that available +/// in the viewport, it will fill all that space: +/// +/// ```dart +/// const MaxSpanExtent(FixedSpanExtent(200.0), RemainingSpanExtent()); +/// ``` +class RemainingSpanExtent extends SpanExtent { + /// Creates a [RemainingSpanExtent]. + const RemainingSpanExtent(); + + @override + double calculateExtent(SpanExtentDelegate delegate) { + return math.max(0.0, delegate.viewportExtent - delegate.precedingExtent); + } +} + +/// Signature for a function that combines the result of two +/// [SpanExtent.calculateExtent] invocations. +/// +/// Used by [CombiningSpanExtent]; +typedef SpanExtentCombiner = double Function(double, double); + +/// Runs the result of two [SpanExtent]s through a `combiner` function +/// to determine the ultimate pixel extent of a span. +class CombiningSpanExtent extends SpanExtent { + /// Creates a [CombiningSpanExtent]; + const CombiningSpanExtent(this._extent1, this._extent2, this._combiner); + + final SpanExtent _extent1; + final SpanExtent _extent2; + final SpanExtentCombiner _combiner; + + @override + double calculateExtent(SpanExtentDelegate delegate) { + return _combiner( + _extent1.calculateExtent(delegate), + _extent2.calculateExtent(delegate), + ); + } +} + +/// Returns the larger pixel extent of the two provided [SpanExtent]. +class MaxSpanExtent extends CombiningSpanExtent { + /// Creates a [MaxSpanExtent]. + const MaxSpanExtent( + SpanExtent extent1, + SpanExtent extent2, + ) : super(extent1, extent2, math.max); +} + +/// Returns the smaller pixel extent of the two provided [SpanExtent]. +class MinSpanExtent extends CombiningSpanExtent { + /// Creates a [MinSpanExtent]. + const MinSpanExtent( + SpanExtent extent1, + SpanExtent extent2, + ) : super(extent1, extent2, math.min); +} + +/// A decoration for a [Span]. +/// +/// When decorating merged cells in the [TableView], a merged cell will take its +/// decoration from the leading cell of the merged span. +class SpanDecoration { + /// Creates a [SpanDecoration]. + const SpanDecoration({ + this.border, + this.color, + this.borderRadius, + this.consumeSpanPadding = true, + }); + + /// The border drawn around the span. + final SpanBorder? border; + + /// The radius by which the leading and trailing ends of a row or + /// column will be rounded. + /// + /// Applies to the [border] and [color] of the given [Span]. + final BorderRadius? borderRadius; + + /// 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 [SpanPadding]. + /// + /// Defaults to true, meaning if a [Span] is a row, the decoration will + /// apply to the full [SpanExtent], including the + /// [SpanPadding.leading] and [SpanPadding.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 + /// [SpanDecoration.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 [SpanDecorationPaintDetails] describes the bounds and + /// orientation of the span that are currently visible inside the viewport. + /// The extent of the actual span may be larger. + /// + /// If a span contains pinned parts, [paint] is invoked separately for the + /// pinned and unpinned parts. For example: If a row contains a pinned column, + /// paint is called with the [SpanDecorationPaintDetails.rect] for the + /// cell representing the pinned column and separately with another + /// [SpanDecorationPaintDetails.rect] containing all the other unpinned + /// cells. + void paint(SpanDecorationPaintDetails details) { + if (color != null) { + final Paint paint = Paint() + ..color = color! + ..isAntiAlias = borderRadius != null; + if (borderRadius == null || borderRadius == BorderRadius.zero) { + details.canvas.drawRect(details.rect, paint); + } else { + details.canvas.drawRRect( + borderRadius!.toRRect(details.rect), + paint, + ); + } + } + if (border != null) { + border!.paint(details, borderRadius); + } + } +} + +/// Describes the border for a [Span]. +class SpanBorder { + /// Creates a [SpanBorder]. + const SpanBorder({ + this.trailing = BorderSide.none, + this.leading = BorderSide.none, + }); + + /// The border to draw on the trailing side of the span, based on the + /// [AxisDirection]. + /// + /// The trailing side of a row is the bottom when [Axis.vertical] is + /// [AxisDirection.down], the trailing side of a column + /// is its right side when the [Axis.horizontal] is [AxisDirection.right]. + final BorderSide trailing; + + /// The border to draw on the leading side of the span. + /// + /// The leading side of a row is the top when [Axis.vertical] is + /// [AxisDirection.down], the leading side of a column + /// is its left side when the [Axis.horizontal] is [AxisDirection.right]. + final BorderSide leading; + + /// Called to draw the border around a span. + /// + /// If the span represents a row, `axisDirection` will be [AxisDirection.left] + /// or [AxisDirection.right]. For columns, the `axisDirection` will be + /// [AxisDirection.down] or [AxisDirection.up]. + /// + /// The provided [SpanDecorationPaintDetails] describes the bounds and + /// orientation of the span that are currently visible inside the viewport. + /// The extent of the actual span may be larger. + /// + /// If a span contains pinned parts, [paint] is invoked separately for the + /// pinned and unpinned parts. For example: If a row contains a pinned column, + /// paint is called with the [SpanDecorationPaintDetails.rect] for the + /// cell representing the pinned column and separately with another + /// [SpanDecorationPaintDetails.rect] containing all the other unpinned + /// cells. + void paint( + SpanDecorationPaintDetails details, + BorderRadius? borderRadius, + ) { + final AxisDirection axisDirection = details.axisDirection; + switch (axisDirectionToAxis(axisDirection)) { + case Axis.horizontal: + final Border border = Border( + top: axisDirection == AxisDirection.right ? leading : trailing, + bottom: axisDirection == AxisDirection.right ? trailing : leading, + ); + border.paint( + details.canvas, + details.rect, + borderRadius: borderRadius, + ); + case Axis.vertical: + final Border border = Border( + left: axisDirection == AxisDirection.down ? leading : trailing, + right: axisDirection == AxisDirection.down ? trailing : leading, + ); + border.paint( + details.canvas, + details.rect, + borderRadius: borderRadius, + ); + } + } +} + +/// Provides the details of a given [SpanDecoration] for painting. +/// +/// Created during paint by the [RenderTableViewport] for the +/// [Span.foregroundDecoration] and [Span.backgroundDecoration]. +class SpanDecorationPaintDetails { + /// Creates the details needed to paint a [SpanDecoration]. + /// + /// The [canvas], [rect], and [axisDirection] must be provided. + SpanDecorationPaintDetails({ + required this.canvas, + required this.rect, + required this.axisDirection, + }); + + /// The [Canvas] that the [SpanDecoration] will be painted to. + final Canvas canvas; + + /// A [Rect] representing the visible area of a row or column in the + /// [TableView], as represented by a [Span]. + /// + /// This Rect contains all of the visible children in a given row or column, + /// which is the area the [SpanDecoration] will be applied to. + final Rect rect; + + /// The [AxisDirection] of the [Axis] of the [Span]. + /// + /// When [AxisDirection.down] or [AxisDirection.up], which would be + /// [Axis.vertical], a column is being painted. When [AxisDirection.left] or + /// [AxisDirection.right], which would be [Axis.horizontal], a row is being + /// painted. + final AxisDirection axisDirection; +} 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 9f2caa667d4a..f2c889556054 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -1007,7 +1007,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _rowMetrics[firstRow]!.leadingOffset + _rowMetrics[firstRow]!.configuration.padding.leading; if (_rowsAreInfinite && _rowMetrics[lastRow] == null) { - // The number of rows is infinte, and we have not calculated + // The number of rows is infinite, and we have not calculated // the metrics to the full extent of the merged cell. Update the // metrics so we have all the information for the merged area. _updateRowMetrics(appendRows: true, toRowIndex: lastRow); @@ -1042,7 +1042,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _columnMetrics[firstColumn]!.configuration.padding.leading; if (_columnsAreInfinite && _columnMetrics[lastColumn] == null) { - // The number of columns is infinte, and we have not calculated + // The number of columns is infinite, and we have not calculated // the metrics to the full extent of the merged cell. Update the // metrics so we have all the information for the merged area. _updateColumnMetrics( diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart index 11ea2459ea08..3bc7d4b64c8e 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table_delegate.dart @@ -317,7 +317,7 @@ class TableCellListDelegate extends TwoDimensionalChildListDelegate @override TableSpan? buildRow(int index) { if (index >= rowCount) { - // The list deleagte has a finite number of rows. + // The list delegate has a finite number of rows. return null; } return _rowBuilder(index); 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 e19a0f899b00..3c6a9657d5a8 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 @@ -2,140 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:math' as math; +import '../common/span.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import 'table.dart'; +// Note: TableSpan and TreeSpan may branch out with unique features in +// the future, so keeping the TableSpan* classes feels more future-safe than +// removing them. Plus, it is not breaking. /// 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; -} +typedef TableSpanPadding = SpanPadding; /// Defines the extent, visual appearance, and gesture handling of a row or /// column in a [TableView]. /// /// A span refers to either a column or a row in a table. -class TableSpan { - /// Creates a [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. - /// - /// If the span represents a row, this is the height of the row. If it - /// 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. - /// - /// If this span represents a row, a factory for a [TapGestureRecognizer] - /// could for example be provided here to recognize taps within the bounds - /// of the row. - /// - /// The content of a cell takes precedence in handling pointer events. Next, - /// the recognizers defined for the [TableView.mainAxis], followed by the - /// other [Axis]. - final Map recognizerFactories; - - /// Triggers when a mouse pointer, with or without buttons pressed, has - /// entered the region encompassing the row or column described by this span. - /// - /// This callback is triggered when the pointer has started to be contained by - /// the region, either due to a pointer event, or due to the movement or - /// appearance of the region. This method is always matched by a later - /// [onExit] call. - final PointerEnterEventListener? onEnter; - - /// Triggered when a mouse pointer, with or without buttons pressed, has - /// exited the region encompassing the row or column described by this span. - /// - /// This callback is triggered when the pointer has stopped being contained - /// by the region, either due to a pointer event, or due to the movement or - /// disappearance of the region. This method always matches an earlier - /// [onEnter] call. - final PointerExitEventListener? onExit; - - /// Mouse cursor to show when the mouse hovers over this span. - /// - /// Defaults to [MouseCursor.defer]. - final MouseCursor cursor; - - /// The [TableSpanDecoration] to paint behind the content of this span. - /// - /// The [backgroundDecoration]s of the [TableView.mainAxis] are painted after - /// the [backgroundDecoration]s of the other [Axis]. On top of that, - /// the content of the individual cells in this span are painted, followed by - /// any specified [foregroundDecoration]. - /// - /// The decorations of pinned rows and columns are painted separately from - /// the decorations of unpinned rows and columns, with the unpinned rows and - /// columns being painted first to account for overlap from pinned rows or - /// columns. - final TableSpanDecoration? backgroundDecoration; - - /// The [TableSpanDecoration] to paint in front of the content of this span. - /// - /// After painting any [backgroundDecoration]s, and the content of the - /// individual cells, the [foregroundDecoration] of the [TableView.mainAxis] - /// are painted after the [foregroundDecoration]s of the other [Axis] - /// - /// The decorations of pinned rows and columns are painted separately from - /// the decorations of unpinned rows and columns, with the unpinned rows and - /// columns being painted first to account for overlap from pinned rows or - /// columns. - final TableSpanDecoration? foregroundDecoration; -} +typedef TableSpan = Span; /// Delegate passed to [TableSpanExtent.calculateExtent] from the /// [RenderTableViewport] during layout. @@ -146,81 +26,22 @@ class TableSpan { /// Extents will not be computed for every frame unless the delegate has been /// updated. Otherwise, after the extents are computed during the first layout /// passed, they are cached and reused in subsequent frames. -class TableSpanExtentDelegate { - /// Creates a [TableSpanExtentDelegate]. - /// - /// Usually, only [TableView]s need to create instances of this class. - const TableSpanExtentDelegate({ - required this.viewportExtent, - required this.precedingExtent, - }); - - /// The size of the viewport in the axis-direction of the span. - /// - /// If the [TableSpanExtent] calculates the extent of a row, this is the - /// height of the viewport. If it calculates the extent of a column, this - /// is the width of the viewport. - final double viewportExtent; - - /// The scroll extent that has already been used up by previous spans. - /// - /// If the [TableSpanExtent] calculates the extent of a row, this is the - /// sum of all row extents prior to this row. If it calculates the extent - /// of a column, this is the sum of all previous columns. - final double precedingExtent; -} +typedef TableSpanExtentDelegate = SpanExtentDelegate; /// Defines the extent of a [TableSpan]. /// /// If the span is a row, its extent is the height of the row. If the span is /// a column, it's the width of that column. -abstract class TableSpanExtent { - /// Creates a [TableSpanExtent]. - const TableSpanExtent(); - - /// Calculates the actual extent of the span in pixels. - /// - /// To assist with the calculation, table metrics obtained from the provided - /// [TableSpanExtentDelegate] may be used. - double calculateExtent(TableSpanExtentDelegate delegate); -} +typedef TableSpanExtent = SpanExtent; /// A span extent with a fixed [pixels] value. -class FixedTableSpanExtent extends TableSpanExtent { - /// Creates a [FixedTableSpanExtent]. - /// - /// The provided [pixels] value must be equal to or greater then zero. - const FixedTableSpanExtent(this.pixels) : assert(pixels >= 0.0); - - /// The extent of the span in pixels. - final double pixels; - - @override - double calculateExtent(TableSpanExtentDelegate delegate) => pixels; -} +typedef FixedTableSpanExtent = FixedSpanExtent; /// Specified the span extent as a fraction of the viewport extent. /// /// For example, a column with a 1.0 as [fraction] will be as wide as the /// viewport. -class FractionalTableSpanExtent extends TableSpanExtent { - /// Creates a [FractionalTableSpanExtent]. - /// - /// The provided [fraction] value must be equal to or greater than zero. - const FractionalTableSpanExtent( - this.fraction, - ) : assert(fraction >= 0.0); - - /// The fraction of the [TableSpanExtentDelegate.viewportExtent] that the - /// span should occupy. - /// - /// The provided [fraction] value must be equal to or greater than zero. - final double fraction; - - @override - double calculateExtent(TableSpanExtentDelegate delegate) => - delegate.viewportExtent * fraction; -} +typedef FractionalTableSpanExtent = FractionalSpanExtent; /// Specifies that the span should occupy the remaining space in the viewport. /// @@ -237,259 +58,35 @@ class FractionalTableSpanExtent extends TableSpanExtent { /// ```dart /// const MaxTableSpanExtent(FixedTableSpanExtent(200.0), RemainingTableSpanExtent()); /// ``` -class RemainingTableSpanExtent extends TableSpanExtent { - /// Creates a [RemainingTableSpanExtent]. - const RemainingTableSpanExtent(); - - @override - double calculateExtent(TableSpanExtentDelegate delegate) { - return math.max(0.0, delegate.viewportExtent - delegate.precedingExtent); - } -} +typedef RemainingTableSpanExtent = RemainingSpanExtent; /// Signature for a function that combines the result of two /// [TableSpanExtent.calculateExtent] invocations. /// /// Used by [CombiningTableSpanExtent]; -typedef TableSpanExtentCombiner = double Function(double, double); +typedef TableSpanExtentCombiner = SpanExtentCombiner; /// Runs the result of two [TableSpanExtent]s through a `combiner` function /// to determine the ultimate pixel extent of a span. -class CombiningTableSpanExtent extends TableSpanExtent { - /// Creates a [CombiningTableSpanExtent]; - const CombiningTableSpanExtent(this._extent1, this._extent2, this._combiner); - - final TableSpanExtent _extent1; - final TableSpanExtent _extent2; - final TableSpanExtentCombiner _combiner; - - @override - double calculateExtent(TableSpanExtentDelegate delegate) { - return _combiner( - _extent1.calculateExtent(delegate), - _extent2.calculateExtent(delegate), - ); - } -} +typedef CombiningTableSpanExtent = CombiningSpanExtent; /// Returns the larger pixel extent of the two provided [TableSpanExtent]. -class MaxTableSpanExtent extends CombiningTableSpanExtent { - /// Creates a [MaxTableSpanExtent]. - const MaxTableSpanExtent( - TableSpanExtent extent1, - TableSpanExtent extent2, - ) : super(extent1, extent2, math.max); -} +typedef MaxTableSpanExtent = MaxSpanExtent; /// Returns the smaller pixel extent of the two provided [TableSpanExtent]. -class MinTableSpanExtent extends CombiningTableSpanExtent { - /// Creates a [MinTableSpanExtent]. - const MinTableSpanExtent( - TableSpanExtent extent1, - TableSpanExtent extent2, - ) : super(extent1, extent2, math.min); -} +typedef MinTableSpanExtent = MinSpanExtent; /// A decoration for a [TableSpan]. /// /// When decorating merged cells in the [TableView], a merged cell will take its /// decoration from the leading cell of the merged span. -class TableSpanDecoration { - /// Creates a [TableSpanDecoration]. - const TableSpanDecoration({ - this.border, - this.color, - this.borderRadius, - this.consumeSpanPadding = true, - }); - - /// The border drawn around the span. - final TableSpanBorder? border; - - /// The radius by which the leading and trailing ends of a row or - /// column will be rounded. - /// - /// Applies to the [border] and [color] of the given [TableSpan]. - final BorderRadius? borderRadius; - - /// 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 - /// orientation of the span that are currently visible inside the viewport of - /// the table. The extent of the actual span may be larger. - /// - /// If a span contains pinned parts, [paint] is invoked separately for the - /// pinned and unpinned parts. For example: If a row contains a pinned column, - /// paint is called with the [TableSpanDecorationPaintDetails.rect] for the - /// cell representing the pinned column and separately with another - /// [TableSpanDecorationPaintDetails.rect] containing all the other unpinned - /// cells. - void paint(TableSpanDecorationPaintDetails details) { - if (color != null) { - final Paint paint = Paint() - ..color = color! - ..isAntiAlias = borderRadius != null; - if (borderRadius == null || borderRadius == BorderRadius.zero) { - details.canvas.drawRect(details.rect, paint); - } else { - details.canvas.drawRRect( - borderRadius!.toRRect(details.rect), - paint, - ); - } - } - if (border != null) { - border!.paint(details, borderRadius); - } - } -} +typedef TableSpanDecoration = SpanDecoration; /// Describes the border for a [TableSpan]. -class TableSpanBorder { - /// Creates a [TableSpanBorder]. - const TableSpanBorder({ - this.trailing = BorderSide.none, - this.leading = BorderSide.none, - }); - - /// The border to draw on the trailing side of the span, based on the - /// [AxisDirection]. - /// - /// The trailing side of a row is the bottom when [Axis.vertical] is - /// [AxisDirection.down], the trailing side of a column - /// is its right side when the [Axis.horizontal] is [AxisDirection.right]. - final BorderSide trailing; - - /// The border to draw on the leading side of the span. - /// - /// The leading side of a row is the top when [Axis.vertical] is - /// [AxisDirection.down], the leading side of a column - /// is its left side when the [Axis.horizontal] is [AxisDirection.right]. - final BorderSide leading; - - /// Called to draw the border around a span. - /// - /// If the span represents a row, `axisDirection` will be [AxisDirection.left] - /// or [AxisDirection.right]. For columns, the `axisDirection` will be - /// [AxisDirection.down] or [AxisDirection.up]. - /// - /// The provided [TableSpanDecorationPaintDetails] describes the bounds and - /// orientation of the span that are currently visible inside the viewport of - /// the table. The extent of the actual span may be larger. - /// - /// If a span contains pinned parts, [paint] is invoked separately for the - /// pinned and unpinned parts. For example: If a row contains a pinned column, - /// paint is called with the [TableSpanDecorationPaintDetails.rect] for the - /// cell representing the pinned column and separately with another - /// [TableSpanDecorationPaintDetails.rect] containing all the other unpinned - /// cells. - void paint( - TableSpanDecorationPaintDetails details, - BorderRadius? borderRadius, - ) { - final AxisDirection axisDirection = details.axisDirection; - switch (axisDirectionToAxis(axisDirection)) { - case Axis.horizontal: - final Border border = Border( - top: axisDirection == AxisDirection.right ? leading : trailing, - bottom: axisDirection == AxisDirection.right ? trailing : leading, - ); - border.paint( - details.canvas, - details.rect, - borderRadius: borderRadius, - ); - case Axis.vertical: - final Border border = Border( - left: axisDirection == AxisDirection.down ? leading : trailing, - right: axisDirection == AxisDirection.down ? trailing : leading, - ); - border.paint( - details.canvas, - details.rect, - borderRadius: borderRadius, - ); - } - } -} +typedef TableSpanBorder = SpanBorder; /// Provides the details of a given [TableSpanDecoration] for painting. /// /// Created during paint by the [RenderTableViewport] for the /// [TableSpan.foregroundDecoration] and [TableSpan.backgroundDecoration]. -class TableSpanDecorationPaintDetails { - /// Creates the details needed to paint a [TableSpanDecoration]. - /// - /// The [canvas], [rect], and [axisDirection] must be provided. - TableSpanDecorationPaintDetails({ - required this.canvas, - required this.rect, - required this.axisDirection, - }); - - /// The [Canvas] that the [TableSpanDecoration] will be painted to. - final Canvas canvas; - - /// A [Rect] representing the visible area of a row or column in the - /// [TableView], as represented by a [TableSpan]. - /// - /// This Rect contains all of the visible children in a given row or column, - /// which is the area the [TableSpanDecoration] will be applied to. - final Rect rect; - - /// The [AxisDirection] of the [Axis] of the [TableSpan]. - /// - /// When [AxisDirection.down] or [AxisDirection.up], which would be - /// [Axis.vertical], a column is being painted. When [AxisDirection.left] or - /// [AxisDirection.right], which would be [Axis.horizontal], a row is being - /// painted. - final AxisDirection axisDirection; -} +typedef TableSpanDecorationPaintDetails = SpanDecorationPaintDetails; diff --git a/packages/two_dimensional_scrollables/lib/two_dimensional_scrollables.dart b/packages/two_dimensional_scrollables/lib/two_dimensional_scrollables.dart index 3107e6e1c0b9..f19cdb4e343f 100644 --- a/packages/two_dimensional_scrollables/lib/two_dimensional_scrollables.dart +++ b/packages/two_dimensional_scrollables/lib/two_dimensional_scrollables.dart @@ -5,7 +5,9 @@ /// The [TableView] and associated widgets. /// /// To use, import `package:two_dimensional_scrollables/two_dimensional_scrollables.dart`. -library table_view; +library two_dimensional_scrollables; + +export 'src/common/span.dart'; export 'src/table_view/table.dart'; export 'src/table_view/table_cell.dart'; diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index fb9dc192f702..1376d5fc29b2 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.2.0 +version: 0.2.1 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/common/span_test.dart b/packages/two_dimensional_scrollables/test/common/span_test.dart new file mode 100644 index 000000000000..147ad2fd9580 --- /dev/null +++ b/packages/two_dimensional_scrollables/test/common/span_test.dart @@ -0,0 +1,202 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; + +void main() { + group('SpanExtent', () { + test('FixedSpanExtent', () { + FixedSpanExtent extent = const FixedSpanExtent(150); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 0, viewportExtent: 0), + ), + 150, + ); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 100, viewportExtent: 1000), + ), + 150, + ); + // asserts value is valid + expect( + () { + extent = FixedSpanExtent(-100); + }, + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'description', + contains('pixels >= 0.0'), + ), + ), + ); + }); + + test('FractionalSpanExtent', () { + FractionalSpanExtent extent = const FractionalSpanExtent(0.5); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 0, viewportExtent: 0), + ), + 0.0, + ); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 100, viewportExtent: 1000), + ), + 500, + ); + // asserts value is valid + expect( + () { + extent = FractionalSpanExtent(-20); + }, + throwsA( + isA().having( + (AssertionError error) => error.toString(), + 'description', + contains('fraction >= 0.0'), + ), + ), + ); + }); + + test('RemainingSpanExtent', () { + const RemainingSpanExtent extent = RemainingSpanExtent(); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 0, viewportExtent: 0), + ), + 0.0, + ); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 100, viewportExtent: 1000), + ), + 900, + ); + }); + + test('CombiningSpanExtent', () { + final CombiningSpanExtent extent = CombiningSpanExtent( + const FixedSpanExtent(100), + const RemainingSpanExtent(), + (double a, double b) { + return a + b; + }, + ); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 0, viewportExtent: 0), + ), + 100, + ); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 100, viewportExtent: 1000), + ), + 1000, + ); + }); + + test('MaxSpanExtent', () { + const MaxSpanExtent extent = MaxSpanExtent( + FixedSpanExtent(100), + RemainingSpanExtent(), + ); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 0, viewportExtent: 0), + ), + 100, + ); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 100, viewportExtent: 1000), + ), + 900, + ); + }); + + test('MinSpanExtent', () { + const MinSpanExtent extent = MinSpanExtent( + FixedSpanExtent(100), + RemainingSpanExtent(), + ); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 0, viewportExtent: 0), + ), + 0, + ); + expect( + extent.calculateExtent( + const SpanExtentDelegate(precedingExtent: 100, viewportExtent: 1000), + ), + 100, + ); + }); + }); + + test('SpanDecoration', () { + SpanDecoration decoration = const SpanDecoration( + color: Color(0xffff0000), + ); + final TestCanvas canvas = TestCanvas(); + const Rect rect = Rect.fromLTWH(0, 0, 10, 10); + final SpanDecorationPaintDetails details = SpanDecorationPaintDetails( + canvas: canvas, + rect: rect, + axisDirection: AxisDirection.down, + ); + final BorderRadius radius = BorderRadius.circular(10.0); + decoration.paint(details); + expect(canvas.rect, rect); + expect(canvas.paint.color, const Color(0xffff0000)); + expect(canvas.paint.isAntiAlias, isFalse); + final TestSpanBorder border = TestSpanBorder( + leading: const BorderSide(), + ); + decoration = SpanDecoration( + border: border, + borderRadius: radius, + ); + decoration.paint(details); + expect(border.details, details); + expect(border.radius, radius); + }); +} + +class TestCanvas implements Canvas { + final List noSuchMethodInvocations = []; + late Rect rect; + late Paint paint; + + @override + void drawRect(Rect rect, Paint paint) { + this.rect = rect; + this.paint = paint; + } + + @override + void noSuchMethod(Invocation invocation) { + noSuchMethodInvocations.add(invocation); + } +} + +class TestSpanBorder extends SpanBorder { + TestSpanBorder({super.leading}); + TableSpanDecorationPaintDetails? details; + BorderRadius? radius; + @override + void paint(TableSpanDecorationPaintDetails details, BorderRadius? radius) { + this.details = details; + this.radius = radius; + } +}