Unverified Commit ac3189c3 authored by chunhtai's avatar chunhtai Committed by GitHub

Remove RenderEditable dependency from TextSelectionHandleOverlay (#97967)

parent 3afbec88
......@@ -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,61 @@ 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)
if (selectionControls == null)
handle = Container();
else {
handle = Visibility(
visible: handlesVisible,
child: _SelectionHandleOverlay(
type: _chooseType(
renderObject.textDirection,
TextSelectionHandleType.left,
TextSelectionHandleType.right,
),
handleLayerLink: startHandleLayerLink,
onSelectionHandleTapped: onSelectionHandleTapped,
onSelectionHandleDragStart: _handleSelectionStartHandleDragStart,
onSelectionHandleDragUpdate: _handleSelectionStartHandleDragUpdate,
selectionControls: selectionControls,
visibility: renderObject.selectionStartInViewport,
preferredLineHeight: renderObject.preferredLineHeight,
glyphHeight: _getStartGlyphHeight(),
dragStartBehavior: dragStartBehavior,
)
);
}
return ExcludeSemantics(
child: handle,
);
}
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: _TextSelectionHandleOverlay(
onSelectionHandleChanged: (TextSelection newSelection) {
_handleSelectionHandleChanged(newSelection, position);
},
child: _SelectionHandleOverlay(
type: _chooseType(
renderObject.textDirection,
TextSelectionHandleType.right,
TextSelectionHandleType.left,
),
handleLayerLink: endHandleLayerLink,
onSelectionHandleTapped: onSelectionHandleTapped,
startHandleLayerLink: startHandleLayerLink,
endHandleLayerLink: endHandleLayerLink,
renderObject: renderObject,
selection: _selection,
onSelectionHandleDragStart: _handleSelectionEndHandleDragStart,
onSelectionHandleDragUpdate: _handleSelectionEndHandleDragUpdate,
selectionControls: selectionControls,
position: position,
visibility: renderObject.selectionEndInViewport,
preferredLineHeight: renderObject.preferredLineHeight,
glyphHeight: _getEndGlyphHeight(),
dragStartBehavior: dragStartBehavior,
selectionDelegate: selectionDelegate!,
),
)
);
}
return ExcludeSemantics(
......@@ -537,6 +565,100 @@ class TextSelectionOverlay {
);
}
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);
}
TextSelectionHandleType _chooseType(
TextDirection textDirection,
TextSelectionHandleType ltrType,
TextSelectionHandleType rtlType,
) {
if (_selection.isCollapsed)
return TextSelectionHandleType.collapsed;
assert(textDirection != null);
switch (textDirection) {
case TextDirection.ltr:
return ltrType;
case TextDirection.rtl:
return rtlType;
}
}
}
/// This widget represents a single draggable text selection handle.
class _TextSelectionHandleOverlay extends StatefulWidget {
const _TextSelectionHandleOverlay({
/// This widget represents a single draggable selection handle.
class _SelectionHandleOverlay extends StatefulWidget {
/// Create selection overlay.
const _SelectionHandleOverlay({
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.type,
required this.handleLayerLink,
this.onSelectionHandleTapped,
this.onSelectionHandleDragStart,
this.onSelectionHandleDragUpdate,
required this.selectionControls,
required this.selectionDelegate,
required this.visibility,
required this.preferredLineHeight,
this.glyphHeight,
this.dragStartBehavior = DragStartBehavior.start,
}) : super(key: key);
final TextSelection selection;
final _TextSelectionHandlePosition position;
final LayerLink startHandleLayerLink;
final LayerLink endHandleLayerLink;
final RenderEditable renderObject;
final ValueChanged<TextSelection> onSelectionHandleChanged;
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;
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;
}
}
State<_SelectionHandleOverlay> createState() => _SelectionHandleOverlayState();
}
class _TextSelectionHandleOverlayState
extends State<_TextSelectionHandleOverlay> with SingleTickerProviderStateMixin {
late Offset _dragPosition;
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].
......
......@@ -9138,7 +9138,7 @@ void main() {
await tester.pumpAndSettle();
final List<FadeTransition> transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionHandleOverlay'),
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2);
......
......@@ -4697,7 +4697,7 @@ void main() {
// direction.
final List<FadeTransition> transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionHandleOverlay'),
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2);
......
......@@ -4128,7 +4128,7 @@ void main() {
await tester.pumpAndSettle();
final List<FadeTransition> transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionHandleOverlay'),
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment