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

Unify Text selection API (#98073)

* Unify Text selection API

* fix test

* further simplify

* fix more tests

* styling change
parent ec588352
......@@ -67,13 +67,13 @@ class _CupertinoDesktopTextSelectionControls extends TextSelectionControls {
/// Builds the text selection handles, but desktop has none.
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) {
return const SizedBox.shrink();
}
/// Gets the position for the text selection handles, but desktop has none.
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight, [double? startGlyphHeight, double? endGlyphHeight]) {
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
return Offset.zero;
}
}
......
......@@ -247,14 +247,8 @@ class CupertinoTextSelectionControls extends TextSelectionControls {
/// Builder for iOS text selection edges.
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) {
// iOS selection handles do not respond to taps.
// We want a size that's a vertical line the height of the text plus a 18.0
// padding in every direction that will constitute the selection drag area.
startGlyphHeight = startGlyphHeight ?? textLineHeight;
endGlyphHeight = endGlyphHeight ?? textLineHeight;
final Size desiredSize;
final Widget handle;
......@@ -267,14 +261,14 @@ class CupertinoTextSelectionControls extends TextSelectionControls {
// on top of the text selection endpoints.
switch (type) {
case TextSelectionHandleType.left:
desiredSize = getHandleSize(startGlyphHeight);
desiredSize = getHandleSize(textLineHeight);
handle = SizedBox.fromSize(
size: desiredSize,
child: customPaint,
);
return handle;
case TextSelectionHandleType.right:
desiredSize = getHandleSize(endGlyphHeight);
desiredSize = getHandleSize(textLineHeight);
handle = SizedBox.fromSize(
size: desiredSize,
child: customPaint,
......@@ -296,17 +290,14 @@ class CupertinoTextSelectionControls extends TextSelectionControls {
///
/// See [TextSelectionControls.getHandleAnchor].
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight, [double? startGlyphHeight, double? endGlyphHeight]) {
startGlyphHeight = startGlyphHeight ?? textLineHeight;
endGlyphHeight = endGlyphHeight ?? textLineHeight;
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
final Size handleSize;
switch (type) {
// The circle is at the top for the left handle, and the anchor point is
// all the way at the bottom of the line.
case TextSelectionHandleType.left:
handleSize = getHandleSize(startGlyphHeight);
handleSize = getHandleSize(textLineHeight);
return Offset(
handleSize.width / 2,
handleSize.height,
......@@ -314,7 +305,7 @@ class CupertinoTextSelectionControls extends TextSelectionControls {
// The right handle is vertically flipped, and the anchor point is near
// the top of the circle to give slight overlap.
case TextSelectionHandleType.right:
handleSize = getHandleSize(endGlyphHeight);
handleSize = getHandleSize(textLineHeight);
return Offset(
handleSize.width / 2,
handleSize.height - 2 * _kSelectionHandleRadius + _kSelectionHandleOverlap,
......
......@@ -53,13 +53,13 @@ class _DesktopTextSelectionControls extends TextSelectionControls {
/// Builds the text selection handles, but desktop has none.
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) {
return const SizedBox.shrink();
}
/// Gets the position for the text selection handles, but desktop has none.
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight, [double? startGlyphHeight, double? endGlyphHeight]) {
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
return Offset.zero;
}
......
......@@ -54,7 +54,7 @@ class MaterialTextSelectionControls extends TextSelectionControls {
/// Builder for material-style text selection handles.
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight, [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight, [VoidCallback? onTap]) {
final ThemeData theme = Theme.of(context);
final Color handleColor = TextSelectionTheme.of(context).selectionHandleColor ?? theme.colorScheme.primary;
final Widget handle = SizedBox(
......@@ -94,7 +94,7 @@ class MaterialTextSelectionControls extends TextSelectionControls {
///
/// See [TextSelectionControls.getHandleAnchor].
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight, [double? startGlyphHeight, double? endGlyphHeight]) {
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
switch (type) {
case TextSelectionHandleType.left:
return const Offset(_kHandleSize, 0);
......
......@@ -22,7 +22,6 @@ import 'gesture_detector.dart';
import 'overlay.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
import 'visibility.dart';
export 'package:flutter/services.dart' show TextSelectionDelegate;
......@@ -116,12 +115,12 @@ abstract class TextSelectionControls {
/// interaction is allowed. As a counterexample, the default selection handle
/// on iOS [cupertinoTextSelectionControls] does not call [onTap] at all,
/// since its handles are not meant to be tapped.
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]);
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]);
/// Get the anchor point of the handle relative to itself. The anchor point is
/// the point that is aligned with a specific point in the text. A handle
/// often visually "points to" that location.
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight, [double? startGlyphHeight, double? endGlyphHeight]);
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight);
/// Builds a toolbar near a text selection.
///
......@@ -274,6 +273,9 @@ class TextSelectionOverlay {
'Usually the Navigator created by WidgetsApp provides the overlay. Perhaps your '
'app content was created above the Navigator with the WidgetsApp builder parameter.',
);
renderObject.selectionStartInViewport.addListener(_updateHandleVisibilities);
renderObject.selectionEndInViewport.addListener(_updateHandleVisibilities);
_updateHandleVisibilities();
_toolbarController = AnimationController(duration: fadeDuration, vsync: overlay!);
}
......@@ -372,6 +374,13 @@ class TextSelectionOverlay {
TextSelection get _selection => _value.selection;
final ValueNotifier<bool> _effectiveStartHandleVisibility = ValueNotifier<bool>(false);
final ValueNotifier<bool> _effectiveEndHandleVisibility = ValueNotifier<bool>(false);
void _updateHandleVisibilities() {
_effectiveStartHandleVisibility.value = _handlesVisible && renderObject.selectionStartInViewport.value;
_effectiveEndHandleVisibility.value = _handlesVisible && renderObject.selectionEndInViewport.value;
}
/// Whether selection handles are visible.
///
/// Set to false if you want to hide the handles. Use this property to show or
......@@ -393,13 +402,7 @@ class TextSelectionOverlay {
if (_handlesVisible == visible)
return;
_handlesVisible = visible;
// If we are in build state, it will be too late to update visibility.
// We will need to schedule the build in next frame.
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild);
} else {
_markNeedsBuild();
}
_updateHandleVisibilities();
}
/// Builds the handles by inserting them into the [context]'s overlay.
......@@ -501,6 +504,8 @@ class TextSelectionOverlay {
void dispose() {
hide();
_toolbarController.dispose();
renderObject.selectionStartInViewport.removeListener(_updateHandleVisibilities);
renderObject.selectionEndInViewport.removeListener(_updateHandleVisibilities);
}
Widget _buildStartHandle(BuildContext context) {
......@@ -509,24 +514,20 @@ class TextSelectionOverlay {
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,
)
handle = _SelectionHandleOverlay(
type: _chooseType(
renderObject.textDirection,
TextSelectionHandleType.left,
TextSelectionHandleType.right,
),
handleLayerLink: startHandleLayerLink,
onSelectionHandleTapped: onSelectionHandleTapped,
onSelectionHandleDragStart: _handleSelectionStartHandleDragStart,
onSelectionHandleDragUpdate: _handleSelectionStartHandleDragUpdate,
selectionControls: selectionControls,
visibility: _effectiveStartHandleVisibility,
preferredLineHeight: _getStartGlyphHeight(),
dragStartBehavior: dragStartBehavior,
);
}
return ExcludeSemantics(
......@@ -540,24 +541,20 @@ class TextSelectionOverlay {
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,
)
handle = _SelectionHandleOverlay(
type: _chooseType(
renderObject.textDirection,
TextSelectionHandleType.right,
TextSelectionHandleType.left,
),
handleLayerLink: endHandleLayerLink,
onSelectionHandleTapped: onSelectionHandleTapped,
onSelectionHandleDragStart: _handleSelectionEndHandleDragStart,
onSelectionHandleDragUpdate: _handleSelectionEndHandleDragUpdate,
selectionControls: selectionControls,
visibility: _effectiveEndHandleVisibility,
preferredLineHeight: _getEndGlyphHeight(),
dragStartBehavior: dragStartBehavior,
);
}
return ExcludeSemantics(
......@@ -565,7 +562,7 @@ class TextSelectionOverlay {
);
}
double? _getStartGlyphHeight() {
double _getStartGlyphHeight() {
final InlineSpan span = renderObject.text!;
final String prevText = span.toPlainText();
final String currText = selectionDelegate.textEditingValue.text;
......@@ -583,10 +580,10 @@ class TextSelectionOverlay {
firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length;
startHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.start, end: _selection.start + firstSelectedGraphemeExtent));
}
return startHandleRect?.height;
return startHandleRect?.height ?? renderObject.preferredLineHeight;
}
double? _getEndGlyphHeight() {
double _getEndGlyphHeight() {
final InlineSpan span = renderObject.text!;
final String prevText = span.toPlainText();
final String currText = selectionDelegate.textEditingValue.text;
......@@ -598,7 +595,7 @@ class TextSelectionOverlay {
lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length;
endHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.end - lastSelectedGraphemeExtent, end: _selection.end));
}
return endHandleRect?.height;
return endHandleRect?.height ?? renderObject.preferredLineHeight;
}
late Offset _dragEndPosition;
......@@ -754,7 +751,6 @@ class _SelectionHandleOverlay extends StatefulWidget {
required this.selectionControls,
required this.visibility,
required this.preferredLineHeight,
this.glyphHeight,
this.dragStartBehavior = DragStartBehavior.start,
}) : super(key: key);
......@@ -765,7 +761,6 @@ class _SelectionHandleOverlay extends StatefulWidget {
final TextSelectionControls selectionControls;
final ValueListenable<bool> visibility;
final double preferredLineHeight;
final double? glyphHeight;
final TextSelectionHandleType type;
final DragStartBehavior dragStartBehavior;
......@@ -817,8 +812,6 @@ class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with S
final Offset handleAnchor = widget.selectionControls.getHandleAnchor(
widget.type,
widget.preferredLineHeight,
widget.glyphHeight,
widget.glyphHeight,
);
final Size handleSize = widget.selectionControls.getHandleSize(
widget.preferredLineHeight,
......@@ -869,8 +862,6 @@ class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with S
widget.type,
widget.preferredLineHeight,
widget.onSelectionHandleTapped,
widget.glyphHeight,
widget.glyphHeight,
),
),
),
......
......@@ -31,7 +31,7 @@ const bool isContextMenuProvidedByPlatform = isBrowser;
class MockTextSelectionControls extends TextSelectionControls {
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) {
throw UnimplementedError();
}
......@@ -50,7 +50,7 @@ class MockTextSelectionControls extends TextSelectionControls {
}
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight, [double? startGlyphHeight, double? endGlyphHeight]) {
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
throw UnimplementedError();
}
......
......@@ -1370,14 +1370,15 @@ void main() {
// Handle not shown.
expect(controller.selection.isCollapsed, true);
final Finder fadeFinder = find.byType(FadeTransition);
expect(fadeFinder, findsNothing);
FadeTransition handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(0.0));
// Tap on the text field to show the handle.
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(fadeFinder, findsNWidgets(1));
final FadeTransition handle = tester.widget(fadeFinder.at(0));
handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(1.0));
// Enter more text.
......@@ -1388,7 +1389,8 @@ void main() {
// Handle not shown.
expect(controller.selection.isCollapsed, true);
expect(fadeFinder, findsNothing);
handle = tester.widget(fadeFinder.at(0));
expect(handle.opacity.value, equals(0.0));
});
testWidgets('selection handles are excluded from the semantics', (WidgetTester tester) async {
......
......@@ -10463,7 +10463,7 @@ class MockTextSelectionControls extends Fake implements TextSelectionControls {
}
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) {
return Container();
}
......@@ -10473,7 +10473,7 @@ class MockTextSelectionControls extends Fake implements TextSelectionControls {
}
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight, [double? startGlyphHeight, double? endGlyphHeight]) {
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
return Offset.zero;
}
......
......@@ -707,7 +707,7 @@ void main() {
await tester.pumpAndSettle();
final Finder gestureDetector = find.descendant(
of: find.byType(Visibility),
of: find.byType(CompositedTransformFollower),
matching: find.descendant(
of: find.byType(FadeTransition),
matching: find.byType(GestureDetector),
......@@ -910,7 +910,7 @@ class FakeRenderEditable extends RenderEditable {
class CustomTextSelectionControls extends TextSelectionControls {
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) {
throw UnimplementedError();
}
......@@ -929,7 +929,7 @@ class CustomTextSelectionControls extends TextSelectionControls {
}
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight, [double? startGlyphHeight, double? endGlyphHeight]) {
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
throw UnimplementedError();
}
......
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