diff --git a/packages/flutter/lib/src/cupertino/text_selection_toolbar.dart b/packages/flutter/lib/src/cupertino/text_selection_toolbar.dart index 9ff7dfc31b..e4703a60f1 100644 --- a/packages/flutter/lib/src/cupertino/text_selection_toolbar.dart +++ b/packages/flutter/lib/src/cupertino/text_selection_toolbar.dart @@ -15,7 +15,7 @@ import 'theme.dart'; // Values extracted from https://developer.apple.com/design/resources/. // 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 // is pointing to. The value used here is eyeballed. const double _kToolbarContentDistance = 8.0; @@ -28,15 +28,29 @@ const double _kArrowScreenPadding = 26.0; // Values extracted from https://developer.apple.com/design/resources/. const Radius _kToolbarBorderRadius = Radius.circular(8); -const CupertinoDynamicColor _kToolbarDividerColor = 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 below. - color: Color(0xFFB6B6B6), - // Color extracted from https://developer.apple.com/design/resources/. - // TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507. - darkColor: Color(0xFF808080), +// 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( + color: Color(0xFFD6D6D6), + darkColor: Color(0xFF424242), +); + +const CupertinoDynamicColor _kToolbarTextColor = CupertinoDynamicColor.withBrightness( + 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 /// child. /// @@ -54,13 +68,6 @@ typedef CupertinoToolbarBuilder = Widget Function( 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. /// /// Typically displays buttons for text manipulation, e.g. copying and pasting @@ -117,29 +124,14 @@ class CupertinoTextSelectionToolbar extends StatelessWidget { /// * [TextSelectionToolbar], which uses this same value as well. static const double kToolbarScreenPadding = 8.0; - // Add the visual vertical line spacer between children buttons. - static List _addChildrenSpacers(List children) { - final List nextChildren = []; - 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 // background and a rounded cutout with an arrow. static Widget _defaultToolbarBuilder(BuildContext context, Offset anchor, bool isAbove, Widget child) { final Widget outputChild = _CupertinoTextSelectionToolbarShape( anchor: anchor, isAbove: isAbove, - child: DecoratedBox( - decoration: BoxDecoration( - color: _kToolbarDividerColor.resolveFrom(context), - ), + child: ColoredBox( + color: _kToolbarBackgroundColor.resolveFrom(context), child: child, ), ); @@ -209,7 +201,7 @@ class CupertinoTextSelectionToolbar extends StatelessWidget { anchor: fitsAbove ? anchorAboveAdjusted : anchorBelowAdjusted, isAbove: fitsAbove, toolbarBuilder: toolbarBuilder, - children: _addChildrenSpacers(children), + children: children, ), ), ); @@ -449,19 +441,43 @@ class _CupertinoTextSelectionToolbarContent extends StatefulWidget { class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSelectionToolbarContent> with TickerProviderStateMixin { // Controls the fading of the buttons within the menu during page transitions. late AnimationController _controller; - int _page = 0; 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() { - _controller.reverse(); - _controller.addStatusListener(_statusListener); - _nextPage = _page + 1; + final RenderBox? renderToolbar = + _toolbarItemsKey.currentContext?.findRenderObject() as RenderBox?; + + if (renderToolbar is _RenderCupertinoTextSelectionToolbarItems && renderToolbar.hasNextPage) { + _controller.reverse(); + _controller.addStatusListener(_statusListener); + _nextPage = _page + 1; + } } void _handlePreviousPage() { - _controller.reverse(); - _controller.addStatusListener(_statusListener); - _nextPage = _page - 1; + final RenderBox? renderToolbar = + _toolbarItemsKey.currentContext?.findRenderObject() as RenderBox?; + + if (renderToolbar is _RenderCupertinoTextSelectionToolbarItems && renderToolbar.hasPreviousPage) { + _controller.reverse(); + _controller.addStatusListener(_statusListener); + _nextPage = _page - 1; + } } void _statusListener(AnimationStatus status) { @@ -484,7 +500,7 @@ class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSel value: 1.0, vsync: this, // 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 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 Widget build(BuildContext context) { return widget.toolbarBuilder(context, widget.anchor, widget.isAbove, FadeTransition( opacity: _controller, - child: _CupertinoTextSelectionToolbarItems( - page: _page, - backButton: CupertinoTextSelectionToolbarButton.text( - onPressed: _handlePreviousPage, - text: '◀', + child: AnimatedSize( + duration: _kToolbarTransitionDuration, + curve: Curves.decelerate, + child: GestureDetector( + onHorizontalDragEnd: _onHorizontalDragEnd, + child: _CupertinoTextSelectionToolbarItems( + key: _toolbarItemsKey, + page: _page, + backButton: CupertinoTextSelectionToolbarButton( + onPressed: _handlePreviousPage, + child: _createChevron(isLeft: true), + ), + dividerColor: _kToolbarDividerColor.resolveFrom(context), + dividerWidth: 1.0 / MediaQuery.devicePixelRatioOf(context), + nextButton: CupertinoTextSelectionToolbarButton( + onPressed: _handleNextPage, + child: _createChevron(isLeft: false), + ), + children: widget.children, + ), ), - dividerWidth: 1.0 / MediaQuery.devicePixelRatioOf(context), - nextButton: CupertinoTextSelectionToolbarButton.text( - onPressed: _handleNextPage, - text: '▶', - ), - nextButtonDisabled: const CupertinoTextSelectionToolbarButton.text( - text: '▶', - ), - 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 // _RenderCupertinoTextSelectionToolbarItems and // _CupertinoTextSelectionToolbarItemsElement, paginates the menu items. class _CupertinoTextSelectionToolbarItems extends RenderObjectWidget { _CupertinoTextSelectionToolbarItems({ + super.key, required this.page, required this.children, required this.backButton, + required this.dividerColor, required this.dividerWidth, required this.nextButton, - required this.nextButtonDisabled, }) : assert(children.isNotEmpty); final Widget backButton; final List children; + final Color dividerColor; final double dividerWidth; final Widget nextButton; - final Widget nextButtonDisabled; final int page; @override _RenderCupertinoTextSelectionToolbarItems createRenderObject(BuildContext context) { return _RenderCupertinoTextSelectionToolbarItems( + dividerColor: dividerColor, dividerWidth: dividerWidth, page: page, ); @@ -562,6 +662,7 @@ class _CupertinoTextSelectionToolbarItems extends RenderObjectWidget { void updateRenderObject(BuildContext context, _RenderCupertinoTextSelectionToolbarItems renderObject) { renderObject ..page = page + ..dividerColor = dividerColor ..dividerWidth = dividerWidth; } @@ -591,8 +692,6 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement { renderObject.backButton = child; case _CupertinoTextSelectionToolbarItemsSlot.nextButton: renderObject.nextButton = child; - case _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled: - renderObject.nextButtonDisabled = child; } } @@ -683,7 +782,6 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement { final _CupertinoTextSelectionToolbarItems toolbarItems = widget as _CupertinoTextSelectionToolbarItems; _mountChild(toolbarItems.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton); _mountChild(toolbarItems.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton); - _mountChild(toolbarItems.nextButtonDisabled, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled); // Mount list children. _children = List.filled(toolbarItems.children.length, _NullElement.instance); @@ -718,7 +816,6 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement { final _CupertinoTextSelectionToolbarItems toolbarItems = widget as _CupertinoTextSelectionToolbarItems; _mountChild(toolbarItems.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton); _mountChild(toolbarItems.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton); - _mountChild(toolbarItems.nextButtonDisabled, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled); // Update list children. _children = updateChildren(_children, toolbarItems.children, forgottenChildren: _forgottenChildren); @@ -729,14 +826,19 @@ class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement { // The custom RenderBox that helps paginate the menu items. class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { _RenderCupertinoTextSelectionToolbarItems({ + required Color dividerColor, required double dividerWidth, required int page, - }) : _dividerWidth = dividerWidth, + }) : _dividerColor = dividerColor, + _dividerWidth = dividerWidth, _page = page, super(); final Map<_CupertinoTextSelectionToolbarItemsSlot, RenderBox> slottedChildren = <_CupertinoTextSelectionToolbarItemsSlot, RenderBox>{}; + late bool hasNextPage; + late bool hasPreviousPage; + RenderBox? _updateChild(RenderBox? oldChild, RenderBox? newChild, _CupertinoTextSelectionToolbarItemsSlot slot) { if (oldChild != null) { dropChild(oldChild); @@ -750,7 +852,7 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container } bool _isSlottedChild(RenderBox child) { - return child == _backButton || child == _nextButton || child == _nextButtonDisabled; + return child == _backButton || child == _nextButton; } int _page; @@ -763,6 +865,16 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container markNeedsLayout(); } + Color _dividerColor; + Color get dividerColor => _dividerColor; + set dividerColor(Color value) { + if (value == _dividerColor) { + return; + } + _dividerColor = value; + markNeedsLayout(); + } + double _dividerWidth; double get dividerWidth => _dividerWidth; set dividerWidth(double value) { @@ -785,12 +897,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container _nextButton = _updateChild(_nextButton, value, _CupertinoTextSelectionToolbarItemsSlot.nextButton); } - RenderBox? _nextButtonDisabled; - RenderBox? get nextButtonDisabled => _nextButtonDisabled; - set nextButtonDisabled(RenderBox? value) { - _nextButtonDisabled = _updateChild(_nextButtonDisabled, value, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled); - } - @override void performLayout() { if (firstChild == null) { @@ -801,7 +907,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container // Layout slotted children. _backButton!.layout(constraints.loosen(), parentUsesSize: true); _nextButton!.layout(constraints.loosen(), parentUsesSize: true); - _nextButtonDisabled!.layout(constraints.loosen(), parentUsesSize: true); final double subsequentPageButtonsWidth = _backButton!.size.width + _nextButton!.size.width; @@ -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. // Note childCount doesn't include slotted children which come before the list ones. paginationButtonsWidth = - i == childCount + 2 ? 0.0 : _nextButton!.size.width; + i == childCount + 1 ? 0.0 : _nextButton!.size.width; } else { paginationButtonsWidth = subsequentPageButtonsWidth; } @@ -881,17 +986,10 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container if (currentPage > 0) { final ToolbarItemsParentData nextButtonParentData = _nextButton!.parentData! as ToolbarItemsParentData; - final ToolbarItemsParentData nextButtonDisabledParentData = - _nextButtonDisabled!.parentData! as ToolbarItemsParentData; final ToolbarItemsParentData backButtonParentData = _backButton!.parentData! as ToolbarItemsParentData; - // The forward button always shows if there is more than one page, even on - // the last page (it's just disabled). - if (page == currentPage) { - nextButtonDisabledParentData.offset = Offset(toolbarWidth, 0.0); - nextButtonDisabledParentData.shouldPaint = true; - toolbarWidth += nextButtonDisabled!.size.width; - } else { + // The forward button only shows when there's a page after this one. + if (page != currentPage) { nextButtonParentData.offset = Offset(toolbarWidth, 0.0); nextButtonParentData.shouldPaint = true; toolbarWidth += nextButton!.size.width; @@ -903,6 +1001,11 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container // already been taken care of when laying out the children to // 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 { // No divider for the next button when there's only one page. toolbarWidth -= dividerWidth; @@ -920,6 +1023,18 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container if (childParentData.shouldPaint) { final Offset childOffset = childParentData.offset + offset; 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 if (hitTestChild(nextButton, result, position: position)) { return true; } - if (hitTestChild(nextButtonDisabled, result, position: position)) { - return true; - } return false; } @@ -1023,9 +1135,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container if (_nextButton != null) { visitor(_nextButton!); } - if (_nextButtonDisabled != null) { - visitor(_nextButtonDisabled!); - } // Visit the list children. super.visitChildren(visitor); } @@ -1051,8 +1160,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container value.add(child.toDiagnosticsNode(name: 'back button')); } else if (child == nextButton) { value.add(child.toDiagnosticsNode(name: 'next button')); - } else if (child == nextButtonDisabled) { - value.add(child.toDiagnosticsNode(name: 'next button disabled')); // List children. } else { @@ -1068,7 +1175,6 @@ class _RenderCupertinoTextSelectionToolbarItems extends RenderBox with Container enum _CupertinoTextSelectionToolbarItemsSlot { backButton, nextButton, - nextButtonDisabled, } class _NullElement extends Element { diff --git a/packages/flutter/lib/src/cupertino/text_selection_toolbar_button.dart b/packages/flutter/lib/src/cupertino/text_selection_toolbar_button.dart index 2d547df0f5..e31946ccb5 100644 --- a/packages/flutter/lib/src/cupertino/text_selection_toolbar_button.dart +++ b/packages/flutter/lib/src/cupertino/text_selection_toolbar_button.dart @@ -11,30 +11,26 @@ import 'localizations.dart'; const TextStyle _kToolbarButtonFontStyle = TextStyle( inherit: false, - fontSize: 14.0, + fontSize: 15.0, letterSpacing: -0.15, 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( color: CupertinoColors.black, darkColor: CupertinoColors.white, ); -// Eyeballed value. -const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 16.0, horizontal: 18.0); +const CupertinoDynamicColor _kToolbarPressedColor = CupertinoDynamicColor.withBrightness( + 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. -class CupertinoTextSelectionToolbarButton extends StatelessWidget { +class CupertinoTextSelectionToolbarButton extends StatefulWidget { /// Create an instance of [CupertinoTextSelectionToolbarButton]. /// /// [child] cannot be null. @@ -114,25 +110,64 @@ class CupertinoTextSelectionToolbarButton extends StatelessWidget { } @override - Widget build(BuildContext context) { - final Widget child = this.child ?? Text( - text ?? getButtonLabel(context, buttonItem!), - overflow: TextOverflow.ellipsis, - style: _kToolbarButtonFontStyle.copyWith( - color: onPressed != null - ? _kToolbarTextColor.resolveFrom(context) - : CupertinoColors.inactiveGray, - ), - ); + State createState() => _CupertinoTextSelectionToolbarButtonState(); +} - return CupertinoButton( +class _CupertinoTextSelectionToolbarButtonState extends State { + 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 + Widget build(BuildContext context) { + final Widget child = CupertinoButton( + color: isPressed + ? _kToolbarPressedColor.resolveFrom(context) + : const Color(0x00000000), borderRadius: null, - color: _kToolbarBackgroundColor, - disabledColor: _kToolbarBackgroundColor, - onPressed: onPressed, + 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, - pressedOpacity: onPressed == null ? 1.0 : 0.7, - child: child, + // 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, + style: _kToolbarButtonFontStyle.copyWith( + color: widget.onPressed != null + ? _kToolbarTextColor.resolveFrom(context) + : CupertinoColors.inactiveGray, + ), + ), ); + + if (widget.onPressed != null) { + // As it's needed to change the CupertinoButton's backgroundColor when + // pressed, not its opacity, this GestureDetector handles both the + // onPressed callback and the backgroundColor change. + return GestureDetector( + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + child: child, + ); + } else { + return child; + } } } diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 5e869ca25e..7a802fcf20 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -1545,7 +1545,7 @@ void main() { Text text = tester.widget(find.text('Paste')); 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!.fontWeight, FontWeight.w400); @@ -1577,7 +1577,7 @@ void main() { text = tester.widget(find.text('Paste')); // The toolbar buttons' text are still the same style. 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!.fontWeight, FontWeight.w400); }, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu. @@ -6537,7 +6537,7 @@ void main() { topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01), leftMatcher: moreOrLessEquals(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() { pathMatcher: PathBoundsMatcher( topMatcher: moreOrLessEquals(bottomLeftSelectionPosition.dy + 8, epsilon: 0.01), 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), ), ), @@ -6650,7 +6650,7 @@ void main() { paints..clipPath( pathMatcher: PathBoundsMatcher( 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), leftMatcher: greaterThanOrEqualTo(8), ), @@ -6719,7 +6719,7 @@ void main() { paints..clipPath( pathMatcher: PathBoundsMatcher( 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), leftMatcher: greaterThanOrEqualTo(8), ), @@ -6792,7 +6792,7 @@ void main() { paints..clipPath( pathMatcher: PathBoundsMatcher( 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), leftMatcher: greaterThanOrEqualTo(8), ), diff --git a/packages/flutter/test/cupertino/text_selection_test.dart b/packages/flutter/test/cupertino/text_selection_test.dart index ed65377857..b679eb6d46 100644 --- a/packages/flutter/test/cupertino/text_selection_test.dart +++ b/packages/flutter/test/cupertino/text_selection_test.dart @@ -60,18 +60,6 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final MockClipboard mockClipboard = MockClipboard(); - // Returns true iff the button is visually enabled. - bool appearsEnabled(WidgetTester tester, String text) { - final CupertinoButton button = tester.widget( - find.ancestor( - of: find.text(text), - matching: find.byType(CupertinoButton), - ), - ); - // Disabled buttons have no opacity change when pressed. - return button.pressedOpacity! < 1.0; - } - List globalize(Iterable points, RenderBox box) { return points.map((TextSelectionPoint point) { return TextSelectionPoint( @@ -191,6 +179,15 @@ void main() { }); 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 { final TextEditingController controller = TextEditingController(text: 'abc def ghi'); await tester.pumpWidget(CupertinoApp( @@ -216,8 +213,8 @@ void main() { expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsNothing); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsNothing); // Long press on an empty space to show the selection menu. await tester.longPressAt(textOffsetToPosition(tester, 4)); @@ -226,8 +223,8 @@ void main() { expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsOneWidget); expect(find.text('Select All'), findsOneWidget); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsNothing); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsNothing); // Double tap to select a word and show the full selection menu. final Offset textOffset = textOffsetToPosition(tester, 1); @@ -241,8 +238,8 @@ void main() { expect(find.text('Copy'), findsOneWidget); expect(find.text('Paste'), findsOneWidget); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsNothing); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsNothing); }, skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. variant: const TargetPlatformVariant({ TargetPlatform.iOS }), @@ -273,8 +270,8 @@ void main() { expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsNothing); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsNothing); // Double tap to select a word and show the selection menu. final Offset textOffset = textOffsetToPosition(tester, 1); @@ -288,32 +285,29 @@ void main() { expect(find.text('Copy'), findsOneWidget); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '▶'), true); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsOneWidget); - // Tapping the next button shows the overflowing button. - await tester.tap(find.text('▶')); + // Tapping the next button shows the overflowing button and the next + // button is hidden as the last page is shown. + await tester.tapAt(tester.getCenter(findOverflowNextButton())); await tester.pumpAndSettle(); expect(find.text('Cut'), findsNothing); expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsOneWidget); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsOneWidget); - expect(appearsEnabled(tester, '◀'), true); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '▶'), false); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsNothing); - // Tapping the back button shows the first page again. - await tester.tap(find.text('◀')); + // Tapping the back button shows the first page again with the next button. + await tester.tapAt(tester.getCenter(findOverflowBackButton())); await tester.pumpAndSettle(); expect(find.text('Cut'), findsOneWidget); expect(find.text('Copy'), findsOneWidget); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '▶'), true); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsOneWidget); }, skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. variant: const TargetPlatformVariant({ TargetPlatform.iOS }), @@ -341,13 +335,13 @@ void main() { )); // 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('Copy'), findsNothing); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsNothing); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsNothing); // Double tap to select a word and show the selection menu. final Offset textOffset = textOffsetToPosition(tester, 1); @@ -357,65 +351,58 @@ void main() { await tester.pumpAndSettle(); // 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('Copy'), findsNothing); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '▶'), true); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsOneWidget); // Tapping the next button shows Copy. - await tester.tap(find.text('▶')); + await tester.tapAt(tester.getCenter(findOverflowNextButton())); await tester.pumpAndSettle(); - expect(find.byType(CupertinoButton), findsNWidgets(3)); + expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(3)); expect(find.text('Cut'), findsNothing); expect(find.text('Copy'), findsOneWidget); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsOneWidget); - expect(appearsEnabled(tester, '◀'), true); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '▶'), true); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsOneWidget); - // Tapping the next button again shows Paste. - await tester.tap(find.text('▶')); + // Tapping the next button again shows Paste and hides the next button as + // the last page is shown. + await tester.tapAt(tester.getCenter(findOverflowNextButton())); await tester.pumpAndSettle(); - expect(find.byType(CupertinoButton), findsNWidgets(3)); + expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(2)); expect(find.text('Cut'), findsNothing); expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsOneWidget); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsOneWidget); - expect(appearsEnabled(tester, '◀'), true); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '▶'), false); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsNothing); - // Tapping the back button shows the second page again. - await tester.tap(find.text('◀')); + // Tapping the back button shows the second page again with the next button. + await tester.tapAt(tester.getCenter(findOverflowBackButton())); await tester.pumpAndSettle(); - expect(find.byType(CupertinoButton), findsNWidgets(3)); + expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(3)); expect(find.text('Cut'), findsNothing); expect(find.text('Copy'), findsOneWidget); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsOneWidget); - expect(appearsEnabled(tester, '◀'), true); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '▶'), true); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsOneWidget); // Tapping the back button again shows the first page again. - await tester.tap(find.text('◀')); + await tester.tapAt(tester.getCenter(findOverflowBackButton())); await tester.pumpAndSettle(); - expect(find.byType(CupertinoButton), findsNWidgets(2)); + expect(find.byType(CupertinoTextSelectionToolbarButton), findsNWidgets(2)); expect(find.text('Cut'), findsOneWidget); expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '▶'), true); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsOneWidget); }, skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. variant: const TargetPlatformVariant({ TargetPlatform.iOS }), @@ -452,8 +439,8 @@ void main() { expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsNothing); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsNothing); // Long press on an empty space to show the selection menu, with only the // paste button visible. @@ -463,21 +450,18 @@ void main() { expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '▶'), true); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsOneWidget); // Tap next to go to the second and final page. - await tester.tap(find.text('▶')); + await tester.tapAt(tester.getCenter(findOverflowNextButton())); await tester.pumpAndSettle(); expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsOneWidget); - expect(find.text('◀'), findsOneWidget); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '◀'), true); - expect(appearsEnabled(tester, '▶'), false); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsNothing); // Tap select all to show the full selection menu. await tester.tap(find.text(_longLocalizations.selectAllButtonLabel)); @@ -488,56 +472,48 @@ void main() { expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '▶'), true); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsOneWidget); // Tap next to go to the second page. - await tester.tap(find.text('▶')); + await tester.tapAt(tester.getCenter(findOverflowNextButton())); await tester.pumpAndSettle(); expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); expect(find.text(_longLocalizations.copyButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); - expect(find.text('◀'), findsOneWidget); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '◀'), true); - expect(appearsEnabled(tester, '▶'), true); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsOneWidget); // Tap next to go to the third and final page. - await tester.tap(find.text('▶')); + await tester.tapAt(tester.getCenter(findOverflowNextButton())); await tester.pumpAndSettle(); expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); - expect(find.text('◀'), findsOneWidget); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '◀'), true); - expect(appearsEnabled(tester, '▶'), false); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsNothing); // Tap back to go to the second page again. - await tester.tap(find.text('◀')); + await tester.tapAt(tester.getCenter(findOverflowBackButton())); await tester.pumpAndSettle(); expect(find.text(_longLocalizations.cutButtonLabel), findsNothing); expect(find.text(_longLocalizations.copyButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); - expect(find.text('◀'), findsOneWidget); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '◀'), true); - expect(appearsEnabled(tester, '▶'), true); + expect(findOverflowBackButton(), findsOneWidget); + expect(findOverflowNextButton(), findsOneWidget); // Tap back to go to the first page again. - await tester.tap(find.text('◀')); + await tester.tapAt(tester.getCenter(findOverflowBackButton())); await tester.pumpAndSettle(); expect(find.text(_longLocalizations.cutButtonLabel), findsOneWidget); expect(find.text(_longLocalizations.copyButtonLabel), findsNothing); expect(find.text(_longLocalizations.pasteButtonLabel), findsNothing); expect(find.text(_longLocalizations.selectAllButtonLabel), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsOneWidget); - expect(appearsEnabled(tester, '▶'), true); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsOneWidget); }, skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. variant: const TargetPlatformVariant({ TargetPlatform.iOS }), @@ -572,8 +548,8 @@ void main() { expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsNothing); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsNothing); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsNothing); // Long press on an space to show the selection menu. await tester.longPressAt(textOffsetToPosition(tester, 1)); @@ -582,8 +558,8 @@ void main() { expect(find.text('Copy'), findsNothing); expect(find.text('Paste'), findsOneWidget); expect(find.text('Select All'), findsOneWidget); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsNothing); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsNothing); // Tap to select all. await tester.tap(find.text('Select All')); @@ -594,8 +570,8 @@ void main() { expect(find.text('Copy'), findsOneWidget); expect(find.text('Paste'), findsOneWidget); expect(find.text('Select All'), findsNothing); - expect(find.text('◀'), findsNothing); - expect(find.text('▶'), findsNothing); + expect(findOverflowBackButton(), findsNothing); + expect(findOverflowNextButton(), findsNothing); // The menu appears at the top of the visible selection. final Offset selectionOffset = tester @@ -603,8 +579,8 @@ void main() { final Offset textFieldOffset = tester.getTopLeft(find.byType(CupertinoTextField)); - // 7.0 + 43.0 + 8.0 - 8.0 = _kToolbarArrowSize + _kToolbarHeight + _kToolbarContentDistance - padding - expect(selectionOffset.dy + 7.0 + 43.0 + 8.0 - 8.0, equals(textFieldOffset.dy)); + // 7.0 + 45.0 + 8.0 - 8.0 = _kToolbarArrowSize + _kToolbarHeight + _kToolbarContentDistance - padding + 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 variant: const TargetPlatformVariant({ TargetPlatform.iOS }), diff --git a/packages/flutter/test/cupertino/text_selection_toolbar_button_test.dart b/packages/flutter/test/cupertino/text_selection_toolbar_button_test.dart index 708183bac8..76eee86317 100644 --- a/packages/flutter/test/cupertino/text_selection_toolbar_button_test.dart +++ b/packages/flutter/test/cupertino/text_selection_toolbar_button_test.dart @@ -29,7 +29,7 @@ void main() { expect(pressed, true); }); - testWidgets('pressedOpacity defaults to 0.1', (WidgetTester tester) async { + testWidgets('background darkens when pressed', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( @@ -41,35 +41,38 @@ void main() { ), ); - // Original at full opacity. - FadeTransition opacity = tester.widget(find.descendant( - of: find.byType(CupertinoTextSelectionToolbarButton), - matching: find.byType(FadeTransition), + // Original with transparent background. + DecoratedBox decoratedBox = tester.widget(find.descendant( + of: find.byType(CupertinoButton), + 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. final Offset center = tester.getCenter(find.byType(CupertinoTextSelectionToolbarButton)); final TestGesture gesture = await tester.startGesture(center); await tester.pumpAndSettle(); - // Opacity reduces during the down gesture. - opacity = tester.widget(find.descendant( + // When pressed, the background darkens. + decoratedBox = tester.widget(find.descendant( 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. await gesture.up(); await tester.pumpAndSettle(); - // Opacity is back to normal. - opacity = tester.widget(find.descendant( + // Color is back to transparent. + decoratedBox = tester.widget(find.descendant( 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 { diff --git a/packages/flutter/test/cupertino/text_selection_toolbar_test.dart b/packages/flutter/test/cupertino/text_selection_toolbar_test.dart index 98a56018ed..5c110b22e9 100644 --- a/packages/flutter/test/cupertino/text_selection_toolbar_test.dart +++ b/packages/flutter/test/cupertino/text_selection_toolbar_test.dart @@ -6,12 +6,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../rendering/mock_canvas.dart'; import '../widgets/editable_text_utils.dart' show textOffsetToPosition; // These constants are copied from cupertino/text_selection_toolbar.dart. const double _kArrowScreenPadding = 26.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. class _CustomCupertinoTextSelectionControls extends CupertinoTextSelectionControls { @@ -60,9 +61,9 @@ class TestBox extends SizedBox { static const double itemWidth = 100.0; } -const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.withBrightness( - color: Color(0xEBF7F7F7), - darkColor: Color(0xEB202020), +const CupertinoDynamicColor _kToolbarTextColor = CupertinoDynamicColor.withBrightness( + color: CupertinoColors.black, + darkColor: CupertinoColors.white, ); void main() { @@ -81,8 +82,65 @@ void main() { // visible part of the toolbar for use in measurements. Finder findToolbar() => findPrivate('_CupertinoTextSelectionToolbarContent'); - Finder findOverflowNextButton() => find.text('▶'); - Finder findOverflowBackButton() => find.text('◀'); + // Check if the middle point of the chevron is pointing left or right. + // + // 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 children = List.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 { late StateSetter setState; @@ -121,22 +179,15 @@ void main() { expect(findOverflowBackButton(), findsNothing); // 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.tapAt(tester.getCenter(findOverflowNextButton())); await tester.pumpAndSettle(); 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(); - expect(find.byType(TestBox), findsNWidgets(1)); - expect(findOverflowNextButton(), findsOneWidget); + expect(findOverflowNextButton(), findsNothing); expect(findOverflowBackButton(), findsOneWidget); // 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(); expect(find.byType(TestBox), findsNWidgets(7)); expect(findOverflowNextButton(), findsOneWidget); @@ -157,7 +208,7 @@ void main() { expect(findOverflowBackButton(), findsNothing); // 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(); // With the back button, only six children fit on this page. expect(find.byType(TestBox), findsNWidgets(6)); @@ -165,21 +216,21 @@ void main() { expect(findOverflowBackButton(), findsOneWidget); // 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(); expect(find.byType(TestBox), findsNWidgets(1)); - expect(findOverflowNextButton(), findsOneWidget); + expect(findOverflowNextButton(), findsNothing); expect(findOverflowBackButton(), findsOneWidget); // 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(); expect(find.byType(TestBox), findsNWidgets(6)); expect(findOverflowNextButton(), findsOneWidget); expect(findOverflowBackButton(), findsOneWidget); // 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(); expect(find.byType(TestBox), findsNWidgets(7)); expect(findOverflowNextButton(), findsOneWidget); @@ -345,13 +396,12 @@ void main() { final Finder buttonFinder = find.byType(CupertinoButton); expect(buttonFinder, findsOneWidget); - final Finder decorationFinder = find.descendant( + final Finder textFinder = find.descendant( of: find.byType(CupertinoButton), - matching: find.byType(DecoratedBox) + matching: find.byType(Text) ); - expect(decorationFinder, findsOneWidget); - final DecoratedBox decoratedBox = tester.widget(decorationFinder); - final BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration; + expect(textFinder, findsOneWidget); + final Text text = tester.widget(textFinder); // Theme brightness is preferred, otherwise MediaQuery brightness is // used. If both are null, defaults to light. @@ -363,10 +413,10 @@ void main() { } expect( - boxDecoration.color!.value, + text.style!.color!.value, effectiveBrightness == Brightness.dark - ? _kToolbarBackgroundColor.darkColor.value - : _kToolbarBackgroundColor.color.value, + ? _kToolbarTextColor.darkColor.value + : _kToolbarTextColor.color.value, ); }, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web. } @@ -419,7 +469,7 @@ void main() { of: find.byType(CupertinoTextSelectionToolbar), matching: find.byType(DecoratedBox), ); - expect(finder, findsNWidgets(2)); + expect(finder, findsOneWidget); DecoratedBox decoratedBox = tester.widget(finder.first); BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration; List? shadows = boxDecoration.boxShadow;