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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com