mirror of
https://github.com/flutter/flutter
synced 2024-10-12 11:12:54 +00:00
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>|
This commit is contained in:
parent
3e66c86ae4
commit
a90c33fd61
|
@ -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<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
|
||||
// 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<Widget> 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<Element>.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<RenderBox, ToolbarItemsParentData>, RenderBoxContainerDefaultsMixin<RenderBox, ToolbarItemsParentData> {
|
||||
_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 {
|
||||
|
|
|
@ -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<StatefulWidget> createState() => _CupertinoTextSelectionToolbarButtonState();
|
||||
}
|
||||
|
||||
return CupertinoButton(
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1545,7 +1545,7 @@ void main() {
|
|||
|
||||
Text text = tester.widget<Text>(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<Text>(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),
|
||||
),
|
||||
|
|
|
@ -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<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) {
|
||||
return points.map<TextSelectionPoint>((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>{ 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>{ 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>{ 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>{ 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>{ TargetPlatform.iOS }),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<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 {
|
||||
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<BoxShadow>? shadows = boxDecoration.boxShadow;
|
||||
|
|
Loading…
Reference in a new issue