Unverified Commit 734c3c4f authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

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.
parent 45f8c390
......@@ -1026,9 +1026,23 @@ class TextSelectionGestureDetectorBuilder {
// The viewport offset pixels of the [RenderEditable] at the last drag start.
double _dragStartViewportOffset = 0.0;
// Returns true iff either shift key is currently down.
bool get _isShiftPressed {
return HardwareKeyboard.instance.logicalKeysPressed
.any(<LogicalKeyboardKey>{
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
}.contains);
}
// True iff a tap + shift has been detected but the tap has not yet come up.
bool _isShiftTapping = false;
// For a shift + tap + drag gesture, the TextSelection at the point of the
// tap. Mac uses this value to reset to the original selection when an
// inversion of the base and offset happens.
TextSelection? _shiftTapDragSelection;
/// Handler for [TextSelectionGestureDetector.onTapDown].
///
/// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets
......@@ -1050,12 +1064,7 @@ class TextSelectionGestureDetectorBuilder {
|| kind == PointerDeviceKind.stylus;
// Handle shift + click selection if needed.
final bool isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed
.any(<LogicalKeyboardKey>{
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
}.contains);
if (isShiftPressed && renderEditable.selection?.baseOffset != null) {
if (_isShiftPressed && renderEditable.selection?.baseOffset != null) {
_isShiftTapping = true;
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
......@@ -1290,10 +1299,27 @@ class TextSelectionGestureDetectorBuilder {
|| kind == PointerDeviceKind.touch
|| kind == PointerDeviceKind.stylus;
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
if (_isShiftPressed && renderEditable.selection != null && renderEditable.selection!.isValid) {
_isShiftTapping = true;
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
_expandSelection(details.globalPosition, SelectionChangedCause.drag);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
_extendSelection(details.globalPosition, SelectionChangedCause.drag);
break;
}
_shiftTapDragSelection = renderEditable.selection;
} else {
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
}
_dragStartViewportOffset = renderEditable.offset.pixels;
}
......@@ -1312,28 +1338,77 @@ class TextSelectionGestureDetectorBuilder {
if (!delegate.selectionEnabled)
return;
// Adjust the drag start offset for possible viewport offset changes.
final Offset startOffset = renderEditable.maxLines == 1
? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
: Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset);
if (!_isShiftTapping) {
// Adjust the drag start offset for possible viewport offset changes.
final Offset startOffset = renderEditable.maxLines == 1
? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
: Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset);
renderEditable.selectPositionAt(
from: startDetails.globalPosition - startOffset,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
return renderEditable.selectPositionAt(
from: startDetails.globalPosition - startOffset,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
}
if (_shiftTapDragSelection!.isCollapsed
|| (defaultTargetPlatform != TargetPlatform.iOS
&& defaultTargetPlatform != TargetPlatform.macOS)) {
return _extendSelection(updateDetails.globalPosition, SelectionChangedCause.drag);
}
// If the drag inverts the selection, Mac and iOS revert to the initial
// selection.
final TextSelection selection = editableText.textEditingValue.selection;
final TextPosition nextExtent = renderEditable.getPositionForPoint(updateDetails.globalPosition);
final bool isShiftTapDragSelectionForward =
_shiftTapDragSelection!.baseOffset < _shiftTapDragSelection!.extentOffset;
final bool isInverted = isShiftTapDragSelectionForward
? nextExtent.offset < _shiftTapDragSelection!.baseOffset
: nextExtent.offset > _shiftTapDragSelection!.baseOffset;
if (isInverted && selection.baseOffset == _shiftTapDragSelection!.baseOffset) {
editableText.userUpdateTextEditingValue(
editableText.textEditingValue.copyWith(
selection: TextSelection(
baseOffset: _shiftTapDragSelection!.extentOffset,
extentOffset: nextExtent.offset,
),
),
SelectionChangedCause.drag,
);
} else if (!isInverted
&& nextExtent.offset != _shiftTapDragSelection!.baseOffset
&& selection.baseOffset != _shiftTapDragSelection!.baseOffset) {
editableText.userUpdateTextEditingValue(
editableText.textEditingValue.copyWith(
selection: TextSelection(
baseOffset: _shiftTapDragSelection!.baseOffset,
extentOffset: nextExtent.offset,
),
),
SelectionChangedCause.drag,
);
} else {
_extendSelection(updateDetails.globalPosition, SelectionChangedCause.drag);
}
}
/// Handler for [TextSelectionGestureDetector.onDragSelectionEnd].
///
/// By default, it services as place holder to enable subclass override.
/// By default, it simply cleans up the state used for handling certain
/// built-in behaviors.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this
/// callback.
@protected
void onDragSelectionEnd(DragEndDetails details) {/* Subclass should override this method if needed. */}
void onDragSelectionEnd(DragEndDetails details) {
if (_isShiftTapping) {
_isShiftTapping = false;
_shiftTapDragSelection = null;
}
}
/// Returns a [TextSelectionGestureDetector] configured with the handlers
/// provided by this builder.
......
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