mirror of
https://github.com/flutter/flutter
synced 2024-10-05 15:59:49 +00:00
Enable SelectionArea double tap/triple tap gesture support for mobile platforms (#149295)
This change enables double tap / triple tap support in SelectionArea for mobile platforms: Android / Fuchsia: - On native, these platforms allow for double tap / double tap + drag to select word-by-word. - On web using touch, these platforms only support double tap to select word. - On web and native using a mouse, these platforms support double click / double click + drag to select word-by-word, and triple click / triple click + drag to select paragraph-by-paragraph. iOS: - On native, these platforms allow for double tap / double tap + drag to select word-by-word. - On web using touch, these platforms do not support double tap/triple tap gestures. - On web using touch, these platforms allow support double tap + drag gestures. - On web and native using a mouse, these platforms support double click / double click + drag to select word-by-word, and triple click / triple click + drag to select paragraph-by-paragraph. Part of: https://github.com/flutter/flutter/issues/129583
This commit is contained in:
parent
9056c0b192
commit
20459dda0d
|
@ -354,9 +354,6 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
Orientation? _lastOrientation;
|
||||
SelectedContent? _lastSelectedContent;
|
||||
|
||||
/// {@macro flutter.rendering.RenderEditable.lastSecondaryTapDownPosition}
|
||||
Offset? lastSecondaryTapDownPosition;
|
||||
|
||||
/// The [SelectionOverlay] that is currently visible on the screen.
|
||||
///
|
||||
/// Can be null if there is no visible [SelectionOverlay].
|
||||
|
@ -375,25 +372,10 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
widget.focusNode.addListener(_handleFocusChanged);
|
||||
_initMouseGestureRecognizer();
|
||||
_initTouchGestureRecognizer();
|
||||
// Taps and right clicks.
|
||||
// Right clicks.
|
||||
_gestureRecognizers[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||
() => TapGestureRecognizer(debugOwner: this),
|
||||
(TapGestureRecognizer instance) {
|
||||
instance.onTapUp = (TapUpDetails details) {
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS && _positionIsOnActiveSelection(globalPosition: details.globalPosition)) {
|
||||
// On iOS when the tap occurs on the previous selection, instead of
|
||||
// moving the selection, the context menu will be toggled.
|
||||
final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
|
||||
if (toolbarIsVisible) {
|
||||
hideToolbar(false);
|
||||
} else {
|
||||
_showToolbar(location: details.globalPosition);
|
||||
}
|
||||
} else {
|
||||
hideToolbar();
|
||||
_collapseSelectionAt(offset: details.globalPosition);
|
||||
}
|
||||
};
|
||||
instance.onSecondaryTapDown = _handleRightClickDown;
|
||||
},
|
||||
);
|
||||
|
@ -487,6 +469,27 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
|
||||
// gestures.
|
||||
|
||||
// The position of the most recent secondary tap down event on this
|
||||
// SelectableRegion.
|
||||
Offset? _lastSecondaryTapDownPosition;
|
||||
|
||||
// The device kind for the pointer of the most recent tap down event on this
|
||||
// SelectableRegion.
|
||||
PointerDeviceKind? _lastPointerDeviceKind;
|
||||
|
||||
static bool _isPrecisePointerDevice(PointerDeviceKind pointerDeviceKind) {
|
||||
switch (pointerDeviceKind) {
|
||||
case PointerDeviceKind.mouse:
|
||||
return true;
|
||||
case PointerDeviceKind.trackpad:
|
||||
case PointerDeviceKind.stylus:
|
||||
case PointerDeviceKind.invertedStylus:
|
||||
case PointerDeviceKind.touch:
|
||||
case PointerDeviceKind.unknown:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Converts the details.consecutiveTapCount from a TapAndDrag*Details object,
|
||||
// which can grow to be infinitely large, to a value between 1 and the supported
|
||||
// max consecutive tap count. The value that the raw count is converted to is
|
||||
|
@ -494,11 +497,24 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
//
|
||||
// This method should be used in all instances when details.consecutiveTapCount
|
||||
// would be used.
|
||||
static int _getEffectiveConsecutiveTapCount(int rawCount) {
|
||||
const int maxConsecutiveTap = 3;
|
||||
int _getEffectiveConsecutiveTapCount(int rawCount) {
|
||||
int maxConsecutiveTap = 3;
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
if (_lastPointerDeviceKind != null && _lastPointerDeviceKind != PointerDeviceKind.mouse) {
|
||||
// When the pointer device kind is not precise like a mouse, native
|
||||
// Android resets the tap count at 2. For example, this is so the
|
||||
// selection can collapse on the third tap.
|
||||
maxConsecutiveTap = 2;
|
||||
}
|
||||
// From observation, these platforms reset their tap count to 0 when
|
||||
// the number of consecutive taps exceeds the max consecutive tap supported.
|
||||
// For example on native Android, when going past a triple click,
|
||||
// on the fourth click the selection is moved to the precise click
|
||||
// position, on the fifth click the word at the position is selected, and
|
||||
// on the sixth click the paragraph at the position is selected.
|
||||
return rawCount <= maxConsecutiveTap ? rawCount : (rawCount % maxConsecutiveTap == 0 ? maxConsecutiveTap : rawCount % maxConsecutiveTap);
|
||||
case TargetPlatform.linux:
|
||||
// From observation, these platforms reset their tap count to 0 when
|
||||
// the number of consecutive taps exceeds the max consecutive tap supported.
|
||||
|
@ -510,7 +526,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
// From observation, these platforms either hold their tap count at the max
|
||||
// From observation, these platforms hold their tap count at the max
|
||||
// consecutive tap supported. For example on macOS, when going past a triple
|
||||
// click, the selection should be retained at the paragraph that was first
|
||||
// selected on triple click.
|
||||
|
@ -519,19 +535,27 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
}
|
||||
|
||||
void _initMouseGestureRecognizer() {
|
||||
_gestureRecognizers[TapAndPanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
|
||||
() => TapAndPanGestureRecognizer(debugOwner:this, supportedDevices: <PointerDeviceKind>{ PointerDeviceKind.mouse }),
|
||||
(TapAndPanGestureRecognizer instance) {
|
||||
instance
|
||||
..onTapDown = _startNewMouseSelectionGesture
|
||||
..onTapUp = _handleMouseTapUp
|
||||
..onDragStart = _handleMouseDragStart
|
||||
..onDragUpdate = _handleMouseDragUpdate
|
||||
..onDragEnd = _handleMouseDragEnd
|
||||
..onCancel = _clearSelection
|
||||
..dragStartBehavior = DragStartBehavior.down;
|
||||
},
|
||||
);
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
_gestureRecognizers[TapAndPanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
|
||||
() => TapAndPanGestureRecognizer(debugOwner:this),
|
||||
(TapAndPanGestureRecognizer instance) {
|
||||
instance
|
||||
..onTapDown = _startNewMouseSelectionGesture
|
||||
..onTapUp = _handleMouseTapUp
|
||||
..onDragStart = _handleMouseDragStart
|
||||
..onDragUpdate = _handleMouseDragUpdate
|
||||
..onDragEnd = _handleMouseDragEnd
|
||||
..onCancel = _clearSelection
|
||||
..dragStartBehavior = DragStartBehavior.down;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _initTouchGestureRecognizer() {
|
||||
|
@ -546,26 +570,59 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
);
|
||||
}
|
||||
|
||||
Offset? _doubleTapOffset;
|
||||
void _startNewMouseSelectionGesture(TapDragDownDetails details) {
|
||||
_lastPointerDeviceKind = details.kind;
|
||||
switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
|
||||
case 1:
|
||||
widget.focusNode.requestFocus();
|
||||
hideToolbar();
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.iOS:
|
||||
// On mobile platforms the selection is set on tap up.
|
||||
// On mobile platforms the selection is set on tap up for the first
|
||||
// tap.
|
||||
break;
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
hideToolbar();
|
||||
_collapseSelectionAt(offset: details.globalPosition);
|
||||
}
|
||||
case 2:
|
||||
_selectWordAt(offset: details.globalPosition);
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.iOS:
|
||||
if (kIsWeb && details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
|
||||
// Double tap on iOS web triggers when a drag begins after the double tap.
|
||||
_doubleTapOffset = details.globalPosition;
|
||||
break;
|
||||
}
|
||||
_selectWordAt(offset: details.globalPosition);
|
||||
if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
|
||||
_showHandles();
|
||||
}
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
_selectWordAt(offset: details.globalPosition);
|
||||
}
|
||||
case 3:
|
||||
_selectParagraphAt(offset: details.globalPosition);
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.iOS:
|
||||
if (details.kind != null && _isPrecisePointerDevice(details.kind!)) {
|
||||
// Triple tap on static text is only supported on mobile
|
||||
// platforms using a precise pointer device.
|
||||
_selectParagraphAt(offset: details.globalPosition);
|
||||
}
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
_selectParagraphAt(offset: details.globalPosition);
|
||||
}
|
||||
}
|
||||
_updateSelectedContentIfNeeded();
|
||||
}
|
||||
|
@ -573,6 +630,10 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
void _handleMouseDragStart(TapDragStartDetails details) {
|
||||
switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
|
||||
case 1:
|
||||
if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
|
||||
// Drag to select is only enabled with a precise pointer device.
|
||||
return;
|
||||
}
|
||||
_selectStartTo(offset: details.globalPosition);
|
||||
}
|
||||
_updateSelectedContentIfNeeded();
|
||||
|
@ -581,27 +642,101 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
void _handleMouseDragUpdate(TapDragUpdateDetails details) {
|
||||
switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
|
||||
case 1:
|
||||
if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
|
||||
// Drag to select is only enabled with a precise pointer device.
|
||||
return;
|
||||
}
|
||||
_selectEndTo(offset: details.globalPosition, continuous: true);
|
||||
case 2:
|
||||
_selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word);
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
// Double tap + drag is only supported on Android when using a precise
|
||||
// pointer device or when not on the web.
|
||||
if (!kIsWeb || details.kind != null && _isPrecisePointerDevice(details.kind!)) {
|
||||
_selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word);
|
||||
}
|
||||
case TargetPlatform.iOS:
|
||||
if (kIsWeb && details.kind != null && !_isPrecisePointerDevice(details.kind!) && _doubleTapOffset != null) {
|
||||
// On iOS web a double tap does not select the word at the position,
|
||||
// until the drag has begun.
|
||||
_selectWordAt(offset: _doubleTapOffset!);
|
||||
_doubleTapOffset = null;
|
||||
}
|
||||
_selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word);
|
||||
if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
|
||||
_showHandles();
|
||||
}
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
_selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word);
|
||||
}
|
||||
case 3:
|
||||
_selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.paragraph);
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.iOS:
|
||||
// Triple tap + drag is only supported on mobile devices when using
|
||||
// a precise pointer device.
|
||||
if (details.kind != null && _isPrecisePointerDevice(details.kind!)) {
|
||||
_selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.paragraph);
|
||||
}
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
_selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.paragraph);
|
||||
}
|
||||
}
|
||||
_updateSelectedContentIfNeeded();
|
||||
}
|
||||
|
||||
void _handleMouseDragEnd(TapDragEndDetails details) {
|
||||
final bool isPointerPrecise = _lastPointerDeviceKind != null && _lastPointerDeviceKind == PointerDeviceKind.mouse;
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
if (!isPointerPrecise) {
|
||||
// On Android, a drag gesture will only show the selection overlay when
|
||||
// the drag has finished and the pointer device kind is not precise.
|
||||
_showHandles();
|
||||
_showToolbar();
|
||||
}
|
||||
case TargetPlatform.iOS:
|
||||
if (!isPointerPrecise) {
|
||||
// On iOS, a drag gesture will only show the selection toolbar when
|
||||
// the drag has finished and the pointer device kind is not precise.
|
||||
_showToolbar();
|
||||
}
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
// The selection overlay is not shown on desktop platforms after a drag.
|
||||
break;
|
||||
}
|
||||
_finalizeSelection();
|
||||
_updateSelectedContentIfNeeded();
|
||||
}
|
||||
|
||||
void _handleMouseTapUp(TapDragUpDetails details) {
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS && _positionIsOnActiveSelection(globalPosition: details.globalPosition)) {
|
||||
// On iOS when the tap occurs on the previous selection, instead of
|
||||
// moving the selection, the context menu will be toggled.
|
||||
final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
|
||||
if (toolbarIsVisible) {
|
||||
hideToolbar(false);
|
||||
} else {
|
||||
_showToolbar();
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
|
||||
case 1:
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.iOS:
|
||||
hideToolbar();
|
||||
_collapseSelectionAt(offset: details.globalPosition);
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
|
@ -609,6 +744,30 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
// On desktop platforms the selection is set on tap down.
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
final bool isPointerPrecise = _isPrecisePointerDevice(details.kind);
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
if (!isPointerPrecise) {
|
||||
// On Android, a double tap will only show the selection overlay after
|
||||
// the following tap up when the pointer device kind is not precise.
|
||||
_showHandles();
|
||||
_showToolbar();
|
||||
}
|
||||
case TargetPlatform.iOS:
|
||||
if (!isPointerPrecise) {
|
||||
// On iOS, a double tap will only show the selection toolbar after
|
||||
// the following tap up when the pointer device kind is not precise.
|
||||
_showToolbar();
|
||||
}
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
// The selection overlay is not shown on desktop platforms
|
||||
// on a double click.
|
||||
break;
|
||||
}
|
||||
}
|
||||
_updateSelectedContentIfNeeded();
|
||||
}
|
||||
|
@ -659,47 +818,47 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
}
|
||||
|
||||
void _handleRightClickDown(TapDownDetails details) {
|
||||
final Offset? previousSecondaryTapDownPosition = lastSecondaryTapDownPosition;
|
||||
final Offset? previousSecondaryTapDownPosition = _lastSecondaryTapDownPosition;
|
||||
final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
|
||||
lastSecondaryTapDownPosition = details.globalPosition;
|
||||
_lastSecondaryTapDownPosition = details.globalPosition;
|
||||
widget.focusNode.requestFocus();
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.windows:
|
||||
// If lastSecondaryTapDownPosition is within the current selection then
|
||||
// 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) {
|
||||
_collapseSelectionAt(offset: lastSecondaryTapDownPosition!);
|
||||
_collapseSelectionAt(offset: _lastSecondaryTapDownPosition!);
|
||||
}
|
||||
_showHandles();
|
||||
_showToolbar(location: lastSecondaryTapDownPosition);
|
||||
_showToolbar(location: _lastSecondaryTapDownPosition);
|
||||
case TargetPlatform.iOS:
|
||||
_selectWordAt(offset: lastSecondaryTapDownPosition!);
|
||||
_selectWordAt(offset: _lastSecondaryTapDownPosition!);
|
||||
_showHandles();
|
||||
_showToolbar(location: lastSecondaryTapDownPosition);
|
||||
_showToolbar(location: _lastSecondaryTapDownPosition);
|
||||
case TargetPlatform.macOS:
|
||||
if (previousSecondaryTapDownPosition == lastSecondaryTapDownPosition && toolbarIsVisible) {
|
||||
if (previousSecondaryTapDownPosition == _lastSecondaryTapDownPosition && toolbarIsVisible) {
|
||||
hideToolbar();
|
||||
return;
|
||||
}
|
||||
_selectWordAt(offset: lastSecondaryTapDownPosition!);
|
||||
_selectWordAt(offset: _lastSecondaryTapDownPosition!);
|
||||
_showHandles();
|
||||
_showToolbar(location: lastSecondaryTapDownPosition);
|
||||
_showToolbar(location: _lastSecondaryTapDownPosition);
|
||||
case TargetPlatform.linux:
|
||||
if (toolbarIsVisible) {
|
||||
hideToolbar();
|
||||
return;
|
||||
}
|
||||
// If lastSecondaryTapDownPosition is within the current selection then
|
||||
// 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) {
|
||||
_collapseSelectionAt(offset: lastSecondaryTapDownPosition!);
|
||||
_collapseSelectionAt(offset: _lastSecondaryTapDownPosition!);
|
||||
}
|
||||
_showHandles();
|
||||
_showToolbar(location: lastSecondaryTapDownPosition);
|
||||
_showToolbar(location: _lastSecondaryTapDownPosition);
|
||||
}
|
||||
_updateSelectedContentIfNeeded();
|
||||
}
|
||||
|
@ -1177,9 +1336,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
/// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s
|
||||
/// for the default context menu buttons.
|
||||
TextSelectionToolbarAnchors get contextMenuAnchors {
|
||||
if (lastSecondaryTapDownPosition != null) {
|
||||
if (_lastSecondaryTapDownPosition != null) {
|
||||
return TextSelectionToolbarAnchors(
|
||||
primaryAnchor: lastSecondaryTapDownPosition!,
|
||||
primaryAnchor: _lastSecondaryTapDownPosition!,
|
||||
);
|
||||
}
|
||||
final RenderBox renderBox = context.findRenderObject()! as RenderBox;
|
||||
|
|
|
@ -110,6 +110,37 @@ void main() {
|
|||
expect(selectionEvent.globalPosition, const Offset(200.0, 200.0));
|
||||
});
|
||||
|
||||
testWidgets('touch double click sends select-word event', (WidgetTester tester) async {
|
||||
final UniqueKey spy = UniqueKey();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: focusNode,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: SelectionSpy(key: spy),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy));
|
||||
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
renderSelectionSpy.events.clear();
|
||||
await gesture.down(const Offset(200.0, 200.0));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
expect(renderSelectionSpy.events.length, 1);
|
||||
expect(renderSelectionSpy.events[0], isA<SelectWordSelectionEvent>());
|
||||
final SelectWordSelectionEvent selectionEvent = renderSelectionSpy.events[0] as SelectWordSelectionEvent;
|
||||
expect(selectionEvent.globalPosition, const Offset(200.0, 200.0));
|
||||
});
|
||||
|
||||
testWidgets('Does not crash when using Navigator pages', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/119776
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
@ -596,6 +627,502 @@ void main() {
|
|||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
}, variant: TargetPlatformVariant.desktop());
|
||||
|
||||
testWidgets('touch can select word-by-word on double tap drag on mobile platforms', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: focusNode,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: const Center(
|
||||
child: Text('How are you'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
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));
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 2));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 3));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 7));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 8));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 8));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
||||
|
||||
// Check backward selection.
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
// Start a new double-click drag.
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
await gesture.down(textOffsetToPosition(paragraph, 5));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
expect(paragraph.selections.isEmpty, isFalse);
|
||||
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 5));
|
||||
await tester.pump(kDoubleTapTimeout);
|
||||
|
||||
// Double-click.
|
||||
await gesture.down(textOffsetToPosition(paragraph, 5));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
await gesture.down(textOffsetToPosition(paragraph, 5));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
|
||||
|
||||
// Selecting across line should select to the end.
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 11));
|
||||
await gesture.up();
|
||||
},
|
||||
variant: TargetPlatformVariant.mobile(),
|
||||
skip: kIsWeb, // [intended] Web does not support double tap + drag gestures on all of the tested platforms.
|
||||
);
|
||||
|
||||
testWidgets('touch can select multiple widgets on double tap drag on mobile platforms', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: focusNode,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: const Column(
|
||||
children: <Widget>[
|
||||
Text('How are you?'),
|
||||
Text('Good, and you?'),
|
||||
Text('Fine, thank you.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2));
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph1, 2));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
||||
await tester.pump();
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
||||
|
||||
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
||||
// Should select the rest of paragraph 1.
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
||||
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
||||
|
||||
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
||||
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
||||
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
||||
|
||||
await gesture.up();
|
||||
},
|
||||
variant: TargetPlatformVariant.mobile(),
|
||||
skip: kIsWeb, // [intended] Web does not support double tap + drag gestures on all of the tested platforms.
|
||||
);
|
||||
|
||||
testWidgets('touch can select multiple widgets on double tap drag and return to origin word on mobile platforms', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: focusNode,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: const Column(
|
||||
children: <Widget>[
|
||||
Text('How are you?'),
|
||||
Text('Good, and you?'),
|
||||
Text('Fine, thank you.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2));
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph1, 2));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
||||
await tester.pump();
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
||||
|
||||
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
||||
// Should select the rest of paragraph 1.
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
||||
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
||||
|
||||
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
||||
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
||||
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
||||
// Should clear the selection on paragraph 3.
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
||||
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
|
||||
expect(paragraph3.selections.isEmpty, isTrue);
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
|
||||
// Should clear the selection on paragraph 2.
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
||||
expect(paragraph2.selections.isEmpty, isTrue);
|
||||
expect(paragraph3.selections.isEmpty, isTrue);
|
||||
|
||||
await gesture.up();
|
||||
},
|
||||
variant: TargetPlatformVariant.mobile(),
|
||||
skip: kIsWeb, // [intended] Web does not support double tap + drag gestures on all of the tested platforms.
|
||||
);
|
||||
|
||||
testWidgets('touch can reverse selection across multiple widgets on double tap drag on mobile platforms', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: focusNode,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: const Column(
|
||||
children: <Widget>[
|
||||
Text('How are you?'),
|
||||
Text('Good, and you?'),
|
||||
Text('Fine, thank you.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph3, 10));
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph3, 10));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph3.selections[0], const TextSelection(baseOffset: 6, extentOffset: 11));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph3, 4));
|
||||
await tester.pump();
|
||||
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 4));
|
||||
|
||||
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
|
||||
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 0));
|
||||
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 5));
|
||||
|
||||
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
||||
expect(paragraph3.selections[0], const TextSelection(baseOffset: 11, extentOffset: 0));
|
||||
expect(paragraph2.selections[0], const TextSelection(baseOffset: 14, extentOffset: 0));
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 12, extentOffset: 4));
|
||||
|
||||
await gesture.up();
|
||||
},
|
||||
variant: TargetPlatformVariant.mobile(),
|
||||
skip: kIsWeb, // [intended] Web does not support double tap + drag gestures on all of the tested platforms.
|
||||
);
|
||||
|
||||
testWidgets('touch cannot triple tap or triple tap drag on Android and iOS', (WidgetTester tester) async {
|
||||
const String longText = 'Hello world this is some long piece of text '
|
||||
'that will represent a long paragraph, when triple clicking this block '
|
||||
'of text all of it will be selected.\n'
|
||||
'This will be the start of a new line. When triple clicking this block '
|
||||
'of text all of it should be selected.';
|
||||
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: focusNode,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: const Center(
|
||||
child: Text(longText),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text(longText), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 2));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 2));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 150));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 155));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 257));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 170));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 257));
|
||||
|
||||
// Check backward selection.
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 150));
|
||||
|
||||
// Start a new triple-click drag.
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||||
await gesture.down(textOffsetToPosition(paragraph, 151));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture.up();
|
||||
expect(paragraph.selections.isNotEmpty, isTrue);
|
||||
expect(paragraph.selections.length, 1);
|
||||
expect(paragraph.selections.first, const TextSelection.collapsed(offset: 151));
|
||||
await tester.pump(kDoubleTapTimeout);
|
||||
|
||||
// Triple-click.
|
||||
await gesture.down(textOffsetToPosition(paragraph, 151));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
await gesture.down(textOffsetToPosition(paragraph, 151));
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
await gesture.down(textOffsetToPosition(paragraph, 151));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 150, extentOffset: 257));
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Reset selection.
|
||||
await tester.tapAt(textOffsetToPosition(paragraph, 0));
|
||||
await tester.pumpAndSettle(kDoubleTapTimeout);
|
||||
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 0));
|
||||
|
||||
// Trying to triple-click with a touch gesture should not work.
|
||||
final TestGesture touchGesture = await tester.startGesture(textOffsetToPosition(paragraph, 2));
|
||||
addTearDown(touchGesture.removePointer);
|
||||
await tester.pump();
|
||||
await touchGesture.up();
|
||||
await tester.pump();
|
||||
|
||||
await touchGesture.down(textOffsetToPosition(paragraph, 2));
|
||||
await tester.pump();
|
||||
await touchGesture.up();
|
||||
await tester.pump();
|
||||
|
||||
await touchGesture.down(textOffsetToPosition(paragraph, 2));
|
||||
await tester.pump();
|
||||
await touchGesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
// The selection is collapsed on Android because the max consecutive tap count
|
||||
// on native Android is 2 when the pointer device kind is not precise like
|
||||
// for a touch.
|
||||
//
|
||||
// On iOS the selection is maintained because the tap occured on the active
|
||||
// selection.
|
||||
expect(paragraph.selections[0], defaultTargetPlatform == TargetPlatform.iOS ? const TextSelection(baseOffset: 0, extentOffset: 5) : const TextSelection.collapsed(offset: 2));
|
||||
},
|
||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }),
|
||||
skip: kIsWeb, // [intended] Web does not support double tap + drag gestures on all of the tested platforms.
|
||||
);
|
||||
|
||||
testWidgets('touch cannot select word-by-word on double tap drag when on Android web', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: focusNode,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: const Center(
|
||||
child: Text('How are you'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
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));
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
await gesture.down(textOffsetToPosition(paragraph, 2));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
// Dragging should not change the selection.
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 3));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 7));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 8));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
|
||||
// Check backward selection.
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
skip: !kIsWeb, // [intended] This test verifies web behavior.
|
||||
);
|
||||
|
||||
testWidgets('touch can double tap + drag on iOS web', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: focusNode,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: const Center(
|
||||
child: Text('How are you'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
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));
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
|
||||
|
||||
// A double tap should not change the selection.
|
||||
await gesture.down(textOffsetToPosition(paragraph, 2));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
|
||||
|
||||
// Dragging should change the selection.
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 3));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 7));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 8));
|
||||
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 8));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
|
||||
|
||||
// Check backward selection.
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
||||
skip: true, // https://github.com/flutter/flutter/issues/125582.
|
||||
);
|
||||
|
||||
testWidgets('touch cannot double tap on iOS web', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: focusNode,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: const Center(
|
||||
child: Text('How are you'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
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));
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
|
||||
|
||||
// A double tap should not change the selection.
|
||||
await gesture.down(textOffsetToPosition(paragraph, 2));
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
||||
skip: !kIsWeb, // [intended] This test verifies web behavior.
|
||||
);
|
||||
|
||||
testWidgets('mouse can select single text on desktop platforms', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
addTearDown(focusNode.dispose);
|
||||
|
|
Loading…
Reference in a new issue