Unverified Commit f60e54b2 authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Fix SelectionArea select-word edge cases (#136920)

This change fixes issues with screen order comparison logic when rects are encompassed within each other. This was causing issues when trying to select text that includes inline `WidgetSpan`s inside of a `SelectionArea`.

* Adds `boundingBoxes` to `Selectable` for a more precise hit testing region.

Fixes #132821
Fixes updating selection edge by word boundary when widget spans are involved.
Fixes crash when sending select word selection event to an unselectable element.
parent 67edaef9
...@@ -115,6 +115,9 @@ class _RenderSelectableAdapter extends RenderProxyBox with Selectable, Selection ...@@ -115,6 +115,9 @@ class _RenderSelectableAdapter extends RenderProxyBox with Selectable, Selection
// Selectable APIs. // Selectable APIs.
@override
List<Rect> get boundingBoxes => <Rect>[paintBounds];
// Adjust this value to enlarge or shrink the selection highlight. // Adjust this value to enlarge or shrink the selection highlight.
static const double _padding = 10.0; static const double _padding = 10.0;
Rect _getSelectionHighlightRect() { Rect _getSelectionHighlightRect() {
......
...@@ -409,7 +409,13 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo ...@@ -409,7 +409,13 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
if (end == -1) { if (end == -1) {
end = plainText.length; end = plainText.length;
} }
result.add(_SelectableFragment(paragraph: this, range: TextRange(start: start, end: end), fullText: plainText)); result.add(
_SelectableFragment(
paragraph: this,
range: TextRange(start: start, end: end),
fullText: plainText,
),
);
start = end; start = end;
} }
start += 1; start += 1;
...@@ -1314,7 +1320,7 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo ...@@ -1314,7 +1320,7 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
/// [PlaceholderSpan]. The [RenderParagraph] splits itself on [PlaceholderSpan] /// [PlaceholderSpan]. The [RenderParagraph] splits itself on [PlaceholderSpan]
/// to create multiple `_SelectableFragment`s so that they can be selected /// to create multiple `_SelectableFragment`s so that they can be selected
/// separately. /// separately.
class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutMetrics { class _SelectableFragment with Selectable, Diagnosticable, ChangeNotifier implements TextLayoutMetrics {
_SelectableFragment({ _SelectableFragment({
required this.paragraph, required this.paragraph,
required this.fullText, required this.fullText,
...@@ -1366,7 +1372,6 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1366,7 +1372,6 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
? startOffsetInParagraphCoordinates ? startOffsetInParagraphCoordinates
: paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd)); : paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd));
final bool flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection); final bool flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection);
final Matrix4 paragraphToFragmentTransform = getTransformToParagraph()..invert();
final TextSelection selection = TextSelection( final TextSelection selection = TextSelection(
baseOffset: selectionStart, baseOffset: selectionStart,
extentOffset: selectionEnd, extentOffset: selectionEnd,
...@@ -1377,12 +1382,12 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1377,12 +1382,12 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
} }
return SelectionGeometry( return SelectionGeometry(
startSelectionPoint: SelectionPoint( startSelectionPoint: SelectionPoint(
localPosition: MatrixUtils.transformPoint(paragraphToFragmentTransform, startOffsetInParagraphCoordinates), localPosition: startOffsetInParagraphCoordinates,
lineHeight: paragraph._textPainter.preferredLineHeight, lineHeight: paragraph._textPainter.preferredLineHeight,
handleType: flipHandles ? TextSelectionHandleType.right : TextSelectionHandleType.left handleType: flipHandles ? TextSelectionHandleType.right : TextSelectionHandleType.left
), ),
endSelectionPoint: SelectionPoint( endSelectionPoint: SelectionPoint(
localPosition: MatrixUtils.transformPoint(paragraphToFragmentTransform, endOffsetInParagraphCoordinates), localPosition: endOffsetInParagraphCoordinates,
lineHeight: paragraph._textPainter.preferredLineHeight, lineHeight: paragraph._textPainter.preferredLineHeight,
handleType: flipHandles ? TextSelectionHandleType.left : TextSelectionHandleType.right, handleType: flipHandles ? TextSelectionHandleType.left : TextSelectionHandleType.right,
), ),
...@@ -1665,7 +1670,16 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1665,7 +1670,16 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
// we do not need to look up the word boundary for that position. This is to // we do not need to look up the word boundary for that position. This is to
// maintain a selectables selection collapsed at 0 when the local position is // maintain a selectables selection collapsed at 0 when the local position is
// not located inside its rect. // not located inside its rect.
final _WordBoundaryRecord? wordBoundary = !_rect.contains(localPosition) ? null : _getWordBoundaryAtPosition(position); _WordBoundaryRecord? wordBoundary = _rect.contains(localPosition) ? _getWordBoundaryAtPosition(position) : null;
if (wordBoundary != null
&& (wordBoundary.wordStart.offset < range.start && wordBoundary.wordEnd.offset <= range.start
|| wordBoundary.wordStart.offset >= range.end && wordBoundary.wordEnd.offset > range.end)) {
// When the position is located at a placeholder inside of the text, then we may compute
// a word boundary that does not belong to the current selectable fragment. In this case
// we should invalidate the word boundary so that it is not taken into account when
// computing the target position.
wordBoundary = null;
}
final TextPosition targetPosition = _clampTextPosition(isEnd ? _updateSelectionEndEdgeByWord(wordBoundary, position, existingSelectionStart, existingSelectionEnd) : _updateSelectionStartEdgeByWord(wordBoundary, position, existingSelectionStart, existingSelectionEnd)); final TextPosition targetPosition = _clampTextPosition(isEnd ? _updateSelectionEndEdgeByWord(wordBoundary, position, existingSelectionStart, existingSelectionEnd) : _updateSelectionStartEdgeByWord(wordBoundary, position, existingSelectionStart, existingSelectionEnd));
_setSelectionPosition(targetPosition, isEnd: isEnd); _setSelectionPosition(targetPosition, isEnd: isEnd);
...@@ -1717,16 +1731,18 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1717,16 +1731,18 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
} }
SelectionResult _handleSelectWord(Offset globalPosition) { SelectionResult _handleSelectWord(Offset globalPosition) {
_selectableContainsOriginWord = true;
final TextPosition position = paragraph.getPositionForOffset(paragraph.globalToLocal(globalPosition)); final TextPosition position = paragraph.getPositionForOffset(paragraph.globalToLocal(globalPosition));
if (_positionIsWithinCurrentSelection(position) && _textSelectionStart != _textSelectionEnd) { if (_positionIsWithinCurrentSelection(position) && _textSelectionStart != _textSelectionEnd) {
return SelectionResult.end; return SelectionResult.end;
} }
final _WordBoundaryRecord wordBoundary = _getWordBoundaryAtPosition(position); final _WordBoundaryRecord wordBoundary = _getWordBoundaryAtPosition(position);
if (wordBoundary.wordStart.offset < range.start && wordBoundary.wordEnd.offset < range.start) { // This fragment may not contain the word, decide what direction the target
// fragment is located in. Because fragments are separated by placeholder
// spans, we also check if the beginning or end of the word is touching
// either edge of this fragment.
if (wordBoundary.wordStart.offset < range.start && wordBoundary.wordEnd.offset <= range.start) {
return SelectionResult.previous; return SelectionResult.previous;
} else if (wordBoundary.wordStart.offset > range.end && wordBoundary.wordEnd.offset > range.end) { } else if (wordBoundary.wordStart.offset >= range.end && wordBoundary.wordEnd.offset > range.end) {
return SelectionResult.next; return SelectionResult.next;
} }
// Fragments are separated by placeholder span, the word boundary shouldn't // Fragments are separated by placeholder span, the word boundary shouldn't
...@@ -1734,6 +1750,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1734,6 +1750,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
assert(wordBoundary.wordStart.offset >= range.start && wordBoundary.wordEnd.offset <= range.end); assert(wordBoundary.wordStart.offset >= range.start && wordBoundary.wordEnd.offset <= range.end);
_textSelectionStart = wordBoundary.wordStart; _textSelectionStart = wordBoundary.wordStart;
_textSelectionEnd = wordBoundary.wordEnd; _textSelectionEnd = wordBoundary.wordEnd;
_selectableContainsOriginWord = true;
return SelectionResult.end; return SelectionResult.end;
} }
...@@ -1957,13 +1974,9 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1957,13 +1974,9 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
} }
} }
Matrix4 getTransformToParagraph() {
return Matrix4.translationValues(_rect.left, _rect.top, 0.0);
}
@override @override
Matrix4 getTransformTo(RenderObject? ancestor) { Matrix4 getTransformTo(RenderObject? ancestor) {
return getTransformToParagraph()..multiply(paragraph.getTransformTo(ancestor)); return paragraph.getTransformTo(ancestor);
} }
@override @override
...@@ -1982,6 +1995,28 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1982,6 +1995,28 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
} }
} }
List<Rect>? _cachedBoundingBoxes;
@override
List<Rect> get boundingBoxes {
if (_cachedBoundingBoxes == null) {
final List<TextBox> boxes = paragraph.getBoxesForSelection(
TextSelection(baseOffset: range.start, extentOffset: range.end),
);
if (boxes.isNotEmpty) {
_cachedBoundingBoxes = <Rect>[];
for (final TextBox textBox in boxes) {
_cachedBoundingBoxes!.add(textBox.toRect());
}
} else {
final Offset offset = paragraph._getOffsetForPosition(TextPosition(offset: range.start));
final Rect rect = Rect.fromPoints(offset, offset.translate(0, - paragraph._textPainter.preferredLineHeight));
_cachedBoundingBoxes = <Rect>[rect];
}
}
return _cachedBoundingBoxes!;
}
Rect? _cachedRect;
Rect get _rect { Rect get _rect {
if (_cachedRect == null) { if (_cachedRect == null) {
final List<TextBox> boxes = paragraph.getBoxesForSelection( final List<TextBox> boxes = paragraph.getBoxesForSelection(
...@@ -2000,7 +2035,6 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -2000,7 +2035,6 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
} }
return _cachedRect!; return _cachedRect!;
} }
Rect? _cachedRect;
void didChangeParagraphLayout() { void didChangeParagraphLayout() {
_cachedRect = null; _cachedRect = null;
...@@ -2028,12 +2062,11 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -2028,12 +2062,11 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
textBox.toRect().shift(offset), selectionPaint); textBox.toRect().shift(offset), selectionPaint);
} }
} }
final Matrix4 transform = getTransformToParagraph();
if (_startHandleLayerLink != null && value.startSelectionPoint != null) { if (_startHandleLayerLink != null && value.startSelectionPoint != null) {
context.pushLayer( context.pushLayer(
LeaderLayer( LeaderLayer(
link: _startHandleLayerLink!, link: _startHandleLayerLink!,
offset: offset + MatrixUtils.transformPoint(transform, value.startSelectionPoint!.localPosition), offset: offset + value.startSelectionPoint!.localPosition,
), ),
(PaintingContext context, Offset offset) { }, (PaintingContext context, Offset offset) { },
Offset.zero, Offset.zero,
...@@ -2043,7 +2076,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -2043,7 +2076,7 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
context.pushLayer( context.pushLayer(
LeaderLayer( LeaderLayer(
link: _endHandleLayerLink!, link: _endHandleLayerLink!,
offset: offset + MatrixUtils.transformPoint(transform, value.endSelectionPoint!.localPosition), offset: offset + value.endSelectionPoint!.localPosition,
), ),
(PaintingContext context, Offset offset) { }, (PaintingContext context, Offset offset) { },
Offset.zero, Offset.zero,
...@@ -2071,4 +2104,12 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -2071,4 +2104,12 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
@override @override
TextRange getWordBoundary(TextPosition position) => paragraph.getWordBoundary(position); TextRange getWordBoundary(TextPosition position) => paragraph.getWordBoundary(position);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<String>('textInsideRange', range.textInside(fullText)));
properties.add(DiagnosticsProperty<TextRange>('range', range));
properties.add(DiagnosticsProperty<String>('fullText', fullText));
}
} }
...@@ -142,6 +142,10 @@ mixin Selectable implements SelectionHandler { ...@@ -142,6 +142,10 @@ mixin Selectable implements SelectionHandler {
/// The size of this [Selectable]. /// The size of this [Selectable].
Size get size; Size get size;
/// A list of [Rect]s that represent the bounding box of this [Selectable]
/// in local coordinates.
List<Rect> get boundingBoxes;
/// Disposes resources held by the mixer. /// Disposes resources held by the mixer.
void dispose(); void dispose();
} }
......
...@@ -1817,6 +1817,14 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai ...@@ -1817,6 +1817,14 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
_updateHandleLayersAndOwners(); _updateHandleLayersAndOwners();
} }
Rect _getBoundingBox(Selectable selectable) {
Rect result = selectable.boundingBoxes.first;
for (int index = 1; index < selectable.boundingBoxes.length; index += 1) {
result = result.expandToInclude(selectable.boundingBoxes[index]);
}
return result;
}
/// The compare function this delegate used for determining the selection /// The compare function this delegate used for determining the selection
/// order of the selectables. /// order of the selectables.
/// ///
...@@ -1827,11 +1835,11 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai ...@@ -1827,11 +1835,11 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
int _compareScreenOrder(Selectable a, Selectable b) { int _compareScreenOrder(Selectable a, Selectable b) {
final Rect rectA = MatrixUtils.transformRect( final Rect rectA = MatrixUtils.transformRect(
a.getTransformTo(null), a.getTransformTo(null),
Rect.fromLTWH(0, 0, a.size.width, a.size.height), _getBoundingBox(a),
); );
final Rect rectB = MatrixUtils.transformRect( final Rect rectB = MatrixUtils.transformRect(
b.getTransformTo(null), b.getTransformTo(null),
Rect.fromLTWH(0, 0, b.size.width, b.size.height), _getBoundingBox(b),
); );
final int result = _compareVertically(rectA, rectB); final int result = _compareVertically(rectA, rectB);
if (result != 0) { if (result != 0) {
...@@ -1846,6 +1854,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai ...@@ -1846,6 +1854,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
/// Returns positive if a is lower, negative if a is higher, 0 if their /// Returns positive if a is lower, negative if a is higher, 0 if their
/// order can't be determine solely by their vertical position. /// order can't be determine solely by their vertical position.
static int _compareVertically(Rect a, Rect b) { static int _compareVertically(Rect a, Rect b) {
// The rectangles overlap so defer to horizontal comparison.
if ((a.top - b.top < _kSelectableVerticalComparingThreshold && a.bottom - b.bottom > - _kSelectableVerticalComparingThreshold) || if ((a.top - b.top < _kSelectableVerticalComparingThreshold && a.bottom - b.bottom > - _kSelectableVerticalComparingThreshold) ||
(b.top - a.top < _kSelectableVerticalComparingThreshold && b.bottom - a.bottom > - _kSelectableVerticalComparingThreshold)) { (b.top - a.top < _kSelectableVerticalComparingThreshold && b.bottom - a.bottom > - _kSelectableVerticalComparingThreshold)) {
return 0; return 0;
...@@ -1863,19 +1872,10 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai ...@@ -1863,19 +1872,10 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
static int _compareHorizontally(Rect a, Rect b) { static int _compareHorizontally(Rect a, Rect b) {
// a encloses b. // a encloses b.
if (a.left - b.left < precisionErrorTolerance && a.right - b.right > - precisionErrorTolerance) { if (a.left - b.left < precisionErrorTolerance && a.right - b.right > - precisionErrorTolerance) {
// b ends before a.
if (a.right - b.right > precisionErrorTolerance) {
return 1;
}
return -1; return -1;
} }
// b encloses a. // b encloses a.
if (b.left - a.left < precisionErrorTolerance && b.right - a.right > - precisionErrorTolerance) { if (b.left - a.left < precisionErrorTolerance && b.right - a.right > - precisionErrorTolerance) {
// a ends before b.
if (b.right - a.right > precisionErrorTolerance) {
return -1;
}
return 1; return 1;
} }
if ((a.left - b.left).abs() > precisionErrorTolerance) { if ((a.left - b.left).abs() > precisionErrorTolerance) {
...@@ -2140,10 +2140,17 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai ...@@ -2140,10 +2140,17 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
SelectionResult handleSelectWord(SelectWordSelectionEvent event) { SelectionResult handleSelectWord(SelectWordSelectionEvent event) {
SelectionResult? lastSelectionResult; SelectionResult? lastSelectionResult;
for (int index = 0; index < selectables.length; index += 1) { for (int index = 0; index < selectables.length; index += 1) {
final Rect localRect = Rect.fromLTWH(0, 0, selectables[index].size.width, selectables[index].size.height); bool globalRectsContainsPosition = false;
final Matrix4 transform = selectables[index].getTransformTo(null); if (selectables[index].boundingBoxes.isNotEmpty) {
final Rect globalRect = MatrixUtils.transformRect(transform, localRect); for (final Rect rect in selectables[index].boundingBoxes) {
if (globalRect.contains(event.globalPosition)) { final Rect globalRect = MatrixUtils.transformRect(selectables[index].getTransformTo(null), rect);
if (globalRect.contains(event.globalPosition)) {
globalRectsContainsPosition = true;
break;
}
}
}
if (globalRectsContainsPosition) {
final SelectionGeometry existingGeometry = selectables[index].value; final SelectionGeometry existingGeometry = selectables[index].value;
lastSelectionResult = dispatchSelectionEventToChild(selectables[index], event); lastSelectionResult = dispatchSelectionEventToChild(selectables[index], event);
if (index == selectables.length - 1 && lastSelectionResult == SelectionResult.next) { if (index == selectables.length - 1 && lastSelectionResult == SelectionResult.next) {
......
...@@ -200,6 +200,9 @@ class _SelectionContainerState extends State<SelectionContainer> with Selectable ...@@ -200,6 +200,9 @@ class _SelectionContainerState extends State<SelectionContainer> with Selectable
@override @override
Size get size => (context.findRenderObject()! as RenderBox).size; Size get size => (context.findRenderObject()! as RenderBox).size;
@override
List<Rect> get boundingBoxes => <Rect>[(context.findRenderObject()! as RenderBox).paintBounds];
@override @override
void dispose() { void dispose() {
if (!widget._disabled) { if (!widget._disabled) {
......
...@@ -90,7 +90,7 @@ void main() { ...@@ -90,7 +90,7 @@ void main() {
), ),
); );
final TestGesture longpress = await tester.startGesture(const Offset(10, 10)); final TestGesture longpress = await tester.startGesture(tester.getCenter(find.byType(Text)));
addTearDown(longpress.removePointer); addTearDown(longpress.removePointer);
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
await longpress.up(); await longpress.up();
......
...@@ -138,9 +138,14 @@ class RenderSelectionSpy extends RenderProxyBox ...@@ -138,9 +138,14 @@ class RenderSelectionSpy extends RenderProxyBox
Size get size => _size; Size get size => _size;
Size _size = Size.zero; Size _size = Size.zero;
@override
List<Rect> get boundingBoxes => _boundingBoxes;
final List<Rect> _boundingBoxes = <Rect>[];
@override @override
Size computeDryLayout(BoxConstraints constraints) { Size computeDryLayout(BoxConstraints constraints) {
_size = Size(constraints.maxWidth, constraints.maxHeight); _size = Size(constraints.maxWidth, constraints.maxHeight);
_boundingBoxes.add(Rect.fromLTWH(0.0, 0.0, constraints.maxWidth, constraints.maxHeight));
return _size; return _size;
} }
......
...@@ -1869,11 +1869,11 @@ void main() { ...@@ -1869,11 +1869,11 @@ void main() {
child: Center( child: Center(
child: Text.rich( child: Text.rich(
const TextSpan( const TextSpan(
children: <InlineSpan>[ children: <InlineSpan>[
TextSpan(text: 'How are you?'), TextSpan(text: 'How are you?'),
WidgetSpan(child: Text('Good, and you?')), WidgetSpan(child: Text('Good, and you?')),
TextSpan(text: 'Fine, thank you.'), TextSpan(text: 'Fine, thank you.'),
] ],
), ),
key: outerText, key: outerText,
), ),
...@@ -1897,6 +1897,198 @@ void main() { ...@@ -1897,6 +1897,198 @@ void main() {
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020 skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
); );
testWidgetsWithLeakTracking(
'double click + drag mouse selection can handle widget span', (WidgetTester tester) async {
final UniqueKey outerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Center(
child: Text.rich(
const TextSpan(
children: <InlineSpan>[
TextSpan(text: 'How are you?'),
WidgetSpan(child: Text('Good, and you?')),
TextSpan(text: 'Fine, thank you.'),
],
),
key: outerText,
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph, 0));
await tester.pump();
await gesture.moveTo(textOffsetToPosition(paragraph, 17)); // right after `Fine`.
await gesture.up();
// keyboard copy.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true));
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
expect(clipboardData['text'], 'How are you?Good, and you?Fine,');
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia }),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgetsWithLeakTracking(
'double click + drag mouse selection can handle widget span - multiline', (WidgetTester tester) async {
final UniqueKey outerText = UniqueKey();
final UniqueKey innerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Center(
child: Text.rich(
TextSpan(
children: <InlineSpan>[
const TextSpan(text: 'How are you\n?'),
WidgetSpan(
child: Text(
'Good, and you?',
key: innerText,
),
),
const TextSpan(text: 'Fine, thank you.'),
],
),
key: outerText,
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
final RenderParagraph innerParagraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(innerText), matching: find.byType(RichText)).first);
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 0), kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(textOffsetToPosition(paragraph, 0));
await tester.pump();
await gesture.moveTo(textOffsetToPosition(innerParagraph, 2)); // on `Good`.
// Should not crash.
expect(tester.takeException(), isNull);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia }),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgetsWithLeakTracking(
'select word event can select inline widget', (WidgetTester tester) async {
final UniqueKey outerText = UniqueKey();
final UniqueKey innerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Center(
child: Text.rich(
TextSpan(
children: <InlineSpan>[
const TextSpan(text: 'How are\n you?'),
WidgetSpan(
child: Text(
'Good, and you?',
key: innerText,
),
),
const TextSpan(text: 'Fine, thank you.'),
],
),
key: outerText,
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
final RenderParagraph innerParagraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(innerText), matching: find.byType(RichText)).first);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(innerText)), kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
// Should select "and".
expect(paragraph.selections.isEmpty, isTrue);
expect(innerParagraph.selections[0], const TextSelection(baseOffset: 6, extentOffset: 9));
},
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgetsWithLeakTracking(
'select word event should not crash when its position is at an unselectable inline element', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final UniqueKey flutterLogo = UniqueKey();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Scaffold(
body: Center(
child: Text.rich(
TextSpan(
children: <InlineSpan>[
const TextSpan(
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
),
WidgetSpan(child: FlutterLogo(key: flutterLogo)),
const TextSpan(text: 'Hello, world.'),
],
),
),
),
),
),
),
);
final Offset gestureOffset = tester.getCenter(find.byKey(flutterLogo).first);
// Right click on unseletable element.
final TestGesture gesture = await tester.startGesture(gestureOffset, kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
// Should not crash.
expect(tester.takeException(), isNull);
},
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgetsWithLeakTracking( testWidgetsWithLeakTracking(
'can select word when a selectables rect is completely inside of another selectables rect', (WidgetTester tester) async { 'can select word when a selectables rect is completely inside of another selectables rect', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/127076. // Regression test for https://github.com/flutter/flutter/issues/127076.
...@@ -1913,14 +2105,64 @@ void main() { ...@@ -1913,14 +2105,64 @@ void main() {
body: Center( body: Center(
child: Text.rich( child: Text.rich(
const TextSpan( const TextSpan(
children: <InlineSpan>[ children: <InlineSpan>[
TextSpan( TextSpan(
text: text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
), ),
WidgetSpan(child: Text('Some text in a WidgetSpan. ')), WidgetSpan(child: Text('Some text in a WidgetSpan. ')),
TextSpan(text: 'Hello, world.'), TextSpan(text: 'Hello, world.'),
], ],
),
key: outerText,
),
),
),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.byKey(outerText), matching: find.byType(RichText)).first);
// Adjust `textOffsetToPosition` result because it returns the wrong vertical position (wrong line).
// TODO(bleroux): Remove when https://github.com/flutter/flutter/issues/133637 is fixed.
final Offset gestureOffset = textOffsetToPosition(paragraph, 125).translate(0, 10);
// Right click to select word at position.
final TestGesture gesture = await tester.startGesture(gestureOffset, kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton);
addTearDown(gesture.removePointer);
await tester.pump();
await gesture.up();
await tester.pump();
// Should select "Hello".
expect(paragraph.selections[0], const TextSelection(baseOffset: 124, extentOffset: 129));
},
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
skip: isBrowser, // https://github.com/flutter/flutter/issues/61020
);
testWidgetsWithLeakTracking(
'can select word when selectable is broken up by an unselectable WidgetSpan', (WidgetTester tester) async {
final UniqueKey outerText = UniqueKey();
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: focusNode,
selectionControls: materialTextSelectionControls,
child: Scaffold(
body: Center(
child: Text.rich(
const TextSpan(
children: <InlineSpan>[
TextSpan(
text:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
),
WidgetSpan(child: SizedBox.shrink()),
TextSpan(text: 'Hello, world.'),
],
), ),
key: outerText, key: outerText,
), ),
...@@ -1963,11 +2205,11 @@ void main() { ...@@ -1963,11 +2205,11 @@ void main() {
child: Center( child: Center(
child: Text.rich( child: Text.rich(
const TextSpan( const TextSpan(
children: <InlineSpan>[ children: <InlineSpan>[
TextSpan(text: 'How are you?'), TextSpan(text: 'How are you?'),
WidgetSpan(child: Placeholder()), WidgetSpan(child: Placeholder()),
TextSpan(text: 'Fine, thank you.'), TextSpan(text: 'Fine, thank you.'),
] ],
), ),
key: outerText, key: outerText,
), ),
...@@ -2006,11 +2248,11 @@ void main() { ...@@ -2006,11 +2248,11 @@ void main() {
child: Center( child: Center(
child: Text.rich( child: Text.rich(
const TextSpan( const TextSpan(
children: <InlineSpan>[ children: <InlineSpan>[
TextSpan(text: 'How are you?'), TextSpan(text: 'How are you?'),
WidgetSpan(child: Placeholder()), WidgetSpan(child: Placeholder()),
TextSpan(text: 'Fine, thank you.'), TextSpan(text: 'Fine, thank you.'),
] ],
), ),
key: outerText, key: outerText,
), ),
...@@ -3542,6 +3784,9 @@ class RenderSelectionSpy extends RenderProxyBox ...@@ -3542,6 +3784,9 @@ class RenderSelectionSpy extends RenderProxyBox
Size get size => _size; Size get size => _size;
Size _size = Size.zero; Size _size = Size.zero;
@override
List<Rect> get boundingBoxes => <Rect>[paintBounds];
@override @override
Size computeDryLayout(BoxConstraints constraints) { Size computeDryLayout(BoxConstraints constraints) {
_size = Size(constraints.maxWidth, constraints.maxHeight); _size = Size(constraints.maxWidth, constraints.maxHeight);
...@@ -3610,6 +3855,9 @@ class RenderSelectAll extends RenderProxyBox ...@@ -3610,6 +3855,9 @@ class RenderSelectAll extends RenderProxyBox
this.registrar = registrar; this.registrar = registrar;
} }
@override
List<Rect> get boundingBoxes => <Rect>[paintBounds];
final Set<VoidCallback> listeners = <VoidCallback>{}; final Set<VoidCallback> listeners = <VoidCallback>{};
LayerLink? startHandle; LayerLink? startHandle;
LayerLink? endHandle; LayerLink? endHandle;
......
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