Unverified Commit 4b4c876f authored by takashi kasai's avatar takashi kasai Committed by GitHub

Text selection located wrong position when selecting multiple lines over max lines (#102747)

Fix for text selection toolbar position in cases with overflowing text.
parent 3f9ec416
...@@ -97,13 +97,16 @@ class _CupertinoTextSelectionControlsToolbarState extends State<_CupertinoTextSe ...@@ -97,13 +97,16 @@ class _CupertinoTextSelectionControlsToolbarState extends State<_CupertinoTextSe
mediaQuery.size.width - mediaQuery.padding.right - _kArrowScreenPadding, mediaQuery.size.width - mediaQuery.padding.right - _kArrowScreenPadding,
); );
final double topAmountInEditableRegion = widget.endpoints.first.point.dy - widget.textLineHeight;
final double anchorTop = math.max(topAmountInEditableRegion, 0) + widget.globalEditableRegion.top;
// The y-coordinate has to be calculated instead of directly quoting // The y-coordinate has to be calculated instead of directly quoting
// selectionMidpoint.dy, since the caller // selectionMidpoint.dy, since the caller
// (TextSelectionOverlay._buildToolbar) does not know whether the toolbar is // (TextSelectionOverlay._buildToolbar) does not know whether the toolbar is
// going to be facing up or down. // going to be facing up or down.
final Offset anchorAbove = Offset( final Offset anchorAbove = Offset(
anchorX, anchorX,
widget.endpoints.first.point.dy - widget.textLineHeight + widget.globalEditableRegion.top, anchorTop,
); );
final Offset anchorBelow = Offset( final Offset anchorBelow = Offset(
anchorX, anchorX,
......
...@@ -129,6 +129,7 @@ class CupertinoTextSelectionToolbar extends StatelessWidget { ...@@ -129,6 +129,7 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
delegate: TextSelectionToolbarLayoutDelegate( delegate: TextSelectionToolbarLayoutDelegate(
anchorAbove: anchorAbove - localAdjustment - contentPaddingAdjustment, anchorAbove: anchorAbove - localAdjustment - contentPaddingAdjustment,
anchorBelow: anchorBelow - localAdjustment + contentPaddingAdjustment, anchorBelow: anchorBelow - localAdjustment + contentPaddingAdjustment,
fitsAbove: fitsAbove,
), ),
child: _CupertinoTextSelectionToolbarContent( child: _CupertinoTextSelectionToolbarContent(
anchor: fitsAbove ? anchorAbove : anchorBelow, anchor: fitsAbove ? anchorAbove : anchorBelow,
......
...@@ -205,9 +205,12 @@ class _TextSelectionControlsToolbarState extends State<_TextSelectionControlsToo ...@@ -205,9 +205,12 @@ class _TextSelectionControlsToolbarState extends State<_TextSelectionControlsToo
final TextSelectionPoint endTextSelectionPoint = widget.endpoints.length > 1 final TextSelectionPoint endTextSelectionPoint = widget.endpoints.length > 1
? widget.endpoints[1] ? widget.endpoints[1]
: widget.endpoints[0]; : widget.endpoints[0];
final double topAmountInEditableRegion = startTextSelectionPoint.point.dy - widget.textLineHeight;
final double anchorTop = math.max(topAmountInEditableRegion, 0) + widget.globalEditableRegion.top - _kToolbarContentDistance;
final Offset anchorAbove = Offset( final Offset anchorAbove = Offset(
widget.globalEditableRegion.left + widget.selectionMidpoint.dx, widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
widget.globalEditableRegion.top + startTextSelectionPoint.point.dy - widget.textLineHeight - _kToolbarContentDistance, anchorTop,
); );
final Offset anchorBelow = Offset( final Offset anchorBelow = Offset(
widget.globalEditableRegion.left + widget.selectionMidpoint.dx, widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
......
...@@ -533,6 +533,69 @@ void main() { ...@@ -533,6 +533,69 @@ void main() {
skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
); );
testWidgets(
'When selecting multiple lines over max lines',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'abc\ndef\nghi\njkl\nmno\npqr');
await tester.pumpWidget(CupertinoApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(size: Size(800.0, 600.0)),
child: Center(
child: CupertinoTextField(
padding: const EdgeInsets.all(8.0),
controller: controller,
maxLines: 2,
),
),
),
),
));
// Initially, the menu isn't shown at all.
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsNothing);
expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing);
expect(find.text('▶'), findsNothing);
// Long press on an space to show the selection menu.
await tester.longPressAt(textOffsetToPosition(tester, 1));
await tester.pumpAndSettle();
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select All'), findsOneWidget);
expect(find.text('◀'), findsNothing);
expect(find.text('▶'), findsNothing);
// Tap to select all.
await tester.tap(find.text('Select All'));
await tester.pumpAndSettle();
// Only Cut, Copy, and Paste are shown.
expect(find.text('Cut'), findsOneWidget);
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select All'), findsNothing);
expect(find.text('◀'), findsNothing);
expect(find.text('▶'), findsNothing);
// The menu appears at the top of the visible selection.
final Offset selectionOffset = tester
.getTopLeft(find.byType(CupertinoTextSelectionToolbarButton).first);
final Offset textFieldOffset =
tester.getTopLeft(find.byType(CupertinoTextField));
// 7.0 + 43.0 + 8.0 - 8.0 = _kToolbarArrowSize + _kToolbarHeight + _kToolbarContentDistance - padding
expect(selectionOffset.dy + 7.0 + 43.0 + 8.0 - 8.0, equals(textFieldOffset.dy));
},
skip: isBrowser, // [intended] the selection menu isn't required by web
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
}); });
testWidgets('iOS selection handles scale with rich text (selection style 1)', (WidgetTester tester) async { testWidgets('iOS selection handles scale with rich text (selection style 1)', (WidgetTester tester) async {
......
...@@ -547,6 +547,83 @@ void main() { ...@@ -547,6 +547,83 @@ void main() {
skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web. skip: isBrowser, // [intended] We do not use Flutter-rendered context menu on the Web.
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
); );
testWidgets(
'When selecting multiple lines over max lines',
(WidgetTester tester) async {
final TextEditingController controller =
TextEditingController(text: 'abc\ndef\nghi\njkl\nmno\npqr');
await tester.pumpWidget(MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(size: Size(800.0, 600.0)),
child: Align(
alignment: Alignment.bottomCenter,
child: Material(
child: TextField(
decoration: const InputDecoration(contentPadding: EdgeInsets.all(8.0)),
style: const TextStyle(fontSize: 32, height: 1),
maxLines: 2,
controller: controller,
),
),
),
),
),
));
// Initially, the menu isn't shown at all.
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsNothing);
expect(find.text('Select all'), findsNothing);
expect(find.byType(IconButton), findsNothing);
// Tap to place the cursor in the field, then tap the handle to show the
// selection menu.
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
expect(endpoints.length, 1);
final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0);
await tester.tapAt(handlePos, pointer: 7);
await tester.pumpAndSettle();
expect(find.text('Cut'), findsNothing);
expect(find.text('Copy'), findsNothing);
expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select all'), findsOneWidget);
expect(find.byType(IconButton), findsNothing);
// Tap to select all.
await tester.tap(find.text('Select all'));
await tester.pumpAndSettle();
// Only Cut, Copy, and Paste are shown.
expect(find.text('Cut'), findsOneWidget);
expect(find.text('Copy'), findsOneWidget);
expect(find.text('Paste'), findsOneWidget);
expect(find.text('Select all'), findsNothing);
expect(find.byType(IconButton), findsNothing);
// The menu appears at the top of the visible selection.
final Offset selectionOffset = tester
.getTopLeft(find.byType(TextSelectionToolbarTextButton).first);
final Offset textFieldOffset =
tester.getTopLeft(find.byType(TextField));
// 44.0 + 8.0 - 8.0 = _kToolbarHeight + _kToolbarContentDistance - contentPadding
expect(selectionOffset.dy + 44.0 + 8.0 - 8.0, equals(textFieldOffset.dy));
},
skip: isBrowser, // [intended] the selection menu isn't required by web
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
);
}); });
group('material handles', () { group('material handles', () {
......
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