Unverified Commit 21ad7122 authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Implement SelectionArea single click/tap gestures (#132682)

This change collapses the selection at the clicked/tapped location on single click down for desktop platforms, and on single click/tap up for mobile platforms to match native.

This is a change from how `SelectionArea` previously worked. Before this change a single click down would clear the selection. From observing a native browser it looks like when tapping on static text the selection is not cleared but collapsed. A user can still attain the selection from static text using the `window.getSelection` API.

https://jsfiddle.net/juepasn3/11/ You can try this demo out here to observe this behavior yourself. When clicking on static text the selection will change.

This change also allows `Paragraph.selections` to return selections that are collapsed. This for testing purposes to confirm where the selection has been collapsed.

Partially fixes: #129583
parent 80fb7bd2
...@@ -24,6 +24,9 @@ void main() { ...@@ -24,6 +24,9 @@ void main() {
// Right clicking the Text in the SelectionArea shows the custom context // Right clicking the Text in the SelectionArea shows the custom context
// menu. // menu.
final TestGesture primaryMouseButtonGesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
final TestGesture gesture = await tester.startGesture( final TestGesture gesture = await tester.startGesture(
tester.getCenter(find.text(example.text)), tester.getCenter(find.text(example.text)),
kind: PointerDeviceKind.mouse, kind: PointerDeviceKind.mouse,
...@@ -37,7 +40,9 @@ void main() { ...@@ -37,7 +40,9 @@ void main() {
expect(find.text('Print'), findsOneWidget); expect(find.text('Print'), findsOneWidget);
// Tap to dismiss. // Tap to dismiss.
await tester.tapAt(tester.getCenter(find.byType(Scaffold))); await primaryMouseButtonGesture.down(tester.getCenter(find.byType(Scaffold)));
await tester.pump();
await primaryMouseButtonGesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
......
...@@ -351,8 +351,7 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo ...@@ -351,8 +351,7 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
final List<TextSelection> results = <TextSelection>[]; final List<TextSelection> results = <TextSelection>[];
for (final _SelectableFragment fragment in _lastSelectableFragments!) { for (final _SelectableFragment fragment in _lastSelectableFragments!) {
if (fragment._textSelectionStart != null && if (fragment._textSelectionStart != null &&
fragment._textSelectionEnd != null && fragment._textSelectionEnd != null) {
fragment._textSelectionStart!.offset != fragment._textSelectionEnd!.offset) {
results.add( results.add(
TextSelection( TextSelection(
baseOffset: fragment._textSelectionStart!.offset, baseOffset: fragment._textSelectionStart!.offset,
...@@ -1309,9 +1308,9 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo ...@@ -1309,9 +1308,9 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
/// A continuous, selectable piece of paragraph. /// A continuous, selectable piece of paragraph.
/// ///
/// Since the selections in [PlaceHolderSpan] are handled independently in its /// Since the selections in [PlaceholderSpan] are handled independently in its
/// subtree, a selection in [RenderParagraph] can't continue across a /// subtree, a selection in [RenderParagraph] can't continue across a
/// [PlaceHolderSpan]. The [RenderParagraph] splits itself on [PlaceHolderSpan] /// [PlaceholderSpan]. The [RenderParagraph] splits itself on [PlaceholderSpan]
/// to create multiple `_SelectableFragment`s so that they can be selected /// to create multiple `_SelectableFragment`s so that they can be selected
/// separately. /// separately.
class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutMetrics { class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutMetrics {
...@@ -1720,7 +1719,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1720,7 +1719,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
_selectableContainsOriginWord = true; _selectableContainsOriginWord = true;
final TextPosition position = paragraph.getPositionForOffset(paragraph.globalToLocal(globalPosition)); final TextPosition position = paragraph.getPositionForOffset(paragraph.globalToLocal(globalPosition));
if (_positionIsWithinCurrentSelection(position)) { if (_positionIsWithinCurrentSelection(position) && _textSelectionStart != _textSelectionEnd) {
return SelectionResult.end; return SelectionResult.end;
} }
final _WordBoundaryRecord wordBoundary = _getWordBoundaryAtPosition(position); final _WordBoundaryRecord wordBoundary = _getWordBoundaryAtPosition(position);
......
...@@ -352,7 +352,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -352,7 +352,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
_showToolbar(location: details.globalPosition); _showToolbar(location: details.globalPosition);
} }
} else { } else {
_clearSelection(); hideToolbar();
_collapseSelectionAt(offset: details.globalPosition);
} }
}; };
instance.onSecondaryTapDown = _handleRightClickDown; instance.onSecondaryTapDown = _handleRightClickDown;
...@@ -472,6 +473,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -472,6 +473,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
(TapAndPanGestureRecognizer instance) { (TapAndPanGestureRecognizer instance) {
instance instance
..onTapDown = _startNewMouseSelectionGesture ..onTapDown = _startNewMouseSelectionGesture
..onTapUp = _handleMouseTapUp
..onDragStart = _handleMouseDragStart ..onDragStart = _handleMouseDragStart
..onDragUpdate = _handleMouseDragUpdate ..onDragUpdate = _handleMouseDragUpdate
..onDragEnd = _handleMouseDragEnd ..onDragEnd = _handleMouseDragEnd
...@@ -498,7 +500,17 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -498,7 +500,17 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
case 1: case 1:
widget.focusNode.requestFocus(); widget.focusNode.requestFocus();
hideToolbar(); hideToolbar();
_clearSelection(); switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
// On mobile platforms the selection is set on tap up.
break;
case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.windows:
_collapseSelectionAt(offset: details.globalPosition);
}
case 2: case 2:
_selectWordAt(offset: details.globalPosition); _selectWordAt(offset: details.globalPosition);
} }
...@@ -528,6 +540,24 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -528,6 +540,24 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
_updateSelectedContentIfNeeded(); _updateSelectedContentIfNeeded();
} }
void _handleMouseTapUp(TapDragUpDetails details) {
switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
case 1:
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
_collapseSelectionAt(offset: details.globalPosition);
case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.windows:
// On desktop platforms the selection is set on tap down.
break;
}
}
_updateSelectedContentIfNeeded();
}
void _updateSelectedContentIfNeeded() { void _updateSelectedContentIfNeeded() {
if (_lastSelectedContent?.plainText != _selectable?.getSelectedContent()?.plainText) { if (_lastSelectedContent?.plainText != _selectable?.getSelectedContent()?.plainText) {
_lastSelectedContent = _selectable?.getSelectedContent(); _lastSelectedContent = _selectable?.getSelectedContent();
...@@ -586,8 +616,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -586,8 +616,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
// keep the current selection, if not then collapse it. // keep the current selection, if not then collapse it.
final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition); final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition);
if (!lastSecondaryTapDownPositionWasOnActiveSelection) { if (!lastSecondaryTapDownPositionWasOnActiveSelection) {
_selectStartTo(offset: lastSecondaryTapDownPosition!); _collapseSelectionAt(offset: lastSecondaryTapDownPosition!);
_selectEndTo(offset: lastSecondaryTapDownPosition!);
} }
_showHandles(); _showHandles();
_showToolbar(location: lastSecondaryTapDownPosition); _showToolbar(location: lastSecondaryTapDownPosition);
...@@ -612,8 +641,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -612,8 +641,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
// keep the current selection, if not then collapse it. // keep the current selection, if not then collapse it.
final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition); final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition);
if (!lastSecondaryTapDownPositionWasOnActiveSelection) { if (!lastSecondaryTapDownPositionWasOnActiveSelection) {
_selectStartTo(offset: lastSecondaryTapDownPosition!); _collapseSelectionAt(offset: lastSecondaryTapDownPosition!);
_selectEndTo(offset: lastSecondaryTapDownPosition!);
} }
_showHandles(); _showHandles();
_showToolbar(location: lastSecondaryTapDownPosition); _showToolbar(location: lastSecondaryTapDownPosition);
...@@ -925,8 +953,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -925,8 +953,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// See also: /// See also:
/// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectStartTo], which sets or updates selection start edge.
/// * [_finalizeSelection], which stops the `continuous` updates. /// * [_finalizeSelection], which stops the `continuous` updates.
/// * [_clearSelection], which clear the ongoing selection. /// * [_clearSelection], which clears the ongoing selection.
/// * [_selectWordAt], which selects a whole word at the location. /// * [_selectWordAt], which selects a whole word at the location.
/// * [_collapseSelectionAt], which collapses the selection at the location.
/// * [selectAll], which selects the entire content. /// * [selectAll], which selects the entire content.
void _selectEndTo({required Offset offset, bool continuous = false, TextGranularity? textGranularity}) { void _selectEndTo({required Offset offset, bool continuous = false, TextGranularity? textGranularity}) {
if (!continuous) { if (!continuous) {
...@@ -964,8 +993,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -964,8 +993,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// See also: /// See also:
/// * [_selectEndTo], which sets or updates selection end edge. /// * [_selectEndTo], which sets or updates selection end edge.
/// * [_finalizeSelection], which stops the `continuous` updates. /// * [_finalizeSelection], which stops the `continuous` updates.
/// * [_clearSelection], which clear the ongoing selection. /// * [_clearSelection], which clears the ongoing selection.
/// * [_selectWordAt], which selects a whole word at the location. /// * [_selectWordAt], which selects a whole word at the location.
/// * [_collapseSelectionAt], which collapses the selection at the location.
/// * [selectAll], which selects the entire content. /// * [selectAll], which selects the entire content.
void _selectStartTo({required Offset offset, bool continuous = false, TextGranularity? textGranularity}) { void _selectStartTo({required Offset offset, bool continuous = false, TextGranularity? textGranularity}) {
if (!continuous) { if (!continuous) {
...@@ -978,6 +1008,20 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -978,6 +1008,20 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
} }
} }
/// Collapses the selection at the given `offset` location.
///
/// See also:
/// * [_selectStartTo], which sets or updates selection start edge.
/// * [_selectEndTo], which sets or updates selection end edge.
/// * [_finalizeSelection], which stops the `continuous` updates.
/// * [_clearSelection], which clears the ongoing selection.
/// * [_selectWordAt], which selects a whole word at the location.
/// * [selectAll], which selects the entire content.
void _collapseSelectionAt({required Offset offset}) {
_selectStartTo(offset: offset);
_selectEndTo(offset: offset);
}
/// Selects a whole word at the `offset` location. /// Selects a whole word at the `offset` location.
/// ///
/// If the whole word is already in the current selection, selection won't /// If the whole word is already in the current selection, selection won't
...@@ -991,7 +1035,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -991,7 +1035,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectStartTo], which sets or updates selection start edge.
/// * [_selectEndTo], which sets or updates selection end edge. /// * [_selectEndTo], which sets or updates selection end edge.
/// * [_finalizeSelection], which stops the `continuous` updates. /// * [_finalizeSelection], which stops the `continuous` updates.
/// * [_clearSelection], which clear the ongoing selection. /// * [_clearSelection], which clears the ongoing selection.
/// * [_collapseSelectionAt], which collapses the selection at the location.
/// * [selectAll], which selects the entire content. /// * [selectAll], which selects the entire content.
void _selectWordAt({required Offset offset}) { void _selectWordAt({required Offset offset}) {
// There may be other selection ongoing. // There may be other selection ongoing.
...@@ -2047,6 +2092,34 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai ...@@ -2047,6 +2092,34 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
); );
} }
// Clears the selection on all selectables not in the range of
// currentSelectionStartIndex..currentSelectionEndIndex.
//
// If one of the edges does not exist, then this method will clear the selection
// in all selectables except the existing edge.
//
// If neither of the edges exist this method immediately returns.
void _flushInactiveSelections() {
if (currentSelectionStartIndex == -1 && currentSelectionEndIndex == -1) {
return;
}
if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
final int skipIndex = currentSelectionStartIndex == -1 ? currentSelectionEndIndex : currentSelectionStartIndex;
selectables
.where((Selectable target) => target != selectables[skipIndex])
.forEach((Selectable target) => dispatchSelectionEventToChild(target, const ClearSelectionEvent()));
return;
}
final int skipStart = min(currentSelectionStartIndex, currentSelectionEndIndex);
final int skipEnd = max(currentSelectionStartIndex, currentSelectionEndIndex);
for (int index = 0; index < selectables.length; index += 1) {
if (index >= skipStart && index <= skipEnd) {
continue;
}
dispatchSelectionEventToChild(selectables[index], const ClearSelectionEvent());
}
}
/// Selects all contents of all selectables. /// Selects all contents of all selectables.
@protected @protected
SelectionResult handleSelectAll(SelectAllSelectionEvent event) { SelectionResult handleSelectAll(SelectAllSelectionEvent event) {
...@@ -2323,6 +2396,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai ...@@ -2323,6 +2396,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
} else { } else {
currentSelectionStartIndex = newIndex; currentSelectionStartIndex = newIndex;
} }
_flushInactiveSelections();
// The result can only be null if the loop went through the entire list // The result can only be null if the loop went through the entire list
// without any of the selection returned end or previous. In this case, the // without any of the selection returned end or previous. In this case, the
// caller of this method needs to find the next selectable in their list. // caller of this method needs to find the next selectable in their list.
...@@ -2345,13 +2419,39 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai ...@@ -2345,13 +2419,39 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
return true; return true;
}()); }());
SelectionResult? finalResult; SelectionResult? finalResult;
int newIndex = isEnd ? currentSelectionEndIndex : currentSelectionStartIndex; // Determines if the edge being adjusted is within the current viewport.
// - If so, we begin the search for the new selection edge position at the
// currentSelectionEndIndex/currentSelectionStartIndex.
// - If not, we attempt to locate the new selection edge starting from
// the opposite end.
// - If neither edge is in the current viewport, the search for the new
// selection edge position begins at 0.
//
// This can happen when there is a scrollable child and the edge being adjusted
// has been scrolled out of view.
final bool isCurrentEdgeWithinViewport = isEnd ? _selectionGeometry.endSelectionPoint != null : _selectionGeometry.startSelectionPoint != null;
final bool isOppositeEdgeWithinViewport = isEnd ? _selectionGeometry.startSelectionPoint != null : _selectionGeometry.endSelectionPoint != null;
int newIndex = switch ((isEnd, isCurrentEdgeWithinViewport, isOppositeEdgeWithinViewport)) {
(true, true, true) => currentSelectionEndIndex,
(true, true, false) => currentSelectionEndIndex,
(true, false, true) => currentSelectionStartIndex,
(true, false, false) => 0,
(false, true, true) => currentSelectionStartIndex,
(false, true, false) => currentSelectionStartIndex,
(false, false, true) => currentSelectionEndIndex,
(false, false, false) => 0,
};
bool? forward; bool? forward;
late SelectionResult currentSelectableResult; late SelectionResult currentSelectableResult;
// This loop sends the selection event to the // This loop sends the selection event to one of the following to determine
// currentSelectionEndIndex/currentSelectionStartIndex to determine the // the direction of the search.
// direction of the search. If the result is `SelectionResult.next`, this // - currentSelectionEndIndex/currentSelectionStartIndex if the current edge
// loop look backward. Otherwise, it looks forward. // is in the current viewport.
// - The opposite edge index if the current edge is not in the current viewport.
// - Index 0 if neither edge is in the current viewport.
//
// If the result is `SelectionResult.next`, this loop look backward.
// Otherwise, it looks forward.
// //
// The terminate condition are: // The terminate condition are:
// 1. the selectable returns end, pending, none. // 1. the selectable returns end, pending, none.
...@@ -2391,6 +2491,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai ...@@ -2391,6 +2491,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
} else { } else {
currentSelectionStartIndex = newIndex; currentSelectionStartIndex = newIndex;
} }
_flushInactiveSelections();
return finalResult!; return finalResult!;
} }
} }
......
...@@ -224,8 +224,14 @@ void main() { ...@@ -224,8 +224,14 @@ void main() {
// Backwards selection. // Backwards selection.
await gesture.down(textOffsetToPosition(paragraph, 3)); await gesture.down(textOffsetToPosition(paragraph, 3));
await tester.pumpAndSettle(); await tester.pump();
expect(content, isNull); await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(content, isNotNull);
expect(content!.plainText, '');
await gesture.down(textOffsetToPosition(paragraph, 3));
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph, 0)); await gesture.moveTo(textOffsetToPosition(paragraph, 0));
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
......
...@@ -978,7 +978,8 @@ void main() { ...@@ -978,7 +978,8 @@ void main() {
granularity: TextGranularity.word, granularity: TextGranularity.word,
), ),
); );
expect(paragraph.selections.length, 0); // how []are you expect(paragraph.selections.length, 1); // how []are you
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 4));
// Equivalent to sending shift + alt + arrow-left. // Equivalent to sending shift + alt + arrow-left.
registrar.selectables[0].dispatchSelectionEvent( registrar.selectables[0].dispatchSelectionEvent(
......
...@@ -281,7 +281,7 @@ void main() { ...@@ -281,7 +281,7 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgetsWithLeakTracking('mouse selection always cancels previous selection', (WidgetTester tester) async { testWidgets('mouse single-click selection collapses the selection', (WidgetTester tester) async {
final UniqueKey spy = UniqueKey(); final UniqueKey spy = UniqueKey();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose); addTearDown(focusNode.dispose);
...@@ -300,9 +300,14 @@ void main() { ...@@ -300,9 +300,14 @@ void main() {
final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy)); final RenderSelectionSpy renderSelectionSpy = tester.renderObject<RenderSelectionSpy>(find.byKey(spy));
final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0), kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(renderSelectionSpy.events.length, 1); expect(renderSelectionSpy.events.length, 2);
expect(renderSelectionSpy.events[0], isA<ClearSelectionEvent>()); expect(renderSelectionSpy.events[0], isA<SelectionEdgeUpdateEvent>());
expect((renderSelectionSpy.events[0] as SelectionEdgeUpdateEvent).type, SelectionEventType.startEdgeUpdate);
expect(renderSelectionSpy.events[1], isA<SelectionEdgeUpdateEvent>());
expect((renderSelectionSpy.events[1] as SelectionEdgeUpdateEvent).type, SelectionEventType.endEdgeUpdate);
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/102410. }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/102410.
testWidgetsWithLeakTracking('touch long press sends select-word event', (WidgetTester tester) async { testWidgetsWithLeakTracking('touch long press sends select-word event', (WidgetTester tester) async {
...@@ -474,7 +479,7 @@ void main() { ...@@ -474,7 +479,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
await gesture.up(); await gesture.up();
expect( expect(
renderSelectionSpy.events.every((SelectionEvent element) => element is ClearSelectionEvent), renderSelectionSpy.events.every((SelectionEvent element) => element is SelectionEdgeUpdateEvent),
isTrue, isTrue,
); );
}); });
...@@ -543,7 +548,7 @@ void main() { ...@@ -543,7 +548,7 @@ void main() {
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
group('SelectionArea integration', () { group('SelectionArea integration', () {
testWidgetsWithLeakTracking('mouse can select single text', (WidgetTester tester) async { testWidgets('mouse can select single text on desktop platforms', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose); addTearDown(focusNode.dispose);
...@@ -574,13 +579,17 @@ void main() { ...@@ -574,13 +579,17 @@ void main() {
// Check backward selection. // Check backward selection.
await gesture.moveTo(textOffsetToPosition(paragraph, 1)); await gesture.moveTo(textOffsetToPosition(paragraph, 1));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 1)); expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 1));
// Start a new drag. // Start a new drag.
await gesture.up(); await gesture.up();
await tester.pumpAndSettle();
await gesture.down(textOffsetToPosition(paragraph, 5)); await gesture.down(textOffsetToPosition(paragraph, 5));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(paragraph.selections.isEmpty, isTrue); expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 5));
// Selecting across line should select to the end. // Selecting across line should select to the end.
await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0)); await gesture.moveTo(textOffsetToPosition(paragraph, 5) + const Offset(0.0, 200.0));
...@@ -588,7 +597,60 @@ void main() { ...@@ -588,7 +597,60 @@ void main() {
expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 11)); expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 11));
await gesture.up(); await gesture.up();
}); }, variant: TargetPlatformVariant.desktop());
testWidgetsWithLeakTracking('mouse can select single text 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), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph, 4));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 4));
await gesture.moveTo(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 6));
// Check backward selection.
await gesture.moveTo(textOffsetToPosition(paragraph, 1));
await tester.pump();
expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 1));
// Start a new drag.
await gesture.up();
await tester.pumpAndSettle();
await gesture.down(textOffsetToPosition(paragraph, 5));
await tester.pumpAndSettle();
await gesture.moveTo(textOffsetToPosition(paragraph, 6));
await tester.pump();
expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection(baseOffset: 5, extentOffset: 6));
// 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: 5, extentOffset: 11));
await gesture.up();
}, variant: TargetPlatformVariant.mobile());
testWidgetsWithLeakTracking('mouse can select word-by-word on double click drag', (WidgetTester tester) async { testWidgetsWithLeakTracking('mouse can select word-by-word on double click drag', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -643,7 +705,8 @@ void main() { ...@@ -643,7 +705,8 @@ void main() {
await gesture.down(textOffsetToPosition(paragraph, 5)); await gesture.down(textOffsetToPosition(paragraph, 5));
await tester.pump(); await tester.pump();
await gesture.up(); await gesture.up();
expect(paragraph.selections.isEmpty, isTrue); expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 5));
await tester.pump(kDoubleTapTimeout); await tester.pump(kDoubleTapTimeout);
// Double-click. // Double-click.
...@@ -761,13 +824,13 @@ void main() { ...@@ -761,13 +824,13 @@ void main() {
// Should clear the selection on paragraph 3. // Should clear the selection on paragraph 3.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12)); expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6)); expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
expect(paragraph3.selections.isEmpty, true); expect(paragraph3.selections.isEmpty, isTrue);
await gesture.moveTo(textOffsetToPosition(paragraph1, 4)); await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
// Should clear the selection on paragraph 2. // Should clear the selection on paragraph 2.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7)); expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
expect(paragraph2.selections.isEmpty, true); expect(paragraph2.selections.isEmpty, isTrue);
expect(paragraph3.selections.isEmpty, true); expect(paragraph3.selections.isEmpty, isTrue);
await gesture.up(); await gesture.up();
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582. }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
...@@ -863,6 +926,52 @@ void main() { ...@@ -863,6 +926,52 @@ void main() {
await gesture.up(); await gesture.up();
}); });
testWidgetsWithLeakTracking('collapsing selection should clear selection of all other selectables', (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), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(paragraph1.selections[0], const TextSelection.collapsed(offset: 2));
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
await gesture.down(textOffsetToPosition(paragraph2, 5));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(paragraph1.selections.isEmpty, isTrue);
expect(paragraph2.selections[0], const TextSelection.collapsed(offset: 5));
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
await gesture.down(textOffsetToPosition(paragraph3, 13));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(paragraph1.selections.isEmpty, isTrue);
expect(paragraph2.selections.isEmpty, isTrue);
expect(paragraph3.selections[0], const TextSelection.collapsed(offset: 13));
});
testWidgetsWithLeakTracking('mouse can work with disabled container', (WidgetTester tester) async { testWidgetsWithLeakTracking('mouse can work with disabled container', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose); addTearDown(focusNode.dispose);
...@@ -1108,10 +1217,11 @@ void main() { ...@@ -1108,10 +1217,11 @@ void main() {
expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget); expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection. // Collapse selection.
await tester.tapAt(textOffsetToPosition(paragraph, 9)); await tester.tapAt(textOffsetToPosition(paragraph, 9));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); expect(paragraph.selections.isEmpty, isFalse);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9));
expect(find.byKey(toolbarKey), findsNothing); expect(find.byKey(toolbarKey), findsNothing);
}, },
variant: TargetPlatformVariant.only(TargetPlatform.iOS), variant: TargetPlatformVariant.only(TargetPlatform.iOS),
...@@ -1151,7 +1261,9 @@ void main() { ...@@ -1151,7 +1261,9 @@ void main() {
expect(find.byKey(toolbarKey), findsNothing); expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText))); final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton); final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(primaryMouseButtonGesture.removePointer);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump(); await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
...@@ -1185,10 +1297,14 @@ void main() { ...@@ -1185,10 +1297,14 @@ void main() {
expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget); expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection. // Collapse selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1)); await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); await primaryMouseButtonGesture.up();
await tester.pumpAndSettle();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing); expect(find.byKey(toolbarKey), findsNothing);
}, },
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
...@@ -1229,6 +1345,8 @@ void main() { ...@@ -1229,6 +1345,8 @@ void main() {
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText))); 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); final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(primaryMouseButtonGesture.removePointer);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump(); await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
...@@ -1286,10 +1404,14 @@ void main() { ...@@ -1286,10 +1404,14 @@ void main() {
expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget); expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection. // Collapse selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1)); await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); await primaryMouseButtonGesture.up();
await tester.pumpAndSettle();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing); expect(find.byKey(toolbarKey), findsNothing);
}, },
variant: TargetPlatformVariant.only(TargetPlatform.macOS), variant: TargetPlatformVariant.only(TargetPlatform.macOS),
...@@ -1330,21 +1452,24 @@ void main() { ...@@ -1330,21 +1452,24 @@ void main() {
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText))); 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); final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(primaryMouseButtonGesture.removePointer);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump(); await tester.pump();
// Selection is collapsed so none is reported.
expect(paragraph.selections.isEmpty, true);
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
expect(buttonTypes.length, 1); expect(buttonTypes.length, 1);
expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget); expect(find.byKey(toolbarKey), findsOneWidget);
await gesture.down(textOffsetToPosition(paragraph, 6)); await gesture.down(textOffsetToPosition(paragraph, 6));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 6));
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
...@@ -1355,7 +1480,8 @@ void main() { ...@@ -1355,7 +1480,8 @@ void main() {
await gesture.down(textOffsetToPosition(paragraph, 9)); await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9));
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
...@@ -1364,20 +1490,23 @@ void main() { ...@@ -1364,20 +1490,23 @@ void main() {
expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget); expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection. // Collapse selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1)); await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); await primaryMouseButtonGesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing); expect(find.byKey(toolbarKey), findsNothing);
// Create an uncollapsed selection by dragging. // Create an uncollapsed selection by dragging.
final TestGesture dragGesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse); await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 0));
addTearDown(dragGesture.removePointer);
await tester.pump(); await tester.pump();
await dragGesture.moveTo(textOffsetToPosition(paragraph, 5)); await primaryMouseButtonGesture.moveTo(textOffsetToPosition(paragraph, 5));
await tester.pump(); await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5)); expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
await dragGesture.up(); await primaryMouseButtonGesture.up();
await tester.pump(); await tester.pump();
// Right click on previous selection should not collapse the selection. // Right click on previous selection should not collapse the selection.
...@@ -1394,13 +1523,18 @@ void main() { ...@@ -1394,13 +1523,18 @@ void main() {
await tester.pump(); await tester.pump();
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 7));
expect(find.byKey(toolbarKey), findsOneWidget); expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection. // Collapse selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1)); await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); await primaryMouseButtonGesture.up();
await tester.pumpAndSettle();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing); expect(find.byKey(toolbarKey), findsNothing);
}, },
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }),
...@@ -1441,13 +1575,15 @@ void main() { ...@@ -1441,13 +1575,15 @@ void main() {
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText))); 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); final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
final TestGesture primaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(primaryMouseButtonGesture.removePointer);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump(); await tester.pump();
// Selection is collapsed so none is reported.
expect(paragraph.selections.isEmpty, true);
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
// Context menu toggled on. // Context menu toggled on.
expect(buttonTypes.length, 1); expect(buttonTypes.length, 1);
...@@ -1456,17 +1592,18 @@ void main() { ...@@ -1456,17 +1592,18 @@ void main() {
await gesture.down(textOffsetToPosition(paragraph, 6)); await gesture.down(textOffsetToPosition(paragraph, 6));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true);
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 2));
// Context menu toggled off. // Context menu toggled off. Selection remains the same.
expect(find.byKey(toolbarKey), findsNothing); expect(find.byKey(toolbarKey), findsNothing);
await gesture.down(textOffsetToPosition(paragraph, 9)); await gesture.down(textOffsetToPosition(paragraph, 9));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 9));
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
...@@ -1476,19 +1613,22 @@ void main() { ...@@ -1476,19 +1613,22 @@ void main() {
expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); expect(buttonTypes, contains(ContextMenuButtonType.selectAll));
expect(find.byKey(toolbarKey), findsOneWidget); expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection. // Collapse selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1)); await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); await primaryMouseButtonGesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing); expect(find.byKey(toolbarKey), findsNothing);
final TestGesture dragGesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse); await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 0));
addTearDown(dragGesture.removePointer);
await tester.pump(); await tester.pump();
await dragGesture.moveTo(textOffsetToPosition(paragraph, 5)); await primaryMouseButtonGesture.moveTo(textOffsetToPosition(paragraph, 5));
await tester.pump(); await tester.pump();
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5)); expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5));
await dragGesture.up(); await primaryMouseButtonGesture.up();
await tester.pump(); await tester.pump();
// Right click on previous selection should not collapse the selection. // Right click on previous selection should not collapse the selection.
...@@ -1514,13 +1654,18 @@ void main() { ...@@ -1514,13 +1654,18 @@ void main() {
await tester.pump(); await tester.pump();
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 7));
expect(find.byKey(toolbarKey), findsOneWidget); expect(find.byKey(toolbarKey), findsOneWidget);
// Clear selection. // Collapse selection.
await tester.tapAt(textOffsetToPosition(paragraph, 1)); await primaryMouseButtonGesture.down(textOffsetToPosition(paragraph, 1));
await tester.pump(); await tester.pump();
expect(paragraph.selections.isEmpty, true); await primaryMouseButtonGesture.up();
await tester.pumpAndSettle();
// Selection is collapsed.
expect(paragraph.selections.isEmpty, false);
expect(paragraph.selections[0], const TextSelection.collapsed(offset: 1));
expect(find.byKey(toolbarKey), findsNothing); expect(find.byKey(toolbarKey), findsNothing);
}, },
variant: TargetPlatformVariant.only(TargetPlatform.linux), variant: TargetPlatformVariant.only(TargetPlatform.linux),
...@@ -2414,7 +2559,9 @@ void main() { ...@@ -2414,7 +2559,9 @@ void main() {
expect(paragraph1.selections.length, 1); expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2); expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12); expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 0); expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 0);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control)); await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control));
await tester.pump(); await tester.pump();
...@@ -2424,7 +2571,9 @@ void main() { ...@@ -2424,7 +2571,9 @@ void main() {
expect(paragraph1.selections.length, 1); expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2); expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 8); expect(paragraph1.selections[0].end, 8);
expect(paragraph2.selections.length, 0); expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 0);
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
testWidgetsWithLeakTracking('can use keyboard to granularly extend selection - line', (WidgetTester tester) async { testWidgetsWithLeakTracking('can use keyboard to granularly extend selection - line', (WidgetTester tester) async {
...@@ -2507,7 +2656,9 @@ void main() { ...@@ -2507,7 +2656,9 @@ void main() {
expect(paragraph1.selections.length, 1); expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2); expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 12); expect(paragraph1.selections[0].end, 12);
expect(paragraph2.selections.length, 0); expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 0);
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, meta: meta)); await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, meta: meta));
await tester.pump(); await tester.pump();
...@@ -2594,8 +2745,12 @@ void main() { ...@@ -2594,8 +2745,12 @@ void main() {
expect(paragraph1.selections.length, 1); expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 0); expect(paragraph1.selections[0].start, 0);
expect(paragraph1.selections[0].end, 2); expect(paragraph1.selections[0].end, 2);
expect(paragraph2.selections.length, 0); expect(paragraph2.selections.length, 1);
expect(paragraph3.selections.length, 0); expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 0);
expect(paragraph3.selections.length, 1);
expect(paragraph3.selections[0].start, 0);
expect(paragraph3.selections[0].end, 0);
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
testWidgetsWithLeakTracking('can use keyboard to directionally extend selection', (WidgetTester tester) async { testWidgetsWithLeakTracking('can use keyboard to directionally extend selection', (WidgetTester tester) async {
...@@ -2666,7 +2821,9 @@ void main() { ...@@ -2666,7 +2821,9 @@ void main() {
expect(paragraph2.selections.length, 1); expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 2); expect(paragraph2.selections[0].start, 2);
expect(paragraph2.selections[0].end, 6); expect(paragraph2.selections[0].end, 6);
expect(paragraph3.selections.length, 0); expect(paragraph3.selections.length, 1);
expect(paragraph3.selections[0].start, 0);
expect(paragraph3.selections[0].end, 0);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
await tester.pump(); await tester.pump();
...@@ -3017,8 +3174,14 @@ void main() { ...@@ -3017,8 +3174,14 @@ void main() {
// Backwards selection. // Backwards selection.
await mouseGesture.down(textOffsetToPosition(paragraph, 3)); await mouseGesture.down(textOffsetToPosition(paragraph, 3));
await tester.pumpAndSettle(); await tester.pump();
expect(content, isNull); await mouseGesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(content, isNotNull);
expect(content!.plainText, '');
await mouseGesture.down(textOffsetToPosition(paragraph, 3));
await tester.pump();
await mouseGesture.moveTo(textOffsetToPosition(paragraph, 0)); await mouseGesture.moveTo(textOffsetToPosition(paragraph, 0));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -3045,9 +3208,10 @@ void main() { ...@@ -3045,9 +3208,10 @@ void main() {
// Called on tap. // Called on tap.
await mouseGesture.down(textOffsetToPosition(paragraph, 0)); await mouseGesture.down(textOffsetToPosition(paragraph, 0));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(content, isNull);
await mouseGesture.up(); await mouseGesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle(kDoubleTapTimeout);
expect(content, isNotNull);
expect(content!.plainText, '');
// With touch gestures. // With touch gestures.
...@@ -3224,7 +3388,7 @@ void main() { ...@@ -3224,7 +3388,7 @@ void main() {
expect(paragraph2.selections.length, 1); expect(paragraph2.selections.length, 1);
expect(paragraph2.selections[0].start, 0); expect(paragraph2.selections[0].start, 0);
expect(paragraph2.selections[0].end, 8); expect(paragraph2.selections[0].end, 8);
expect(paragraph3.selections.length, 0); expect(paragraph3.selections.length, 1);
expect(content, isNotNull); expect(content, isNotNull);
expect(content!.plainText, 'w are you?Good, an'); expect(content!.plainText, 'w are you?Good, an');
...@@ -3233,8 +3397,8 @@ void main() { ...@@ -3233,8 +3397,8 @@ void main() {
expect(paragraph1.selections.length, 1); expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2); expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 7); expect(paragraph1.selections[0].end, 7);
expect(paragraph2.selections.length, 0); expect(paragraph2.selections.length, 1);
expect(paragraph3.selections.length, 0); expect(paragraph3.selections.length, 1);
expect(content, isNotNull); expect(content, isNotNull);
expect(content!.plainText, 'w are'); expect(content!.plainText, 'w are');
...@@ -3243,8 +3407,8 @@ void main() { ...@@ -3243,8 +3407,8 @@ void main() {
expect(paragraph1.selections.length, 1); expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 0); expect(paragraph1.selections[0].start, 0);
expect(paragraph1.selections[0].end, 2); expect(paragraph1.selections[0].end, 2);
expect(paragraph2.selections.length, 0); expect(paragraph2.selections.length, 1);
expect(paragraph3.selections.length, 0); expect(paragraph3.selections.length, 1);
expect(content, isNotNull); expect(content, isNotNull);
expect(content!.plainText, 'Ho'); expect(content!.plainText, 'Ho');
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment