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 ...@@ -923,7 +923,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(state: this); _selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(
state: this,
);
if (widget.controller == null) { if (widget.controller == null) {
_createLocalController(); _createLocalController();
} }
......
...@@ -500,7 +500,9 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio ...@@ -500,7 +500,9 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectionGestureDetectorBuilder = _SelectableTextSelectionGestureDetectorBuilder(state: this); _selectionGestureDetectorBuilder = _SelectableTextSelectionGestureDetectorBuilder(
state: this,
);
_controller = _TextSpanEditingController( _controller = _TextSpanEditingController(
textSpan: widget.textSpan ?? TextSpan(text: widget.data), textSpan: widget.textSpan ?? TextSpan(text: widget.data),
); );
......
...@@ -2862,6 +2862,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2862,6 +2862,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final RevealedOffset targetOffset = _getOffsetToRevealCaret(_currentCaretRect!); 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) { if (withAnimation) {
_scrollController.animateTo( _scrollController.animateTo(
targetOffset.offset, targetOffset.offset,
...@@ -2869,15 +2879,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2869,15 +2879,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
curve: _caretAnimationCurve, curve: _caretAnimationCurve,
); );
renderEditable.showOnScreen( renderEditable.showOnScreen(
rect: caretPadding.inflateRect(targetOffset.rect), rect: caretPadding.inflateRect(rectToReveal),
duration: _caretAnimationDuration, duration: _caretAnimationDuration,
curve: _caretAnimationCurve, curve: _caretAnimationCurve,
); );
} else { } else {
_scrollController.jumpTo(targetOffset.offset); _scrollController.jumpTo(targetOffset.offset);
renderEditable.showOnScreen( if (_value.selection.isCollapsed) {
rect: caretPadding.inflateRect(targetOffset.rect), renderEditable.showOnScreen(
); rect: caretPadding.inflateRect(rectToReveal),
);
}
} }
}); });
} }
......
...@@ -22,6 +22,7 @@ import 'framework.dart'; ...@@ -22,6 +22,7 @@ import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
import 'magnifier.dart'; import 'magnifier.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'scrollable.dart';
import 'tap_region.dart'; import 'tap_region.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
import 'transitions.dart'; import 'transitions.dart';
...@@ -1714,7 +1715,11 @@ class TextSelectionGestureDetectorBuilder { ...@@ -1714,7 +1715,11 @@ class TextSelectionGestureDetectorBuilder {
@protected @protected
RenderEditable get renderEditable => editableText.renderEditable; 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; double _dragStartViewportOffset = 0.0;
// Returns true iff either shift key is currently down. // Returns true iff either shift key is currently down.
...@@ -1726,6 +1731,16 @@ class TextSelectionGestureDetectorBuilder { ...@@ -1726,6 +1731,16 @@ class TextSelectionGestureDetectorBuilder {
}.contains); }.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. // True iff a tap + shift has been detected but the tap has not yet come up.
bool _isShiftTapping = false; bool _isShiftTapping = false;
...@@ -2114,6 +2129,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2114,6 +2129,7 @@ class TextSelectionGestureDetectorBuilder {
); );
} }
_dragStartScrollOffset = _scrollPosition;
_dragStartViewportOffset = renderEditable.offset.pixels; _dragStartViewportOffset = renderEditable.offset.pixels;
} }
...@@ -2134,12 +2150,15 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2134,12 +2150,15 @@ class TextSelectionGestureDetectorBuilder {
if (!_isShiftTapping) { if (!_isShiftTapping) {
// Adjust the drag start offset for possible viewport offset changes. // 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(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
: Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset); : Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset);
final Offset scrollableOffset = Offset(
0.0,
_scrollPosition - _dragStartScrollOffset,
);
return renderEditable.selectPositionAt( return renderEditable.selectPositionAt(
from: startDetails.globalPosition - startOffset, from: startDetails.globalPosition - editableOffset - scrollableOffset,
to: updateDetails.globalPosition, to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag, cause: SelectionChangedCause.drag,
); );
......
...@@ -79,8 +79,9 @@ void main() { ...@@ -79,8 +79,9 @@ void main() {
forcePressEnabled: forcePressEnabled, forcePressEnabled: forcePressEnabled,
selectionEnabled: selectionEnabled, selectionEnabled: selectionEnabled,
); );
final TextSelectionGestureDetectorBuilder provider = final TextSelectionGestureDetectorBuilder provider =
TextSelectionGestureDetectorBuilder(delegate: delegate); TextSelectionGestureDetectorBuilder(delegate: delegate);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -1329,6 +1330,160 @@ void main() { ...@@ -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 { 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