Unverified Commit 297a7c75 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Desktop edge scrolling (#93170)

parent 3f8e77ed
......@@ -967,15 +967,30 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
}
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
if (cause == SelectionChangedCause.longPress) {
_editableText.bringIntoView(selection.base);
}
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
if (willShowSelectionHandles != _showSelectionHandles) {
setState(() {
_showSelectionHandles = willShowSelectionHandles;
});
}
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
if (cause == SelectionChangedCause.longPress
|| cause == SelectionChangedCause.drag) {
_editableText.bringIntoView(selection.extent);
}
return;
case TargetPlatform.linux:
case TargetPlatform.windows:
case TargetPlatform.fuchsia:
case TargetPlatform.android:
if (cause == SelectionChangedCause.drag) {
_editableText.bringIntoView(selection.extent);
}
return;
}
}
@override
......
......@@ -614,7 +614,13 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
/// some time has passed.
class PanGestureRecognizer extends DragGestureRecognizer {
/// Create a gesture recognizer for tracking movement on a plane.
PanGestureRecognizer({ Object? debugOwner }) : super(debugOwner: debugOwner);
PanGestureRecognizer({
Object? debugOwner,
Set<PointerDeviceKind>? supportedDevices,
}) : super(
debugOwner: debugOwner,
supportedDevices: supportedDevices,
);
@override
bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
......
......@@ -1075,15 +1075,19 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
if (cause == SelectionChangedCause.longPress) {
_editableText?.bringIntoView(selection.base);
if (cause == SelectionChangedCause.longPress
|| cause == SelectionChangedCause.drag) {
_editableText?.bringIntoView(selection.extent);
}
return;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
// Do nothing.
case TargetPlatform.fuchsia:
case TargetPlatform.android:
if (cause == SelectionChangedCause.drag) {
_editableText?.bringIntoView(selection.extent);
}
return;
}
}
......
......@@ -1527,11 +1527,9 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
if (widget.onDragSelectionStart != null ||
widget.onDragSelectionUpdate != null ||
widget.onDragSelectionEnd != null) {
// TODO(mdebbar): Support dragging in any direction (for multiline text).
// https://github.com/flutter/flutter/issues/28676
gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.mouse),
(HorizontalDragGestureRecognizer instance) {
gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => PanGestureRecognizer(debugOwner: this, supportedDevices: <PointerDeviceKind>{ PointerDeviceKind.mouse }),
(PanGestureRecognizer instance) {
instance
// Text selection should start from the position of the first pointer
// down event.
......
......@@ -2414,7 +2414,7 @@ void main() {
expect(firstCharEndpoint.length, 1);
// The first character is now offscreen to the left.
expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-308.20, epsilon: 0.25));
});
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets(
'long tap after a double tap select is not affected',
......
......@@ -7794,9 +7794,379 @@ void main() {
);
expect(firstCharEndpoint.length, 1);
// The first character is now offscreen to the left.
expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257, epsilon: 1));
expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-257.0, epsilon: 1));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('mouse click and drag can edge scroll', (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,
),
),
),
),
);
// Just testing the test and making sure that the last character is off
// the right side of the screen.
expect(textOffsetToPosition(tester, 66).dx, 1056);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 19),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
addTearDown(gesture.removePointer);
await gesture.moveTo(textOffsetToPosition(tester, 56));
// To the edge of the screen basically.
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 56),
);
// Keep moving out.
await gesture.moveTo(textOffsetToPosition(tester, 62));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 62),
);
await gesture.moveTo(textOffsetToPosition(tester, 66));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 66),
); // We're at the edge now.
expect(find.byType(CupertinoButton), findsNothing);
await gesture.up();
await tester.pumpAndSettle();
// The selection isn't affected by the gesture lift.
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 66),
);
// The last character is now on screen near the right edge.
expect(
textOffsetToPosition(tester, 66).dx,
moreOrLessEquals(TestSemantics.fullScreen.width, epsilon: 2.0),
);
// The first character is now offscreen to the left.
expect(textOffsetToPosition(tester, 0).dx, moreOrLessEquals(-257.0, epsilon: 1));
}, variant: TargetPlatformVariant.all());
testWidgets('keyboard selection change scrolls the field', (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,
),
),
),
),
);
// Just testing the test and making sure that the last character is off
// the right side of the screen.
expect(textOffsetToPosition(tester, 66).dx, 1056);
await tester.tapAt(textOffsetToPosition(tester, 13));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 13),
);
// Move to position 56 with the right arrow (near the edge of the screen).
for (int i = 0; i < (56 - 13); i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
}
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 56),
);
// Keep moving out.
for (int i = 0; i < (62 - 56); i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
}
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 62),
);
for (int i = 0; i < (66 - 62); i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
}
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 66),
); // We're at the edge now.
await tester.pumpAndSettle();
// The last character is now on screen near the right edge.
expect(
textOffsetToPosition(tester, 66).dx,
moreOrLessEquals(TestSemantics.fullScreen.width, epsilon: 2.0),
);
// The first character is now offscreen to the left.
expect(textOffsetToPosition(tester, 0).dx, moreOrLessEquals(-257.0, epsilon: 1));
}, variant: TargetPlatformVariant.all(),
skip: isBrowser, // [intended] Browser handles arrow keys differently.
);
testWidgets('long press drag can edge scroll vertically', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neigse Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLines: 2,
controller: controller,
),
),
),
),
);
// Just testing the test and making sure that the last character is outside
// the bottom of the field.
final int textLength = controller.text.length;
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
final double firstCharY = textOffsetToPosition(tester, 0).dy;
expect(
textOffsetToPosition(tester, textLength).dy,
moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1),
);
// Start long pressing on the first line.
final TestGesture gesture =
await tester.startGesture(textOffsetToPosition(tester, 19));
// TODO(justinmc): Make sure you've got all things torn down.
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
expect(
controller.selection,
const TextSelection.collapsed(offset: 19),
);
await tester.pumpAndSettle();
// Move down to the second line.
await gesture.moveBy(Offset(0.0, lineHeight));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 65),
);
// Still hasn't scrolled.
expect(
textOffsetToPosition(tester, 65).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Keep selecting down to the third and final line.
await gesture.moveBy(Offset(0.0, lineHeight));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 110),
);
// The last character is no longer three line heights down from the top of
// the field, it's now only two line heights down, because it has scrolled
// down by one line.
expect(
textOffsetToPosition(tester, 110).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Likewise, the first character is now scrolled out of the top of the field
// by one line.
expect(
textOffsetToPosition(tester, 0).dy,
moreOrLessEquals(firstCharY - lineHeight, epsilon: 1),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('keyboard selection change scrolls the field vertically', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLines: 2,
controller: controller,
),
),
),
),
);
// Just testing the test and making sure that the last character is outside
// the bottom of the field.
final int textLength = controller.text.length;
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
final double firstCharY = textOffsetToPosition(tester, 0).dy;
expect(
textOffsetToPosition(tester, textLength).dy,
moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1),
);
await tester.tapAt(textOffsetToPosition(tester, 13));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 13),
);
// Move down to the second line.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 59),
);
// Still hasn't scrolled.
expect(
textOffsetToPosition(tester, 66).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Move down to the third and final line.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 104),
);
// The last character is no longer three line heights down from the top of
// the field, it's now only two line heights down, because it has scrolled
// down by one line.
expect(
textOffsetToPosition(tester, textLength).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Likewise, the first character is now scrolled out of the top of the field
// by one line.
expect(
textOffsetToPosition(tester, 0).dy,
moreOrLessEquals(firstCharY - lineHeight, epsilon: 1),
);
}, variant: TargetPlatformVariant.all(),
skip: isBrowser, // [intended] Browser handles arrow keys differently.
);
testWidgets('mouse click and drag can edge scroll vertically', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges Atwater Peel Sherbrooke Bonaventure Angrignon Peel Côte-des-Neiges',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLines: 2,
controller: controller,
),
),
),
),
);
// Just testing the test and making sure that the last character is outside
// the bottom of the field.
final int textLength = controller.text.length;
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
final double firstCharY = textOffsetToPosition(tester, 0).dy;
expect(
textOffsetToPosition(tester, textLength).dy,
moreOrLessEquals(firstCharY + lineHeight * 2, epsilon: 1),
);
// Start selecting on the first line.
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 19),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
addTearDown(gesture.removePointer);
// Still hasn't scrolled.
expect(
textOffsetToPosition(tester, 60).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Select down to the second line.
await gesture.moveBy(Offset(0.0, lineHeight));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 65),
);
// Still hasn't scrolled.
expect(
textOffsetToPosition(tester, 60).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Keep selecting down to the third and final line.
await gesture.moveBy(Offset(0.0, lineHeight));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 19, extentOffset: 110),
);
// The last character is no longer three line heights down from the top of
// the field, it's now only two line heights down, because it has scrolled
// down by one line.
expect(
textOffsetToPosition(tester, textLength).dy,
moreOrLessEquals(firstCharY + lineHeight, epsilon: 1),
);
// Likewise, the first character is now scrolled out of the top of the field
// by one line.
expect(
textOffsetToPosition(tester, 0).dy,
moreOrLessEquals(firstCharY - lineHeight, epsilon: 1),
);
}, variant: TargetPlatformVariant.all());
testWidgets(
'long tap after a double tap select is not affected',
(WidgetTester tester) async {
......
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