Skip to content

Commit 2b21c3e

Browse files
authored
Shift tap gesture (flutter#93835)
1 parent c44424b commit 2b21c3e

File tree

5 files changed

+326
-52
lines changed

5 files changed

+326
-52
lines changed

packages/flutter/lib/src/material/text_field.dart

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -86,33 +86,7 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
8686
@override
8787
void onSingleTapUp(TapUpDetails details) {
8888
editableText.hideToolbar();
89-
if (delegate.selectionEnabled) {
90-
switch (Theme.of(_state.context).platform) {
91-
case TargetPlatform.iOS:
92-
case TargetPlatform.macOS:
93-
switch (details.kind) {
94-
case PointerDeviceKind.mouse:
95-
case PointerDeviceKind.stylus:
96-
case PointerDeviceKind.invertedStylus:
97-
// Precise devices should place the cursor at a precise position.
98-
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
99-
break;
100-
case PointerDeviceKind.touch:
101-
case PointerDeviceKind.unknown:
102-
// On macOS/iOS/iPadOS a touch tap places the cursor at the edge
103-
// of the word.
104-
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
105-
break;
106-
}
107-
break;
108-
case TargetPlatform.android:
109-
case TargetPlatform.fuchsia:
110-
case TargetPlatform.linux:
111-
case TargetPlatform.windows:
112-
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
113-
break;
114-
}
115-
}
89+
super.onSingleTapUp(details);
11690
_state._requestKeyboard();
11791
_state.widget.onTap?.call();
11892
}

packages/flutter/lib/src/widgets/text_selection.dart

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,65 @@ class TextSelectionGestureDetectorBuilder {
946946
&& renderEditable.selection!.end >= textPosition.offset;
947947
}
948948

949+
// Expand the selection to the given global position.
950+
//
951+
// Either base or extent will be moved to the last tapped position, whichever
952+
// is closest. The selection will never shrink or pivot, only grow.
953+
//
954+
// See also:
955+
//
956+
// * [_extendSelection], which is similar but pivots the selection around
957+
// the base.
958+
void _expandSelection(Offset offset, SelectionChangedCause cause) {
959+
assert(cause != null);
960+
assert(offset != null);
961+
assert(renderEditable.selection?.baseOffset != null);
962+
963+
final TextPosition tappedPosition = renderEditable.getPositionForPoint(offset);
964+
final TextSelection selection = renderEditable.selection!;
965+
final bool baseIsCloser =
966+
(tappedPosition.offset - selection.baseOffset).abs()
967+
< (tappedPosition.offset - selection.extentOffset).abs();
968+
final TextSelection nextSelection = selection.copyWith(
969+
baseOffset: baseIsCloser ? selection.extentOffset : selection.baseOffset,
970+
extentOffset: tappedPosition.offset,
971+
);
972+
973+
editableText.userUpdateTextEditingValue(
974+
editableText.textEditingValue.copyWith(
975+
selection: nextSelection,
976+
),
977+
cause,
978+
);
979+
}
980+
981+
// Extend the selection to the given global position.
982+
//
983+
// Holds the base in place and moves the extent.
984+
//
985+
// See also:
986+
//
987+
// * [_expandSelection], which is similar but always increases the size of
988+
// the selection.
989+
void _extendSelection(Offset offset, SelectionChangedCause cause) {
990+
assert(cause != null);
991+
assert(offset != null);
992+
assert(renderEditable.selection?.baseOffset != null);
993+
994+
final TextPosition tappedPosition = renderEditable.getPositionForPoint(offset);
995+
final TextSelection selection = renderEditable.selection!;
996+
final TextSelection nextSelection = selection.copyWith(
997+
extentOffset: tappedPosition.offset,
998+
);
999+
1000+
editableText.userUpdateTextEditingValue(
1001+
editableText.textEditingValue.copyWith(
1002+
selection: nextSelection,
1003+
),
1004+
cause,
1005+
);
1006+
}
1007+
9491008
/// Whether to show the selection toolbar.
9501009
///
9511010
/// It is based on the signal source when a [onTapDown] is called. This getter
@@ -964,9 +1023,12 @@ class TextSelectionGestureDetectorBuilder {
9641023
@protected
9651024
RenderEditable get renderEditable => editableText.renderEditable;
9661025

967-
/// The viewport offset pixels of the [RenderEditable] at the last drag start.
1026+
// The viewport offset pixels of the [RenderEditable] at the last drag start.
9681027
double _dragStartViewportOffset = 0.0;
9691028

1029+
// True iff a tap + shift has been detected but the tap has not yet come up.
1030+
bool _isShiftTapping = false;
1031+
9701032
/// Handler for [TextSelectionGestureDetector.onTapDown].
9711033
///
9721034
/// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets
@@ -986,6 +1048,28 @@ class TextSelectionGestureDetectorBuilder {
9861048
_shouldShowSelectionToolbar = kind == null
9871049
|| kind == PointerDeviceKind.touch
9881050
|| kind == PointerDeviceKind.stylus;
1051+
1052+
// Handle shift + click selection if needed.
1053+
final bool isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed
1054+
.any(<LogicalKeyboardKey>{
1055+
LogicalKeyboardKey.shiftLeft,
1056+
LogicalKeyboardKey.shiftRight,
1057+
}.contains);
1058+
if (isShiftPressed && renderEditable.selection?.baseOffset != null) {
1059+
_isShiftTapping = true;
1060+
switch (defaultTargetPlatform) {
1061+
case TargetPlatform.iOS:
1062+
case TargetPlatform.macOS:
1063+
_expandSelection(details.globalPosition, SelectionChangedCause.tap);
1064+
break;
1065+
case TargetPlatform.android:
1066+
case TargetPlatform.fuchsia:
1067+
case TargetPlatform.linux:
1068+
case TargetPlatform.windows:
1069+
_extendSelection(details.globalPosition, SelectionChangedCause.tap);
1070+
break;
1071+
}
1072+
}
9891073
}
9901074

9911075
/// Handler for [TextSelectionGestureDetector.onForcePressStart].
@@ -1043,8 +1127,37 @@ class TextSelectionGestureDetectorBuilder {
10431127
/// this callback.
10441128
@protected
10451129
void onSingleTapUp(TapUpDetails details) {
1130+
if (_isShiftTapping) {
1131+
_isShiftTapping = false;
1132+
return;
1133+
}
1134+
10461135
if (delegate.selectionEnabled) {
1047-
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
1136+
switch (defaultTargetPlatform) {
1137+
case TargetPlatform.iOS:
1138+
case TargetPlatform.macOS:
1139+
switch (details.kind) {
1140+
case PointerDeviceKind.mouse:
1141+
case PointerDeviceKind.stylus:
1142+
case PointerDeviceKind.invertedStylus:
1143+
// Precise devices should place the cursor at a precise position.
1144+
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
1145+
break;
1146+
case PointerDeviceKind.touch:
1147+
case PointerDeviceKind.unknown:
1148+
// On macOS/iOS/iPadOS a touch tap places the cursor at the edge
1149+
// of the word.
1150+
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
1151+
break;
1152+
}
1153+
break;
1154+
case TargetPlatform.android:
1155+
case TargetPlatform.fuchsia:
1156+
case TargetPlatform.linux:
1157+
case TargetPlatform.windows:
1158+
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
1159+
break;
1160+
}
10481161
}
10491162
}
10501163

0 commit comments

Comments
 (0)