refactor out selection handlers (#35207)

diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart
index 6a77c11..b43eb77 100644
--- a/packages/flutter/lib/src/cupertino/text_field.dart
+++ b/packages/flutter/lib/src/cupertino/text_field.dart
@@ -68,6 +68,39 @@
   always,
 }
 
+class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
+  _CupertinoTextFieldSelectionGestureDetectorBuilder({
+    @required _CupertinoTextFieldState state
+  }) : _state = state,
+       super(delegate: state);
+
+  final _CupertinoTextFieldState _state;
+
+  @override
+  void onSingleTapUp(TapUpDetails details) {
+    // Because TextSelectionGestureDetector listens to taps that happen on
+    // widgets in front of it, tapping the clear button will also trigger
+    // this handler. If the the clear button widget recognizes the up event,
+    // then do not handle it.
+    if (_state._clearGlobalKey.currentContext != null) {
+      final RenderBox renderBox = _state._clearGlobalKey.currentContext.findRenderObject();
+      final Offset localOffset = renderBox.globalToLocal(details.globalPosition);
+      if (renderBox.hitTest(BoxHitTestResult(), position: localOffset)) {
+        return;
+      }
+    }
+    super.onSingleTapUp(details);
+    _state._requestKeyboard();
+    if (_state.widget.onTap != null)
+      _state.widget.onTap();
+  }
+
+  @override
+  void onDragSelectionEnd(DragEndDetails details) {
+    _state._requestKeyboard();
+  }
+}
+
 /// An iOS-style text field.
 ///
 /// A text field lets the user enter text, either with a hardware keyboard or with
@@ -506,9 +539,8 @@
   }
 }
 
