|
|
|
|
@@ -62,10 +62,6 @@ enum TextSelectionHandleType {
|
|
|
|
|
collapsed,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The text position that a give selection handle manipulates. Dragging the
|
|
|
|
|
/// [start] handle always moves the [start]/[baseOffset] of the selection.
|
|
|
|
|
enum _TextSelectionHandlePosition { start, end }
|
|
|
|
|
|
|
|
|
|
/// Signature for when a pointer that's dragging to select text has moved again.
|
|
|
|
|
///
|
|
|
|
|
/// The first argument [startDetails] contains the details of the event that
|
|
|
|
|
@@ -262,7 +258,7 @@ class TextSelectionOverlay {
|
|
|
|
|
required this.renderObject,
|
|
|
|
|
this.selectionControls,
|
|
|
|
|
bool handlesVisible = false,
|
|
|
|
|
this.selectionDelegate,
|
|
|
|
|
required this.selectionDelegate,
|
|
|
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
|
|
|
this.onSelectionHandleTapped,
|
|
|
|
|
this.clipboardStatus,
|
|
|
|
|
@@ -312,7 +308,7 @@ class TextSelectionOverlay {
|
|
|
|
|
|
|
|
|
|
/// The delegate for manipulating the current selection in the owning
|
|
|
|
|
/// text field.
|
|
|
|
|
final TextSelectionDelegate? selectionDelegate;
|
|
|
|
|
final TextSelectionDelegate selectionDelegate;
|
|
|
|
|
|
|
|
|
|
/// Determines the way that drag start behavior is handled.
|
|
|
|
|
///
|
|
|
|
|
@@ -412,8 +408,8 @@ class TextSelectionOverlay {
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
_handles = <OverlayEntry>[
|
|
|
|
|
OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.start)),
|
|
|
|
|
OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.end)),
|
|
|
|
|
OverlayEntry(builder: (BuildContext context) => _buildStartHandle(context)),
|
|
|
|
|
OverlayEntry(builder: (BuildContext context) => _buildEndHandle(context)),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
|
|
|
|
|
@@ -507,29 +503,30 @@ class TextSelectionOverlay {
|
|
|
|
|
_toolbarController.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) {
|
|
|
|
|
Widget _buildStartHandle(BuildContext context) {
|
|
|
|
|
final Widget handle;
|
|
|
|
|
final TextSelectionControls? selectionControls = this.selectionControls;
|
|
|
|
|
if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) ||
|
|
|
|
|
selectionControls == null)
|
|
|
|
|
handle = Container(); // hide the second handle when collapsed
|
|
|
|
|
if (selectionControls == null)
|
|
|
|
|
handle = Container();
|
|
|
|
|
else {
|
|
|
|
|
handle = Visibility(
|
|
|
|
|
visible: handlesVisible,
|
|
|
|
|
child: _TextSelectionHandleOverlay(
|
|
|
|
|
onSelectionHandleChanged: (TextSelection newSelection) {
|
|
|
|
|
_handleSelectionHandleChanged(newSelection, position);
|
|
|
|
|
},
|
|
|
|
|
child: _SelectionHandleOverlay(
|
|
|
|
|
type: _chooseType(
|
|
|
|
|
renderObject.textDirection,
|
|
|
|
|
TextSelectionHandleType.left,
|
|
|
|
|
TextSelectionHandleType.right,
|
|
|
|
|
),
|
|
|
|
|
handleLayerLink: startHandleLayerLink,
|
|
|
|
|
onSelectionHandleTapped: onSelectionHandleTapped,
|
|
|
|
|
startHandleLayerLink: startHandleLayerLink,
|
|
|
|
|
endHandleLayerLink: endHandleLayerLink,
|
|
|
|
|
renderObject: renderObject,
|
|
|
|
|
selection: _selection,
|
|
|
|
|
onSelectionHandleDragStart: _handleSelectionStartHandleDragStart,
|
|
|
|
|
onSelectionHandleDragUpdate: _handleSelectionStartHandleDragUpdate,
|
|
|
|
|
selectionControls: selectionControls,
|
|
|
|
|
position: position,
|
|
|
|
|
visibility: renderObject.selectionStartInViewport,
|
|
|
|
|
preferredLineHeight: renderObject.preferredLineHeight,
|
|
|
|
|
glyphHeight: _getStartGlyphHeight(),
|
|
|
|
|
dragStartBehavior: dragStartBehavior,
|
|
|
|
|
selectionDelegate: selectionDelegate!,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return ExcludeSemantics(
|
|
|
|
|
@@ -537,6 +534,131 @@ class TextSelectionOverlay {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildEndHandle(BuildContext context) {
|
|
|
|
|
final Widget handle;
|
|
|
|
|
final TextSelectionControls? selectionControls = this.selectionControls;
|
|
|
|
|
if (_selection.isCollapsed || selectionControls == null)
|
|
|
|
|
handle = Container(); // hide the second handle when collapsed
|
|
|
|
|
else {
|
|
|
|
|
handle = Visibility(
|
|
|
|
|
visible: handlesVisible,
|
|
|
|
|
child: _SelectionHandleOverlay(
|
|
|
|
|
type: _chooseType(
|
|
|
|
|
renderObject.textDirection,
|
|
|
|
|
TextSelectionHandleType.right,
|
|
|
|
|
TextSelectionHandleType.left,
|
|
|
|
|
),
|
|
|
|
|
handleLayerLink: endHandleLayerLink,
|
|
|
|
|
onSelectionHandleTapped: onSelectionHandleTapped,
|
|
|
|
|
onSelectionHandleDragStart: _handleSelectionEndHandleDragStart,
|
|
|
|
|
onSelectionHandleDragUpdate: _handleSelectionEndHandleDragUpdate,
|
|
|
|
|
selectionControls: selectionControls,
|
|
|
|
|
visibility: renderObject.selectionEndInViewport,
|
|
|
|
|
preferredLineHeight: renderObject.preferredLineHeight,
|
|
|
|
|
glyphHeight: _getEndGlyphHeight(),
|
|
|
|
|
dragStartBehavior: dragStartBehavior,
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return ExcludeSemantics(
|
|
|
|
|
child: handle,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double? _getStartGlyphHeight() {
|
|
|
|
|
final InlineSpan span = renderObject.text!;
|
|
|
|
|
final String prevText = span.toPlainText();
|
|
|
|
|
final String currText = selectionDelegate.textEditingValue.text;
|
|
|
|
|
final int firstSelectedGraphemeExtent;
|
|
|
|
|
Rect? startHandleRect;
|
|
|
|
|
// Only calculate handle rects if the text in the previous frame
|
|
|
|
|
// is the same as the text in the current frame. This is done because
|
|
|
|
|
// widget.renderObject contains the renderEditable from the previous frame.
|
|
|
|
|
// If the text changed between the current and previous frames then
|
|
|
|
|
// widget.renderObject.getRectForComposingRange might fail. In cases where
|
|
|
|
|
// the current frame is different from the previous we fall back to
|
|
|
|
|
// renderObject.preferredLineHeight.
|
|
|
|
|
if (prevText == currText && _selection != null && _selection.isValid && !_selection.isCollapsed) {
|
|
|
|
|
final String selectedGraphemes = _selection.textInside(currText);
|
|
|
|
|
firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length;
|
|
|
|
|
startHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.start, end: _selection.start + firstSelectedGraphemeExtent));
|
|
|
|
|
}
|
|
|
|
|
return startHandleRect?.height;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double? _getEndGlyphHeight() {
|
|
|
|
|
final InlineSpan span = renderObject.text!;
|
|
|
|
|
final String prevText = span.toPlainText();
|
|
|
|
|
final String currText = selectionDelegate.textEditingValue.text;
|
|
|
|
|
final int lastSelectedGraphemeExtent;
|
|
|
|
|
Rect? endHandleRect;
|
|
|
|
|
// See the explanation in _getStartGlyphHeight.
|
|
|
|
|
if (prevText == currText && _selection != null && _selection.isValid && !_selection.isCollapsed) {
|
|
|
|
|
final String selectedGraphemes = _selection.textInside(currText);
|
|
|
|
|
lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length;
|
|
|
|
|
endHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.end - lastSelectedGraphemeExtent, end: _selection.end));
|
|
|
|
|
}
|
|
|
|
|
return endHandleRect?.height;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
late Offset _dragEndPosition;
|
|
|
|
|
|
|
|
|
|
void _handleSelectionEndHandleDragStart(DragStartDetails details) {
|
|
|
|
|
final Size handleSize = selectionControls!.getHandleSize(
|
|
|
|
|
renderObject.preferredLineHeight,
|
|
|
|
|
);
|
|
|
|
|
_dragEndPosition = details.globalPosition + Offset(0.0, -handleSize.height);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) {
|
|
|
|
|
_dragEndPosition += details.delta;
|
|
|
|
|
final TextPosition position = renderObject.getPositionForPoint(_dragEndPosition);
|
|
|
|
|
|
|
|
|
|
if (_selection.isCollapsed) {
|
|
|
|
|
_handleSelectionHandleChanged(TextSelection.fromPosition(position), isEnd: true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final TextSelection newSelection = TextSelection(
|
|
|
|
|
baseOffset: _selection.baseOffset,
|
|
|
|
|
extentOffset: position.offset,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (newSelection.baseOffset >= newSelection.extentOffset)
|
|
|
|
|
return; // don't allow order swapping.
|
|
|
|
|
|
|
|
|
|
_handleSelectionHandleChanged(newSelection, isEnd: true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
late Offset _dragStartPosition;
|
|
|
|
|
|
|
|
|
|
void _handleSelectionStartHandleDragStart(DragStartDetails details) {
|
|
|
|
|
final Size handleSize = selectionControls!.getHandleSize(
|
|
|
|
|
renderObject.preferredLineHeight,
|
|
|
|
|
);
|
|
|
|
|
_dragStartPosition = details.globalPosition + Offset(0.0, -handleSize.height);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) {
|
|
|
|
|
_dragStartPosition += details.delta;
|
|
|
|
|
final TextPosition position = renderObject.getPositionForPoint(_dragStartPosition);
|
|
|
|
|
|
|
|
|
|
if (_selection.isCollapsed) {
|
|
|
|
|
_handleSelectionHandleChanged(TextSelection.fromPosition(position), isEnd: false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final TextSelection newSelection = TextSelection(
|
|
|
|
|
baseOffset: position.offset,
|
|
|
|
|
extentOffset: _selection.extentOffset,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (newSelection.baseOffset >= newSelection.extentOffset)
|
|
|
|
|
return; // don't allow order swapping.
|
|
|
|
|
|
|
|
|
|
_handleSelectionHandleChanged(newSelection, isEnd: false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildToolbar(BuildContext context) {
|
|
|
|
|
if (selectionControls == null)
|
|
|
|
|
return Container();
|
|
|
|
|
@@ -581,7 +703,7 @@ class TextSelectionOverlay {
|
|
|
|
|
renderObject.preferredLineHeight,
|
|
|
|
|
midpoint,
|
|
|
|
|
endpoints,
|
|
|
|
|
selectionDelegate!,
|
|
|
|
|
selectionDelegate,
|
|
|
|
|
clipboardStatus!,
|
|
|
|
|
renderObject.lastSecondaryTapDownPosition,
|
|
|
|
|
);
|
|
|
|
|
@@ -592,67 +714,67 @@ class TextSelectionOverlay {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleSelectionHandleChanged(TextSelection newSelection, _TextSelectionHandlePosition position) {
|
|
|
|
|
final TextPosition textPosition;
|
|
|
|
|
switch (position) {
|
|
|
|
|
case _TextSelectionHandlePosition.start:
|
|
|
|
|
textPosition = newSelection.base;
|
|
|
|
|
break;
|
|
|
|
|
case _TextSelectionHandlePosition.end:
|
|
|
|
|
textPosition = newSelection.extent;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
selectionDelegate!.userUpdateTextEditingValue(
|
|
|
|
|
void _handleSelectionHandleChanged(TextSelection newSelection, {required bool isEnd}) {
|
|
|
|
|
final TextPosition textPosition = isEnd ? newSelection.extent : newSelection.base;
|
|
|
|
|
selectionDelegate.userUpdateTextEditingValue(
|
|
|
|
|
_value.copyWith(selection: newSelection),
|
|
|
|
|
SelectionChangedCause.drag,
|
|
|
|
|
);
|
|
|
|
|
selectionDelegate!.bringIntoView(textPosition);
|
|
|
|
|
selectionDelegate.bringIntoView(textPosition);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// This widget represents a single draggable text selection handle.
|
|
|
|
|
class _TextSelectionHandleOverlay extends StatefulWidget {
|
|
|
|
|
const _TextSelectionHandleOverlay({
|
|
|
|
|
Key? key,
|
|
|
|
|
required this.selection,
|
|
|
|
|
required this.position,
|
|
|
|
|
required this.startHandleLayerLink,
|
|
|
|
|
required this.endHandleLayerLink,
|
|
|
|
|
required this.renderObject,
|
|
|
|
|
required this.onSelectionHandleChanged,
|
|
|
|
|
required this.onSelectionHandleTapped,
|
|
|
|
|
required this.selectionControls,
|
|
|
|
|
required this.selectionDelegate,
|
|
|
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
|
|
|
}) : super(key: key);
|
|
|
|
|
TextSelectionHandleType _chooseType(
|
|
|
|
|
TextDirection textDirection,
|
|
|
|
|
TextSelectionHandleType ltrType,
|
|
|
|
|
TextSelectionHandleType rtlType,
|
|
|
|
|
) {
|
|
|
|
|
if (_selection.isCollapsed)
|
|
|
|
|
return TextSelectionHandleType.collapsed;
|
|
|
|
|
|
|
|
|
|
final TextSelection selection;
|
|
|
|
|
final _TextSelectionHandlePosition position;
|
|
|
|
|
final LayerLink startHandleLayerLink;
|
|
|
|
|
final LayerLink endHandleLayerLink;
|
|
|
|
|
final RenderEditable renderObject;
|
|
|
|
|
final ValueChanged<TextSelection> onSelectionHandleChanged;
|
|
|
|
|
final VoidCallback? onSelectionHandleTapped;
|
|
|
|
|
final TextSelectionControls selectionControls;
|
|
|
|
|
final DragStartBehavior dragStartBehavior;
|
|
|
|
|
final TextSelectionDelegate selectionDelegate;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
_TextSelectionHandleOverlayState createState() => _TextSelectionHandleOverlayState();
|
|
|
|
|
|
|
|
|
|
ValueListenable<bool> get _visibility {
|
|
|
|
|
switch (position) {
|
|
|
|
|
case _TextSelectionHandlePosition.start:
|
|
|
|
|
return renderObject.selectionStartInViewport;
|
|
|
|
|
case _TextSelectionHandlePosition.end:
|
|
|
|
|
return renderObject.selectionEndInViewport;
|
|
|
|
|
assert(textDirection != null);
|
|
|
|
|
switch (textDirection) {
|
|
|
|
|
case TextDirection.ltr:
|
|
|
|
|
return ltrType;
|
|
|
|
|
case TextDirection.rtl:
|
|
|
|
|
return rtlType;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _TextSelectionHandleOverlayState
|
|
|
|
|
extends State<_TextSelectionHandleOverlay> with SingleTickerProviderStateMixin {
|
|
|
|
|
late Offset _dragPosition;
|
|
|
|
|
/// This widget represents a single draggable selection handle.
|
|
|
|
|
class _SelectionHandleOverlay extends StatefulWidget {
|
|
|
|
|
/// Create selection overlay.
|
|
|
|
|
const _SelectionHandleOverlay({
|
|
|
|
|
Key? key,
|
|
|
|
|
required this.type,
|
|
|
|
|
required this.handleLayerLink,
|
|
|
|
|
this.onSelectionHandleTapped,
|
|
|
|
|
this.onSelectionHandleDragStart,
|
|
|
|
|
this.onSelectionHandleDragUpdate,
|
|
|
|
|
required this.selectionControls,
|
|
|
|
|
required this.visibility,
|
|
|
|
|
required this.preferredLineHeight,
|
|
|
|
|
this.glyphHeight,
|
|
|
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
|
|
final LayerLink handleLayerLink;
|
|
|
|
|
final VoidCallback? onSelectionHandleTapped;
|
|
|
|
|
final ValueChanged<DragStartDetails>? onSelectionHandleDragStart;
|
|
|
|
|
final ValueChanged<DragUpdateDetails>? onSelectionHandleDragUpdate;
|
|
|
|
|
final TextSelectionControls selectionControls;
|
|
|
|
|
final ValueListenable<bool> visibility;
|
|
|
|
|
final double preferredLineHeight;
|
|
|
|
|
final double? glyphHeight;
|
|
|
|
|
final TextSelectionHandleType type;
|
|
|
|
|
final DragStartBehavior dragStartBehavior;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<_SelectionHandleOverlay> createState() => _SelectionHandleOverlayState();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with SingleTickerProviderStateMixin {
|
|
|
|
|
|
|
|
|
|
late AnimationController _controller;
|
|
|
|
|
Animation<double> get _opacity => _controller.view;
|
|
|
|
|
@@ -664,11 +786,11 @@ class _TextSelectionHandleOverlayState
|
|
|
|
|
_controller = AnimationController(duration: TextSelectionOverlay.fadeDuration, vsync: this);
|
|
|
|
|
|
|
|
|
|
_handleVisibilityChanged();
|
|
|
|
|
widget._visibility.addListener(_handleVisibilityChanged);
|
|
|
|
|
widget.visibility.addListener(_handleVisibilityChanged);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleVisibilityChanged() {
|
|
|
|
|
if (widget._visibility.value) {
|
|
|
|
|
if (widget.visibility.value) {
|
|
|
|
|
_controller.forward();
|
|
|
|
|
} else {
|
|
|
|
|
_controller.reverse();
|
|
|
|
|
@@ -676,126 +798,30 @@ class _TextSelectionHandleOverlayState
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void didUpdateWidget(_TextSelectionHandleOverlay oldWidget) {
|
|
|
|
|
void didUpdateWidget(_SelectionHandleOverlay oldWidget) {
|
|
|
|
|
super.didUpdateWidget(oldWidget);
|
|
|
|
|
oldWidget._visibility.removeListener(_handleVisibilityChanged);
|
|
|
|
|
oldWidget.visibility.removeListener(_handleVisibilityChanged);
|
|
|
|
|
_handleVisibilityChanged();
|
|
|
|
|
widget._visibility.addListener(_handleVisibilityChanged);
|
|
|
|
|
widget.visibility.addListener(_handleVisibilityChanged);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
widget._visibility.removeListener(_handleVisibilityChanged);
|
|
|
|
|
widget.visibility.removeListener(_handleVisibilityChanged);
|
|
|
|
|
_controller.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleDragStart(DragStartDetails details) {
|
|
|
|
|
final Size handleSize = widget.selectionControls.getHandleSize(
|
|
|
|
|
widget.renderObject.preferredLineHeight,
|
|
|
|
|
);
|
|
|
|
|
_dragPosition = details.globalPosition + Offset(0.0, -handleSize.height);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleDragUpdate(DragUpdateDetails details) {
|
|
|
|
|
_dragPosition += details.delta;
|
|
|
|
|
final TextPosition position = widget.renderObject.getPositionForPoint(_dragPosition);
|
|
|
|
|
|
|
|
|
|
if (widget.selection.isCollapsed) {
|
|
|
|
|
widget.onSelectionHandleChanged(TextSelection.fromPosition(position));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final TextSelection newSelection;
|
|
|
|
|
switch (widget.position) {
|
|
|
|
|
case _TextSelectionHandlePosition.start:
|
|
|
|
|
newSelection = TextSelection(
|
|
|
|
|
baseOffset: position.offset,
|
|
|
|
|
extentOffset: widget.selection.extentOffset,
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
case _TextSelectionHandlePosition.end:
|
|
|
|
|
newSelection = TextSelection(
|
|
|
|
|
baseOffset: widget.selection.baseOffset,
|
|
|
|
|
extentOffset: position.offset,
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newSelection.baseOffset >= newSelection.extentOffset)
|
|
|
|
|
return; // don't allow order swapping.
|
|
|
|
|
|
|
|
|
|
widget.onSelectionHandleChanged(newSelection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final LayerLink layerLink;
|
|
|
|
|
final TextSelectionHandleType type;
|
|
|
|
|
|
|
|
|
|
switch (widget.position) {
|
|
|
|
|
case _TextSelectionHandlePosition.start:
|
|
|
|
|
layerLink = widget.startHandleLayerLink;
|
|
|
|
|
type = _chooseType(
|
|
|
|
|
widget.renderObject.textDirection,
|
|
|
|
|
TextSelectionHandleType.left,
|
|
|
|
|
TextSelectionHandleType.right,
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
case _TextSelectionHandlePosition.end:
|
|
|
|
|
// For collapsed selections, we shouldn't be building the [end] handle.
|
|
|
|
|
assert(!widget.selection.isCollapsed);
|
|
|
|
|
layerLink = widget.endHandleLayerLink;
|
|
|
|
|
type = _chooseType(
|
|
|
|
|
widget.renderObject.textDirection,
|
|
|
|
|
TextSelectionHandleType.right,
|
|
|
|
|
TextSelectionHandleType.left,
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// On some platforms we may want to calculate the start and end handles
|
|
|
|
|
// separately so they scale for the selected content.
|
|
|
|
|
//
|
|
|
|
|
// For the start handle we compute the rectangles that encompass the range
|
|
|
|
|
// of the first full selected grapheme cluster at the beginning of the selection.
|
|
|
|
|
//
|
|
|
|
|
// For the end handle we compute the rectangles that encompass the range
|
|
|
|
|
// of the last full selected grapheme cluster at the end of the selection.
|
|
|
|
|
//
|
|
|
|
|
// Only calculate start/end handle rects if the text in the previous frame
|
|
|
|
|
// is the same as the text in the current frame. This is done because
|
|
|
|
|
// widget.renderObject contains the renderEditable from the previous frame.
|
|
|
|
|
// If the text changed between the current and previous frames then
|
|
|
|
|
// widget.renderObject.getRectForComposingRange might fail. In cases where
|
|
|
|
|
// the current frame is different from the previous we fall back to
|
|
|
|
|
// widget.renderObject.preferredLineHeight.
|
|
|
|
|
final InlineSpan span = widget.renderObject.text!;
|
|
|
|
|
final String prevText = span.toPlainText();
|
|
|
|
|
final String currText = widget.selectionDelegate.textEditingValue.text;
|
|
|
|
|
final int firstSelectedGraphemeExtent;
|
|
|
|
|
final int lastSelectedGraphemeExtent;
|
|
|
|
|
final TextSelection selection = widget.selection;
|
|
|
|
|
Rect? startHandleRect;
|
|
|
|
|
Rect? endHandleRect;
|
|
|
|
|
|
|
|
|
|
if (prevText == currText && selection != null && selection.isValid && !selection.isCollapsed) {
|
|
|
|
|
final String selectedGraphemes = selection.textInside(currText);
|
|
|
|
|
firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length;
|
|
|
|
|
lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length;
|
|
|
|
|
assert(firstSelectedGraphemeExtent <= selectedGraphemes.length && lastSelectedGraphemeExtent <= selectedGraphemes.length);
|
|
|
|
|
startHandleRect = widget.renderObject.getRectForComposingRange(TextRange(start: selection.start, end: selection.start + firstSelectedGraphemeExtent));
|
|
|
|
|
endHandleRect = widget.renderObject.getRectForComposingRange(TextRange(start: selection.end - lastSelectedGraphemeExtent, end: selection.end));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final Offset handleAnchor = widget.selectionControls.getHandleAnchor(
|
|
|
|
|
type,
|
|
|
|
|
widget.renderObject.preferredLineHeight,
|
|
|
|
|
startHandleRect?.height ?? widget.renderObject.preferredLineHeight,
|
|
|
|
|
endHandleRect?.height ?? widget.renderObject.preferredLineHeight,
|
|
|
|
|
widget.type,
|
|
|
|
|
widget.preferredLineHeight,
|
|
|
|
|
widget.glyphHeight,
|
|
|
|
|
widget.glyphHeight,
|
|
|
|
|
);
|
|
|
|
|
final Size handleSize = widget.selectionControls.getHandleSize(
|
|
|
|
|
widget.renderObject.preferredLineHeight,
|
|
|
|
|
widget.preferredLineHeight,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final Rect handleRect = Rect.fromLTWH(
|
|
|
|
|
@@ -817,7 +843,7 @@ class _TextSelectionHandleOverlayState
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return CompositedTransformFollower(
|
|
|
|
|
link: layerLink,
|
|
|
|
|
link: widget.handleLayerLink,
|
|
|
|
|
offset: interactiveRect.topLeft,
|
|
|
|
|
showWhenUnlinked: false,
|
|
|
|
|
child: FadeTransition(
|
|
|
|
|
@@ -829,8 +855,8 @@ class _TextSelectionHandleOverlayState
|
|
|
|
|
child: GestureDetector(
|
|
|
|
|
behavior: HitTestBehavior.translucent,
|
|
|
|
|
dragStartBehavior: widget.dragStartBehavior,
|
|
|
|
|
onPanStart: _handleDragStart,
|
|
|
|
|
onPanUpdate: _handleDragUpdate,
|
|
|
|
|
onPanStart: widget.onSelectionHandleDragStart,
|
|
|
|
|
onPanUpdate: widget.onSelectionHandleDragUpdate,
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: EdgeInsets.only(
|
|
|
|
|
left: padding.left,
|
|
|
|
|
@@ -840,11 +866,11 @@ class _TextSelectionHandleOverlayState
|
|
|
|
|
),
|
|
|
|
|
child: widget.selectionControls.buildHandle(
|
|
|
|
|
context,
|
|
|
|
|
type,
|
|
|
|
|
widget.renderObject.preferredLineHeight,
|
|
|
|
|
widget.type,
|
|
|
|
|
widget.preferredLineHeight,
|
|
|
|
|
widget.onSelectionHandleTapped,
|
|
|
|
|
startHandleRect?.height ?? widget.renderObject.preferredLineHeight,
|
|
|
|
|
endHandleRect?.height ?? widget.renderObject.preferredLineHeight,
|
|
|
|
|
widget.glyphHeight,
|
|
|
|
|
widget.glyphHeight,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
@@ -852,23 +878,6 @@ class _TextSelectionHandleOverlayState
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TextSelectionHandleType _chooseType(
|
|
|
|
|
TextDirection textDirection,
|
|
|
|
|
TextSelectionHandleType ltrType,
|
|
|
|
|
TextSelectionHandleType rtlType,
|
|
|
|
|
) {
|
|
|
|
|
if (widget.selection.isCollapsed)
|
|
|
|
|
return TextSelectionHandleType.collapsed;
|
|
|
|
|
|
|
|
|
|
assert(textDirection != null);
|
|
|
|
|
switch (textDirection) {
|
|
|
|
|
case TextDirection.ltr:
|
|
|
|
|
return ltrType;
|
|
|
|
|
case TextDirection.rtl:
|
|
|
|
|
return rtlType;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Delegate interface for the [TextSelectionGestureDetectorBuilder].
|
|
|
|
|
|