diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart index 1786b00ff7..568cb64507 100644 --- a/packages/flutter/lib/src/widgets/selectable_region.dart +++ b/packages/flutter/lib/src/widgets/selectable_region.dart @@ -39,6 +39,11 @@ const Set _kLongPressSelectionDevices = { PointerDeviceKind.invertedStylus, }; +// In practice some selectables like widgetspan shift several pixels. So when +// the vertical position diff is within the threshold, compare the horizontal +// position to make the compareScreenOrder function more robust. +const double _kSelectableVerticalComparingThreshold = 3.0; + /// A widget that introduces an area for user selections. /// /// Flutter widgets are not selectable by default. Wrapping a widget subtree @@ -1703,11 +1708,11 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai /// Returns positive if a is lower, negative if a is higher, 0 if their /// order can't be determine solely by their vertical position. static int _compareVertically(Rect a, Rect b) { - if ((a.top - b.top < precisionErrorTolerance && a.bottom - b.bottom > - precisionErrorTolerance) || - (b.top - a.top < precisionErrorTolerance && b.bottom - a.bottom > - precisionErrorTolerance)) { + if ((a.top - b.top < _kSelectableVerticalComparingThreshold && a.bottom - b.bottom > - _kSelectableVerticalComparingThreshold) || + (b.top - a.top < _kSelectableVerticalComparingThreshold && b.bottom - a.bottom > - _kSelectableVerticalComparingThreshold)) { return 0; } - if ((a.top - b.top).abs() > precisionErrorTolerance) { + if ((a.top - b.top).abs() > _kSelectableVerticalComparingThreshold) { return a.top > b.top ? 1 : -1; } return a.bottom > b.bottom ? 1 : -1; diff --git a/packages/flutter/test/widgets/selectable_region_test.dart b/packages/flutter/test/widgets/selectable_region_test.dart index e2903ab778..7d9fbd174c 100644 --- a/packages/flutter/test/widgets/selectable_region_test.dart +++ b/packages/flutter/test/widgets/selectable_region_test.dart @@ -2469,6 +2469,51 @@ void main() { skip: !kIsWeb, // [intended] ); }); + + testWidgets('Multiple selectables on a single line should be in screen order', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/127942. + final UniqueKey outerText = UniqueKey(); + const TextStyle textStyle = TextStyle(fontSize: 10); + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: FocusNode(), + selectionControls: materialTextSelectionControls, + child: Scaffold( + body: Center( + child: Text.rich( + const TextSpan( + children: [ + TextSpan(text: 'Hello my name is ', style: textStyle), + WidgetSpan( + child: Text('Dash', style: textStyle), + alignment: PlaceholderAlignment.middle, + ), + TextSpan(text: '.', style: textStyle), + ], + ), + key: outerText, + ), + ), + ), + ), + ), + ); + final RenderParagraph paragraph1 = tester.renderObject(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 0), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + + // Select all. + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true)); + + // keyboard copy. + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true)); + + final Map clipboardData = mockClipboard.clipboardData as Map; + expect(clipboardData['text'], 'Hello my name is Dash.'); + }); } class SelectionSpy extends LeafRenderObjectWidget {