text_selection.dart 59.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

xster's avatar
xster committed
5
import 'dart:async';
6
import 'dart:math' as math;
xster's avatar
xster committed
7

8 9
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
10
import 'package:flutter/rendering.dart';
11
import 'package:flutter/scheduler.dart';
12
import 'package:flutter/services.dart';
13 14

import 'basic.dart';
15
import 'binding.dart';
16
import 'constants.dart';
17
import 'container.dart';
18
import 'editable_text.dart';
19 20 21
import 'framework.dart';
import 'gesture_detector.dart';
import 'overlay.dart';
22
import 'ticker_provider.dart';
23
import 'transitions.dart';
24
import 'visibility.dart';
25

jslavitz's avatar
jslavitz committed
26 27
export 'package:flutter/services.dart' show TextSelectionDelegate;

28 29 30 31
/// A duration that controls how often the drag selection update callback is
/// called.
const Duration _kDragSelectionUpdateThrottle = Duration(milliseconds: 50);

32 33 34 35
/// Which type of selection handle to be displayed.
///
/// With mixed-direction text, both handles may be the same type. Examples:
///
36 37 38 39 40
/// * LTR text: 'the <quick brown> fox':
///
///   The '<' is drawn with the [left] type, the '>' with the [right]
///
/// * RTL text: 'XOF <NWORB KCIUQ> EHT':
41
///
42
///   Same as above.
43
///
44 45 46 47 48 49 50 51 52
/// * mixed text: '<the NWOR<B KCIUQ fox'
///
///   Here 'the QUICK B' is selected, but 'QUICK BROWN' is RTL. Both are drawn
///   with the [left] type.
///
/// See also:
///
///  * [TextDirection], which discusses left-to-right and right-to-left text in
///    more detail.
53 54 55
enum TextSelectionHandleType {
  /// The selection handle is to the left of the selection end point.
  left,
56

57 58 59 60 61 62 63
  /// The selection handle is to the right of the selection end point.
  right,

  /// The start and end of the selection are co-incident at this point.
  collapsed,
}

64 65 66 67
/// The text position that a give selection handle manipulates. Dragging the
/// [start] handle always moves the [start]/[baseOffset] of the selection.
enum _TextSelectionHandlePosition { start, end }

68 69 70 71 72 73 74 75 76 77 78 79 80
/// Signature for when a pointer that's dragging to select text has moved again.
///
/// The first argument [startDetails] contains the details of the event that
/// initiated the dragging.
///
/// The second argument [updateDetails] contains the details of the current
/// pointer movement. It's the same as the one passed to [DragGestureRecognizer.onUpdate].
///
/// This signature is different from [GestureDragUpdateCallback] to make it
/// easier for various text fields to use [TextSelectionGestureDetector] without
/// having to store the start position.
typedef DragSelectionUpdateCallback = void Function(DragStartDetails startDetails, DragUpdateDetails updateDetails);

81 82 83 84 85 86 87 88 89 90 91
/// The type for a Function that builds a toolbar's container with the given
/// child.
///
/// See also:
///
///   * [TextSelectionToolbar.toolbarBuilder], which is of this type.
///     type.
///   * [CupertinoTextSelectionToolbar.toolbarBuilder], which is similar, but
///     for a Cupertino-style toolbar.
typedef ToolbarBuilder = Widget Function(BuildContext context, Widget child);

92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
/// ParentData that determines whether or not to paint the corresponding child.
///
/// Used in the layout of the Cupertino and Material text selection menus, which
/// decide whether or not to paint their buttons after laying them out and
/// determining where they overflow.
class ToolbarItemsParentData extends ContainerBoxParentData<RenderBox> {
  /// Whether or not this child is painted.
  ///
  /// Children in the selection toolbar may be laid out for measurement purposes
  /// but not painted. This allows these children to be identified.
  bool shouldPaint = false;

  @override
  String toString() => '${super.toString()}; shouldPaint=$shouldPaint';
}

108
/// An interface for building the selection UI, to be provided by the
109
/// implementer of the toolbar widget.
xster's avatar
xster committed
110 111
///
/// Override text operations such as [handleCut] if needed.
112
abstract class TextSelectionControls {
113
  /// Builds a selection handle of the given `type`.
xster's avatar
xster committed
114 115 116
  ///
  /// The top left corner of this widget is positioned at the bottom of the
  /// selection position.
117 118 119 120 121
  ///
  /// The supplied [onTap] should be invoked when the handle is tapped, if such
  /// interaction is allowed. As a counterexample, the default selection handle
  /// on iOS [cupertinoTextSelectionControls] does not call [onTap] at all,
  /// since its handles are not meant to be tapped.
122
  Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]);
123

124 125 126 127 128
  /// Get the anchor point of the handle relative to itself. The anchor point is
  /// the point that is aligned with a specific point in the text. A handle
  /// often visually "points to" that location.
  Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight);

129 130 131
  /// Builds a toolbar near a text selection.
  ///
  /// Typically displays buttons for copying and pasting text.
132 133 134 135
  ///
  /// [globalEditableRegion] is the TextField size of the global coordinate system
  /// in logical pixels.
  ///
136 137 138
  /// [textLineHeight] is the `preferredLineHeight` of the [RenderEditable] we
  /// are building a toolbar for.
  ///
139 140 141 142 143 144
  /// The [position] is a general calculation midpoint parameter of the toolbar.
  /// If you want more detailed position information, can use [endpoints]
  /// to calculate it.
  Widget buildToolbar(
    BuildContext context,
    Rect globalEditableRegion,
145
    double textLineHeight,
146 147 148
    Offset position,
    List<TextSelectionPoint> endpoints,
    TextSelectionDelegate delegate,
149
    ClipboardStatusNotifier clipboardStatus,
150
    Offset? lastSecondaryTapDownPosition,
151
  );
152 153

  /// Returns the size of the selection handle.
154
  Size getHandleSize(double textLineHeight);
xster's avatar
xster committed
155

156 157 158 159 160 161 162 163 164
  /// Whether the current selection of the text field managed by the given
  /// `delegate` can be removed from the text field and placed into the
  /// [Clipboard].
  ///
  /// By default, false is returned when nothing is selected in the text field.
  ///
  /// Subclasses can use this to decide if they should expose the cut
  /// functionality to the user.
  bool canCut(TextSelectionDelegate delegate) {
165
    return delegate.cutEnabled && !delegate.textEditingValue.selection.isCollapsed;
166 167 168 169 170 171 172 173 174 175
  }

  /// Whether the current selection of the text field managed by the given
  /// `delegate` can be copied to the [Clipboard].
  ///
  /// By default, false is returned when nothing is selected in the text field.
  ///
  /// Subclasses can use this to decide if they should expose the copy
  /// functionality to the user.
  bool canCopy(TextSelectionDelegate delegate) {
176
    return delegate.copyEnabled && !delegate.textEditingValue.selection.isCollapsed;
177 178
  }

179 180
  /// Whether the text field managed by the given `delegate` supports pasting
  /// from the clipboard.
181 182 183
  ///
  /// Subclasses can use this to decide if they should expose the paste
  /// functionality to the user.
184 185 186 187
  ///
  /// This does not consider the contents of the clipboard. Subclasses may want
  /// to, for example, disallow pasting when the clipboard contains an empty
  /// string.
188
  bool canPaste(TextSelectionDelegate delegate) {
189
    return delegate.pasteEnabled;
190 191
  }

192
  /// Whether the current selection of the text field managed by the given
193 194 195 196 197 198
  /// `delegate` can be extended to include the entire content of the text
  /// field.
  ///
  /// Subclasses can use this to decide if they should expose the select all
  /// functionality to the user.
  bool canSelectAll(TextSelectionDelegate delegate) {
199
    return delegate.selectAllEnabled && delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
200 201
  }

202 203 204
  // TODO(justinmc): This and other methods should be ported to Actions and
  // removed, along with their keyboard shortcut equivalents.
  // https://github.com/flutter/flutter/issues/75004
205 206 207 208 209 210
  /// Copy the current selection of the text field managed by the given
  /// `delegate` to the [Clipboard]. Then, remove the selected text from the
  /// text field and hide the toolbar.
  ///
  /// This is called by subclasses when their cut affordance is activated by
  /// the user.
xster's avatar
xster committed
211 212
  void handleCut(TextSelectionDelegate delegate) {
    final TextEditingValue value = delegate.textEditingValue;
213
    Clipboard.setData(ClipboardData(
xster's avatar
xster committed
214 215
      text: value.selection.textInside(value.text),
    ));
216 217 218 219 220
    delegate.userUpdateTextEditingValue(
      TextEditingValue(
        text: value.selection.textBefore(value.text)
            + value.selection.textAfter(value.text),
        selection: TextSelection.collapsed(
221 222
          offset: value.selection.start,
        ),
xster's avatar
xster committed
223
      ),
224
      SelectionChangedCause.toolBar,
xster's avatar
xster committed
225
    );
226
    delegate.bringIntoView(delegate.textEditingValue.selection.extent);
xster's avatar
xster committed
227 228 229
    delegate.hideToolbar();
  }

230 231 232 233 234 235
  /// Copy the current selection of the text field managed by the given
  /// `delegate` to the [Clipboard]. Then, move the cursor to the end of the
  /// text (collapsing the selection in the process), and hide the toolbar.
  ///
  /// This is called by subclasses when their copy affordance is activated by
  /// the user.
236
  void handleCopy(TextSelectionDelegate delegate, ClipboardStatusNotifier? clipboardStatus) {
xster's avatar
xster committed
237
    final TextEditingValue value = delegate.textEditingValue;
238
    Clipboard.setData(ClipboardData(
xster's avatar
xster committed
239 240
      text: value.selection.textInside(value.text),
    ));
241
    clipboardStatus?.update();
242
    delegate.bringIntoView(delegate.textEditingValue.selection.extent);
243 244 245 246 247 248 249 250 251 252 253 254

    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
        // Hide the toolbar, but keep the selection and keep the handles.
        delegate.hideToolbar(false);
        return;
      case TargetPlatform.macOS:
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        // Collapse the selection and hide the toolbar and handles.
255 256 257 258 259 260
        delegate.userUpdateTextEditingValue(
          TextEditingValue(
            text: value.text,
            selection: TextSelection.collapsed(offset: value.selection.end),
          ),
          SelectionChangedCause.toolBar,
261 262 263 264
        );
        delegate.hideToolbar();
        return;
    }
xster's avatar
xster committed
265 266
  }

267 268 269 270 271 272 273 274 275 276 277
  /// Paste the current clipboard selection (obtained from [Clipboard]) into
  /// the text field managed by the given `delegate`, replacing its current
  /// selection, if any. Then, hide the toolbar.
  ///
  /// This is called by subclasses when their paste affordance is activated by
  /// the user.
  ///
  /// This function is asynchronous since interacting with the clipboard is
  /// asynchronous. Race conditions may exist with this API as currently
  /// implemented.
  // TODO(ianh): https://github.com/flutter/flutter/issues/11427
