Unverified Commit a90c33fd authored by Luccas Clezar's avatar Luccas Clezar Committed by GitHub

iOS TextSelectionToolbar fidelity (#127757)

CupertinoTextSelectionToolbar is different from the native one, with some UI and UX issues. More details on the linked issue.

https://github.com/flutter/flutter/issues/127756

Currently the only problem that I listed on the linked issue that I couldn't fix was the horizontal scrolling, but to workaround this I added a GestureDetector to change pages when swiping the toolbar. It's not exactly the same as native as there is no scroll animation, but it works.

I'm creating this PR a little early to have some feedback as these changes were more complex than the ones in my last PR. Probably best if @justinmc is involved 😅

|Version|Video|
|-|-|
|Flutter Old|<video src="https://github.com/flutter/flutter/assets/12024080/7cf81075-46ec-4970-b118-cc27b60ddac0"></video>|
|Flutter New|<video src="https://github.com/flutter/flutter/assets/12024080/c9e27a53-f94c-4cb0-9b76-e47b73841dcb"></video>|
|Native|<video src="https://github.com/flutter/flutter/assets/12024080/468c7d5b-ba93-4bd4-8f6e-8ec2644b9866"></video>|
parent 3e66c86a
...@@ -15,7 +15,7 @@ import 'theme.dart'; ...@@ -15,7 +15,7 @@ import 'theme.dart';
// Values extracted from https://developer.apple.com/design/resources/. // Values extracted from https://developer.apple.com/design/resources/.
// The height of the toolbar, including the arrow. // The height of the toolbar, including the arrow.
const double _kToolbarHeight = 43.0; const double _kToolbarHeight = 45.0;
// Vertical distance between the tip of the arrow and the line of text the arrow // Vertical distance between the tip of the arrow and the line of text the arrow
// is pointing to. The value used here is eyeballed. // is pointing to. The value used here is eyeballed.
const double _kToolbarContentDistance = 8.0; const double _kToolbarContentDistance = 8.0;
...@@ -28,15 +28,29 @@ const double _kArrowScreenPadding = 26.0; ...@@ -28,15 +28,29 @@ const double _kArrowScreenPadding = 26.0;
// Values extracted from https://developer.apple.com/design/resources/. // Values extracted from https://developer.apple.com/design/resources/.
const Radius _kToolbarBorderRadius = Radius.circular(8); const Radius _kToolbarBorderRadius = Radius.circular(8);
// Color was measured from a screenshot of iOS 16.0.2
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507.
const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFFF6F6F6),
darkColor: Color(0xFF222222),
);
const double _kToolbarChevronSize = 10;
const double _kToolbarChevronThickness = 2;
// Color was measured from a screenshot of iOS 16.0.2.
const CupertinoDynamicColor _kToolbarDividerColor = CupertinoDynamicColor.withBrightness( const CupertinoDynamicColor _kToolbarDividerColor = CupertinoDynamicColor.withBrightness(
// This value was extracted from a screenshot of iOS 16.0.3, as light mode color: Color(0xFFD6D6D6),
// didn't appear in the Apple design resources assets linked below. darkColor: Color(0xFF424242),
color: Color(0xFFB6B6B6), );
// Color extracted from https://developer.apple.com/design/resources/.
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507. const CupertinoDynamicColor _kToolbarTextColor = CupertinoDynamicColor.withBrightness(
darkColor: Color(0xFF808080), color: CupertinoColors.black,
darkColor: CupertinoColors.white,
); );
const Duration _kToolbarTransitionDuration = Duration(milliseconds: 125);
/// The type for a Function that builds a toolbar's container with the given /// The type for a Function that builds a toolbar's container with the given
/// child. /// child.
/// ///
...@@ -54,13 +68,6 @@ typedef CupertinoToolbarBuilder = Widget Function( ...@@ -54,13 +68,6 @@ typedef CupertinoToolbarBuilder = Widget Function(
Widget child, Widget child,
); );
class _CupertinoToolbarButtonDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(width: 1.0 / MediaQuery.devicePixelRatioOf(context));
}
}
/// An iOS-style text selection toolbar. /// An iOS-style text selection toolbar.
/// ///
/// Typically displays buttons for text manipulation, e.g. copying and pasting /// Typically displays buttons for text manipulation, e.g. copying and pasting
...@@ -117,29 +124,14 @@ class CupertinoTextSelectionToolbar extends StatelessWidget { ...@@ -117,29 +124,14 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
/// * [TextSelectionToolbar], which uses this same value as well. /// * [TextSelectionToolbar], which uses this same value as well.
static const double kToolbarScreenPadding = 8.0; static const double kToolbarScreenPadding = 8.0;
// Add the visual vertical line spacer between children buttons.
static List<Widget> _addChildrenSpacers(List<Widget> children) {
final List<Widget> nextChildren = <Widget>[];
for (int i = 0; i < children.length; i++) {
final Widget child = children[i];
if (i != 0) {
nextChildren.add(_CupertinoToolbarButtonDivider());
}
nextChildren.add(child);
}
return nextChildren;
}
// Builds a toolbar just like the default iOS toolbar, with the right color // Builds a toolbar just like the default iOS toolbar, with the right color
// background and a rounded cutout with an arrow. // background and a rounded cutout with an arrow.
static Widget _defaultToolbarBuilder(BuildContext context, Offset anchor, bool isAbove, Widget child) { static Widget _defaultToolbarBuilder(BuildContext context, Offset anchor, bool isAbove, Widget child) {
final Widget outputChild = _CupertinoTextSelectionToolbarShape( final Widget outputChild = _CupertinoTextSelectionToolbarShape(
anchor: anchor, anchor: anchor,
isAbove: isAbove, isAbove: isAbove,
child: DecoratedBox( child: ColoredBox(
decoration: BoxDecoration( color: _kToolbarBackgroundColor.resolveFrom(context),
color: _kToolbarDividerColor.resolveFrom(context),
),
child: child, child: child,
), ),
); );
...@@ -209,7 +201,7 @@ class CupertinoTextSelectionToolbar extends StatelessWidget { ...@@ -209,7 +201,7 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
anchor: fitsAbove ? anchorAboveAdjusted : anchorBelowAdjusted, anchor: fitsAbove ? anchorAboveAdjusted : anchorBelowAdjusted,
isAbove: fitsAbove, isAbove: fitsAbove,
toolbarBuilder: toolbarBuilder, toolbarBuilder: toolbarBuilder,
children: _addChildrenSpacers(children), children: children,
), ),
), ),
); );
...@@ -449,20 +441,44 @@ class _CupertinoTextSelectionToolbarContent extends StatefulWidget { ...@@ -449,20 +441,44 @@ class _CupertinoTextSelectionToolbarContent extends StatefulWidget {
class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSelectionToolbarContent> with TickerProviderStateMixin { class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSelectionToolbarContent> with TickerProviderStateMixin {
// Controls the fading of the buttons within the menu during page transitions. // Controls the fading of the buttons within the menu during page transitions.
late AnimationController _controller; late AnimationController _controller;
int _page = 0;
int? _nextPage; int? _nextPage;
int _page = 0;
final GlobalKey _toolbarItemsKey = GlobalKey();
void _onHorizontalDragEnd(DragEndDetails details) {
final double? velocity = details.primaryVelocity;
if (velocity != null && velocity != 0) {
if (velocity > 0) {
_handlePreviousPage();
} else {
_handleNextPage();
}
}
}
void _handleNextPage() { void _handleNextPage() {
final RenderBox? renderToolbar =
_toolbarItemsKey.currentContext?.findRenderObject() as RenderBox?;
if (renderToolbar is _RenderCupertinoTextSelectionToolbarItems && renderToolbar.hasNextPage) {
_controller.reverse(); _controller.reverse();
_controller.addStatusListener(_statusListener); _controller.addStatusListener(_statusListener);
_nextPage = _page + 1; _nextPage = _page + 1;
} }
}
void _handlePreviousPage() { void _handlePreviousPage() {
final RenderBox? renderToolbar =
_toolbarItemsKey.currentContext?.findRenderObject() as RenderBox?;
if (renderToolbar is _RenderCupertinoTextSelectionToolbarItems && renderToolbar.hasPreviousPage) {
_controller.reverse(); _controller.reverse();
_controller.addStatusListener(_statusListener); _controller.addStatusListener(_statusListener);
_nextPage = _page - 1; _nextPage = _page - 1;
} }
}
void _statusListener(AnimationStatus status) { void _statusListener(AnimationStatus status) {
if (status != AnimationStatus.dismissed) { if (status != AnimationStatus.dismissed) {
...@@ -484,7 +500,7 @@ class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSel ...@@ -484,7 +500,7 @@ class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSel
value: 1.0, value: 1.0,
vsync: this, vsync: this,
// This was eyeballed on a physical iOS device running iOS 13. // This was eyeballed on a physical iOS device running iOS 13.
duration: const Duration(milliseconds: 150), duration: _kToolbarTransitionDuration,
); );
} }
...@@ -506,53 +522,137 @@ class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSel ...@@ -506,53 +522,137 @@ class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSel
super.dispose(); super.dispose();
} }
Widget _createChevron({required bool isLeft}) {
final Color color = _kToolbarTextColor.resolveFrom(context);
return IgnorePointer(
child: Center(
// If widthFactor is not set to 0, the button is given unbounded width.
widthFactor: 0,
child: CustomPaint(
painter: isLeft
? _LeftCupertinoChevronPainter(color: color)
: _RightCupertinoChevronPainter(color: color),
size: const Size.square(_kToolbarChevronSize),
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.toolbarBuilder(context, widget.anchor, widget.isAbove, FadeTransition( return widget.toolbarBuilder(context, widget.anchor, widget.isAbove, FadeTransition(
opacity: _controller, opacity: _controller,
child: AnimatedSize(
duration: _kToolbarTransitionDuration,
curve: Curves.decelerate,
child: GestureDetector(
onHorizontalDragEnd: _onHorizontalDragEnd,
child: _CupertinoTextSelectionToolbarItems( child: _CupertinoTextSelectionToolbarItems(
key: _toolbarItemsKey,
page: _page, page: _page,
backButton: CupertinoTextSelectionToolbarButton.text( backButton: CupertinoTextSelectionToolbarButton(
onPressed: _handlePreviousPage, onPressed: _handlePreviousPage,
text: '◀', child: _createChevron(isLeft: true),
), ),
dividerColor: _kToolbarDividerColor.resolveFrom(context),
dividerWidth: 1.0 / MediaQuery.devicePixelRatioOf(context), dividerWidth: 1.0 / MediaQuery.devicePixelRatioOf(context),
nextButton: CupertinoTextSelectionToolbarButton.text( nextButton: CupertinoTextSelectionToolbarButton(
onPressed: _handleNextPage, onPressed: _handleNextPage,
text: '▶', child: _createChevron(isLeft: false),
),
nextButtonDisabled: const CupertinoTextSelectionToolbarButton.text(
text: '▶',
), ),
children: widget.children, children: widget.children,
), ),
),
),
)); ));
} }
} }
// These classes help to test the chevrons. As _CupertinoChevronPainter must be
// private, it's possible to check the runtimeType of each chevron to know if
// they should be pointing left or right.
class _LeftCupertinoChevronPainter extends _CupertinoChevronPainter {
_LeftCupertinoChevronPainter({required super.color}) : super(isLeft: true);
}
class _RightCupertinoChevronPainter extends _CupertinoChevronPainter {
_RightCupertinoChevronPainter({required super.color}) : super(isLeft: false);
}
abstract class _CupertinoChevronPainter extends CustomPainter {
_CupertinoChevronPainter({
required this.color,
required this.isLeft,
});
final Color color;
/// If this is true the chevron will point left, else it will point right.
final bool isLeft;
@override
void paint(Canvas canvas, Size size) {
assert(size.height == size.width, 'size must have the same height and width');
final double iconSize = size.height;
// The chevron is half of a square rotated 45˚, so it needs a margin of 1/4
// its size on each side to be centered horizontally.
//
// If pointing left, it means the left half of a square is being used and
// the offset is positive. If pointing right, the right half is being used
// and the offset is negative.
final Offset centerOffset = Offset(
iconSize / 4 * (isLeft ? 1 : -1),
0,
);
final Offset firstPoint = Offset(iconSize / 2, 0) + centerOffset;
final Offset middlePoint = Offset(isLeft ? 0 : iconSize, iconSize / 2) + centerOffset;
final Offset lowerPoint = Offset(iconSize / 2, iconSize) + centerOffset;
final Paint paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = _kToolbarChevronThickness
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
// `drawLine` is used here because it's testable. When using `drawPath`,
// there's no way to test that the chevron points to the correct side.
canvas.drawLine(firstPoint, middlePoint, paint);
canvas.drawLine(middlePoint, lowerPoint, paint);
}
@override
bool shouldRepaint(_CupertinoChevronPainter oldDelegate) =>
oldDelegate.color != color || oldDelegate.isLeft != isLeft;
}
// The custom RenderObjectWidget that, together with // The custom RenderObjectWidget that, together with
// _RenderCupertinoTextSelectionToolbarItems and // _RenderCupertinoTextSelectionToolbarItems and
// _CupertinoTextSelectionToolbarItemsElement, paginates the menu items. // _CupertinoTextSelectionToolbarItemsElement, paginates the menu items.
class _CupertinoTextSelectionToolbarItems extends RenderObjectWidget { class _CupertinoTextSelectionToolbarItems extends RenderObjectWidget {
_CupertinoTextSelectionToolbarItems({ _CupertinoTextSelectionToolbarItems({
super.key,
required this.page, required this.page,
required this.children, required this.children,
required this.backButton, required this.backButton,
required this.dividerColor,
required this.dividerWidth, required this.dividerWidth,
required this.nextButton, required this.nextButton,
required this.nextButtonDisabled,
}) : assert(children.isNotEmpty); }) : assert(children.isNotEmpty);
final Widget backButton; final Widget backButton;
final List<Widget> children; final List<Widget> children;
final Color dividerColor;
final double dividerWidth; final double dividerWidth;
final Widget nextButton; final Widget nextButton;
final Widget nextButtonDisabled;
final int page; final int page;
@override @override
_RenderCupertinoTextSelectionToolbarItems createRenderObject(BuildContext context) { _RenderCupertinoTextSelectionToolbarItems createRenderObject(BuildContext context) {
return _RenderCupertinoTextSelectionToolbarItems( return _RenderCupertinoTextSelectionToolbarItems(
dividerColor: dividerColor,
dividerWidth: dividerWidth, dividerWidth: dividerWidth,
page: page, page: page,
); );
...@@ -562,6 +662,7 @@ class _CupertinoTextSelectionToolbarItems extends RenderObjectWidget { ...@@ -562,6 +662,7 @@ class _CupertinoTextSelectionToolbarItems extends RenderObjectWidget {
void updateRenderObject(BuildContext context, _RenderCupertinoTextSelectionToolbarItems renderObject) { void updateRenderObject(BuildContext context, _RenderCupertinoTextSelectionToolbarItems renderObject) {
renderObject renderObject
..page = page ..page = page
..dividerColor = dividerColor
..dividerWidth = dividerWidth; ..dividerWidth = dividerWidth;
} }
...@@ -591,8 +692,6 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement { ...@@ -591,8 +692,6 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement {
renderObject.backButton = child; renderObject.backButton = child;
case _CupertinoTextSelectionToolbarItemsSlot.nextButton: case _CupertinoTextSelectionToolbarItemsSlot.nextButton:
renderObject.nextButton = child; renderObject.nextButton = child;
case _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled:
renderObject.nextButtonDisabled = child;
} }
} }
...@@ -683,7 +782,6 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement { ...@@ -683,7 +782,6 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement {
final _CupertinoTextSelectionToolbarItems toolbarItems = widget as _CupertinoTextSelectionToolbarItems; final _CupertinoTextSelectionToolbarItems toolbarItems = widget as _CupertinoTextSelectionToolbarItems;
_mountChild(toolbarItems.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton); _mountChild(toolbarItems.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton);
_mountChild(toolbarItems.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton); _mountChild(toolbarItems.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
_mountChild(toolbarItems.nextButtonDisabled, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
// Mount list children. // Mount list children.
_children = List<Element>.filled(toolbarItems.children.length, _NullElement.instance); _children = List<Element>.filled(toolbarItems.children.length, _NullElement.instance);
...@@ -718,7 +816,6 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement { ...@@ -718,7 +816,6 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement {
final _CupertinoTextSelectionToolbarItems toolbarItems = widget as _CupertinoTextSelectionToolbarItems; final _CupertinoTextSelectionToolbarItems toolbarItems = widget as _CupertinoTextSelectionToolbarItems;
_mountChild(toolbarItems.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton); _mountChild(toolbarItems.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton);
_mountChild(toolbarItems.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton); _mountChild(toolbarItems.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
_mountChild(toolbarItems.nextButtonDisabled, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
// Update list children. // Update list children.
_children = updateChildren(_children, toolbarItems.children, forgottenChildren: _forgottenChildren); _children = updateChildren(_children, toolbarItems.children, forgottenChildren: _forgottenChildren);
...@@ -729,14 +826,19 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement { ...@@ -729,14 +826,19 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement {
// The custom RenderBox that helps paginate the menu items. // The custom RenderBox that helps paginate the menu items.
class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with ContainerRenderObjectMixin<RenderBox, ToolbarItemsParentData>, RenderBoxContainerDefaultsMixin<RenderBox, ToolbarItemsParentData> { class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with ContainerRenderObjectMixin<RenderBox, ToolbarItemsParentData>, RenderBoxContainerDefaultsMixin<RenderBox, ToolbarItemsParentData> {
_RenderCupertinoTextSelectionToolbarItems({ _RenderCupertinoTextSelectionToolbarItems({
required Color dividerColor,
required double dividerWidth, required double dividerWidth,
required int page, required int page,
}) : _dividerWidth = dividerWidth, }) : _dividerColor = dividerColor,
_dividerWidth = dividerWidth,
_page = page, _page = page,
super(); super();
final Map<_CupertinoTextSelectionToolbarItemsSlot, RenderBox> slottedChildren = <_CupertinoTextSelectionToolbarItemsSlot, RenderBox>{}; final Map<_CupertinoTextSelectionToolbarItemsSlot, RenderBox> slottedChildren = <_CupertinoTextSelectionToolbarItemsSlot, RenderBox>{};
late bool hasNextPage;
late bool hasPreviousPage;
RenderBox? _updateChild(RenderBox? oldChild, RenderBox? newChild, _CupertinoTextSelectionToolbarItemsSlot slot) { RenderBox? _updateChild(RenderBox? oldChild, RenderBox? newChild, _CupertinoTextSelectionToolbarItemsSlot slot) {
if (oldChild != null) { if (oldChild != null) {
dropChild(oldChild); dropChild(oldChild);
...@@ -750,7 +852,7 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -750,7 +852,7 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
} }
bool _isSlottedChild(RenderBox child) { bool _isSlottedChild(RenderBox child) {
return child == _backButton || child == _nextButton || child == _nextButtonDisabled; return child == _backButton || child == _nextButton;
} }
int _page; int _page;
...@@ -763,6 +865,16 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -763,6 +865,16 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
markNeedsLayout(); markNeedsLayout();
} }
Color _dividerColor;
Color get dividerColor => _dividerColor;
set dividerColor(Color value) {
if (value == _dividerColor) {
return;
}
_dividerColor = value;
markNeedsLayout();
}
double _dividerWidth; double _dividerWidth;
double get dividerWidth => _dividerWidth; double get dividerWidth => _dividerWidth;
set dividerWidth(double value) { set dividerWidth(double value) {
...@@ -785,12 +897,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -785,12 +897,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
_nextButton = _updateChild(_nextButton, value, _CupertinoTextSelectionToolbarItemsSlot.nextButton); _nextButton = _updateChild(_nextButton, value, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
} }
RenderBox? _nextButtonDisabled;
RenderBox? get nextButtonDisabled => _nextButtonDisabled;
set nextButtonDisabled(RenderBox? value) {
_nextButtonDisabled = _updateChild(_nextButtonDisabled, value, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
}
@override @override
void performLayout() { void performLayout() {
if (firstChild == null) { if (firstChild == null) {
...@@ -801,7 +907,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -801,7 +907,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
// Layout slotted children. // Layout slotted children.
_backButton!.layout(constraints.loosen(), parentUsesSize: true); _backButton!.layout(constraints.loosen(), parentUsesSize: true);
_nextButton!.layout(constraints.loosen(), parentUsesSize: true); _nextButton!.layout(constraints.loosen(), parentUsesSize: true);
_nextButtonDisabled!.layout(constraints.loosen(), parentUsesSize: true);
final double subsequentPageButtonsWidth = final double subsequentPageButtonsWidth =
_backButton!.size.width + _nextButton!.size.width; _backButton!.size.width + _nextButton!.size.width;
...@@ -828,7 +933,7 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -828,7 +933,7 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
// If this is the last child, it's ok to fit without a forward button. // If this is the last child, it's ok to fit without a forward button.
// Note childCount doesn't include slotted children which come before the list ones. // Note childCount doesn't include slotted children which come before the list ones.
paginationButtonsWidth = paginationButtonsWidth =
i == childCount + 2 ? 0.0 : _nextButton!.size.width; i == childCount + 1 ? 0.0 : _nextButton!.size.width;
} else { } else {
paginationButtonsWidth = subsequentPageButtonsWidth; paginationButtonsWidth = subsequentPageButtonsWidth;
} }
...@@ -881,17 +986,10 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -881,17 +986,10 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
if (currentPage > 0) { if (currentPage > 0) {
final ToolbarItemsParentData nextButtonParentData = final ToolbarItemsParentData nextButtonParentData =
_nextButton!.parentData! as ToolbarItemsParentData; _nextButton!.parentData! as ToolbarItemsParentData;
final ToolbarItemsParentData nextButtonDisabledParentData =
_nextButtonDisabled!.parentData! as ToolbarItemsParentData;
final ToolbarItemsParentData backButtonParentData = final ToolbarItemsParentData backButtonParentData =
_backButton!.parentData! as ToolbarItemsParentData; _backButton!.parentData! as ToolbarItemsParentData;
// The forward button always shows if there is more than one page, even on // The forward button only shows when there's a page after this one.
// the last page (it's just disabled). if (page != currentPage) {
if (page == currentPage) {
nextButtonDisabledParentData.offset = Offset(toolbarWidth, 0.0);
nextButtonDisabledParentData.shouldPaint = true;
toolbarWidth += nextButtonDisabled!.size.width;
} else {
nextButtonParentData.offset = Offset(toolbarWidth, 0.0); nextButtonParentData.offset = Offset(toolbarWidth, 0.0);
nextButtonParentData.shouldPaint = true; nextButtonParentData.shouldPaint = true;
toolbarWidth += nextButton!.size.width; toolbarWidth += nextButton!.size.width;
...@@ -903,6 +1001,11 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -903,6 +1001,11 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
// already been taken care of when laying out the children to // already been taken care of when laying out the children to
// accommodate the back button. // accommodate the back button.
} }
// Update previous/next page values so that we can check in the horizontal
// drag gesture callback if it's possible to navigate.
hasNextPage = page != currentPage;
hasPreviousPage = page > 0;
} else { } else {
// No divider for the next button when there's only one page. // No divider for the next button when there's only one page.
toolbarWidth -= dividerWidth; toolbarWidth -= dividerWidth;
...@@ -920,6 +1023,18 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -920,6 +1023,18 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
if (childParentData.shouldPaint) { if (childParentData.shouldPaint) {
final Offset childOffset = childParentData.offset + offset; final Offset childOffset = childParentData.offset + offset;
context.paintChild(child, childOffset); context.paintChild(child, childOffset);
// backButton is a slotted child and is not in the children list, so its
// childParentData.nextSibling is null. So either when there's a
// nextSibling or when child is the backButton, draw a divider to the
// child's right.
if (childParentData.nextSibling != null || child == backButton) {
context.canvas.drawLine(
Offset(child.size.width, 0) + childOffset,
Offset(child.size.width, child.size.height) + childOffset,
Paint()..color = dividerColor,
);
}
} }
}); });
} }
...@@ -977,9 +1092,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -977,9 +1092,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
if (hitTestChild(nextButton, result, position: position)) { if (hitTestChild(nextButton, result, position: position)) {
return true; return true;
} }
if (hitTestChild(nextButtonDisabled, result, position: position)) {
return true;
}
return false; return false;
} }
...@@ -1023,9 +1135,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -1023,9 +1135,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
if (_nextButton != null) { if (_nextButton != null) {
visitor(_nextButton!); visitor(_nextButton!);
} }
if (_nextButtonDisabled != null) {
visitor(_nextButtonDisabled!);
}
// Visit the list children. // Visit the list children.
super.visitChildren(visitor); super.visitChildren(visitor);
} }
...@@ -1051,8 +1160,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -1051,8 +1160,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
value.add(child.toDiagnosticsNode(name: 'back button')); value.add(child.toDiagnosticsNode(name: 'back button'));
} else if (child == nextButton) { } else if (child == nextButton) {
value.add(child.toDiagnosticsNode(name: 'next button')); value.add(child.toDiagnosticsNode(name: 'next button'));
} else if (child == nextButtonDisabled) {
value.add(child.toDiagnosticsNode(name: 'next button disabled'));
// List children. // List children.
} else { } else {
...@@ -1068,7 +1175,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container ...@@ -1068,7 +1175,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container
enum _CupertinoTextSelectionToolbarItemsSlot { enum _CupertinoTextSelectionToolbarItemsSlot {
backButton, backButton,
nextButton, nextButton,
nextButtonDisabled,
} }
class _NullElement extends Element { class _NullElement extends Element {
......
...@@ -11,30 +11,26 @@ import 'localizations.dart'; ...@@ -11,30 +11,26 @@ import 'localizations.dart';
const TextStyle _kToolbarButtonFontStyle = TextStyle( const TextStyle _kToolbarButtonFontStyle = TextStyle(
inherit: false, inherit: false,
fontSize: 14.0, fontSize: 15.0,
letterSpacing: -0.15, letterSpacing: -0.15,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
); );
// Colors extracted from https://developer.apple.com/design/resources/.
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507.
const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.withBrightness(
// This value was extracted from a screenshot of iOS 16.0.3, as light mode
// didn't appear in the Apple design resources assets linked above.
color: Color(0xEBF7F7F7),
darkColor: Color(0xEB202020),
);
const CupertinoDynamicColor _kToolbarTextColor = CupertinoDynamicColor.withBrightness( const CupertinoDynamicColor _kToolbarTextColor = CupertinoDynamicColor.withBrightness(
color: CupertinoColors.black, color: CupertinoColors.black,
darkColor: CupertinoColors.white, darkColor: CupertinoColors.white,
); );
// Eyeballed value. const CupertinoDynamicColor _kToolbarPressedColor = CupertinoDynamicColor.withBrightness(
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 16.0, horizontal: 18.0); color: Color(0x10000000),
darkColor: Color(0x10FFFFFF),
);
// Value measured from screenshot of iOS 16.0.2
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 18.0, horizontal: 16.0);
/// A button in the style of the iOS text selection toolbar buttons. /// A button in the style of the iOS text selection toolbar buttons.
class CupertinoTextSelectionToolbarButton extends StatelessWidget { class CupertinoTextSelectionToolbarButton extends StatefulWidget {
/// Create an instance of [CupertinoTextSelectionToolbarButton]. /// Create an instance of [CupertinoTextSelectionToolbarButton].
/// ///
/// [child] cannot be null. /// [child] cannot be null.
...@@ -113,26 +109,65 @@ class CupertinoTextSelectionToolbarButton extends StatelessWidget { ...@@ -113,26 +109,65 @@ class CupertinoTextSelectionToolbarButton extends StatelessWidget {
} }
} }
@override
State<StatefulWidget> createState() => _CupertinoTextSelectionToolbarButtonState();
}
class _CupertinoTextSelectionToolbarButtonState extends State<CupertinoTextSelectionToolbarButton> {
bool isPressed = false;
void _onTapDown(TapDownDetails details) {
setState(() => isPressed = true);
}
void _onTapUp(TapUpDetails details) {
setState(() => isPressed = false);
widget.onPressed?.call();
}
void _onTapCancel() {
setState(() => isPressed = false);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget child = this.child ?? Text( final Widget child = CupertinoButton(
text ?? getButtonLabel(context, buttonItem!), color: isPressed
? _kToolbarPressedColor.resolveFrom(context)
: const Color(0x00000000),
borderRadius: null,
disabledColor: const Color(0x00000000),
// This CupertinoButton does not actually handle the onPressed callback,
// this is only here to correctly enable/disable the button (see
// GestureDetector comment below).
onPressed: widget.onPressed,
padding: _kToolbarButtonPadding,
// There's no foreground fade on iOS toolbar anymore, just the background
// is darkened.
pressedOpacity: 1.0,
child: widget.child ?? Text(
widget.text ?? CupertinoTextSelectionToolbarButton.getButtonLabel(context, widget.buttonItem!),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: _kToolbarButtonFontStyle.copyWith( style: _kToolbarButtonFontStyle.copyWith(
color: onPressed != null color: widget.onPressed != null
? _kToolbarTextColor.resolveFrom(context) ? _kToolbarTextColor.resolveFrom(context)
: CupertinoColors.inactiveGray, : CupertinoColors.inactiveGray,
), ),
),
); );
return CupertinoButton( if (widget.onPressed != null) {
borderRadius: null, // As it's needed to change the CupertinoButton's backgroundColor when
color: _kToolbarBackgroundColor, // pressed, not its opacity, this GestureDetector handles both the
disabledColor: _kToolbarBackgroundColor, // onPressed callback and the backgroundColor change.
onPressed: onPressed, return GestureDetector(
padding: _kToolbarButtonPadding, onTapDown: _onTapDown,
pressedOpacity: onPressed == null ? 1.0 : 0.7, onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: child, child: child,
); );
} else {
return child;
}
} }
} }
...@@ -1545,7 +1545,7 @@ void main() { ...@@ -1545,7 +1545,7 @@ void main() {
Text text = tester.widget<Text>(find.text('Paste')); Text text = tester.widget<Text>(find.text('Paste'));
expect(text.style!.color!.value, CupertinoColors.black.value); expect(text.style!.color!.value, CupertinoColors.black.value);
expect(text.style!.fontSize, 14); expect(text.style!.fontSize, 15);
expect(text.style!.letterSpacing, -0.15); expect(text.style!.letterSpacing, -0.15);
expect(text.style!.fontWeight, FontWeight.w400); expect(text.style!.fontWeight, FontWeight.w400);
...@@ -1577,7 +1577,7 @@ void main() { ...@@ -1577,7 +1577,7 @@ void main() {
text = tester.widget<Text>(find.text('Paste')); text = tester.widget<Text>(find.text('Paste'));
// The toolbar buttons' text are still the same style. // The toolbar buttons' text are still the same style.
expect(text.style!.color!.value, CupertinoColors.white.value); expect(text.style!.color!.value, CupertinoColors.white.value);
expect(text.style!.fontSize, 14); expect(text.style!.fontSize, 15);
expect(text.style!.letterSpacing, -0.15); expect(text.style!.letterSpacing, -0.15);
expect(text.style!.fontWeight, FontWeight.w400); expect(text.style!.fontWeight, FontWeight.w400);
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
...@@ -6537,7 +6537,7 @@ void main() { ...@@ -6537,7 +6537,7 @@ void main() {
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01), topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01),
leftMatcher: moreOrLessEquals(8), leftMatcher: moreOrLessEquals(8),
rightMatcher: lessThanOrEqualTo(400 - 8), rightMatcher: lessThanOrEqualTo(400 - 8),
bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 43, epsilon: 0.01), bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 45, epsilon: 0.01),
), ),
), ),
); );
...@@ -6597,7 +6597,7 @@ void main() { ...@@ -6597,7 +6597,7 @@ void main() {
pathMatcher: PathBoundsMatcher( pathMatcher: PathBoundsMatcher(
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01), topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01),
rightMatcher: moreOrLessEquals(400.0 - 8), rightMatcher: moreOrLessEquals(400.0 - 8),
bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 43, epsilon: 0.01), bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8 + 45, epsilon: 0.01),
leftMatcher: greaterThanOrEqualTo(8), leftMatcher: greaterThanOrEqualTo(8),
), ),
), ),
...@@ -6650,7 +6650,7 @@ void main() { ...@@ -6650,7 +6650,7 @@ void main() {
paints..clipPath( paints..clipPath(
pathMatcher: PathBoundsMatcher( pathMatcher: PathBoundsMatcher(
bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight, epsilon: 0.01), bottomMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight - 43, epsilon: 0.01), topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy - 8 - lineHeight - 45, epsilon: 0.01),
rightMatcher: lessThanOrEqualTo(400 - 8), rightMatcher: lessThanOrEqualTo(400 - 8),
leftMatcher: greaterThanOrEqualTo(8), leftMatcher: greaterThanOrEqualTo(8),
), ),
...@@ -6719,7 +6719,7 @@ void main() { ...@@ -6719,7 +6719,7 @@ void main() {
paints..clipPath( paints..clipPath(
pathMatcher: PathBoundsMatcher( pathMatcher: PathBoundsMatcher(
bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01), bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 43, epsilon: 0.01), topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 45, epsilon: 0.01),
rightMatcher: lessThanOrEqualTo(400 - 8), rightMatcher: lessThanOrEqualTo(400 - 8),
leftMatcher: greaterThanOrEqualTo(8), leftMatcher: greaterThanOrEqualTo(8),
), ),
...@@ -6792,7 +6792,7 @@ void main() { ...@@ -6792,7 +6792,7 @@ void main() {
paints..clipPath( paints..clipPath(
pathMatcher: PathBoundsMatcher( pathMatcher: PathBoundsMatcher(
bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01), bottomMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight, epsilon: 0.01),
topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 43, epsilon: 0.01), topMatcher: moreOrLessEquals(selectionPosition.dy - 8 - lineHeight - 45, epsilon: 0.01),
rightMatcher: lessThanOrEqualTo(400 - 8), rightMatcher: lessThanOrEqualTo(400 - 8),
leftMatcher: greaterThanOrEqualTo(8), leftMatcher: greaterThanOrEqualTo(8),
), ),
......
...@@ -60,18 +60,6 @@ void main() { ...@@ -60,18 +60,6 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final MockClipboard mockClipboard = MockClipboard(); final MockClipboard mockClipboard = MockClipboard();
// Returns true iff the button is visually enabled.
bool appearsEnabled(WidgetTester tester, String text) {
final CupertinoButton button = tester.widget<CupertinoButton>(
find.ancestor(
of: find.text(text),
matching: find.byType(CupertinoButton),
),
);
// Disabled buttons have no opacity change when pressed.
return button.pressedOpacity! < 1.0;
}
List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) { List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) {
return points.map<TextSelectionPoint>((TextSelectionPoint point) { return points.map<TextSelectionPoint>((TextSelectionPoint point) {
return TextSelectionPoint( return TextSelectionPoint(
...@@ -191,6 +179,15 @@ void main() { ...@@ -191,6 +179,15 @@ void main() {
}); });
group('Text selection menu overflow (iOS)', () { group('Text selection menu overflow (iOS)', () {
Finder findOverflowNextButton() => find.byWidgetPredicate((Widget widget) =>
widget is CustomPaint &&
'${widget.painter?.runtimeType}' == '_RightCupertinoChevronPainter',
);
Finder findOverflowBackButton() => find.byWidgetPredicate((Widget widget) =>
widget is CustomPaint &&
'${widget.painter?.runtimeType}' == '_LeftCupertinoChevronPainter',
);
testWidgets('All menu items show when they fit.', (WidgetTester tester) async { testWidgets('All menu items show when they fit.', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'abc def ghi'); final TextEditingController controller = TextEditingController(text: 'abc def ghi');
await tester.pumpWidget(CupertinoApp( await tester.pumpWidget(CupertinoApp(
...@@ -216,8 +213,8 @@ void main() { ...@@ -216,8 +213,8 @@ void main() {
expect(find.text('Copy'), findsNothing); expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsNothing); expect(findOverflowNextButton(), findsNothing);
// Long press on an empty space to show the selection menu. // Long press on an empty space to show the selection menu.
await tester.longPressAt(textOffsetToPosition(tester, 4)); await tester.longPressAt(textOffsetToPosition(tester, 4));
...@@ -226,8 +223,8 @@ void main() { ...@@ -226,8 +223,8 @@ void main() {
expect(find.text('Copy'), findsNothing); expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsOneWidget); expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select All'), findsOneWidget); expect(find.text('Select All'), findsOneWidget);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsNothing); expect(findOverflowNextButton(), findsNothing);
// Double tap to select a word and show the full selection menu. // Double tap to select a word and show the full selection menu.
final Offset textOffset = textOffsetToPosition(tester, 1); final Offset textOffset = textOffsetToPosition(tester, 1);
...@@ -241,8 +238,8 @@ void main() { ...@@ -241,8 +238,8 @@ void main() {
expect(find.text('Copy'), findsOneWidget); expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsOneWidget); expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsNothing); expect(findOverflowNextButton(), findsNothing);
}, },
skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
...@@ -273,8 +270,8 @@ void main() { ...@@ -273,8 +270,8 @@ void main() {
expect(find.text('Copy'), findsNothing); expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsNothing); expect(findOverflowNextButton(), findsNothing);
// Double tap to select a word and show the selection menu. // Double tap to select a word and show the selection menu.
final Offset textOffset = textOffsetToPosition(tester, 1); final Offset textOffset = textOffsetToPosition(tester, 1);
...@@ -288,32 +285,29 @@ void main() { ...@@ -288,32 +285,29 @@ void main() {
expect(find.text('Copy'), findsOneWidget); expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
expect(appearsEnabled(tester, '▶'), true);
// Tapping the next button shows the overflowing button. // Tapping the next button shows the overflowing button and the next
await tester.tap(find.text('▶')); // button is hidden as the last page is shown.
await tester.tapAt(tester.getCenter(findOverflowNextButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Cut'), findsNothing); expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing); expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsOneWidget); expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
expect(appearsEnabled(tester, '◀'), true); expect(findOverflowNextButton(), findsNothing);
expect(find.text('▶'), findsOneWidget);
expect(appearsEnabled(tester, '▶'), false);
// Tapping the back button shows the first page again. // Tapping the back button shows the first page again with the next button.
await tester.tap(find.text('◀')); await tester.tapAt(tester.getCenter(findOverflowBackButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Cut'), findsOneWidget); expect(find.text('Cut'), findsOneWidget);
expect(find.text('Copy'), findsOneWidget); expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
expect(appearsEnabled(tester, '▶'), true);
}, },
skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
...@@ -341,13 +335,13 @@ void main() { ...@@ -341,13 +335,13 @@ void main() {
)); ));
// Initially, the menu isn't shown at all. // Initially, the menu isn't shown at all.
expect(find.byType(CupertinoButton), findsNothing); expect(find.byType(CupertinoTextSelectionToolbarButton), findsNothing);
expect(find.text('Cut'), findsNothing); expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing); expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsNothing); expect(findOverflowNextButton(), findsNothing);
// Double tap to select a word and show the selection menu. // Double tap to select a word and show the selection menu.
final Offset textOffset = textOffsetToPosition(tester, 1); final Offset textOffset = textOffsetToPosition(tester, 1);
...@@ -357,65 +351,58 @@ void main() { ...@@ -357,65 +351,58 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Only the first button fits, and a next button is shown. // Only the first button fits, and a next button is shown.
expect(find.byType(CupertinoButton), findsNWidgets(2)); expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(2));
expect(find.text('Cut'), findsOneWidget); expect(find.text('Cut'), findsOneWidget);
expect(find.text('Copy'), findsNothing); expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
expect(appearsEnabled(tester, '▶'), true);
// Tapping the next button shows Copy. // Tapping the next button shows Copy.
await tester.tap(find.text('▶')); await tester.tapAt(tester.getCenter(findOverflowNextButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(CupertinoButton), findsNWidgets(3)); expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(3));
expect(find.text('Cut'), findsNothing); expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsOneWidget); expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
expect(appearsEnabled(tester, '◀'), true); expect(findOverflowNextButton(), findsOneWidget);
expect(find.text('▶'), findsOneWidget);
expect(appearsEnabled(tester, '▶'), true);
// Tapping the next button again shows Paste. // Tapping the next button again shows Paste and hides the next button as
await tester.tap(find.text('▶')); // the last page is shown.
await tester.tapAt(tester.getCenter(findOverflowNextButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(CupertinoButton), findsNWidgets(3)); expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(2));
expect(find.text('Cut'), findsNothing); expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing); expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsOneWidget); expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
expect(appearsEnabled(tester, '◀'), true); expect(findOverflowNextButton(), findsNothing);
expect(find.text('▶'), findsOneWidget);
expect(appearsEnabled(tester, '▶'), false);
// Tapping the back button shows the second page again. // Tapping the back button shows the second page again with the next button.
await tester.tap(find.text('◀')); await tester.tapAt(tester.getCenter(findOverflowBackButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(CupertinoButton), findsNWidgets(3)); expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(3));
expect(find.text('Cut'), findsNothing); expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsOneWidget); expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
expect(appearsEnabled(tester, '◀'), true); expect(findOverflowNextButton(), findsOneWidget);
expect(find.text('▶'), findsOneWidget);
expect(appearsEnabled(tester, '▶'), true);
// Tapping the back button again shows the first page again. // Tapping the back button again shows the first page again.
await tester.tap(find.text('◀')); await tester.tapAt(tester.getCenter(findOverflowBackButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(CupertinoButton), findsNWidgets(2)); expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(2));
expect(find.text('Cut'), findsOneWidget); expect(find.text('Cut'), findsOneWidget);
expect(find.text('Copy'), findsNothing); expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
expect(appearsEnabled(tester, '▶'), true);
}, },
skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
...@@ -452,8 +439,8 @@ void main() { ...@@ -452,8 +439,8 @@ void main() {
expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsNothing); expect(findOverflowNextButton(), findsNothing);
// Long press on an empty space to show the selection menu, with only the // Long press on an empty space to show the selection menu, with only the
// paste button visible. // paste button visible.
...@@ -463,21 +450,18 @@ void main() { ...@@ -463,21 +450,18 @@ void main() {
expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
expect(find.text(_longLocalizations.pasteButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.pasteButtonLabel), findsOneWidget);
expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
expect(appearsEnabled(tester, '▶'), true);
// Tap next to go to the second and final page. // Tap next to go to the second and final page.
await tester.tap(find.text('▶')); await tester.tapAt(tester.getCenter(findOverflowNextButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); expect(find.text(_longLocalizations.cutButtonLabel), findsNothing);
expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
expect(find.text(_longLocalizations.selectAllButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.selectAllButtonLabel), findsOneWidget);
expect(find.text('◀'), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
expect(find.text('▶'), findsOneWidget); expect(findOverflowNextButton(), findsNothing);
expect(appearsEnabled(tester, '◀'), true);
expect(appearsEnabled(tester, '▶'), false);
// Tap select all to show the full selection menu. // Tap select all to show the full selection menu.
await tester.tap(find.text(_longLocalizations.selectAllButtonLabel)); await tester.tap(find.text(_longLocalizations.selectAllButtonLabel));
...@@ -488,56 +472,48 @@ void main() { ...@@ -488,56 +472,48 @@ void main() {
expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
expect(appearsEnabled(tester, '▶'), true);
// Tap next to go to the second page. // Tap next to go to the second page.
await tester.tap(find.text('▶')); await tester.tapAt(tester.getCenter(findOverflowNextButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); expect(find.text(_longLocalizations.cutButtonLabel), findsNothing);
expect(find.text(_longLocalizations.copyButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.copyButtonLabel), findsOneWidget);
expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
expect(find.text('◀'), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
expect(find.text('▶'), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
expect(appearsEnabled(tester, '◀'), true);
expect(appearsEnabled(tester, '▶'), true);
// Tap next to go to the third and final page. // Tap next to go to the third and final page.
await tester.tap(find.text('▶')); await tester.tapAt(tester.getCenter(findOverflowNextButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); expect(find.text(_longLocalizations.cutButtonLabel), findsNothing);
expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
expect(find.text(_longLocalizations.pasteButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.pasteButtonLabel), findsOneWidget);
expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
expect(find.text('◀'), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
expect(find.text('▶'), findsOneWidget); expect(findOverflowNextButton(), findsNothing);
expect(appearsEnabled(tester, '◀'), true);
expect(appearsEnabled(tester, '▶'), false);
// Tap back to go to the second page again. // Tap back to go to the second page again.
await tester.tap(find.text('◀')); await tester.tapAt(tester.getCenter(findOverflowBackButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); expect(find.text(_longLocalizations.cutButtonLabel), findsNothing);
expect(find.text(_longLocalizations.copyButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.copyButtonLabel), findsOneWidget);
expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
expect(find.text('◀'), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
expect(find.text('▶'), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
expect(appearsEnabled(tester, '◀'), true);
expect(appearsEnabled(tester, '▶'), true);
// Tap back to go to the first page again. // Tap back to go to the first page again.
await tester.tap(find.text('◀')); await tester.tapAt(tester.getCenter(findOverflowBackButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text(_longLocalizations.cutButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.cutButtonLabel), findsOneWidget);
expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.copyButtonLabel), findsNothing);
expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing);
expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
expect(appearsEnabled(tester, '▶'), true);
}, },
skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
...@@ -572,8 +548,8 @@ void main() { ...@@ -572,8 +548,8 @@ void main() {
expect(find.text('Copy'), findsNothing); expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsNothing); expect(find.text('Paste'), findsNothing);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsNothing); expect(findOverflowNextButton(), findsNothing);
// Long press on an space to show the selection menu. // Long press on an space to show the selection menu.
await tester.longPressAt(textOffsetToPosition(tester, 1)); await tester.longPressAt(textOffsetToPosition(tester, 1));
...@@ -582,8 +558,8 @@ void main() { ...@@ -582,8 +558,8 @@ void main() {
expect(find.text('Copy'), findsNothing); expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsOneWidget); expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select All'), findsOneWidget); expect(find.text('Select All'), findsOneWidget);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsNothing); expect(findOverflowNextButton(), findsNothing);
// Tap to select all. // Tap to select all.
await tester.tap(find.text('Select All')); await tester.tap(find.text('Select All'));
...@@ -594,8 +570,8 @@ void main() { ...@@ -594,8 +570,8 @@ void main() {
expect(find.text('Copy'), findsOneWidget); expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsOneWidget); expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select All'), findsNothing); expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing); expect(findOverflowBackButton(), findsNothing);
expect(find.text('▶'), findsNothing); expect(findOverflowNextButton(), findsNothing);
// The menu appears at the top of the visible selection. // The menu appears at the top of the visible selection.
final Offset selectionOffset = tester final Offset selectionOffset = tester
...@@ -603,8 +579,8 @@ void main() { ...@@ -603,8 +579,8 @@ void main() {
final Offset textFieldOffset = final Offset textFieldOffset =
tester.getTopLeft(find.byType(CupertinoTextField)); tester.getTopLeft(find.byType(CupertinoTextField));
// 7.0 + 43.0 + 8.0 - 8.0 = _kToolbarArrowSize + _kToolbarHeight + _kToolbarContentDistance - padding // 7.0 + 45.0 + 8.0 - 8.0 = _kToolbarArrowSize + _kToolbarHeight + _kToolbarContentDistance - padding
expect(selectionOffset.dy + 7.0 + 43.0 + 8.0 - 8.0, equals(textFieldOffset.dy)); expect(selectionOffset.dy + 7.0 + 45.0 + 8.0 - 8.0, equals(textFieldOffset.dy));
}, },
skip: isBrowser, // [intended] the selection menu isn't required by web skip: isBrowser, // [intended] the selection menu isn't required by web
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
......
...@@ -29,7 +29,7 @@ void main() { ...@@ -29,7 +29,7 @@ void main() {
expect(pressed, true); expect(pressed, true);
}); });
testWidgets('pressedOpacity defaults to 0.1', (WidgetTester tester) async { testWidgets('background darkens when pressed', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
home: Center( home: Center(
...@@ -41,35 +41,38 @@ void main() { ...@@ -41,35 +41,38 @@ void main() {
), ),
); );
// Original at full opacity. // Original with transparent background.
FadeTransition opacity = tester.widget(find.descendant( DecoratedBox decoratedBox = tester.widget(find.descendant(
of: find.byType(CupertinoTextSelectionToolbarButton), of: find.byType(CupertinoButton),
matching: find.byType(FadeTransition), matching: find.byType(DecoratedBox),
)); ));
expect(opacity.opacity.value, 1.0); BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration;
expect(boxDecoration.color, const Color(0x00000000));
// Make a "down" gesture on the button. // Make a "down" gesture on the button.
final Offset center = tester.getCenter(find.byType(CupertinoTextSelectionToolbarButton)); final Offset center = tester.getCenter(find.byType(CupertinoTextSelectionToolbarButton));
final TestGesture gesture = await tester.startGesture(center); final TestGesture gesture = await tester.startGesture(center);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Opacity reduces during the down gesture. // When pressed, the background darkens.
opacity = tester.widget(find.descendant( decoratedBox = tester.widget(find.descendant(
of: find.byType(CupertinoTextSelectionToolbarButton), of: find.byType(CupertinoTextSelectionToolbarButton),
matching: find.byType(FadeTransition), matching: find.byType(DecoratedBox),
)); ));
expect(opacity.opacity.value, 0.7); boxDecoration = decoratedBox.decoration as BoxDecoration;
expect(boxDecoration.color!.value, const Color(0x10000000).value);
// Release the down gesture. // Release the down gesture.
await gesture.up(); await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Opacity is back to normal. // Color is back to transparent.
opacity = tester.widget(find.descendant( decoratedBox = tester.widget(find.descendant(
of: find.byType(CupertinoTextSelectionToolbarButton), of: find.byType(CupertinoTextSelectionToolbarButton),
matching: find.byType(FadeTransition), matching: find.byType(DecoratedBox),
)); ));
expect(opacity.opacity.value, 1.0); boxDecoration = decoratedBox.decoration as BoxDecoration;
expect(boxDecoration.color, const Color(0x00000000));
}); });
testWidgets('passing null to onPressed disables the button', (WidgetTester tester) async { testWidgets('passing null to onPressed disables the button', (WidgetTester tester) async {
......
...@@ -6,12 +6,13 @@ import 'package:flutter/cupertino.dart'; ...@@ -6,12 +6,13 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/editable_text_utils.dart' show textOffsetToPosition; import '../widgets/editable_text_utils.dart' show textOffsetToPosition;
// These constants are copied from cupertino/text_selection_toolbar.dart. // These constants are copied from cupertino/text_selection_toolbar.dart.
const double _kArrowScreenPadding = 26.0; const double _kArrowScreenPadding = 26.0;
const double _kToolbarContentDistance = 8.0; const double _kToolbarContentDistance = 8.0;
const double _kToolbarHeight = 43.0; const double _kToolbarHeight = 45.0;
// A custom text selection menu that just displays a single custom button. // A custom text selection menu that just displays a single custom button.
class _CustomCupertinoTextSelectionControls extends CupertinoTextSelectionControls { class _CustomCupertinoTextSelectionControls extends CupertinoTextSelectionControls {
...@@ -60,9 +61,9 @@ class TestBox extends SizedBox { ...@@ -60,9 +61,9 @@ class TestBox extends SizedBox {
static const double itemWidth = 100.0; static const double itemWidth = 100.0;
} }
const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.withBrightness( const CupertinoDynamicColor _kToolbarTextColor = CupertinoDynamicColor.withBrightness(
color: Color(0xEBF7F7F7), color: CupertinoColors.black,
darkColor: Color(0xEB202020), darkColor: CupertinoColors.white,
); );
void main() { void main() {
...@@ -81,8 +82,65 @@ void main() { ...@@ -81,8 +82,65 @@ void main() {
// visible part of the toolbar for use in measurements. // visible part of the toolbar for use in measurements.
Finder findToolbar() => findPrivate('_CupertinoTextSelectionToolbarContent'); Finder findToolbar() => findPrivate('_CupertinoTextSelectionToolbarContent');
Finder findOverflowNextButton() => find.text('▶'); // Check if the middle point of the chevron is pointing left or right.
Finder findOverflowBackButton() => find.text('◀'); //
// Offset.dx: a right or left margin (_kToolbarChevronSize / 4 => 2.5) to center the icon horizontally
// Offset.dy: always in the exact vertical center (_kToolbarChevronSize / 2 => 5)
PaintPattern overflowNextPaintPattern() => paints
..line(p1: const Offset(2.5, 0), p2: const Offset(7.5, 5))
..line(p1: const Offset(7.5, 5), p2: const Offset(2.5, 10));
PaintPattern overflowBackPaintPattern() => paints
..line(p1: const Offset(7.5, 0), p2: const Offset(2.5, 5))
..line(p1: const Offset(2.5, 5), p2: const Offset(7.5, 10));
Finder findOverflowNextButton() => find.byWidgetPredicate((Widget widget) =>
widget is CustomPaint &&
'${widget.painter?.runtimeType}' == '_RightCupertinoChevronPainter',
);
Finder findOverflowBackButton() => find.byWidgetPredicate((Widget widget) =>
widget is CustomPaint &&
'${widget.painter?.runtimeType}' == '_LeftCupertinoChevronPainter',
);
testWidgets('chevrons point to the correct side', (WidgetTester tester) async {
// Add enough TestBoxes to need 3 pages.
final List<Widget> children = List<Widget>.generate(15, (int i) => const TestBox());
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextSelectionToolbar(
anchorAbove: const Offset(50.0, 100.0),
anchorBelow: const Offset(50.0, 200.0),
children: children,
),
),
),
);
expect(findOverflowBackButton(), findsNothing);
expect(findOverflowNextButton(), findsOneWidget);
expect(findOverflowNextButton(), overflowNextPaintPattern());
// Tap the overflow next button to show the next page of children.
await tester.tapAt(tester.getCenter(findOverflowNextButton()));
await tester.pumpAndSettle();
expect(findOverflowBackButton(), findsOneWidget);
expect(findOverflowNextButton(), findsOneWidget);
expect(findOverflowBackButton(), overflowBackPaintPattern());
expect(findOverflowNextButton(), overflowNextPaintPattern());
// Tap the overflow next button to show the last page of children.
await tester.tapAt(tester.getCenter(findOverflowNextButton()));
await tester.pumpAndSettle();
expect(findOverflowBackButton(), findsOneWidget);
expect(findOverflowNextButton(), findsNothing);
expect(findOverflowBackButton(), overflowBackPaintPattern());
}, skip: kIsWeb); // Path.combine is not implemented in the HTML backend https://github.com/flutter/flutter/issues/44572
testWidgets('paginates children if they overflow', (WidgetTester tester) async { testWidgets('paginates children if they overflow', (WidgetTester tester) async {
late StateSetter setState; late StateSetter setState;
...@@ -121,22 +179,15 @@ void main() { ...@@ -121,22 +179,15 @@ void main() {
expect(findOverflowBackButton(), findsNothing); expect(findOverflowBackButton(), findsNothing);
// Tap the overflow next button to show the next page of children. // Tap the overflow next button to show the next page of children.
await tester.tap(findOverflowNextButton()); // The next button is hidden as there's no next page.
await tester.pumpAndSettle(); await tester.tapAt(tester.getCenter(findOverflowNextButton()));
expect(find.byType(TestBox), findsNWidgets(1));
expect(findOverflowNextButton(), findsOneWidget);
expect(findOverflowBackButton(), findsOneWidget);
// Tapping the overflow next button again does nothing because it is
// disabled and there are no more children to display.
await tester.tap(findOverflowNextButton());
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(TestBox), findsNWidgets(1)); expect(find.byType(TestBox), findsNWidgets(1));
expect(findOverflowNextButton(), findsOneWidget); expect(findOverflowNextButton(), findsNothing);
expect(findOverflowBackButton(), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
// Tap the overflow back button to go back to the first page. // Tap the overflow back button to go back to the first page.
await tester.tap(findOverflowBackButton()); await tester.tapAt(tester.getCenter(findOverflowBackButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(TestBox), findsNWidgets(7)); expect(find.byType(TestBox), findsNWidgets(7));
expect(findOverflowNextButton(), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
...@@ -157,7 +208,7 @@ void main() { ...@@ -157,7 +208,7 @@ void main() {
expect(findOverflowBackButton(), findsNothing); expect(findOverflowBackButton(), findsNothing);
// Tap the overflow next button to show the second page of children. // Tap the overflow next button to show the second page of children.
await tester.tap(findOverflowNextButton()); await tester.tapAt(tester.getCenter(findOverflowNextButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// With the back button, only six children fit on this page. // With the back button, only six children fit on this page.
expect(find.byType(TestBox), findsNWidgets(6)); expect(find.byType(TestBox), findsNWidgets(6));
...@@ -165,21 +216,21 @@ void main() { ...@@ -165,21 +216,21 @@ void main() {
expect(findOverflowBackButton(), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
// Tap the overflow next button again to show the third page of children. // Tap the overflow next button again to show the third page of children.
await tester.tap(findOverflowNextButton()); await tester.tapAt(tester.getCenter(findOverflowNextButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(TestBox), findsNWidgets(1)); expect(find.byType(TestBox), findsNWidgets(1));
expect(findOverflowNextButton(), findsOneWidget); expect(findOverflowNextButton(), findsNothing);
expect(findOverflowBackButton(), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
// Tap the overflow back button to go back to the second page. // Tap the overflow back button to go back to the second page.
await tester.tap(findOverflowBackButton()); await tester.tapAt(tester.getCenter(findOverflowBackButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(TestBox), findsNWidgets(6)); expect(find.byType(TestBox), findsNWidgets(6));
expect(findOverflowNextButton(), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
expect(findOverflowBackButton(), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget);
// Tap the overflow back button to go back to the first page. // Tap the overflow back button to go back to the first page.
await tester.tap(findOverflowBackButton()); await tester.tapAt(tester.getCenter(findOverflowBackButton()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(TestBox), findsNWidgets(7)); expect(find.byType(TestBox), findsNWidgets(7));
expect(findOverflowNextButton(), findsOneWidget); expect(findOverflowNextButton(), findsOneWidget);
...@@ -345,13 +396,12 @@ void main() { ...@@ -345,13 +396,12 @@ void main() {
final Finder buttonFinder = find.byType(CupertinoButton); final Finder buttonFinder = find.byType(CupertinoButton);
expect(buttonFinder, findsOneWidget); expect(buttonFinder, findsOneWidget);
final Finder decorationFinder = find.descendant( final Finder textFinder = find.descendant(
of: find.byType(CupertinoButton), of: find.byType(CupertinoButton),
matching: find.byType(DecoratedBox) matching: find.byType(Text)
); );
expect(decorationFinder, findsOneWidget); expect(textFinder, findsOneWidget);
final DecoratedBox decoratedBox = tester.widget(decorationFinder); final Text text = tester.widget(textFinder);
final BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration;
// Theme brightness is preferred, otherwise MediaQuery brightness is // Theme brightness is preferred, otherwise MediaQuery brightness is
// used. If both are null, defaults to light. // used. If both are null, defaults to light.
...@@ -363,10 +413,10 @@ void main() { ...@@ -363,10 +413,10 @@ void main() {
} }
expect( expect(
boxDecoration.color!.value, text.style!.color!.value,
effectiveBrightness == Brightness.dark effectiveBrightness == Brightness.dark
? _kToolbarBackgroundColor.darkColor.value ? _kToolbarTextColor.darkColor.value
: _kToolbarBackgroundColor.color.value, : _kToolbarTextColor.color.value,
); );
}, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web. }, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web.
} }
...@@ -419,7 +469,7 @@ void main() { ...@@ -419,7 +469,7 @@ void main() {
of: find.byType(CupertinoTextSelectionToolbar), of: find.byType(CupertinoTextSelectionToolbar),
matching: find.byType(DecoratedBox), matching: find.byType(DecoratedBox),
); );
expect(finder, findsNWidgets(2)); expect(finder, findsOneWidget);
DecoratedBox decoratedBox = tester.widget(finder.first); DecoratedBox decoratedBox = tester.widget(finder.first);
BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration; BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration;
List<BoxShadow>? shadows = boxDecoration.boxShadow; List<BoxShadow>? shadows = boxDecoration.boxShadow;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment