Unverified Commit 3c2f500b authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Fix edge scrolling on platforms that select word by word on long press move (#113128)

parent 2dd87fbd
...@@ -1035,16 +1035,12 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1035,16 +1035,12 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS: case TargetPlatform.macOS:
if (cause == SelectionChangedCause.longPress
|| cause == SelectionChangedCause.drag) {
_editableText.bringIntoView(selection.extent);
}
break;
case TargetPlatform.linux: case TargetPlatform.linux:
case TargetPlatform.windows: case TargetPlatform.windows:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.android: case TargetPlatform.android:
if (cause == SelectionChangedCause.drag) { if (cause == SelectionChangedCause.longPress
|| cause == SelectionChangedCause.drag) {
_editableText.bringIntoView(selection.extent); _editableText.bringIntoView(selection.extent);
} }
break; break;
......
...@@ -62,45 +62,6 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete ...@@ -62,45 +62,6 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
// Not required. // Not required.
} }
@override
void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
if (delegate.selectionEnabled) {
final TargetPlatform targetPlatform = Theme.of(_state.context).platform;
switch (targetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.longPress,
);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
renderEditable.selectWordsInRange(
from: details.globalPosition - details.offsetFromOrigin,
to: details.globalPosition,
cause: SelectionChangedCause.longPress,
);
break;
}
switch (targetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.showMagnifier(details.globalPosition);
break;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
}
}
@override @override
void onSingleTapUp(TapUpDetails details) { void onSingleTapUp(TapUpDetails details) {
super.onSingleTapUp(details); super.onSingleTapUp(details);
...@@ -110,37 +71,19 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete ...@@ -110,37 +71,19 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
@override @override
void onSingleLongTapStart(LongPressStartDetails details) { void onSingleLongTapStart(LongPressStartDetails details) {
super.onSingleLongTapStart(details);
if (delegate.selectionEnabled) { if (delegate.selectionEnabled) {
final TargetPlatform targetPlatform = Theme.of(_state.context).platform; switch (Theme.of(_state.context).platform) {
switch (targetPlatform) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS: case TargetPlatform.macOS:
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.longPress,
);
break; break;
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.linux: case TargetPlatform.linux:
case TargetPlatform.windows: case TargetPlatform.windows:
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
Feedback.forLongPress(_state.context); Feedback.forLongPress(_state.context);
break; break;
} }
switch (targetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.showMagnifier(details.globalPosition);
break;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
} }
} }
} }
...@@ -1150,16 +1093,12 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1150,16 +1093,12 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
switch (Theme.of(context).platform) { switch (Theme.of(context).platform) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS: case TargetPlatform.macOS:
if (cause == SelectionChangedCause.longPress
|| cause == SelectionChangedCause.drag) {
_editableText?.bringIntoView(selection.extent);
}
break;
case TargetPlatform.linux: case TargetPlatform.linux:
case TargetPlatform.windows: case TargetPlatform.windows:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.android: case TargetPlatform.android:
if (cause == SelectionChangedCause.drag) { if (cause == SelectionChangedCause.longPress
|| cause == SelectionChangedCause.drag) {
_editableText?.bringIntoView(selection.extent); _editableText?.bringIntoView(selection.extent);
} }
break; break;
......
...@@ -1996,10 +1996,21 @@ class TextSelectionGestureDetectorBuilder { ...@@ -1996,10 +1996,21 @@ class TextSelectionGestureDetectorBuilder {
@protected @protected
void onSingleLongTapStart(LongPressStartDetails details) { void onSingleLongTapStart(LongPressStartDetails details) {
if (delegate.selectionEnabled) { if (delegate.selectionEnabled) {
renderEditable.selectPositionAt( switch (defaultTargetPlatform) {
from: details.globalPosition, case TargetPlatform.iOS:
cause: SelectionChangedCause.longPress, case TargetPlatform.macOS:
); renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.longPress,
);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
break;
}
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.android: case TargetPlatform.android:
...@@ -2012,6 +2023,9 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2012,6 +2023,9 @@ class TextSelectionGestureDetectorBuilder {
case TargetPlatform.windows: case TargetPlatform.windows:
break; break;
} }
_dragStartViewportOffset = renderEditable.offset.pixels;
_dragStartScrollOffset = _scrollPosition;
} }
} }
...@@ -2027,11 +2041,35 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2027,11 +2041,35 @@ class TextSelectionGestureDetectorBuilder {
@protected @protected
void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
if (delegate.selectionEnabled) { if (delegate.selectionEnabled) {
renderEditable.selectPositionAt( // Adjust the drag start offset for possible viewport offset changes.
from: details.globalPosition, final Offset editableOffset = renderEditable.maxLines == 1
cause: SelectionChangedCause.longPress, ? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
: Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset);
final Offset scrollableOffset = Offset(
0.0,
_scrollPosition - _dragStartScrollOffset,
); );
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.longPress,
);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
renderEditable.selectWordsInRange(
from: details.globalPosition - details.offsetFromOrigin - editableOffset - scrollableOffset,
to: details.globalPosition,
cause: SelectionChangedCause.longPress,
);
break;
}
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.iOS: case TargetPlatform.iOS:
...@@ -2070,6 +2108,8 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2070,6 +2108,8 @@ class TextSelectionGestureDetectorBuilder {
if (shouldShowSelectionToolbar) { if (shouldShowSelectionToolbar) {
editableText.showToolbar(); editableText.showToolbar();
} }
_dragStartViewportOffset = 0.0;
_dragStartScrollOffset = 0.0;
} }
/// Handler for [TextSelectionGestureDetector.onSecondaryTap]. /// Handler for [TextSelectionGestureDetector.onSecondaryTap].
......
...@@ -2254,7 +2254,123 @@ void main() { ...@@ -2254,7 +2254,123 @@ void main() {
expect(controller.selection.extentOffset, testValue.indexOf('g')); expect(controller.selection.extentOffset, testValue.indexOf('g'));
}); });
testWidgets('Can drag handles to change selection', (WidgetTester tester) async { testWidgets('Can drag handles to change selection on Apple platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Double tap the 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
// The first tap.
TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
await tester.pump();
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
// The second tap.
await gesture.down(ePos);
await tester.pump();
await gesture.up();
await tester.pump();
final TextSelection selection = controller.selection;
expect(selection.baseOffset, 4);
expect(selection.extentOffset, 7);
final RenderEditable renderEditable = findRenderEditable(tester);
List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Drag the right handle 2 letters to the right.
// We use a small offset because the endpoint is on the very corner
// of the handle.
Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
Offset newHandlePos = textOffsetToPosition(tester, testValue.length);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 11);
// Drag the left handle 2 letters to the left.
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
newHandlePos = textOffsetToPosition(tester, 2);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
switch (defaultTargetPlatform) {
// On Apple platforms, dragging the base handle makes it the extent.
case TargetPlatform.iOS:
case TargetPlatform.macOS:
expect(controller.selection.baseOffset, 11);
expect(controller.selection.extentOffset, 2);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection.baseOffset, 2);
expect(controller.selection.extentOffset, 11);
break;
}
// Drag the left handle 2 letters to the left again.
endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
newHandlePos = textOffsetToPosition(tester, 0);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// The left handle was already the extent, and it remains so.
expect(controller.selection.baseOffset, 11);
expect(controller.selection.extentOffset, 0);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 11);
break;
}
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets('Can drag handles to change selection on non-Apple platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -2360,7 +2476,7 @@ void main() { ...@@ -2360,7 +2476,7 @@ void main() {
break; break;
} }
}, },
variant: TargetPlatformVariant.all(), variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async { testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async {
...@@ -8578,7 +8694,95 @@ void main() { ...@@ -8578,7 +8694,95 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets('long press drag can edge scroll', (WidgetTester tester) async { testWidgets('long press drag can edge scroll on non-Apple platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
),
),
),
),
);
final RenderEditable renderEditable = findRenderEditable(tester);
List<TextSelectionPoint> lastCharEndpoint = renderEditable.getEndpointsForSelection(
const TextSelection.collapsed(offset: 66), // Last character's position.
);
expect(lastCharEndpoint.length, 1);
// Just testing the test and making sure that the last character is off
// the right side of the screen.
expect(lastCharEndpoint[0].point.dx, 1056);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
final TestGesture gesture =
await tester.startGesture(textfieldStart);
await tester.pump(const Duration(milliseconds: 500));
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7, affinity: TextAffinity.upstream),
);
expect(find.byType(TextButton), findsNothing);
await gesture.moveBy(const Offset(900, 5));
// To the edge of the screen basically.
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 59),
);
// Keep moving out.
await gesture.moveBy(const Offset(1, 0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 66),
);
await gesture.moveBy(const Offset(1, 0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream),
); // We're at the edge now.
expect(find.byType(TextButton), findsNothing);
await gesture.up();
await tester.pumpAndSettle();
// The selection isn't affected by the gesture lift.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 66, affinity: TextAffinity.upstream),
);
// The toolbar now shows up.
expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
lastCharEndpoint = renderEditable.getEndpointsForSelection(
const TextSelection.collapsed(offset: 66), // Last character's position.
);
expect(lastCharEndpoint.length, 1);
// The last character is now on screen near the right edge.
expect(lastCharEndpoint[0].point.dx, moreOrLessEquals(798, epsilon: 1));
final List<TextSelectionPoint> firstCharEndpoint = renderEditable.getEndpointsForSelection(
const TextSelection.collapsed(offset: 0), // First character's position.
);
expect(firstCharEndpoint.length, 1);
// The first character is now offscreen to the left.
expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
testWidgets('long press drag can edge scroll on Apple platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController( final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges', text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
); );
......
...@@ -433,7 +433,7 @@ void main() { ...@@ -433,7 +433,7 @@ void main() {
expect(dragEndCount, 1); expect(dragEndCount, 1);
}); });
testWidgets('test TextSelectionGestureDetectorBuilder long press', (WidgetTester tester) async { testWidgets('test TextSelectionGestureDetectorBuilder long press on Apple Platforms', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester); await pumpTextSelectionGestureDetectorBuilder(tester);
final TestGesture gesture = await tester.startGesture( final TestGesture gesture = await tester.startGesture(
const Offset(200.0, 200.0), const Offset(200.0, 200.0),
...@@ -447,7 +447,23 @@ void main() { ...@@ -447,7 +447,23 @@ void main() {
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
expect(state.showToolbarCalled, isTrue); expect(state.showToolbarCalled, isTrue);
expect(renderEditable.selectPositionAtCalled, isTrue); expect(renderEditable.selectPositionAtCalled, isTrue);
}); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('test TextSelectionGestureDetectorBuilder long press on non-Apple Platforms', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester);
final TestGesture gesture = await tester.startGesture(
const Offset(200.0, 200.0),
pointer: 0,
);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pumpAndSettle();
final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
expect(state.showToolbarCalled, isTrue);
expect(renderEditable.selectWordCalled, isTrue);
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('TextSelectionGestureDetectorBuilder right click Apple platforms', (WidgetTester tester) async { testWidgets('TextSelectionGestureDetectorBuilder right click Apple platforms', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/80119 // Regression test for https://github.com/flutter/flutter/issues/80119
......
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