278
  Future<void> handlePaste(TextSelectionDelegate delegate) async {
279
    final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`.
280
    final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
xster's avatar
xster committed
281
    if (data != null) {
282 283 284 285 286 287
      delegate.userUpdateTextEditingValue(
        TextEditingValue(
          text: value.selection.textBefore(value.text)
              + data.text!
              + value.selection.textAfter(value.text),
          selection: TextSelection.collapsed(
288
              offset: value.selection.start + data.text!.length,
289
          ),
xster's avatar
xster committed
290
        ),
291
        SelectionChangedCause.toolBar,
xster's avatar
xster committed
292 293
      );
    }
294
    delegate.bringIntoView(delegate.textEditingValue.selection.extent);
xster's avatar
xster committed
295 296 297
    delegate.hideToolbar();
  }

298 299 300 301 302 303 304
  /// Adjust the selection of the text field managed by the given `delegate` so
  /// that everything is selected.
  ///
  /// Does not hide the toolbar.
  ///
  /// This is called by subclasses when their select-all affordance is activated
  /// by the user.
xster's avatar
xster committed
305
  void handleSelectAll(TextSelectionDelegate delegate) {
306 307 308 309 310 311 312
    delegate.userUpdateTextEditingValue(
      TextEditingValue(
        text: delegate.textEditingValue.text,
        selection: TextSelection(
          baseOffset: 0,
          extentOffset: delegate.textEditingValue.text.length,
        ),
xster's avatar
xster committed
313
      ),
314
      SelectionChangedCause.toolBar,
xster's avatar
xster committed
315
    );
316
    delegate.bringIntoView(delegate.textEditingValue.selection.extent);
xster's avatar
xster committed
317
  }
318 319
}

320 321 322 323
/// An object that manages a pair of text selection handles.
///
/// The selection handles are displayed in the [Overlay] that most closely
/// encloses the given [BuildContext].
324
class TextSelectionOverlay {
Marc Plano-Lesay's avatar
Marc Plano-Lesay committed
325
  /// Creates an object that manages overlay entries for selection handles.
326 327
  ///
  /// The [context] must not be null and must have an [Overlay] as an ancestor.
328
  TextSelectionOverlay({
329 330
    required TextEditingValue value,
    required this.context,
331
    this.debugRequiredFor,
332 333 334 335
    required this.toolbarLayerLink,
    required this.startHandleLayerLink,
    required this.endHandleLayerLink,
    required this.renderObject,
336
    this.selectionControls,
337
    bool handlesVisible = false,
338
    this.selectionDelegate,
339
    this.dragStartBehavior = DragStartBehavior.start,
340
    this.onSelectionHandleTapped,
341
    this.clipboardStatus,
342 343
  }) : assert(value != null),
       assert(context != null),
344 345
       assert(handlesVisible != null),
       _handlesVisible = handlesVisible,
346
       _value = value {
347
    final OverlayState? overlay = Overlay.of(context, rootOverlay: true);
348 349
    assert(
      overlay != null,
350 351
      'No Overlay widget exists above $context.\n'
      'Usually the Navigator created by WidgetsApp provides the overlay. Perhaps your '
352 353
      'app content was created above the Navigator with the WidgetsApp builder parameter.',
    );
354
    _toolbarController = AnimationController(duration: fadeDuration, vsync: overlay!);
355
  }
356

357 358 359 360
  /// The context in which the selection handles should appear.
  ///
  /// This context must have an [Overlay] as an ancestor because this object
  /// will display the text selection handles in that [Overlay].
361
  final BuildContext context;
362 363

  /// Debugging information for explaining why the [Overlay] is required.
364
  final Widget? debugRequiredFor;
365

366 367
  /// The object supplied to the [CompositedTransformTarget] that wraps the text
  /// field.
368 369 370 371 372 373 374 375 376
  final LayerLink toolbarLayerLink;

  /// The objects supplied to the [CompositedTransformTarget] that wraps the
  /// location of start selection handle.
  final LayerLink startHandleLayerLink;

  /// The objects supplied to the [CompositedTransformTarget] that wraps the
  /// location of end selection handle.
  final LayerLink endHandleLayerLink;
377

378 379
  // TODO(mpcomplete): what if the renderObject is removed or replaced, or
  // moves? Not sure what cases I need to handle, or how to handle them.
380
  /// The editable line in which the selected text is being displayed.
381
  final RenderEditable renderObject;
382

383
  /// Builds text selection handles and toolbar.
384
  final TextSelectionControls? selectionControls;
385

386 387
  /// The delegate for manipulating the current selection in the owning
  /// text field.
388
  final TextSelectionDelegate? selectionDelegate;
389

390 391 392
  /// Determines the way that drag start behavior is handled.
  ///
  /// If set to [DragStartBehavior.start], handle drag behavior will
393 394 395
  /// begin at the position where the drag gesture won the arena. If set to
  /// [DragStartBehavior.down] it will begin at the position where a down
  /// event is first detected.
396 397 398 399 400
  ///
  /// In general, setting this to [DragStartBehavior.start] will make drag
  /// animation smoother and setting it to [DragStartBehavior.down] will make
  /// drag behavior feel slightly more reactive.
  ///
401
  /// By default, the drag start behavior is [DragStartBehavior.start].
402 403 404 405 406 407
  ///
  /// See also:
  ///
  ///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
  final DragStartBehavior dragStartBehavior;

408
  /// {@template flutter.widgets.TextSelectionOverlay.onSelectionHandleTapped}
409 410 411 412 413 414 415 416 417 418
  /// A callback that's optionally invoked when a selection handle is tapped.
  ///
  /// The [TextSelectionControls.buildHandle] implementation the text field
  /// uses decides where the the handle's tap "hotspot" is, or whether the
  /// selection handle supports tap gestures at all. For instance,
  /// [MaterialTextSelectionControls] calls [onSelectionHandleTapped] when the
  /// selection handle's "knob" is tapped, while
  /// [CupertinoTextSelectionControls] builds a handle that's not sufficiently
  /// large for tapping (as it's not meant to be tapped) so it does not call
  /// [onSelectionHandleTapped] even when tapped.
419
  /// {@endtemplate}
420 421
  // See https://github.com/flutter/flutter/issues/39376#issuecomment-848406415
  // for provenance.
422
  final VoidCallback? onSelectionHandleTapped;
423

424 425 426 427 428
  /// Maintains the status of the clipboard for determining if its contents can
  /// be pasted or not.
  ///
  /// Useful because the actual value of the clipboard can only be checked
  /// asynchronously (see [Clipboard.getData]).
429
  final ClipboardStatusNotifier? clipboardStatus;
430

431 432 433
  /// Controls the fade-in and fade-out animations for the toolbar and handles.
  static const Duration fadeDuration = Duration(milliseconds: 150);

434
  late AnimationController _toolbarController;
435 436
  Animation<double> get _toolbarOpacity => _toolbarController.view;

437 438 439 440
  /// Retrieve current value.
  @visibleForTesting
  TextEditingValue get value => _value;

441
  TextEditingValue _value;
442 443 444

  /// A pair of handles. If this is non-null, there are always 2, though the
  /// second is hidden when the selection is collapsed.
445
  List<OverlayEntry>? _handles;
446

447
  /// A copy/paste toolbar.
448
  OverlayEntry? _toolbar;
449

450
  TextSelection get _selection => _value.selection;
451

452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
  /// Whether selection handles are visible.
  ///
  /// Set to false if you want to hide the handles. Use this property to show or
  /// hide the handle without rebuilding them.
  ///
  /// If this method is called while the [SchedulerBinding.schedulerPhase] is
  /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
  /// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed
  /// until the post-frame callbacks phase. Otherwise the update is done
  /// synchronously. This means that it is safe to call during builds, but also
  /// that if you do call this during a build, the UI will not update until the
  /// next frame (i.e. many milliseconds later).
  ///
  /// Defaults to false.
  bool get handlesVisible => _handlesVisible;
  bool _handlesVisible = false;
  set handlesVisible(bool visible) {
    assert(visible != null);
    if (_handlesVisible == visible)
      return;
    _handlesVisible = visible;
    // If we are in build state, it will be too late to update visibility.
    // We will need to schedule the build in next frame.
475 476
    if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.persistentCallbacks) {
      SchedulerBinding.instance!.addPostFrameCallback(_markNeedsBuild);
477 478 479 480 481 482
    } else {
      _markNeedsBuild();
    }
  }

  /// Builds the handles by inserting them into the [context]'s overlay.
483
  void showHandles() {
484 485 486
    if (_handles != null)
      return;

487
    _handles = <OverlayEntry>[
488 489
      OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.start)),
      OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.end)),
490
    ];
491

492 493
    Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
      .insertAll(_handles!);
494 495
  }

496 497 498
  /// Destroys the handles by removing them from overlay.
  void hideHandles() {
    if (_handles != null) {
499 500
      _handles![0].remove();
      _handles![1].remove();
501 502 503 504
      _handles = null;
    }
  }

505 506 507
  /// Shows the toolbar by inserting it into the [context]'s overlay.
  void showToolbar() {
    assert(_toolbar == null);
508
    _toolbar = OverlayEntry(builder: _buildToolbar);
509
    Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!.insert(_toolbar!);
510
    _toolbarController.forward(from: 0.0);
511 512
  }

513
  /// Updates the overlay after the selection has changed.
514 515
  ///
  /// If this method is called while the [SchedulerBinding.schedulerPhase] is
516 517
  /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
  /// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed
518 519 520 521
  /// until the post-frame callbacks phase. Otherwise the update is done
  /// synchronously. This means that it is safe to call during builds, but also
  /// that if you do call this during a build, the UI will not update until the
  /// next frame (i.e. many milliseconds later).
522 523
  void update(TextEditingValue newValue) {
    if (_value == newValue)
524
      return;
525
    _value = newValue;
526 527
    if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.persistentCallbacks) {
      SchedulerBinding.instance!.addPostFrameCallback(_markNeedsBuild);
528 529 530 531 532
    } else {
      _markNeedsBuild();
    }
  }

533 534 535 536 537 538 539 540
  /// Causes the overlay to update its rendering.
  ///
  /// This is intended to be called when the [renderObject] may have changed its
  /// text metrics (e.g. because the text was scrolled).
  void updateForScroll() {
    _markNeedsBuild();
  }

541
  void _markNeedsBuild([ Duration? duration ]) {
542
    if (_handles != null) {
543 544
      _handles![0].markNeedsBuild();
      _handles![1].markNeedsBuild();
545 546
    }
    _toolbar?.markNeedsBuild();
547 548
  }

549
  /// Whether the handles are currently visible.
550
  bool get handlesAreVisible => _handles != null && handlesVisible;
551 552 553 554

  /// Whether the toolbar is currently visible.
  bool get toolbarIsVisible => _toolbar != null;

555
  /// Hides the entire overlay including the toolbar and the handles.
556
  void hide() {
557
    if (_handles != null) {
558 559
      _handles![0].remove();
      _handles![1].remove();
560 561
      _handles = null;
    }
562 563 564 565
    if (_toolbar != null) {
      hideToolbar();
    }
  }
566

567 568 569 570 571
  /// Hides the toolbar part of the overlay.
  ///
  /// To hide the whole overlay, see [hide].
  void hideToolbar() {
    assert(_toolbar != null);
572
    _toolbarController.stop();
573
    _toolbar!.remove();
574
    _toolbar = null;
575 576 577 578 579 580
  }

  /// Final cleanup.
  void dispose() {
    hide();
    _toolbarController.dispose();
581 582
  }

583
  Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) {
584 585
    final Widget handle;
    final TextSelectionControls? selectionControls = this.selectionControls;
586
    if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) ||
587
         selectionControls == null)
588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603
      handle = Container(); // hide the second handle when collapsed
    else {
      handle = Visibility(
        visible: handlesVisible,
        child: _TextSelectionHandleOverlay(
          onSelectionHandleChanged: (TextSelection newSelection) {
            _handleSelectionHandleChanged(newSelection, position);
          },
          onSelectionHandleTapped: onSelectionHandleTapped,
          startHandleLayerLink: startHandleLayerLink,
          endHandleLayerLink: endHandleLayerLink,
          renderObject: renderObject,
          selection: _selection,
          selectionControls: selectionControls,
          position: position,
          dragStartBehavior: dragStartBehavior,
604
        ),
605 606 607 608 609
      );
    }
    return ExcludeSemantics(
      child: handle,
    );
610 611
  }

612
  Widget _buildToolbar(BuildContext context) {
613
    if (selectionControls == null)
614
      return Container();
615 616

    // Find the horizontal midpoint, just above the selected text.
617 618
    final List<TextSelectionPoint> endpoints =
        renderObject.getEndpointsForSelection(_selection);
619

620
    final Rect editingRegion = Rect.fromPoints(
621 622
      renderObject.localToGlobal(Offset.zero),
      renderObject.localToGlobal(renderObject.size.bottomRight(Offset.zero)),
623 624
    );

625 626 627 628 629 630 631 632 633 634 635 636 637 638 639
    final bool isMultiline = endpoints.last.point.dy - endpoints.first.point.dy >
          renderObject.preferredLineHeight / 2;

    // If the selected text spans more than 1 line, horizontally center the toolbar.
    // Derived from both iOS and Android.
    final double midX = isMultiline
      ? editingRegion.width / 2
      : (endpoints.first.point.dx + endpoints.last.point.dx) / 2;

    final Offset midpoint = Offset(
      midX,
      // The y-coordinate won't be made use of most likely.
      endpoints[0].point.dy - renderObject.preferredLineHeight,
    );

640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657
    return Directionality(
      textDirection: Directionality.of(this.context),
      child: FadeTransition(
        opacity: _toolbarOpacity,
        child: CompositedTransformFollower(
          link: toolbarLayerLink,
          showWhenUnlinked: false,
          offset: -editingRegion.topLeft,
          child: Builder(
            builder: (BuildContext context) {
              return selectionControls!.buildToolbar(
                context,
                editingRegion,
                renderObject.preferredLineHeight,
                midpoint,
                endpoints,
                selectionDelegate!,
                clipboardStatus!,
658
                renderObject.lastSecondaryTapDownPosition,
659 660 661
              );
            },
          ),
662
        ),
663
      ),
664
    );
665 666
  }

667
  void _handleSelectionHandleChanged(TextSelection newSelection, _TextSelectionHandlePosition position) {
668
    final TextPosition textPosition;
669 670
    switch (position) {
      case _TextSelectionHandlePosition.start:
671
        textPosition = newSelection.base;
672 673
        break;
      case _TextSelectionHandlePosition.end:
674
        textPosition = newSelection.extent;
675 676
        break;
    }
677 678 679 680
    selectionDelegate!.userUpdateTextEditingValue(
      _value.copyWith(selection: newSelection, composing: TextRange.empty),
      SelectionChangedCause.drag,
    );
681
    selectionDelegate!.bringIntoView(textPosition);
682
  }
683 684 685 686
}

/// This widget represents a single draggable text selection handle.
class _TextSelectionHandleOverlay extends StatefulWidget {
687
  const _TextSelectionHandleOverlay({
688 689 690 691 692 693 694 695 696
    Key? key,
    required this.selection,
    required this.position,
    required this.startHandleLayerLink,
    required this.endHandleLayerLink,
    required this.renderObject,
    required this.onSelectionHandleChanged,
    required this.onSelectionHandleTapped,
    required this.selectionControls,
697
    this.dragStartBehavior = DragStartBehavior.start,
698 699 700 701
  }) : super(key: key);

  final TextSelection selection;
  final _TextSelectionHandlePosition position;
702 703
  final LayerLink startHandleLayerLink;
  final LayerLink endHandleLayerLink;
704
  final RenderEditable renderObject;
705
  final ValueChanged<TextSelection> onSelectionHandleChanged;
706
  final VoidCallback? onSelectionHandleTapped;
707
  final TextSelectionControls selectionControls;
708
  final DragStartBehavior dragStartBehavior;
709 710

  @override
711
  _TextSelectionHandleOverlayState createState() => _TextSelectionHandleOverlayState();
712 713 714 715 716 717 718 719 720

  ValueListenable<bool> get _visibility {
    switch (position) {
      case _TextSelectionHandlePosition.start:
        return renderObject.selectionStartInViewport;
      case _TextSelectionHandlePosition.end:
        return renderObject.selectionEndInViewport;
    }
  }
721 722
}

723 724
class _TextSelectionHandleOverlayState
    extends State<_TextSelectionHandleOverlay> with SingleTickerProviderStateMixin {
725
  late Offset _dragPosition;
726

727
  late AnimationController _controller;
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762
  Animation<double> get _opacity => _controller.view;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(duration: TextSelectionOverlay.fadeDuration, vsync: this);

    _handleVisibilityChanged();
    widget._visibility.addListener(_handleVisibilityChanged);
  }

  void _handleVisibilityChanged() {
    if (widget._visibility.value) {
      _controller.forward();
    } else {
      _controller.reverse();
    }
  }

  @override
  void didUpdateWidget(_TextSelectionHandleOverlay oldWidget) {
    super.didUpdateWidget(oldWidget);
    oldWidget._visibility.removeListener(_handleVisibilityChanged);
    _handleVisibilityChanged();
    widget._visibility.addListener(_handleVisibilityChanged);
  }

  @override
  void dispose() {
    widget._visibility.removeListener(_handleVisibilityChanged);
    _controller.dispose();
    super.dispose();
  }

763
  void _handleDragStart(DragStartDetails details) {
764
    final Size handleSize = widget.selectionControls.getHandleSize(
765 766 767
      widget.renderObject.preferredLineHeight,
    );
    _dragPosition = details.globalPosition + Offset(0.0, -handleSize.height);
768 769
  }

770 771
  void _handleDragUpdate(DragUpdateDetails details) {
    _dragPosition += details.delta;
772
    final TextPosition position = widget.renderObject.getPositionForPoint(_dragPosition);
773

774
    if (widget.selection.isCollapsed) {
775
      widget.onSelectionHandleChanged(TextSelection.fromPosition(position));
776 777 778
      return;
    }

779
    final TextSelection newSelection;
780
    switch (widget.position) {
781
      case _TextSelectionHandlePosition.start:
782
        newSelection = TextSelection(
783
          baseOffset: position.offset,
784
          extentOffset: widget.selection.extentOffset,
785 786 787
        );
        break;
      case _TextSelectionHandlePosition.end:
788
        newSelection = TextSelection(
789
          baseOffset: widget.selection.baseOffset,
790
          extentOffset: position.offset,
791 792 793 794 795 796 797
        );
        break;
    }

    if (newSelection.baseOffset >= newSelection.extentOffset)
      return; // don't allow order swapping.

798
    widget.onSelectionHandleChanged(newSelection);
799 800 801 802
  }

  @override
  Widget build(BuildContext context) {
803 804
    final LayerLink layerLink;
    final TextSelectionHandleType type;
805

806
    switch (widget.position) {
807
      case _TextSelectionHandlePosition.start:
808 809 810 811 812 813
        layerLink = widget.startHandleLayerLink;
        type = _chooseType(
          widget.renderObject.textDirection,
          TextSelectionHandleType.left,
          TextSelectionHandleType.right,
        );
814 815
        break;
      case _TextSelectionHandlePosition.end:
816 817 818 819 820 821 822 823
        // For collapsed selections, we shouldn't be building the [end] handle.
        assert(!widget.selection.isCollapsed);
        layerLink = widget.endHandleLayerLink;
        type = _chooseType(
          widget.renderObject.textDirection,
          TextSelectionHandleType.right,
          TextSelectionHandleType.left,
        );
824 825 826
        break;
    }

827
    final Offset handleAnchor = widget.selectionControls.getHandleAnchor(
828 829 830
      type,
      widget.renderObject.preferredLineHeight,
    );
831
    final Size handleSize = widget.selectionControls.getHandleSize(
832 833
      widget.renderObject.preferredLineHeight,
    );
834

835
    final Rect handleRect = Rect.fromLTWH(
836 837
      -handleAnchor.dx,
      -handleAnchor.dy,
838 839 840 841 842 843
      handleSize.width,
      handleSize.height,
    );

    // Make sure the GestureDetector is big enough to be easily interactive.
    final Rect interactiveRect = handleRect.expandToInclude(
844
      Rect.fromCircle(center: handleRect.center, radius: kMinInteractiveDimension/ 2),
845 846 847 848 849 850 851 852
    );
    final RelativeRect padding = RelativeRect.fromLTRB(
      math.max((interactiveRect.width - handleRect.width) / 2, 0),
      math.max((interactiveRect.height - handleRect.height) / 2, 0),
      math.max((interactiveRect.width - handleRect.width) / 2, 0),
      math.max((interactiveRect.height - handleRect.height) / 2, 0),
    );

853
    return CompositedTransformFollower(
854
      link: layerLink,
855
      offset: interactiveRect.topLeft,
856
      showWhenUnlinked: false,
857 858
      child: FadeTransition(
        opacity: _opacity,
859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874
        child: Container(
          alignment: Alignment.topLeft,
          width: interactiveRect.width,
          height: interactiveRect.height,
          child: GestureDetector(
            behavior: HitTestBehavior.translucent,
            dragStartBehavior: widget.dragStartBehavior,
            onPanStart: _handleDragStart,
            onPanUpdate: _handleDragUpdate,
            child: Padding(
              padding: EdgeInsets.only(
                left: padding.left,
                top: padding.top,
                right: padding.right,
                bottom: padding.bottom,
              ),
875
              child: widget.selectionControls.buildHandle(
876 877 878
                context,
                type,
                widget.renderObject.preferredLineHeight,
879
                widget.onSelectionHandleTapped,
xster's avatar
xster committed
880
              ),
881
            ),
882
          ),
883
        ),
884
      ),
885 886 887 888
    );
  }

  TextSelectionHandleType _chooseType(
889
    TextDirection textDirection,
890
    TextSelectionHandleType ltrType,
891
    TextSelectionHandleType rtlType,
892
  ) {
893
    if (widget.selection.isCollapsed)
894 895
      return TextSelectionHandleType.collapsed;

896 897
    assert(textDirection != null);
    switch (textDirection) {
898 899 900 901 902 903 904
      case TextDirection.ltr:
        return ltrType;
      case TextDirection.rtl:
        return rtlType;
    }
  }
}
905

906 907 908 909 910 911 912 913 914 915 916
/// Delegate interface for the [TextSelectionGestureDetectorBuilder].
///
/// The interface is usually implemented by textfield implementations wrapping
/// [EditableText], that use a [TextSelectionGestureDetectorBuilder] to build a
/// [TextSelectionGestureDetector] for their [EditableText]. The delegate provides
/// the builder with information about the current state of the textfield.
/// Based on these information, the builder adds the correct gesture handlers
/// to the gesture detector.
///
/// See also:
///
917 918 919
///  * [TextField], which implements this delegate for the Material textfield.
///  * [CupertinoTextField], which implements this delegate for the Cupertino
///    textfield.
920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935
abstract class TextSelectionGestureDetectorBuilderDelegate {
  /// [GlobalKey] to the [EditableText] for which the
  /// [TextSelectionGestureDetectorBuilder] will build a [TextSelectionGestureDetector].
  GlobalKey<EditableTextState> get editableTextKey;

  /// Whether the textfield should respond to force presses.
  bool get forcePressEnabled;

  /// Whether the user may select text in the textfield.
  bool get selectionEnabled;
}

/// Builds a [TextSelectionGestureDetector] to wrap an [EditableText].
///
/// The class implements sensible defaults for many user interactions
/// with an [EditableText] (see the documentation of the various gesture handler
936 937
/// methods, e.g. [onTapDown], [onForcePressStart], etc.). Subclasses of
/// [TextSelectionGestureDetectorBuilder] can change the behavior performed in
938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954
/// responds to these gesture events by overriding the corresponding handler
/// methods of this class.
///
/// The resulting [TextSelectionGestureDetector] to wrap an [EditableText] is
/// obtained by calling [buildGestureDetector].
///
/// See also:
///
///  * [TextField], which uses a subclass to implement the Material-specific
///    gesture logic of an [EditableText].
///  * [CupertinoTextField], which uses a subclass to implement the
///    Cupertino-specific gesture logic of an [EditableText].
class TextSelectionGestureDetectorBuilder {
  /// Creates a [TextSelectionGestureDetectorBuilder].
  ///
  /// The [delegate] must not be null.
  TextSelectionGestureDetectorBuilder({
955
    required this.delegate,
956 957 958 959 960 961 962 963 964 965
  }) : assert(delegate != null);

  /// The delegate for this [TextSelectionGestureDetectorBuilder].
  ///
  /// The delegate provides the builder with information about what actions can
  /// currently be performed on the textfield. Based on this, the builder adds
  /// the correct gesture handlers to the gesture detector.
  @protected
  final TextSelectionGestureDetectorBuilderDelegate delegate;

966
  /// Returns true if lastSecondaryTapDownPosition was on selection.
967 968 969 970 971 972 973 974 975 976
  bool get _lastSecondaryTapWasOnSelection {
    assert(renderEditable.lastSecondaryTapDownPosition != null);
    if (renderEditable.selection == null) {
      return false;
    }

    final TextPosition textPosition = renderEditable.getPositionForPoint(
      renderEditable.lastSecondaryTapDownPosition!,
    );

977 978
    return renderEditable.selection!.start <= textPosition.offset
        && renderEditable.selection!.end >= textPosition.offset;
979 980
  }

981
  /// Whether to show the selection toolbar.
982 983 984 985 986 987 988 989 990 991
  ///
  /// It is based on the signal source when a [onTapDown] is called. This getter
  /// will return true if current [onTapDown] event is triggered by a touch or
  /// a stylus.
  bool get shouldShowSelectionToolbar => _shouldShowSelectionToolbar;
  bool _shouldShowSelectionToolbar = true;

  /// The [State] of the [EditableText] for which the builder will provide a
  /// [TextSelectionGestureDetector].
  @protected
992
  EditableTextState get editableText => delegate.editableTextKey.currentState!;
993 994 995 996

  /// The [RenderObject] of the [EditableText] for which the builder will
  /// provide a [TextSelectionGestureDetector].
  @protected
997
  RenderEditable get renderEditable => editableText.renderEditable;
998

999 1000 1001
  /// The viewport offset pixels of the [RenderEditable] at the last drag start.
  double _dragStartViewportOffset = 0.0;

1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016
  /// Handler for [TextSelectionGestureDetector.onTapDown].
  ///
  /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets
  /// [shouldShowSelectionToolbar] to true if the tap was initiated by a finger or stylus.
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onTapDown], which triggers this callback.
  @protected
  void onTapDown(TapDownDetails details) {
    renderEditable.handleTapDown(details);
    // The selection overlay should only be shown when the user is interacting
    // through a touch screen (via either a finger or a stylus). A mouse shouldn't
    // trigger the selection overlay.
    // For backwards-compatibility, we treat a null kind the same as touch.
1017
    final PointerDeviceKind? kind = details.kind;
1018
    _shouldShowSelectionToolbar = kind == null
1019 1020
      || kind == PointerDeviceKind.touch
      || kind == PointerDeviceKind.stylus;
1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048
  }

  /// Handler for [TextSelectionGestureDetector.onForcePressStart].
  ///
  /// By default, it selects the word at the position of the force press,
  /// if selection is enabled.
  ///
  /// This callback is only applicable when force press is enabled.
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onForcePressStart], which triggers this
  ///    callback.
  @protected
  void onForcePressStart(ForcePressDetails details) {
    assert(delegate.forcePressEnabled);
    _shouldShowSelectionToolbar = true;
    if (delegate.selectionEnabled) {
      renderEditable.selectWordsInRange(
        from: details.globalPosition,
        cause: SelectionChangedCause.forcePress,
      );
    }
  }

  /// Handler for [TextSelectionGestureDetector.onForcePressEnd].
  ///
  /// By default, it selects words in the range specified in [details] and shows
1049
  /// toolbar if it is necessary.
1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064
  ///
  /// This callback is only applicable when force press is enabled.
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onForcePressEnd], which triggers this
  ///    callback.
  @protected
  void onForcePressEnd(ForcePressDetails details) {
    assert(delegate.forcePressEnabled);
    renderEditable.selectWordsInRange(
      from: details.globalPosition,
      cause: SelectionChangedCause.forcePress,
    );
    if (shouldShowSelectionToolbar)
1065
      editableText.showToolbar();
1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133
  }

  /// Handler for [TextSelectionGestureDetector.onSingleTapUp].
  ///
  /// By default, it selects word edge if selection is enabled.
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onSingleTapUp], which triggers
  ///    this callback.
  @protected
  void onSingleTapUp(TapUpDetails details) {
    if (delegate.selectionEnabled) {
      renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
    }
  }

  /// Handler for [TextSelectionGestureDetector.onSingleTapCancel].
  ///
  /// By default, it services as place holder to enable subclass override.
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onSingleTapCancel], which triggers
  ///    this callback.
  @protected
  void onSingleTapCancel() {/* Subclass should override this method if needed. */}

  /// Handler for [TextSelectionGestureDetector.onSingleLongTapStart].
  ///
  /// By default, it selects text position specified in [details] if selection
  /// is enabled.
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onSingleLongTapStart], which triggers
  ///    this callback.
  @protected
  void onSingleLongTapStart(LongPressStartDetails details) {
    if (delegate.selectionEnabled) {
      renderEditable.selectPositionAt(
        from: details.globalPosition,
        cause: SelectionChangedCause.longPress,
      );
    }
  }

  /// Handler for [TextSelectionGestureDetector.onSingleLongTapMoveUpdate].
  ///
  /// By default, it updates the selection location specified in [details] if
  /// selection is enabled.
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onSingleLongTapMoveUpdate], which
  ///    triggers this callback.
  @protected
  void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
    if (delegate.selectionEnabled) {
      renderEditable.selectPositionAt(
        from: details.globalPosition,
        cause: SelectionChangedCause.longPress,
      );
    }
  }

  /// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd].
  ///
1134
  /// By default, it shows toolbar if necessary.
1135 1136 1137 1138 1139 1140 1141 1142
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onSingleLongTapEnd], which triggers this
  ///    callback.
  @protected
  void onSingleLongTapEnd(LongPressEndDetails details) {
    if (shouldShowSelectionToolbar)
1143
      editableText.showToolbar();
1144 1145
  }

1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174
  /// Handler for [TextSelectionGestureDetector.onSecondaryTap].
  ///
  /// By default, selects the word if possible and shows the toolbar.
  @protected
  void onSecondaryTap() {
    if (delegate.selectionEnabled) {
      if (!_lastSecondaryTapWasOnSelection) {
        renderEditable.selectWord(cause: SelectionChangedCause.tap);
      }
      if (shouldShowSelectionToolbar) {
        editableText.hideToolbar();
        editableText.showToolbar();
      }
    }
  }

  /// Handler for [TextSelectionGestureDetector.onSecondaryTapDown].
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onSecondaryTapDown], which triggers this
  ///    callback.
  ///  * [onSecondaryTap], which is typically called after this.
  @protected
  void onSecondaryTapDown(TapDownDetails details) {
    renderEditable.handleSecondaryTapDown(details);
    _shouldShowSelectionToolbar = true;
  }

1175 1176
  /// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
  ///
1177
  /// By default, it selects a word through [RenderEditable.selectWord] if
1178
  /// selectionEnabled and shows toolbar if necessary.
1179 1180 1181 1182 1183 1184 1185 1186 1187 1188
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this
  ///    callback.
  @protected
  void onDoubleTapDown(TapDownDetails details) {
    if (delegate.selectionEnabled) {
      renderEditable.selectWord(cause: SelectionChangedCause.tap);
      if (shouldShowSelectionToolbar)
1189
        editableText.showToolbar();
1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202
    }
  }

  /// Handler for [TextSelectionGestureDetector.onDragSelectionStart].
  ///
  /// By default, it selects a text position specified in [details].
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onDragSelectionStart], which triggers
  ///    this callback.
  @protected
  void onDragSelectionStart(DragStartDetails details) {
1203 1204
    if (!delegate.selectionEnabled)
      return;
1205 1206 1207 1208 1209
    final PointerDeviceKind? kind = details.kind;
    _shouldShowSelectionToolbar = kind == null
      || kind == PointerDeviceKind.touch
      || kind == PointerDeviceKind.stylus;

1210 1211 1212 1213
    renderEditable.selectPositionAt(
      from: details.globalPosition,
      cause: SelectionChangedCause.drag,
    );
1214 1215

    _dragStartViewportOffset = renderEditable.offset.pixels;
1216 1217 1218 1219
  }

  /// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate].
  ///
1220 1221
  /// By default, it updates the selection location specified in the provided
  /// details objects.
1222 1223 1224 1225 1226 1227 1228
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onDragSelectionUpdate], which triggers
  ///    this callback./lib/src/material/text_field.dart
  @protected
  void onDragSelectionUpdate(DragStartDetails startDetails, DragUpdateDetails updateDetails) {
1229 1230
    if (!delegate.selectionEnabled)
      return;
1231 1232 1233 1234 1235 1236

    // Adjust the drag start offset for possible viewport offset changes.
    final Offset startOffset = renderEditable.maxLines == 1
        ? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
        : Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset);

1237
    renderEditable.selectPositionAt(
1238
      from: startDetails.globalPosition - startOffset,
1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259
      to: updateDetails.globalPosition,
      cause: SelectionChangedCause.drag,
    );
  }

  /// Handler for [TextSelectionGestureDetector.onDragSelectionEnd].
  ///
  /// By default, it services as place holder to enable subclass override.
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this
  ///    callback.
  @protected
  void onDragSelectionEnd(DragEndDetails details) {/* Subclass should override this method if needed. */}

  /// Returns a [TextSelectionGestureDetector] configured with the handlers
  /// provided by this builder.
  ///
  /// The [child] or its subtree should contain [EditableText].
  Widget buildGestureDetector({
1260 1261 1262
    Key? key,
    HitTestBehavior? behavior,
    required Widget child,
1263 1264 1265 1266 1267 1268
  }) {
    return TextSelectionGestureDetector(
      key: key,
      onTapDown: onTapDown,
      onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
      onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
1269 1270
      onSecondaryTap: onSecondaryTap,
      onSecondaryTapDown: onSecondaryTapDown,
1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285
      onSingleTapUp: onSingleTapUp,
      onSingleTapCancel: onSingleTapCancel,
      onSingleLongTapStart: onSingleLongTapStart,
      onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
      onSingleLongTapEnd: onSingleLongTapEnd,
      onDoubleTapDown: onDoubleTapDown,
      onDragSelectionStart: onDragSelectionStart,
      onDragSelectionUpdate: onDragSelectionUpdate,
      onDragSelectionEnd: onDragSelectionEnd,
      behavior: behavior,
      child: child,
    );
  }
}

1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303
/// A gesture detector to respond to non-exclusive event chains for a text field.
///
/// An ordinary [GestureDetector] configured to handle events like tap and
/// double tap will only recognize one or the other. This widget detects both:
/// first the tap and then, if another tap down occurs within a time limit, the
/// double tap.
///
/// See also:
///
///  * [TextField], a Material text field which uses this gesture detector.
///  * [CupertinoTextField], a Cupertino text field which uses this gesture
///    detector.
class TextSelectionGestureDetector extends StatefulWidget {
  /// Create a [TextSelectionGestureDetector].
  ///
  /// Multiple callbacks can be called for one sequence of input gesture.
  /// The [child] parameter must not be null.
  const TextSelectionGestureDetector({
1304
    Key? key,
1305
    this.onTapDown,
1306 1307
    this.onForcePressStart,
    this.onForcePressEnd,
1308 1309
    this.onSecondaryTap,
    this.onSecondaryTapDown,
1310 1311
    this.onSingleTapUp,
    this.onSingleTapCancel,
1312 1313 1314
    this.onSingleLongTapStart,
    this.onSingleLongTapMoveUpdate,
    this.onSingleLongTapEnd,
1315
    this.onDoubleTapDown,
1316 1317 1318
    this.onDragSelectionStart,
    this.onDragSelectionUpdate,
    this.onDragSelectionEnd,
1319
    this.behavior,
1320
    required this.child,
1321 1322 1323 1324 1325 1326
  }) : assert(child != null),
       super(key: key);

  /// Called for every tap down including every tap down that's part of a
  /// double click or a long press, except touches that include enough movement
  /// to not qualify as taps (e.g. pans and flings).
1327
  final GestureTapDownCallback? onTapDown;
1328

1329
  /// Called when a pointer has tapped down and the force of the pointer has
1330
  /// just become greater than [ForcePressGestureRecognizer.startPressure].
1331
  final GestureForcePressStartCallback? onForcePressStart;
1332 1333 1334

  /// Called when a pointer that had previously triggered [onForcePressStart] is
  /// lifted off the screen.
1335
  final GestureForcePressEndCallback? onForcePressEnd;
1336

1337 1338 1339 1340 1341 1342
  /// Called for a tap event with the secondary mouse button.
  final GestureTapCallback? onSecondaryTap;

  /// Called for a tap down event with the secondary mouse button.
  final GestureTapDownCallback? onSecondaryTapDown;

1343
  /// Called for each distinct tap except for every second tap of a double tap.
1344
  /// For example, if the detector was configured with [onTapDown] and
1345 1346
  /// [onDoubleTapDown], three quick taps would be recognized as a single tap
  /// down, followed by a double tap down, followed by a single tap down.
1347
  final GestureTapUpCallback? onSingleTapUp;
1348 1349 1350 1351

  /// Called for each touch that becomes recognized as a gesture that is not a
  /// short tap, such as a long tap or drag. It is called at the moment when
  /// another gesture from the touch is recognized.
1352
  final GestureTapCancelCallback? onSingleTapCancel;
1353 1354 1355 1356

  /// Called for a single long tap that's sustained for longer than
  /// [kLongPressTimeout] but not necessarily lifted. Not called for a
  /// double-tap-hold, which calls [onDoubleTapDown] instead.
1357
  final GestureLongPressStartCallback? onSingleLongTapStart;
1358 1359

  /// Called after [onSingleLongTapStart] when the pointer is dragged.
1360
  final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate;
1361 1362

  /// Called after [onSingleLongTapStart] when the pointer is lifted.
1363
  final GestureLongPressEndCallback? onSingleLongTapEnd;
1364 1365 1366

  /// Called after a momentary hold or a short tap that is close in space and
  /// time (within [kDoubleTapTimeout]) to a previous short tap.
1367
  final GestureTapDownCallback? onDoubleTapDown;
1368

1369
  /// Called when a mouse starts dragging to select text.
1370
  final GestureDragStartCallback? onDragSelectionStart;
1371 1372 1373 1374 1375 1376

  /// Called repeatedly as a mouse moves while dragging.
  ///
  /// The frequency of calls is throttled to avoid excessive text layout
  /// operations in text fields. The throttling is controlled by the constant
  /// [_kDragSelectionUpdateThrottle].
1377
  final DragSelectionUpdateCallback? onDragSelectionUpdate;
1378 1379

  /// Called when a mouse that was previously dragging is released.
1380
  final GestureDragEndCallback? onDragSelectionEnd;
1381

1382 1383 1384
  /// How this gesture detector should behave during hit testing.
  ///
  /// This defaults to [HitTestBehavior.deferToChild].
1385
  final HitTestBehavior? behavior;
1386 1387 1388 1389 1390 1391 1392 1393 1394 1395

  /// Child below this widget.
  final Widget child;

  @override
  State<StatefulWidget> createState() => _TextSelectionGestureDetectorState();
}

class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetector> {
  // Counts down for a short duration after a previous tap. Null otherwise.
1396 1397
  Timer? _doubleTapTimer;
  Offset? _lastTapOffset;
1398 1399 1400 1401 1402 1403 1404
  // True if a second tap down of a double tap is detected. Used to discard
  // subsequent tap up / tap hold of the same tap.
  bool _isDoubleTap = false;

  @override
  void dispose() {
    _doubleTapTimer?.cancel();
1405
    _dragUpdateThrottleTimer?.cancel();
1406 1407 1408 1409 1410 1411
    super.dispose();
  }

  // The down handler is force-run on success of a single tap and optimistically
  // run before a long press success.
  void _handleTapDown(TapDownDetails details) {
1412
    widget.onTapDown?.call(details);
1413 1414 1415 1416 1417 1418 1419
    // This isn't detected as a double tap gesture in the gesture recognizer
    // because it's 2 single taps, each of which may do different things depending
    // on whether it's a single tap, the first tap of a double tap, the second
    // tap held down, a clean double tap etc.
    if (_doubleTapTimer != null && _isWithinDoubleTapTolerance(details.globalPosition)) {
      // If there was already a previous tap, the second down hold/tap is a
      // double tap down.
1420
      widget.onDoubleTapDown?.call(details);
1421

1422
      _doubleTapTimer!.cancel();
1423 1424 1425 1426 1427 1428 1429
      _doubleTapTimeout();
      _isDoubleTap = true;
    }
  }

  void _handleTapUp(TapUpDetails details) {
    if (!_isDoubleTap) {
1430
      widget.onSingleTapUp?.call(details);
1431 1432 1433 1434 1435 1436 1437
      _lastTapOffset = details.globalPosition;
      _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout);
    }
    _isDoubleTap = false;
  }

  void _handleTapCancel() {
1438
    widget.onSingleTapCancel?.call();
1439 1440
  }

1441 1442 1443
  DragStartDetails? _lastDragStartDetails;
  DragUpdateDetails? _lastDragUpdateDetails;
  Timer? _dragUpdateThrottleTimer;
1444 1445 1446 1447

  void _handleDragStart(DragStartDetails details) {
    assert(_lastDragStartDetails == null);
    _lastDragStartDetails = details;
1448
    widget.onDragSelectionStart?.call(details);
1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465
  }

  void _handleDragUpdate(DragUpdateDetails details) {
    _lastDragUpdateDetails = details;
    // Only schedule a new timer if there's no one pending.
    _dragUpdateThrottleTimer ??= Timer(_kDragSelectionUpdateThrottle, _handleDragUpdateThrottled);
  }

  /// Drag updates are being throttled to avoid excessive text layouts in text
  /// fields. The frequency of invocations is controlled by the constant
  /// [_kDragSelectionUpdateThrottle].
  ///
  /// Once the drag gesture ends, any pending drag update will be fired
  /// immediately. See [_handleDragEnd].
  void _handleDragUpdateThrottled() {
    assert(_lastDragStartDetails != null);
    assert(_lastDragUpdateDetails != null);
1466
    widget.onDragSelectionUpdate?.call(_lastDragStartDetails!, _lastDragUpdateDetails!);
1467 1468 1469 1470 1471 1472 1473 1474 1475
    _dragUpdateThrottleTimer = null;
    _lastDragUpdateDetails = null;
  }

  void _handleDragEnd(DragEndDetails details) {
    assert(_lastDragStartDetails != null);
    if (_dragUpdateThrottleTimer != null) {
      // If there's already an update scheduled, trigger it immediately and
      // cancel the timer.
1476
      _dragUpdateThrottleTimer!.cancel();
1477 1478
      _handleDragUpdateThrottled();
    }
1479
    widget.onDragSelectionEnd?.call(details);
1480 1481 1482 1483 1484
    _dragUpdateThrottleTimer = null;
    _lastDragStartDetails = null;
    _lastDragUpdateDetails = null;
  }

1485 1486 1487
  void _forcePressStarted(ForcePressDetails details) {
    _doubleTapTimer?.cancel();
    _doubleTapTimer = null;
1488
    widget.onForcePressStart?.call(details);
1489 1490 1491
  }

  void _forcePressEnded(ForcePressDetails details) {
1492
    widget.onForcePressEnd?.call(details);
1493 1494
  }

1495 1496
  void _handleLongPressStart(LongPressStartDetails details) {
    if (!_isDoubleTap && widget.onSingleLongTapStart != null) {
1497
      widget.onSingleLongTapStart!(details);
1498 1499 1500 1501 1502
    }
  }

  void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
    if (!_isDoubleTap && widget.onSingleLongTapMoveUpdate != null) {
1503
      widget.onSingleLongTapMoveUpdate!(details);
1504 1505 1506
    }
  }

1507
  void _handleLongPressEnd(LongPressEndDetails details) {
1508
    if (!_isDoubleTap && widget.onSingleLongTapEnd != null) {
1509
      widget.onSingleLongTapEnd!(details);
1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524
    }
    _isDoubleTap = false;
  }

  void _doubleTapTimeout() {
    _doubleTapTimer = null;
    _lastTapOffset = null;
  }

  bool _isWithinDoubleTapTolerance(Offset secondTapOffset) {
    assert(secondTapOffset != null);
    if (_lastTapOffset == null) {
      return false;
    }

1525
    final Offset difference = secondTapOffset - _lastTapOffset!;
1526 1527 1528 1529 1530
    return difference.distance <= kDoubleTapSlop;
  }

  @override
  Widget build(BuildContext context) {
1531 1532
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

1533 1534 1535
    gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
      () => TapGestureRecognizer(debugOwner: this),
      (TapGestureRecognizer instance) {
1536
        instance
1537 1538
          ..onSecondaryTap = widget.onSecondaryTap
          ..onSecondaryTapDown = widget.onSecondaryTapDown
1539 1540 1541 1542 1543 1544 1545 1546 1547 1548
          ..onTapDown = _handleTapDown
          ..onTapUp = _handleTapUp
          ..onTapCancel = _handleTapCancel;
      },
    );

    if (widget.onSingleLongTapStart != null ||
        widget.onSingleLongTapMoveUpdate != null ||
        widget.onSingleLongTapEnd != null) {
      gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
1549 1550
        () => LongPressGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.touch),
        (LongPressGestureRecognizer instance) {
1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564
          instance
            ..onLongPressStart = _handleLongPressStart
            ..onLongPressMoveUpdate = _handleLongPressMoveUpdate
            ..onLongPressEnd = _handleLongPressEnd;
        },
      );
    }

    if (widget.onDragSelectionStart != null ||
        widget.onDragSelectionUpdate != null ||
        widget.onDragSelectionEnd != null) {
      // TODO(mdebbar): Support dragging in any direction (for multiline text).
      // https://github.com/flutter/flutter/issues/28676
      gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
1565 1566
        () => HorizontalDragGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.mouse),
        (HorizontalDragGestureRecognizer instance) {
1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579
          instance
            // Text selection should start from the position of the first pointer
            // down event.
            ..dragStartBehavior = DragStartBehavior.down
            ..onStart = _handleDragStart
            ..onUpdate = _handleDragUpdate
            ..onEnd = _handleDragEnd;
        },
      );
    }

    if (widget.onForcePressStart != null || widget.onForcePressEnd != null) {
      gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
1580 1581
        () => ForcePressGestureRecognizer(debugOwner: this),
        (ForcePressGestureRecognizer instance) {
1582 1583 1584 1585 1586 1587 1588 1589 1590
          instance
            ..onStart = widget.onForcePressStart != null ? _forcePressStarted : null
            ..onEnd = widget.onForcePressEnd != null ? _forcePressEnded : null;
        },
      );
    }

    return RawGestureDetector(
      gestures: gestures,
1591 1592 1593 1594 1595 1596
      excludeFromSemantics: true,
      behavior: widget.behavior,
      child: widget.child,
    );
  }
}
1597

1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610
/// A [ValueNotifier] whose [value] indicates whether the current contents of
/// the clipboard can be pasted.
///
/// The contents of the clipboard can only be read asynchronously, via
/// [Clipboard.getData], so this maintains a value that can be used
/// synchronously. Call [update] to asynchronously update value if needed.
class ClipboardStatusNotifier extends ValueNotifier<ClipboardStatus> with WidgetsBindingObserver {
  /// Create a new ClipboardStatusNotifier.
  ClipboardStatusNotifier({
    ClipboardStatus value = ClipboardStatus.unknown,
  }) : super(value);

  bool _disposed = false;
1611
  /// True if this instance has been disposed.
1612 1613 1614
  bool get disposed => _disposed;

  /// Check the [Clipboard] and update [value] if needed.
1615
  Future<void> update() async {
1616 1617
    // iOS 14 added a notification that appears when an app accesses the
    // clipboard. To avoid the notification, don't access the clipboard on iOS,
1618
    // and instead always show the paste button, even when the clipboard is
1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634
    // empty.
    // TODO(justinmc): Use the new iOS 14 clipboard API method hasStrings that
    // won't trigger the notification.
    // https://github.com/flutter/flutter/issues/60145
    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
        value = ClipboardStatus.pasteable;
        return;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.macOS:
      case TargetPlatform.windows:
        break;
    }

1635
    ClipboardData? data;
1636 1637 1638 1639 1640 1641
    try {
      data = await Clipboard.getData(Clipboard.kTextPlain);
    } catch (stacktrace) {
      // In the case of an error from the Clipboard API, set the value to
      // unknown so that it will try to update again later.
      if (_disposed || value == ClipboardStatus.unknown) {
1642 1643
        return;
      }
1644 1645 1646 1647
      value = ClipboardStatus.unknown;
      return;
    }

1648
    final ClipboardStatus clipboardStatus = data != null && data.text != null && data.text!.isNotEmpty
1649 1650 1651 1652 1653 1654
        ? ClipboardStatus.pasteable
        : ClipboardStatus.notPasteable;
    if (_disposed || clipboardStatus == value) {
      return;
    }
    value = clipboardStatus;
1655 1656 1657 1658 1659
  }

  @override
  void addListener(VoidCallback listener) {
    if (!hasListeners) {
1660
      WidgetsBinding.instance!.addObserver(this);
1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671
    }
    if (value == ClipboardStatus.unknown) {
      update();
    }
    super.addListener(listener);
  }

  @override
  void removeListener(VoidCallback listener) {
    super.removeListener(listener);
    if (!hasListeners) {
1672
      WidgetsBinding.instance!.removeObserver(this);
1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691
    }
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.resumed:
        update();
        break;
      case AppLifecycleState.detached:
      case AppLifecycleState.inactive:
      case AppLifecycleState.paused:
        // Nothing to do.
    }
  }

  @override
  void dispose() {
    super.dispose();
1692
    WidgetsBinding.instance!.removeObserver(this);
1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709
    _disposed = true;
  }
}

/// An enumeration of the status of the content on the user's clipboard.
enum ClipboardStatus {
  /// The clipboard content can be pasted, such as a String of nonzero length.
  pasteable,

  /// The status of the clipboard is unknown. Since getting clipboard data is
  /// asynchronous (see [Clipboard.getData]), this status often exists while
  /// waiting to receive the clipboard contents for the first time.
  unknown,

  /// The content on the clipboard is not pasteable, such as when it is empty.
  notPasteable,
}