Unverified Commit d440ee82 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Consider Scrollable location in text selection drag events (#102992)

Fixes scrolling by dragging a selection when the field is in a Scrollable.
parent da64b912
......@@ -923,7 +923,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
@override
void initState() {
super.initState();
_selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(state: this);
_selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(
state: this,
);
if (widget.controller == null) {
_createLocalController();
}
......
......@@ -500,7 +500,9 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
@override
void initState() {
super.initState();
_selectionGestureDetectorBuilder = _SelectableTextSelectionGestureDetectorBuilder(state: this);
_selectionGestureDetectorBuilder = _SelectableTextSelectionGestureDetectorBuilder(
state: this,
);
_controller = _TextSpanEditingController(
textSpan: widget.textSpan ?? TextSpan(text: widget.data),
);
......
......@@ -2862,6 +2862,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final RevealedOffset targetOffset = _getOffsetToRevealCaret(_currentCaretRect!);
final Rect rectToReveal;
final TextSelection selection = textEditingValue.selection;
if (selection.isCollapsed) {
rectToReveal = targetOffset.rect;
} else {
final List<Rect> selectionBoxes = renderEditable.getBoxesForSelection(selection);
rectToReveal = selection.baseOffset < selection.extentOffset ?
selectionBoxes.last : selectionBoxes.first;
}
if (withAnimation) {
_scrollController.animateTo(
targetOffset.offset,
......@@ -2869,15 +2879,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
curve: _caretAnimationCurve,
);
renderEditable.showOnScreen(
rect: caretPadding.inflateRect(targetOffset.rect),
rect: caretPadding.inflateRect(rectToReveal),
duration: _caretAnimationDuration,
curve: _caretAnimationCurve,
);
} else {
_scrollController.jumpTo(targetOffset.offset);
renderEditable.showOnScreen(
rect: caretPadding.inflateRect(targetOffset.rect),
);
if (_value.selection.isCollapsed) {
renderEditable.showOnScreen(
rect: caretPadding.inflateRect(rectToReveal),
);
}
}
});
}
......
......@@ -22,6 +22,7 @@ import 'framework.dart';
import 'gesture_detector.dart';
import 'magnifier.dart';
import 'overlay.dart';
import 'scrollable.dart';
import 'tap_region.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
......@@ -1714,7 +1715,11 @@ class TextSelectionGestureDetectorBuilder {
@protected
RenderEditable get renderEditable => editableText.renderEditable;
// The viewport offset pixels of the [RenderEditable] at the last drag start.
/// The viewport offset pixels of any [Scrollable] containing the
/// [RenderEditable] at the last drag start.
double _dragStartScrollOffset = 0.0;
/// The viewport offset pixels of the [RenderEditable] at the last drag start.
double _dragStartViewportOffset = 0.0;
// Returns true iff either shift key is currently down.
......@@ -1726,6 +1731,16 @@ class TextSelectionGestureDetectorBuilder {
}.contains);
}
double get _scrollPosition {
final ScrollableState? scrollableState =
delegate.editableTextKey.currentContext == null
? null
: Scrollable.of(delegate.editableTextKey.currentContext!);
return scrollableState == null
? 0.0
: scrollableState.position.pixels;
}
// True iff a tap + shift has been detected but the tap has not yet come up.
bool _isShiftTapping = false;
......@@ -2114,6 +2129,7 @@ class TextSelectionGestureDetectorBuilder {
);
}
_dragStartScrollOffset = _scrollPosition;
_dragStartViewportOffset = renderEditable.offset.pixels;
}
......@@ -2134,12 +2150,15 @@ class TextSelectionGestureDetectorBuilder {
if (!_isShiftTapping) {
// Adjust the drag start offset for possible viewport offset changes.
final Offset startOffset = renderEditable.maxLines == 1
final Offset editableOffset = renderEditable.maxLines == 1
? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
: Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset);
final Offset scrollableOffset = Offset(
0.0,
_scrollPosition - _dragStartScrollOffset,
);
return renderEditable.selectPositionAt(
from: startDetails.globalPosition - startOffset,
from: startDetails.globalPosition - editableOffset - scrollableOffset,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
......
......@@ -79,8 +79,9 @@ void main() {
forcePressEnabled: forcePressEnabled,
selectionEnabled: selectionEnabled,
);
final TextSelectionGestureDetectorBuilder provider =
TextSelectionGestureDetectorBuilder(delegate: delegate);
TextSelectionGestureDetectorBuilder(delegate: delegate);
await tester.pumpWidget(
MaterialApp(
......@@ -1329,6 +1330,160 @@ void main() {
});
});
});
testWidgets('Mouse edge scrolling works in an outer scrollable', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/102484
final TextEditingController controller = TextEditingController(
text: 'I love flutter!\n' * 8,
);
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate(
editableTextKey: editableTextKey,
forcePressEnabled: false,
selectionEnabled: true,
);
final ScrollController scrollController = ScrollController();
const double kLineHeight = 16.0;
final TextSelectionGestureDetectorBuilder provider =
TextSelectionGestureDetectorBuilder(
delegate: delegate,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
// Only 4 lines visible of 8 given.
height: kLineHeight * 4,
child: SingleChildScrollView(
controller: scrollController,
child: provider.buildGestureDetector(
behavior: HitTestBehavior.translucent,
child: EditableText(
key: editableTextKey,
controller: controller,
focusNode: FocusNode(),
backgroundCursorColor: Colors.white,
cursorColor: Colors.white,
style: const TextStyle(),
selectionControls: materialTextSelectionControls,
// EditableText will expand to the full 8 line height and will
// not scroll itself.
maxLines: null,
),
),
),
),
),
),
);
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, -1);
expect(scrollController.position.pixels, 0.0);
final Offset position = textOffsetToPosition(tester, 4);
await tester.tapAt(position);
await tester.pump();
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 4);
// Select all text with the mouse.
final TestGesture gesture = await tester.startGesture(position, kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(tester, (controller.text.length / 2).floor()));
await tester.pump();
await gesture.moveTo(textOffsetToPosition(tester, controller.text.length));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, isFalse);
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, controller.text.length);
expect(scrollController.position.pixels, scrollController.position.maxScrollExtent);
});
testWidgets('Mouse edge scrolling works with both an outer scrollable and scrolling in the EditableText', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/102484
final TextEditingController controller = TextEditingController(
text: 'I love flutter!\n' * 8,
);
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate(
editableTextKey: editableTextKey,
forcePressEnabled: false,
selectionEnabled: true,
);
final ScrollController scrollController = ScrollController();
const double kLineHeight = 16.0;
final TextSelectionGestureDetectorBuilder provider =
TextSelectionGestureDetectorBuilder(
delegate: delegate,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
// Only 4 lines visible of 8 given.
height: kLineHeight * 4,
child: SingleChildScrollView(
controller: scrollController,
child: provider.buildGestureDetector(
behavior: HitTestBehavior.translucent,
child: EditableText(
key: editableTextKey,
controller: controller,
focusNode: FocusNode(),
backgroundCursorColor: Colors.white,
cursorColor: Colors.white,
style: const TextStyle(),
selectionControls: materialTextSelectionControls,
// EditableText is taller than the SizedBox but not taller
// than the text.
maxLines: 6,
),
),
),
),
),
),
);
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, -1);
expect(scrollController.position.pixels, 0.0);
final Offset position = textOffsetToPosition(tester, 4);
await tester.tapAt(position);
await tester.pump();
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 4);
// Select all text with the mouse.
final TestGesture gesture = await tester.startGesture(position, kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.moveTo(textOffsetToPosition(tester, (controller.text.length / 2).floor()));
await tester.pump();
await gesture.moveTo(textOffsetToPosition(tester, controller.text.length));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, isFalse);
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, controller.text.length);
expect(scrollController.position.pixels, scrollController.position.maxScrollExtent);
});
}
class FakeTextSelectionGestureDetectorBuilderDelegate implements TextSelectionGestureDetectorBuilderDelegate {
......
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