@@ -1026,9 +1026,23 @@ class TextSelectionGestureDetectorBuilder {
10261026 // The viewport offset pixels of the [RenderEditable] at the last drag start.
10271027 double _dragStartViewportOffset = 0.0 ;
10281028
1029+ // Returns true iff either shift key is currently down.
1030+ bool get _isShiftPressed {
1031+ return HardwareKeyboard .instance.logicalKeysPressed
1032+ .any (< LogicalKeyboardKey > {
1033+ LogicalKeyboardKey .shiftLeft,
1034+ LogicalKeyboardKey .shiftRight,
1035+ }.contains);
1036+ }
1037+
10291038 // True iff a tap + shift has been detected but the tap has not yet come up.
10301039 bool _isShiftTapping = false ;
10311040
1041+ // For a shift + tap + drag gesture, the TextSelection at the point of the
1042+ // tap. Mac uses this value to reset to the original selection when an
1043+ // inversion of the base and offset happens.
1044+ TextSelection ? _shiftTapDragSelection;
1045+
10321046 /// Handler for [TextSelectionGestureDetector.onTapDown] .
10331047 ///
10341048 /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets
@@ -1050,12 +1064,7 @@ class TextSelectionGestureDetectorBuilder {
10501064 || kind == PointerDeviceKind .stylus;
10511065
10521066 // 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 ) {
1067+ if (_isShiftPressed && renderEditable.selection? .baseOffset != null ) {
10591068 _isShiftTapping = true ;
10601069 switch (defaultTargetPlatform) {
10611070 case TargetPlatform .iOS:
@@ -1290,10 +1299,27 @@ class TextSelectionGestureDetectorBuilder {
12901299 || kind == PointerDeviceKind .touch
12911300 || kind == PointerDeviceKind .stylus;
12921301
1293- renderEditable.selectPositionAt (
1294- from: details.globalPosition,
1295- cause: SelectionChangedCause .drag,
1296- );
1302+ if (_isShiftPressed && renderEditable.selection != null && renderEditable.selection! .isValid) {
1303+ _isShiftTapping = true ;
1304+ switch (defaultTargetPlatform) {
1305+ case TargetPlatform .iOS:
1306+ case TargetPlatform .macOS:
1307+ _expandSelection (details.globalPosition, SelectionChangedCause .drag);
1308+ break ;
1309+ case TargetPlatform .android:
1310+ case TargetPlatform .fuchsia:
1311+ case TargetPlatform .linux:
1312+ case TargetPlatform .windows:
1313+ _extendSelection (details.globalPosition, SelectionChangedCause .drag);
1314+ break ;
1315+ }
1316+ _shiftTapDragSelection = renderEditable.selection;
1317+ } else {
1318+ renderEditable.selectPositionAt (
1319+ from: details.globalPosition,
1320+ cause: SelectionChangedCause .drag,
1321+ );
1322+ }
12971323
12981324 _dragStartViewportOffset = renderEditable.offset.pixels;
12991325 }
@@ -1312,28 +1338,77 @@ class TextSelectionGestureDetectorBuilder {
13121338 if (! delegate.selectionEnabled)
13131339 return ;
13141340
1315- // Adjust the drag start offset for possible viewport offset changes.
1316- final Offset startOffset = renderEditable.maxLines == 1
1317- ? Offset (renderEditable.offset.pixels - _dragStartViewportOffset, 0.0 )
1318- : Offset (0.0 , renderEditable.offset.pixels - _dragStartViewportOffset);
1341+ if (! _isShiftTapping) {
1342+ // Adjust the drag start offset for possible viewport offset changes.
1343+ final Offset startOffset = renderEditable.maxLines == 1
1344+ ? Offset (renderEditable.offset.pixels - _dragStartViewportOffset, 0.0 )
1345+ : Offset (0.0 , renderEditable.offset.pixels - _dragStartViewportOffset);
13191346
1320- renderEditable.selectPositionAt (
1321- from: startDetails.globalPosition - startOffset,
1322- to: updateDetails.globalPosition,
1323- cause: SelectionChangedCause .drag,
1324- );
1347+ return renderEditable.selectPositionAt (
1348+ from: startDetails.globalPosition - startOffset,
1349+ to: updateDetails.globalPosition,
1350+ cause: SelectionChangedCause .drag,
1351+ );
1352+ }
1353+
1354+ if (_shiftTapDragSelection! .isCollapsed
1355+ || (defaultTargetPlatform != TargetPlatform .iOS
1356+ && defaultTargetPlatform != TargetPlatform .macOS)) {
1357+ return _extendSelection (updateDetails.globalPosition, SelectionChangedCause .drag);
1358+ }
1359+
1360+ // If the drag inverts the selection, Mac and iOS revert to the initial
1361+ // selection.
1362+ final TextSelection selection = editableText.textEditingValue.selection;
1363+ final TextPosition nextExtent = renderEditable.getPositionForPoint (updateDetails.globalPosition);
1364+ final bool isShiftTapDragSelectionForward =
1365+ _shiftTapDragSelection! .baseOffset < _shiftTapDragSelection! .extentOffset;
1366+ final bool isInverted = isShiftTapDragSelectionForward
1367+ ? nextExtent.offset < _shiftTapDragSelection! .baseOffset
1368+ : nextExtent.offset > _shiftTapDragSelection! .baseOffset;
1369+ if (isInverted && selection.baseOffset == _shiftTapDragSelection! .baseOffset) {
1370+ editableText.userUpdateTextEditingValue (
1371+ editableText.textEditingValue.copyWith (
1372+ selection: TextSelection (
1373+ baseOffset: _shiftTapDragSelection! .extentOffset,
1374+ extentOffset: nextExtent.offset,
1375+ ),
1376+ ),
1377+ SelectionChangedCause .drag,
1378+ );
1379+ } else if (! isInverted
1380+ && nextExtent.offset != _shiftTapDragSelection! .baseOffset
1381+ && selection.baseOffset != _shiftTapDragSelection! .baseOffset) {
1382+ editableText.userUpdateTextEditingValue (
1383+ editableText.textEditingValue.copyWith (
1384+ selection: TextSelection (
1385+ baseOffset: _shiftTapDragSelection! .baseOffset,
1386+ extentOffset: nextExtent.offset,
1387+ ),
1388+ ),
1389+ SelectionChangedCause .drag,
1390+ );
1391+ } else {
1392+ _extendSelection (updateDetails.globalPosition, SelectionChangedCause .drag);
1393+ }
13251394 }
13261395
13271396 /// Handler for [TextSelectionGestureDetector.onDragSelectionEnd] .
13281397 ///
1329- /// By default, it services as place holder to enable subclass override.
1398+ /// By default, it simply cleans up the state used for handling certain
1399+ /// built-in behaviors.
13301400 ///
13311401 /// See also:
13321402 ///
13331403 /// * [TextSelectionGestureDetector.onDragSelectionEnd] , which triggers this
13341404 /// callback.
13351405 @protected
1336- void onDragSelectionEnd (DragEndDetails details) {/* Subclass should override this method if needed. */ }
1406+ void onDragSelectionEnd (DragEndDetails details) {
1407+ if (_isShiftTapping) {
1408+ _isShiftTapping = false ;
1409+ _shiftTapDragSelection = null ;
1410+ }
1411+ }
13371412
13381413 /// Returns a [TextSelectionGestureDetector] configured with the handlers
13391414 /// provided by this builder.
0 commit comments