text_selection.dart 34.5 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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 6
import 'dart:async';

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/gestures.dart' show kDoubleTapTimeout, kDoubleTapSlop;
9
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 'container.dart';
16
import 'editable_text.dart';
17 18 19
import 'framework.dart';
import 'gesture_detector.dart';
import 'overlay.dart';
20
import 'ticker_provider.dart';
21
import 'transitions.dart';
22

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

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

29 30 31 32
/// Which type of selection handle to be displayed.
///
/// With mixed-direction text, both handles may be the same type. Examples:
///
33 34 35 36 37
/// * LTR text: 'the <quick brown> fox':
///
///   The '<' is drawn with the [left] type, the '>' with the [right]
///
/// * RTL text: 'XOF <NWORB KCIUQ> EHT':
38
///
39
///   Same as above.
40
///
41 42 43 44 45 46 47 48 49
/// * 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.
50 51 52
enum TextSelectionHandleType {
  /// The selection handle is to the left of the selection end point.
  left,
53

54 55 56 57 58 59 60
  /// 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,
}

61 62 63 64
/// 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 }

65 66 67 68
/// Signature for reporting changes to the selection component of a
/// [TextEditingValue] for the purposes of a [TextSelectionOverlay]. The
/// [caretRect] argument gives the location of the caret in the coordinate space
/// of the [RenderBox] given by the [TextSelectionOverlay.renderObject].
69 70
///
/// Used by [TextSelectionOverlay.onSelectionOverlayChanged].
71
typedef TextSelectionOverlayChanged = void Function(TextEditingValue value, Rect caretRect);
72

73 74 75 76 77 78 79 80 81 82 83 84 85
/// 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);

86 87
/// An interface for building the selection UI, to be provided by the
/// implementor of the toolbar widget.
xster's avatar
xster committed
88 89
///
/// Override text operations such as [handleCut] if needed.
90 91
abstract class TextSelectionControls {
  /// Builds a selection handle of the given type.
xster's avatar
xster committed
92 93 94 95
  ///
  /// The top left corner of this widget is positioned at the bottom of the
  /// selection position.
  Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight);
96 97 98 99

  /// Builds a toolbar near a text selection.
  ///
  /// Typically displays buttons for copying and pasting text.
100
  Widget buildToolbar(BuildContext context, Rect globalEditableRegion, Offset position, TextSelectionDelegate delegate);
101 102 103

  /// Returns the size of the selection handle.
  Size get handleSize;
xster's avatar
xster committed
104

105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
  /// 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) {
    return !delegate.textEditingValue.selection.isCollapsed;
  }

  /// 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) {
    return !delegate.textEditingValue.selection.isCollapsed;
  }

  /// Whether the current [Clipboard] content can be pasted into the text field
  /// managed by the given `delegate`.
  ///
  /// Subclasses can use this to decide if they should expose the paste
  /// functionality to the user.
  bool canPaste(TextSelectionDelegate delegate) {
    // TODO(goderbauer): return false when clipboard is empty, https://github.com/flutter/flutter/issues/11254
    return true;
  }

138
  /// Whether the current selection of the text field managed by the given
139 140 141 142 143 144 145 146 147
  /// `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) {
    return delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
  }

148 149 150 151 152 153
  /// 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
154 155
  void handleCut(TextSelectionDelegate delegate) {
    final TextEditingValue value = delegate.textEditingValue;
156
    Clipboard.setData(ClipboardData(
xster's avatar
xster committed
157 158
      text: value.selection.textInside(value.text),
    ));
159
    delegate.textEditingValue = TextEditingValue(
xster's avatar
xster committed
160 161
      text: value.selection.textBefore(value.text)
          + value.selection.textAfter(value.text),
162
      selection: TextSelection.collapsed(
xster's avatar
xster committed
163 164 165
        offset: value.selection.start
      ),
    );
166
    delegate.bringIntoView(delegate.textEditingValue.selection.extent);
xster's avatar
xster committed
167 168 169
    delegate.hideToolbar();
  }

170 171 172 173 174 175
  /// 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.