-class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticKeepAliveClientMixin {
+class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
   final GlobalKey _clearGlobalKey = GlobalKey();
-  final GlobalKey<EditableTextState> _editableTextKey = GlobalKey<EditableTextState>();
 
   TextEditingController _controller;
   TextEditingController get _effectiveController => widget.controller ?? _controller;
@@ -516,17 +548,25 @@
   FocusNode _focusNode;
   FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
 
-  // 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.
-  bool _shouldShowSelectionToolbar = true;
-
   bool _showSelectionHandles = false;
 
+  _CupertinoTextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
+
+  // API for TextSelectionGestureDetectorBuilderDelegate.
+  @override
+  bool get forcePressEnabled => true;
+
+  @override
+  final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
+
+  @override
+  bool get selectionEnabled => widget.selectionEnabled;
+  // End of API for TextSelectionGestureDetectorBuilderDelegate.
+
   @override
   void initState() {
     super.initState();
+    _selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(state: this);
     if (widget.controller == null) {
       _controller = TextEditingController();
       _controller.addListener(updateKeepAlive);
@@ -556,103 +596,16 @@
     super.dispose();
   }
 
-  EditableTextState get _editableText => _editableTextKey.currentState;
+  EditableTextState get _editableText => editableTextKey.currentState;
 
   void _requestKeyboard() {
     _editableText?.requestKeyboard();
   }
 
-  RenderEditable get _renderEditable => _editableText.renderEditable;
-
-  void _handleTapDown(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.
-    final PointerDeviceKind kind = details.kind;
-    _shouldShowSelectionToolbar =
-        kind == null ||
-        kind == PointerDeviceKind.touch ||
-        kind == PointerDeviceKind.stylus;
-  }
-
-  void _handleForcePressStarted(ForcePressDetails details) {
-    if (widget.selectionEnabled) {
-      _renderEditable.selectWordsInRange(
-        from: details.globalPosition,
-        cause: SelectionChangedCause.forcePress,
-      );
-    }
-  }
-
-  void _handleForcePressEnded(ForcePressDetails details) {
-    _renderEditable.selectWordsInRange(
-      from: details.globalPosition,
-      cause: SelectionChangedCause.forcePress,
-    );
-    if (_shouldShowSelectionToolbar)
-      _editableText.showToolbar();
-  }
-
-  void _handleSingleTapUp(TapUpDetails details) {
-    // Because TextSelectionGestureDetector listens to taps that happen on
-    // widgets in front of it, tapping the clear button will also trigger
-    // this handler. If the the clear button widget recognizes the up event,
-    // then do not handle it.
-    if (_clearGlobalKey.currentContext != null) {
-      final RenderBox renderBox = _clearGlobalKey.currentContext.findRenderObject();
-      final Offset localOffset = renderBox.globalToLocal(details.globalPosition);
-      if(renderBox.hitTest(BoxHitTestResult(), position: localOffset)) {
-        return;
-      }
-    }
-
-    if (widget.selectionEnabled) {
-      _renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
-    }
-    _requestKeyboard();
-    if (widget.onTap != null) {
-      widget.onTap();
-    }
-  }
-
-  void _handleSingleLongTapStart(LongPressStartDetails details) {
-    if (widget.selectionEnabled) {
-      _renderEditable.selectPositionAt(
-        from: details.globalPosition,
-        cause: SelectionChangedCause.longPress,
-      );
-    }
-  }
-
-  void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
-    if (widget.selectionEnabled) {
-      _renderEditable.selectPositionAt(
-        from: details.globalPosition,
-        cause: SelectionChangedCause.longPress,
-      );
-    }
-  }
-
-  void _handleSingleLongTapEnd(LongPressEndDetails details) {
-    if (_shouldShowSelectionToolbar)
-      _editableText.showToolbar();
-  }
-
-  void _handleDoubleTapDown(TapDownDetails details) {
-    if (widget.selectionEnabled) {
-      _renderEditable.selectWord(cause: SelectionChangedCause.tap);
-      if (_shouldShowSelectionToolbar)
-        _editableText.showToolbar();
-    }
-  }
-
   bool _shouldShowSelectionHandles(SelectionChangedCause cause) {
     // When the text field is activated by something that doesn't trigger the
     // selection overlay, we shouldn't show the handles either.
-    if (!_shouldShowSelectionToolbar)
+    if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
       return false;
 
     // On iOS, we don't show handles when the selection is collapsed.
@@ -668,28 +621,6 @@
     return false;
   }
 
-  void _handleMouseDragSelectionStart(DragStartDetails details) {
-    _renderEditable.selectPositionAt(
-      from: details.globalPosition,
-      cause: SelectionChangedCause.drag,
-    );
-  }
-
-  void _handleMouseDragSelectionUpdate(
-      DragStartDetails startDetails,
-      DragUpdateDetails updateDetails,
-  ) {
-    _renderEditable.selectPositionAt(
-      from: startDetails.globalPosition,
-      to: updateDetails.globalPosition,
-      cause: SelectionChangedCause.drag,
-    );
-  }
-
-  void _handleMouseDragSelectionEnd(DragEndDetails details) {
-    _requestKeyboard();
-  }
-
   void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
     if (cause == SelectionChangedCause.longPress) {
       _editableText?.bringIntoView(selection.base);
@@ -870,7 +801,7 @@
       padding: widget.padding,
       child: RepaintBoundary(
         child: EditableText(
-          key: _editableTextKey,
+          key: editableTextKey,
           controller: controller,
           readOnly: widget.readOnly,
           showCursor: widget.showCursor,
@@ -925,18 +856,7 @@
         ignoring: !enabled,
         child: Container(
           decoration: effectiveDecoration,
-          child: TextSelectionGestureDetector(
-            onTapDown: _handleTapDown,
-            onForcePressStart: _handleForcePressStarted,
-            onForcePressEnd: _handleForcePressEnded,
-            onSingleTapUp: _handleSingleTapUp,
-            onSingleLongTapStart: _handleSingleLongTapStart,
-            onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
-            onSingleLongTapEnd: _handleSingleLongTapEnd,
-            onDoubleTapDown: _handleDoubleTapDown,
-            onDragSelectionStart: _handleMouseDragSelectionStart,
-            onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
-            onDragSelectionEnd: _handleMouseDragSelectionEnd,
+          child: _selectionGestureDetectorBuilder.buildGestureDetector(
             behavior: HitTestBehavior.translucent,
             child: Align(
               alignment: Alignment(-1.0, _textAlignVertical.y),
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index 0a1d29a..ffb715b 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -35,6 +35,107 @@
   @required bool isFocused,
 });
 
+class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
+  _TextFieldSelectionGestureDetectorBuilder({
+    @required _TextFieldState state
+  }) : _state = state,
+       super(delegate: state);
+
+  final _TextFieldState _state;
+
+  @override
+  void onTapDown(TapDownDetails details) {
+    super.onTapDown(details);
+    _state._startSplash(details.globalPosition);
+  }
+
+  @override
+  void onForcePressStart(ForcePressDetails details) {
+    super.onForcePressStart(details);
+    if (delegate.selectionEnabled && shouldShowSelectionToolbar) {
+      editableText.showToolbar();
+    }
+  }
+
+  @override
+  void onForcePressEnd(ForcePressDetails details) {
+    // Not required.
+  }
+
+  @override
+  void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
+    if (delegate.selectionEnabled) {
+      switch (Theme.of(_state.context).platform) {
+        case TargetPlatform.iOS:
+          renderEditable.selectPositionAt(
+            from: details.globalPosition,
+            cause: SelectionChangedCause.longPress,
+          );
+          break;
+        case TargetPlatform.android:
+        case TargetPlatform.fuchsia:
+          renderEditable.selectWordsInRange(
+            from: details.globalPosition - details.offsetFromOrigin,
+            to: details.globalPosition,
+            cause: SelectionChangedCause.longPress,
+          );
+          break;
+      }
+    }
+  }
+
+  @override
+  void onSingleTapUp(TapUpDetails details) {
+    editableText.hideToolbar();
+    if (delegate.selectionEnabled) {
+      switch (Theme.of(_state.context).platform) {
+        case TargetPlatform.iOS:
+          renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
+          break;
+        case TargetPlatform.android:
+        case TargetPlatform.fuchsia:
+          renderEditable.selectPosition(cause: SelectionChangedCause.tap);
+          break;
+      }
+    }
+    _state._requestKeyboard();
+    _state._confirmCurrentSplash();
+    if (_state.widget.onTap != null)
+      _state.widget.onTap();
+  }
+
+  @override
+  void onSingleTapCancel() {
+    _state._cancelCurrentSplash();
+  }
+
+  @override
+  void onSingleLongTapStart(LongPressStartDetails details) {
+    if (delegate.selectionEnabled) {
+      switch (Theme.of(_state.context).platform) {
+        case TargetPlatform.iOS:
+          renderEditable.selectPositionAt(
+            from: details.globalPosition,
+            cause: SelectionChangedCause.longPress,
+          );
+          break;
+        case TargetPlatform.android:
+        case TargetPlatform.fuchsia:
+        renderEditable.selectWord(cause: SelectionChangedCause.longPress);
+          Feedback.forLongPress(_state.context);
+          break;
+      }
+    }
+    _state._confirmCurrentSplash();
+  }
+
+  @override
+  void onDragSelectionStart(DragStartDetails details) {
+    super.onDragSelectionStart(details);
+    _state._startSplash(details.globalPosition);
+  }
+}
+
 /// A material design text field.
 ///
 /// A text field lets the user enter text, either with hardware keyboard or with
@@ -531,9 +632,7 @@
   }
 }
 
-class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixin {
-  final GlobalKey<EditableTextState> _editableTextKey = GlobalKey<EditableTextState>();
-
+class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
   Set<InteractiveInkFeature> _splashes;
   InteractiveInkFeature _currentSplash;
 
@@ -549,10 +648,21 @@
     && widget.decoration != null
     && widget.decoration.counterText == null;
 
-  bool _shouldShowSelectionToolbar = true;
-
   bool _showSelectionHandles = false;
 
+  _TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
+
+  // API for TextSelectionGestureDetectorBuilderDelegate.
+  @override
+  bool forcePressEnabled;
+
+  @override
+  final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
+
+  @override
+  bool get selectionEnabled => widget.selectionEnabled;
+  // End of API for TextSelectionGestureDetectorBuilderDelegate.
+
   InputDecoration _getEffectiveDecoration() {
     final MaterialLocalizations localizations = MaterialLocalizations.of(context);
     final ThemeData themeData = Theme.of(context);
@@ -621,6 +731,7 @@
   @override
   void initState() {
     super.initState();
+    _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
     if (widget.controller == null)
       _controller = TextEditingController();
   }
@@ -650,7 +761,7 @@
     super.dispose();
   }
 
-  EditableTextState get _editableText => _editableTextKey.currentState;
+  EditableTextState get _editableText => editableTextKey.currentState;
 
   void _requestKeyboard() {
     _editableText?.requestKeyboard();
@@ -659,7 +770,7 @@
   bool _shouldShowSelectionHandles(SelectionChangedCause cause) {
     // When the text field is activated by something that doesn't trigger the
     // selection overlay, we shouldn't show the handles either.
-    if (!_shouldShowSelectionToolbar)
+    if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
       return false;
 
     if (cause == SelectionChangedCause.keyboard)
@@ -707,7 +818,7 @@
   InteractiveInkFeature _createInkFeature(Offset globalPosition) {
     final MaterialInkController inkController = Material.of(context);
     final ThemeData themeData = Theme.of(context);
-    final BuildContext editableContext = _editableTextKey.currentContext;
+    final BuildContext editableContext = editableTextKey.currentContext;
     final RenderBox referenceBox = InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject();
     final Offset position = referenceBox.globalToLocal(globalPosition);
     final Color color = themeData.splashColor;
@@ -738,133 +849,6 @@
     return splash;
   }
 
-  RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable;
-
-  void _handleTapDown(TapDownDetails details) {
-    _renderEditable.handleTapDown(details);
-    _startSplash(details.globalPosition);
-
-    // 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.
-    final PointerDeviceKind kind = details.kind;
-    _shouldShowSelectionToolbar =
-        kind == null ||
-        kind == PointerDeviceKind.touch ||
-        kind == PointerDeviceKind.stylus;
-  }
-
-  void _handleForcePressStarted(ForcePressDetails details) {
-    if (widget.selectionEnabled) {
-      _renderEditable.selectWordsInRange(
-        from: details.globalPosition,
-        cause: SelectionChangedCause.forcePress,
-      );
-      if (_shouldShowSelectionToolbar) {
-        _editableTextKey.currentState.showToolbar();
-      }
-    }
-  }
-
-  void _handleSingleTapUp(TapUpDetails details) {
-    _editableTextKey.currentState.hideToolbar();
-    if (widget.selectionEnabled) {
-      switch (Theme.of(context).platform) {
-        case TargetPlatform.iOS:
-          _renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
-          break;
-        case TargetPlatform.android:
-        case TargetPlatform.fuchsia:
-          _renderEditable.selectPosition(cause: SelectionChangedCause.tap);
-          break;
-      }
-    }
-    _requestKeyboard();
-    _confirmCurrentSplash();
-    if (widget.onTap != null)
-      widget.onTap();
-  }
-
-  void _handleSingleTapCancel() {
-    _cancelCurrentSplash();
-  }
-
-  void _handleSingleLongTapStart(LongPressStartDetails details) {
-    if (widget.selectionEnabled) {
-      switch (Theme.of(context).platform) {
-        case TargetPlatform.iOS:
-          _renderEditable.selectPositionAt(
-            from: details.globalPosition,
-            cause: SelectionChangedCause.longPress,
-          );
-          break;
-        case TargetPlatform.android:
-        case TargetPlatform.fuchsia:
-          _renderEditable.selectWord(cause: SelectionChangedCause.longPress);
-          Feedback.forLongPress(context);
-          break;
-      }
-    }
-    _confirmCurrentSplash();
-  }
-
-  void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
-    if (widget.selectionEnabled) {
-      switch (Theme.of(context).platform) {
-        case TargetPlatform.iOS:
-          _renderEditable.selectPositionAt(
-            from: details.globalPosition,
-            cause: SelectionChangedCause.longPress,
-          );
-          break;
-        case TargetPlatform.android:
-        case TargetPlatform.fuchsia:
-          _renderEditable.selectWordsInRange(
-            from: details.globalPosition - details.offsetFromOrigin,
-            to: details.globalPosition,
-            cause: SelectionChangedCause.longPress,
-          );
-          break;
-      }
-    }
-  }
-
-  void _handleSingleLongTapEnd(LongPressEndDetails details) {
-    if (widget.selectionEnabled) {
-      if (_shouldShowSelectionToolbar)
-        _editableTextKey.currentState.showToolbar();
-    }
-  }
-
-  void _handleDoubleTapDown(TapDownDetails details) {
-    if (widget.selectionEnabled) {
-      _renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
-      if (_shouldShowSelectionToolbar) {
-        _editableText.showToolbar();
-      }
-    }
-  }
-
-  void _handleMouseDragSelectionStart(DragStartDetails details) {
-    _renderEditable.selectPositionAt(
-      from: details.globalPosition,
-      cause: SelectionChangedCause.drag,
-    );
-    _startSplash(details.globalPosition);
-  }
-
-  void _handleMouseDragSelectionUpdate(
-      DragStartDetails startDetails,
-      DragUpdateDetails updateDetails,
-  ) {
-    _renderEditable.selectPositionAt(
-      from: startDetails.globalPosition,
-      to: updateDetails.globalPosition,
-      cause: SelectionChangedCause.drag,
-    );
-  }
-
   void _startSplash(Offset globalPosition) {
     if (_effectiveFocusNode.hasFocus)
       return;
@@ -933,7 +917,6 @@
     if (widget.maxLength != null && widget.maxLengthEnforced)
       formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
 
-    bool forcePressEnabled;
     TextSelectionControls textSelectionControls;
     bool paintCursorAboveText;
     bool cursorOpacityAnimates;
@@ -971,7 +954,7 @@
 
     Widget child = RepaintBoundary(
       child: EditableText(
-        key: _editableTextKey,
+        key: editableTextKey,
         readOnly: widget.readOnly,
         showCursor: widget.showCursor,
         showSelectionHandles: _showSelectionHandles,
@@ -1046,17 +1029,7 @@
         onPointerExit: _handlePointerExit,
         child: IgnorePointer(
           ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
-          child: TextSelectionGestureDetector(
-            onTapDown: _handleTapDown,
-            onForcePressStart: forcePressEnabled ? _handleForcePressStarted : null,
-            onSingleTapUp: _handleSingleTapUp,
-            onSingleTapCancel: _handleSingleTapCancel,
-            onSingleLongTapStart: _handleSingleLongTapStart,
-            onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
-            onSingleLongTapEnd: _handleSingleLongTapEnd,
-            onDoubleTapDown: _handleDoubleTapDown,
-            onDragSelectionStart: _handleMouseDragSelectionStart,
-            onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
+          child: _selectionGestureDetectorBuilder.buildGestureDetector(
             behavior: HitTestBehavior.translucent,
             child: child,
           ),
diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart
index ea572c9..af17c6c 100644
--- a/packages/flutter/lib/src/widgets/text_selection.dart
+++ b/packages/flutter/lib/src/widgets/text_selection.dart
@@ -813,6 +813,318 @@
   }
 }
 
+/// 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:
+///
+/// * [TextField], which implements this delegate for the Material textfield.
+/// * [CupertinoTextField], which implements this delegate for the Cupertino textfield.
+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
+/// methods, e.g. [onTapDown], [onFrocePress], etc.). Subclasses of
+/// [EditableTextSelectionHandlesProvider] can change the behavior performed in
+/// 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({
+    @required this.delegate,
+  }) : 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;
+
+  /// Whether to show the selection tool bar.
+  ///
+  /// 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
+  EditableTextState get editableText => delegate.editableTextKey.currentState;
+
+  /// The [RenderObject] of the [EditableText] for which the builder will
+  /// provide a [TextSelectionGestureDetector].
+  @protected
+  RenderEditable get renderEditable => editableText.renderEditable;
+
+  /// 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.
+    final PointerDeviceKind kind = details.kind;
+    _shouldShowSelectionToolbar = kind == null
+                              || kind == PointerDeviceKind.touch
+                              || kind == PointerDeviceKind.stylus;
+  }
+
+  /// 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
+  /// tool bar if it is necessary.
+  ///
+  /// 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)
+      editableText.showToolbar();
+  }
+
+  /// 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].
+  ///
+  /// By default, it shows tool bar if necessary.
+  ///
+  /// See also:
+  ///
+  ///  * [TextSelectionGestureDetector.onSingleLongTapEnd], which triggers this
+  ///    callback.
+  @protected
+  void onSingleLongTapEnd(LongPressEndDetails details) {
+    if (shouldShowSelectionToolbar)
+      editableText.showToolbar();
+  }
+
+  /// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
+  ///
+  /// By default, it selects a word through [renderEditable.selectWord] if
+  /// selectionEnabled and shows tool bar if necessary.
+  ///
+  /// See also:
+  ///
+  ///  * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this
+  ///    callback.
+  @protected
+  void onDoubleTapDown(TapDownDetails details) {
+    if (delegate.selectionEnabled) {
+      renderEditable.selectWord(cause: SelectionChangedCause.tap);
+      if (shouldShowSelectionToolbar)
+        editableText.showToolbar();
+    }
+  }
+
+  /// 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) {
+    renderEditable.selectPositionAt(
+      from: details.globalPosition,
+      cause: SelectionChangedCause.drag,
+    );
+  }
+
+  /// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate].
+  ///
+  /// By default, it updates the selection location specified in [details].
+  ///
+  /// See also:
+  ///
+  ///  * [TextSelectionGestureDetector.onDragSelectionUpdate], which triggers
+  ///    this callback./lib/src/material/text_field.dart
+  @protected
+  void onDragSelectionUpdate(DragStartDetails startDetails, DragUpdateDetails updateDetails) {
+    renderEditable.selectPositionAt(
+      from: startDetails.globalPosition,
+      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({
+    Key key,
+    HitTestBehavior behavior,
+    Widget child
+  }) {
+    return TextSelectionGestureDetector(
+      key: key,
+      onTapDown: onTapDown,
+      onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
+      onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
+      onSingleTapUp: onSingleTapUp,
+      onSingleTapCancel: onSingleTapCancel,
+      onSingleLongTapStart: onSingleLongTapStart,
+      onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
+      onSingleLongTapEnd: onSingleLongTapEnd,
+      onDoubleTapDown: onDoubleTapDown,
+      onDragSelectionStart: onDragSelectionStart,
+      onDragSelectionUpdate: onDragSelectionUpdate,
+      onDragSelectionEnd: onDragSelectionEnd,
+      behavior: behavior,
+      child: child,
+    );
+  }
+}
+
 /// A gesture detector to respond to non-exclusive event chains for a text field.
 ///
 /// An ordinary [GestureDetector] configured to handle events like tap and
diff --git a/packages/flutter/test/widgets/text_selection_test.dart b/packages/flutter/test/widgets/text_selection_test.dart
index b978ef5..41ebfe6 100644
--- a/packages/flutter/test/widgets/text_selection_test.dart
+++ b/packages/flutter/test/widgets/text_selection_test.dart
@@ -5,6 +5,8 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:flutter/gestures.dart' show PointerDeviceKind;
 import 'package:flutter/widgets.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/material.dart';
 
 void main() {
   int tapCount;
@@ -62,6 +64,30 @@
     );
   }
 
+  Future<void> pumpTextSelectionGestureDetectorBuilder(
+    WidgetTester tester, {
+    bool forcePressEnabled = true,
+    bool selectionEnabled = true,
+  }) async {
+    final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
+    final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate(
+      editableTextKey: editableTextKey,
+      forcePressEnabled: forcePressEnabled,
+      selectionEnabled: selectionEnabled,
+    );
+    final TextSelectionGestureDetectorBuilder provider =
+    TextSelectionGestureDetectorBuilder(delegate: delegate);
+
+    await tester.pumpWidget(
+      MaterialApp(
+        home: provider.buildGestureDetector(
+          behavior: HitTestBehavior.translucent,
+          child: FakeEditableText(key: editableTextKey)
+        )
+      )
+    );
+  }
+
   testWidgets('a series of taps all call onTaps', (WidgetTester tester) async {
     await pumpGestureDetector(tester);
     await tester.tapAt(const Offset(200, 200));
@@ -380,4 +406,221 @@
 
     await gesture.removePointer();
   });
+
+  testWidgets('test TextSelectionGestureDetectorBuilder long press', (WidgetTester tester) async {
+    await pumpTextSelectionGestureDetectorBuilder(tester);
+    final TestGesture gesture =
+      await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
+    await tester.pump(const Duration(seconds: 2));
+    await gesture.up();
+    await tester.pumpAndSettle();
+
+    final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+    final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+    expect(state.showToolbarCalled, isTrue);
+    expect(renderEditable.selectPositionAtCalled, isTrue);
+  });
+
+  testWidgets('test TextSelectionGestureDetectorBuilder tap', (WidgetTester tester) async {
+    await pumpTextSelectionGestureDetectorBuilder(tester);
+    final TestGesture gesture =
+      await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
+    await gesture.up();
+    await tester.pumpAndSettle();
+
+    final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+    final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+    expect(state.showToolbarCalled, isFalse);
+    expect(renderEditable.selectWordEdgeCalled, isTrue);
+  });
+
+  testWidgets('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async {
+    await pumpTextSelectionGestureDetectorBuilder(tester);
+    final TestGesture gesture =
+      await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
+    await tester.pump(const Duration(milliseconds: 50));
+    await gesture.up();
+    await gesture.down(const Offset(200.0, 200.0));
+    await tester.pump(const Duration(milliseconds: 50));
+    await gesture.up();
+    await tester.pumpAndSettle();
+
+    final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+    final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+    expect(state.showToolbarCalled, isTrue);
+    expect(renderEditable.selectWordCalled, isTrue);
+  });
+
+  testWidgets('test TextSelectionGestureDetectorBuilder forcePress enabled', (WidgetTester tester) async {
+    await pumpTextSelectionGestureDetectorBuilder(tester);
+    final TestGesture gesture = await tester.createGesture();
+    await gesture.downWithCustomEvent(
+      const Offset(200.0, 200.0),
+      const PointerDownEvent(
+        pointer: 0,
+        position: Offset(200.0, 200.0),
+        pressure: 3.0,
+        pressureMax: 6.0,
+        pressureMin: 0.0,
+      ),
+    );
+    await gesture.updateWithCustomEvent(
+      const PointerUpEvent(
+        pointer: 0,
+        position: Offset(200.0, 200.0),
+        pressure: 0.0,
+        pressureMax: 6.0,
+        pressureMin: 0.0,
+      ),
+    );
+    await tester.pump();
+
+    final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+    final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+    expect(state.showToolbarCalled, isTrue);
+    expect(renderEditable.selectWordsInRangeCalled, isTrue);
+  });
+
+  testWidgets('test TextSelectionGestureDetectorBuilder selection disabled', (WidgetTester tester) async {
+    await pumpTextSelectionGestureDetectorBuilder(tester, selectionEnabled: false);
+    final TestGesture gesture =
+      await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
+    await tester.pump(const Duration(seconds: 2));
+    await gesture.up();
+    await tester.pumpAndSettle();
+
+    final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+    final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+    expect(state.showToolbarCalled, isTrue);
+    expect(renderEditable.selectWordsInRangeCalled, isFalse);
+  });
+
+  testWidgets('test TextSelectionGestureDetectorBuilder forcePress disabled', (WidgetTester tester) async {
+    await pumpTextSelectionGestureDetectorBuilder(tester, forcePressEnabled: false);
+    final TestGesture gesture = await tester.createGesture();
+    await gesture.downWithCustomEvent(
+      const Offset(200.0, 200.0),
+      const PointerDownEvent(
+        pointer: 0,
+        position: Offset(200.0, 200.0),
+        pressure: 3.0,
+        pressureMax: 6.0,
+        pressureMin: 0.0,
+      ),
+    );
+    await gesture.up();
+    await tester.pump();
+
+    final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
+    final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
+    expect(state.showToolbarCalled, isFalse);
+    expect(renderEditable.selectWordsInRangeCalled, isFalse);
+  });
+}
+
+class FakeTextSelectionGestureDetectorBuilderDelegate implements TextSelectionGestureDetectorBuilderDelegate {
+  FakeTextSelectionGestureDetectorBuilderDelegate({
+    this.editableTextKey,
+    this.forcePressEnabled,
+    this.selectionEnabled,
+  });
+
+  @override
+  final GlobalKey<EditableTextState> editableTextKey;
+
+  @override
+  final bool forcePressEnabled;
+
+  @override
+  final bool selectionEnabled;
+}
+
+class FakeEditableText extends EditableText {
+  FakeEditableText({Key key}): super(
+    key: key,
+    controller: TextEditingController(),
+    focusNode: FocusNode(),
+    backgroundCursorColor: Colors.white,
+    cursorColor: Colors.white,
+    style: const TextStyle(),
+  );
+
+  @override
+  FakeEditableTextState createState() => FakeEditableTextState();
+}
+
+class FakeEditableTextState extends EditableTextState {
+  final GlobalKey _editableKey = GlobalKey();
+  bool showToolbarCalled = false;
+
+  @override
+  RenderEditable get renderEditable => _editableKey.currentContext.findRenderObject();
+
+  @override
+  bool showToolbar() {
+    showToolbarCalled = true;
+    return true;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    super.build(context);
+    return FakeEditable(this, key: _editableKey);
+  }
+}
+
+class FakeEditable extends LeafRenderObjectWidget {
+  const FakeEditable(
+    this.delegate, {
+    Key key,
+  }) : super(key: key);
+  final EditableTextState delegate;
+
+  @override
+  RenderEditable createRenderObject(BuildContext context) {
+    return FakeRenderEditable(delegate);
+  }
+}
+
+class FakeRenderEditable extends RenderEditable {
+  FakeRenderEditable(EditableTextState delegate) : super(
+    text: const TextSpan(
+      style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
+      text: 'placeholder',
+    ),
+    startHandleLayerLink: LayerLink(),
+    endHandleLayerLink: LayerLink(),
+    textAlign: TextAlign.start,
+    textDirection: TextDirection.ltr,
+    locale: const Locale('en', 'US'),
+    offset: ViewportOffset.fixed(10.0),
+    textSelectionDelegate: delegate,
+    selection: const TextSelection.collapsed(
+      offset: 0,
+    ),
+  );
+
+  bool selectWordsInRangeCalled = false;
+  @override
+  void selectWordsInRange({ @required Offset from, Offset to, @required SelectionChangedCause cause }) {
+    selectWordsInRangeCalled = true;
+  }
+
+  bool selectWordEdgeCalled = false;
+  @override
+  void selectWordEdge({ @required SelectionChangedCause cause }) {
+    selectWordEdgeCalled = true;
+  }
+
+  bool selectPositionAtCalled = false;
+  @override
+  void selectPositionAt({ @required Offset from, Offset to, @required SelectionChangedCause cause }) {
+    selectPositionAtCalled = true;
+  }
+
+  bool selectWordCalled = false;
+  @override
+  void selectWord({ @required SelectionChangedCause cause }) {
+    selectWordCalled = true;
+  }
 }