Skip to content

Commit 734c3c4

Browse files
authored
Text editing shift + tap + drag interaction (#95213)
Supports the desktop text editing interaction of holding shift, tapping the field, and dragging to modify the selection.
1 parent 45f8c39 commit 734c3c4

File tree

3 files changed

+872
-21
lines changed

3 files changed

+872
-21
lines changed

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

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)