mirror of
https://github.com/flutter/flutter
synced 2024-10-04 07:19:46 +00:00
Support keyboard selection in SelectabledRegion (#112584)
* Support keyboard selection in selectable region * fix some comments * addressing comments
This commit is contained in:
parent
cfb2f158d6
commit
80bf355192
|
@ -194,6 +194,80 @@ class _RenderSelectableAdapter extends RenderProxyBox with Selectable, Selection
|
|||
_start = Offset.zero;
|
||||
_end = Offset.infinite;
|
||||
break;
|
||||
case SelectionEventType.granularlyExtendSelection:
|
||||
result = SelectionResult.end;
|
||||
final GranularlyExtendSelectionEvent extendSelectionEvent = event as GranularlyExtendSelectionEvent;
|
||||
// Initialize the offset it there is no ongoing selection.
|
||||
if (_start == null || _end == null) {
|
||||
if (extendSelectionEvent.forward) {
|
||||
_start = _end = Offset.zero;
|
||||
} else {
|
||||
_start = _end = Offset.infinite;
|
||||
}
|
||||
}
|
||||
// Move the corresponding selection edge.
|
||||
final Offset newOffset = extendSelectionEvent.forward ? Offset.infinite : Offset.zero;
|
||||
if (extendSelectionEvent.isEnd) {
|
||||
if (newOffset == _end) {
|
||||
result = extendSelectionEvent.forward ? SelectionResult.next : SelectionResult.previous;
|
||||
}
|
||||
_end = newOffset;
|
||||
} else {
|
||||
if (newOffset == _start) {
|
||||
result = extendSelectionEvent.forward ? SelectionResult.next : SelectionResult.previous;
|
||||
}
|
||||
_start = newOffset;
|
||||
}
|
||||
break;
|
||||
case SelectionEventType.directionallyExtendSelection:
|
||||
result = SelectionResult.end;
|
||||
final DirectionallyExtendSelectionEvent extendSelectionEvent = event as DirectionallyExtendSelectionEvent;
|
||||
// Convert to local coordinates.
|
||||
final double horizontalBaseLine = globalToLocal(Offset(event.dx, 0)).dx;
|
||||
final Offset newOffset;
|
||||
final bool forward;
|
||||
switch(extendSelectionEvent.direction) {
|
||||
case SelectionExtendDirection.backward:
|
||||
case SelectionExtendDirection.previousLine:
|
||||
forward = false;
|
||||
// Initialize the offset it there is no ongoing selection.
|
||||
if (_start == null || _end == null) {
|
||||
_start = _end = Offset.infinite;
|
||||
}
|
||||
// Move the corresponding selection edge.
|
||||
if (extendSelectionEvent.direction == SelectionExtendDirection.previousLine || horizontalBaseLine < 0) {
|
||||
newOffset = Offset.zero;
|
||||
} else {
|
||||
newOffset = Offset.infinite;
|
||||
}
|
||||
break;
|
||||
case SelectionExtendDirection.nextLine:
|
||||
case SelectionExtendDirection.forward:
|
||||
forward = true;
|
||||
// Initialize the offset it there is no ongoing selection.
|
||||
if (_start == null || _end == null) {
|
||||
_start = _end = Offset.zero;
|
||||
}
|
||||
// Move the corresponding selection edge.
|
||||
if (extendSelectionEvent.direction == SelectionExtendDirection.nextLine || horizontalBaseLine > size.width) {
|
||||
newOffset = Offset.infinite;
|
||||
} else {
|
||||
newOffset = Offset.zero;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (extendSelectionEvent.isEnd) {
|
||||
if (newOffset == _end) {
|
||||
result = forward ? SelectionResult.next : SelectionResult.previous;
|
||||
}
|
||||
_end = newOffset;
|
||||
} else {
|
||||
if (newOffset == _start) {
|
||||
result = forward ? SelectionResult.next : SelectionResult.previous;
|
||||
}
|
||||
_start = newOffset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
_updateGeometry();
|
||||
return result;
|
||||
|
|
|
@ -4,15 +4,17 @@
|
|||
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Gradient, PlaceholderAlignment, Shader, TextBox, TextHeightBehavior;
|
||||
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Gradient, LineMetrics, PlaceholderAlignment, Shader, TextBox, TextHeightBehavior;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'box.dart';
|
||||
import 'debug.dart';
|
||||
import 'editable.dart';
|
||||
import 'layer.dart';
|
||||
import 'object.dart';
|
||||
import 'selection.dart';
|
||||
|
@ -151,11 +153,11 @@ class RenderParagraph extends RenderBox
|
|||
_cachedCombinedSemanticsInfos = null;
|
||||
_extractPlaceholderSpans(value);
|
||||
markNeedsLayout();
|
||||
_removeSelectionRegistrarSubscription();
|
||||
_disposeSelectableFragments();
|
||||
_updateSelectionRegistrarSubscription();
|
||||
break;
|
||||
}
|
||||
_removeSelectionRegistrarSubscription();
|
||||
_disposeSelectableFragments();
|
||||
_updateSelectionRegistrarSubscription();
|
||||
}
|
||||
|
||||
/// The ongoing selections in this paragraph.
|
||||
|
@ -226,7 +228,7 @@ class RenderParagraph extends RenderBox
|
|||
if (end == -1) {
|
||||
end = plainText.length;
|
||||
}
|
||||
result.add(_SelectableFragment(paragraph: this, range: TextRange(start: start, end: end)));
|
||||
result.add(_SelectableFragment(paragraph: this, range: TextRange(start: start, end: end), fullText: plainText));
|
||||
start = end;
|
||||
}
|
||||
start += 1;
|
||||
|
@ -439,6 +441,10 @@ class RenderParagraph extends RenderBox
|
|||
return getOffsetForCaret(position, Rect.zero) + Offset(0, getFullHeightForCaret(position) ?? 0.0);
|
||||
}
|
||||
|
||||
List<ui.LineMetrics> _computeLineMetrics() {
|
||||
return _textPainter.computeLineMetrics();
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
if (!_canComputeIntrinsics()) {
|
||||
|
@ -1027,6 +1033,28 @@ class RenderParagraph extends RenderBox
|
|||
return _textPainter.getWordBoundary(position);
|
||||
}
|
||||
|
||||
TextRange _getLineAtOffset(TextPosition position) => _textPainter.getLineBoundary(position);
|
||||
|
||||
TextPosition _getTextPositionAbove(TextPosition position) {
|
||||
// -0.5 of preferredLineHeight points to the middle of the line above.
|
||||
final double preferredLineHeight = _textPainter.preferredLineHeight;
|
||||
final double verticalOffset = -0.5 * preferredLineHeight;
|
||||
return _getTextPositionVertical(position, verticalOffset);
|
||||
}
|
||||
|
||||
TextPosition _getTextPositionBelow(TextPosition position) {
|
||||
// 1.5 of preferredLineHeight points to the middle of the line below.
|
||||
final double preferredLineHeight = _textPainter.preferredLineHeight;
|
||||
final double verticalOffset = 1.5 * preferredLineHeight;
|
||||
return _getTextPositionVertical(position, verticalOffset);
|
||||
}
|
||||
|
||||
TextPosition _getTextPositionVertical(TextPosition position, double verticalOffset) {
|
||||
final Offset caretOffset = _textPainter.getOffsetForCaret(position, Rect.zero);
|
||||
final Offset caretOffsetTranslated = caretOffset.translate(0.0, verticalOffset);
|
||||
return _textPainter.getPositionForOffset(caretOffsetTranslated);
|
||||
}
|
||||
|
||||
/// Returns the size of the text as laid out.
|
||||
///
|
||||
/// This can differ from [size] if the text overflowed or if the [constraints]
|
||||
|
@ -1271,9 +1299,10 @@ class RenderParagraph extends RenderBox
|
|||
/// [PlaceHolderSpan]. The [RenderParagraph] splits itself on [PlaceHolderSpan]
|
||||
/// to create multiple `_SelectableFragment`s so that they can be selected
|
||||
/// separately.
|
||||
class _SelectableFragment with Selectable, ChangeNotifier {
|
||||
class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutMetrics {
|
||||
_SelectableFragment({
|
||||
required this.paragraph,
|
||||
required this.fullText,
|
||||
required this.range,
|
||||
}) : assert(range.isValid && !range.isCollapsed && range.isNormalized) {
|
||||
_selectionGeometry = _getSelectionGeometry();
|
||||
|
@ -1281,6 +1310,7 @@ class _SelectableFragment with Selectable, ChangeNotifier {
|
|||
|
||||
final TextRange range;
|
||||
final RenderParagraph paragraph;
|
||||
final String fullText;
|
||||
|
||||
TextPosition? _textSelectionStart;
|
||||
TextPosition? _textSelectionEnd;
|
||||
|
@ -1356,6 +1386,22 @@ class _SelectableFragment with Selectable, ChangeNotifier {
|
|||
final SelectWordSelectionEvent selectWord = event as SelectWordSelectionEvent;
|
||||
result = _handleSelectWord(selectWord.globalPosition);
|
||||
break;
|
||||
case SelectionEventType.granularlyExtendSelection:
|
||||
final GranularlyExtendSelectionEvent granularlyExtendSelection = event as GranularlyExtendSelectionEvent;
|
||||
result = _handleGranularlyExtendSelection(
|
||||
granularlyExtendSelection.forward,
|
||||
granularlyExtendSelection.isEnd,
|
||||
granularlyExtendSelection.granularity,
|
||||
);
|
||||
break;
|
||||
case SelectionEventType.directionallyExtendSelection:
|
||||
final DirectionallyExtendSelectionEvent directionallyExtendSelection = event as DirectionallyExtendSelectionEvent;
|
||||
result = _handleDirectionallyExtendSelection(
|
||||
directionallyExtendSelection.dx,
|
||||
directionallyExtendSelection.isEnd,
|
||||
directionallyExtendSelection.direction,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (existingSelectionStart != _textSelectionStart ||
|
||||
|
@ -1373,7 +1419,7 @@ class _SelectableFragment with Selectable, ChangeNotifier {
|
|||
final int start = math.min(_textSelectionStart!.offset, _textSelectionEnd!.offset);
|
||||
final int end = math.max(_textSelectionStart!.offset, _textSelectionEnd!.offset);
|
||||
return SelectedContent(
|
||||
plainText: paragraph.text.toPlainText(includeSemanticsLabels: false).substring(start, end),
|
||||
plainText: fullText.substring(start, end),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1466,6 +1512,155 @@ class _SelectableFragment with Selectable, ChangeNotifier {
|
|||
return SelectionResult.end;
|
||||
}
|
||||
|
||||
SelectionResult _handleDirectionallyExtendSelection(double horizontalBaseline, bool isExtent, SelectionExtendDirection movement) {
|
||||
final Matrix4 transform = paragraph.getTransformTo(null);
|
||||
if (transform.invert() == 0.0) {
|
||||
switch(movement) {
|
||||
case SelectionExtendDirection.previousLine:
|
||||
case SelectionExtendDirection.backward:
|
||||
return SelectionResult.previous;
|
||||
case SelectionExtendDirection.nextLine:
|
||||
case SelectionExtendDirection.forward:
|
||||
return SelectionResult.next;
|
||||
}
|
||||
}
|
||||
final double baselineInParagraphCoordinates = MatrixUtils.transformPoint(transform, Offset(horizontalBaseline, 0)).dx;
|
||||
assert(!baselineInParagraphCoordinates.isNaN);
|
||||
final TextPosition newPosition;
|
||||
final SelectionResult result;
|
||||
switch(movement) {
|
||||
case SelectionExtendDirection.previousLine:
|
||||
case SelectionExtendDirection.nextLine:
|
||||
assert(_textSelectionEnd != null && _textSelectionStart != null);
|
||||
final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!;
|
||||
final MapEntry<TextPosition, SelectionResult> moveResult = _handleVerticalMovement(
|
||||
targetedEdge,
|
||||
horizontalBaselineInParagraphCoordinates: baselineInParagraphCoordinates,
|
||||
below: movement == SelectionExtendDirection.nextLine,
|
||||
);
|
||||
newPosition = moveResult.key;
|
||||
result = moveResult.value;
|
||||
break;
|
||||
case SelectionExtendDirection.forward:
|
||||
case SelectionExtendDirection.backward:
|
||||
_textSelectionEnd ??= movement == SelectionExtendDirection.forward
|
||||
? TextPosition(offset: range.start)
|
||||
: TextPosition(offset: range.end, affinity: TextAffinity.upstream);
|
||||
_textSelectionStart ??= _textSelectionEnd;
|
||||
final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!;
|
||||
final Offset edgeOffsetInParagraphCoordinates = paragraph._getOffsetForPosition(targetedEdge);
|
||||
final Offset baselineOffsetInParagraphCoordinates = Offset(
|
||||
baselineInParagraphCoordinates,
|
||||
// Use half of line height to point to the middle of the line.
|
||||
edgeOffsetInParagraphCoordinates.dy - paragraph._textPainter.preferredLineHeight / 2,
|
||||
);
|
||||
newPosition = paragraph.getPositionForOffset(baselineOffsetInParagraphCoordinates);
|
||||
result = SelectionResult.end;
|
||||
break;
|
||||
}
|
||||
if (isExtent) {
|
||||
_textSelectionEnd = newPosition;
|
||||
} else {
|
||||
_textSelectionStart = newPosition;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
SelectionResult _handleGranularlyExtendSelection(bool forward, bool isExtent, TextGranularity granularity) {
|
||||
_textSelectionEnd ??= forward
|
||||
? TextPosition(offset: range.start)
|
||||
: TextPosition(offset: range.end, affinity: TextAffinity.upstream);
|
||||
_textSelectionStart ??= _textSelectionEnd;
|
||||
final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!;
|
||||
if (forward && (targetedEdge.offset == range.end)) {
|
||||
return SelectionResult.next;
|
||||
}
|
||||
if (!forward && (targetedEdge.offset == range.start)) {
|
||||
return SelectionResult.previous;
|
||||
}
|
||||
final SelectionResult result;
|
||||
final TextPosition newPosition;
|
||||
switch (granularity) {
|
||||
case TextGranularity.character:
|
||||
final String text = range.textInside(fullText);
|
||||
newPosition = _getNextPosition(CharacterBoundary(text), targetedEdge, forward);
|
||||
result = SelectionResult.end;
|
||||
break;
|
||||
case TextGranularity.word:
|
||||
final String text = range.textInside(fullText);
|
||||
newPosition = _getNextPosition(WhitespaceBoundary(text) + WordBoundary(this), targetedEdge, forward);
|
||||
result = SelectionResult.end;
|
||||
break;
|
||||
case TextGranularity.line:
|
||||
newPosition = _getNextPosition(LineBreak(this), targetedEdge, forward);
|
||||
result = SelectionResult.end;
|
||||
break;
|
||||
case TextGranularity.document:
|
||||
final String text = range.textInside(fullText);
|
||||
newPosition = _getNextPosition(DocumentBoundary(text), targetedEdge, forward);
|
||||
if (forward && newPosition.offset == range.end) {
|
||||
result = SelectionResult.next;
|
||||
} else if (!forward && newPosition.offset == range.start) {
|
||||
result = SelectionResult.previous;
|
||||
} else {
|
||||
result = SelectionResult.end;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (isExtent) {
|
||||
_textSelectionEnd = newPosition;
|
||||
} else {
|
||||
_textSelectionStart = newPosition;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
TextPosition _getNextPosition(TextBoundary boundary, TextPosition position, bool forward) {
|
||||
if (forward) {
|
||||
return _clampTextPosition(
|
||||
(PushTextPosition.forward + boundary).getTrailingTextBoundaryAt(position)
|
||||
);
|
||||
}
|
||||
return _clampTextPosition(
|
||||
(PushTextPosition.backward + boundary).getLeadingTextBoundaryAt(position),
|
||||
);
|
||||
}
|
||||
|
||||
MapEntry<TextPosition, SelectionResult> _handleVerticalMovement(TextPosition position, {required double horizontalBaselineInParagraphCoordinates, required bool below}) {
|
||||
final List<ui.LineMetrics> lines = paragraph._computeLineMetrics();
|
||||
final Offset offset = paragraph.getOffsetForCaret(position, Rect.zero);
|
||||
int currentLine = lines.length - 1;
|
||||
for (final ui.LineMetrics lineMetrics in lines) {
|
||||
if (lineMetrics.baseline > offset.dy) {
|
||||
currentLine = lineMetrics.lineNumber;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final TextPosition newPosition;
|
||||
if (below && currentLine == lines.length - 1) {
|
||||
newPosition = TextPosition(offset: range.end, affinity: TextAffinity.upstream);
|
||||
} else if (!below && currentLine == 0) {
|
||||
newPosition = TextPosition(offset: range.start);
|
||||
} else {
|
||||
final int newLine = below ? currentLine + 1 : currentLine - 1;
|
||||
newPosition = _clampTextPosition(
|
||||
paragraph.getPositionForOffset(Offset(horizontalBaselineInParagraphCoordinates, lines[newLine].baseline))
|
||||
);
|
||||
}
|
||||
final SelectionResult result;
|
||||
if (newPosition.offset == range.start) {
|
||||
result = SelectionResult.previous;
|
||||
} else if (newPosition.offset == range.end) {
|
||||
result = SelectionResult.next;
|
||||
} else {
|
||||
result = SelectionResult.end;
|
||||
}
|
||||
assert(result != SelectionResult.next || below);
|
||||
assert(result != SelectionResult.previous || !below);
|
||||
return MapEntry<TextPosition, SelectionResult>(newPosition, result);
|
||||
}
|
||||
|
||||
/// Whether the given text position is contained in current selection
|
||||
/// range.
|
||||
///
|
||||
|
@ -1596,4 +1791,25 @@ class _SelectableFragment with Selectable, ChangeNotifier {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
TextSelection getLineAtOffset(TextPosition position) {
|
||||
final TextRange line = paragraph._getLineAtOffset(position);
|
||||
final int start = line.start.clamp(range.start, range.end); // ignore_clamp_double_lint
|
||||
final int end = line.end.clamp(range.start, range.end); // ignore_clamp_double_lint
|
||||
return TextSelection(baseOffset: start, extentOffset: end);
|
||||
}
|
||||
|
||||
@override
|
||||
TextPosition getTextPositionAbove(TextPosition position) {
|
||||
return _clampTextPosition(paragraph._getTextPositionAbove(position));
|
||||
}
|
||||
|
||||
@override
|
||||
TextPosition getTextPositionBelow(TextPosition position) {
|
||||
return _clampTextPosition(paragraph._getTextPositionBelow(position));
|
||||
}
|
||||
|
||||
@override
|
||||
TextRange getWordBoundary(TextPosition position) => paragraph.getWordBoundary(position);
|
||||
}
|
||||
|
|
|
@ -296,6 +296,30 @@ enum SelectionEventType {
|
|||
///
|
||||
/// Used by [SelectWordSelectionEvent].
|
||||
selectWord,
|
||||
|
||||
/// An event that extends the selection by a specific [TextGranularity].
|
||||
granularlyExtendSelection,
|
||||
|
||||
/// An event that extends the selection in a specific direction.
|
||||
directionallyExtendSelection,
|
||||
}
|
||||
|
||||
/// The unit of how selection handles move in text.
|
||||
///
|
||||
/// The [GranularlyExtendSelectionEvent] uses this enum to describe how
|
||||
/// [Selectable] should extend its selection.
|
||||
enum TextGranularity {
|
||||
/// Treats each character as an atomic unit when moving the selection handles.
|
||||
character,
|
||||
|
||||
/// Treats word as an atomic unit when moving the selection handles.
|
||||
word,
|
||||
|
||||
/// Treats each line break as an atomic unit when moving the selection handles.
|
||||
line,
|
||||
|
||||
/// Treats the entire document as an atomic unit when moving the selection handles.
|
||||
document,
|
||||
}
|
||||
|
||||
/// An abstract base class for selection events.
|
||||
|
@ -375,6 +399,127 @@ class SelectionEdgeUpdateEvent extends SelectionEvent {
|
|||
final Offset globalPosition;
|
||||
}
|
||||
|
||||
/// Extends the start or end of the selection by a given [TextGranularity].
|
||||
///
|
||||
/// To handle this event, move the associated selection edge, as dictated by
|
||||
/// [isEnd], according to the [granularity].
|
||||
class GranularlyExtendSelectionEvent extends SelectionEvent {
|
||||
/// Creates a [GranularlyExtendSelectionEvent].
|
||||
///
|
||||
/// All parameters are required and must not be null.
|
||||
const GranularlyExtendSelectionEvent({
|
||||
required this.forward,
|
||||
required this.isEnd,
|
||||
required this.granularity,
|
||||
}) : super._(SelectionEventType.granularlyExtendSelection);
|
||||
|
||||
/// Whether to extend the selection forward.
|
||||
final bool forward;
|
||||
|
||||
/// Whether this event is updating the end selection edge.
|
||||
final bool isEnd;
|
||||
|
||||
/// The granularity for which the selection extend.
|
||||
final TextGranularity granularity;
|
||||
}
|
||||
|
||||
/// The direction to extend a selection.
|
||||
///
|
||||
/// The [DirectionallyExtendSelectionEvent] uses this enum to describe how
|
||||
/// [Selectable] should extend their selection.
|
||||
enum SelectionExtendDirection {
|
||||
/// Move one edge of the selection vertically to the previous adjacent line.
|
||||
///
|
||||
/// For text selection, it should consider both soft and hard linebreak.
|
||||
///
|
||||
/// See [DirectionallyExtendSelectionEvent.dx] on how to
|
||||
/// calculate the horizontal offset.
|
||||
previousLine,
|
||||
|
||||
/// Move one edge of the selection vertically to the next adjacent line.
|
||||
///
|
||||
/// For text selection, it should consider both soft and hard linebreak.
|
||||
///
|
||||
/// See [DirectionallyExtendSelectionEvent.dx] on how to
|
||||
/// calculate the horizontal offset.
|
||||
nextLine,
|
||||
|
||||
/// Move the selection edges forward to a certain horizontal offset in the
|
||||
/// same line.
|
||||
///
|
||||
/// If there is no on-going selection, the selection must start with the first
|
||||
/// line (or equivalence of first line in a non-text selectable) and select
|
||||
/// toward the horizontal offset in the same line.
|
||||
///
|
||||
/// The selectable that receives [DirectionallyExtendSelectionEvent] with this
|
||||
/// enum must return [SelectionResult.end].
|
||||
///
|
||||
/// See [DirectionallyExtendSelectionEvent.dx] on how to
|
||||
/// calculate the horizontal offset.
|
||||
forward,
|
||||
|
||||
/// Move the selection edges backward to a certain horizontal offset in the
|
||||
/// same line.
|
||||
///
|
||||
/// If there is no on-going selection, the selection must start with the last
|
||||
/// line (or equivalence of last line in a non-text selectable) and select
|
||||
/// backward the horizontal offset in the same line.
|
||||
///
|
||||
/// The selectable that receives [DirectionallyExtendSelectionEvent] with this
|
||||
/// enum must return [SelectionResult.end].
|
||||
///
|
||||
/// See [DirectionallyExtendSelectionEvent.dx] on how to
|
||||
/// calculate the horizontal offset.
|
||||
backward,
|
||||
}
|
||||
|
||||
/// Extends the current selection with respect to a [direction].
|
||||
///
|
||||
/// To handle this event, move the associated selection edge, as dictated by
|
||||
/// [isEnd], according to the [direction].
|
||||
///
|
||||
/// The movements are always based on [dx]. The value is in
|
||||
/// global coordinates and is the horizontal offset the selection edge should
|
||||
/// move to when moving to across lines.
|
||||
class DirectionallyExtendSelectionEvent extends SelectionEvent {
|
||||
/// Creates a [DirectionallyExtendSelectionEvent].
|
||||
///
|
||||
/// All parameters are required and must not be null.
|
||||
const DirectionallyExtendSelectionEvent({
|
||||
required this.dx,
|
||||
required this.isEnd,
|
||||
required this.direction,
|
||||
}) : super._(SelectionEventType.directionallyExtendSelection);
|
||||
|
||||
/// The horizontal offset the selection should move to.
|
||||
///
|
||||
/// The offset is in global coordinates.
|
||||
final double dx;
|
||||
|
||||
/// Whether this event is updating the end selection edge.
|
||||
final bool isEnd;
|
||||
|
||||
/// The directional movement of this event.
|
||||
///
|
||||
/// See also:
|
||||
/// * [SelectionExtendDirection], which explains how to handle each enum.
|
||||
final SelectionExtendDirection direction;
|
||||
|
||||
/// Makes a copy of this object with its property replaced with the new
|
||||
/// values.
|
||||
DirectionallyExtendSelectionEvent copyWith({
|
||||
double? dx,
|
||||
bool? isEnd,
|
||||
SelectionExtendDirection? direction,
|
||||
}) {
|
||||
return DirectionallyExtendSelectionEvent(
|
||||
dx: dx ?? this.dx,
|
||||
isEnd: isEnd ?? this.isEnd,
|
||||
direction: direction ?? this.direction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A registrar that keeps track of [Selectable]s in the subtree.
|
||||
///
|
||||
/// A [Selectable] is only included in the [SelectableRegion] if they are
|
||||
|
|
|
@ -295,8 +295,8 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
|
|||
|
||||
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: false),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: true),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false, collapseAtReversal: true),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false, collapseAtReversal: true),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: false),
|
||||
|
||||
const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
|
||||
|
|
|
@ -1248,11 +1248,11 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont
|
|||
/// selection is triggered by none drag events. The
|
||||
/// [_currentDragStartRelatedToOrigin] and [_currentDragEndRelatedToOrigin]
|
||||
/// are essential to handle future [SelectionEdgeUpdateEvent]s.
|
||||
void _updateDragLocationsFromGeometries() {
|
||||
void _updateDragLocationsFromGeometries({bool forceUpdateStart = true, bool forceUpdateEnd = true}) {
|
||||
final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
|
||||
final RenderBox box = state.context.findRenderObject()! as RenderBox;
|
||||
final Matrix4 transform = box.getTransformTo(null);
|
||||
if (currentSelectionStartIndex != -1) {
|
||||
if (currentSelectionStartIndex != -1 && (_currentDragStartRelatedToOrigin == null || forceUpdateStart)) {
|
||||
final SelectionGeometry geometry = selectables[currentSelectionStartIndex].value;
|
||||
assert(geometry.hasSelection);
|
||||
final SelectionPoint start = geometry.startSelectionPoint!;
|
||||
|
@ -1263,7 +1263,7 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont
|
|||
);
|
||||
_currentDragStartRelatedToOrigin = MatrixUtils.transformPoint(transform, localDragStart + deltaToOrigin);
|
||||
}
|
||||
if (currentSelectionEndIndex != -1) {
|
||||
if (currentSelectionEndIndex != -1 && (_currentDragEndRelatedToOrigin == null || forceUpdateEnd)) {
|
||||
final SelectionGeometry geometry = selectables[currentSelectionEndIndex].value;
|
||||
assert(geometry.hasSelection);
|
||||
final SelectionPoint end = geometry.endSelectionPoint!;
|
||||
|
@ -1295,6 +1295,116 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont
|
|||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
SelectionResult handleGranularlyExtendSelection(GranularlyExtendSelectionEvent event) {
|
||||
final SelectionResult result = super.handleGranularlyExtendSelection(event);
|
||||
// The selection geometry may not have the accurate offset for the edges
|
||||
// that are outside of the viewport whose transform may not be valid. Only
|
||||
// the edge this event is updating is sure to be accurate.
|
||||
_updateDragLocationsFromGeometries(
|
||||
forceUpdateStart: !event.isEnd,
|
||||
forceUpdateEnd: event.isEnd,
|
||||
);
|
||||
if (_selectionStartsInScrollable) {
|
||||
_jumpToEdge(event.isEnd);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
SelectionResult handleDirectionallyExtendSelection(DirectionallyExtendSelectionEvent event) {
|
||||
final SelectionResult result = super.handleDirectionallyExtendSelection(event);
|
||||
// The selection geometry may not have the accurate offset for the edges
|
||||
// that are outside of the viewport whose transform may not be valid. Only
|
||||
// the edge this event is updating is sure to be accurate.
|
||||
_updateDragLocationsFromGeometries(
|
||||
forceUpdateStart: !event.isEnd,
|
||||
forceUpdateEnd: event.isEnd,
|
||||
);
|
||||
if (_selectionStartsInScrollable) {
|
||||
_jumpToEdge(event.isEnd);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void _jumpToEdge(bool isExtent) {
|
||||
final Selectable selectable;
|
||||
final double? lineHeight;
|
||||
final SelectionPoint? edge;
|
||||
if (isExtent) {
|
||||
selectable = selectables[currentSelectionEndIndex];
|
||||
edge = selectable.value.endSelectionPoint;
|
||||
lineHeight = selectable.value.endSelectionPoint!.lineHeight;
|
||||
} else {
|
||||
selectable = selectables[currentSelectionStartIndex];
|
||||
edge = selectable.value.startSelectionPoint;
|
||||
lineHeight = selectable.value.startSelectionPoint?.lineHeight;
|
||||
}
|
||||
if (lineHeight == null || edge == null) {
|
||||
return;
|
||||
}
|
||||
final RenderBox scrollableBox = state.context.findRenderObject()! as RenderBox;
|
||||
final Matrix4 transform = selectable.getTransformTo(scrollableBox);
|
||||
final Offset edgeOffsetInScrollableCoordinates = MatrixUtils.transformPoint(transform, edge.localPosition);
|
||||
final Rect scrollableRect = Rect.fromLTRB(0, 0, scrollableBox.size.width, scrollableBox.size.height);
|
||||
switch (state.axisDirection) {
|
||||
case AxisDirection.up:
|
||||
final double edgeBottom = edgeOffsetInScrollableCoordinates.dy;
|
||||
final double edgeTop = edgeOffsetInScrollableCoordinates.dy - lineHeight;
|
||||
if (edgeBottom >= scrollableRect.bottom && edgeTop <= scrollableRect.top) {
|
||||
return;
|
||||
}
|
||||
if (edgeBottom > scrollableRect.bottom) {
|
||||
position.jumpTo(position.pixels + scrollableRect.bottom - edgeBottom);
|
||||
return;
|
||||
}
|
||||
if (edgeTop < scrollableRect.top) {
|
||||
position.jumpTo(position.pixels + scrollableRect.top - edgeTop);
|
||||
}
|
||||
return;
|
||||
case AxisDirection.right:
|
||||
final double edge = edgeOffsetInScrollableCoordinates.dx;
|
||||
if (edge >= scrollableRect.right && edge <= scrollableRect.left) {
|
||||
return;
|
||||
}
|
||||
if (edge > scrollableRect.right) {
|
||||
position.jumpTo(position.pixels + edge - scrollableRect.right);
|
||||
return;
|
||||
}
|
||||
if (edge < scrollableRect.left) {
|
||||
position.jumpTo(position.pixels + edge - scrollableRect.left);
|
||||
}
|
||||
return;
|
||||
case AxisDirection.down:
|
||||
final double edgeBottom = edgeOffsetInScrollableCoordinates.dy;
|
||||
final double edgeTop = edgeOffsetInScrollableCoordinates.dy - lineHeight;
|
||||
if (edgeBottom >= scrollableRect.bottom && edgeTop <= scrollableRect.top) {
|
||||
return;
|
||||
}
|
||||
if (edgeBottom > scrollableRect.bottom) {
|
||||
position.jumpTo(position.pixels + edgeBottom - scrollableRect.bottom);
|
||||
return;
|
||||
}
|
||||
if (edgeTop < scrollableRect.top) {
|
||||
position.jumpTo(position.pixels + edgeTop - scrollableRect.top);
|
||||
}
|
||||
return;
|
||||
case AxisDirection.left:
|
||||
final double edge = edgeOffsetInScrollableCoordinates.dx;
|
||||
if (edge >= scrollableRect.right && edge <= scrollableRect.left) {
|
||||
return;
|
||||
}
|
||||
if (edge > scrollableRect.right) {
|
||||
position.jumpTo(position.pixels + scrollableRect.right - edge);
|
||||
return;
|
||||
}
|
||||
if (edge < scrollableRect.left) {
|
||||
position.jumpTo(position.pixels + scrollableRect.left - edge);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool _globalPositionInScrollable(Offset globalPosition) {
|
||||
final RenderBox box = state.context.findRenderObject()! as RenderBox;
|
||||
final Offset localPosition = box.globalToLocal(globalPosition);
|
||||
|
@ -1317,6 +1427,12 @@ class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionCont
|
|||
_selectableEndEdgeUpdateRecords[selectable] = state.position.pixels;
|
||||
ensureChildUpdated(selectable);
|
||||
break;
|
||||
case SelectionEventType.granularlyExtendSelection:
|
||||
case SelectionEventType.directionallyExtendSelection:
|
||||
ensureChildUpdated(selectable);
|
||||
_selectableStartEdgeUpdateRecords[selectable] = state.position.pixels;
|
||||
_selectableEndEdgeUpdateRecords[selectable] = state.position.pixels;
|
||||
break;
|
||||
case SelectionEventType.clear:
|
||||
_selectableEndEdgeUpdateRecords.remove(selectable);
|
||||
_selectableStartEdgeUpdateRecords.remove(selectable);
|
||||
|
|
|
@ -291,7 +291,16 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
|
||||
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
|
||||
CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
|
||||
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_GranularlyExtendSelectionAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(this, granularity: TextGranularity.word)),
|
||||
ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(_GranularlyExtendSelectionAction<ExpandSelectionToDocumentBoundaryIntent>(this, granularity: TextGranularity.document)),
|
||||
ExpandSelectionToLineBreakIntent: _makeOverridable(_GranularlyExtendSelectionAction<ExpandSelectionToLineBreakIntent>(this, granularity: TextGranularity.line)),
|
||||
ExtendSelectionByCharacterIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionByCharacterIntent>(this, granularity: TextGranularity.character)),
|
||||
ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, granularity: TextGranularity.word)),
|
||||
ExtendSelectionToLineBreakIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionToLineBreakIntent>(this, granularity: TextGranularity.line)),
|
||||
ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_DirectionallyExtendCaretSelectionAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this)),
|
||||
ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, granularity: TextGranularity.document)),
|
||||
};
|
||||
|
||||
final Map<Type, GestureRecognizerFactory> _gestureRecognizers = <Type, GestureRecognizerFactory>{};
|
||||
SelectionOverlay? _selectionOverlay;
|
||||
final LayerLink _startHandleLayerLink = LayerLink();
|
||||
|
@ -329,7 +338,6 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.iOS:
|
||||
|
@ -864,6 +872,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
/// Removes the ongoing selection.
|
||||
void _clearSelection() {
|
||||
_finalizeSelection();
|
||||
_directionalHorizontalBaseline = null;
|
||||
_adjustingSelectionEnd = null;
|
||||
_selectable?.dispatchSelectionEvent(const ClearSelectionEvent());
|
||||
_updateSelectedContentIfNeeded();
|
||||
}
|
||||
|
@ -897,6 +907,63 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
|
|||
);
|
||||
}
|
||||
|
||||
bool? _adjustingSelectionEnd;
|
||||
bool _determineIsAdjustingSelectionEnd(bool forward) {
|
||||
if (_adjustingSelectionEnd != null) {
|
||||
return _adjustingSelectionEnd!;
|
||||
}
|
||||
final bool isReversed;
|
||||
final SelectionPoint start = _selectionDelegate.value
|
||||
.startSelectionPoint!;
|
||||
final SelectionPoint end = _selectionDelegate.value.endSelectionPoint!;
|
||||
if (start.localPosition.dy > end.localPosition.dy) {
|
||||
isReversed = true;
|
||||
} else if (start.localPosition.dy < end.localPosition.dy) {
|
||||
isReversed = false;
|
||||
} else {
|
||||
isReversed = start.localPosition.dx > end.localPosition.dx;
|
||||
}
|
||||
// Always move the selection edge that increases the selection range.
|
||||
return _adjustingSelectionEnd = forward != isReversed;
|
||||
}
|
||||
|
||||
void _granularlyExtendSelection(TextGranularity granularity, bool forward) {
|
||||
_directionalHorizontalBaseline = null;
|
||||
if (!_selectionDelegate.value.hasSelection) {
|
||||
return;
|
||||
}
|
||||
_selectable?.dispatchSelectionEvent(
|
||||
GranularlyExtendSelectionEvent(
|
||||
forward: forward,
|
||||
isEnd: _determineIsAdjustingSelectionEnd(forward),
|
||||
granularity: granularity,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double? _directionalHorizontalBaseline;
|
||||
|
||||
void _directionallyExtendSelection(bool forward) {
|
||||
if (!_selectionDelegate.value.hasSelection) {
|
||||
return;
|
||||
}
|
||||
final bool adjustingSelectionExtend = _determineIsAdjustingSelectionEnd(forward);
|
||||
final SelectionPoint baseLinePoint = adjustingSelectionExtend
|
||||
? _selectionDelegate.value.endSelectionPoint!
|
||||
: _selectionDelegate.value.startSelectionPoint!;
|
||||
_directionalHorizontalBaseline ??= baseLinePoint.localPosition.dx;
|
||||
final Offset globalSelectionPointOffset = MatrixUtils.transformPoint(context.findRenderObject()!.getTransformTo(null), Offset(_directionalHorizontalBaseline!, 0));
|
||||
_selectable?.dispatchSelectionEvent(
|
||||
DirectionallyExtendSelectionEvent(
|
||||
isEnd: _adjustingSelectionEnd!,
|
||||
direction: forward ? SelectionExtendDirection.nextLine : SelectionExtendDirection.previousLine,
|
||||
dx: globalSelectionPointOffset.dx,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// [TextSelectionDelegate] overrides.
|
||||
|
||||
/// Returns the [ContextMenuButtonItem]s representing the buttons in this
|
||||
/// platform's default selection menu.
|
||||
///
|
||||
|
@ -1147,6 +1214,49 @@ class _CopySelectionAction extends _NonOverrideAction<CopySelectionTextIntent> {
|
|||
}
|
||||
}
|
||||
|
||||
class _GranularlyExtendSelectionAction<T extends DirectionalTextEditingIntent> extends _NonOverrideAction<T> {
|
||||
_GranularlyExtendSelectionAction(this.state, {required this.granularity});
|
||||
|
||||
final SelectableRegionState state;
|
||||
final TextGranularity granularity;
|
||||
|
||||
@override
|
||||
void invokeAction(T intent, [BuildContext? context]) {
|
||||
state._granularlyExtendSelection(granularity, intent.forward);
|
||||
}
|
||||
}
|
||||
|
||||
class _GranularlyExtendCaretSelectionAction<T extends DirectionalCaretMovementIntent> extends _NonOverrideAction<T> {
|
||||
_GranularlyExtendCaretSelectionAction(this.state, {required this.granularity});
|
||||
|
||||
final SelectableRegionState state;
|
||||
final TextGranularity granularity;
|
||||
|
||||
@override
|
||||
void invokeAction(T intent, [BuildContext? context]) {
|
||||
if (intent.collapseSelection) {
|
||||
// Selectable region never collapses selection.
|
||||
return;
|
||||
}
|
||||
state._granularlyExtendSelection(granularity, intent.forward);
|
||||
}
|
||||
}
|
||||
|
||||
class _DirectionallyExtendCaretSelectionAction<T extends DirectionalCaretMovementIntent> extends _NonOverrideAction<T> {
|
||||
_DirectionallyExtendCaretSelectionAction(this.state);
|
||||
|
||||
final SelectableRegionState state;
|
||||
|
||||
@override
|
||||
void invokeAction(T intent, [BuildContext? context]) {
|
||||
if (intent.collapseSelection) {
|
||||
// Selectable region never collapses selection.
|
||||
return;
|
||||
}
|
||||
state._directionallyExtendSelection(intent.forward);
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectableRegionContainerDelegate extends MultiSelectableSelectionContainerDelegate {
|
||||
final Set<Selectable> _hasReceivedStartEvent = <Selectable>{};
|
||||
final Set<Selectable> _hasReceivedEndEvent = <Selectable>{};
|
||||
|
@ -1248,6 +1358,12 @@ class _SelectableRegionContainerDelegate extends MultiSelectableSelectionContain
|
|||
case SelectionEventType.selectAll:
|
||||
case SelectionEventType.selectWord:
|
||||
break;
|
||||
case SelectionEventType.granularlyExtendSelection:
|
||||
case SelectionEventType.directionallyExtendSelection:
|
||||
_hasReceivedStartEvent.add(selectable);
|
||||
_hasReceivedEndEvent.add(selectable);
|
||||
ensureChildUpdated(selectable);
|
||||
break;
|
||||
}
|
||||
return super.dispatchSelectionEventToChild(selectable, event);
|
||||
}
|
||||
|
@ -1340,6 +1456,8 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
|
|||
bool _selectionInProgress = false;
|
||||
Set<Selectable> _additions = <Selectable>{};
|
||||
|
||||
bool _extendSelectionInProgress = false;
|
||||
|
||||
@override
|
||||
void add(Selectable selectable) {
|
||||
assert(!selectables.contains(selectable));
|
||||
|
@ -1548,14 +1666,16 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
|
|||
);
|
||||
}
|
||||
|
||||
currentSelectionStartIndex = _adjustSelectionIndexBasedOnSelectionGeometry(
|
||||
currentSelectionStartIndex,
|
||||
currentSelectionEndIndex,
|
||||
);
|
||||
currentSelectionEndIndex = _adjustSelectionIndexBasedOnSelectionGeometry(
|
||||
currentSelectionEndIndex,
|
||||
currentSelectionStartIndex,
|
||||
);
|
||||
if (!_extendSelectionInProgress) {
|
||||
currentSelectionStartIndex = _adjustSelectionIndexBasedOnSelectionGeometry(
|
||||
currentSelectionStartIndex,
|
||||
currentSelectionEndIndex,
|
||||
);
|
||||
currentSelectionEndIndex = _adjustSelectionIndexBasedOnSelectionGeometry(
|
||||
currentSelectionEndIndex,
|
||||
currentSelectionStartIndex,
|
||||
);
|
||||
}
|
||||
|
||||
// Need to find the non-null start selection point.
|
||||
SelectionGeometry startGeometry = selectables[currentSelectionStartIndex].value;
|
||||
|
@ -1760,6 +1880,100 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
|
|||
return SelectionResult.none;
|
||||
}
|
||||
|
||||
/// Extend current selection in a certain text granularity.
|
||||
@protected
|
||||
SelectionResult handleGranularlyExtendSelection(GranularlyExtendSelectionEvent event) {
|
||||
assert((currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1));
|
||||
if (currentSelectionStartIndex == -1) {
|
||||
if (event.forward) {
|
||||
currentSelectionStartIndex = currentSelectionEndIndex = 0;
|
||||
} else {
|
||||
currentSelectionStartIndex = currentSelectionEndIndex = selectables.length;
|
||||
}
|
||||
}
|
||||
int targetIndex = event.isEnd ? currentSelectionEndIndex : currentSelectionStartIndex;
|
||||
SelectionResult result = dispatchSelectionEventToChild(selectables[targetIndex], event);
|
||||
if (event.forward) {
|
||||
assert(result != SelectionResult.previous);
|
||||
while (targetIndex < selectables.length - 1 && result == SelectionResult.next) {
|
||||
targetIndex += 1;
|
||||
result = dispatchSelectionEventToChild(selectables[targetIndex], event);
|
||||
assert(result != SelectionResult.previous);
|
||||
}
|
||||
} else {
|
||||
assert(result != SelectionResult.next);
|
||||
while (targetIndex > 0 && result == SelectionResult.previous) {
|
||||
targetIndex -= 1;
|
||||
result = dispatchSelectionEventToChild(selectables[targetIndex], event);
|
||||
assert(result != SelectionResult.next);
|
||||
}
|
||||
}
|
||||
if (event.isEnd) {
|
||||
currentSelectionEndIndex = targetIndex;
|
||||
} else {
|
||||
currentSelectionStartIndex = targetIndex;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Extend current selection in a certain text granularity.
|
||||
@protected
|
||||
SelectionResult handleDirectionallyExtendSelection(DirectionallyExtendSelectionEvent event) {
|
||||
assert((currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1));
|
||||
if (currentSelectionStartIndex == -1) {
|
||||
switch(event.direction) {
|
||||
case SelectionExtendDirection.previousLine:
|
||||
case SelectionExtendDirection.backward:
|
||||
currentSelectionStartIndex = currentSelectionEndIndex = selectables.length;
|
||||
break;
|
||||
case SelectionExtendDirection.nextLine:
|
||||
case SelectionExtendDirection.forward:
|
||||
currentSelectionStartIndex = currentSelectionEndIndex = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int targetIndex = event.isEnd ? currentSelectionEndIndex : currentSelectionStartIndex;
|
||||
SelectionResult result = dispatchSelectionEventToChild(selectables[targetIndex], event);
|
||||
switch (event.direction) {
|
||||
case SelectionExtendDirection.previousLine:
|
||||
assert(result == SelectionResult.end || result == SelectionResult.previous);
|
||||
if (result == SelectionResult.previous) {
|
||||
if (targetIndex > 0) {
|
||||
targetIndex -= 1;
|
||||
result = dispatchSelectionEventToChild(
|
||||
selectables[targetIndex],
|
||||
event.copyWith(direction: SelectionExtendDirection.backward),
|
||||
);
|
||||
assert(result == SelectionResult.end);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SelectionExtendDirection.nextLine:
|
||||
assert(result == SelectionResult.end || result == SelectionResult.next);
|
||||
if (result == SelectionResult.next) {
|
||||
if (targetIndex < selectables.length - 1) {
|
||||
targetIndex += 1;
|
||||
result = dispatchSelectionEventToChild(
|
||||
selectables[targetIndex],
|
||||
event.copyWith(direction: SelectionExtendDirection.forward),
|
||||
);
|
||||
assert(result == SelectionResult.end);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SelectionExtendDirection.forward:
|
||||
case SelectionExtendDirection.backward:
|
||||
assert(result == SelectionResult.end);
|
||||
break;
|
||||
}
|
||||
if (event.isEnd) {
|
||||
currentSelectionEndIndex = targetIndex;
|
||||
} else {
|
||||
currentSelectionStartIndex = targetIndex;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Updates the selection edges.
|
||||
@protected
|
||||
SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
|
||||
|
@ -1782,17 +1996,29 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
|
|||
switch (event.type) {
|
||||
case SelectionEventType.startEdgeUpdate:
|
||||
case SelectionEventType.endEdgeUpdate:
|
||||
_extendSelectionInProgress = false;
|
||||
result = handleSelectionEdgeUpdate(event as SelectionEdgeUpdateEvent);
|
||||
break;
|
||||
case SelectionEventType.clear:
|
||||
_extendSelectionInProgress = false;
|
||||
result = handleClearSelection(event as ClearSelectionEvent);
|
||||
break;
|
||||
case SelectionEventType.selectAll:
|
||||
_extendSelectionInProgress = false;
|
||||
result = handleSelectAll(event as SelectAllSelectionEvent);
|
||||
break;
|
||||
case SelectionEventType.selectWord:
|
||||
_extendSelectionInProgress = false;
|
||||
result = handleSelectWord(event as SelectWordSelectionEvent);
|
||||
break;
|
||||
case SelectionEventType.granularlyExtendSelection:
|
||||
_extendSelectionInProgress = true;
|
||||
result = handleGranularlyExtendSelection(event as GranularlyExtendSelectionEvent);
|
||||
break;
|
||||
case SelectionEventType.directionallyExtendSelection:
|
||||
_extendSelectionInProgress = true;
|
||||
result = handleDirectionallyExtendSelection(event as DirectionallyExtendSelectionEvent);
|
||||
break;
|
||||
}
|
||||
_isHandlingSelectionEvent = false;
|
||||
_updateSelectionGeometry();
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'rendering_tester.dart';
|
||||
|
||||
const String _kText = "I polished up that handle so carefullee\nThat now I am the Ruler of the Queen's Navee!";
|
||||
const bool isCanvasKit = bool.fromEnvironment('FLUTTER_WEB_USE_SKIA');
|
||||
|
||||
// A subclass of RenderParagraph that returns an empty list in getBoxesForSelection
|
||||
// for a given TextSelection.
|
||||
|
@ -821,12 +822,12 @@ void main() {
|
|||
for (final Selectable selectable in (paragraph.registrar! as TestSelectionRegistrar).selectables) {
|
||||
selectable.dispatchSelectionEvent(
|
||||
SelectionEdgeUpdateEvent.forStart(
|
||||
globalPosition: paragraph.getOffsetForCaret(start, Rect.zero),
|
||||
globalPosition: paragraph.getOffsetForCaret(start, Rect.zero) + const Offset(0, 5),
|
||||
),
|
||||
);
|
||||
selectable.dispatchSelectionEvent(
|
||||
SelectionEdgeUpdateEvent.forEnd(
|
||||
globalPosition: paragraph.getOffsetForCaret(end, Rect.zero),
|
||||
globalPosition: paragraph.getOffsetForCaret(end, Rect.zero) + const Offset(0, 5),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -911,6 +912,392 @@ void main() {
|
|||
expect(geometry2.hasContent, true);
|
||||
expect(geometry2.status, SelectionStatus.uncollapsed);
|
||||
});
|
||||
|
||||
test('can granularly extend selection - character', () async {
|
||||
final TestSelectionRegistrar registrar = TestSelectionRegistrar();
|
||||
final List<RenderBox> renderBoxes = <RenderBox>[];
|
||||
final RenderParagraph paragraph = RenderParagraph(
|
||||
const TextSpan(
|
||||
children: <InlineSpan>[
|
||||
TextSpan(text: 'how are you\nI am fine\nThank you'),
|
||||
]
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
registrar: registrar,
|
||||
children: renderBoxes,
|
||||
);
|
||||
layout(paragraph);
|
||||
|
||||
expect(registrar.selectables.length, 1);
|
||||
selectionParagraph(paragraph, const TextPosition(offset: 4), const TextPosition(offset: 5));
|
||||
expect(paragraph.selections.length, 1);
|
||||
TextSelection selection = paragraph.selections[0];
|
||||
expect(selection.start, 4); // how [a]re you
|
||||
expect(selection.end, 5);
|
||||
|
||||
// Equivalent to sending shift + arrow-right
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const GranularlyExtendSelectionEvent(
|
||||
forward: true,
|
||||
isEnd: true,
|
||||
granularity: TextGranularity.character,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
expect(selection.start, 4); // how [ar]e you
|
||||
expect(selection.end, 6);
|
||||
|
||||
// Equivalent to sending shift + arrow-left
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const GranularlyExtendSelectionEvent(
|
||||
forward: false,
|
||||
isEnd: true,
|
||||
granularity: TextGranularity.character,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
expect(selection.start, 4); // how [a]re you
|
||||
expect(selection.end, 5);
|
||||
});
|
||||
|
||||
test('can granularly extend selection - word', () async {
|
||||
final TestSelectionRegistrar registrar = TestSelectionRegistrar();
|
||||
final List<RenderBox> renderBoxes = <RenderBox>[];
|
||||
final RenderParagraph paragraph = RenderParagraph(
|
||||
const TextSpan(
|
||||
children: <InlineSpan>[
|
||||
TextSpan(text: 'how are you\nI am fine\nThank you'),
|
||||
]
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
registrar: registrar,
|
||||
children: renderBoxes,
|
||||
);
|
||||
layout(paragraph);
|
||||
|
||||
expect(registrar.selectables.length, 1);
|
||||
selectionParagraph(paragraph, const TextPosition(offset: 4), const TextPosition(offset: 5));
|
||||
expect(paragraph.selections.length, 1);
|
||||
TextSelection selection = paragraph.selections[0];
|
||||
expect(selection.start, 4); // how [a]re you
|
||||
expect(selection.end, 5);
|
||||
|
||||
// Equivalent to sending shift + alt + arrow-right.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const GranularlyExtendSelectionEvent(
|
||||
forward: true,
|
||||
isEnd: true,
|
||||
granularity: TextGranularity.word,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
expect(selection.start, 4); // how [are] you
|
||||
expect(selection.end, 7);
|
||||
|
||||
// Equivalent to sending shift + alt + arrow-left.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const GranularlyExtendSelectionEvent(
|
||||
forward: false,
|
||||
isEnd: true,
|
||||
granularity: TextGranularity.word,
|
||||
),
|
||||
);
|
||||
expect(paragraph.selections.length, 0); // how []are you
|
||||
|
||||
// Equivalent to sending shift + alt + arrow-left.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const GranularlyExtendSelectionEvent(
|
||||
forward: false,
|
||||
isEnd: true,
|
||||
granularity: TextGranularity.word,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
expect(selection.start, 0); // [how ]are you
|
||||
expect(selection.end, 4);
|
||||
});
|
||||
|
||||
test('can granularly extend selection - line', () async {
|
||||
final TestSelectionRegistrar registrar = TestSelectionRegistrar();
|
||||
final List<RenderBox> renderBoxes = <RenderBox>[];
|
||||
final RenderParagraph paragraph = RenderParagraph(
|
||||
const TextSpan(
|
||||
children: <InlineSpan>[
|
||||
TextSpan(text: 'how are you\nI am fine\nThank you'),
|
||||
]
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
registrar: registrar,
|
||||
children: renderBoxes,
|
||||
);
|
||||
layout(paragraph);
|
||||
|
||||
expect(registrar.selectables.length, 1);
|
||||
selectionParagraph(paragraph, const TextPosition(offset: 4), const TextPosition(offset: 5));
|
||||
expect(paragraph.selections.length, 1);
|
||||
TextSelection selection = paragraph.selections[0];
|
||||
expect(selection.start, 4); // how [a]re you
|
||||
expect(selection.end, 5);
|
||||
|
||||
// Equivalent to sending shift + meta + arrow-right.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const GranularlyExtendSelectionEvent(
|
||||
forward: true,
|
||||
isEnd: true,
|
||||
granularity: TextGranularity.line,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
expect(selection.start, 4); // how [are you]
|
||||
if (isBrowser && !isCanvasKit) {
|
||||
expect(selection.end, 12);
|
||||
} else {
|
||||
expect(selection.end, 11);
|
||||
}
|
||||
|
||||
// Equivalent to sending shift + meta + arrow-left.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const GranularlyExtendSelectionEvent(
|
||||
forward: false,
|
||||
isEnd: true,
|
||||
granularity: TextGranularity.line,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
expect(selection.start, 0); // [how ]are you
|
||||
expect(selection.end, 4);
|
||||
});
|
||||
|
||||
test('can granularly extend selection - document', () async {
|
||||
final TestSelectionRegistrar registrar = TestSelectionRegistrar();
|
||||
final List<RenderBox> renderBoxes = <RenderBox>[];
|
||||
final RenderParagraph paragraph = RenderParagraph(
|
||||
const TextSpan(
|
||||
children: <InlineSpan>[
|
||||
TextSpan(text: 'how are you\nI am fine\nThank you'),
|
||||
]
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
registrar: registrar,
|
||||
children: renderBoxes,
|
||||
);
|
||||
layout(paragraph);
|
||||
|
||||
expect(registrar.selectables.length, 1);
|
||||
selectionParagraph(paragraph, const TextPosition(offset: 14), const TextPosition(offset: 15));
|
||||
expect(paragraph.selections.length, 1);
|
||||
TextSelection selection = paragraph.selections[0];
|
||||
// how are you
|
||||
// I [a]m fine
|
||||
expect(selection.start, 14);
|
||||
expect(selection.end, 15);
|
||||
|
||||
// Equivalent to sending shift + meta + arrow-down.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const GranularlyExtendSelectionEvent(
|
||||
forward: true,
|
||||
isEnd: true,
|
||||
granularity: TextGranularity.document,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
// how are you
|
||||
// I [am fine
|
||||
// Thank you]
|
||||
expect(selection.start, 14);
|
||||
expect(selection.end, 31);
|
||||
|
||||
// Equivalent to sending shift + meta + arrow-up.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const GranularlyExtendSelectionEvent(
|
||||
forward: false,
|
||||
isEnd: true,
|
||||
granularity: TextGranularity.document,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
// [how are you
|
||||
// I ]am fine
|
||||
// Thank you
|
||||
expect(selection.start, 0);
|
||||
expect(selection.end, 14);
|
||||
});
|
||||
|
||||
test('can granularly extend selection when no active selection', () async {
|
||||
final TestSelectionRegistrar registrar = TestSelectionRegistrar();
|
||||
final List<RenderBox> renderBoxes = <RenderBox>[];
|
||||
final RenderParagraph paragraph = RenderParagraph(
|
||||
const TextSpan(
|
||||
children: <InlineSpan>[
|
||||
TextSpan(text: 'how are you\nI am fine\nThank you'),
|
||||
]
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
registrar: registrar,
|
||||
children: renderBoxes,
|
||||
);
|
||||
layout(paragraph);
|
||||
|
||||
expect(registrar.selectables.length, 1);
|
||||
expect(paragraph.selections.length, 0);
|
||||
|
||||
// Equivalent to sending shift + alt + right.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const GranularlyExtendSelectionEvent(
|
||||
forward: true,
|
||||
isEnd: true,
|
||||
granularity: TextGranularity.word,
|
||||
),
|
||||
);
|
||||
TextSelection selection = paragraph.selections[0];
|
||||
// [how] are you
|
||||
// I am fine
|
||||
// Thank you
|
||||
expect(selection.start, 0);
|
||||
expect(selection.end, 3);
|
||||
|
||||
// Remove selection
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const ClearSelectionEvent(),
|
||||
);
|
||||
expect(paragraph.selections.length, 0);
|
||||
|
||||
// Equivalent to sending shift + alt + left.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const GranularlyExtendSelectionEvent(
|
||||
forward: false,
|
||||
isEnd: true,
|
||||
granularity: TextGranularity.word,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
// how are you
|
||||
// I am fine
|
||||
// Thank [you]
|
||||
expect(selection.start, 28);
|
||||
expect(selection.end, 31);
|
||||
});
|
||||
|
||||
test('can directionally extend selection', () async {
|
||||
final TestSelectionRegistrar registrar = TestSelectionRegistrar();
|
||||
final List<RenderBox> renderBoxes = <RenderBox>[];
|
||||
final RenderParagraph paragraph = RenderParagraph(
|
||||
const TextSpan(
|
||||
children: <InlineSpan>[
|
||||
TextSpan(text: 'how are you\nI am fine\nThank you'),
|
||||
]
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
registrar: registrar,
|
||||
children: renderBoxes,
|
||||
);
|
||||
layout(paragraph);
|
||||
|
||||
expect(registrar.selectables.length, 1);
|
||||
selectionParagraph(paragraph, const TextPosition(offset: 14), const TextPosition(offset: 15));
|
||||
expect(paragraph.selections.length, 1);
|
||||
TextSelection selection = paragraph.selections[0];
|
||||
// how are you
|
||||
// I [a]m fine
|
||||
expect(selection.start, 14);
|
||||
expect(selection.end, 15);
|
||||
|
||||
final Matrix4 transform = registrar.selectables[0].getTransformTo(null);
|
||||
final double baseline = MatrixUtils.transformPoint(
|
||||
transform,
|
||||
registrar.selectables[0].value.endSelectionPoint!.localPosition,
|
||||
).dx;
|
||||
|
||||
// Equivalent to sending shift + arrow-down.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
DirectionallyExtendSelectionEvent(
|
||||
isEnd: true,
|
||||
dx: baseline,
|
||||
direction: SelectionExtendDirection.nextLine,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
// how are you
|
||||
// I [am fine
|
||||
// Tha]nk you
|
||||
expect(selection.start, 14);
|
||||
expect(selection.end, 25);
|
||||
|
||||
// Equivalent to sending shift + arrow-up.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
DirectionallyExtendSelectionEvent(
|
||||
isEnd: true,
|
||||
dx: baseline,
|
||||
direction: SelectionExtendDirection.previousLine,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
// how are you
|
||||
// I [a]m fine
|
||||
// Thank you
|
||||
expect(selection.start, 14);
|
||||
expect(selection.end, 15);
|
||||
});
|
||||
|
||||
test('can directionally extend selection when no selection', () async {
|
||||
final TestSelectionRegistrar registrar = TestSelectionRegistrar();
|
||||
final List<RenderBox> renderBoxes = <RenderBox>[];
|
||||
final RenderParagraph paragraph = RenderParagraph(
|
||||
const TextSpan(
|
||||
children: <InlineSpan>[
|
||||
TextSpan(text: 'how are you\nI am fine\nThank you'),
|
||||
]
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
registrar: registrar,
|
||||
children: renderBoxes,
|
||||
);
|
||||
layout(paragraph);
|
||||
|
||||
expect(registrar.selectables.length, 1);
|
||||
expect(paragraph.selections.length, 0);
|
||||
|
||||
final Matrix4 transform = registrar.selectables[0].getTransformTo(null);
|
||||
final double baseline = MatrixUtils.transformPoint(
|
||||
transform,
|
||||
Offset(registrar.selectables[0].size.width / 2, 0),
|
||||
).dx;
|
||||
|
||||
// Equivalent to sending shift + arrow-down.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
DirectionallyExtendSelectionEvent(
|
||||
isEnd: true,
|
||||
dx: baseline,
|
||||
direction: SelectionExtendDirection.forward,
|
||||
),
|
||||
);
|
||||
TextSelection selection = paragraph.selections[0];
|
||||
// [how ar]e you
|
||||
// I am fine
|
||||
// Thank you
|
||||
expect(selection.start, 0);
|
||||
expect(selection.end, 6);
|
||||
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
const ClearSelectionEvent(),
|
||||
);
|
||||
expect(paragraph.selections.length, 0);
|
||||
|
||||
// Equivalent to sending shift + arrow-up.
|
||||
registrar.selectables[0].dispatchSelectionEvent(
|
||||
DirectionallyExtendSelectionEvent(
|
||||
isEnd: true,
|
||||
dx: baseline,
|
||||
direction: SelectionExtendDirection.backward,
|
||||
),
|
||||
);
|
||||
selection = paragraph.selections[0];
|
||||
// how are you
|
||||
// I am fine
|
||||
// Thank [you]
|
||||
expect(selection.start, 28);
|
||||
expect(selection.end, 31);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -7,26 +7,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Future<void> sendKeyCombination(
|
||||
WidgetTester tester,
|
||||
SingleActivator activator,
|
||||
) async {
|
||||
final List<LogicalKeyboardKey> modifiers = <LogicalKeyboardKey>[
|
||||
if (activator.control) LogicalKeyboardKey.control,
|
||||
if (activator.shift) LogicalKeyboardKey.shift,
|
||||
if (activator.alt) LogicalKeyboardKey.alt,
|
||||
if (activator.meta) LogicalKeyboardKey.meta,
|
||||
];
|
||||
for (final LogicalKeyboardKey modifier in modifiers) {
|
||||
await tester.sendKeyDownEvent(modifier);
|
||||
}
|
||||
await tester.sendKeyDownEvent(activator.trigger);
|
||||
await tester.sendKeyUpEvent(activator.trigger);
|
||||
await tester.pump();
|
||||
for (final LogicalKeyboardKey modifier in modifiers.reversed) {
|
||||
await tester.sendKeyUpEvent(modifier);
|
||||
}
|
||||
}
|
||||
import 'keyboard_utils.dart';
|
||||
|
||||
void main() {
|
||||
Widget buildSpyAboveEditableText({
|
||||
|
|
|
@ -8,27 +8,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'clipboard_utils.dart';
|
||||
|
||||
Future<void> sendKeyCombination(
|
||||
WidgetTester tester,
|
||||
SingleActivator activator,
|
||||
) async {
|
||||
final List<LogicalKeyboardKey> modifiers = <LogicalKeyboardKey>[
|
||||
if (activator.control) LogicalKeyboardKey.control,
|
||||
if (activator.shift) LogicalKeyboardKey.shift,
|
||||
if (activator.alt) LogicalKeyboardKey.alt,
|
||||
if (activator.meta) LogicalKeyboardKey.meta,
|
||||
];
|
||||
for (final LogicalKeyboardKey modifier in modifiers) {
|
||||
await tester.sendKeyDownEvent(modifier);
|
||||
}
|
||||
await tester.sendKeyDownEvent(activator.trigger);
|
||||
await tester.sendKeyUpEvent(activator.trigger);
|
||||
await tester.pump();
|
||||
for (final LogicalKeyboardKey modifier in modifiers.reversed) {
|
||||
await tester.sendKeyUpEvent(modifier);
|
||||
}
|
||||
}
|
||||
import 'keyboard_utils.dart';
|
||||
|
||||
Iterable<SingleActivator> allModifierVariants(LogicalKeyboardKey trigger) {
|
||||
const Iterable<bool> trueFalse = <bool>[false, true];
|
||||
|
|
28
packages/flutter/test/widgets/keyboard_utils.dart
Normal file
28
packages/flutter/test/widgets/keyboard_utils.dart
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Future<void> sendKeyCombination(
|
||||
WidgetTester tester,
|
||||
SingleActivator activator,
|
||||
) async {
|
||||
final List<LogicalKeyboardKey> modifiers = <LogicalKeyboardKey>[
|
||||
if (activator.control) LogicalKeyboardKey.control,
|
||||
if (activator.shift) LogicalKeyboardKey.shift,
|
||||
if (activator.alt) LogicalKeyboardKey.alt,
|
||||
if (activator.meta) LogicalKeyboardKey.meta,
|
||||
];
|
||||
for (final LogicalKeyboardKey modifier in modifiers) {
|
||||
await tester.sendKeyDownEvent(modifier);
|
||||
}
|
||||
await tester.sendKeyDownEvent(activator.trigger);
|
||||
await tester.sendKeyUpEvent(activator.trigger);
|
||||
await tester.pump();
|
||||
for (final LogicalKeyboardKey modifier in modifiers.reversed) {
|
||||
await tester.sendKeyUpEvent(modifier);
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'clipboard_utils.dart';
|
||||
import 'keyboard_utils.dart';
|
||||
|
||||
Offset textOffsetToPosition(RenderParagraph paragraph, int offset) {
|
||||
const Rect caret = Rect.fromLTWH(0.0, 0.0, 2.0, 20.0);
|
||||
|
@ -400,10 +401,7 @@ void main() {
|
|||
));
|
||||
await tester.pumpAndSettle();
|
||||
node.requestFocus();
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true));
|
||||
await tester.pump();
|
||||
|
||||
for (int i = 0; i < 13; i += 1) {
|
||||
|
@ -429,10 +427,7 @@ void main() {
|
|||
));
|
||||
await tester.pumpAndSettle();
|
||||
node.requestFocus();
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, meta: true));
|
||||
await tester.pump();
|
||||
|
||||
for (int i = 0; i < 13; i += 1) {
|
||||
|
@ -498,6 +493,234 @@ void main() {
|
|||
await gesture.up();
|
||||
});
|
||||
|
||||
testWidgets('keyboard selection should auto scroll - vertical', (WidgetTester tester) async {
|
||||
final FocusNode node = FocusNode();
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: SelectionArea(
|
||||
focusNode: node,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
itemCount: 100,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Text('Item $index');
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
final RenderParagraph paragraph9 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 9'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph9, 2), kind: ui.PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph9, 4) + const Offset(0, 5));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
expect(paragraph9.selections.length, 1);
|
||||
expect(paragraph9.selections[0].start, 2);
|
||||
expect(paragraph9.selections[0].end, 4);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
|
||||
await tester.pump();
|
||||
final RenderParagraph paragraph10 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 10'), matching: find.byType(RichText)));
|
||||
expect(paragraph10.selections.length, 1);
|
||||
expect(paragraph10.selections[0].start, 0);
|
||||
expect(paragraph10.selections[0].end, 4);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
|
||||
await tester.pump();
|
||||
final RenderParagraph paragraph11 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 11'), matching: find.byType(RichText)));
|
||||
expect(paragraph11.selections.length, 1);
|
||||
expect(paragraph11.selections[0].start, 0);
|
||||
expect(paragraph11.selections[0].end, 4);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
// Should start scrolling.
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
|
||||
await tester.pump();
|
||||
final RenderParagraph paragraph12 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 12'), matching: find.byType(RichText)));
|
||||
expect(paragraph12.selections.length, 1);
|
||||
expect(paragraph12.selections[0].start, 0);
|
||||
expect(paragraph12.selections[0].end, 4);
|
||||
expect(controller.offset, 24.0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
|
||||
await tester.pump();
|
||||
final RenderParagraph paragraph13 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 13'), matching: find.byType(RichText)));
|
||||
expect(paragraph13.selections.length, 1);
|
||||
expect(paragraph13.selections[0].start, 0);
|
||||
expect(paragraph13.selections[0].end, 4);
|
||||
expect(controller.offset, 72.0);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
testWidgets('keyboard selection should auto scroll - vertical reversed', (WidgetTester tester) async {
|
||||
final FocusNode node = FocusNode();
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: SelectionArea(
|
||||
focusNode: node,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
reverse: true,
|
||||
itemCount: 100,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Text('Item $index');
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
final RenderParagraph paragraph9 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 9'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph9, 2), kind: ui.PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph9, 4) + const Offset(0, 5));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
expect(paragraph9.selections.length, 1);
|
||||
expect(paragraph9.selections[0].start, 2);
|
||||
expect(paragraph9.selections[0].end, 4);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
|
||||
await tester.pump();
|
||||
final RenderParagraph paragraph10 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 10'), matching: find.byType(RichText)));
|
||||
expect(paragraph10.selections.length, 1);
|
||||
expect(paragraph10.selections[0].start, 2);
|
||||
expect(paragraph10.selections[0].end, 7);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
|
||||
await tester.pump();
|
||||
final RenderParagraph paragraph11 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 11'), matching: find.byType(RichText)));
|
||||
expect(paragraph11.selections.length, 1);
|
||||
expect(paragraph11.selections[0].start, 2);
|
||||
expect(paragraph11.selections[0].end, 7);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
// Should start scrolling.
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
|
||||
await tester.pump();
|
||||
final RenderParagraph paragraph12 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 12'), matching: find.byType(RichText)));
|
||||
expect(paragraph12.selections.length, 1);
|
||||
expect(paragraph12.selections[0].start, 2);
|
||||
expect(paragraph12.selections[0].end, 7);
|
||||
expect(controller.offset, 24.0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
|
||||
await tester.pump();
|
||||
final RenderParagraph paragraph13 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 13'), matching: find.byType(RichText)));
|
||||
expect(paragraph13.selections.length, 1);
|
||||
expect(paragraph13.selections[0].start, 2);
|
||||
expect(paragraph13.selections[0].end, 7);
|
||||
expect(controller.offset, 72.0);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
testWidgets('keyboard selection should auto scroll - horizontal', (WidgetTester tester) async {
|
||||
final FocusNode node = FocusNode();
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: SelectionArea(
|
||||
focusNode: node,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 100,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Text('Item $index');
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 2'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph2, 0), kind: ui.PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph2, 1) + const Offset(0, 5));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 0);
|
||||
expect(paragraph2.selections[0].end, 1);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
|
||||
await tester.pump();
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 0);
|
||||
expect(paragraph2.selections[0].end, 6);
|
||||
expect(controller.offset, 64.0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
|
||||
await tester.pump();
|
||||
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 3'), matching: find.byType(RichText)));
|
||||
expect(paragraph3.selections.length, 1);
|
||||
expect(paragraph3.selections[0].start, 0);
|
||||
expect(paragraph3.selections[0].end, 6);
|
||||
expect(controller.offset, 352.0);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
testWidgets('keyboard selection should auto scroll - horizontal reversed', (WidgetTester tester) async {
|
||||
final FocusNode node = FocusNode();
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: SelectionArea(
|
||||
focusNode: node,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
reverse: true,
|
||||
itemCount: 100,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Text('Item $index');
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 1'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 5) + const Offset(0, 5), kind: ui.PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph1, 4) + const Offset(0, 5));
|
||||
await tester.pumpAndSettle();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 4);
|
||||
expect(paragraph1.selections[0].end, 5);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
|
||||
await tester.pump();
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 0);
|
||||
expect(paragraph1.selections[0].end, 5);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
|
||||
await tester.pump();
|
||||
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 2'), matching: find.byType(RichText)));
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 0);
|
||||
expect(paragraph2.selections[0].end, 6);
|
||||
expect(controller.offset, 64.0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
|
||||
await tester.pump();
|
||||
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Item 3'), matching: find.byType(RichText)));
|
||||
expect(paragraph3.selections.length, 1);
|
||||
expect(paragraph3.selections[0].start, 0);
|
||||
expect(paragraph3.selections[0].end, 6);
|
||||
expect(controller.offset, 352.0);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
group('Complex cases', () {
|
||||
testWidgets('selection starts outside of the scrollable', (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
|
@ -642,10 +865,7 @@ void main() {
|
|||
expect(find.descendant(of: find.text('Item 1'), matching: find.byType(RichText)), findsNothing);
|
||||
|
||||
// Start copying.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, meta: true));
|
||||
|
||||
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
||||
expect(clipboardData['text'], 'em 0It');
|
||||
|
@ -686,10 +906,7 @@ void main() {
|
|||
expect(find.descendant(of: find.text('Item 1'), matching: find.byType(RichText)), findsNothing);
|
||||
|
||||
// Start copying.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true));
|
||||
|
||||
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
||||
expect(clipboardData['text'], 'em 0It');
|
||||
|
|
|
@ -9,7 +9,8 @@ import 'package:flutter/rendering.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widgets/clipboard_utils.dart';
|
||||
import 'clipboard_utils.dart';
|
||||
import 'keyboard_utils.dart';
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
Offset textOffsetToPosition(RenderParagraph paragraph, int offset) {
|
||||
|
@ -488,10 +489,7 @@ void main() {
|
|||
await gesture.up();
|
||||
|
||||
// keyboard copy.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true));
|
||||
|
||||
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
||||
expect(clipboardData['text'], 'w are you?Good, and you?Fine, ');
|
||||
|
@ -524,10 +522,7 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
// Make sure keyboard select all works on TextField.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true));
|
||||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 21));
|
||||
|
||||
// Make sure no selection in SelectableRegion.
|
||||
|
@ -542,10 +537,7 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
// Make sure keyboard select all will be handled by selectable region now.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true));
|
||||
expect(controller.selection, const TextSelection.collapsed(offset: -1));
|
||||
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
||||
|
@ -581,10 +573,7 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
// Make sure keyboard select all works on TextField.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, meta: true));
|
||||
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 21));
|
||||
|
||||
// Make sure no selection in SelectableRegion.
|
||||
|
@ -599,10 +588,7 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
// Make sure keyboard select all will be handled by selectable region now.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, meta: true));
|
||||
expect(controller.selection, const TextSelection.collapsed(offset: -1));
|
||||
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
|
||||
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
|
||||
|
@ -631,10 +617,7 @@ void main() {
|
|||
focusNode.requestFocus();
|
||||
|
||||
// keyboard select all.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true));
|
||||
|
||||
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
|
||||
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
|
||||
|
@ -675,10 +658,7 @@ void main() {
|
|||
await gesture.up();
|
||||
|
||||
// keyboard copy.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true));
|
||||
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
||||
expect(clipboardData['text'], 'w are you?Good, and you?Fine');
|
||||
},
|
||||
|
@ -718,10 +698,7 @@ void main() {
|
|||
await gesture.up();
|
||||
|
||||
// keyboard copy.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, control: true));
|
||||
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
||||
expect(clipboardData['text'], 'w are you?Fine');
|
||||
},
|
||||
|
@ -761,10 +738,7 @@ void main() {
|
|||
await gesture.up();
|
||||
|
||||
// keyboard copy.
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft);
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft);
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyC, meta: true));
|
||||
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
|
||||
expect(clipboardData['text'], 'w are you?Fine');
|
||||
},
|
||||
|
@ -1100,6 +1074,438 @@ void main() {
|
|||
expect(clipboardData['text'], 'thank');
|
||||
}, skip: kIsWeb); // [intended] Web uses its native context menu.
|
||||
|
||||
testWidgets('can use keyboard to granularly extend selection - character', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: Column(
|
||||
children: const <Widget>[
|
||||
Text('How are you?'),
|
||||
Text('Good, and you?'),
|
||||
Text('Fine, thank you.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
|
||||
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// Ho[w ar]e you?
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 6);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true));
|
||||
await tester.pump();
|
||||
// Ho[w are] you?
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 7);
|
||||
|
||||
for (int i = 0; i < 5; i += 1) {
|
||||
await sendKeyCombination(tester,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true));
|
||||
await tester.pump();
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 8 + i);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i += 1) {
|
||||
await sendKeyCombination(tester,
|
||||
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true));
|
||||
await tester.pump();
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 11 - i);
|
||||
}
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
testWidgets('can use keyboard to granularly extend selection - word', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: Column(
|
||||
children: const <Widget>[
|
||||
Text('How are you?'),
|
||||
Text('Good, and you?'),
|
||||
Text('Fine, thank you.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
|
||||
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
final bool alt;
|
||||
final bool control;
|
||||
switch(defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
alt = false;
|
||||
control = true;
|
||||
break;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
alt = true;
|
||||
control = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Ho[w ar]e you?
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 6);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control));
|
||||
await tester.pump();
|
||||
// Ho[w are] you?
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 7);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control));
|
||||
await tester.pump();
|
||||
// Ho[w are you]?
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 11);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control));
|
||||
await tester.pump();
|
||||
// Ho[w are you?]
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 12);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, control: control));
|
||||
await tester.pump();
|
||||
// Ho[w are you?
|
||||
// Good], and you?
|
||||
// Fine, thank you.
|
||||
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 12);
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 0);
|
||||
expect(paragraph2.selections[0].end, 4);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control));
|
||||
await tester.pump();
|
||||
// Ho[w are you?
|
||||
// ]Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 12);
|
||||
expect(paragraph2.selections.length, 0);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control));
|
||||
await tester.pump();
|
||||
// Ho[w are you]?
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 11);
|
||||
expect(paragraph2.selections.length, 0);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
testWidgets('can use keyboard to granularly extend selection - line', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: Column(
|
||||
children: const <Widget>[
|
||||
Text('How are you?'),
|
||||
Text('Good, and you?'),
|
||||
Text('Fine, thank you.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
|
||||
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
final bool alt;
|
||||
final bool meta;
|
||||
switch(defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
meta = false;
|
||||
alt = true;
|
||||
break;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
meta = true;
|
||||
alt = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Ho[w ar]e you?
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 6);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, meta: meta));
|
||||
await tester.pump();
|
||||
// Ho[w are you?]
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 12);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: alt, meta: meta));
|
||||
await tester.pump();
|
||||
// Ho[w are you?
|
||||
// Good, and you?]
|
||||
// Fine, thank you.
|
||||
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 12);
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 0);
|
||||
expect(paragraph2.selections[0].end, 14);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, meta: meta));
|
||||
await tester.pump();
|
||||
// Ho[w are you?]
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 12);
|
||||
expect(paragraph2.selections.length, 0);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, meta: meta));
|
||||
await tester.pump();
|
||||
// [Ho]w are you?
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 0);
|
||||
expect(paragraph1.selections[0].end, 2);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
testWidgets('can use keyboard to granularly extend selection - document', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: Column(
|
||||
children: const <Widget>[
|
||||
Text('How are you?'),
|
||||
Text('Good, and you?'),
|
||||
Text('Fine, thank you.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// Select from offset 2 of paragraph1 to offset 6 of paragraph1.
|
||||
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
final bool alt;
|
||||
final bool meta;
|
||||
switch(defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
meta = false;
|
||||
alt = true;
|
||||
break;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
meta = true;
|
||||
alt = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Ho[w ar]e you?
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 6);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, meta: meta, alt: alt));
|
||||
await tester.pump();
|
||||
// Ho[w are you?
|
||||
// Good, and you?
|
||||
// Fine, thank you.]
|
||||
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
|
||||
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 2);
|
||||
expect(paragraph1.selections[0].end, 12);
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 0);
|
||||
expect(paragraph2.selections[0].end, 14);
|
||||
expect(paragraph3.selections.length, 1);
|
||||
expect(paragraph3.selections[0].start, 0);
|
||||
expect(paragraph3.selections[0].end, 16);
|
||||
|
||||
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, meta: meta, alt: alt));
|
||||
await tester.pump();
|
||||
// [Ho]w are you?
|
||||
// Good, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 0);
|
||||
expect(paragraph1.selections[0].end, 2);
|
||||
expect(paragraph2.selections.length, 0);
|
||||
expect(paragraph3.selections.length, 0);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
testWidgets('can use keyboard to directionally extend selection', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SelectableRegion(
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: materialTextSelectionControls,
|
||||
child: Column(
|
||||
children: const <Widget>[
|
||||
Text('How are you?'),
|
||||
Text('Good, and you?'),
|
||||
Text('Fine, thank you.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// Select from offset 2 of paragraph2 to offset 6 of paragraph2.
|
||||
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
|
||||
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph2, 2), kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await tester.pump();
|
||||
await gesture.moveTo(textOffsetToPosition(paragraph2, 6));
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// How are you?
|
||||
// Go[od, ]and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 2);
|
||||
expect(paragraph2.selections[0].end, 6);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
|
||||
await tester.pump();
|
||||
// How are you?
|
||||
// Go[od, and you?
|
||||
// Fine, t]hank you.
|
||||
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 2);
|
||||
expect(paragraph2.selections[0].end, 14);
|
||||
expect(paragraph3.selections.length, 1);
|
||||
expect(paragraph3.selections[0].start, 0);
|
||||
expect(paragraph3.selections[0].end, 7);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
|
||||
await tester.pump();
|
||||
// How are you?
|
||||
// Go[od, and you?
|
||||
// Fine, thank you.]
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 2);
|
||||
expect(paragraph2.selections[0].end, 14);
|
||||
expect(paragraph3.selections.length, 1);
|
||||
expect(paragraph3.selections[0].start, 0);
|
||||
expect(paragraph3.selections[0].end, 16);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
|
||||
await tester.pump();
|
||||
// How are you?
|
||||
// Go[od, ]and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 2);
|
||||
expect(paragraph2.selections[0].end, 6);
|
||||
expect(paragraph3.selections.length, 0);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
|
||||
await tester.pump();
|
||||
// How a[re you?
|
||||
// Go]od, and you?
|
||||
// Fine, thank you.
|
||||
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 5);
|
||||
expect(paragraph1.selections[0].end, 12);
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 0);
|
||||
expect(paragraph2.selections[0].end, 2);
|
||||
|
||||
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
|
||||
await tester.pump();
|
||||
// [How are you?
|
||||
// Go]od, and you?
|
||||
// Fine, thank you.
|
||||
expect(paragraph1.selections.length, 1);
|
||||
expect(paragraph1.selections[0].start, 0);
|
||||
expect(paragraph1.selections[0].end, 12);
|
||||
expect(paragraph2.selections.length, 1);
|
||||
expect(paragraph2.selections[0].start, 0);
|
||||
expect(paragraph2.selections[0].end, 2);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
group('magnifier', () {
|
||||
late ValueNotifier<MagnifierInfo> magnifierInfo;
|
||||
final Widget fakeMagnifier = Container(key: UniqueKey());
|
||||
|
|
Loading…
Reference in a new issue