From 24f0d2856286fc1c2a87aaeb8461ab09f8bb7d2c Mon Sep 17 00:00:00 2001 From: Tomasz Gucio <72562119+tgucio@users.noreply.github.com> Date: Thu, 22 Apr 2021 03:34:03 +0200 Subject: [PATCH] Adjust selection drag start position for viewport offset changes (#80047) --- .../lib/src/widgets/text_selection.dart | 17 ++++++-- .../test/widgets/text_selection_test.dart | 42 +++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index a89616bdff1..45aaa3ac28d 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -952,7 +952,7 @@ class TextSelectionGestureDetectorBuilder { @protected final TextSelectionGestureDetectorBuilderDelegate delegate; - /// Returns true iff lastSecondaryTapDownPosition was on selection. + /// Returns true if lastSecondaryTapDownPosition was on selection. bool get _lastSecondaryTapWasOnSelection { assert(renderEditable.lastSecondaryTapDownPosition != null); if (renderEditable.selection == null) { @@ -985,6 +985,9 @@ class TextSelectionGestureDetectorBuilder { @protected RenderEditable get renderEditable => editableText.renderEditable; + /// The viewport offset pixels of the [RenderEditable] at the last drag start. + double _dragStartViewportOffset = 0.0; + /// Handler for [TextSelectionGestureDetector.onTapDown]. /// /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets @@ -1197,6 +1200,8 @@ class TextSelectionGestureDetectorBuilder { from: details.globalPosition, cause: SelectionChangedCause.drag, ); + + _dragStartViewportOffset = renderEditable.offset.pixels; } /// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate]. @@ -1212,8 +1217,14 @@ class TextSelectionGestureDetectorBuilder { void onDragSelectionUpdate(DragStartDetails startDetails, DragUpdateDetails updateDetails) { if (!delegate.selectionEnabled) return; + + // Adjust the drag start offset for possible viewport offset changes. + final Offset startOffset = renderEditable.maxLines == 1 + ? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0) + : Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset); + renderEditable.selectPositionAt( - from: startDetails.globalPosition, + from: startDetails.globalPosition - startOffset, to: updateDetails.globalPosition, cause: SelectionChangedCause.drag, ); @@ -1618,7 +1629,7 @@ class ClipboardStatusNotifier extends ValueNotifier with Widget }) : super(value); bool _disposed = false; - /// True iff this instance has been disposed. + /// True if this instance has been disposed. bool get disposed => _disposed; /// Check the [Clipboard] and update [value] if needed. diff --git a/packages/flutter/test/widgets/text_selection_test.dart b/packages/flutter/test/widgets/text_selection_test.dart index 1381dab7b45..8192d15728f 100644 --- a/packages/flutter/test/widgets/text_selection_test.dart +++ b/packages/flutter/test/widgets/text_selection_test.dart @@ -624,6 +624,44 @@ void main() { expect(editableText.selectionOverlay!.toolbarIsVisible, isFalse); }); + testWidgets('test TextSelectionGestureDetectorBuilder drag with RenderEditable viewport offset change', (WidgetTester tester) async { + await pumpTextSelectionGestureDetectorBuilder(tester); + final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable)); + + // Reconfigure the RenderEditable for multi-line. + renderEditable.maxLines = null; + renderEditable.offset = ViewportOffset.fixed(20.0); + renderEditable.layout(const BoxConstraints.tightFor(width: 400, height: 300.0)); + await tester.pumpAndSettle(); + + final TestGesture gesture = await tester.startGesture( + const Offset(200.0, 200.0), + kind: PointerDeviceKind.mouse, + ); + addTearDown(gesture.removePointer); + await tester.pumpAndSettle(); + expect(renderEditable.selectPositionAtCalled, isFalse); + + await gesture.moveTo(const Offset(300.0, 200.0)); + await tester.pumpAndSettle(); + expect(renderEditable.selectPositionAtCalled, isTrue); + expect(renderEditable.selectPositionAtFrom, const Offset(200.0, 200.0)); + expect(renderEditable.selectPositionAtTo, const Offset(300.0, 200.0)); + + // Move the viewport offset (scroll). + renderEditable.offset = ViewportOffset.fixed(150.0); + renderEditable.layout(const BoxConstraints.tightFor(width: 400, height: 300.0)); + await tester.pumpAndSettle(); + + await gesture.moveTo(const Offset(300.0, 400.0)); + await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(renderEditable.selectPositionAtCalled, isTrue); + expect(renderEditable.selectPositionAtFrom, const Offset(200.0, 70.0)); + expect(renderEditable.selectPositionAtTo, const Offset(300.0, 400.0)); + }); + testWidgets('test TextSelectionGestureDetectorBuilder selection disabled', (WidgetTester tester) async { await pumpTextSelectionGestureDetectorBuilder(tester, selectionEnabled: false); final TestGesture gesture = await tester.startGesture( @@ -863,9 +901,13 @@ class FakeRenderEditable extends RenderEditable { } bool selectPositionAtCalled = false; + Offset? selectPositionAtFrom; + Offset? selectPositionAtTo; @override void selectPositionAt({ required Offset from, Offset? to, required SelectionChangedCause cause }) { selectPositionAtCalled = true; + selectPositionAtFrom = from; + selectPositionAtTo = to; } bool selectWordCalled = false;