Unverified Commit 4abe6fda authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Reland "Single tap on the previous selection should toggle the toolbar on iOS #108913" (#111995)

parent 57e577a5
...@@ -102,7 +102,6 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe ...@@ -102,7 +102,6 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe
@override @override
void onSingleTapUp(TapUpDetails details) { void onSingleTapUp(TapUpDetails details) {
editableText.hideToolbar();
// Because TextSelectionGestureDetector listens to taps that happen on // Because TextSelectionGestureDetector listens to taps that happen on
// widgets in front of it, tapping the clear button will also trigger // widgets in front of it, tapping the clear button will also trigger
// this handler. If the clear button widget recognizes the up event, // this handler. If the clear button widget recognizes the up event,
......
...@@ -103,7 +103,6 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete ...@@ -103,7 +103,6 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
@override @override
void onSingleTapUp(TapUpDetails details) { void onSingleTapUp(TapUpDetails details) {
editableText.hideToolbar();
super.onSingleTapUp(details); super.onSingleTapUp(details);
_state._requestKeyboard(); _state._requestKeyboard();
_state.widget.onTap?.call(); _state.widget.onTap?.call();
......
...@@ -2724,8 +2724,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2724,8 +2724,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_scribbleCacheKey = null; _scribbleCacheKey = null;
} }
void _createSelectionOverlay() { TextSelectionOverlay _createSelectionOverlay() {
_selectionOverlay = TextSelectionOverlay( final TextSelectionOverlay selectionOverlay = TextSelectionOverlay(
clipboardStatus: _clipboardStatus, clipboardStatus: _clipboardStatus,
context: context, context: context,
value: _value, value: _value,
...@@ -2740,6 +2740,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2740,6 +2740,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
onSelectionHandleTapped: widget.onSelectionHandleTapped, onSelectionHandleTapped: widget.onSelectionHandleTapped,
magnifierConfiguration: widget.magnifierConfiguration, magnifierConfiguration: widget.magnifierConfiguration,
); );
return selectionOverlay;
} }
@pragma('vm:notify-debugger-on-exception') @pragma('vm:notify-debugger-on-exception')
...@@ -2780,7 +2782,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2780,7 +2782,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_selectionOverlay = null; _selectionOverlay = null;
} else { } else {
if (_selectionOverlay == null) { if (_selectionOverlay == null) {
_createSelectionOverlay(); _selectionOverlay = _createSelectionOverlay();
} else { } else {
_selectionOverlay!.update(_value); _selectionOverlay!.update(_value);
} }
...@@ -3266,7 +3268,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3266,7 +3268,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (value == textEditingValue) { if (value == textEditingValue) {
if (!widget.focusNode.hasFocus) { if (!widget.focusNode.hasFocus) {
widget.focusNode.requestFocus(); widget.focusNode.requestFocus();
_createSelectionOverlay(); _selectionOverlay = _createSelectionOverlay();
} }
return; return;
} }
...@@ -3317,10 +3319,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3317,10 +3319,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
/// Toggles the visibility of the toolbar. /// Toggles the visibility of the toolbar.
void toggleToolbar() { void toggleToolbar([bool hideHandles = true]) {
assert(_selectionOverlay != null); final TextSelectionOverlay selectionOverlay = _selectionOverlay ??= _createSelectionOverlay();
if (_selectionOverlay!.toolbarIsVisible) {
hideToolbar(); if (selectionOverlay.toolbarIsVisible) {
hideToolbar(hideHandles);
} else { } else {
showToolbar(); showToolbar();
} }
......
...@@ -1635,6 +1635,26 @@ class TextSelectionGestureDetectorBuilder { ...@@ -1635,6 +1635,26 @@ class TextSelectionGestureDetectorBuilder {
&& renderEditable.selection!.end >= textPosition.offset; && renderEditable.selection!.end >= textPosition.offset;
} }
bool _positionWasOnSelectionExclusive(TextPosition textPosition) {
final TextSelection? selection = renderEditable.selection;
if (selection == null) {
return false;
}
return selection.start < textPosition.offset
&& selection.end > textPosition.offset;
}
bool _positionWasOnSelectionInclusive(TextPosition textPosition) {
final TextSelection? selection = renderEditable.selection;
if (selection == null) {
return false;
}
return selection.start <= textPosition.offset
&& selection.end >= textPosition.offset;
}
// Expand the selection to the given global position. // Expand the selection to the given global position.
// //
// Either base or extent will be moved to the last tapped position, whichever // Either base or extent will be moved to the last tapped position, whichever
...@@ -1879,6 +1899,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -1879,6 +1899,7 @@ class TextSelectionGestureDetectorBuilder {
case TargetPlatform.linux: case TargetPlatform.linux:
case TargetPlatform.macOS: case TargetPlatform.macOS:
case TargetPlatform.windows: case TargetPlatform.windows:
editableText.hideToolbar();
// On desktop platforms the selection is set on tap down. // On desktop platforms the selection is set on tap down.
if (_isShiftTapping) { if (_isShiftTapping) {
_isShiftTapping = false; _isShiftTapping = false;
...@@ -1886,6 +1907,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -1886,6 +1907,7 @@ class TextSelectionGestureDetectorBuilder {
break; break;
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
editableText.hideToolbar();
if (isShiftPressedValid) { if (isShiftPressedValid) {
_isShiftTapping = true; _isShiftTapping = true;
_extendSelection(details.globalPosition, SelectionChangedCause.tap); _extendSelection(details.globalPosition, SelectionChangedCause.tap);
...@@ -1918,8 +1940,32 @@ class TextSelectionGestureDetectorBuilder { ...@@ -1918,8 +1940,32 @@ class TextSelectionGestureDetectorBuilder {
break; break;
case PointerDeviceKind.touch: case PointerDeviceKind.touch:
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
// On iOS/iPadOS a touch tap places the cursor at the edge of the word. // Toggle the toolbar if the `previousSelection` is collapsed, the tap is on the selection, the
// TextAffinity remains the same, and the editable is focused. The TextAffinity is important when the
// cursor is on the boundary of a line wrap, if the affinity is different (i.e. it is downstream), the
// selection should move to the following line and not toggle the toolbar.
//
// Toggle the toolbar when the tap is exclusively within the bounds of a non-collapsed `previousSelection`,
// and the editable is focused.
//
// Selects the word edge closest to the tap when the editable is not focused, or if the tap was neither exclusively
// or inclusively on `previousSelection`. If the selection remains the same after selecting the word edge, then we
// toggle the toolbar. If the selection changes then we hide the toolbar.
final TextSelection previousSelection = renderEditable.selection ?? editableText.textEditingValue.selection;
final TextPosition textPosition = renderEditable.getPositionForPoint(details.globalPosition);
final bool isAffinityTheSame = textPosition.affinity == previousSelection.affinity;
if (((_positionWasOnSelectionExclusive(textPosition) && !previousSelection.isCollapsed)
|| (_positionWasOnSelectionInclusive(textPosition) && previousSelection.isCollapsed && isAffinityTheSame))
&& renderEditable.hasFocus) {
editableText.toggleToolbar(false);
} else {
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
if (previousSelection == editableText.textEditingValue.selection && renderEditable.hasFocus) {
editableText.toggleToolbar(false);
} else {
editableText.hideToolbar(false);
}
}
break; break;
} }
break; break;
......
...@@ -570,6 +570,38 @@ void main() { ...@@ -570,6 +570,38 @@ void main() {
} }
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
testWidgets('test TextSelectionGestureDetectorBuilder toggles toolbar on single tap on previous selection iOS', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester);
final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
expect(state.showToolbarCalled, isFalse);
expect(state.toggleToolbarCalled, isFalse);
renderEditable.selection = const TextSelection(baseOffset: 2, extentOffset: 6);
renderEditable.hasFocus = true;
final TestGesture gesture = await tester.startGesture(
const Offset(25.0, 200.0),
pointer: 0,
);
await gesture.up();
await tester.pumpAndSettle();
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
expect(renderEditable.selectWordEdgeCalled, isFalse);
expect(state.toggleToolbarCalled, isTrue);
break;
case TargetPlatform.macOS:
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
expect(renderEditable.selectPositionAtCalled, isTrue);
break;
}
}, variant: TargetPlatformVariant.all());
testWidgets('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async { testWidgets('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester); await pumpTextSelectionGestureDetectorBuilder(tester);
final TestGesture gesture = await tester.startGesture( final TestGesture gesture = await tester.startGesture(
...@@ -1519,6 +1551,7 @@ class FakeEditableText extends EditableText { ...@@ -1519,6 +1551,7 @@ class FakeEditableText extends EditableText {
class FakeEditableTextState extends EditableTextState { class FakeEditableTextState extends EditableTextState {
final GlobalKey _editableKey = GlobalKey(); final GlobalKey _editableKey = GlobalKey();
bool showToolbarCalled = false; bool showToolbarCalled = false;
bool toggleToolbarCalled = false;
@override @override
RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable; RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable;
...@@ -1530,7 +1563,8 @@ class FakeEditableTextState extends EditableTextState { ...@@ -1530,7 +1563,8 @@ class FakeEditableTextState extends EditableTextState {
} }
@override @override
void toggleToolbar() { void toggleToolbar([bool hideHandles = true]) {
toggleToolbarCalled = true;
return; return;
} }
......
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