Selection area right click behavior should match native (#128224)

This change updates `SelectableRegion`s right-click gesture to match native platform behavior.

Before: Right-click gesture selects word at position and opens context menu (All Platforms)
After: 
- Linux, toggles context menu on/off, and collapses selection when click was not on an active selection (uncollapsed).
- Windows, Android, Fuchsia, shows context menu at right-clicked position (unless the click is at an active selection).
- macOS, toggles the context menu if right click was at the same position as the previous / or selects word at position and opens context menu.
- iOS, selects word at position and opens context menu.

This change also prevents the `copy` menu button from being shown when there is a collapsed selection (nothing to copy).

Fixes #117561
This commit is contained in:
Renzo Olivares 2023-06-21 12:32:04 -07:00 committed by GitHub
parent c40baf47c5
commit b36ef583fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 503 additions and 12 deletions

View file

@ -157,6 +157,7 @@ class _RenderSelectableAdapter extends RenderProxyBox with Selectable, Selection
hasContent: true,
startSelectionPoint: isReversed ? secondSelectionPoint : firstSelectionPoint,
endSelectionPoint: isReversed ? firstSelectionPoint : secondSelectionPoint,
selectionRects: <Rect>[selectionRect],
);
}
}

View file

@ -1335,6 +1335,14 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
: paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd));
final bool flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection);
final Matrix4 paragraphToFragmentTransform = getTransformToParagraph()..invert();
final TextSelection selection = TextSelection(
baseOffset: selectionStart,
extentOffset: selectionEnd,
);
final List<Rect> selectionRects = <Rect>[];
for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) {
selectionRects.add(textBox.toRect());
}
return SelectionGeometry(
startSelectionPoint: SelectionPoint(
localPosition: MatrixUtils.transformPoint(paragraphToFragmentTransform, startOffsetInParagraphCoordinates),
@ -1346,6 +1354,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
lineHeight: paragraph._textPainter.preferredLineHeight,
handleType: flipHandles ? TextSelectionHandleType.left : TextSelectionHandleType.right,
),
selectionRects: selectionRects,
status: _textSelectionStart!.offset == _textSelectionEnd!.offset
? SelectionStatus.collapsed
: SelectionStatus.uncollapsed,

View file

@ -576,8 +576,8 @@ enum SelectionStatus {
/// The geometry of the current selection.
///
/// This includes details such as the locations of the selection start and end,
/// line height, etc. This information is used for drawing selection controls
/// for mobile platforms.
/// line height, the rects that encompass the selection, etc. This information
/// is used for drawing selection controls for mobile platforms.
///
/// The positions in geometry are in local coordinates of the [SelectionHandler]
/// or [Selectable].
@ -590,6 +590,7 @@ class SelectionGeometry {
const SelectionGeometry({
this.startSelectionPoint,
this.endSelectionPoint,
this.selectionRects = const <Rect>[],
required this.status,
required this.hasContent,
}) : assert((startSelectionPoint == null && endSelectionPoint == null) || status != SelectionStatus.none);
@ -627,6 +628,10 @@ class SelectionGeometry {
/// The status of ongoing selection in the [Selectable] or [SelectionHandler].
final SelectionStatus status;
/// The rects in the local coordinates of the containing [Selectable] that
/// represent the selection if there is any.
final List<Rect> selectionRects;
/// Whether there is any selectable content in the [Selectable] or
/// [SelectionHandler].
final bool hasContent;
@ -638,12 +643,14 @@ class SelectionGeometry {
SelectionGeometry copyWith({
SelectionPoint? startSelectionPoint,
SelectionPoint? endSelectionPoint,
List<Rect>? selectionRects,
SelectionStatus? status,
bool? hasContent,
}) {
return SelectionGeometry(
startSelectionPoint: startSelectionPoint ?? this.startSelectionPoint,
endSelectionPoint: endSelectionPoint ?? this.endSelectionPoint,
selectionRects: selectionRects ?? this.selectionRects,
status: status ?? this.status,
hasContent: hasContent ?? this.hasContent,
);
@ -660,6 +667,7 @@ class SelectionGeometry {
return other is SelectionGeometry
&& other.startSelectionPoint == startSelectionPoint
&& other.endSelectionPoint == endSelectionPoint
&& other.selectionRects == selectionRects
&& other.status == status
&& other.hasContent == hasContent;
}
@ -669,6 +677,7 @@ class SelectionGeometry {
return Object.hash(
startSelectionPoint,
endSelectionPoint,
selectionRects,
status,
hasContent,
);

View file

@ -263,7 +263,7 @@ class SelectableRegion extends StatefulWidget {
required final VoidCallback onCopy,
required final VoidCallback onSelectAll,
}) {
final bool canCopy = selectionGeometry.hasSelection;
final bool canCopy = selectionGeometry.status == SelectionStatus.uncollapsed;
final bool canSelectAll = selectionGeometry.hasContent;
// Determine which buttons will appear so that the order and total number is
@ -489,12 +489,62 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
_updateSelectedContentIfNeeded();
}
bool _positionIsOnActiveSelection({required Offset globalPosition}) {
for (final Rect selectionRect in _selectionDelegate.value.selectionRects) {
final Matrix4 transform = _selectable!.getTransformTo(null);
final Rect globalRect = MatrixUtils.transformRect(transform, selectionRect);
if (globalRect.contains(globalPosition)) {
return true;
}
}
return false;
}
void _handleRightClickDown(TapDownDetails details) {
final Offset? previousSecondaryTapDownPosition = lastSecondaryTapDownPosition;
final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
lastSecondaryTapDownPosition = details.globalPosition;
widget.focusNode.requestFocus();
_selectWordAt(offset: details.globalPosition);
_showHandles();
_showToolbar(location: details.globalPosition);
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.windows:
// If lastSecondaryTapDownPosition is within the current selection then
// keep the current selection, if not then collapse it.
final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition);
if (!lastSecondaryTapDownPositionWasOnActiveSelection) {
_selectStartTo(offset: lastSecondaryTapDownPosition!);
_selectEndTo(offset: lastSecondaryTapDownPosition!);
}
_showHandles();
_showToolbar(location: lastSecondaryTapDownPosition);
case TargetPlatform.iOS:
_selectWordAt(offset: lastSecondaryTapDownPosition!);
_showHandles();
_showToolbar(location: lastSecondaryTapDownPosition);
case TargetPlatform.macOS:
if (previousSecondaryTapDownPosition == lastSecondaryTapDownPosition && toolbarIsVisible) {
hideToolbar();
return;
}
_selectWordAt(offset: lastSecondaryTapDownPosition!);
_showHandles();
_showToolbar(location: lastSecondaryTapDownPosition);
case TargetPlatform.linux:
if (toolbarIsVisible) {
hideToolbar();
return;
}
// If lastSecondaryTapDownPosition is within the current selection then
// keep the current selection, if not then collapse it.
final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition);
if (!lastSecondaryTapDownPositionWasOnActiveSelection) {
_selectStartTo(offset: lastSecondaryTapDownPosition!);
_selectEndTo(offset: lastSecondaryTapDownPosition!);
}
_showHandles();
_showToolbar(location: lastSecondaryTapDownPosition);
}
_updateSelectedContentIfNeeded();
}
@ -1770,9 +1820,30 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
}
}
// Need to collect selection rects from selectables ranging from the
// currentSelectionStartIndex to the currentSelectionEndIndex.
final List<Rect> selectionRects = <Rect>[];
final Rect? drawableArea = hasSize ? Rect
.fromLTWH(0, 0, containerSize.width, containerSize.height) : null;
for (int index = currentSelectionStartIndex; index <= currentSelectionEndIndex; index++) {
final List<Rect> currSelectableSelectionRects = selectables[index].value.selectionRects;
final List<Rect> selectionRectsWithinDrawableArea = currSelectableSelectionRects.map((Rect selectionRect) {
final Matrix4 transform = getTransformFrom(selectables[index]);
final Rect localRect = MatrixUtils.transformRect(transform, selectionRect);
if (drawableArea != null) {
return drawableArea.intersect(localRect);
}
return localRect;
}).where((Rect selectionRect) {
return selectionRect.isFinite && !selectionRect.isEmpty;
}).toList();
selectionRects.addAll(selectionRectsWithinDrawableArea);
}
return SelectionGeometry(
startSelectionPoint: startPoint,
endSelectionPoint: endPoint,
selectionRects: selectionRects,
status: startGeometry != endGeometry
? SelectionStatus.uncollapsed
: startGeometry.status,

View file

@ -562,15 +562,13 @@ class TextSelectionOverlay {
/// Whether the handles are currently visible.
bool get handlesAreVisible => _selectionOverlay._handles != null && handlesVisible;
/// Whether the toolbar is currently visible.
///
/// Includes both the text selection toolbar and the spell check menu.
/// {@macro flutter.widgets.SelectionOverlay.toolbarIsVisible}
///
/// See also:
///
/// * [spellCheckToolbarIsVisible], which is only whether the spell check menu
/// specifically is visible.
bool get toolbarIsVisible => _selectionOverlay._toolbarIsVisible;
bool get toolbarIsVisible => _selectionOverlay.toolbarIsVisible;
/// Whether the magnifier is currently visible.
bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown;
@ -984,7 +982,12 @@ class SelectionOverlay {
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details}
final TextMagnifierConfiguration magnifierConfiguration;
bool get _toolbarIsVisible {
/// {@template flutter.widgets.SelectionOverlay.toolbarIsVisible}
/// Whether the toolbar is currently visible.
///
/// Includes both the text selection toolbar and the spell check menu.
/// {@endtemplate}
bool get toolbarIsVisible {
return selectionControls is TextSelectionHandleControls
? _contextMenuController.isShown || _spellCheckToolbarController.isShown
: _toolbar != null || _spellCheckToolbarController.isShown;
@ -1001,7 +1004,7 @@ class SelectionOverlay {
/// [MagnifierController.shown].
/// {@endtemplate}
void showMagnifier(MagnifierInfo initialMagnifierInfo) {
if (_toolbarIsVisible) {
if (toolbarIsVisible) {
hideToolbar();
}

View file

@ -560,6 +560,403 @@ void main() {
await gesture.up();
});
testWidgets(
'right-click mouse can select word at position on Apple platforms',
(WidgetTester tester) async {
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
final UniqueKey toolbarKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: FocusNode(),
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonTypes = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
.toSet();
return SizedBox.shrink(key: toolbarKey);
},
child: const Center(
child: Text('How are you'),
),
),
),
);
expect(buttonTypes.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(gesture.removePointer);
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1));
await tester.pump();
expect(paragraph.selections.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets(
'right-click mouse at the same position as previous right-click toggles the context menu on macOS',
(WidgetTester tester) async {
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
final UniqueKey toolbarKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: FocusNode(),
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonTypes = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
.toSet();
return SizedBox.shrink(key: toolbarKey);
},
child: const Center(
child: Text('How are you'),
),
),
),
);
expect(buttonTypes.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(gesture.removePointer);
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 2));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
await gesture.up();
await tester.pump();
// Right-click at same position will toggle the context menu off.
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsNothing);
await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 8, extentOffset: 11));
await gesture.up();
await tester.pump();
// Right-click at same position will toggle the context menu off.
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsNothing);
await gesture.down(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
await gesture.up();
await tester.pump();
expect(buttonTypes, contains(ContextMenuButtonType.copy));
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1));
await tester.pump();
expect(paragraph.selections.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
},
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets(
'right-click mouse shows the context menu at position on Android, Fucshia, and Windows',
(WidgetTester tester) async {
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
final UniqueKey toolbarKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: FocusNode(),
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonTypes = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
.toSet();
return SizedBox.shrink(key: toolbarKey);
},
child: const Center(
child: Text('How are you'),
),
),
),
);
expect(buttonTypes.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(gesture.removePointer);
await tester.pump();
// Selection is collapsed so none is reported.
expect(paragraph.selections.isEmpty, true);
await gesture.up();
await tester.pump();
expect(buttonTypes.length, 1);
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections.isEmpty, true);
await gesture.up();
await tester.pump();
expect(buttonTypes.length, 1);
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump();
expect(paragraph.selections.isEmpty, true);
await gesture.up();
await tester.pump();
expect(buttonTypes.length, 1);
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1));
await tester.pump();
expect(paragraph.selections.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
// Create an uncollapsed selection by dragging.
final TestGesture dragGesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse);
addTearDown(dragGesture.removePointer);
await tester.pump();
await dragGesture.moveTo(textOffsetToPosition(paragraph, 5));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
await dragGesture.up();
await tester.pump();
// Right click on previous selection should not collapse the selection.
await gesture.down(textOffsetToPosition(paragraph, 2));
await tester.pump();
await gesture.up();
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.byKey(toolbarKey), findsOneWidget);
// Right click anywhere outside previous selection should collapse the
// selection.
await gesture.down(textOffsetToPosition(paragraph, 7));
await tester.pump();
await gesture.up();
await tester.pump();
expect(paragraph.selections.isEmpty, true);
expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1));
await tester.pump();
expect(paragraph.selections.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets(
'right-click mouse toggles the context menu on Linux',
(WidgetTester tester) async {
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
final UniqueKey toolbarKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: FocusNode(),
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonTypes = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
.toSet();
return SizedBox.shrink(key: toolbarKey);
},
child: const Center(
child: Text('How are you'),
),
),
),
);
expect(buttonTypes.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(gesture.removePointer);
await tester.pump();
// Selection is collapsed so none is reported.
expect(paragraph.selections.isEmpty, true);
await gesture.up();
await tester.pump();
// Context menu toggled on.
expect(buttonTypes.length, 1);
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections.isEmpty, true);
await gesture.up();
await tester.pump();
// Context menu toggled off.
expect(find.byKey(toolbarKey), findsNothing);
await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump();
expect(paragraph.selections.isEmpty, true);
await gesture.up();
await tester.pump();
// Context menu toggled on.
expect(buttonTypes.length, 1);
expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1));
await tester.pump();
expect(paragraph.selections.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final TestGesture dragGesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse);
addTearDown(dragGesture.removePointer);
await tester.pump();
await dragGesture.moveTo(textOffsetToPosition(paragraph, 5));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
await dragGesture.up();
await tester.pump();
// Right click on previous selection should not collapse the selection.
await gesture.down(textOffsetToPosition(paragraph, 2));
await tester.pump();
await gesture.up();
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.byKey(toolbarKey), findsOneWidget);
// Right click anywhere outside previous selection should first toggle the context
// menu off.
await gesture.down(textOffsetToPosition(paragraph, 7));
await tester.pump();
await gesture.up();
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
expect(find.byKey(toolbarKey), findsNothing);
// Right click again should collapse the selection and toggle the context
// menu on.
await gesture.down(textOffsetToPosition(paragraph, 7));
await tester.pump();
await gesture.up();
await tester.pump();
expect(paragraph.selections.isEmpty, true);
expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1));
await tester.pump();
expect(paragraph.selections.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
},
variant: TargetPlatformVariant.only(TargetPlatform.linux),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgets('can copy a selection made with the mouse', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
@ -808,6 +1205,7 @@ void main() {
// Should select "Hello".
expect(paragraph.selections[0], const TextSelection(baseOffset: 124, extentOffset: 129));
},
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);