1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/cupertino.dart';
6/// @docImport 'package:flutter/material.dart';
7library;
8
9import 'dart:async';
10import 'dart:math' as math;
11
12import 'package:characters/characters.dart';
13import 'package:flutter/foundation.dart';
14import 'package:flutter/gestures.dart';
15import 'package:flutter/rendering.dart';
16import 'package:flutter/scheduler.dart';
17import 'package:flutter/services.dart';
18
19import 'basic.dart';
20import 'binding.dart';
21import 'constants.dart';
22import 'context_menu_controller.dart';
23import 'debug.dart';
24import 'editable_text.dart';
25import 'feedback.dart';
26import 'framework.dart';
27import 'gesture_detector.dart';
28import 'inherited_theme.dart';
29import 'magnifier.dart';
30import 'overlay.dart';
31import 'scrollable.dart';
32import 'tap_region.dart';
33import 'ticker_provider.dart';
34import 'transitions.dart';
35
36export 'package:flutter/rendering.dart' show TextSelectionPoint;
37export 'package:flutter/services.dart' show TextSelectionDelegate;
38
39/// The type for a Function that builds a toolbar's container with the given
40/// child.
41///
42/// See also:
43///
44/// * [TextSelectionToolbar.toolbarBuilder], which is of this type.
45/// type.
46/// * [CupertinoTextSelectionToolbar.toolbarBuilder], which is similar, but
47/// for a Cupertino-style toolbar.
48typedef ToolbarBuilder = Widget Function(BuildContext context, Widget child);
49
50/// ParentData that determines whether or not to paint the corresponding child.
51///
52/// Used in the layout of the Cupertino and Material text selection menus, which
53/// decide whether or not to paint their buttons after laying them out and
54/// determining where they overflow.
55class ToolbarItemsParentData extends ContainerBoxParentData<RenderBox> {
56 /// Whether or not this child is painted.
57 ///
58 /// Children in the selection toolbar may be laid out for measurement purposes
59 /// but not painted. This allows these children to be identified.
60 bool shouldPaint = false;
61
62 @override
63 String toString() => '${super.toString()}; shouldPaint=$shouldPaint';
64}
65
66/// An interface for building the selection UI, to be provided by the
67/// implementer of the toolbar widget.
68///
69/// Parts of this class, including [buildToolbar], have been deprecated in favor
70/// of [EditableText.contextMenuBuilder], which is now the preferred way to
71/// customize the context menus.
72///
73/// ## Use with [EditableText.contextMenuBuilder]
74///
75/// For backwards compatibility during the deprecation period, when
76/// [EditableText.selectionControls] is set to an object that does not mix in
77/// [TextSelectionHandleControls], [EditableText.contextMenuBuilder] is ignored
78/// in favor of the deprecated [buildToolbar].
79///
80/// To migrate code from [buildToolbar] to the preferred
81/// [EditableText.contextMenuBuilder], while still using [buildHandle], mix in
82/// [TextSelectionHandleControls] into the [TextSelectionControls] subclass when
83/// moving any toolbar code to a callback passed to
84/// [EditableText.contextMenuBuilder].
85///
86/// In due course, [buildToolbar] will be removed, and the mixin will no longer
87/// be necessary as a way to flag to the framework that the code has been
88/// migrated and does not expect [buildToolbar] to be called.
89///
90/// For more information, see <https://docs.flutter.dev/release/breaking-changes/context-menus>.
91///
92/// See also:
93///
94/// * [SelectionArea], which selects appropriate text selection controls
95/// based on the current platform.
96abstract class TextSelectionControls {
97 /// Builds a selection handle of the given `type`.
98 ///
99 /// The top left corner of this widget is positioned at the bottom of the
100 /// selection position.
101 ///
102 /// The supplied [onTap] should be invoked when the handle is tapped, if such
103 /// interaction is allowed. As a counterexample, the default selection handle
104 /// on iOS [cupertinoTextSelectionControls] does not call [onTap] at all,
105 /// since its handles are not meant to be tapped.
106 Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]);
107
108 /// Get the anchor point of the handle relative to itself. The anchor point is
109 /// the point that is aligned with a specific point in the text. A handle
110 /// often visually "points to" that location.
111 Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight);
112
113 /// Builds a toolbar near a text selection.
114 ///
115 /// Typically displays buttons for copying and pasting text.
116 ///
117 /// The [globalEditableRegion] parameter is the TextField size of the global
118 /// coordinate system in logical pixels.
119 ///
120 /// The [textLineHeight] parameter is the [RenderEditable.preferredLineHeight]
121 /// of the [RenderEditable] we are building a toolbar for.
122 ///
123 /// The [selectionMidpoint] parameter is a general calculation midpoint
124 /// parameter of the toolbar. More detailed position information
125 /// is computable from the [endpoints] parameter.
126 @Deprecated(
127 'Use `contextMenuBuilder` instead. '
128 'This feature was deprecated after v3.3.0-0.5.pre.',
129 )
130 Widget buildToolbar(
131 BuildContext context,
132 Rect globalEditableRegion,
133 double textLineHeight,
134 Offset selectionMidpoint,
135 List<TextSelectionPoint> endpoints,
136 TextSelectionDelegate delegate,
137 ValueListenable<ClipboardStatus>? clipboardStatus,
138 Offset? lastSecondaryTapDownPosition,
139 );
140
141 /// Returns the size of the selection handle.
142 Size getHandleSize(double textLineHeight);
143
144 /// Whether the current selection of the text field managed by the given
145 /// `delegate` can be removed from the text field and placed into the
146 /// [Clipboard].
147 ///
148 /// By default, false is returned when nothing is selected in the text field.
149 ///
150 /// Subclasses can use this to decide if they should expose the cut
151 /// functionality to the user.
152 @Deprecated(
153 'Use `contextMenuBuilder` instead. '
154 'This feature was deprecated after v3.3.0-0.5.pre.',
155 )
156 bool canCut(TextSelectionDelegate delegate) {
157 return delegate.cutEnabled && !delegate.textEditingValue.selection.isCollapsed;
158 }
159
160 /// Whether the current selection of the text field managed by the given
161 /// `delegate` can be copied to the [Clipboard].
162 ///
163 /// By default, false is returned when nothing is selected in the text field.
164 ///
165 /// Subclasses can use this to decide if they should expose the copy
166 /// functionality to the user.
167 @Deprecated(
168 'Use `contextMenuBuilder` instead. '
169 'This feature was deprecated after v3.3.0-0.5.pre.',
170 )
171 bool canCopy(TextSelectionDelegate delegate) {
172 return delegate.copyEnabled && !delegate.textEditingValue.selection.isCollapsed;
173 }
174
175 /// Whether the text field managed by the given `delegate` supports pasting
176 /// from the clipboard.
177 ///
178 /// Subclasses can use this to decide if they should expose the paste
179 /// functionality to the user.
180 ///
181 /// This does not consider the contents of the clipboard. Subclasses may want
182 /// to, for example, disallow pasting when the clipboard contains an empty
183 /// string.
184 @Deprecated(
185 'Use `contextMenuBuilder` instead. '
186 'This feature was deprecated after v3.3.0-0.5.pre.',
187 )
188 bool canPaste(TextSelectionDelegate delegate) {
189 return delegate.pasteEnabled;
190 }
191
192 /// Whether the current selection of the text field managed by the given
193 /// `delegate` can be extended to include the entire content of the text
194 /// field.
195 ///
196 /// Subclasses can use this to decide if they should expose the select all
197 /// functionality to the user.
198 @Deprecated(
199 'Use `contextMenuBuilder` instead. '
200 'This feature was deprecated after v3.3.0-0.5.pre.',
201 )
202 bool canSelectAll(TextSelectionDelegate delegate) {
203 return delegate.selectAllEnabled && delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
204 }
205
206 /// Call [TextSelectionDelegate.cutSelection] to cut current selection.
207 ///
208 /// This is called by subclasses when their cut affordance is activated by
209 /// the user.
210 @Deprecated(
211 'Use `contextMenuBuilder` instead. '
212 'This feature was deprecated after v3.3.0-0.5.pre.',
213 )
214 void handleCut(TextSelectionDelegate delegate) {
215 delegate.cutSelection(SelectionChangedCause.toolbar);
216 }
217
218 /// Call [TextSelectionDelegate.copySelection] to copy current selection.
219 ///
220 /// This is called by subclasses when their copy affordance is activated by
221 /// the user.
222 @Deprecated(
223 'Use `contextMenuBuilder` instead. '
224 'This feature was deprecated after v3.3.0-0.5.pre.',
225 )
226 void handleCopy(TextSelectionDelegate delegate) {
227 delegate.copySelection(SelectionChangedCause.toolbar);
228 }
229
230 /// Call [TextSelectionDelegate.pasteText] to paste text.
231 ///
232 /// This is called by subclasses when their paste affordance is activated by
233 /// the user.
234 ///
235 /// This function is asynchronous since interacting with the clipboard is
236 /// asynchronous. Race conditions may exist with this API as currently
237 /// implemented.
238 // TODO(ianh): https://github.com/flutter/flutter/issues/11427
239 @Deprecated(
240 'Use `contextMenuBuilder` instead. '
241 'This feature was deprecated after v3.3.0-0.5.pre.',
242 )
243 Future<void> handlePaste(TextSelectionDelegate delegate) async {
244 delegate.pasteText(SelectionChangedCause.toolbar);
245 }
246
247 /// Call [TextSelectionDelegate.selectAll] to set the current selection to
248 /// contain the entire text value.
249 ///
250 /// Does not hide the toolbar.
251 ///
252 /// This is called by subclasses when their select-all affordance is activated
253 /// by the user.
254 @Deprecated(
255 'Use `contextMenuBuilder` instead. '
256 'This feature was deprecated after v3.3.0-0.5.pre.',
257 )
258 void handleSelectAll(TextSelectionDelegate delegate) {
259 delegate.selectAll(SelectionChangedCause.toolbar);
260 }
261}
262
263/// Text selection controls that do not show any toolbars or handles.
264///
265/// This is a placeholder, suitable for temporary use during development, but
266/// not practical for production. For example, it provides no way for the user
267/// to interact with selections: no context menus on desktop, no toolbars or
268/// drag handles on mobile, etc. For production, consider using
269/// [MaterialTextSelectionControls] or creating a custom subclass of
270/// [TextSelectionControls].
271///
272/// The [emptyTextSelectionControls] global variable has a
273/// suitable instance of this class.
274class EmptyTextSelectionControls extends TextSelectionControls {
275 @override
276 Size getHandleSize(double textLineHeight) => Size.zero;
277
278 @override
279 Widget buildToolbar(
280 BuildContext context,
281 Rect globalEditableRegion,
282 double textLineHeight,
283 Offset selectionMidpoint,
284 List<TextSelectionPoint> endpoints,
285 TextSelectionDelegate delegate,
286 ValueListenable<ClipboardStatus>? clipboardStatus,
287 Offset? lastSecondaryTapDownPosition,
288 ) => const SizedBox.shrink();
289
290 @override
291 Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) {
292 return const SizedBox.shrink();
293 }
294
295 @override
296 Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
297 return Offset.zero;
298 }
299}
300
301/// Text selection controls that do not show any toolbars or handles.
302///
303/// This is a placeholder, suitable for temporary use during development, but
304/// not practical for production. For example, it provides no way for the user
305/// to interact with selections: no context menus on desktop, no toolbars or
306/// drag handles on mobile, etc. For production, consider using
307/// [materialTextSelectionControls] or creating a custom subclass of
308/// [TextSelectionControls].
309final TextSelectionControls emptyTextSelectionControls = EmptyTextSelectionControls();
310
311
312/// An object that manages a pair of text selection handles for a
313/// [RenderEditable].
314///
315/// This class is a wrapper of [SelectionOverlay] to provide APIs specific for
316/// [RenderEditable]s. To manage selection handles for custom widgets, use
317/// [SelectionOverlay] instead.
318class TextSelectionOverlay {
319 /// Creates an object that manages overlay entries for selection handles.
320 ///
321 /// The [context] must have an [Overlay] as an ancestor.
322 TextSelectionOverlay({
323 required TextEditingValue value,
324 required this.context,
325 Widget? debugRequiredFor,
326 required LayerLink toolbarLayerLink,
327 required LayerLink startHandleLayerLink,
328 required LayerLink endHandleLayerLink,
329 required this.renderObject,
330 this.selectionControls,
331 bool handlesVisible = false,
332 required this.selectionDelegate,
333 DragStartBehavior dragStartBehavior = DragStartBehavior.start,
334 VoidCallback? onSelectionHandleTapped,
335 ClipboardStatusNotifier? clipboardStatus,
336 this.contextMenuBuilder,
337 required TextMagnifierConfiguration magnifierConfiguration,
338 }) : _handlesVisible = handlesVisible,
339 _value = value {
340 // TODO(polina-c): stop duplicating code across disposables
341 // https://github.com/flutter/flutter/issues/137435
342 if (kFlutterMemoryAllocationsEnabled) {
343 FlutterMemoryAllocations.instance.dispatchObjectCreated(
344 library: 'package:flutter/widgets.dart',
345 className: '$TextSelectionOverlay',
346 object: this,
347 );
348 }
349 renderObject.selectionStartInViewport.addListener(_updateTextSelectionOverlayVisibilities);
350 renderObject.selectionEndInViewport.addListener(_updateTextSelectionOverlayVisibilities);
351 _updateTextSelectionOverlayVisibilities();
352 _selectionOverlay = SelectionOverlay(
353 magnifierConfiguration: magnifierConfiguration,
354 context: context,
355 debugRequiredFor: debugRequiredFor,
356 // The metrics will be set when show handles.
357 startHandleType: TextSelectionHandleType.collapsed,
358 startHandlesVisible: _effectiveStartHandleVisibility,
359 lineHeightAtStart: 0.0,
360 onStartHandleDragStart: _handleSelectionStartHandleDragStart,
361 onStartHandleDragUpdate: _handleSelectionStartHandleDragUpdate,
362 onEndHandleDragEnd: _handleAnyDragEnd,
363 endHandleType: TextSelectionHandleType.collapsed,
364 endHandlesVisible: _effectiveEndHandleVisibility,
365 lineHeightAtEnd: 0.0,
366 onEndHandleDragStart: _handleSelectionEndHandleDragStart,
367 onEndHandleDragUpdate: _handleSelectionEndHandleDragUpdate,
368 onStartHandleDragEnd: _handleAnyDragEnd,
369 toolbarVisible: _effectiveToolbarVisibility,
370 selectionEndpoints: const <TextSelectionPoint>[],
371 selectionControls: selectionControls,
372 selectionDelegate: selectionDelegate,
373 clipboardStatus: clipboardStatus,
374 startHandleLayerLink: startHandleLayerLink,
375 endHandleLayerLink: endHandleLayerLink,
376 toolbarLayerLink: toolbarLayerLink,
377 onSelectionHandleTapped: onSelectionHandleTapped,
378 dragStartBehavior: dragStartBehavior,
379 toolbarLocation: renderObject.lastSecondaryTapDownPosition,
380 );
381 }
382
383 /// {@template flutter.widgets.SelectionOverlay.context}
384 /// The context in which the selection UI should appear.
385 ///
386 /// This context must have an [Overlay] as an ancestor because this object
387 /// will display the text selection handles in that [Overlay].
388 /// {@endtemplate}
389 final BuildContext context;
390
391 // TODO(mpcomplete): what if the renderObject is removed or replaced, or
392 // moves? Not sure what cases I need to handle, or how to handle them.
393 /// The editable line in which the selected text is being displayed.
394 final RenderEditable renderObject;
395
396 /// {@macro flutter.widgets.SelectionOverlay.selectionControls}
397 final TextSelectionControls? selectionControls;
398
399 /// {@macro flutter.widgets.SelectionOverlay.selectionDelegate}
400 final TextSelectionDelegate selectionDelegate;
401
402 late final SelectionOverlay _selectionOverlay;
403
404 /// {@macro flutter.widgets.EditableText.contextMenuBuilder}
405 ///
406 /// If not provided, no context menu will be built.
407 final WidgetBuilder? contextMenuBuilder;
408
409 /// Retrieve current value.
410 @visibleForTesting
411 TextEditingValue get value => _value;
412
413 TextEditingValue _value;
414
415 TextSelection get _selection => _value.selection;
416
417 final ValueNotifier<bool> _effectiveStartHandleVisibility = ValueNotifier<bool>(false);
418 final ValueNotifier<bool> _effectiveEndHandleVisibility = ValueNotifier<bool>(false);
419 final ValueNotifier<bool> _effectiveToolbarVisibility = ValueNotifier<bool>(false);
420
421 void _updateTextSelectionOverlayVisibilities() {
422 _effectiveStartHandleVisibility.value = _handlesVisible && renderObject.selectionStartInViewport.value;
423 _effectiveEndHandleVisibility.value = _handlesVisible && renderObject.selectionEndInViewport.value;
424 _effectiveToolbarVisibility.value = renderObject.selectionStartInViewport.value || renderObject.selectionEndInViewport.value;
425 }
426
427 /// Whether selection handles are visible.
428 ///
429 /// Set to false if you want to hide the handles. Use this property to show or
430 /// hide the handle without rebuilding them.
431 ///
432 /// Defaults to false.
433 bool get handlesVisible => _handlesVisible;
434 bool _handlesVisible = false;
435 set handlesVisible(bool visible) {
436 if (_handlesVisible == visible) {
437 return;
438 }
439 _handlesVisible = visible;
440 _updateTextSelectionOverlayVisibilities();
441 }
442
443 /// {@macro flutter.widgets.SelectionOverlay.showHandles}
444 void showHandles() {
445 _updateSelectionOverlay();
446 _selectionOverlay.showHandles();
447 }
448
449 /// {@macro flutter.widgets.SelectionOverlay.hideHandles}
450 void hideHandles() => _selectionOverlay.hideHandles();
451
452 /// {@macro flutter.widgets.SelectionOverlay.showToolbar}
453 void showToolbar() {
454 _updateSelectionOverlay();
455
456 if (selectionControls != null && selectionControls is! TextSelectionHandleControls) {
457 _selectionOverlay.showToolbar();
458 return;
459 }
460
461 if (contextMenuBuilder == null) {
462 return;
463 }
464
465 assert(context.mounted);
466 _selectionOverlay.showToolbar(
467 context: context,
468 contextMenuBuilder: contextMenuBuilder,
469 );
470 return;
471 }
472
473 /// Shows toolbar with spell check suggestions of misspelled words that are
474 /// available for click-and-replace.
475 void showSpellCheckSuggestionsToolbar(
476 WidgetBuilder spellCheckSuggestionsToolbarBuilder
477 ) {
478 _updateSelectionOverlay();
479 assert(context.mounted);
480 _selectionOverlay
481 .showSpellCheckSuggestionsToolbar(
482 context: context,
483 builder: spellCheckSuggestionsToolbarBuilder,
484 );
485 hideHandles();
486 }
487
488 /// {@macro flutter.widgets.SelectionOverlay.showMagnifier}
489 void showMagnifier(Offset positionToShow) {
490 final TextPosition position = renderObject.getPositionForPoint(positionToShow);
491 _updateSelectionOverlay();
492 _selectionOverlay.showMagnifier(
493 _buildMagnifier(
494 currentTextPosition: position,
495 globalGesturePosition: positionToShow,
496 renderEditable: renderObject,
497 ),
498 );
499 }
500
501 /// {@macro flutter.widgets.SelectionOverlay.updateMagnifier}
502 void updateMagnifier(Offset positionToShow) {
503 final TextPosition position = renderObject.getPositionForPoint(positionToShow);
504 _updateSelectionOverlay();
505 _selectionOverlay.updateMagnifier(
506 _buildMagnifier(
507 currentTextPosition: position,
508 globalGesturePosition: positionToShow,
509 renderEditable: renderObject,
510 ),
511 );
512 }
513
514 /// {@macro flutter.widgets.SelectionOverlay.hideMagnifier}
515 void hideMagnifier() {
516 _selectionOverlay.hideMagnifier();
517 }
518
519 /// Updates the overlay after the selection has changed.
520 ///
521 /// If this method is called while the [SchedulerBinding.schedulerPhase] is
522 /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
523 /// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed
524 /// until the post-frame callbacks phase. Otherwise the update is done
525 /// synchronously. This means that it is safe to call during builds, but also
526 /// that if you do call this during a build, the UI will not update until the
527 /// next frame (i.e. many milliseconds later).
528 void update(TextEditingValue newValue) {
529 if (_value == newValue) {
530 return;
531 }
532 _value = newValue;
533 _updateSelectionOverlay();
534 // _updateSelectionOverlay may not rebuild the selection overlay if the
535 // text metrics and selection doesn't change even if the text has changed.
536 // This rebuild is needed for the toolbar to update based on the latest text
537 // value.
538 _selectionOverlay.markNeedsBuild();
539 }
540
541 void _updateSelectionOverlay() {
542 _selectionOverlay
543 // Update selection handle metrics.
544 ..startHandleType = _chooseType(
545 renderObject.textDirection,
546 TextSelectionHandleType.left,
547 TextSelectionHandleType.right,
548 )
549 ..lineHeightAtStart = _getStartGlyphHeight()
550 ..endHandleType = _chooseType(
551 renderObject.textDirection,
552 TextSelectionHandleType.right,
553 TextSelectionHandleType.left,
554 )
555 ..lineHeightAtEnd = _getEndGlyphHeight()
556 // Update selection toolbar metrics.
557 ..selectionEndpoints = renderObject.getEndpointsForSelection(_selection)
558 ..toolbarLocation = renderObject.lastSecondaryTapDownPosition;
559 }
560
561 /// Causes the overlay to update its rendering.
562 ///
563 /// This is intended to be called when the [renderObject] may have changed its
564 /// text metrics (e.g. because the text was scrolled).
565 void updateForScroll() {
566 _updateSelectionOverlay();
567 // This method may be called due to windows metrics changes. In that case,
568 // non of the properties in _selectionOverlay will change, but a rebuild is
569 // still needed.
570 _selectionOverlay.markNeedsBuild();
571 }
572
573 /// Whether the handles are currently visible.
574 bool get handlesAreVisible => _selectionOverlay._handles != null && handlesVisible;
575
576 /// {@macro flutter.widgets.SelectionOverlay.toolbarIsVisible}
577 ///
578 /// See also:
579 ///
580 /// * [spellCheckToolbarIsVisible], which is only whether the spell check menu
581 /// specifically is visible.
582 bool get toolbarIsVisible => _selectionOverlay.toolbarIsVisible;
583
584 /// Whether the magnifier is currently visible.
585 bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown;
586
587 /// Whether the spell check menu is currently visible.
588 ///
589 /// See also:
590 ///
591 /// * [toolbarIsVisible], which is whether any toolbar is visible.
592 bool get spellCheckToolbarIsVisible => _selectionOverlay._spellCheckToolbarController.isShown;
593
594 /// {@macro flutter.widgets.SelectionOverlay.hide}
595 void hide() => _selectionOverlay.hide();
596
597 /// {@macro flutter.widgets.SelectionOverlay.hideToolbar}
598 void hideToolbar() => _selectionOverlay.hideToolbar();
599
600 /// {@macro flutter.widgets.SelectionOverlay.dispose}
601 void dispose() {
602 // TODO(polina-c): stop duplicating code across disposables
603 // https://github.com/flutter/flutter/issues/137435
604 if (kFlutterMemoryAllocationsEnabled) {
605 FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
606 }
607 _selectionOverlay.dispose();
608 renderObject.selectionStartInViewport.removeListener(_updateTextSelectionOverlayVisibilities);
609 renderObject.selectionEndInViewport.removeListener(_updateTextSelectionOverlayVisibilities);
610 _effectiveToolbarVisibility.dispose();
611 _effectiveStartHandleVisibility.dispose();
612 _effectiveEndHandleVisibility.dispose();
613 hideToolbar();
614 }
615
616 double _getStartGlyphHeight() {
617 final String currText = selectionDelegate.textEditingValue.text;
618 final int firstSelectedGraphemeExtent;
619 Rect? startHandleRect;
620 // Only calculate handle rects if the text in the previous frame
621 // is the same as the text in the current frame. This is done because
622 // widget.renderObject contains the renderEditable from the previous frame.
623 // If the text changed between the current and previous frames then
624 // widget.renderObject.getRectForComposingRange might fail. In cases where
625 // the current frame is different from the previous we fall back to
626 // renderObject.preferredLineHeight.
627 if (renderObject.plainText == currText && _selection.isValid && !_selection.isCollapsed) {
628 final String selectedGraphemes = _selection.textInside(currText);
629 firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length;
630 startHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.start, end: _selection.start + firstSelectedGraphemeExtent));
631 }
632 return startHandleRect?.height ?? renderObject.preferredLineHeight;
633 }
634
635 double _getEndGlyphHeight() {
636 final String currText = selectionDelegate.textEditingValue.text;
637 final int lastSelectedGraphemeExtent;
638 Rect? endHandleRect;
639 // See the explanation in _getStartGlyphHeight.
640 if (renderObject.plainText == currText && _selection.isValid && !_selection.isCollapsed) {
641 final String selectedGraphemes = _selection.textInside(currText);
642 lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length;
643 endHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.end - lastSelectedGraphemeExtent, end: _selection.end));
644 }
645 return endHandleRect?.height ?? renderObject.preferredLineHeight;
646 }
647
648 MagnifierInfo _buildMagnifier({
649 required RenderEditable renderEditable,
650 required Offset globalGesturePosition,
651 required TextPosition currentTextPosition,
652 }) {
653 final TextSelection lineAtOffset = renderEditable.getLineAtOffset(currentTextPosition);
654 final TextPosition positionAtEndOfLine = TextPosition(
655 offset: lineAtOffset.extentOffset,
656 affinity: TextAffinity.upstream,
657 );
658
659 // Default affinity is downstream.
660 final TextPosition positionAtBeginningOfLine = TextPosition(
661 offset: lineAtOffset.baseOffset,
662 );
663
664 final Rect localLineBoundaries = Rect.fromPoints(
665 renderEditable.getLocalRectForCaret(positionAtBeginningOfLine).topCenter,
666 renderEditable.getLocalRectForCaret(positionAtEndOfLine).bottomCenter,
667 );
668 final RenderBox? overlay = Overlay.of(context, rootOverlay: true).context.findRenderObject() as RenderBox?;
669 final Matrix4 transformToOverlay = renderEditable.getTransformTo(overlay);
670 final Rect overlayLineBoundaries = MatrixUtils.transformRect(
671 transformToOverlay,
672 localLineBoundaries,
673 );
674
675 final Rect localCaretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
676 final Rect overlayCaretRect = MatrixUtils.transformRect(
677 transformToOverlay,
678 localCaretRect,
679 );
680
681 final Offset overlayGesturePosition = overlay?.globalToLocal(globalGesturePosition) ?? globalGesturePosition;
682
683 return MagnifierInfo(
684 fieldBounds: MatrixUtils.transformRect(transformToOverlay, renderEditable.paintBounds),
685 globalGesturePosition: overlayGesturePosition,
686 caretRect: overlayCaretRect,
687 currentLineBoundaries: overlayLineBoundaries,
688 );
689 }
690
691 // The contact position of the gesture at the current end handle location, in
692 // global coordinates. Updated when the handle moves.
693 late double _endHandleDragPosition;
694
695 // The distance from _endHandleDragPosition to the center of the line that it
696 // corresponds to, in global coordinates.
697 late double _endHandleDragTarget;
698
699 void _handleSelectionEndHandleDragStart(DragStartDetails details) {
700 if (!renderObject.attached) {
701 return;
702 }
703
704 _endHandleDragPosition = details.globalPosition.dy;
705
706 // Use local coordinates when dealing with line height. because in case of a
707 // scale transformation, the line height will also be scaled.
708 final double centerOfLineLocal = _selectionOverlay.selectionEndpoints.last.point.dy
709 - renderObject.preferredLineHeight / 2;
710 final double centerOfLineGlobal = renderObject.localToGlobal(
711 Offset(0.0, centerOfLineLocal),
712 ).dy;
713 _endHandleDragTarget = centerOfLineGlobal - details.globalPosition.dy;
714 // Instead of finding the TextPosition at the handle's location directly,
715 // use the vertical center of the line that it points to. This is because
716 // selection handles typically hang above or below the line that they point
717 // to.
718 final TextPosition position = renderObject.getPositionForPoint(
719 Offset(
720 details.globalPosition.dx,
721 centerOfLineGlobal,
722 ),
723 );
724
725 _selectionOverlay.showMagnifier(
726 _buildMagnifier(
727 currentTextPosition: position,
728 globalGesturePosition: details.globalPosition,
729 renderEditable: renderObject,
730 ),
731 );
732 }
733
734 /// Given a handle position and drag position, returns the position of handle
735 /// after the drag.
736 ///
737 /// The handle jumps instantly between lines when the drag reaches a full
738 /// line's height away from the original handle position. In other words, the
739 /// line jump happens when the contact point would be located at the same
740 /// place on the handle at the new line as when the gesture started, for both
741 /// directions.
742 ///
743 /// This is not the same as just maintaining an offset from the target and the
744 /// contact point. There is no point at which moving the drag up and down a
745 /// small sub-line-height distance will cause the cursor to jump up and down
746 /// between lines. The drag distance must be a full line height for the cursor
747 /// to change lines, for both directions.
748 ///
749 /// Both parameters must be in local coordinates because the untransformed
750 /// line height is used, and the return value is in local coordinates as well.
751 double _getHandleDy(double dragDy, double handleDy) {
752 final double distanceDragged = dragDy - handleDy;
753 final int dragDirection = distanceDragged < 0.0 ? -1 : 1;
754 final int linesDragged =
755 dragDirection * (distanceDragged.abs() / renderObject.preferredLineHeight).floor();
756 return handleDy + linesDragged * renderObject.preferredLineHeight;
757 }
758
759 void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) {
760 if (!renderObject.attached) {
761 return;
762 }
763
764 // This is NOT the same as details.localPosition. That is relative to the
765 // selection handle, whereas this is relative to the RenderEditable.
766 final Offset localPosition = renderObject.globalToLocal(details.globalPosition);
767
768 final double nextEndHandleDragPositionLocal = _getHandleDy(
769 localPosition.dy,
770 renderObject.globalToLocal(Offset(0.0, _endHandleDragPosition)).dy,
771 );
772 _endHandleDragPosition = renderObject.localToGlobal(
773 Offset(0.0, nextEndHandleDragPositionLocal),
774 ).dy;
775
776 final Offset handleTargetGlobal = Offset(
777 details.globalPosition.dx,
778 _endHandleDragPosition + _endHandleDragTarget,
779 );
780
781 final TextPosition position = renderObject.getPositionForPoint(handleTargetGlobal);
782
783 if (_selection.isCollapsed) {
784 _selectionOverlay.updateMagnifier(_buildMagnifier(
785 currentTextPosition: position,
786 globalGesturePosition: details.globalPosition,
787 renderEditable: renderObject,
788 ));
789
790 final TextSelection currentSelection = TextSelection.fromPosition(position);
791 _handleSelectionHandleChanged(currentSelection);
792 return;
793 }
794
795 final TextSelection newSelection;
796 switch (defaultTargetPlatform) {
797 // On Apple platforms, dragging the base handle makes it the extent.
798 case TargetPlatform.iOS:
799 case TargetPlatform.macOS:
800 newSelection = TextSelection(
801 extentOffset: position.offset,
802 baseOffset: _selection.start,
803 );
804 if (position.offset <= _selection.start) {
805 return; // Don't allow order swapping.
806 }
807 case TargetPlatform.android:
808 case TargetPlatform.fuchsia:
809 case TargetPlatform.linux:
810 case TargetPlatform.windows:
811 newSelection = TextSelection(
812 baseOffset: _selection.baseOffset,
813 extentOffset: position.offset,
814 );
815 if (newSelection.baseOffset >= newSelection.extentOffset) {
816 return; // Don't allow order swapping.
817 }
818 }
819
820 _handleSelectionHandleChanged(newSelection);
821
822 _selectionOverlay.updateMagnifier(_buildMagnifier(
823 currentTextPosition: newSelection.extent,
824 globalGesturePosition: details.globalPosition,
825 renderEditable: renderObject,
826 ));
827 }
828
829 // The contact position of the gesture at the current start handle location,
830 // in global coordinates. Updated when the handle moves.
831 late double _startHandleDragPosition;
832
833 // The distance from _startHandleDragPosition to the center of the line that
834 // it corresponds to, in global coordinates.
835 late double _startHandleDragTarget;
836
837 void _handleSelectionStartHandleDragStart(DragStartDetails details) {
838 if (!renderObject.attached) {
839 return;
840 }
841
842 _startHandleDragPosition = details.globalPosition.dy;
843
844 // Use local coordinates when dealing with line height. because in case of a
845 // scale transformation, the line height will also be scaled.
846 final double centerOfLineLocal = _selectionOverlay.selectionEndpoints.first.point.dy
847 - renderObject.preferredLineHeight / 2;
848 final double centerOfLineGlobal = renderObject.localToGlobal(
849 Offset(0.0, centerOfLineLocal),
850 ).dy;
851 _startHandleDragTarget = centerOfLineGlobal - details.globalPosition.dy;
852 // Instead of finding the TextPosition at the handle's location directly,
853 // use the vertical center of the line that it points to. This is because
854 // selection handles typically hang above or below the line that they point
855 // to.
856 final TextPosition position = renderObject.getPositionForPoint(
857 Offset(
858 details.globalPosition.dx,
859 centerOfLineGlobal,
860 ),
861 );
862
863 _selectionOverlay.showMagnifier(
864 _buildMagnifier(
865 currentTextPosition: position,
866 globalGesturePosition: details.globalPosition,
867 renderEditable: renderObject,
868 ),
869 );
870 }
871
872 void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) {
873 if (!renderObject.attached) {
874 return;
875 }
876
877 // This is NOT the same as details.localPosition. That is relative to the
878 // selection handle, whereas this is relative to the RenderEditable.
879 final Offset localPosition = renderObject.globalToLocal(details.globalPosition);
880 final double nextStartHandleDragPositionLocal = _getHandleDy(
881 localPosition.dy,
882 renderObject.globalToLocal(Offset(0.0, _startHandleDragPosition)).dy,
883 );
884 _startHandleDragPosition = renderObject.localToGlobal(
885 Offset(0.0, nextStartHandleDragPositionLocal),
886 ).dy;
887 final Offset handleTargetGlobal = Offset(
888 details.globalPosition.dx,
889 _startHandleDragPosition + _startHandleDragTarget,
890 );
891 final TextPosition position = renderObject.getPositionForPoint(handleTargetGlobal);
892
893 if (_selection.isCollapsed) {
894 _selectionOverlay.updateMagnifier(_buildMagnifier(
895 currentTextPosition: position,
896 globalGesturePosition: details.globalPosition,
897 renderEditable: renderObject,
898 ));
899
900 final TextSelection currentSelection = TextSelection.fromPosition(position);
901 _handleSelectionHandleChanged(currentSelection);
902 return;
903 }
904
905 final TextSelection newSelection;
906 switch (defaultTargetPlatform) {
907 // On Apple platforms, dragging the base handle makes it the extent.
908 case TargetPlatform.iOS:
909 case TargetPlatform.macOS:
910 newSelection = TextSelection(
911 extentOffset: position.offset,
912 baseOffset: _selection.end,
913 );
914 if (newSelection.extentOffset >= _selection.end) {
915 return; // Don't allow order swapping.
916 }
917 case TargetPlatform.android:
918 case TargetPlatform.fuchsia:
919 case TargetPlatform.linux:
920 case TargetPlatform.windows:
921 newSelection = TextSelection(
922 baseOffset: position.offset,
923 extentOffset: _selection.extentOffset,
924 );
925 if (newSelection.baseOffset >= newSelection.extentOffset) {
926 return; // Don't allow order swapping.
927 }
928 }
929
930 _selectionOverlay.updateMagnifier(_buildMagnifier(
931 currentTextPosition: newSelection.extent.offset < newSelection.base.offset ? newSelection.extent : newSelection.base,
932 globalGesturePosition: details.globalPosition,
933 renderEditable: renderObject,
934 ));
935
936 _handleSelectionHandleChanged(newSelection);
937 }
938
939 void _handleAnyDragEnd(DragEndDetails details) {
940 if (!context.mounted) {
941 return;
942 }
943 if (selectionControls is! TextSelectionHandleControls) {
944 _selectionOverlay.hideMagnifier();
945 if (!_selection.isCollapsed) {
946 _selectionOverlay.showToolbar();
947 }
948 return;
949 }
950 _selectionOverlay.hideMagnifier();
951 if (!_selection.isCollapsed) {
952 _selectionOverlay.showToolbar(
953 context: context,
954 contextMenuBuilder: contextMenuBuilder,
955 );
956 }
957 }
958
959 void _handleSelectionHandleChanged(TextSelection newSelection) {
960 selectionDelegate.userUpdateTextEditingValue(
961 _value.copyWith(selection: newSelection),
962 SelectionChangedCause.drag,
963 );
964 }
965
966 TextSelectionHandleType _chooseType(
967 TextDirection textDirection,
968 TextSelectionHandleType ltrType,
969 TextSelectionHandleType rtlType,
970 ) {
971 if (_selection.isCollapsed) {
972 return TextSelectionHandleType.collapsed;
973 }
974
975 return switch (textDirection) {
976 TextDirection.ltr => ltrType,
977 TextDirection.rtl => rtlType,
978 };
979 }
980}
981
982/// An object that manages a pair of selection handles and a toolbar.
983///
984/// The selection handles are displayed in the [Overlay] that most closely
985/// encloses the given [BuildContext].
986class SelectionOverlay {
987 /// Creates an object that manages overlay entries for selection handles.
988 ///
989 /// The [context] must have an [Overlay] as an ancestor.
990 SelectionOverlay({
991 required this.context,
992 this.debugRequiredFor,
993 required TextSelectionHandleType startHandleType,
994 required double lineHeightAtStart,
995 this.startHandlesVisible,
996 this.onStartHandleDragStart,
997 this.onStartHandleDragUpdate,
998 this.onStartHandleDragEnd,
999 required TextSelectionHandleType endHandleType,
1000 required double lineHeightAtEnd,
1001 this.endHandlesVisible,
1002 this.onEndHandleDragStart,
1003 this.onEndHandleDragUpdate,
1004 this.onEndHandleDragEnd,
1005 this.toolbarVisible,
1006 required List<TextSelectionPoint> selectionEndpoints,
1007 required this.selectionControls,
1008 @Deprecated(
1009 'Use `contextMenuBuilder` in `showToolbar` instead. '
1010 'This feature was deprecated after v3.3.0-0.5.pre.',
1011 )
1012 required this.selectionDelegate,
1013 required this.clipboardStatus,
1014 required this.startHandleLayerLink,
1015 required this.endHandleLayerLink,
1016 required this.toolbarLayerLink,
1017 this.dragStartBehavior = DragStartBehavior.start,
1018 this.onSelectionHandleTapped,
1019 @Deprecated(
1020 'Use `contextMenuBuilder` in `showToolbar` instead. '
1021 'This feature was deprecated after v3.3.0-0.5.pre.',
1022 )
1023 Offset? toolbarLocation,
1024 this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
1025 }) : _startHandleType = startHandleType,
1026 _lineHeightAtStart = lineHeightAtStart,
1027 _endHandleType = endHandleType,
1028 _lineHeightAtEnd = lineHeightAtEnd,
1029 _selectionEndpoints = selectionEndpoints,
1030 _toolbarLocation = toolbarLocation,
1031 assert(debugCheckHasOverlay(context)) {
1032 // TODO(polina-c): stop duplicating code across disposables
1033 // https://github.com/flutter/flutter/issues/137435
1034 if (kFlutterMemoryAllocationsEnabled) {
1035 FlutterMemoryAllocations.instance.dispatchObjectCreated(
1036 library: 'package:flutter/widgets.dart',
1037 className: '$SelectionOverlay',
1038 object: this,
1039 );
1040 }
1041 }
1042
1043 /// {@macro flutter.widgets.SelectionOverlay.context}
1044 final BuildContext context;
1045
1046 final ValueNotifier<MagnifierInfo> _magnifierInfo =
1047 ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
1048
1049 // [MagnifierController.show] and [MagnifierController.hide] should not be
1050 // called directly, except from inside [showMagnifier] and [hideMagnifier]. If
1051 // it is desired to show or hide the magnifier, call [showMagnifier] or
1052 // [hideMagnifier]. This is because the magnifier needs to orchestrate with
1053 // other properties in [SelectionOverlay].
1054 final MagnifierController _magnifierController = MagnifierController();
1055
1056 /// The configuration for the magnifier.
1057 ///
1058 /// By default, [SelectionOverlay]'s [TextMagnifierConfiguration] is disabled.
1059 ///
1060 /// {@macro flutter.widgets.magnifier.intro}
1061 final TextMagnifierConfiguration magnifierConfiguration;
1062
1063 /// {@template flutter.widgets.SelectionOverlay.toolbarIsVisible}
1064 /// Whether the toolbar is currently visible.
1065 ///
1066 /// Includes both the text selection toolbar and the spell check menu.
1067 /// {@endtemplate}
1068 bool get toolbarIsVisible {
1069 return selectionControls is TextSelectionHandleControls
1070 ? _contextMenuController.isShown || _spellCheckToolbarController.isShown
1071 : _toolbar != null || _spellCheckToolbarController.isShown;
1072 }
1073
1074 /// {@template flutter.widgets.SelectionOverlay.showMagnifier}
1075 /// Shows the magnifier, and hides the toolbar if it was showing when [showMagnifier]
1076 /// was called. This is safe to call on platforms not mobile, since
1077 /// a magnifierBuilder will not be provided, or the magnifierBuilder will return null
1078 /// on platforms not mobile.
1079 ///
1080 /// This is NOT the source of truth for if the magnifier is up or not,
1081 /// since magnifiers may hide themselves. If this info is needed, check
1082 /// [MagnifierController.shown].
1083 /// {@endtemplate}
1084 void showMagnifier(MagnifierInfo initialMagnifierInfo) {
1085 if (toolbarIsVisible) {
1086 hideToolbar();
1087 }
1088
1089 // Start from empty, so we don't utilize any remnant values.
1090 _magnifierInfo.value = initialMagnifierInfo;
1091
1092 // Pre-build the magnifiers so we can tell if we've built something
1093 // or not. If we don't build a magnifiers, then we should not
1094 // insert anything in the overlay.
1095 final Widget? builtMagnifier = magnifierConfiguration.magnifierBuilder(
1096 context,
1097 _magnifierController,
1098 _magnifierInfo,
1099 );
1100
1101 if (builtMagnifier == null) {
1102 return;
1103 }
1104
1105 _magnifierController.show(
1106 context: context,
1107 below: magnifierConfiguration.shouldDisplayHandlesInMagnifier
1108 ? null
1109 : _handles?.start,
1110 builder: (_) => builtMagnifier,
1111 );
1112 }
1113
1114 /// {@template flutter.widgets.SelectionOverlay.hideMagnifier}
1115 /// Hide the current magnifier.
1116 ///
1117 /// This does nothing if there is no magnifier.
1118 /// {@endtemplate}
1119 void hideMagnifier() {
1120 // This cannot be a check on `MagnifierController.shown`, since
1121 // it's possible that the magnifier is still in the overlay, but
1122 // not shown in cases where the magnifier hides itself.
1123 if (_magnifierController.overlayEntry == null) {
1124 return;
1125 }
1126
1127 _magnifierController.hide();
1128 }
1129
1130 /// The type of start selection handle.
1131 ///
1132 /// Changing the value while the handles are visible causes them to rebuild.
1133 TextSelectionHandleType get startHandleType => _startHandleType;
1134 TextSelectionHandleType _startHandleType;
1135 set startHandleType(TextSelectionHandleType value) {
1136 if (_startHandleType == value) {
1137 return;
1138 }
1139 _startHandleType = value;
1140 markNeedsBuild();
1141 }
1142
1143 /// The line height at the selection start.
1144 ///
1145 /// This value is used for calculating the size of the start selection handle.
1146 ///
1147 /// Changing the value while the handles are visible causes them to rebuild.
1148 double get lineHeightAtStart => _lineHeightAtStart;
1149 double _lineHeightAtStart;
1150 set lineHeightAtStart(double value) {
1151 if (_lineHeightAtStart == value) {
1152 return;
1153 }
1154 _lineHeightAtStart = value;
1155 markNeedsBuild();
1156 }
1157
1158 bool _isDraggingStartHandle = false;
1159
1160 /// Whether the start handle is visible.
1161 ///
1162 /// If the value changes, the start handle uses [FadeTransition] to transition
1163 /// itself on and off the screen.
1164 ///
1165 /// If this is null, the start selection handle will always be visible.
1166 final ValueListenable<bool>? startHandlesVisible;
1167
1168 /// Called when the users start dragging the start selection handles.
1169 final ValueChanged<DragStartDetails>? onStartHandleDragStart;
1170
1171 void _handleStartHandleDragStart(DragStartDetails details) {
1172 assert(!_isDraggingStartHandle);
1173 // Calling OverlayEntry.remove may not happen until the following frame, so
1174 // it's possible for the handles to receive a gesture after calling remove.
1175 if (_handles == null) {
1176 _isDraggingStartHandle = false;
1177 return;
1178 }
1179 _isDraggingStartHandle = details.kind == PointerDeviceKind.touch;
1180 onStartHandleDragStart?.call(details);
1181 }
1182
1183 void _handleStartHandleDragUpdate(DragUpdateDetails details) {
1184 // Calling OverlayEntry.remove may not happen until the following frame, so
1185 // it's possible for the handles to receive a gesture after calling remove.
1186 if (_handles == null) {
1187 _isDraggingStartHandle = false;
1188 return;
1189 }
1190 onStartHandleDragUpdate?.call(details);
1191 }
1192
1193 /// Called when the users drag the start selection handles to new locations.
1194 final ValueChanged<DragUpdateDetails>? onStartHandleDragUpdate;
1195
1196 /// Called when the users lift their fingers after dragging the start selection
1197 /// handles.
1198 final ValueChanged<DragEndDetails>? onStartHandleDragEnd;
1199
1200 void _handleStartHandleDragEnd(DragEndDetails details) {
1201 _isDraggingStartHandle = false;
1202 // Calling OverlayEntry.remove may not happen until the following frame, so
1203 // it's possible for the handles to receive a gesture after calling remove.
1204 if (_handles == null) {
1205 return;
1206 }
1207 onStartHandleDragEnd?.call(details);
1208 }
1209
1210 /// The type of end selection handle.
1211 ///
1212 /// Changing the value while the handles are visible causes them to rebuild.
1213 TextSelectionHandleType get endHandleType => _endHandleType;
1214 TextSelectionHandleType _endHandleType;
1215 set endHandleType(TextSelectionHandleType value) {
1216 if (_endHandleType == value) {
1217 return;
1218 }
1219 _endHandleType = value;
1220 markNeedsBuild();
1221 }
1222
1223 /// The line height at the selection end.
1224 ///
1225 /// This value is used for calculating the size of the end selection handle.
1226 ///
1227 /// Changing the value while the handles are visible causes them to rebuild.
1228 double get lineHeightAtEnd => _lineHeightAtEnd;
1229 double _lineHeightAtEnd;
1230 set lineHeightAtEnd(double value) {
1231 if (_lineHeightAtEnd == value) {
1232 return;
1233 }
1234 _lineHeightAtEnd = value;
1235 markNeedsBuild();
1236 }
1237
1238 bool _isDraggingEndHandle = false;
1239
1240 /// Whether the end handle is visible.
1241 ///
1242 /// If the value changes, the end handle uses [FadeTransition] to transition
1243 /// itself on and off the screen.
1244 ///
1245 /// If this is null, the end selection handle will always be visible.
1246 final ValueListenable<bool>? endHandlesVisible;
1247
1248 /// Called when the users start dragging the end selection handles.
1249 final ValueChanged<DragStartDetails>? onEndHandleDragStart;
1250
1251 void _handleEndHandleDragStart(DragStartDetails details) {
1252 assert(!_isDraggingEndHandle);
1253 // Calling OverlayEntry.remove may not happen until the following frame, so
1254 // it's possible for the handles to receive a gesture after calling remove.
1255 if (_handles == null) {
1256 _isDraggingEndHandle = false;
1257 return;
1258 }
1259 _isDraggingEndHandle = details.kind == PointerDeviceKind.touch;
1260 onEndHandleDragStart?.call(details);
1261 }
1262
1263 void _handleEndHandleDragUpdate(DragUpdateDetails details) {
1264 // Calling OverlayEntry.remove may not happen until the following frame, so
1265 // it's possible for the handles to receive a gesture after calling remove.
1266 if (_handles == null) {
1267 _isDraggingEndHandle = false;
1268 return;
1269 }
1270 onEndHandleDragUpdate?.call(details);
1271 }
1272
1273 /// Called when the users drag the end selection handles to new locations.
1274 final ValueChanged<DragUpdateDetails>? onEndHandleDragUpdate;
1275
1276 /// Called when the users lift their fingers after dragging the end selection
1277 /// handles.
1278 final ValueChanged<DragEndDetails>? onEndHandleDragEnd;
1279
1280 void _handleEndHandleDragEnd(DragEndDetails details) {
1281 _isDraggingEndHandle = false;
1282 // Calling OverlayEntry.remove may not happen until the following frame, so
1283 // it's possible for the handles to receive a gesture after calling remove.
1284 if (_handles == null) {
1285 return;
1286 }
1287 onEndHandleDragEnd?.call(details);
1288 }
1289
1290 /// Whether the toolbar is visible.
1291 ///
1292 /// If the value changes, the toolbar uses [FadeTransition] to transition
1293 /// itself on and off the screen.
1294 ///
1295 /// If this is null the toolbar will always be visible.
1296 final ValueListenable<bool>? toolbarVisible;
1297
1298 /// The text selection positions of selection start and end.
1299 List<TextSelectionPoint> get selectionEndpoints => _selectionEndpoints;
1300 List<TextSelectionPoint> _selectionEndpoints;
1301 set selectionEndpoints(List<TextSelectionPoint> value) {
1302 if (!listEquals(_selectionEndpoints, value)) {
1303 markNeedsBuild();
1304 if (_isDraggingEndHandle || _isDraggingStartHandle) {
1305 switch (defaultTargetPlatform) {
1306 case TargetPlatform.android:
1307 HapticFeedback.selectionClick();
1308 case TargetPlatform.fuchsia:
1309 case TargetPlatform.iOS:
1310 case TargetPlatform.linux:
1311 case TargetPlatform.macOS:
1312 case TargetPlatform.windows:
1313 break;
1314 }
1315 }
1316 }
1317 _selectionEndpoints = value;
1318 }
1319
1320 /// Debugging information for explaining why the [Overlay] is required.
1321 final Widget? debugRequiredFor;
1322
1323 /// The object supplied to the [CompositedTransformTarget] that wraps the text
1324 /// field.
1325 final LayerLink toolbarLayerLink;
1326
1327 /// The objects supplied to the [CompositedTransformTarget] that wraps the
1328 /// location of start selection handle.
1329 final LayerLink startHandleLayerLink;
1330
1331 /// The objects supplied to the [CompositedTransformTarget] that wraps the
1332 /// location of end selection handle.
1333 final LayerLink endHandleLayerLink;
1334
1335 /// {@template flutter.widgets.SelectionOverlay.selectionControls}
1336 /// Builds text selection handles and toolbar.
1337 /// {@endtemplate}
1338 final TextSelectionControls? selectionControls;
1339
1340 /// {@template flutter.widgets.SelectionOverlay.selectionDelegate}
1341 /// The delegate for manipulating the current selection in the owning
1342 /// text field.
1343 /// {@endtemplate}
1344 @Deprecated(
1345 'Use `contextMenuBuilder` instead. '
1346 'This feature was deprecated after v3.3.0-0.5.pre.',
1347 )
1348 final TextSelectionDelegate? selectionDelegate;
1349
1350 /// Determines the way that drag start behavior is handled.
1351 ///
1352 /// If set to [DragStartBehavior.start], handle drag behavior will
1353 /// begin at the position where the drag gesture won the arena. If set to
1354 /// [DragStartBehavior.down] it will begin at the position where a down
1355 /// event is first detected.
1356 ///
1357 /// In general, setting this to [DragStartBehavior.start] will make drag
1358 /// animation smoother and setting it to [DragStartBehavior.down] will make
1359 /// drag behavior feel slightly more reactive.
1360 ///
1361 /// By default, the drag start behavior is [DragStartBehavior.start].
1362 ///
1363 /// See also:
1364 ///
1365 /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
1366 final DragStartBehavior dragStartBehavior;
1367
1368 /// {@template flutter.widgets.SelectionOverlay.onSelectionHandleTapped}
1369 /// A callback that's optionally invoked when a selection handle is tapped.
1370 ///
1371 /// The [TextSelectionControls.buildHandle] implementation the text field
1372 /// uses decides where the handle's tap "hotspot" is, or whether the
1373 /// selection handle supports tap gestures at all. For instance,
1374 /// [MaterialTextSelectionControls] calls [onSelectionHandleTapped] when the
1375 /// selection handle's "knob" is tapped, while
1376 /// [CupertinoTextSelectionControls] builds a handle that's not sufficiently
1377 /// large for tapping (as it's not meant to be tapped) so it does not call
1378 /// [onSelectionHandleTapped] even when tapped.
1379 /// {@endtemplate}
1380 // See https://github.com/flutter/flutter/issues/39376#issuecomment-848406415
1381 // for provenance.
1382 final VoidCallback? onSelectionHandleTapped;
1383
1384 /// Maintains the status of the clipboard for determining if its contents can
1385 /// be pasted or not.
1386 ///
1387 /// Useful because the actual value of the clipboard can only be checked
1388 /// asynchronously (see [Clipboard.getData]).
1389 final ClipboardStatusNotifier? clipboardStatus;
1390
1391 /// The location of where the toolbar should be drawn in relative to the
1392 /// location of [toolbarLayerLink].
1393 ///
1394 /// If this is null, the toolbar is drawn based on [selectionEndpoints] and
1395 /// the rect of render object of [context].
1396 ///
1397 /// This is useful for displaying toolbars at the mouse right-click locations
1398 /// in desktop devices.
1399 @Deprecated(
1400 'Use the `contextMenuBuilder` parameter in `showToolbar` instead. '
1401 'This feature was deprecated after v3.3.0-0.5.pre.',
1402 )
1403 Offset? get toolbarLocation => _toolbarLocation;
1404 Offset? _toolbarLocation;
1405 set toolbarLocation(Offset? value) {
1406 if (_toolbarLocation == value) {
1407 return;
1408 }
1409 _toolbarLocation = value;
1410 markNeedsBuild();
1411 }
1412
1413 /// Controls the fade-in and fade-out animations for the toolbar and handles.
1414 static const Duration fadeDuration = Duration(milliseconds: 150);
1415
1416 /// A pair of handles. If this is non-null, there are always 2, though the
1417 /// second is hidden when the selection is collapsed.
1418 ({OverlayEntry start, OverlayEntry end})? _handles;
1419
1420 /// A copy/paste toolbar.
1421 OverlayEntry? _toolbar;
1422
1423 // Manages the context menu. Not necessarily visible when non-null.
1424 final ContextMenuController _contextMenuController = ContextMenuController();
1425
1426 final ContextMenuController _spellCheckToolbarController = ContextMenuController();
1427
1428 /// {@template flutter.widgets.SelectionOverlay.showHandles}
1429 /// Builds the handles by inserting them into the [context]'s overlay.
1430 /// {@endtemplate}
1431 void showHandles() {
1432 if (_handles != null) {
1433 return;
1434 }
1435
1436 final OverlayState overlay = Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor);
1437
1438 final CapturedThemes capturedThemes = InheritedTheme.capture(
1439 from: context,
1440 to: overlay.context,
1441 );
1442
1443 _handles = (
1444 start: OverlayEntry(builder: (BuildContext context) {
1445 return capturedThemes.wrap(_buildStartHandle(context));
1446 }),
1447 end: OverlayEntry(builder: (BuildContext context) {
1448 return capturedThemes.wrap(_buildEndHandle(context));
1449 }),
1450 );
1451 overlay.insertAll(<OverlayEntry>[_handles!.start, _handles!.end]);
1452 }
1453
1454 /// {@template flutter.widgets.SelectionOverlay.hideHandles}
1455 /// Destroys the handles by removing them from overlay.
1456 /// {@endtemplate}
1457 void hideHandles() {
1458 if (_handles != null) {
1459 _handles!.start.remove();
1460 _handles!.start.dispose();
1461 _handles!.end.remove();
1462 _handles!.end.dispose();
1463 _handles = null;
1464 }
1465 }
1466
1467 /// {@template flutter.widgets.SelectionOverlay.showToolbar}
1468 /// Shows the toolbar by inserting it into the [context]'s overlay.
1469 /// {@endtemplate}
1470 void showToolbar({
1471 BuildContext? context,
1472 WidgetBuilder? contextMenuBuilder,
1473 }) {
1474 if (contextMenuBuilder == null) {
1475 if (_toolbar != null) {
1476 return;
1477 }
1478 _toolbar = OverlayEntry(builder: _buildToolbar);
1479 Overlay.of(this.context, rootOverlay: true, debugRequiredFor: debugRequiredFor).insert(_toolbar!);
1480 return;
1481 }
1482
1483 if (context == null) {
1484 return;
1485 }
1486
1487 final RenderBox renderBox = context.findRenderObject()! as RenderBox;
1488 _contextMenuController.show(
1489 context: context,
1490 contextMenuBuilder: (BuildContext context) {
1491 return _SelectionToolbarWrapper(
1492 visibility: toolbarVisible,
1493 layerLink: toolbarLayerLink,
1494 offset: -renderBox.localToGlobal(Offset.zero),
1495 child: contextMenuBuilder(context),
1496 );
1497 },
1498 );
1499 }
1500
1501 /// Shows toolbar with spell check suggestions of misspelled words that are
1502 /// available for click-and-replace.
1503 void showSpellCheckSuggestionsToolbar({
1504 BuildContext? context,
1505 required WidgetBuilder builder,
1506 }) {
1507 if (context == null) {
1508 return;
1509 }
1510
1511 final RenderBox renderBox = context.findRenderObject()! as RenderBox;
1512 _spellCheckToolbarController.show(
1513 context: context,
1514 contextMenuBuilder: (BuildContext context) {
1515 return _SelectionToolbarWrapper(
1516 layerLink: toolbarLayerLink,
1517 offset: -renderBox.localToGlobal(Offset.zero),
1518 child: builder(context),
1519 );
1520 },
1521 );
1522 }
1523
1524 bool _buildScheduled = false;
1525
1526 /// Rebuilds the selection toolbar or handles if they are present.
1527 void markNeedsBuild() {
1528 if (_handles == null && _toolbar == null) {
1529 return;
1530 }
1531 // If we are in build state, it will be too late to update visibility.
1532 // We will need to schedule the build in next frame.
1533 if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
1534 if (_buildScheduled) {
1535 return;
1536 }
1537 _buildScheduled = true;
1538 SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
1539 _buildScheduled = false;
1540 _handles?.start.markNeedsBuild();
1541 _handles?.end.markNeedsBuild();
1542 _toolbar?.markNeedsBuild();
1543 if (_contextMenuController.isShown) {
1544 _contextMenuController.markNeedsBuild();
1545 } else if (_spellCheckToolbarController.isShown) {
1546 _spellCheckToolbarController.markNeedsBuild();
1547 }
1548 }, debugLabel: 'SelectionOverlay.markNeedsBuild');
1549 } else {
1550 if (_handles != null) {
1551 _handles!.start.markNeedsBuild();
1552 _handles!.end.markNeedsBuild();
1553 }
1554 _toolbar?.markNeedsBuild();
1555 if (_contextMenuController.isShown) {
1556 _contextMenuController.markNeedsBuild();
1557 } else if (_spellCheckToolbarController.isShown) {
1558 _spellCheckToolbarController.markNeedsBuild();
1559 }
1560 }
1561 }
1562
1563 /// {@template flutter.widgets.SelectionOverlay.hide}
1564 /// Hides the entire overlay including the toolbar and the handles.
1565 /// {@endtemplate}
1566 void hide() {
1567 _magnifierController.hide();
1568 hideHandles();
1569 if (_toolbar != null || _contextMenuController.isShown || _spellCheckToolbarController.isShown) {
1570 hideToolbar();
1571 }
1572 }
1573
1574 /// {@template flutter.widgets.SelectionOverlay.hideToolbar}
1575 /// Hides the toolbar part of the overlay.
1576 ///
1577 /// To hide the whole overlay, see [hide].
1578 /// {@endtemplate}
1579 void hideToolbar() {
1580 _contextMenuController.remove();
1581 _spellCheckToolbarController.remove();
1582 if (_toolbar == null) {
1583 return;
1584 }
1585 _toolbar?.remove();
1586 _toolbar?.dispose();
1587 _toolbar = null;
1588 }
1589
1590 /// {@template flutter.widgets.SelectionOverlay.dispose}
1591 /// Disposes this object and release resources.
1592 /// {@endtemplate}
1593 void dispose() {
1594 // TODO(polina-c): stop duplicating code across disposables
1595 // https://github.com/flutter/flutter/issues/137435
1596 if (kFlutterMemoryAllocationsEnabled) {
1597 FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
1598 }
1599 hide();
1600 _magnifierInfo.dispose();
1601 }
1602
1603 Widget _buildStartHandle(BuildContext context) {
1604 final Widget handle;
1605 final TextSelectionControls? selectionControls = this.selectionControls;
1606 if (selectionControls == null) {
1607 handle = const SizedBox.shrink();
1608 } else {
1609 handle = _SelectionHandleOverlay(
1610 type: _startHandleType,
1611 handleLayerLink: startHandleLayerLink,
1612 onSelectionHandleTapped: onSelectionHandleTapped,
1613 onSelectionHandleDragStart: _handleStartHandleDragStart,
1614 onSelectionHandleDragUpdate: _handleStartHandleDragUpdate,
1615 onSelectionHandleDragEnd: _handleStartHandleDragEnd,
1616 selectionControls: selectionControls,
1617 visibility: startHandlesVisible,
1618 preferredLineHeight: _lineHeightAtStart,
1619 dragStartBehavior: dragStartBehavior,
1620 );
1621 }
1622 return TextFieldTapRegion(
1623 child: ExcludeSemantics(
1624 child: handle,
1625 ),
1626 );
1627 }
1628
1629 Widget _buildEndHandle(BuildContext context) {
1630 final Widget handle;
1631 final TextSelectionControls? selectionControls = this.selectionControls;
1632 if (selectionControls == null || _startHandleType == TextSelectionHandleType.collapsed) {
1633 // Hide the second handle when collapsed.
1634 handle = const SizedBox.shrink();
1635 } else {
1636 handle = _SelectionHandleOverlay(
1637 type: _endHandleType,
1638 handleLayerLink: endHandleLayerLink,
1639 onSelectionHandleTapped: onSelectionHandleTapped,
1640 onSelectionHandleDragStart: _handleEndHandleDragStart,
1641 onSelectionHandleDragUpdate: _handleEndHandleDragUpdate,
1642 onSelectionHandleDragEnd: _handleEndHandleDragEnd,
1643 selectionControls: selectionControls,
1644 visibility: endHandlesVisible,
1645 preferredLineHeight: _lineHeightAtEnd,
1646 dragStartBehavior: dragStartBehavior,
1647 );
1648 }
1649 return TextFieldTapRegion(
1650 child: ExcludeSemantics(
1651 child: handle,
1652 ),
1653 );
1654 }
1655
1656 // Build the toolbar via TextSelectionControls.
1657 Widget _buildToolbar(BuildContext context) {
1658 if (selectionControls == null) {
1659 return const SizedBox.shrink();
1660 }
1661 assert(selectionDelegate != null, 'If not using contextMenuBuilder, must pass selectionDelegate.');
1662
1663 final RenderBox renderBox = this.context.findRenderObject()! as RenderBox;
1664
1665 final Rect editingRegion = Rect.fromPoints(
1666 renderBox.localToGlobal(Offset.zero),
1667 renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero)),
1668 );
1669
1670 final bool isMultiline = selectionEndpoints.last.point.dy - selectionEndpoints.first.point.dy >
1671 lineHeightAtEnd / 2;
1672
1673 // If the selected text spans more than 1 line, horizontally center the toolbar.
1674 // Derived from both iOS and Android.
1675 final double midX = isMultiline
1676 ? editingRegion.width / 2
1677 : (selectionEndpoints.first.point.dx + selectionEndpoints.last.point.dx) / 2;
1678
1679 final Offset midpoint = Offset(
1680 midX,
1681 // The y-coordinate won't be made use of most likely.
1682 selectionEndpoints.first.point.dy - lineHeightAtStart,
1683 );
1684
1685 return _SelectionToolbarWrapper(
1686 visibility: toolbarVisible,
1687 layerLink: toolbarLayerLink,
1688 offset: -editingRegion.topLeft,
1689 child: Builder(
1690 builder: (BuildContext context) {
1691 return selectionControls!.buildToolbar(
1692 context,
1693 editingRegion,
1694 lineHeightAtStart,
1695 midpoint,
1696 selectionEndpoints,
1697 selectionDelegate!,
1698 clipboardStatus,
1699 toolbarLocation,
1700 );
1701 },
1702 ),
1703 );
1704 }
1705
1706 /// {@template flutter.widgets.SelectionOverlay.updateMagnifier}
1707 /// Update the current magnifier with new selection data, so the magnifier
1708 /// can respond accordingly.
1709 ///
1710 /// If the magnifier is not shown, this still updates the magnifier position
1711 /// because the magnifier may have hidden itself and is looking for a cue to reshow
1712 /// itself.
1713 ///
1714 /// If there is no magnifier in the overlay, this does nothing.
1715 /// {@endtemplate}
1716 void updateMagnifier(MagnifierInfo magnifierInfo) {
1717 if (_magnifierController.overlayEntry == null) {
1718 return;
1719 }
1720
1721 _magnifierInfo.value = magnifierInfo;
1722 }
1723}
1724
1725// TODO(justinmc): Currently this fades in but not out on all platforms. It
1726// should follow the correct fading behavior for the current platform, then be
1727// made public and de-duplicated with widgets/selectable_region.dart.
1728// https://github.com/flutter/flutter/issues/107732
1729// Wrap the given child in the widgets common to both contextMenuBuilder and
1730// TextSelectionControls.buildToolbar.
1731class _SelectionToolbarWrapper extends StatefulWidget {
1732 const _SelectionToolbarWrapper({
1733 this.visibility,
1734 required this.layerLink,
1735 required this.offset,
1736 required this.child,
1737 });
1738
1739 final Widget child;
1740 final Offset offset;
1741 final LayerLink layerLink;
1742 final ValueListenable<bool>? visibility;
1743
1744 @override
1745 State<_SelectionToolbarWrapper> createState() => _SelectionToolbarWrapperState();
1746}
1747
1748class _SelectionToolbarWrapperState extends State<_SelectionToolbarWrapper> with SingleTickerProviderStateMixin {
1749 late AnimationController _controller;
1750 Animation<double> get _opacity => _controller.view;
1751
1752 @override
1753 void initState() {
1754 super.initState();
1755
1756 _controller = AnimationController(duration: SelectionOverlay.fadeDuration, vsync: this);
1757
1758 _toolbarVisibilityChanged();
1759 widget.visibility?.addListener(_toolbarVisibilityChanged);
1760 }
1761
1762 @override
1763 void didUpdateWidget(_SelectionToolbarWrapper oldWidget) {
1764 super.didUpdateWidget(oldWidget);
1765 if (oldWidget.visibility == widget.visibility) {
1766 return;
1767 }
1768 oldWidget.visibility?.removeListener(_toolbarVisibilityChanged);
1769 _toolbarVisibilityChanged();
1770 widget.visibility?.addListener(_toolbarVisibilityChanged);
1771 }
1772
1773 @override
1774 void dispose() {
1775 widget.visibility?.removeListener(_toolbarVisibilityChanged);
1776 _controller.dispose();
1777 super.dispose();
1778 }
1779
1780 void _toolbarVisibilityChanged() {
1781 if (widget.visibility?.value ?? true) {
1782 _controller.forward();
1783 } else {
1784 _controller.reverse();
1785 }
1786 }
1787
1788 @override
1789 Widget build(BuildContext context) {
1790 return TextFieldTapRegion(
1791 child: Directionality(
1792 textDirection: Directionality.of(this.context),
1793 child: FadeTransition(
1794 opacity: _opacity,
1795 child: CompositedTransformFollower(
1796 link: widget.layerLink,
1797 showWhenUnlinked: false,
1798 offset: widget.offset,
1799 child: widget.child,
1800 ),
1801 ),
1802 ),
1803 );
1804 }
1805}
1806
1807/// This widget represents a single draggable selection handle.
1808class _SelectionHandleOverlay extends StatefulWidget {
1809 /// Create selection overlay.
1810 const _SelectionHandleOverlay({
1811 required this.type,
1812 required this.handleLayerLink,
1813 this.onSelectionHandleTapped,
1814 this.onSelectionHandleDragStart,
1815 this.onSelectionHandleDragUpdate,
1816 this.onSelectionHandleDragEnd,
1817 required this.selectionControls,
1818 this.visibility,
1819 required this.preferredLineHeight,
1820 this.dragStartBehavior = DragStartBehavior.start,
1821 });
1822
1823 final LayerLink handleLayerLink;
1824 final VoidCallback? onSelectionHandleTapped;
1825 final ValueChanged<DragStartDetails>? onSelectionHandleDragStart;
1826 final ValueChanged<DragUpdateDetails>? onSelectionHandleDragUpdate;
1827 final ValueChanged<DragEndDetails>? onSelectionHandleDragEnd;
1828 final TextSelectionControls selectionControls;
1829 final ValueListenable<bool>? visibility;
1830 final double preferredLineHeight;
1831 final TextSelectionHandleType type;
1832 final DragStartBehavior dragStartBehavior;
1833
1834 @override
1835 State<_SelectionHandleOverlay> createState() => _SelectionHandleOverlayState();
1836}
1837
1838class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with SingleTickerProviderStateMixin {
1839 late AnimationController _controller;
1840 Animation<double> get _opacity => _controller.view;
1841
1842 @override
1843 void initState() {
1844 super.initState();
1845
1846 _controller = AnimationController(duration: SelectionOverlay.fadeDuration, vsync: this);
1847
1848 _handleVisibilityChanged();
1849 widget.visibility?.addListener(_handleVisibilityChanged);
1850 }
1851
1852 void _handleVisibilityChanged() {
1853 if (widget.visibility?.value ?? true) {
1854 _controller.forward();
1855 } else {
1856 _controller.reverse();
1857 }
1858 }
1859
1860 @override
1861 void didUpdateWidget(_SelectionHandleOverlay oldWidget) {
1862 super.didUpdateWidget(oldWidget);
1863 oldWidget.visibility?.removeListener(_handleVisibilityChanged);
1864 _handleVisibilityChanged();
1865 widget.visibility?.addListener(_handleVisibilityChanged);
1866 }
1867
1868 @override
1869 void dispose() {
1870 widget.visibility?.removeListener(_handleVisibilityChanged);
1871 _controller.dispose();
1872 super.dispose();
1873 }
1874
1875 @override
1876 Widget build(BuildContext context) {
1877 final Offset handleAnchor = widget.selectionControls.getHandleAnchor(
1878 widget.type,
1879 widget.preferredLineHeight,
1880 );
1881 final Size handleSize = widget.selectionControls.getHandleSize(
1882 widget.preferredLineHeight,
1883 );
1884
1885 final Rect handleRect = Rect.fromLTWH(
1886 -handleAnchor.dx,
1887 -handleAnchor.dy,
1888 handleSize.width,
1889 handleSize.height,
1890 );
1891
1892 // Make sure the GestureDetector is big enough to be easily interactive.
1893 final Rect interactiveRect = handleRect.expandToInclude(
1894 Rect.fromCircle(center: handleRect.center, radius: kMinInteractiveDimension / 2),
1895 );
1896 final RelativeRect padding = RelativeRect.fromLTRB(
1897 math.max((interactiveRect.width - handleRect.width) / 2, 0),
1898 math.max((interactiveRect.height - handleRect.height) / 2, 0),
1899 math.max((interactiveRect.width - handleRect.width) / 2, 0),
1900 math.max((interactiveRect.height - handleRect.height) / 2, 0),
1901 );
1902
1903 // Make sure a drag is eagerly accepted. This is used on iOS to match the
1904 // behavior where a drag directly on a collapse handle will always win against
1905 // other drag gestures.
1906 final bool eagerlyAcceptDragWhenCollapsed = widget.type == TextSelectionHandleType.collapsed && defaultTargetPlatform == TargetPlatform.iOS;
1907
1908 return CompositedTransformFollower(
1909 link: widget.handleLayerLink,
1910 offset: interactiveRect.topLeft,
1911 showWhenUnlinked: false,
1912 child: FadeTransition(
1913 opacity: _opacity,
1914 child: SizedBox(
1915 width: interactiveRect.width,
1916 height: interactiveRect.height,
1917 child: Align(
1918 alignment: Alignment.topLeft,
1919 child: RawGestureDetector(
1920 behavior: HitTestBehavior.translucent,
1921 gestures: <Type, GestureRecognizerFactory>{
1922 PanGestureRecognizer: GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
1923 () => PanGestureRecognizer(
1924 debugOwner: this,
1925 // Mouse events select the text and do not drag the cursor.
1926 supportedDevices: <PointerDeviceKind>{
1927 PointerDeviceKind.touch,
1928 PointerDeviceKind.stylus,
1929 PointerDeviceKind.unknown,
1930 },
1931 ),
1932 (PanGestureRecognizer instance) {
1933 instance
1934 ..dragStartBehavior = widget.dragStartBehavior
1935 ..gestureSettings = eagerlyAcceptDragWhenCollapsed ? const DeviceGestureSettings(touchSlop: 1.0) : null
1936 ..onStart = widget.onSelectionHandleDragStart
1937 ..onUpdate = widget.onSelectionHandleDragUpdate
1938 ..onEnd = widget.onSelectionHandleDragEnd;
1939 },
1940 ),
1941 },
1942 child: Padding(
1943 padding: EdgeInsets.only(
1944 left: padding.left,
1945 top: padding.top,
1946 right: padding.right,
1947 bottom: padding.bottom,
1948 ),
1949 child: widget.selectionControls.buildHandle(
1950 context,
1951 widget.type,
1952 widget.preferredLineHeight,
1953 widget.onSelectionHandleTapped,
1954 ),
1955 ),
1956 ),
1957 ),
1958 ),
1959 ),
1960 );
1961 }
1962}
1963
1964/// Delegate interface for the [TextSelectionGestureDetectorBuilder].
1965///
1966/// The interface is usually implemented by the [State] of text field
1967/// implementations wrapping [EditableText], so that they can use a
1968/// [TextSelectionGestureDetectorBuilder] to build a
1969/// [TextSelectionGestureDetector] for their [EditableText]. The delegate
1970/// provides the builder with information about the current state of the text
1971/// field. Based on that information, the builder adds the correct gesture
1972/// handlers to the gesture detector.
1973///
1974/// See also:
1975///
1976/// * [TextField], which implements this delegate for the Material text field.
1977/// * [CupertinoTextField], which implements this delegate for the Cupertino
1978/// text field.
1979abstract class TextSelectionGestureDetectorBuilderDelegate {
1980 /// [GlobalKey] to the [EditableText] for which the
1981 /// [TextSelectionGestureDetectorBuilder] will build a [TextSelectionGestureDetector].
1982 GlobalKey<EditableTextState> get editableTextKey;
1983
1984 /// Whether the text field should respond to force presses.
1985 bool get forcePressEnabled;
1986
1987 /// Whether the user may select text in the text field.
1988 bool get selectionEnabled;
1989}
1990
1991/// Builds a [TextSelectionGestureDetector] to wrap an [EditableText].
1992///
1993/// The class implements sensible defaults for many user interactions
1994/// with an [EditableText] (see the documentation of the various gesture handler
1995/// methods, e.g. [onTapDown], [onForcePressStart], etc.). Subclasses of
1996/// [TextSelectionGestureDetectorBuilder] can change the behavior performed in
1997/// responds to these gesture events by overriding the corresponding handler
1998/// methods of this class.
1999///
2000/// The resulting [TextSelectionGestureDetector] to wrap an [EditableText] is
2001/// obtained by calling [buildGestureDetector].
2002///
2003/// A [TextSelectionGestureDetectorBuilder] must be provided a
2004/// [TextSelectionGestureDetectorBuilderDelegate], from which information about
2005/// the [EditableText] may be obtained. Typically, the [State] of the widget
2006/// that builds the [EditableText] implements this interface, and then passes
2007/// itself as the [delegate].
2008///
2009/// See also:
2010///
2011/// * [TextField], which uses a subclass to implement the Material-specific
2012/// gesture logic of an [EditableText].
2013/// * [CupertinoTextField], which uses a subclass to implement the
2014/// Cupertino-specific gesture logic of an [EditableText].
2015class TextSelectionGestureDetectorBuilder {
2016 /// Creates a [TextSelectionGestureDetectorBuilder].
2017 TextSelectionGestureDetectorBuilder({
2018 required this.delegate,
2019 });
2020
2021 /// The delegate for this [TextSelectionGestureDetectorBuilder].
2022 ///
2023 /// The delegate provides the builder with information about what actions can
2024 /// currently be performed on the text field. Based on this, the builder adds
2025 /// the correct gesture handlers to the gesture detector.
2026 ///
2027 /// Typically implemented by a [State] of a widget that builds an
2028 /// [EditableText].
2029 @protected
2030 final TextSelectionGestureDetectorBuilderDelegate delegate;
2031
2032 // Shows the magnifier on supported platforms at the given offset, currently
2033 // only Android and iOS.
2034 void _showMagnifierIfSupportedByPlatform(Offset positionToShow) {
2035 switch (defaultTargetPlatform) {
2036 case TargetPlatform.android:
2037 case TargetPlatform.iOS:
2038 editableText.showMagnifier(positionToShow);
2039 case TargetPlatform.fuchsia:
2040 case TargetPlatform.linux:
2041 case TargetPlatform.macOS:
2042 case TargetPlatform.windows:
2043 }
2044 }
2045
2046 // Hides the magnifier on supported platforms, currently only Android and iOS.
2047 void _hideMagnifierIfSupportedByPlatform() {
2048 switch (defaultTargetPlatform) {
2049 case TargetPlatform.android:
2050 case TargetPlatform.iOS:
2051 editableText.hideMagnifier();
2052 case TargetPlatform.fuchsia:
2053 case TargetPlatform.linux:
2054 case TargetPlatform.macOS:
2055 case TargetPlatform.windows:
2056 }
2057 }
2058
2059 /// Returns true if lastSecondaryTapDownPosition was on selection.
2060 bool get _lastSecondaryTapWasOnSelection {
2061 assert(renderEditable.lastSecondaryTapDownPosition != null);
2062 if (renderEditable.selection == null) {
2063 return false;
2064 }
2065
2066 final TextPosition textPosition = renderEditable.getPositionForPoint(
2067 renderEditable.lastSecondaryTapDownPosition!,
2068 );
2069
2070 return renderEditable.selection!.start <= textPosition.offset
2071 && renderEditable.selection!.end >= textPosition.offset;
2072 }
2073
2074 bool _positionWasOnSelectionExclusive(TextPosition textPosition) {
2075 final TextSelection? selection = renderEditable.selection;
2076 if (selection == null) {
2077 return false;
2078 }
2079
2080 return selection.start < textPosition.offset
2081 && selection.end > textPosition.offset;
2082 }
2083
2084 bool _positionWasOnSelectionInclusive(TextPosition textPosition) {
2085 final TextSelection? selection = renderEditable.selection;
2086 if (selection == null) {
2087 return false;
2088 }
2089
2090 return selection.start <= textPosition.offset
2091 && selection.end >= textPosition.offset;
2092 }
2093
2094 // Expand the selection to the given global position.
2095 //
2096 // Either base or extent will be moved to the last tapped position, whichever
2097 // is closest. The selection will never shrink or pivot, only grow.
2098 //
2099 // If fromSelection is given, will expand from that selection instead of the
2100 // current selection in renderEditable.
2101 //
2102 // See also:
2103 //
2104 // * [_extendSelection], which is similar but pivots the selection around
2105 // the base.
2106 void _expandSelection(Offset offset, SelectionChangedCause cause, [TextSelection? fromSelection]) {
2107 assert(renderEditable.selection?.baseOffset != null);
2108
2109 final TextPosition tappedPosition = renderEditable.getPositionForPoint(offset);
2110 final TextSelection selection = fromSelection ?? renderEditable.selection!;
2111 final bool baseIsCloser =
2112 (tappedPosition.offset - selection.baseOffset).abs()
2113 < (tappedPosition.offset - selection.extentOffset).abs();
2114 final TextSelection nextSelection = selection.copyWith(
2115 baseOffset: baseIsCloser ? selection.extentOffset : selection.baseOffset,
2116 extentOffset: tappedPosition.offset,
2117 );
2118
2119 editableText.userUpdateTextEditingValue(
2120 editableText.textEditingValue.copyWith(
2121 selection: nextSelection,
2122 ),
2123 cause,
2124 );
2125 }
2126
2127 // Extend the selection to the given global position.
2128 //
2129 // Holds the base in place and moves the extent.
2130 //
2131 // See also:
2132 //
2133 // * [_expandSelection], which is similar but always increases the size of
2134 // the selection.
2135 void _extendSelection(Offset offset, SelectionChangedCause cause) {
2136 assert(renderEditable.selection?.baseOffset != null);
2137
2138 final TextPosition tappedPosition = renderEditable.getPositionForPoint(offset);
2139 final TextSelection selection = renderEditable.selection!;
2140 final TextSelection nextSelection = selection.copyWith(
2141 extentOffset: tappedPosition.offset,
2142 );
2143
2144 editableText.userUpdateTextEditingValue(
2145 editableText.textEditingValue.copyWith(
2146 selection: nextSelection,
2147 ),
2148 cause,
2149 );
2150 }
2151
2152 /// Whether to show the selection toolbar.
2153 ///
2154 /// It is based on the signal source when a [onTapDown] is called. This getter
2155 /// will return true if current [onTapDown] event is triggered by a touch or
2156 /// a stylus.
2157 bool get shouldShowSelectionToolbar => _shouldShowSelectionToolbar;
2158 bool _shouldShowSelectionToolbar = true;
2159
2160 /// The [State] of the [EditableText] for which the builder will provide a
2161 /// [TextSelectionGestureDetector].
2162 @protected
2163 EditableTextState get editableText => delegate.editableTextKey.currentState!;
2164
2165 /// The [RenderObject] of the [EditableText] for which the builder will
2166 /// provide a [TextSelectionGestureDetector].
2167 @protected
2168 RenderEditable get renderEditable => editableText.renderEditable;
2169
2170 /// Whether the Shift key was pressed when the most recent [PointerDownEvent]
2171 /// was tracked by the [BaseTapAndDragGestureRecognizer].
2172 bool _isShiftPressed = false;
2173
2174 /// The viewport offset pixels of any [Scrollable] containing the
2175 /// [RenderEditable] at the last drag start.
2176 double _dragStartScrollOffset = 0.0;
2177
2178 /// The viewport offset pixels of the [RenderEditable] at the last drag start.
2179 double _dragStartViewportOffset = 0.0;
2180
2181 double get _scrollPosition {
2182 final ScrollableState? scrollableState =
2183 delegate.editableTextKey.currentContext == null
2184 ? null
2185 : Scrollable.maybeOf(delegate.editableTextKey.currentContext!);
2186 return scrollableState == null
2187 ? 0.0
2188 : scrollableState.position.pixels;
2189 }
2190
2191 AxisDirection? get _scrollDirection {
2192 final ScrollableState? scrollableState =
2193 delegate.editableTextKey.currentContext == null
2194 ? null
2195 : Scrollable.maybeOf(delegate.editableTextKey.currentContext!);
2196 return scrollableState?.axisDirection;
2197 }
2198
2199 // For a shift + tap + drag gesture, the TextSelection at the point of the
2200 // tap. Mac uses this value to reset to the original selection when an
2201 // inversion of the base and offset happens.
2202 TextSelection? _dragStartSelection;
2203
2204 // For iOS long press behavior when the field is not focused. iOS uses this value
2205 // to determine if a long press began on a field that was not focused.
2206 //
2207 // If the field was not focused when the long press began, a long press will select
2208 // the word and a long press move will select word-by-word. If the field was
2209 // focused, the cursor moves to the long press position.
2210 bool _longPressStartedWithoutFocus = false;
2211
2212 /// Handler for [TextSelectionGestureDetector.onTapTrackStart].
2213 ///
2214 /// See also:
2215 ///
2216 /// * [TextSelectionGestureDetector.onTapTrackStart], which triggers this
2217 /// callback.
2218 @protected
2219 void onTapTrackStart() {
2220 _isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed
2221 .intersection(<LogicalKeyboardKey>{LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight})
2222 .isNotEmpty;
2223 }
2224
2225 /// Handler for [TextSelectionGestureDetector.onTapTrackReset].
2226 ///
2227 /// See also:
2228 ///
2229 /// * [TextSelectionGestureDetector.onTapTrackReset], which triggers this
2230 /// callback.
2231 @protected
2232 void onTapTrackReset() {
2233 _isShiftPressed = false;
2234 }
2235
2236 /// Handler for [TextSelectionGestureDetector.onTapDown].
2237 ///
2238 /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets
2239 /// [shouldShowSelectionToolbar] to true if the tap was initiated by a finger or stylus.
2240 ///
2241 /// See also:
2242 ///
2243 /// * [TextSelectionGestureDetector.onTapDown], which triggers this callback.
2244 @protected
2245 void onTapDown(TapDragDownDetails details) {
2246 if (!delegate.selectionEnabled) {
2247 return;
2248 }
2249 // TODO(Renzo-Olivares): Migrate text selection gestures away from saving state
2250 // in renderEditable. The gesture callbacks can use the details objects directly
2251 // in callbacks variants that provide them [TapGestureRecognizer.onSecondaryTap]
2252 // vs [TapGestureRecognizer.onSecondaryTapUp] instead of having to track state in
2253 // renderEditable. When this migration is complete we should remove this hack.
2254 // See https://github.com/flutter/flutter/issues/115130.
2255 renderEditable.handleTapDown(TapDownDetails(globalPosition: details.globalPosition));
2256 // The selection overlay should only be shown when the user is interacting
2257 // through a touch screen (via either a finger or a stylus). A mouse shouldn't
2258 // trigger the selection overlay.
2259 // For backwards-compatibility, we treat a null kind the same as touch.
2260 final PointerDeviceKind? kind = details.kind;
2261 // TODO(justinmc): Should a desktop platform show its selection toolbar when
2262 // receiving a tap event? Say a Windows device with a touchscreen.
2263 // https://github.com/flutter/flutter/issues/106586
2264 _shouldShowSelectionToolbar = kind == null
2265 || kind == PointerDeviceKind.touch
2266 || kind == PointerDeviceKind.stylus;
2267
2268 // It is impossible to extend the selection when the shift key is pressed, if the
2269 // renderEditable.selection is invalid.
2270 final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null;
2271 switch (defaultTargetPlatform) {
2272 case TargetPlatform.android:
2273 case TargetPlatform.fuchsia:
2274 case TargetPlatform.iOS:
2275 // On mobile platforms the selection is set on tap up.
2276 break;
2277 case TargetPlatform.macOS:
2278 editableText.hideToolbar();
2279 // On macOS, a shift-tapped unfocused field expands from 0, not from the
2280 // previous selection.
2281 if (isShiftPressedValid) {
2282 final TextSelection? fromSelection = renderEditable.hasFocus
2283 ? null
2284 : const TextSelection.collapsed(offset: 0);
2285 _expandSelection(
2286 details.globalPosition,
2287 SelectionChangedCause.tap,
2288 fromSelection,
2289 );
2290 return;
2291 }
2292 // On macOS, a tap/click places the selection in a precise position.
2293 // This differs from iOS/iPadOS, where if the gesture is done by a touch
2294 // then the selection moves to the closest word edge, instead of a
2295 // precise position.
2296 renderEditable.selectPosition(cause: SelectionChangedCause.tap);
2297 case TargetPlatform.linux:
2298 case TargetPlatform.windows:
2299 editableText.hideToolbar();
2300 if (isShiftPressedValid) {
2301 _extendSelection(details.globalPosition, SelectionChangedCause.tap);
2302 return;
2303 }
2304 renderEditable.selectPosition(cause: SelectionChangedCause.tap);
2305 }
2306 }
2307
2308 /// Handler for [TextSelectionGestureDetector.onForcePressStart].
2309 ///
2310 /// By default, it selects the word at the position of the force press,
2311 /// if selection is enabled.
2312 ///
2313 /// This callback is only applicable when force press is enabled.
2314 ///
2315 /// See also:
2316 ///
2317 /// * [TextSelectionGestureDetector.onForcePressStart], which triggers this
2318 /// callback.
2319 @protected
2320 void onForcePressStart(ForcePressDetails details) {
2321 assert(delegate.forcePressEnabled);
2322 _shouldShowSelectionToolbar = true;
2323 if (!delegate.selectionEnabled) {
2324 return;
2325 }
2326 renderEditable.selectWordsInRange(
2327 from: details.globalPosition,
2328 cause: SelectionChangedCause.forcePress,
2329 );
2330 editableText.showToolbar();
2331 }
2332
2333 /// Handler for [TextSelectionGestureDetector.onForcePressEnd].
2334 ///
2335 /// By default, it selects words in the range specified in [details] and shows
2336 /// toolbar if it is necessary.
2337 ///
2338 /// This callback is only applicable when force press is enabled.
2339 ///
2340 /// See also:
2341 ///
2342 /// * [TextSelectionGestureDetector.onForcePressEnd], which triggers this
2343 /// callback.
2344 @protected
2345 void onForcePressEnd(ForcePressDetails details) {
2346 assert(delegate.forcePressEnabled);
2347 renderEditable.selectWordsInRange(
2348 from: details.globalPosition,
2349 cause: SelectionChangedCause.forcePress,
2350 );
2351 if (shouldShowSelectionToolbar) {
2352 editableText.showToolbar();
2353 }
2354 }
2355
2356 /// Whether the provided [onUserTap] callback should be dispatched on every
2357 /// tap or only non-consecutive taps.
2358 ///
2359 /// Defaults to false.
2360 @protected
2361 bool get onUserTapAlwaysCalled => false;
2362
2363 /// Handler for [TextSelectionGestureDetector.onUserTap].
2364 ///
2365 /// By default, it serves as placeholder to enable subclass override.
2366 ///
2367 /// See also:
2368 ///
2369 /// * [TextSelectionGestureDetector.onUserTap], which triggers this
2370 /// callback.
2371 /// * [TextSelectionGestureDetector.onUserTapAlwaysCalled], which controls
2372 /// whether this callback is called only on the first tap in a series
2373 /// of taps.
2374 @protected
2375 void onUserTap() { /* Subclass should override this method if needed. */ }
2376
2377 /// Handler for [TextSelectionGestureDetector.onSingleTapUp].
2378 ///
2379 /// By default, it selects word edge if selection is enabled.
2380 ///
2381 /// See also:
2382 ///
2383 /// * [TextSelectionGestureDetector.onSingleTapUp], which triggers
2384 /// this callback.
2385 @protected
2386 void onSingleTapUp(TapDragUpDetails details) {
2387 if (!delegate.selectionEnabled) {
2388 editableText.requestKeyboard();
2389 return;
2390 }
2391 // It is impossible to extend the selection when the shift key is pressed, if the
2392 // renderEditable.selection is invalid.
2393 final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null;
2394 switch (defaultTargetPlatform) {
2395 case TargetPlatform.linux:
2396 case TargetPlatform.macOS:
2397 case TargetPlatform.windows:
2398 break;
2399 // On desktop platforms the selection is set on tap down.
2400 case TargetPlatform.android:
2401 editableText.hideToolbar(false);
2402 if (isShiftPressedValid) {
2403 _extendSelection(details.globalPosition, SelectionChangedCause.tap);
2404 return;
2405 }
2406 renderEditable.selectPosition(cause: SelectionChangedCause.tap);
2407 editableText.showSpellCheckSuggestionsToolbar();
2408 case TargetPlatform.fuchsia:
2409 editableText.hideToolbar(false);
2410 if (isShiftPressedValid) {
2411 _extendSelection(details.globalPosition, SelectionChangedCause.tap);
2412 return;
2413 }
2414 renderEditable.selectPosition(cause: SelectionChangedCause.tap);
2415 case TargetPlatform.iOS:
2416 if (isShiftPressedValid) {
2417 // On iOS, a shift-tapped unfocused field expands from 0, not from
2418 // the previous selection.
2419 final TextSelection? fromSelection = renderEditable.hasFocus
2420 ? null
2421 : const TextSelection.collapsed(offset: 0);
2422 _expandSelection(
2423 details.globalPosition,
2424 SelectionChangedCause.tap,
2425 fromSelection,
2426 );
2427 return;
2428 }
2429 switch (details.kind) {
2430 case PointerDeviceKind.mouse:
2431 case PointerDeviceKind.trackpad:
2432 case PointerDeviceKind.stylus:
2433 case PointerDeviceKind.invertedStylus:
2434 // TODO(camsim99): Determine spell check toolbar behavior in these cases:
2435 // https://github.com/flutter/flutter/issues/119573.
2436 // Precise devices should place the cursor at a precise position if the
2437 // word at the text position is not misspelled.
2438 renderEditable.selectPosition(cause: SelectionChangedCause.tap);
2439 case PointerDeviceKind.touch:
2440 case PointerDeviceKind.unknown:
2441 // If the word that was tapped is misspelled, select the word and show the spell check suggestions
2442 // toolbar once. If additional taps are made on a misspelled word, toggle the toolbar. If the word
2443 // is not misspelled, default to the following behavior:
2444 //
2445 // Toggle the toolbar when the tap is exclusively within the bounds of a non-collapsed `previousSelection`,
2446 // and the editable is focused.
2447 //
2448 // Toggle the toolbar if the `previousSelection` is collapsed, the tap is on the selection, the
2449 // TextAffinity remains the same, the editable field is not read only, and the editable is focused.
2450 // The TextAffinity is important when the cursor is on the boundary of a line wrap, if the affinity
2451 // is different (i.e. it is downstream), the selection should move to the following line and not toggle
2452 // the toolbar.
2453 //
2454 // Selects the word edge closest to the tap when the editable is not focused, or if the tap was neither exclusively
2455 // or inclusively on `previousSelection`. If the selection remains the same after selecting the word edge, then we
2456 // toggle the toolbar, if the editable field is not read only. If the selection changes then we hide the toolbar.
2457 final TextSelection previousSelection = renderEditable.selection ?? editableText.textEditingValue.selection;
2458 final TextPosition textPosition = renderEditable.getPositionForPoint(details.globalPosition);
2459 final bool isAffinityTheSame = textPosition.affinity == previousSelection.affinity;
2460 final bool wordAtCursorIndexIsMisspelled = editableText.findSuggestionSpanAtCursorIndex(textPosition.offset) != null;
2461
2462 if (wordAtCursorIndexIsMisspelled) {
2463 renderEditable.selectWord(cause: SelectionChangedCause.tap);
2464 if (previousSelection != editableText.textEditingValue.selection) {
2465 editableText.showSpellCheckSuggestionsToolbar();
2466 } else {
2467 editableText.toggleToolbar(false);
2468 }
2469 } else if (((_positionWasOnSelectionExclusive(textPosition) && !previousSelection.isCollapsed)
2470 || (_positionWasOnSelectionInclusive(textPosition) && previousSelection.isCollapsed && isAffinityTheSame && !renderEditable.readOnly))
2471 && renderEditable.hasFocus) {
2472 editableText.toggleToolbar(false);
2473 } else {
2474 renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
2475 if (previousSelection == editableText.textEditingValue.selection
2476 && renderEditable.hasFocus
2477 && !renderEditable.readOnly) {
2478 editableText.toggleToolbar(false);
2479 } else {
2480 editableText.hideToolbar(false);
2481 }
2482 }
2483 }
2484 }
2485 editableText.requestKeyboard();
2486 }
2487
2488 /// Handler for [TextSelectionGestureDetector.onSingleTapCancel].
2489 ///
2490 /// By default, it serves as placeholder to enable subclass override.
2491 ///
2492 /// See also:
2493 ///
2494 /// * [TextSelectionGestureDetector.onSingleTapCancel], which triggers
2495 /// this callback.
2496 @protected
2497 void onSingleTapCancel() { /* Subclass should override this method if needed. */ }
2498
2499 /// Handler for [TextSelectionGestureDetector.onSingleLongTapStart].
2500 ///
2501 /// By default, it selects text position specified in [details] if selection
2502 /// is enabled.
2503 ///
2504 /// See also:
2505 ///
2506 /// * [TextSelectionGestureDetector.onSingleLongTapStart], which triggers
2507 /// this callback.
2508 @protected
2509 void onSingleLongTapStart(LongPressStartDetails details) {
2510 if (!delegate.selectionEnabled) {
2511 return;
2512 }
2513 switch (defaultTargetPlatform) {
2514 case TargetPlatform.iOS:
2515 case TargetPlatform.macOS:
2516 if (!renderEditable.hasFocus) {
2517 _longPressStartedWithoutFocus = true;
2518 renderEditable.selectWord(cause: SelectionChangedCause.longPress);
2519 } else if (renderEditable.readOnly) {
2520 renderEditable.selectWord(cause: SelectionChangedCause.longPress);
2521 if (editableText.context.mounted) {
2522 Feedback.forLongPress(editableText.context);
2523 }
2524 } else {
2525 renderEditable.selectPositionAt(
2526 from: details.globalPosition,
2527 cause: SelectionChangedCause.longPress,
2528 );
2529 // Show the floating cursor.
2530 final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
2531 state: FloatingCursorDragState.Start,
2532 startLocation: (
2533 renderEditable.globalToLocal(details.globalPosition),
2534 TextPosition(
2535 offset: editableText.textEditingValue.selection.baseOffset,
2536 affinity: editableText.textEditingValue.selection.affinity,
2537 ),
2538 ),
2539 offset: Offset.zero,
2540 );
2541 editableText.updateFloatingCursor(cursorPoint);
2542 }
2543 case TargetPlatform.android:
2544 case TargetPlatform.fuchsia:
2545 case TargetPlatform.linux:
2546 case TargetPlatform.windows:
2547 renderEditable.selectWord(cause: SelectionChangedCause.longPress);
2548 if (editableText.context.mounted) {
2549 Feedback.forLongPress(editableText.context);
2550 }
2551 }
2552
2553 _showMagnifierIfSupportedByPlatform(details.globalPosition);
2554
2555 _dragStartViewportOffset = renderEditable.offset.pixels;
2556 _dragStartScrollOffset = _scrollPosition;
2557 }
2558
2559 /// Handler for [TextSelectionGestureDetector.onSingleLongTapMoveUpdate].
2560 ///
2561 /// By default, it updates the selection location specified in [details] if
2562 /// selection is enabled.
2563 ///
2564 /// See also:
2565 ///
2566 /// * [TextSelectionGestureDetector.onSingleLongTapMoveUpdate], which
2567 /// triggers this callback.
2568 @protected
2569 void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
2570 if (!delegate.selectionEnabled) {
2571 return;
2572 }
2573 // Adjust the drag start offset for possible viewport offset changes.
2574 final Offset editableOffset = renderEditable.maxLines == 1
2575 ? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
2576 : Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset);
2577 final Offset scrollableOffset = switch (axisDirectionToAxis(_scrollDirection ?? AxisDirection.left)) {
2578 Axis.horizontal => Offset(_scrollPosition - _dragStartScrollOffset, 0.0),
2579 Axis.vertical => Offset(0.0, _scrollPosition - _dragStartScrollOffset),
2580 };
2581 switch (defaultTargetPlatform) {
2582 case TargetPlatform.iOS:
2583 case TargetPlatform.macOS:
2584 if (_longPressStartedWithoutFocus || renderEditable.readOnly) {
2585 renderEditable.selectWordsInRange(
2586 from: details.globalPosition - details.offsetFromOrigin - editableOffset - scrollableOffset,
2587 to: details.globalPosition,
2588 cause: SelectionChangedCause.longPress,
2589 );
2590 } else {
2591 renderEditable.selectPositionAt(
2592 from: details.globalPosition,
2593 cause: SelectionChangedCause.longPress,
2594 );
2595 // Update the floating cursor.
2596 final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
2597 state: FloatingCursorDragState.Update,
2598 offset: details.offsetFromOrigin,
2599 );
2600 editableText.updateFloatingCursor(cursorPoint);
2601 }
2602 case TargetPlatform.android:
2603 case TargetPlatform.fuchsia:
2604 case TargetPlatform.linux:
2605 case TargetPlatform.windows:
2606 renderEditable.selectWordsInRange(
2607 from: details.globalPosition - details.offsetFromOrigin - editableOffset - scrollableOffset,
2608 to: details.globalPosition,
2609 cause: SelectionChangedCause.longPress,
2610 );
2611 }
2612
2613 _showMagnifierIfSupportedByPlatform(details.globalPosition);
2614 }
2615
2616 /// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd].
2617 ///
2618 /// By default, it shows toolbar if necessary.
2619 ///
2620 /// See also:
2621 ///
2622 /// * [TextSelectionGestureDetector.onSingleLongTapEnd], which triggers this
2623 /// callback.
2624 @protected
2625 void onSingleLongTapEnd(LongPressEndDetails details) {
2626 _hideMagnifierIfSupportedByPlatform();
2627 if (shouldShowSelectionToolbar) {
2628 editableText.showToolbar();
2629 }
2630 _longPressStartedWithoutFocus = false;
2631 _dragStartViewportOffset = 0.0;
2632 _dragStartScrollOffset = 0.0;
2633 if (defaultTargetPlatform == TargetPlatform.iOS && delegate.selectionEnabled && editableText.textEditingValue.selection.isCollapsed) {
2634 // Update the floating cursor.
2635 final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
2636 state: FloatingCursorDragState.End
2637 );
2638 editableText.updateFloatingCursor(cursorPoint);
2639 }
2640 }
2641
2642 /// Handler for [TextSelectionGestureDetector.onSecondaryTap].
2643 ///
2644 /// By default, selects the word if possible and shows the toolbar.
2645 @protected
2646 void onSecondaryTap() {
2647 if (!delegate.selectionEnabled) {
2648 return;
2649 }
2650 switch (defaultTargetPlatform) {
2651 case TargetPlatform.iOS:
2652 case TargetPlatform.macOS:
2653 if (!_lastSecondaryTapWasOnSelection || !renderEditable.hasFocus) {
2654 renderEditable.selectWord(cause: SelectionChangedCause.tap);
2655 }
2656 if (shouldShowSelectionToolbar) {
2657 editableText.hideToolbar();
2658 editableText.showToolbar();
2659 }
2660 case TargetPlatform.android:
2661 case TargetPlatform.fuchsia:
2662 case TargetPlatform.linux:
2663 case TargetPlatform.windows:
2664 if (!renderEditable.hasFocus) {
2665 renderEditable.selectPosition(cause: SelectionChangedCause.tap);
2666 }
2667 editableText.toggleToolbar();
2668 }
2669 }
2670
2671 /// Handler for [TextSelectionGestureDetector.onSecondaryTapDown].
2672 ///
2673 /// See also:
2674 ///
2675 /// * [TextSelectionGestureDetector.onSecondaryTapDown], which triggers this
2676 /// callback.
2677 /// * [onSecondaryTap], which is typically called after this.
2678 @protected
2679 void onSecondaryTapDown(TapDownDetails details) {
2680 // TODO(Renzo-Olivares): Migrate text selection gestures away from saving state
2681 // in renderEditable. The gesture callbacks can use the details objects directly
2682 // in callbacks variants that provide them [TapGestureRecognizer.onSecondaryTap]
2683 // vs [TapGestureRecognizer.onSecondaryTapUp] instead of having to track state in
2684 // renderEditable. When this migration is complete we should remove this hack.
2685 // See https://github.com/flutter/flutter/issues/115130.
2686 renderEditable.handleSecondaryTapDown(TapDownDetails(globalPosition: details.globalPosition));
2687 _shouldShowSelectionToolbar = true;
2688 }
2689
2690 /// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
2691 ///
2692 /// By default, it selects a word through [RenderEditable.selectWord] if
2693 /// selectionEnabled and shows toolbar if necessary.
2694 ///
2695 /// See also:
2696 ///
2697 /// * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this
2698 /// callback.
2699 @protected
2700 void onDoubleTapDown(TapDragDownDetails details) {
2701 if (delegate.selectionEnabled) {
2702 renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
2703 if (shouldShowSelectionToolbar) {
2704 editableText.showToolbar();
2705 }
2706 }
2707 }
2708
2709 // Selects the set of paragraphs in a document that intersect a given range of
2710 // global positions.
2711 void _selectParagraphsInRange({required Offset from, Offset? to, SelectionChangedCause? cause}) {
2712 final TextBoundary paragraphBoundary = ParagraphBoundary(editableText.textEditingValue.text);
2713 _selectTextBoundariesInRange(boundary: paragraphBoundary, from: from, to: to, cause: cause);
2714 }
2715
2716 // Selects the set of lines in a document that intersect a given range of
2717 // global positions.
2718 void _selectLinesInRange({required Offset from, Offset? to, SelectionChangedCause? cause}) {
2719 final TextBoundary lineBoundary = LineBoundary(renderEditable);
2720 _selectTextBoundariesInRange(boundary: lineBoundary, from: from, to: to, cause: cause);
2721 }
2722
2723 // Returns the location of a text boundary at `extent`. When `extent` is at
2724 // the end of the text, returns the previous text boundary's location.
2725 TextRange _moveToTextBoundary(TextPosition extent, TextBoundary textBoundary) {
2726 assert(extent.offset >= 0);
2727 // Use extent.offset - 1 when `extent` is at the end of the text to retrieve
2728 // the previous text boundary's location.
2729 final int start = textBoundary.getLeadingTextBoundaryAt(extent.offset == editableText.textEditingValue.text.length ? extent.offset - 1 : extent.offset) ?? 0;
2730 final int end = textBoundary.getTrailingTextBoundaryAt(extent.offset) ?? editableText.textEditingValue.text.length;
2731 return TextRange(start: start, end: end);
2732 }
2733
2734 // Selects the set of text boundaries in a document that intersect a given
2735 // range of global positions.
2736 //
2737 // The set of text boundaries selected are not strictly bounded by the range
2738 // of global positions.
2739 //
2740 // The first and last endpoints of the selection will always be at the
2741 // beginning and end of a text boundary respectively.
2742 void _selectTextBoundariesInRange({required TextBoundary boundary, required Offset from, Offset? to, SelectionChangedCause? cause}) {
2743 final TextPosition fromPosition = renderEditable.getPositionForPoint(from);
2744 final TextRange fromRange = _moveToTextBoundary(fromPosition, boundary);
2745 final TextPosition toPosition = to == null
2746 ? fromPosition
2747 : renderEditable.getPositionForPoint(to);
2748 final TextRange toRange = toPosition == fromPosition
2749 ? fromRange
2750 : _moveToTextBoundary(toPosition, boundary);
2751 final bool isFromBoundaryBeforeToBoundary = fromRange.start < toRange.end;
2752
2753 final TextSelection newSelection = isFromBoundaryBeforeToBoundary
2754 ? TextSelection(baseOffset: fromRange.start, extentOffset: toRange.end)
2755 : TextSelection(baseOffset: fromRange.end, extentOffset: toRange.start);
2756
2757 editableText.userUpdateTextEditingValue(
2758 editableText.textEditingValue.copyWith(selection: newSelection),
2759 cause,
2760 );
2761 }
2762
2763 /// Handler for [TextSelectionGestureDetector.onTripleTapDown].
2764 ///
2765 /// By default, it selects a paragraph if
2766 /// [TextSelectionGestureDetectorBuilderDelegate.selectionEnabled] is true
2767 /// and shows the toolbar if necessary.
2768 ///
2769 /// See also:
2770 ///
2771 /// * [TextSelectionGestureDetector.onTripleTapDown], which triggers this
2772 /// callback.
2773 @protected
2774 void onTripleTapDown(TapDragDownDetails details) {
2775 if (!delegate.selectionEnabled) {
2776 return;
2777 }
2778 if (renderEditable.maxLines == 1) {
2779 editableText.selectAll(SelectionChangedCause.tap);
2780 } else {
2781 switch (defaultTargetPlatform) {
2782 case TargetPlatform.android:
2783 case TargetPlatform.fuchsia:
2784 case TargetPlatform.iOS:
2785 case TargetPlatform.macOS:
2786 case TargetPlatform.windows:
2787 _selectParagraphsInRange(from: details.globalPosition, cause: SelectionChangedCause.tap);
2788 case TargetPlatform.linux:
2789 _selectLinesInRange(from: details.globalPosition, cause: SelectionChangedCause.tap);
2790 }
2791 }
2792 if (shouldShowSelectionToolbar) {
2793 editableText.showToolbar();
2794 }
2795 }
2796
2797 /// Handler for [TextSelectionGestureDetector.onDragSelectionStart].
2798 ///
2799 /// By default, it selects a text position specified in [details].
2800 ///
2801 /// See also:
2802 ///
2803 /// * [TextSelectionGestureDetector.onDragSelectionStart], which triggers
2804 /// this callback.
2805 @protected
2806 void onDragSelectionStart(TapDragStartDetails details) {
2807 if (!delegate.selectionEnabled) {
2808 return;
2809 }
2810 final PointerDeviceKind? kind = details.kind;
2811 _shouldShowSelectionToolbar = kind == null
2812 || kind == PointerDeviceKind.touch
2813 || kind == PointerDeviceKind.stylus;
2814
2815 _dragStartSelection = renderEditable.selection;
2816 _dragStartScrollOffset = _scrollPosition;
2817 _dragStartViewportOffset = renderEditable.offset.pixels;
2818
2819 if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) > 1) {
2820 // Do not set the selection on a consecutive tap and drag.
2821 return;
2822 }
2823
2824 if (_isShiftPressed && renderEditable.selection != null && renderEditable.selection!.isValid) {
2825 switch (defaultTargetPlatform) {
2826 case TargetPlatform.iOS:
2827 case TargetPlatform.macOS:
2828 _expandSelection(details.globalPosition, SelectionChangedCause.drag);
2829 case TargetPlatform.android:
2830 case TargetPlatform.fuchsia:
2831 case TargetPlatform.linux:
2832 case TargetPlatform.windows:
2833 _extendSelection(details.globalPosition, SelectionChangedCause.drag);
2834 }
2835 } else {
2836 switch (defaultTargetPlatform) {
2837 case TargetPlatform.iOS:
2838 switch (details.kind) {
2839 case PointerDeviceKind.mouse:
2840 case PointerDeviceKind.trackpad:
2841 renderEditable.selectPositionAt(
2842 from: details.globalPosition,
2843 cause: SelectionChangedCause.drag,
2844 );
2845 case PointerDeviceKind.stylus:
2846 case PointerDeviceKind.invertedStylus:
2847 case PointerDeviceKind.touch:
2848 case PointerDeviceKind.unknown:
2849 case null:
2850 }
2851 case TargetPlatform.android:
2852 case TargetPlatform.fuchsia:
2853 switch (details.kind) {
2854 case PointerDeviceKind.mouse:
2855 case PointerDeviceKind.trackpad:
2856 renderEditable.selectPositionAt(
2857 from: details.globalPosition,
2858 cause: SelectionChangedCause.drag,
2859 );
2860 case PointerDeviceKind.stylus:
2861 case PointerDeviceKind.invertedStylus:
2862 case PointerDeviceKind.touch:
2863 case PointerDeviceKind.unknown:
2864 // For Android, Fuchsia, and iOS platforms, a touch drag
2865 // does not initiate unless the editable has focus.
2866 if (renderEditable.hasFocus) {
2867 renderEditable.selectPositionAt(
2868 from: details.globalPosition,
2869 cause: SelectionChangedCause.drag,
2870 );
2871 _showMagnifierIfSupportedByPlatform(details.globalPosition);
2872 }
2873 case null:
2874 }
2875 case TargetPlatform.linux:
2876 case TargetPlatform.macOS:
2877 case TargetPlatform.windows:
2878 renderEditable.selectPositionAt(
2879 from: details.globalPosition,
2880 cause: SelectionChangedCause.drag,
2881 );
2882 }
2883 }
2884 }
2885
2886 /// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate].
2887 ///
2888 /// By default, it updates the selection location specified in the provided
2889 /// details objects.
2890 ///
2891 /// See also:
2892 ///
2893 /// * [TextSelectionGestureDetector.onDragSelectionUpdate], which triggers
2894 /// this callback./lib/src/material/text_field.dart
2895 @protected
2896 void onDragSelectionUpdate(TapDragUpdateDetails details) {
2897 if (!delegate.selectionEnabled) {
2898 return;
2899 }
2900
2901 if (!_isShiftPressed) {
2902 // Adjust the drag start offset for possible viewport offset changes.
2903 final Offset editableOffset = renderEditable.maxLines == 1
2904 ? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
2905 : Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset);
2906 final Offset scrollableOffset = switch (axisDirectionToAxis(_scrollDirection ?? AxisDirection.left)) {
2907 Axis.horizontal => Offset(_scrollPosition - _dragStartScrollOffset, 0.0),
2908 Axis.vertical => Offset(0.0, _scrollPosition - _dragStartScrollOffset),
2909 };
2910 final Offset dragStartGlobalPosition = details.globalPosition - details.offsetFromOrigin;
2911
2912 // Select word by word.
2913 if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) {
2914 renderEditable.selectWordsInRange(
2915 from: dragStartGlobalPosition - editableOffset - scrollableOffset,
2916 to: details.globalPosition,
2917 cause: SelectionChangedCause.drag,
2918 );
2919
2920 switch (details.kind) {
2921 case PointerDeviceKind.stylus:
2922 case PointerDeviceKind.invertedStylus:
2923 case PointerDeviceKind.touch:
2924 case PointerDeviceKind.unknown:
2925 return _showMagnifierIfSupportedByPlatform(details.globalPosition);
2926 case PointerDeviceKind.mouse:
2927 case PointerDeviceKind.trackpad:
2928 case null:
2929 return;
2930 }
2931 }
2932
2933 // Select paragraph-by-paragraph.
2934 if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 3) {
2935 switch (defaultTargetPlatform) {
2936 case TargetPlatform.android:
2937 case TargetPlatform.fuchsia:
2938 case TargetPlatform.iOS:
2939 switch (details.kind) {
2940 case PointerDeviceKind.mouse:
2941 case PointerDeviceKind.trackpad:
2942 return _selectParagraphsInRange(
2943 from: dragStartGlobalPosition - editableOffset - scrollableOffset,
2944 to: details.globalPosition,
2945 cause: SelectionChangedCause.drag,
2946 );
2947 case PointerDeviceKind.stylus:
2948 case PointerDeviceKind.invertedStylus:
2949 case PointerDeviceKind.touch:
2950 case PointerDeviceKind.unknown:
2951 case null:
2952 // Triple tap to drag is not present on these platforms when using
2953 // non-precise pointer devices at the moment.
2954 break;
2955 }
2956 return;
2957 case TargetPlatform.linux:
2958 return _selectLinesInRange(
2959 from: dragStartGlobalPosition - editableOffset - scrollableOffset,
2960 to: details.globalPosition,
2961 cause: SelectionChangedCause.drag,
2962 );
2963 case TargetPlatform.windows:
2964 case TargetPlatform.macOS:
2965 return _selectParagraphsInRange(
2966 from: dragStartGlobalPosition - editableOffset - scrollableOffset,
2967 to: details.globalPosition,
2968 cause: SelectionChangedCause.drag,
2969 );
2970 }
2971 }
2972
2973 switch (defaultTargetPlatform) {
2974 case TargetPlatform.iOS:
2975 // With a mouse device, a drag should select the range from the origin of the drag
2976 // to the current position of the drag.
2977 //
2978 // With a touch device, nothing should happen.
2979 switch (details.kind) {
2980 case PointerDeviceKind.mouse:
2981 case PointerDeviceKind.trackpad:
2982 return renderEditable.selectPositionAt(
2983 from: dragStartGlobalPosition - editableOffset - scrollableOffset,
2984 to: details.globalPosition,
2985 cause: SelectionChangedCause.drag,
2986 );
2987 case PointerDeviceKind.stylus:
2988 case PointerDeviceKind.invertedStylus:
2989 case PointerDeviceKind.touch:
2990 case PointerDeviceKind.unknown:
2991 case null:
2992 break;
2993 }
2994 return;
2995 case TargetPlatform.android:
2996 case TargetPlatform.fuchsia:
2997 // With a precise pointer device, such as a mouse, trackpad, or stylus,
2998 // the drag will select the text spanning the origin of the drag to the end of the drag.
2999 // With a touch device, the cursor should move with the drag.
3000 switch (details.kind) {
3001 case PointerDeviceKind.mouse:
3002 case PointerDeviceKind.trackpad:
3003 case PointerDeviceKind.stylus:
3004 case PointerDeviceKind.invertedStylus:
3005 return renderEditable.selectPositionAt(
3006 from: dragStartGlobalPosition - editableOffset - scrollableOffset,
3007 to: details.globalPosition,
3008 cause: SelectionChangedCause.drag,
3009 );
3010 case PointerDeviceKind.touch:
3011 case PointerDeviceKind.unknown:
3012 if (renderEditable.hasFocus) {
3013 renderEditable.selectPositionAt(
3014 from: details.globalPosition,
3015 cause: SelectionChangedCause.drag,
3016 );
3017 return _showMagnifierIfSupportedByPlatform(details.globalPosition);
3018 }
3019 case null:
3020 break;
3021 }
3022 return;
3023 case TargetPlatform.macOS:
3024 case TargetPlatform.linux:
3025 case TargetPlatform.windows:
3026 return renderEditable.selectPositionAt(
3027 from: dragStartGlobalPosition - editableOffset - scrollableOffset,
3028 to: details.globalPosition,
3029 cause: SelectionChangedCause.drag,
3030 );
3031 }
3032 }
3033
3034 if (_dragStartSelection!.isCollapsed
3035 || (defaultTargetPlatform != TargetPlatform.iOS
3036 && defaultTargetPlatform != TargetPlatform.macOS)) {
3037 return _extendSelection(details.globalPosition, SelectionChangedCause.drag);
3038 }
3039
3040 // If the drag inverts the selection, Mac and iOS revert to the initial
3041 // selection.
3042 final TextSelection selection = editableText.textEditingValue.selection;
3043 final TextPosition nextExtent = renderEditable.getPositionForPoint(details.globalPosition);
3044 final bool isShiftTapDragSelectionForward =
3045 _dragStartSelection!.baseOffset < _dragStartSelection!.extentOffset;
3046 final bool isInverted = isShiftTapDragSelectionForward
3047 ? nextExtent.offset < _dragStartSelection!.baseOffset
3048 : nextExtent.offset > _dragStartSelection!.baseOffset;
3049 if (isInverted && selection.baseOffset == _dragStartSelection!.baseOffset) {
3050 editableText.userUpdateTextEditingValue(
3051 editableText.textEditingValue.copyWith(
3052 selection: TextSelection(
3053 baseOffset: _dragStartSelection!.extentOffset,
3054 extentOffset: nextExtent.offset,
3055 ),
3056 ),
3057 SelectionChangedCause.drag,
3058 );
3059 } else if (!isInverted
3060 && nextExtent.offset != _dragStartSelection!.baseOffset
3061 && selection.baseOffset != _dragStartSelection!.baseOffset) {
3062 editableText.userUpdateTextEditingValue(
3063 editableText.textEditingValue.copyWith(
3064 selection: TextSelection(
3065 baseOffset: _dragStartSelection!.baseOffset,
3066 extentOffset: nextExtent.offset,
3067 ),
3068 ),
3069 SelectionChangedCause.drag,
3070 );
3071 } else {
3072 _extendSelection(details.globalPosition, SelectionChangedCause.drag);
3073 }
3074 }
3075
3076 /// Handler for [TextSelectionGestureDetector.onDragSelectionEnd].
3077 ///
3078 /// By default, it cleans up the state used for handling certain
3079 /// built-in behaviors.
3080 ///
3081 /// See also:
3082 ///
3083 /// * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this
3084 /// callback.
3085 @protected
3086 void onDragSelectionEnd(TapDragEndDetails details) {
3087 if (_shouldShowSelectionToolbar && _TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) {
3088 editableText.showToolbar();
3089 }
3090
3091 if (_isShiftPressed) {
3092 _dragStartSelection = null;
3093 }
3094
3095 _hideMagnifierIfSupportedByPlatform();
3096 }
3097
3098 /// Returns a [TextSelectionGestureDetector] configured with the handlers
3099 /// provided by this builder.
3100 ///
3101 /// The [child] or its subtree should contain an [EditableText] whose key is
3102 /// the [GlobalKey] provided by the [delegate]'s
3103 /// [TextSelectionGestureDetectorBuilderDelegate.editableTextKey].
3104 Widget buildGestureDetector({
3105 Key? key,
3106 HitTestBehavior? behavior,
3107 required Widget child,
3108 }) {
3109 return TextSelectionGestureDetector(
3110 key: key,
3111 onTapTrackStart: onTapTrackStart,
3112 onTapTrackReset: onTapTrackReset,
3113 onTapDown: onTapDown,
3114 onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
3115 onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
3116 onSecondaryTap: onSecondaryTap,
3117 onSecondaryTapDown: onSecondaryTapDown,
3118 onSingleTapUp: onSingleTapUp,
3119 onSingleTapCancel: onSingleTapCancel,
3120 onUserTap: onUserTap,
3121 onSingleLongTapStart: onSingleLongTapStart,
3122 onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
3123 onSingleLongTapEnd: onSingleLongTapEnd,
3124 onDoubleTapDown: onDoubleTapDown,
3125 onTripleTapDown: onTripleTapDown,
3126 onDragSelectionStart: onDragSelectionStart,
3127 onDragSelectionUpdate: onDragSelectionUpdate,
3128 onDragSelectionEnd: onDragSelectionEnd,
3129 onUserTapAlwaysCalled: onUserTapAlwaysCalled,
3130 behavior: behavior,
3131 child: child,
3132 );
3133 }
3134}
3135
3136/// A gesture detector to respond to non-exclusive event chains for a text field.
3137///
3138/// An ordinary [GestureDetector] configured to handle events like tap and
3139/// double tap will only recognize one or the other. This widget detects both:
3140/// the first tap and then any subsequent taps that occurs within a time limit
3141/// after the first.
3142///
3143/// See also:
3144///
3145/// * [TextField], a Material text field which uses this gesture detector.
3146/// * [CupertinoTextField], a Cupertino text field which uses this gesture
3147/// detector.
3148class TextSelectionGestureDetector extends StatefulWidget {
3149 /// Create a [TextSelectionGestureDetector].
3150 ///
3151 /// Multiple callbacks can be called for one sequence of input gesture.
3152 const TextSelectionGestureDetector({
3153 super.key,
3154 this.onTapTrackStart,
3155 this.onTapTrackReset,
3156 this.onTapDown,
3157 this.onForcePressStart,
3158 this.onForcePressEnd,
3159 this.onSecondaryTap,
3160 this.onSecondaryTapDown,
3161 this.onSingleTapUp,
3162 this.onSingleTapCancel,
3163 this.onUserTap,
3164 this.onSingleLongTapStart,
3165 this.onSingleLongTapMoveUpdate,
3166 this.onSingleLongTapEnd,
3167 this.onDoubleTapDown,
3168 this.onTripleTapDown,
3169 this.onDragSelectionStart,
3170 this.onDragSelectionUpdate,
3171 this.onDragSelectionEnd,
3172 this.onUserTapAlwaysCalled = false,
3173 this.behavior,
3174 required this.child,
3175 });
3176
3177 /// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackStart}
3178 /// Callback used to indicate that a tap tracking has started upon
3179 /// a [PointerDownEvent].
3180 /// {@endtemplate}
3181 final VoidCallback? onTapTrackStart;
3182
3183 /// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackReset}
3184 /// Callback used to indicate that a tap tracking has been reset which
3185 /// happens on the next [PointerDownEvent] after the timer between two taps
3186 /// elapses, the recognizer loses the arena, the gesture is cancelled or
3187 /// the recognizer is disposed of.
3188 /// {@endtemplate}
3189 final VoidCallback? onTapTrackReset;
3190
3191 /// Called for every tap down including every tap down that's part of a
3192 /// double click or a long press, except touches that include enough movement
3193 /// to not qualify as taps (e.g. pans and flings).
3194 final GestureTapDragDownCallback? onTapDown;
3195
3196 /// Called when a pointer has tapped down and the force of the pointer has
3197 /// just become greater than [ForcePressGestureRecognizer.startPressure].
3198 final GestureForcePressStartCallback? onForcePressStart;
3199
3200 /// Called when a pointer that had previously triggered [onForcePressStart] is
3201 /// lifted off the screen.
3202 final GestureForcePressEndCallback? onForcePressEnd;
3203
3204 /// Called for a tap event with the secondary mouse button.
3205 final GestureTapCallback? onSecondaryTap;
3206
3207 /// Called for a tap down event with the secondary mouse button.
3208 final GestureTapDownCallback? onSecondaryTapDown;
3209
3210 /// Called for the first tap in a series of taps, consecutive taps do not call
3211 /// this method.
3212 ///
3213 /// For example, if the detector was configured with [onTapDown] and
3214 /// [onDoubleTapDown], three quick taps would be recognized as a single tap
3215 /// down, followed by a tap up, then a double tap down, followed by a single tap down.
3216 final GestureTapDragUpCallback? onSingleTapUp;
3217
3218 /// Called for each touch that becomes recognized as a gesture that is not a
3219 /// short tap, such as a long tap or drag. It is called at the moment when
3220 /// another gesture from the touch is recognized.
3221 final GestureCancelCallback? onSingleTapCancel;
3222
3223 /// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is
3224 /// disabled, which is the default behavior.
3225 ///
3226 /// When [onUserTapAlwaysCalled] is enabled, this is called for every tap,
3227 /// including consecutive taps.
3228 final GestureTapCallback? onUserTap;
3229
3230 /// Called for a single long tap that's sustained for longer than
3231 /// [kLongPressTimeout] but not necessarily lifted. Not called for a
3232 /// double-tap-hold, which calls [onDoubleTapDown] instead.
3233 final GestureLongPressStartCallback? onSingleLongTapStart;
3234
3235 /// Called after [onSingleLongTapStart] when the pointer is dragged.
3236 final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate;
3237
3238 /// Called after [onSingleLongTapStart] when the pointer is lifted.
3239 final GestureLongPressEndCallback? onSingleLongTapEnd;
3240
3241 /// Called after a momentary hold or a short tap that is close in space and
3242 /// time (within [kDoubleTapTimeout]) to a previous short tap.
3243 final GestureTapDragDownCallback? onDoubleTapDown;
3244
3245 /// Called after a momentary hold or a short tap that is close in space and
3246 /// time (within [kDoubleTapTimeout]) to a previous double-tap.
3247 final GestureTapDragDownCallback? onTripleTapDown;
3248
3249 /// Called when a mouse starts dragging to select text.
3250 final GestureTapDragStartCallback? onDragSelectionStart;
3251
3252 /// Called repeatedly as a mouse moves while dragging.
3253 final GestureTapDragUpdateCallback? onDragSelectionUpdate;
3254
3255 /// Called when a mouse that was previously dragging is released.
3256 final GestureTapDragEndCallback? onDragSelectionEnd;
3257
3258 /// Whether [onUserTap] will be called for all taps including consecutive taps.
3259 ///
3260 /// Defaults to false, so [onUserTap] is only called for each distinct tap.
3261 final bool onUserTapAlwaysCalled;
3262
3263 /// How this gesture detector should behave during hit testing.
3264 ///
3265 /// This defaults to [HitTestBehavior.deferToChild].
3266 final HitTestBehavior? behavior;
3267
3268 /// Child below this widget.
3269 final Widget child;
3270
3271 @override
3272 State<StatefulWidget> createState() => _TextSelectionGestureDetectorState();
3273}
3274
3275class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetector> {
3276
3277 // Converts the details.consecutiveTapCount from a TapAndDrag*Details object,
3278 // which can grow to be infinitely large, to a value between 1 and 3. The value
3279 // that the raw count is converted to is based on the default observed behavior
3280 // on the native platforms.
3281 //
3282 // This method should be used in all instances when details.consecutiveTapCount
3283 // would be used.
3284 static int _getEffectiveConsecutiveTapCount(int rawCount) {
3285 switch (defaultTargetPlatform) {
3286 case TargetPlatform.android:
3287 case TargetPlatform.fuchsia:
3288 case TargetPlatform.linux:
3289 // From observation, these platform's reset their tap count to 0 when
3290 // the number of consecutive taps exceeds 3. For example on Debian Linux
3291 // with GTK, when going past a triple click, on the fourth click the
3292 // selection is moved to the precise click position, on the fifth click
3293 // the word at the position is selected, and on the sixth click the
3294 // paragraph at the position is selected.
3295 return rawCount <= 3 ? rawCount : (rawCount % 3 == 0 ? 3 : rawCount % 3);
3296 case TargetPlatform.iOS:
3297 case TargetPlatform.macOS:
3298 // From observation, these platform's either hold their tap count at 3.
3299 // For example on macOS, when going past a triple click, the selection
3300 // should be retained at the paragraph that was first selected on triple
3301 // click.
3302 return math.min(rawCount, 3);
3303 case TargetPlatform.windows:
3304 // From observation, this platform's consecutive tap actions alternate
3305 // between double click and triple click actions. For example, after a
3306 // triple click has selected a paragraph, on the next click the word at
3307 // the clicked position will be selected, and on the next click the
3308 // paragraph at the position is selected.
3309 return rawCount < 2 ? rawCount : 2 + rawCount % 2;
3310 }
3311 }
3312
3313 void _handleTapTrackStart() {
3314 widget.onTapTrackStart?.call();
3315 }
3316
3317 void _handleTapTrackReset() {
3318 widget.onTapTrackReset?.call();
3319 }
3320
3321 // The down handler is force-run on success of a single tap and optimistically
3322 // run before a long press success.
3323 void _handleTapDown(TapDragDownDetails details) {
3324 widget.onTapDown?.call(details);
3325 // This isn't detected as a double tap gesture in the gesture recognizer
3326 // because it's 2 single taps, each of which may do different things depending
3327 // on whether it's a single tap, the first tap of a double tap, the second
3328 // tap held down, a clean double tap etc.
3329 if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) {
3330 return widget.onDoubleTapDown?.call(details);
3331 }
3332
3333 if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 3) {
3334 return widget.onTripleTapDown?.call(details);
3335 }
3336 }
3337
3338 void _handleTapUp(TapDragUpDetails details) {
3339 if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) {
3340 widget.onSingleTapUp?.call(details);
3341 widget.onUserTap?.call();
3342 } else if (widget.onUserTapAlwaysCalled) {
3343 widget.onUserTap?.call();
3344 }
3345 }
3346
3347 void _handleTapCancel() {
3348 widget.onSingleTapCancel?.call();
3349 }
3350
3351 void _handleDragStart(TapDragStartDetails details) {
3352 widget.onDragSelectionStart?.call(details);
3353 }
3354
3355 void _handleDragUpdate(TapDragUpdateDetails details) {
3356 widget.onDragSelectionUpdate?.call(details);
3357 }
3358
3359 void _handleDragEnd(TapDragEndDetails details) {
3360 widget.onDragSelectionEnd?.call(details);
3361 }
3362
3363 void _forcePressStarted(ForcePressDetails details) {
3364 widget.onForcePressStart?.call(details);
3365 }
3366
3367 void _forcePressEnded(ForcePressDetails details) {
3368 widget.onForcePressEnd?.call(details);
3369 }
3370
3371 void _handleLongPressStart(LongPressStartDetails details) {
3372 if (widget.onSingleLongTapStart != null) {
3373 widget.onSingleLongTapStart!(details);
3374 }
3375 }
3376
3377 void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
3378 if (widget.onSingleLongTapMoveUpdate != null) {
3379 widget.onSingleLongTapMoveUpdate!(details);
3380 }
3381 }
3382
3383 void _handleLongPressEnd(LongPressEndDetails details) {
3384 if (widget.onSingleLongTapEnd != null) {
3385 widget.onSingleLongTapEnd!(details);
3386 }
3387 }
3388
3389 @override
3390 Widget build(BuildContext context) {
3391 final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
3392
3393 gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
3394 () => TapGestureRecognizer(debugOwner: this),
3395 (TapGestureRecognizer instance) {
3396 instance
3397 ..onSecondaryTap = widget.onSecondaryTap
3398 ..onSecondaryTapDown = widget.onSecondaryTapDown;
3399 },
3400 );
3401
3402 if (widget.onSingleLongTapStart != null ||
3403 widget.onSingleLongTapMoveUpdate != null ||
3404 widget.onSingleLongTapEnd != null) {
3405 gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
3406 () => LongPressGestureRecognizer(debugOwner: this, supportedDevices: <PointerDeviceKind>{ PointerDeviceKind.touch }),
3407 (LongPressGestureRecognizer instance) {
3408 instance
3409 ..onLongPressStart = _handleLongPressStart
3410 ..onLongPressMoveUpdate = _handleLongPressMoveUpdate
3411 ..onLongPressEnd = _handleLongPressEnd;
3412 },
3413 );
3414 }
3415
3416 if (widget.onDragSelectionStart != null ||
3417 widget.onDragSelectionUpdate != null ||
3418 widget.onDragSelectionEnd != null) {
3419 switch (defaultTargetPlatform) {
3420 case TargetPlatform.android:
3421 case TargetPlatform.fuchsia:
3422 case TargetPlatform.iOS:
3423 gestures[TapAndHorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndHorizontalDragGestureRecognizer>(
3424 () => TapAndHorizontalDragGestureRecognizer(debugOwner: this),
3425 (TapAndHorizontalDragGestureRecognizer instance) {
3426 instance
3427 // Text selection should start from the position of the first pointer
3428 // down event.
3429 ..dragStartBehavior = DragStartBehavior.down
3430 ..eagerVictoryOnDrag = defaultTargetPlatform != TargetPlatform.iOS
3431 ..onTapTrackStart = _handleTapTrackStart
3432 ..onTapTrackReset = _handleTapTrackReset
3433 ..onTapDown = _handleTapDown
3434 ..onDragStart = _handleDragStart
3435 ..onDragUpdate = _handleDragUpdate
3436 ..onDragEnd = _handleDragEnd
3437 ..onTapUp = _handleTapUp
3438 ..onCancel = _handleTapCancel;
3439 },
3440 );
3441 case TargetPlatform.linux:
3442 case TargetPlatform.macOS:
3443 case TargetPlatform.windows:
3444 gestures[TapAndPanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
3445 () => TapAndPanGestureRecognizer(debugOwner: this),
3446 (TapAndPanGestureRecognizer instance) {
3447 instance
3448 // Text selection should start from the position of the first pointer
3449 // down event.
3450 ..dragStartBehavior = DragStartBehavior.down
3451 ..onTapTrackStart = _handleTapTrackStart
3452 ..onTapTrackReset = _handleTapTrackReset
3453 ..onTapDown = _handleTapDown
3454 ..onDragStart = _handleDragStart
3455 ..onDragUpdate = _handleDragUpdate
3456 ..onDragEnd = _handleDragEnd
3457 ..onTapUp = _handleTapUp
3458 ..onCancel = _handleTapCancel;
3459 },
3460 );
3461 }
3462 }
3463
3464 if (widget.onForcePressStart != null || widget.onForcePressEnd != null) {
3465 gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
3466 () => ForcePressGestureRecognizer(debugOwner: this),
3467 (ForcePressGestureRecognizer instance) {
3468 instance
3469 ..onStart = widget.onForcePressStart != null ? _forcePressStarted : null
3470 ..onEnd = widget.onForcePressEnd != null ? _forcePressEnded : null;
3471 },
3472 );
3473 }
3474
3475 return RawGestureDetector(
3476 gestures: gestures,
3477 excludeFromSemantics: true,
3478 behavior: widget.behavior,
3479 child: widget.child,
3480 );
3481 }
3482}
3483
3484/// A [ValueNotifier] whose [value] indicates whether the current contents of
3485/// the clipboard can be pasted.
3486///
3487/// The contents of the clipboard can only be read asynchronously, via
3488/// [Clipboard.getData], so this maintains a value that can be used
3489/// synchronously. Call [update] to asynchronously update value if needed.
3490class ClipboardStatusNotifier extends ValueNotifier<ClipboardStatus> with WidgetsBindingObserver {
3491 /// Create a new ClipboardStatusNotifier.
3492 ClipboardStatusNotifier({
3493 ClipboardStatus value = ClipboardStatus.unknown,
3494 }) : super(value);
3495
3496 bool _disposed = false;
3497
3498 /// Check the [Clipboard] and update [value] if needed.
3499 Future<void> update() async {
3500 if (_disposed) {
3501 return;
3502 }
3503
3504 final bool hasStrings;
3505 try {
3506 hasStrings = await Clipboard.hasStrings();
3507 } catch (exception, stack) {
3508 FlutterError.reportError(FlutterErrorDetails(
3509 exception: exception,
3510 stack: stack,
3511 library: 'widget library',
3512 context: ErrorDescription('while checking if the clipboard has strings'),
3513 ));
3514 // In the case of an error from the Clipboard API, set the value to
3515 // unknown so that it will try to update again later.
3516 if (_disposed) {
3517 return;
3518 }
3519 value = ClipboardStatus.unknown;
3520 return;
3521 }
3522 final ClipboardStatus nextStatus = hasStrings
3523 ? ClipboardStatus.pasteable
3524 : ClipboardStatus.notPasteable;
3525
3526 if (_disposed) {
3527 return;
3528 }
3529 value = nextStatus;
3530 }
3531
3532 @override
3533 void addListener(VoidCallback listener) {
3534 if (!hasListeners) {
3535 WidgetsBinding.instance.addObserver(this);
3536 }
3537 if (value == ClipboardStatus.unknown) {
3538 update();
3539 }
3540 super.addListener(listener);
3541 }
3542
3543 @override
3544 void removeListener(VoidCallback listener) {
3545 super.removeListener(listener);
3546 if (!_disposed && !hasListeners) {
3547 WidgetsBinding.instance.removeObserver(this);
3548 }
3549 }
3550
3551 @override
3552 void didChangeAppLifecycleState(AppLifecycleState state) {
3553 switch (state) {
3554 case AppLifecycleState.resumed:
3555 update();
3556 case AppLifecycleState.detached:
3557 case AppLifecycleState.inactive:
3558 case AppLifecycleState.hidden:
3559 case AppLifecycleState.paused:
3560 // Nothing to do.
3561 break;
3562 }
3563 }
3564
3565 @override
3566 void dispose() {
3567 WidgetsBinding.instance.removeObserver(this);
3568 _disposed = true;
3569 super.dispose();
3570 }
3571}
3572
3573/// An enumeration of the status of the content on the user's clipboard.
3574enum ClipboardStatus {
3575 /// The clipboard content can be pasted, such as a String of nonzero length.
3576 pasteable,
3577
3578 /// The status of the clipboard is unknown. Since getting clipboard data is
3579 /// asynchronous (see [Clipboard.getData]), this status often exists while
3580 /// waiting to receive the clipboard contents for the first time.
3581 unknown,
3582
3583 /// The content on the clipboard is not pasteable, such as when it is empty.
3584 notPasteable,
3585}
3586
3587/// A [ValueNotifier] whose [value] indicates whether the current device supports the Live Text
3588/// (OCR) function.
3589///
3590/// See also:
3591/// * [LiveText], where the availability of Live Text input can be obtained.
3592/// * [LiveTextInputStatus], an enumeration that indicates whether the current device is available
3593/// for Live Text input.
3594///
3595/// Call [update] to asynchronously update [value] if needed.
3596class LiveTextInputStatusNotifier extends ValueNotifier<LiveTextInputStatus> with WidgetsBindingObserver {
3597 /// Create a new LiveTextStatusNotifier.
3598 LiveTextInputStatusNotifier({
3599 LiveTextInputStatus value = LiveTextInputStatus.unknown,
3600 }) : super(value);
3601
3602 bool _disposed = false;
3603
3604 /// Check the [LiveTextInputStatus] and update [value] if needed.
3605 Future<void> update() async {
3606 if (_disposed) {
3607 return;
3608 }
3609
3610 final bool isLiveTextInputEnabled;
3611 try {
3612 isLiveTextInputEnabled = await LiveText.isLiveTextInputAvailable();
3613 } catch (exception, stack) {
3614 FlutterError.reportError(FlutterErrorDetails(
3615 exception: exception,
3616 stack: stack,
3617 library: 'widget library',
3618 context: ErrorDescription('while checking the availability of Live Text input'),
3619 ));
3620 // In the case of an error from the Live Text API, set the value to
3621 // unknown so that it will try to update again later.
3622 if (_disposed || value == LiveTextInputStatus.unknown) {
3623 return;
3624 }
3625 value = LiveTextInputStatus.unknown;
3626 return;
3627 }
3628
3629 final LiveTextInputStatus nextStatus = isLiveTextInputEnabled
3630 ? LiveTextInputStatus.enabled
3631 : LiveTextInputStatus.disabled;
3632
3633 if (_disposed || nextStatus == value) {
3634 return;
3635 }
3636 value = nextStatus;
3637 }
3638
3639 @override
3640 void addListener(VoidCallback listener) {
3641 if (!hasListeners) {
3642 WidgetsBinding.instance.addObserver(this);
3643 }
3644 if (value == LiveTextInputStatus.unknown) {
3645 update();
3646 }
3647 super.addListener(listener);
3648 }
3649
3650 @override
3651 void removeListener(VoidCallback listener) {
3652 super.removeListener(listener);
3653 if (!_disposed && !hasListeners) {
3654 WidgetsBinding.instance.removeObserver(this);
3655 }
3656 }
3657
3658 @override
3659 void didChangeAppLifecycleState(AppLifecycleState state) {
3660 switch (state) {
3661 case AppLifecycleState.resumed:
3662 update();
3663 case AppLifecycleState.detached:
3664 case AppLifecycleState.inactive:
3665 case AppLifecycleState.paused:
3666 case AppLifecycleState.hidden:
3667 // Nothing to do.
3668 }
3669 }
3670
3671 @override
3672 void dispose() {
3673 WidgetsBinding.instance.removeObserver(this);
3674 _disposed = true;
3675 super.dispose();
3676 }
3677}
3678
3679/// An enumeration that indicates whether the current device is available for Live Text input.
3680///
3681/// See also:
3682/// * [LiveText], where the availability of Live Text input can be obtained.
3683enum LiveTextInputStatus {
3684 /// This device supports Live Text input currently.
3685 enabled,
3686
3687 /// The status of the Live Text input is unknown. Since getting the Live Text input availability
3688 /// is asynchronous (see [LiveText.isLiveTextInputAvailable]), this status often exists while
3689 /// waiting to receive the status value for the first time.
3690 unknown,
3691
3692 /// The current device doesn't support Live Text input.
3693 disabled,
3694}
3695
3696// TODO(justinmc): Deprecate this after TextSelectionControls.buildToolbar is
3697// deleted, when users should migrate back to TextSelectionControls.buildHandle.
3698// See https://github.com/flutter/flutter/pull/124262
3699/// [TextSelectionControls] that specifically do not manage the toolbar in order
3700/// to leave that to [EditableText.contextMenuBuilder].
3701mixin TextSelectionHandleControls on TextSelectionControls {
3702 @override
3703 Widget buildToolbar(
3704 BuildContext context,
3705 Rect globalEditableRegion,
3706 double textLineHeight,
3707 Offset selectionMidpoint,
3708 List<TextSelectionPoint> endpoints,
3709 TextSelectionDelegate delegate,
3710 ValueListenable<ClipboardStatus>? clipboardStatus,
3711 Offset? lastSecondaryTapDownPosition,
3712 ) => const SizedBox.shrink();
3713
3714 @override
3715 bool canCut(TextSelectionDelegate delegate) => false;
3716
3717 @override
3718 bool canCopy(TextSelectionDelegate delegate) => false;
3719
3720 @override
3721 bool canPaste(TextSelectionDelegate delegate) => false;
3722
3723 @override
3724 bool canSelectAll(TextSelectionDelegate delegate) => false;
3725
3726 @override
3727 void handleCut(TextSelectionDelegate delegate, [ClipboardStatusNotifier? clipboardStatus]) {}
3728
3729 @override
3730 void handleCopy(TextSelectionDelegate delegate, [ClipboardStatusNotifier? clipboardStatus]) {}
3731
3732 @override
3733 Future<void> handlePaste(TextSelectionDelegate delegate) async {}
3734
3735 @override
3736 void handleSelectAll(TextSelectionDelegate delegate) {}
3737}
3738