Unverified Commit baafc668 authored by chunhtai's avatar chunhtai Committed by GitHub

Enable gesture recognizer in selectable rich text (#54479)

parent b00b4e83
...@@ -297,9 +297,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -297,9 +297,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
/// Called during the paint phase when the caret location changes. /// Called during the paint phase when the caret location changes.
CaretChangedHandler onCaretChanged; CaretChangedHandler onCaretChanged;
/// If true [handleEvent] does nothing and it's assumed that this /// Whether the [handleEvent] will propagate pointer events to selection
/// renderer will be notified of input gestures via [handleTapDown], /// handlers.
/// [handleTap], [handleDoubleTap], and [handleLongPress]. ///
/// If this property is true, the [handleEvent] assumes that this renderer
/// will be notified of input gestures via [handleTapDown], [handleTap],
/// [handleDoubleTap], and [handleLongPress].
///
/// If there are any gesture recognizers in the text span, the [handleEvent]
/// will still propagate pointer events to those recognizers
/// ///
/// The default value of this property is false. /// The default value of this property is false.
bool ignorePointer; bool ignorePointer;
...@@ -1544,14 +1550,25 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1544,14 +1550,25 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
@override @override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) { void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
if (ignorePointer)
return;
assert(debugHandleEvent(event, entry)); assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && onSelectionChanged != null) { if (event is PointerDownEvent) {
assert(!debugNeedsLayout);
// Checks if there is any gesture recognizer in the text span.
final Offset offset = entry.localPosition;
final TextPosition position = _textPainter.getPositionForOffset(offset);
final InlineSpan span = _textPainter.text.getSpanForPosition(position);
if (span != null && span is TextSpan) {
final TextSpan textSpan = span;
textSpan.recognizer?.addPointer(event);
}
if (!ignorePointer && onSelectionChanged != null) {
// Propagates the pointer event to selection handlers.
_tap.addPointer(event); _tap.addPointer(event);
_longPress.addPointer(event); _longPress.addPointer(event);
} }
} }
}
Offset _lastTapDownPosition; Offset _lastTapDownPosition;
......
...@@ -36,6 +36,8 @@ void main() { ...@@ -36,6 +36,8 @@ void main() {
), ),
onSelectionChanged: (_, __, ___) { }, onSelectionChanged: (_, __, ___) { },
); );
editable.layout(BoxConstraints.loose(const Size(1000.0, 1000.0)));
final PipelineOwner owner = PipelineOwner(onNeedVisualUpdate: () { }); final PipelineOwner owner = PipelineOwner(onNeedVisualUpdate: () { });
final _PointerRouterSpy spy = GestureBinding.instance.pointerRouter as _PointerRouterSpy; final _PointerRouterSpy spy = GestureBinding.instance.pointerRouter as _PointerRouterSpy;
editable.attach(owner); editable.attach(owner);
......
...@@ -10,7 +10,7 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -10,7 +10,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind; import 'package:flutter/gestures.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
...@@ -3743,4 +3743,105 @@ void main() { ...@@ -3743,4 +3743,105 @@ void main() {
expect(editableText.selectionOverlay.handlesAreVisible, isFalse); expect(editableText.selectionOverlay.handlesAreVisible, isFalse);
}, },
); );
testWidgets('text span with tap gesture recognizer works in selectable rich text', (WidgetTester tester) async {
int spyTaps = 0;
final TapGestureRecognizer spyRecognizer = TapGestureRecognizer()
..onTap = () {
spyTaps += 1;
};
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SelectableText.rich(
TextSpan(
children: <TextSpan>[
const TextSpan(text: 'Atwater '),
TextSpan(text: 'Peel', recognizer: spyRecognizer),
const TextSpan(text: ' Sherbrooke Bonaventure'),
],
),
),
),
),
),
);
expect(spyTaps, 0);
final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));
await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
expect(spyTaps, 1);
// Waits for a while to avoid double taps.
await tester.pump(const Duration(seconds: 1));
// Starts a long press.
final TestGesture gesture =
await tester.startGesture(selectableTextStart + const Offset(150.0, 5.0));
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump();
final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
final TextEditingController controller = editableTextWidget.controller;
// Long press still triggers selection.
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
// Long press does not trigger gesture recognizer.
expect(spyTaps, 1);
});
testWidgets('text span with long press gesture recognizer works in selectable rich text', (WidgetTester tester) async {
int spyLongPress = 0;
final LongPressGestureRecognizer spyRecognizer = LongPressGestureRecognizer()
..onLongPress = () {
spyLongPress += 1;
};
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SelectableText.rich(
TextSpan(
children: <TextSpan>[
const TextSpan(text: 'Atwater '),
TextSpan(text: 'Peel', recognizer: spyRecognizer),
const TextSpan(text: ' Sherbrooke Bonaventure'),
],
),
),
),
),
),
);
expect(spyLongPress, 0);
final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));
await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0));
expect(spyLongPress, 0);
// Waits for a while to avoid double taps.
await tester.pump(const Duration(seconds: 1));
// Starts a long press.
final TestGesture gesture =
await tester.startGesture(selectableTextStart + const Offset(150.0, 5.0));
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump();
final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
final TextEditingController controller = editableTextWidget.controller;
// Long press does not trigger selection if there is text span with long
// press recognizer.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 11, affinity: TextAffinity.upstream),
);
// Long press triggers gesture recognizer.
expect(spyLongPress, 1);
});
} }
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