xster's avatar
xster committed
176 177
  void handleCopy(TextSelectionDelegate delegate) {
    final TextEditingValue value = delegate.textEditingValue;
178
    Clipboard.setData(ClipboardData(
xster's avatar
xster committed
179 180
      text: value.selection.textInside(value.text),
    ));
181
    delegate.textEditingValue = TextEditingValue(
xster's avatar
xster committed
182
      text: value.text,
183
      selection: TextSelection.collapsed(offset: value.selection.end),
xster's avatar
xster committed
184
    );
185
    delegate.bringIntoView(delegate.textEditingValue.selection.extent);
xster's avatar
xster committed
186 187 188
    delegate.hideToolbar();
  }

189 190 191 192 193 194 195 196 197 198 199
  /// 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
200
  Future<void> handlePaste(TextSelectionDelegate delegate) async {
201
    final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`.
xster's avatar
xster committed
202 203
    final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
    if (data != null) {
204
      delegate.textEditingValue = TextEditingValue(
xster's avatar
xster committed
205 206 207
        text: value.selection.textBefore(value.text)
            + data.text
            + value.selection.textAfter(value.text),
208
        selection: TextSelection.collapsed(
xster's avatar
xster committed
209 210 211 212
          offset: value.selection.start + data.text.length
        ),
      );
    }
213
    delegate.bringIntoView(delegate.textEditingValue.selection.extent);
xster's avatar
xster committed
214 215 216
    delegate.hideToolbar();
  }

217 218 219 220 221 222 223
  /// 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
224
  void handleSelectAll(TextSelectionDelegate delegate) {
225
    delegate.textEditingValue = TextEditingValue(
xster's avatar
xster committed
226
      text: delegate.textEditingValue.text,
227
      selection: TextSelection(
xster's avatar
xster committed
228
        baseOffset: 0,
229
        extentOffset: delegate.textEditingValue.text.length,
xster's avatar
xster committed
230 231
      ),
    );
232
    delegate.bringIntoView(delegate.textEditingValue.selection.extent);
xster's avatar
xster committed
233
  }
234 235
}

236 237 238 239
/// 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].
240
class TextSelectionOverlay {
241 242 243
  /// Creates an object that manages overly entries for selection handles.
  ///
  /// The [context] must not be null and must have an [Overlay] as an ancestor.
244
  TextSelectionOverlay({
245
    @required TextEditingValue value,
246
    @required this.context,
247
    this.debugRequiredFor,
248 249
    @required this.layerLink,
    @required this.renderObject,
250
    this.selectionControls,
251
    this.selectionDelegate,
252
    this.dragStartBehavior = DragStartBehavior.start,
253 254 255
  }) : assert(value != null),
       assert(context != null),
       _value = value {
256
    final OverlayState overlay = Overlay.of(context);
257 258 259 260
    assert(overlay != null,
      'No Overlay widget exists above $context.\n'
      'Usually the Navigator created by WidgetsApp provides the overlay. Perhaps your '
      'app content was created above the Navigator with the WidgetsApp builder parameter.');
261
    _toolbarController = AnimationController(duration: fadeDuration, vsync: overlay);
262
  }
263

264 265 266 267
  /// 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].
268
  final BuildContext context;
269 270

  /// Debugging information for explaining why the [Overlay] is required.
271
  final Widget debugRequiredFor;
272

273 274 275 276
  /// The object supplied to the [CompositedTransformTarget] that wraps the text
  /// field.
  final LayerLink layerLink;

277 278
  // 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.
279
  /// The editable line in which the selected text is being displayed.
280
  final RenderEditable renderObject;
281

282 283
  /// Builds text selection handles and toolbar.
  final TextSelectionControls selectionControls;
284

285 286 287 288
  /// The delegate for manipulating the current selection in the owning
  /// text field.
  final TextSelectionDelegate selectionDelegate;

289 290 291 292 293 294 295 296 297 298
  /// Determines the way that drag start behavior is handled.
  ///
  /// If set to [DragStartBehavior.start], handle drag behavior will
  /// begin upon the detection of a drag gesture. If set to
  /// [DragStartBehavior.down] it will begin when a down event is first detected.
  ///
  /// 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.
  ///
299
  /// By default, the drag start behavior is [DragStartBehavior.start].
300 301 302 303 304 305
  ///
  /// See also:
  ///
  ///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
  final DragStartBehavior dragStartBehavior;

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

309
  AnimationController _toolbarController;
310 311
  Animation<double> get _toolbarOpacity => _toolbarController.view;

312
  TextEditingValue _value;
313 314 315 316 317

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

318
  /// A copy/paste toolbar.
319 320
  OverlayEntry _toolbar;

321
  TextSelection get _selection => _value.selection;
322

323
  /// Shows the handles by inserting them into the [context]'s overlay.
324
  void showHandles() {
325 326
    assert(_handles == null);
    _handles = <OverlayEntry>[
327 328
      OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.start)),
      OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.end)),
329 330
    ];
    Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles);
331 332 333 334 335
  }

  /// Shows the toolbar by inserting it into the [context]'s overlay.
  void showToolbar() {
    assert(_toolbar == null);
336
    _toolbar = OverlayEntry(builder: _buildToolbar);
337
    Overlay.of(context, debugRequiredFor: debugRequiredFor).insert(_toolbar);
338
    _toolbarController.forward(from: 0.0);
339 340
  }

341
  /// Updates the overlay after the selection has changed.
342 343
  ///
  /// If this method is called while the [SchedulerBinding.schedulerPhase] is
344 345
  /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
  /// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed
346 347 348 349
  /// 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).
350 351
  void update(TextEditingValue newValue) {
    if (_value == newValue)
352
      return;
353
    _value = newValue;
354 355 356 357 358 359 360
    if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
      SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild);
    } else {
      _markNeedsBuild();
    }
  }

361 362 363 364 365 366 367 368
  /// 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();
  }

369
  void _markNeedsBuild([ Duration duration ]) {
370 371 372 373 374
    if (_handles != null) {
      _handles[0].markNeedsBuild();
      _handles[1].markNeedsBuild();
    }
    _toolbar?.markNeedsBuild();
375 376
  }

377 378 379 380 381 382
  /// Whether the handles are currently visible.
  bool get handlesAreVisible => _handles != null;

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

383
  /// Hides the overlay.
384
  void hide() {
385 386 387 388 389 390
    if (_handles != null) {
      _handles[0].remove();
      _handles[1].remove();
      _handles = null;
    }
    _toolbar?.remove();
391
    _toolbar = null;
392 393 394 395 396 397 398 399

    _toolbarController.stop();
  }

  /// Final cleanup.
  void dispose() {
    hide();
    _toolbarController.dispose();
400 401
  }

402
  Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) {
403
    if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) ||
404
         selectionControls == null)
405
      return Container(); // hide the second handle when collapsed
406 407 408 409 410 411 412 413 414
    return _TextSelectionHandleOverlay(
      onSelectionHandleChanged: (TextSelection newSelection) { _handleSelectionHandleChanged(newSelection, position); },
      onSelectionHandleTapped: _handleSelectionHandleTapped,
      layerLink: layerLink,
      renderObject: renderObject,
      selection: _selection,
      selectionControls: selectionControls,
      position: position,
      dragStartBehavior: dragStartBehavior,
415 416 417
    );
  }

418
  Widget _buildToolbar(BuildContext context) {
419
    if (selectionControls == null)
420
      return Container();
421 422

    // Find the horizontal midpoint, just above the selected text.
423
    final List<TextSelectionPoint> endpoints = renderObject.getEndpointsForSelection(_selection);
424
    final Offset midpoint = Offset(
425
      (endpoints.length == 1) ?
426 427
        endpoints[0].point.dx :
        (endpoints[0].point.dx + endpoints[1].point.dx) / 2.0,
428
      endpoints[0].point.dy - renderObject.preferredLineHeight,
429 430
    );

431
    final Rect editingRegion = Rect.fromPoints(
432 433
      renderObject.localToGlobal(Offset.zero),
      renderObject.localToGlobal(renderObject.size.bottomRight(Offset.zero)),
434 435
    );

436
    return FadeTransition(
437
      opacity: _toolbarOpacity,
438
      child: CompositedTransformFollower(
439 440 441
        link: layerLink,
        showWhenUnlinked: false,
        offset: -editingRegion.topLeft,
442
        child: selectionControls.buildToolbar(context, editingRegion, midpoint, selectionDelegate),
443
      ),
444
    );
445 446
  }

447
  void _handleSelectionHandleChanged(TextSelection newSelection, _TextSelectionHandlePosition position) {
448
    TextPosition textPosition;
449 450
    switch (position) {
      case _TextSelectionHandlePosition.start:
451
        textPosition = newSelection.base;
452 453
        break;
      case _TextSelectionHandlePosition.end:
454
        textPosition =newSelection.extent;
455 456
        break;
    }
457 458
    selectionDelegate.textEditingValue = _value.copyWith(selection: newSelection, composing: TextRange.empty);
    selectionDelegate.bringIntoView(textPosition);
459 460
  }

461
  void _handleSelectionHandleTapped() {
462
    if (_value.selection.isCollapsed) {
463 464 465 466 467 468 469 470
      if (_toolbar != null) {
        _toolbar?.remove();
        _toolbar = null;
      } else {
        showToolbar();
      }
    }
  }
471 472 473 474
}

/// This widget represents a single draggable text selection handle.
class _TextSelectionHandleOverlay extends StatefulWidget {
475
  const _TextSelectionHandleOverlay({
476
    Key key,
477 478 479 480 481 482
    @required this.selection,
    @required this.position,
    @required this.layerLink,
    @required this.renderObject,
    @required this.onSelectionHandleChanged,
    @required this.onSelectionHandleTapped,
483
    @required this.selectionControls,
484
    this.dragStartBehavior = DragStartBehavior.start,
485 486 487 488
  }) : super(key: key);

  final TextSelection selection;
  final _TextSelectionHandlePosition position;
489
  final LayerLink layerLink;
490
  final RenderEditable renderObject;
491
  final ValueChanged<TextSelection> onSelectionHandleChanged;
492
  final VoidCallback onSelectionHandleTapped;
493
  final TextSelectionControls selectionControls;
494
  final DragStartBehavior dragStartBehavior;
495 496

  @override
497
  _TextSelectionHandleOverlayState createState() => _TextSelectionHandleOverlayState();
498 499 500 501 502 503 504 505 506 507

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

510 511
class _TextSelectionHandleOverlayState
    extends State<_TextSelectionHandleOverlay> with SingleTickerProviderStateMixin {
512
  Offset _dragPosition;
513

514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
  AnimationController _controller;
  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();
  }

550
  void _handleDragStart(DragStartDetails details) {
551
    _dragPosition = details.globalPosition + Offset(0.0, -widget.selectionControls.handleSize.height);
552 553
  }

554 555
  void _handleDragUpdate(DragUpdateDetails details) {
    _dragPosition += details.delta;
556
    final TextPosition position = widget.renderObject.getPositionForPoint(_dragPosition);
557

558
    if (widget.selection.isCollapsed) {
559
      widget.onSelectionHandleChanged(TextSelection.fromPosition(position));
560 561 562 563
      return;
    }

    TextSelection newSelection;
564
    switch (widget.position) {
565
      case _TextSelectionHandlePosition.start:
566
        newSelection = TextSelection(
567
          baseOffset: position.offset,
568
          extentOffset: widget.selection.extentOffset,
569 570 571
        );
        break;
      case _TextSelectionHandlePosition.end:
572
        newSelection = TextSelection(
573
          baseOffset: widget.selection.baseOffset,
574
          extentOffset: position.offset,
575 576 577 578 579 580 581
        );
        break;
    }

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

582
    widget.onSelectionHandleChanged(newSelection);
583 584
  }

585
  void _handleTap() {
586
    widget.onSelectionHandleTapped();
587 588
  }

589 590
  @override
  Widget build(BuildContext context) {
591
    final List<TextSelectionPoint> endpoints = widget.renderObject.getEndpointsForSelection(widget.selection);
592
    Offset point;
593 594
    TextSelectionHandleType type;

595
    switch (widget.position) {
596 597 598 599 600 601 602 603 604 605 606 607 608
      case _TextSelectionHandlePosition.start:
        point = endpoints[0].point;
        type = _chooseType(endpoints[0], TextSelectionHandleType.left, TextSelectionHandleType.right);
        break;
      case _TextSelectionHandlePosition.end:
        // [endpoints] will only contain 1 point for collapsed selections, in
        // which case we shouldn't be building the [end] handle.
        assert(endpoints.length == 2);
        point = endpoints[1].point;
        type = _chooseType(endpoints[1], TextSelectionHandleType.right, TextSelectionHandleType.left);
        break;
    }

609 610 611 612 613 614
    final Size viewport = widget.renderObject.size;
    point = Offset(
      point.dx.clamp(0.0, viewport.width),
      point.dy.clamp(0.0, viewport.height),
    );

615
    return CompositedTransformFollower(
616 617
      link: widget.layerLink,
      showWhenUnlinked: false,
618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
      child: FadeTransition(
        opacity: _opacity,
        child: GestureDetector(
          dragStartBehavior: widget.dragStartBehavior,
          onPanStart: _handleDragStart,
          onPanUpdate: _handleDragUpdate,
          onTap: _handleTap,
          child: Stack(
            // Always let the selection handles draw outside of the conceptual
            // box where (0,0) is the top left corner of the RenderEditable.
            overflow: Overflow.visible,
            children: <Widget>[
              Positioned(
                left: point.dx,
                top: point.dy,
                child: widget.selectionControls.buildHandle(
                  context,
                  type,
                  widget.renderObject.preferredLineHeight,
                ),
xster's avatar
xster committed
638
              ),
639 640
            ],
          ),
641
        ),
642
      ),
643 644 645 646 647 648
    );
  }

  TextSelectionHandleType _chooseType(
    TextSelectionPoint endpoint,
    TextSelectionHandleType ltrType,
649
    TextSelectionHandleType rtlType,
650
  ) {
651
    if (widget.selection.isCollapsed)
652 653
      return TextSelectionHandleType.collapsed;

654
    assert(endpoint.direction != null);
655
    switch (endpoint.direction) {
656 657 658 659 660
      case TextDirection.ltr:
        return ltrType;
      case TextDirection.rtl:
        return rtlType;
    }
pq's avatar
pq committed
661
    return null;
662 663
  }
}
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684

/// 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({
    Key key,
    this.onTapDown,
685 686
    this.onForcePressStart,
    this.onForcePressEnd,
687 688
    this.onSingleTapUp,
    this.onSingleTapCancel,
689 690 691
    this.onSingleLongTapStart,
    this.onSingleLongTapMoveUpdate,
    this.onSingleLongTapEnd,
692
    this.onDoubleTapDown,
693 694 695
    this.onDragSelectionStart,
    this.onDragSelectionUpdate,
    this.onDragSelectionEnd,
696 697 698 699 700 701 702 703 704 705
    this.behavior,
    @required this.child,
  }) : 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).
  final GestureTapDownCallback onTapDown;

706 707 708 709 710 711 712 713
  /// Called when a pointer has tapped down and the force of the pointer has
  /// just become greater than [ForcePressGestureDetector.startPressure].
  final GestureForcePressStartCallback onForcePressStart;

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

714 715 716 717 718 719 720 721 722 723 724 725 726 727
  /// Called for each distinct tap except for every second tap of a double tap.
  /// For example, if the detector was configured [onSingleTapDown] and
  /// [onDoubleTapDown], three quick taps would be recognized as a single tap
  /// down, followed by a double tap down, followed by a single tap down.
  final GestureTapUpCallback onSingleTapUp;

  /// 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.
  final GestureTapCancelCallback onSingleTapCancel;

  /// 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.
728 729 730 731 732 733 734
  final GestureLongPressStartCallback onSingleLongTapStart;

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

  /// Called after [onSingleLongTapStart] when the pointer is lifted.
  final GestureLongPressEndCallback onSingleLongTapEnd;
735 736 737 738 739

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

740 741 742 743 744 745 746 747 748 749 750 751 752
  /// Called when a mouse starts dragging to select text.
  final GestureDragStartCallback onDragSelectionStart;

  /// 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].
  final DragSelectionUpdateCallback onDragSelectionUpdate;

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

753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775
  /// How this gesture detector should behave during hit testing.
  ///
  /// This defaults to [HitTestBehavior.deferToChild].
  final HitTestBehavior behavior;

  /// 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.
  Timer _doubleTapTimer;
  Offset _lastTapOffset;
  // 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();
776
    _dragUpdateThrottleTimer?.cancel();
777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819
    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) {
    if (widget.onTapDown != null) {
      widget.onTapDown(details);
    }
    // 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.
      if (widget.onDoubleTapDown != null) {
        widget.onDoubleTapDown(details);
      }

      _doubleTapTimer.cancel();
      _doubleTapTimeout();
      _isDoubleTap = true;
    }
  }

  void _handleTapUp(TapUpDetails details) {
    if (!_isDoubleTap) {
      if (widget.onSingleTapUp != null) {
        widget.onSingleTapUp(details);
      }
      _lastTapOffset = details.globalPosition;
      _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout);
    }
    _isDoubleTap = false;
  }

  void _handleTapCancel() {
    if (widget.onSingleTapCancel != null) {
      widget.onSingleTapCancel();
    }
  }

820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869
  DragStartDetails _lastDragStartDetails;
  DragUpdateDetails _lastDragUpdateDetails;
  Timer _dragUpdateThrottleTimer;

  void _handleDragStart(DragStartDetails details) {
    assert(_lastDragStartDetails == null);
    _lastDragStartDetails = details;
    if (widget.onDragSelectionStart != null) {
      widget.onDragSelectionStart(details);
    }
  }

  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);
    if (widget.onDragSelectionUpdate != null) {
      widget.onDragSelectionUpdate(_lastDragStartDetails, _lastDragUpdateDetails);
    }
    _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.
      _dragUpdateThrottleTimer.cancel();
      _handleDragUpdateThrottled();
    }
    if (widget.onDragSelectionEnd != null) {
      widget.onDragSelectionEnd(details);
    }
    _dragUpdateThrottleTimer = null;
    _lastDragStartDetails = null;
    _lastDragUpdateDetails = null;
  }

870 871 872
  void _forcePressStarted(ForcePressDetails details) {
    _doubleTapTimer?.cancel();
    _doubleTapTimer = null;
873 874
    if (widget.onForcePressStart != null)
      widget.onForcePressStart(details);
875 876 877 878 879 880 881
  }

  void _forcePressEnded(ForcePressDetails details) {
    if (widget.onForcePressEnd != null)
      widget.onForcePressEnd(details);
  }

882 883 884 885 886 887 888 889 890 891 892 893
  void _handleLongPressStart(LongPressStartDetails details) {
    if (!_isDoubleTap && widget.onSingleLongTapStart != null) {
      widget.onSingleLongTapStart(details);
    }
  }

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

894
  void _handleLongPressEnd(LongPressEndDetails details) {
895 896
    if (!_isDoubleTap && widget.onSingleLongTapEnd != null) {
      widget.onSingleLongTapEnd(details);
897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917
    }
    _isDoubleTap = false;
  }

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

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

    final Offset difference = secondTapOffset - _lastTapOffset;
    return difference.distance <= kDoubleTapSlop;
  }

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

    gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
921 922
      () => TapGestureRecognizer(debugOwner: this),
      (TapGestureRecognizer instance) {
923 924 925 926 927 928 929 930 931 932 933
        instance
          ..onTapDown = _handleTapDown
          ..onTapUp = _handleTapUp
          ..onTapCancel = _handleTapCancel;
      },
    );

    if (widget.onSingleLongTapStart != null ||
        widget.onSingleLongTapMoveUpdate != null ||
        widget.onSingleLongTapEnd != null) {
      gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
934 935
        () => LongPressGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.touch),
        (LongPressGestureRecognizer instance) {
936 937 938 939 940 941 942 943 944 945 946 947 948 949
          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>(
950 951
        () => HorizontalDragGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.mouse),
        (HorizontalDragGestureRecognizer instance) {
952 953 954 955 956 957 958 959 960 961 962 963 964
          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>(
965 966
        () => ForcePressGestureRecognizer(debugOwner: this),
        (ForcePressGestureRecognizer instance) {
967 968 969 970 971 972 973 974 975
          instance
            ..onStart = widget.onForcePressStart != null ? _forcePressStarted : null
            ..onEnd = widget.onForcePressEnd != null ? _forcePressEnded : null;
        },
      );
    }

    return RawGestureDetector(
      gestures: gestures,
976 977 978 979 980 981
      excludeFromSemantics: true,
      behavior: widget.behavior,
      child: widget.child,
    );
  }
}