@@ -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