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

Add selection feedback for both selection area and text field (#115373)

* Add selection feedback for both selection area and text field

* Addressing comment

* Fixes more test
parent a1ea383f
...@@ -52,6 +52,19 @@ class TextSelectionPoint { ...@@ -52,6 +52,19 @@ class TextSelectionPoint {
/// Direction of the text at this edge of the selection. /// Direction of the text at this edge of the selection.
final TextDirection? direction; final TextDirection? direction;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is TextSelectionPoint
&& other.point == point
&& other.direction == direction;
}
@override @override
String toString() { String toString() {
switch (direction) { switch (direction) {
...@@ -63,6 +76,10 @@ class TextSelectionPoint { ...@@ -63,6 +76,10 @@ class TextSelectionPoint {
return '$point'; return '$point';
} }
} }
@override
int get hashCode => Object.hash(point, direction);
} }
/// The consecutive sequence of [TextPosition]s that the caret should move to /// The consecutive sequence of [TextPosition]s that the caret should move to
......
...@@ -467,6 +467,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -467,6 +467,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
} }
void _handleTouchLongPressStart(LongPressStartDetails details) { void _handleTouchLongPressStart(LongPressStartDetails details) {
HapticFeedback.selectionClick();
widget.focusNode.requestFocus(); widget.focusNode.requestFocus();
_selectWordAt(offset: details.globalPosition); _selectWordAt(offset: details.globalPosition);
_showToolbar(); _showToolbar();
...@@ -537,6 +538,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -537,6 +538,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
}, },
); );
} }
_stopSelectionStartEdgeUpdate();
_stopSelectionEndEdgeUpdate(); _stopSelectionEndEdgeUpdate();
_updateSelectedContentIfNeeded(); _updateSelectedContentIfNeeded();
} }
......
...@@ -515,6 +515,11 @@ class TextSelectionOverlay { ...@@ -515,6 +515,11 @@ class TextSelectionOverlay {
} }
_value = newValue; _value = newValue;
_updateSelectionOverlay(); _updateSelectionOverlay();
// _updateSelectionOverlay may not rebuild the selection overlay if the
// text metrics and selection doesn't change even if the text has changed.
// This rebuild is needed for the toolbar to update based on the latest text
// value.
_selectionOverlay.markNeedsBuild();
} }
void _updateSelectionOverlay() { void _updateSelectionOverlay() {
...@@ -541,7 +546,13 @@ class TextSelectionOverlay { ...@@ -541,7 +546,13 @@ class TextSelectionOverlay {
/// ///
/// This is intended to be called when the [renderObject] may have changed its /// This is intended to be called when the [renderObject] may have changed its
/// text metrics (e.g. because the text was scrolled). /// text metrics (e.g. because the text was scrolled).
void updateForScroll() => _updateSelectionOverlay(); void updateForScroll() {
_updateSelectionOverlay();
// This method may be called due to windows metrics changes. In that case,
// non of the properties in _selectionOverlay will change, but a rebuild is
// still needed.
_selectionOverlay.markNeedsBuild();
}
/// Whether the handles are currently visible. /// Whether the handles are currently visible.
bool get handlesAreVisible => _selectionOverlay._handles != null && handlesVisible; bool get handlesAreVisible => _selectionOverlay._handles != null && handlesVisible;
...@@ -1030,7 +1041,7 @@ class SelectionOverlay { ...@@ -1030,7 +1041,7 @@ class SelectionOverlay {
return; return;
} }
_startHandleType = value; _startHandleType = value;
_markNeedsBuild(); markNeedsBuild();
} }
/// The line height at the selection start. /// The line height at the selection start.
...@@ -1045,9 +1056,11 @@ class SelectionOverlay { ...@@ -1045,9 +1056,11 @@ class SelectionOverlay {
return; return;
} }
_lineHeightAtStart = value; _lineHeightAtStart = value;
_markNeedsBuild(); markNeedsBuild();
} }
bool _isDraggingStartHandle = false;
/// Whether the start handle is visible. /// Whether the start handle is visible.
/// ///
/// If the value changes, the start handle uses [FadeTransition] to transition /// If the value changes, the start handle uses [FadeTransition] to transition
...@@ -1059,6 +1072,12 @@ class SelectionOverlay { ...@@ -1059,6 +1072,12 @@ class SelectionOverlay {
/// Called when the users start dragging the start selection handles. /// Called when the users start dragging the start selection handles.
final ValueChanged<DragStartDetails>? onStartHandleDragStart; final ValueChanged<DragStartDetails>? onStartHandleDragStart;
void _handleStartHandleDragStart(DragStartDetails details) {
assert(!_isDraggingStartHandle);
_isDraggingStartHandle = details.kind == PointerDeviceKind.touch;
onStartHandleDragStart?.call(details);
}
/// Called when the users drag the start selection handles to new locations. /// Called when the users drag the start selection handles to new locations.
final ValueChanged<DragUpdateDetails>? onStartHandleDragUpdate; final ValueChanged<DragUpdateDetails>? onStartHandleDragUpdate;
...@@ -1066,6 +1085,11 @@ class SelectionOverlay { ...@@ -1066,6 +1085,11 @@ class SelectionOverlay {
/// handles. /// handles.
final ValueChanged<DragEndDetails>? onStartHandleDragEnd; final ValueChanged<DragEndDetails>? onStartHandleDragEnd;
void _handleStartHandleDragEnd(DragEndDetails details) {
_isDraggingStartHandle = false;
onStartHandleDragEnd?.call(details);
}
/// The type of end selection handle. /// The type of end selection handle.
/// ///
/// Changing the value while the handles are visible causes them to rebuild. /// Changing the value while the handles are visible causes them to rebuild.
...@@ -1076,7 +1100,7 @@ class SelectionOverlay { ...@@ -1076,7 +1100,7 @@ class SelectionOverlay {
return; return;
} }
_endHandleType = value; _endHandleType = value;
_markNeedsBuild(); markNeedsBuild();
} }
/// The line height at the selection end. /// The line height at the selection end.
...@@ -1091,9 +1115,11 @@ class SelectionOverlay { ...@@ -1091,9 +1115,11 @@ class SelectionOverlay {
return; return;
} }
_lineHeightAtEnd = value; _lineHeightAtEnd = value;
_markNeedsBuild(); markNeedsBuild();
} }
bool _isDraggingEndHandle = false;
/// Whether the end handle is visible. /// Whether the end handle is visible.
/// ///
/// If the value changes, the end handle uses [FadeTransition] to transition /// If the value changes, the end handle uses [FadeTransition] to transition
...@@ -1105,6 +1131,12 @@ class SelectionOverlay { ...@@ -1105,6 +1131,12 @@ class SelectionOverlay {
/// Called when the users start dragging the end selection handles. /// Called when the users start dragging the end selection handles.
final ValueChanged<DragStartDetails>? onEndHandleDragStart; final ValueChanged<DragStartDetails>? onEndHandleDragStart;
void _handleEndHandleDragStart(DragStartDetails details) {
assert(!_isDraggingEndHandle);
_isDraggingEndHandle = details.kind == PointerDeviceKind.touch;
onEndHandleDragStart?.call(details);
}
/// Called when the users drag the end selection handles to new locations. /// Called when the users drag the end selection handles to new locations.
final ValueChanged<DragUpdateDetails>? onEndHandleDragUpdate; final ValueChanged<DragUpdateDetails>? onEndHandleDragUpdate;
...@@ -1112,6 +1144,11 @@ class SelectionOverlay { ...@@ -1112,6 +1144,11 @@ class SelectionOverlay {
/// handles. /// handles.
final ValueChanged<DragEndDetails>? onEndHandleDragEnd; final ValueChanged<DragEndDetails>? onEndHandleDragEnd;
void _handleEndHandleDragEnd(DragEndDetails details) {
_isDraggingEndHandle = false;
onEndHandleDragEnd?.call(details);
}
/// Whether the toolbar is visible. /// Whether the toolbar is visible.
/// ///
/// If the value changes, the toolbar uses [FadeTransition] to transition /// If the value changes, the toolbar uses [FadeTransition] to transition
...@@ -1125,7 +1162,21 @@ class SelectionOverlay { ...@@ -1125,7 +1162,21 @@ class SelectionOverlay {
List<TextSelectionPoint> _selectionEndpoints; List<TextSelectionPoint> _selectionEndpoints;
set selectionEndpoints(List<TextSelectionPoint> value) { set selectionEndpoints(List<TextSelectionPoint> value) {
if (!listEquals(_selectionEndpoints, value)) { if (!listEquals(_selectionEndpoints, value)) {
_markNeedsBuild(); markNeedsBuild();
if ((_isDraggingEndHandle || _isDraggingStartHandle) &&
_startHandleType != TextSelectionHandleType.collapsed) {
switch(defaultTargetPlatform) {
case TargetPlatform.android:
HapticFeedback.selectionClick();
break;
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
}
} }
_selectionEndpoints = value; _selectionEndpoints = value;
} }
...@@ -1220,7 +1271,7 @@ class SelectionOverlay { ...@@ -1220,7 +1271,7 @@ class SelectionOverlay {
return; return;
} }
_toolbarLocation = value; _toolbarLocation = value;
_markNeedsBuild(); markNeedsBuild();
} }
/// Controls the fade-in and fade-out animations for the toolbar and handles. /// Controls the fade-in and fade-out animations for the toolbar and handles.
...@@ -1250,7 +1301,6 @@ class SelectionOverlay { ...@@ -1250,7 +1301,6 @@ class SelectionOverlay {
OverlayEntry(builder: _buildStartHandle), OverlayEntry(builder: _buildStartHandle),
OverlayEntry(builder: _buildEndHandle), OverlayEntry(builder: _buildEndHandle),
]; ];
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor).insertAll(_handles!); Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor).insertAll(_handles!);
} }
...@@ -1299,7 +1349,9 @@ class SelectionOverlay { ...@@ -1299,7 +1349,9 @@ class SelectionOverlay {
} }
bool _buildScheduled = false; bool _buildScheduled = false;
void _markNeedsBuild() {
/// Rebuilds the selection toolbar or handles if they are present.
void markNeedsBuild() {
if (_handles == null && _toolbar == null) { if (_handles == null && _toolbar == null) {
return; return;
} }
...@@ -1379,9 +1431,9 @@ class SelectionOverlay { ...@@ -1379,9 +1431,9 @@ class SelectionOverlay {
type: _startHandleType, type: _startHandleType,
handleLayerLink: startHandleLayerLink, handleLayerLink: startHandleLayerLink,
onSelectionHandleTapped: onSelectionHandleTapped, onSelectionHandleTapped: onSelectionHandleTapped,
onSelectionHandleDragStart: onStartHandleDragStart, onSelectionHandleDragStart: _handleStartHandleDragStart,
onSelectionHandleDragUpdate: onStartHandleDragUpdate, onSelectionHandleDragUpdate: onStartHandleDragUpdate,
onSelectionHandleDragEnd: onStartHandleDragEnd, onSelectionHandleDragEnd: _handleStartHandleDragEnd,
selectionControls: selectionControls, selectionControls: selectionControls,
visibility: startHandlesVisible, visibility: startHandlesVisible,
preferredLineHeight: _lineHeightAtStart, preferredLineHeight: _lineHeightAtStart,
...@@ -1406,9 +1458,9 @@ class SelectionOverlay { ...@@ -1406,9 +1458,9 @@ class SelectionOverlay {
type: _endHandleType, type: _endHandleType,
handleLayerLink: endHandleLayerLink, handleLayerLink: endHandleLayerLink,
onSelectionHandleTapped: onSelectionHandleTapped, onSelectionHandleTapped: onSelectionHandleTapped,
onSelectionHandleDragStart: onEndHandleDragStart, onSelectionHandleDragStart: _handleEndHandleDragStart,
onSelectionHandleDragUpdate: onEndHandleDragUpdate, onSelectionHandleDragUpdate: onEndHandleDragUpdate,
onSelectionHandleDragEnd: onEndHandleDragEnd, onSelectionHandleDragEnd: _handleEndHandleDragEnd,
selectionControls: selectionControls, selectionControls: selectionControls,
visibility: endHandlesVisible, visibility: endHandlesVisible,
preferredLineHeight: _lineHeightAtEnd, preferredLineHeight: _lineHeightAtEnd,
......
...@@ -2477,6 +2477,61 @@ void main() { ...@@ -2477,6 +2477,61 @@ void main() {
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets('Drag handles trigger feedback', (WidgetTester tester) async {
final FeedbackTester feedback = FeedbackTester();
addTearDown(feedback.dispose);
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
expect(feedback.hapticCount, 0);
await skipPastScrollingAnimation(tester);
// Long press the 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
final TextSelection selection = controller.selection;
expect(selection.baseOffset, 4);
expect(selection.extentOffset, 7);
expect(feedback.hapticCount, 1);
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
expect(endpoints.length, 2);
// Drag the right handle 2 letters to the right.
// Use a small offset because the endpoint is on the very corner
// of the handle.
final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
final Offset newHandlePos = textOffsetToPosition(tester, testValue.length);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 11);
expect(feedback.hapticCount, 2);
});
testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async { testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
...@@ -4965,6 +5020,7 @@ void main() { ...@@ -4965,6 +5020,7 @@ void main() {
testWidgets('haptic feedback', (WidgetTester tester) async { testWidgets('haptic feedback', (WidgetTester tester) async {
final FeedbackTester feedback = FeedbackTester(); final FeedbackTester feedback = FeedbackTester();
addTearDown(feedback.dispose);
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4987,8 +5043,6 @@ void main() { ...@@ -4987,8 +5043,6 @@ void main() {
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0); expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 1); expect(feedback.hapticCount, 1);
feedback.dispose();
}); });
testWidgets('Text field drops selection color when losing focus', (WidgetTester tester) async { testWidgets('Text field drops selection color when losing focus', (WidgetTester tester) async {
......
...@@ -915,6 +915,20 @@ void main() { ...@@ -915,6 +915,20 @@ void main() {
expect(endpoints[0].point.dx, 0); expect(endpoints[0].point.dx, 0);
}); });
test('TextSelectionPoint can compare', () {
// ignore: prefer_const_constructors
final TextSelectionPoint first = TextSelectionPoint(Offset(1, 2), TextDirection.ltr);
// ignore: prefer_const_constructors
final TextSelectionPoint second = TextSelectionPoint(Offset(1, 2), TextDirection.ltr);
expect(first == second, isTrue);
expect(first.hashCode == second.hashCode, isTrue);
// ignore: prefer_const_constructors
final TextSelectionPoint different = TextSelectionPoint(Offset(2, 2), TextDirection.ltr);
expect(first == different, isFalse);
expect(first.hashCode == different.hashCode, isFalse);
});
group('getRectForComposingRange', () { group('getRectForComposingRange', () {
const TextSpan emptyTextSpan = TextSpan(text: '\u200e'); const TextSpan emptyTextSpan = TextSpan(text: '\u200e');
final TextSelectionDelegate delegate = _FakeEditableTextState(); final TextSelectionDelegate delegate = _FakeEditableTextState();
......
...@@ -463,6 +463,7 @@ void main() { ...@@ -463,6 +463,7 @@ void main() {
final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]); final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]);
expect(boxes.length, 1); expect(boxes.length, 1);
// Find end handle.
final Offset handlePos = globalize(boxes[0].toRect().bottomRight, paragraph0); final Offset handlePos = globalize(boxes[0].toRect().bottomRight, paragraph0);
await gesture.down(handlePos); await gesture.down(handlePos);
...@@ -493,6 +494,113 @@ void main() { ...@@ -493,6 +494,113 @@ void main() {
await gesture.up(); await gesture.up();
}); });
testWidgets('select to scroll by dragging start selection handle stops scroll when released', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(MaterialApp(
home: SelectionArea(
selectionControls: materialTextSelectionControls,
child: ListView.builder(
controller: controller,
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Text('Item $index');
},
),
),
));
await tester.pumpAndSettle();
// Long press to bring up the selection handles.
final RenderParagraph paragraph0 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 0'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph0, 2));
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
expect(paragraph0.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]);
expect(boxes.length, 1);
// Find start handle.
final Offset handlePos = globalize(boxes[0].toRect().bottomLeft, paragraph0);
await gesture.down(handlePos);
expect(controller.offset, 0.0);
double previousOffset = controller.offset;
// Scrollable only auto scroll if the drag passes the boundary.
await gesture.moveTo(tester.getBottomRight(find.byType(ListView)) + const Offset(0, 40));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(controller.offset > previousOffset, isTrue);
previousOffset = controller.offset;
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(controller.offset > previousOffset, isTrue);
previousOffset = controller.offset;
// Release handle should stop scrolling.
await gesture.up();
// Last scheduled scroll.
await tester.pump();
await tester.pump(const Duration(seconds: 1));
previousOffset = controller.offset;
await tester.pumpAndSettle();
expect(controller.offset, previousOffset);
});
testWidgets('select to scroll by dragging end selection handle stops scroll when released', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(MaterialApp(
home: SelectionArea(
selectionControls: materialTextSelectionControls,
child: ListView.builder(
controller: controller,
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Text('Item $index');
},
),
),
));
await tester.pumpAndSettle();
// Long press to bring up the selection handles.
final RenderParagraph paragraph0 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 0'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph0, 2));
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
expect(paragraph0.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]);
expect(boxes.length, 1);
final Offset handlePos = globalize(boxes[0].toRect().bottomRight, paragraph0);
await gesture.down(handlePos);
expect(controller.offset, 0.0);
double previousOffset = controller.offset;
// Scrollable only auto scroll if the drag passes the boundary
await gesture.moveTo(tester.getBottomRight(find.byType(ListView)) + const Offset(0, 40));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(controller.offset > previousOffset, isTrue);
previousOffset = controller.offset;
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(controller.offset > previousOffset, isTrue);
previousOffset = controller.offset;
// Release handle should stop scrolling.
await gesture.up();
// Last scheduled scroll.
await tester.pump();
await tester.pump(const Duration(seconds: 1));
previousOffset = controller.offset;
await tester.pumpAndSettle();
expect(controller.offset, previousOffset);
});
testWidgets('keyboard selection should auto scroll - vertical', (WidgetTester tester) async { testWidgets('keyboard selection should auto scroll - vertical', (WidgetTester tester) async {
final FocusNode node = FocusNode(); final FocusNode node = FocusNode();
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
......
...@@ -298,6 +298,67 @@ void main() { ...@@ -298,6 +298,67 @@ void main() {
}); });
}); });
testWidgets('dragging handle or selecting word triggers haptic feedback on Android', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
log.add(methodCall);
return null;
});
addTearDown(() {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
});
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: FocusNode(),
selectionControls: materialTextSelectionControls,
child: const Text('How are you?'),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 6)); // at the 'r'
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await gesture.up();
await tester.pump(const Duration(milliseconds: 500));
// `are` is selected.
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
expect(
log.last,
isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.selectionClick'),
);
log.clear();
final List<TextBox> boxes = paragraph.getBoxesForSelection(paragraph.selections[0]);
expect(boxes.length, 1);
final Offset handlePos = globalize(boxes[0].toRect().bottomRight, paragraph);
await gesture.down(handlePos);
final Offset endPos = Offset(textOffsetToPosition(paragraph, 8).dx, handlePos.dy);
// Select 1 more character by dragging end handle to trigger feedback.
await gesture.moveTo(endPos);
expect(paragraph.selections[0], const TextSelection(baseOffset: 4, extentOffset: 8));
// Only Android vibrate when dragging the handle.
switch(defaultTargetPlatform) {
case TargetPlatform.android:
expect(
log.last,
isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.selectionClick'),
);
break;
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expect(log, isEmpty);
break;
}
await gesture.up();
}, variant: TargetPlatformVariant.all());
group('SelectionArea integration', () { group('SelectionArea integration', () {
testWidgets('mouse can select single text', (WidgetTester tester) async { testWidgets('mouse can select single text', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
......
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