Skip to content

Commit 4b4c876

Browse files
authored
Text selection located wrong position when selecting multiple lines over max lines (#102747)
Fix for text selection toolbar position in cases with overflowing text.
1 parent 3f9ec41 commit 4b4c876

File tree

5 files changed

+149
-2
lines changed

5 files changed

+149
-2
lines changed

packages/flutter/lib/src/cupertino/text_selection.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,16 @@ class _CupertinoTextSelectionControlsToolbarState extends State<_CupertinoTextSe
9797
mediaQuery.size.width - mediaQuery.padding.right - _kArrowScreenPadding,
9898
);
9999

100+
final double topAmountInEditableRegion = widget.endpoints.first.point.dy - widget.textLineHeight;
101+
final double anchorTop = math.max(topAmountInEditableRegion, 0) + widget.globalEditableRegion.top;
102+
100103
// The y-coordinate has to be calculated instead of directly quoting
101104
// selectionMidpoint.dy, since the caller
102105
// (TextSelectionOverlay._buildToolbar) does not know whether the toolbar is
103106
// going to be facing up or down.
104107
final Offset anchorAbove = Offset(
105108
anchorX,
106-
widget.endpoints.first.point.dy - widget.textLineHeight + widget.globalEditableRegion.top,
109+
anchorTop,
107110
);
108111
final Offset anchorBelow = Offset(
109112
anchorX,

packages/flutter/lib/src/cupertino/text_selection_toolbar.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
129129
delegate: TextSelectionToolbarLayoutDelegate(
130130
anchorAbove: anchorAbove - localAdjustment - contentPaddingAdjustment,
131131
anchorBelow: anchorBelow - localAdjustment + contentPaddingAdjustment,
132+
fitsAbove: fitsAbove,
132133
),
133134
child: _CupertinoTextSelectionToolbarContent(
134135
anchor: fitsAbove ? anchorAbove : anchorBelow,

packages/flutter/lib/src/material/text_selection.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,12 @@ class _TextSelectionControlsToolbarState extends State<_TextSelectionControlsToo
205205
final TextSelectionPoint endTextSelectionPoint = widget.endpoints.length > 1
206206
? widget.endpoints[1]
207207
: widget.endpoints[0];
208+
final double topAmountInEditableRegion = startTextSelectionPoint.point.dy - widget.textLineHeight;
209+
final double anchorTop = math.max(topAmountInEditableRegion, 0) + widget.globalEditableRegion.top - _kToolbarContentDistance;
210+
208211
final Offset anchorAbove = Offset(
209212
widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
210-
widget.globalEditableRegion.top + startTextSelectionPoint.point.dy - widget.textLineHeight - _kToolbarContentDistance,
213+
anchorTop,
211214
);
212215
final Offset anchorBelow = Offset(
213216
widget.globalEditableRegion.left + widget.selectionMidpoint.dx,

packages/flutter/test/cupertino/text_selection_test.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,69 @@ void main() {
533533
skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
534534
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
535535
);
536+
537+
testWidgets(
538+
'When selecting multiple lines over max lines',
539+
(WidgetTester tester) async {
540+
final TextEditingController controller = TextEditingController(text: 'abc\ndef\nghi\njkl\nmno\npqr');
541+
await tester.pumpWidget(CupertinoApp(
542+
home: Directionality(
543+
textDirection: TextDirection.ltr,
544+
child: MediaQuery(
545+
data: const MediaQueryData(size: Size(800.0, 600.0)),
546+
child: Center(
547+
child: CupertinoTextField(
548+
padding: const EdgeInsets.all(8.0),
549+
controller: controller,
550+
maxLines: 2,
551+
),
552+
),
553+
),
554+
),
555+
));
556+
557+
// Initially, the menu isn't shown at all.
558+
expect(find.text('Cut'), findsNothing);
559+
expect(find.text('Copy'), findsNothing);
560+
expect(find.text('Paste'), findsNothing);
561+
expect(find.text('Select All'), findsNothing);
562+
expect(find.text('◀'), findsNothing);
563+
expect(find.text('▶'), findsNothing);
564+
565+
// Long press on an space to show the selection menu.
566+
await tester.longPressAt(textOffsetToPosition(tester, 1));
567+
await tester.pumpAndSettle();
568+
expect(find.text('Cut'), findsNothing);
569+
expect(find.text('Copy'), findsNothing);
570+
expect(find.text('Paste'), findsOneWidget);
571+
expect(find.text('Select All'), findsOneWidget);
572+
expect(find.text('◀'), findsNothing);
573+
expect(find.text('▶'), findsNothing);
574+
575+
// Tap to select all.
576+
await tester.tap(find.text('Select All'));
577+
await tester.pumpAndSettle();
578+
579+
// Only Cut, Copy, and Paste are shown.
580+
expect(find.text('Cut'), findsOneWidget);
581+
expect(find.text('Copy'), findsOneWidget);
582+
expect(find.text('Paste'), findsOneWidget);
583+
expect(find.text('Select All'), findsNothing);
584+
expect(find.text('◀'), findsNothing);
585+
expect(find.text('▶'), findsNothing);
586+
587+
// The menu appears at the top of the visible selection.
588+
final Offset selectionOffset = tester
589+
.getTopLeft(find.byType(CupertinoTextSelectionToolbarButton).first);
590+
final Offset textFieldOffset =
591+
tester.getTopLeft(find.byType(CupertinoTextField));
592+
593+
// 7.0 + 43.0 + 8.0 - 8.0 = _kToolbarArrowSize + _kToolbarHeight + _kToolbarContentDistance - padding
594+
expect(selectionOffset.dy + 7.0 + 43.0 + 8.0 - 8.0, equals(textFieldOffset.dy));
595+
},
596+
skip: isBrowser, // [intended] the selection menu isn't required by web
597+
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
598+
);
536599
});
537600

538601
testWidgets('iOS selection handles scale with rich text (selection style 1)', (WidgetTester tester) async {

packages/flutter/test/material/text_selection_test.dart

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,83 @@ void main() {
547547
skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
548548
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
549549
);
550+
551+
testWidgets(
552+
'When selecting multiple lines over max lines',
553+
(WidgetTester tester) async {
554+
final TextEditingController controller =
555+
TextEditingController(text: 'abc\ndef\nghi\njkl\nmno\npqr');
556+
await tester.pumpWidget(MaterialApp(
557+
theme: ThemeData(platform: TargetPlatform.android),
558+
home: Directionality(
559+
textDirection: TextDirection.ltr,
560+
child: MediaQuery(
561+
data: const MediaQueryData(size: Size(800.0, 600.0)),
562+
child: Align(
563+
alignment: Alignment.bottomCenter,
564+
child: Material(
565+
child: TextField(
566+
decoration: const InputDecoration(contentPadding: EdgeInsets.all(8.0)),
567+
style: const TextStyle(fontSize: 32, height: 1),
568+
maxLines: 2,
569+
controller: controller,
570+
),
571+
),
572+
),
573+
),
574+
),
575+
));
576+
577+
// Initially, the menu isn't shown at all.
578+
expect(find.text('Cut'), findsNothing);
579+
expect(find.text('Copy'), findsNothing);
580+
expect(find.text('Paste'), findsNothing);
581+
expect(find.text('Select all'), findsNothing);
582+
expect(find.byType(IconButton), findsNothing);
583+
584+
// Tap to place the cursor in the field, then tap the handle to show the
585+
// selection menu.
586+
await tester.tap(find.byType(TextField));
587+
await tester.pumpAndSettle();
588+
final RenderEditable renderEditable = findRenderEditable(tester);
589+
final List<TextSelectionPoint> endpoints = globalize(
590+
renderEditable.getEndpointsForSelection(controller.selection),
591+
renderEditable,
592+
);
593+
expect(endpoints.length, 1);
594+
final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0);
595+
await tester.tapAt(handlePos, pointer: 7);
596+
await tester.pumpAndSettle();
597+
expect(find.text('Cut'), findsNothing);
598+
expect(find.text('Copy'), findsNothing);
599+
expect(find.text('Paste'), findsOneWidget);
600+
expect(find.text('Select all'), findsOneWidget);
601+
expect(find.byType(IconButton), findsNothing);
602+
603+
// Tap to select all.
604+
await tester.tap(find.text('Select all'));
605+
await tester.pumpAndSettle();
606+
607+
// Only Cut, Copy, and Paste are shown.
608+
expect(find.text('Cut'), findsOneWidget);
609+
expect(find.text('Copy'), findsOneWidget);
610+
expect(find.text('Paste'), findsOneWidget);
611+
expect(find.text('Select all'), findsNothing);
612+
expect(find.byType(IconButton), findsNothing);
613+
614+
615+
// The menu appears at the top of the visible selection.
616+
final Offset selectionOffset = tester
617+
.getTopLeft(find.byType(TextSelectionToolbarTextButton).first);
618+
final Offset textFieldOffset =
619+
tester.getTopLeft(find.byType(TextField));
620+
621+
// 44.0 + 8.0 - 8.0 = _kToolbarHeight + _kToolbarContentDistance - contentPadding
622+
expect(selectionOffset.dy + 44.0 + 8.0 - 8.0, equals(textFieldOffset.dy));
623+
},
624+
skip: isBrowser, // [intended] the selection menu isn't required by web
625+
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
626+
);
550627
});
551628

552629
group('material handles', () {

0 commit comments

Comments
 (0)