LCOV - code coverage report
Current view: top level - editing/text_selection - mongol_text_selection.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 214 286 74.8 %
Date: 2021-08-02 17:55:49 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright 2014 The Flutter Authors.
       2             : // Copyright 2021 Suragch.
       3             : // All rights reserved.
       4             : // Use of this source code is governed by a BSD-style license that can be
       5             : // found in the LICENSE file.
       6             : 
       7             : import 'dart:math' as math;
       8             : 
       9             : import 'package:flutter/foundation.dart' show ValueListenable;
      10             : import 'package:flutter/gestures.dart';
      11             : import 'package:flutter/material.dart' show kMinInteractiveDimension;
      12             : import 'package:flutter/scheduler.dart' show SchedulerBinding, SchedulerPhase;
      13             : export 'package:flutter/services.dart' show TextSelectionDelegate;
      14             : import 'package:flutter/widgets.dart';
      15             : 
      16             : import '../mongol_editable_text.dart';
      17             : import '../mongol_render_editable.dart';
      18             : 
      19             : /// The text position that a give selection handle manipulates. Dragging the
      20             : /// [start] handle always moves the [start]/[baseOffset] of the selection.
      21          10 : enum _TextSelectionHandlePosition { start, end }
      22             : 
      23             : /// An object that manages a pair of text selection handles.
      24             : ///
      25             : /// The selection handles are displayed in the [Overlay] that most closely
      26             : /// encloses the given [BuildContext].
      27             : class MongolTextSelectionOverlay {
      28             :   /// Creates an object that manages overlay entries for selection handles.
      29             :   ///
      30             :   /// The [context] must not be null and must have an [Overlay] as an ancestor.
      31           2 :   MongolTextSelectionOverlay({
      32             :     required TextEditingValue value,
      33             :     required this.context,
      34             :     this.debugRequiredFor,
      35             :     required this.toolbarLayerLink,
      36             :     required this.startHandleLayerLink,
      37             :     required this.endHandleLayerLink,
      38             :     required this.renderObject,
      39             :     this.selectionControls,
      40             :     bool handlesVisible = false,
      41             :     this.selectionDelegate,
      42             :     this.dragStartBehavior = DragStartBehavior.start,
      43             :     this.onSelectionHandleTapped,
      44             :     this.clipboardStatus,
      45             :   })  : _handlesVisible = handlesVisible,
      46             :         _value = value {
      47           4 :     final overlay = Overlay.of(context, rootOverlay: true);
      48             :     assert(
      49           0 :         overlay != null,
      50           0 :         'No Overlay widget exists above $context.\n'
      51             :         'Usually the Navigator created by WidgetsApp provides the overlay. Perhaps your '
      52             :         'app content was created above the Navigator with the WidgetsApp builder parameter.');
      53           2 :     _toolbarController =
      54           2 :         AnimationController(duration: fadeDuration, vsync: overlay!);
      55             :   }
      56             : 
      57             :   /// The context in which the selection handles should appear.
      58             :   ///
      59             :   /// This context must have an [Overlay] as an ancestor because this object
      60             :   /// will display the text selection handles in that [Overlay].
      61             :   final BuildContext context;
      62             : 
      63             :   /// Debugging information for explaining why the [Overlay] is required.
      64             :   final Widget? debugRequiredFor;
      65             : 
      66             :   /// The object supplied to the [CompositedTransformTarget] that wraps the text
      67             :   /// field.
      68             :   final LayerLink toolbarLayerLink;
      69             : 
      70             :   /// The objects supplied to the [CompositedTransformTarget] that wraps the
      71             :   /// location of start selection handle.
      72             :   final LayerLink startHandleLayerLink;
      73             : 
      74             :   /// The objects supplied to the [CompositedTransformTarget] that wraps the
      75             :   /// location of end selection handle.
      76             :   final LayerLink endHandleLayerLink;
      77             : 
      78             :   /// The editable line in which the selected text is being displayed.
      79             :   final MongolRenderEditable renderObject;
      80             : 
      81             :   /// Builds text selection handles and toolbar.
      82             :   final TextSelectionControls? selectionControls;
      83             : 
      84             :   /// The delegate for manipulating the current selection in the owning
      85             :   /// text field.
      86             :   final TextSelectionDelegate? selectionDelegate;
      87             : 
      88             :   /// Determines the way that drag start behavior is handled.
      89             :   ///
      90             :   /// If set to [DragStartBehavior.start], handle drag behavior will
      91             :   /// begin upon the detection of a drag gesture. If set to
      92             :   /// [DragStartBehavior.down] it will begin when a down event is first detected.
      93             :   ///
      94             :   /// In general, setting this to [DragStartBehavior.start] will make drag
      95             :   /// animation smoother and setting it to [DragStartBehavior.down] will make
      96             :   /// drag behavior feel slightly more reactive.
      97             :   ///
      98             :   /// By default, the drag start behavior is [DragStartBehavior.start].
      99             :   ///
     100             :   /// See also:
     101             :   ///
     102             :   ///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
     103             :   final DragStartBehavior dragStartBehavior;
     104             : 
     105             :   /// A callback that's invoked when a selection handle is tapped.
     106             :   ///
     107             :   /// Both regular taps and long presses invoke this callback, but a drag
     108             :   /// gesture won't.
     109             :   final VoidCallback? onSelectionHandleTapped;
     110             : 
     111             :   /// Maintains the status of the clipboard for determining if its contents can
     112             :   /// be pasted or not.
     113             :   ///
     114             :   /// Useful because the actual value of the clipboard can only be checked
     115             :   /// asynchronously (see [Clipboard.getData]).
     116             :   final ClipboardStatusNotifier? clipboardStatus;
     117             : 
     118             :   /// Controls the fade-in and fade-out animations for the toolbar and handles.
     119             :   static const Duration fadeDuration = Duration(milliseconds: 150);
     120             : 
     121             :   late AnimationController _toolbarController;
     122           6 :   Animation<double> get _toolbarOpacity => _toolbarController.view;
     123             : 
     124             :   /// Retrieve current value.
     125           1 :   @visibleForTesting
     126           1 :   TextEditingValue get value => _value;
     127             : 
     128             :   TextEditingValue _value;
     129             : 
     130             :   /// A pair of handles. If this is non-null, there are always 2, though the
     131             :   /// second is hidden when the selection is collapsed.
     132             :   List<OverlayEntry>? _handles;
     133             : 
     134             :   /// A copy/paste toolbar.
     135             :   OverlayEntry? _toolbar;
     136             : 
     137           6 :   TextSelection get _selection => _value.selection;
     138             : 
     139             :   /// Whether selection handles are visible.
     140             :   ///
     141             :   /// Set to false if you want to hide the handles. Use this property to show or
     142             :   /// hide the handle without rebuilding them.
     143             :   ///
     144             :   /// If this method is called while the [SchedulerBinding.schedulerPhase] is
     145             :   /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
     146             :   /// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed
     147             :   /// until the post-frame callbacks phase. Otherwise the update is done
     148             :   /// synchronously. This means that it is safe to call during builds, but also
     149             :   /// that if you do call this during a build, the UI will not update until the
     150             :   /// next frame (i.e. many milliseconds later).
     151             :   ///
     152             :   /// Defaults to false.
     153           4 :   bool get handlesVisible => _handlesVisible;
     154             :   bool _handlesVisible = false;
     155           2 :   set handlesVisible(bool visible) {
     156           4 :     if (_handlesVisible == visible) {
     157             :       return;
     158             :     }
     159           2 :     _handlesVisible = visible;
     160             :     // If we are in build state, it will be too late to update visibility.
     161             :     // We will need to schedule the build in next frame.
     162           6 :     if (SchedulerBinding.instance!.schedulerPhase ==
     163             :         SchedulerPhase.persistentCallbacks) {
     164           3 :       SchedulerBinding.instance!.addPostFrameCallback(_markNeedsBuild);
     165             :     } else {
     166           1 :       _markNeedsBuild();
     167             :     }
     168             :   }
     169             : 
     170             :   /// Builds the handles by inserting them into the [context]'s overlay.
     171           2 :   void showHandles() {
     172           2 :     if (_handles != null) {
     173             :       return;
     174             :     }
     175             : 
     176           4 :     _handles = <OverlayEntry>[
     177           2 :       OverlayEntry(
     178           2 :           builder: (BuildContext context) =>
     179           2 :               _buildHandle(context, _TextSelectionHandlePosition.start)),
     180           2 :       OverlayEntry(
     181           2 :           builder: (BuildContext context) =>
     182           2 :               _buildHandle(context, _TextSelectionHandlePosition.end)),
     183             :     ];
     184             : 
     185           6 :     Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
     186           4 :         .insertAll(_handles!);
     187             :   }
     188             : 
     189             :   /// Destroys the handles by removing them from overlay.
     190           0 :   void hideHandles() {
     191           0 :     if (_handles != null) {
     192           0 :       _handles![0].remove();
     193           0 :       _handles![1].remove();
     194           0 :       _handles = null;
     195             :     }
     196             :   }
     197             : 
     198             :   /// Shows the toolbar by inserting it into the [context]'s overlay.
     199           2 :   void showToolbar() {
     200           2 :     assert(_toolbar == null);
     201           6 :     _toolbar = OverlayEntry(builder: _buildToolbar);
     202           6 :     Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
     203           4 :         .insert(_toolbar!);
     204           4 :     _toolbarController.forward(from: 0.0);
     205             :   }
     206             : 
     207             :   /// Updates the overlay after the selection has changed.
     208             :   ///
     209             :   /// If this method is called while the [SchedulerBinding.schedulerPhase] is
     210             :   /// [SchedulerPhase.persistentCallbacks], i.e. during the build, layout, or
     211             :   /// paint phases (see [WidgetsBinding.drawFrame]), then the update is delayed
     212             :   /// until the post-frame callbacks phase. Otherwise the update is done
     213             :   /// synchronously. This means that it is safe to call during builds, but also
     214             :   /// that if you do call this during a build, the UI will not update until the
     215             :   /// next frame (i.e. many milliseconds later).
     216           2 :   void update(TextEditingValue newValue) {
     217           4 :     if (_value == newValue) return;
     218           2 :     _value = newValue;
     219           6 :     if (SchedulerBinding.instance!.schedulerPhase ==
     220             :         SchedulerPhase.persistentCallbacks) {
     221           6 :       SchedulerBinding.instance!.addPostFrameCallback(_markNeedsBuild);
     222             :     } else {
     223           2 :       _markNeedsBuild();
     224             :     }
     225             :   }
     226             : 
     227             :   /// Causes the overlay to update its rendering.
     228             :   ///
     229             :   /// This is intended to be called when the [renderObject] may have changed its
     230             :   /// text metrics (e.g. because the text was scrolled).
     231           1 :   void updateForScroll() {
     232           1 :     _markNeedsBuild();
     233             :   }
     234             : 
     235           2 :   void _markNeedsBuild([Duration? duration]) {
     236           2 :     if (_handles != null) {
     237           6 :       _handles![0].markNeedsBuild();
     238           6 :       _handles![1].markNeedsBuild();
     239             :     }
     240           3 :     _toolbar?.markNeedsBuild();
     241             :   }
     242             : 
     243             :   /// Whether the handles are currently visible.
     244           3 :   bool get handlesAreVisible => _handles != null && handlesVisible;
     245             : 
     246             :   /// Whether the toolbar is currently visible.
     247           4 :   bool get toolbarIsVisible => _toolbar != null;
     248             : 
     249             :   /// Hides the entire overlay including the toolbar and the handles.
     250           2 :   void hide() {
     251           2 :     if (_handles != null) {
     252           6 :       _handles![0].remove();
     253           6 :       _handles![1].remove();
     254           2 :       _handles = null;
     255             :     }
     256           2 :     if (_toolbar != null) {
     257           2 :       hideToolbar();
     258             :     }
     259             :   }
     260             : 
     261             :   /// Hides the toolbar part of the overlay.
     262             :   ///
     263             :   /// To hide the whole overlay, see [hide].
     264           2 :   void hideToolbar() {
     265           2 :     assert(_toolbar != null);
     266           4 :     _toolbarController.stop();
     267           4 :     _toolbar!.remove();
     268           2 :     _toolbar = null;
     269             :   }
     270             : 
     271             :   /// Final cleanup.
     272           2 :   void dispose() {
     273           2 :     hide();
     274           4 :     _toolbarController.dispose();
     275             :   }
     276             : 
     277           2 :   Widget _buildHandle(
     278             :       BuildContext context, _TextSelectionHandlePosition position) {
     279             :     Widget handle;
     280           4 :     if ((_selection.isCollapsed &&
     281           2 :             position == _TextSelectionHandlePosition.end) ||
     282           2 :         selectionControls == null) {
     283           2 :       handle = Container(); // hide the second handle when collapsed
     284             :     } else {
     285           2 :       handle = Visibility(
     286           2 :         visible: handlesVisible,
     287           2 :         child: _TextSelectionHandleOverlay(
     288           0 :           onSelectionHandleChanged: (TextSelection newSelection) {
     289           0 :             _handleSelectionHandleChanged(newSelection, position);
     290             :           },
     291           2 :           onSelectionHandleTapped: onSelectionHandleTapped,
     292           2 :           startHandleLayerLink: startHandleLayerLink,
     293           2 :           endHandleLayerLink: endHandleLayerLink,
     294           2 :           renderObject: renderObject,
     295           2 :           selection: _selection,
     296           2 :           selectionControls: selectionControls,
     297             :           position: position,
     298           2 :           dragStartBehavior: dragStartBehavior,
     299             :         ),
     300             :       );
     301             :     }
     302           2 :     return ExcludeSemantics(
     303             :       child: handle,
     304             :     );
     305             :   }
     306             : 
     307           2 :   Widget _buildToolbar(BuildContext context) {
     308           2 :     if (selectionControls == null) return Container();
     309             : 
     310             :     // Find the vertical midpoint, just to the left of the selected text.
     311           6 :     final endpoints = renderObject.getEndpointsForSelection(_selection);
     312           2 :     final editingRegion = Rect.fromPoints(
     313           4 :       renderObject.localToGlobal(Offset.zero),
     314          10 :       renderObject.localToGlobal(renderObject.size.bottomRight(Offset.zero)),
     315             :     );
     316          16 :     final isMultiline = endpoints.last.point.dx - endpoints.first.point.dx >
     317           6 :         renderObject.preferredLineWidth / 2;
     318             : 
     319             :     // If the selected text spans more than 1 line, vertically center the toolbar.
     320             :     // Derived from both iOS and Android.
     321             :     final midY = (isMultiline)
     322           4 :         ? editingRegion.height / 2
     323          16 :         : (endpoints.first.point.dy + endpoints.last.point.dy) / 2;
     324             : 
     325           2 :     final midpoint = Offset(
     326             :       // The x-coordinate won't be made use of most likely.
     327          12 :       endpoints[0].point.dx - renderObject.preferredLineWidth,
     328             :       midY,
     329             :     );
     330             : 
     331           2 :     return Directionality(
     332             :       textDirection: TextDirection.ltr,
     333           2 :       child: FadeTransition(
     334           2 :         opacity: _toolbarOpacity,
     335           2 :         child: CompositedTransformFollower(
     336           2 :           link: toolbarLayerLink,
     337             :           showWhenUnlinked: false,
     338           4 :           offset: -editingRegion.topLeft,
     339           4 :           child: selectionControls!.buildToolbar(
     340             :             context,
     341             :             editingRegion,
     342           4 :             renderObject.preferredLineWidth,
     343             :             midpoint,
     344             :             endpoints,
     345           2 :             selectionDelegate!,
     346           2 :             clipboardStatus!,
     347           4 :             renderObject.lastSecondaryTapDownPosition,
     348             :           ),
     349             :         ),
     350             :       ),
     351             :     );
     352             :   }
     353             : 
     354           0 :   void _handleSelectionHandleChanged(
     355             :       TextSelection newSelection, _TextSelectionHandlePosition position) {
     356             :     final TextPosition textPosition;
     357             :     switch (position) {
     358           0 :       case _TextSelectionHandlePosition.start:
     359           0 :         textPosition = newSelection.base;
     360             :         break;
     361           0 :       case _TextSelectionHandlePosition.end:
     362           0 :         textPosition = newSelection.extent;
     363             :         break;
     364             :     }
     365           0 :     selectionDelegate!.userUpdateTextEditingValue(
     366           0 :       _value.copyWith(selection: newSelection, composing: TextRange.empty),
     367             :       SelectionChangedCause.drag,
     368             :     );
     369           0 :     selectionDelegate!.bringIntoView(textPosition);
     370             :   }
     371             : }
     372             : 
     373             : /// This widget represents a single draggable text selection handle.
     374             : class _TextSelectionHandleOverlay extends StatefulWidget {
     375           2 :   const _TextSelectionHandleOverlay({
     376             :     Key? key,
     377             :     required this.selection,
     378             :     required this.position,
     379             :     required this.startHandleLayerLink,
     380             :     required this.endHandleLayerLink,
     381             :     required this.renderObject,
     382             :     required this.onSelectionHandleChanged,
     383             :     required this.onSelectionHandleTapped,
     384             :     required this.selectionControls,
     385             :     this.dragStartBehavior = DragStartBehavior.start,
     386           2 :   }) : super(key: key);
     387             : 
     388             :   final TextSelection selection;
     389             :   final _TextSelectionHandlePosition position;
     390             :   final LayerLink startHandleLayerLink;
     391             :   final LayerLink endHandleLayerLink;
     392             :   final MongolRenderEditable renderObject;
     393             :   final ValueChanged<TextSelection> onSelectionHandleChanged;
     394             :   final VoidCallback? onSelectionHandleTapped;
     395             :   final TextSelectionControls? selectionControls;
     396             :   final DragStartBehavior dragStartBehavior;
     397             : 
     398           1 :   @override
     399             :   _TextSelectionHandleOverlayState createState() =>
     400           1 :       _TextSelectionHandleOverlayState();
     401             : 
     402           1 :   ValueListenable<bool> get _visibility {
     403           1 :     switch (position) {
     404           1 :       case _TextSelectionHandlePosition.start:
     405           2 :         return renderObject.selectionStartInViewport;
     406           1 :       case _TextSelectionHandlePosition.end:
     407           2 :         return renderObject.selectionEndInViewport;
     408             :     }
     409             :   }
     410             : }
     411             : 
     412             : class _TextSelectionHandleOverlayState
     413             :     extends State<_TextSelectionHandleOverlay>
     414             :     with SingleTickerProviderStateMixin {
     415             :   late Offset _dragPosition;
     416             : 
     417             :   late AnimationController _controller;
     418           3 :   Animation<double> get _opacity => _controller.view;
     419             : 
     420           1 :   @override
     421             :   void initState() {
     422           1 :     super.initState();
     423             : 
     424           2 :     _controller = AnimationController(
     425             :         duration: TextSelectionOverlay.fadeDuration, vsync: this);
     426             : 
     427           1 :     _handleVisibilityChanged();
     428           4 :     widget._visibility.addListener(_handleVisibilityChanged);
     429             :   }
     430             : 
     431           1 :   void _handleVisibilityChanged() {
     432           3 :     if (widget._visibility.value) {
     433           2 :       _controller.forward();
     434             :     } else {
     435           0 :       _controller.reverse();
     436             :     }
     437             :   }
     438             : 
     439           1 :   @override
     440             :   void didUpdateWidget(_TextSelectionHandleOverlay oldWidget) {
     441           1 :     super.didUpdateWidget(oldWidget);
     442           3 :     oldWidget._visibility.removeListener(_handleVisibilityChanged);
     443           1 :     _handleVisibilityChanged();
     444           4 :     widget._visibility.addListener(_handleVisibilityChanged);
     445             :   }
     446             : 
     447           1 :   @override
     448             :   void dispose() {
     449           4 :     widget._visibility.removeListener(_handleVisibilityChanged);
     450           2 :     _controller.dispose();
     451           1 :     super.dispose();
     452             :   }
     453             : 
     454           0 :   void _handleDragStart(DragStartDetails details) {
     455           0 :     final handleSize = widget.selectionControls!.getHandleSize(
     456           0 :       widget.renderObject.preferredLineWidth,
     457             :     );
     458           0 :     _dragPosition = details.globalPosition + Offset(-handleSize.width, 0.0);
     459             :   }
     460             : 
     461           0 :   void _handleDragUpdate(DragUpdateDetails details) {
     462           0 :     _dragPosition += details.delta;
     463           0 :     final position = widget.renderObject.getPositionForPoint(_dragPosition);
     464             : 
     465           0 :     if (widget.selection.isCollapsed) {
     466           0 :       widget.onSelectionHandleChanged(TextSelection.fromPosition(position));
     467             :       return;
     468             :     }
     469             : 
     470             :     final TextSelection newSelection;
     471           0 :     switch (widget.position) {
     472           0 :       case _TextSelectionHandlePosition.start:
     473           0 :         newSelection = TextSelection(
     474           0 :           baseOffset: position.offset,
     475           0 :           extentOffset: widget.selection.extentOffset,
     476             :         );
     477             :         break;
     478           0 :       case _TextSelectionHandlePosition.end:
     479           0 :         newSelection = TextSelection(
     480           0 :           baseOffset: widget.selection.baseOffset,
     481           0 :           extentOffset: position.offset,
     482             :         );
     483             :         break;
     484             :     }
     485             : 
     486           0 :     if (newSelection.baseOffset >= newSelection.extentOffset) {
     487             :       return; // don't allow order swapping.
     488             :     }
     489             : 
     490           0 :     widget.onSelectionHandleChanged(newSelection);
     491             :   }
     492             : 
     493           1 :   void _handleTap() {
     494           2 :     if (widget.onSelectionHandleTapped != null) {
     495           3 :       widget.onSelectionHandleTapped!();
     496             :     }
     497             :   }
     498             : 
     499           1 :   @override
     500             :   Widget build(BuildContext context) {
     501             :     final LayerLink layerLink;
     502             :     final TextSelectionHandleType type;
     503             : 
     504           2 :     switch (widget.position) {
     505           1 :       case _TextSelectionHandlePosition.start:
     506           2 :         layerLink = widget.startHandleLayerLink;
     507           1 :         type = _chooseType(TextSelectionHandleType.left);
     508             :         break;
     509           1 :       case _TextSelectionHandlePosition.end:
     510             :         // For collapsed selections, we shouldn't be building the [end] handle.
     511           3 :         assert(!widget.selection.isCollapsed);
     512           2 :         layerLink = widget.endHandleLayerLink;
     513           1 :         type = _chooseType(TextSelectionHandleType.right);
     514             :         break;
     515             :     }
     516             : 
     517           3 :     final handleAnchor = widget.selectionControls!.getHandleAnchor(
     518             :       type,
     519           3 :       widget.renderObject.preferredLineWidth,
     520             :     );
     521           3 :     final handleSize = widget.selectionControls!.getHandleSize(
     522           3 :       widget.renderObject.preferredLineWidth,
     523             :     );
     524             : 
     525           1 :     final handleRect = Rect.fromLTWH(
     526           2 :       -handleAnchor.dx,
     527           2 :       -handleAnchor.dy,
     528           1 :       handleSize.width,
     529           1 :       handleSize.height,
     530             :     );
     531             : 
     532             :     // Make sure the GestureDetector is big enough to be easily interactive.
     533           1 :     final interactiveRect = handleRect.expandToInclude(
     534           1 :       Rect.fromCircle(
     535           2 :           center: handleRect.center, radius: kMinInteractiveDimension / 2),
     536             :     );
     537           1 :     final padding = RelativeRect.fromLTRB(
     538           5 :       math.max((interactiveRect.width - handleRect.width) / 2, 0),
     539           5 :       math.max((interactiveRect.height - handleRect.height) / 2, 0),
     540           5 :       math.max((interactiveRect.width - handleRect.width) / 2, 0),
     541           5 :       math.max((interactiveRect.height - handleRect.height) / 2, 0),
     542             :     );
     543             : 
     544           1 :     return CompositedTransformFollower(
     545             :       link: layerLink,
     546           1 :       offset: interactiveRect.topLeft,
     547             :       showWhenUnlinked: false,
     548           1 :       child: FadeTransition(
     549           1 :         opacity: _opacity,
     550           1 :         child: Container(
     551             :           alignment: Alignment.topLeft,
     552           1 :           width: interactiveRect.width,
     553           1 :           height: interactiveRect.height,
     554           1 :           child: GestureDetector(
     555             :             behavior: HitTestBehavior.translucent,
     556           2 :             dragStartBehavior: widget.dragStartBehavior,
     557           1 :             onPanStart: _handleDragStart,
     558           1 :             onPanUpdate: _handleDragUpdate,
     559           1 :             onTap: _handleTap,
     560           1 :             child: Padding(
     561           1 :               padding: EdgeInsets.only(
     562           1 :                 left: padding.left,
     563           1 :                 top: padding.top,
     564           1 :                 right: padding.right,
     565           1 :                 bottom: padding.bottom,
     566             :               ),
     567           3 :               child: widget.selectionControls!.buildHandle(
     568             :                 context,
     569             :                 type,
     570           3 :                 widget.renderObject.preferredLineWidth,
     571             :               ),
     572             :             ),
     573             :           ),
     574             :         ),
     575             :       ),
     576             :     );
     577             :   }
     578             : 
     579           1 :   TextSelectionHandleType _chooseType(TextSelectionHandleType type) {
     580           3 :     if (widget.selection.isCollapsed) {
     581             :       return TextSelectionHandleType.collapsed;
     582             :     }
     583             :     return type;
     584             :   }
     585             : }
     586             : 
     587             : /// Delegate interface for the [MongolTextSelectionGestureDetectorBuilder].
     588             : ///
     589             : /// The interface is usually implemented by textfield implementations wrapping
     590             : /// [MongolEditableText], that use a [MongolTextSelectionGestureDetectorBuilder] to build a
     591             : /// [TextSelectionGestureDetector] for their [MongolEditableText]. The delegate provides
     592             : /// the builder with information about the current state of the textfield.
     593             : /// Based on this information, the builder adds the correct gesture handlers
     594             : /// to the gesture detector.
     595             : ///
     596             : /// See also:
     597             : ///
     598             : ///  * [MongolTextField], which implements this delegate for the Material textfield.
     599             : abstract class MongolTextSelectionGestureDetectorBuilderDelegate {
     600             :   /// [GlobalKey] to the [MongolEditableText] for which the
     601             :   /// [MongolTextSelectionGestureDetectorBuilder] will build a [TextSelectionGestureDetector].
     602             :   GlobalKey<MongolEditableTextState> get editableTextKey;
     603             : 
     604             :   /// Whether the text field should respond to force presses.
     605             :   bool get forcePressEnabled;
     606             : 
     607             :   /// Whether the user may select text in the text field.
     608             :   bool get selectionEnabled;
     609             : }
     610             : 
     611             : /// Builds a [TextSelectionGestureDetector] to wrap an [MongolEditableText].
     612             : ///
     613             : /// The class implements sensible defaults for many user interactions
     614             : /// with an [MongolEditableText] (see the documentation of the various gesture handler
     615             : /// methods, e.g. [onTapDown], [onForcePressStart], etc.). Subclasses of
     616             : /// [MongolTextSelectionGestureDetectorBuilder] can change the behavior performed in
     617             : /// responds to these gesture events by overriding the corresponding handler
     618             : /// methods of this class.
     619             : ///
     620             : /// The resulting [TextSelectionGestureDetector] to wrap an [MongolEditableText] is
     621             : /// obtained by calling [buildGestureDetector].
     622             : ///
     623             : /// See also:
     624             : ///
     625             : ///  * [MongolTextField], which uses a subclass to implement the Material-specific
     626             : ///    gesture logic of an [MongolEditableText].
     627             : class MongolTextSelectionGestureDetectorBuilder {
     628             :   /// Creates a [MongolTextSelectionGestureDetectorBuilder].
     629           1 :   MongolTextSelectionGestureDetectorBuilder({
     630             :     required this.delegate,
     631             :   });
     632             : 
     633             :   /// The delegate for this [MongolTextSelectionGestureDetectorBuilder].
     634             :   ///
     635             :   /// The delegate provides the builder with information about what actions can
     636             :   /// currently be performed on the text field. Based on this, the builder adds
     637             :   /// the correct gesture handlers to the gesture detector.
     638             :   @protected
     639             :   final MongolTextSelectionGestureDetectorBuilderDelegate delegate;
     640             : 
     641             :   /// Whether to show the selection toolbar.
     642             :   ///
     643             :   /// It is based on the signal source when a [onTapDown] is called. This getter
     644             :   /// will return true if current [onTapDown] event is triggered by a touch or
     645             :   /// a stylus.
     646           2 :   bool get shouldShowSelectionToolbar => _shouldShowSelectionToolbar;
     647             :   bool _shouldShowSelectionToolbar = true;
     648             : 
     649             :   /// The [State] of the [EditableText] for which the builder will provide a
     650             :   /// [TextSelectionGestureDetector].
     651           1 :   @protected
     652             :   MongolEditableTextState get editableText =>
     653           3 :       delegate.editableTextKey.currentState!;
     654             : 
     655             :   /// The [RenderObject] of the [MongolEditableText] for which the builder will
     656             :   /// provide a [TextSelectionGestureDetector].
     657           1 :   @protected
     658           2 :   MongolRenderEditable get renderEditable => editableText.renderEditable;
     659             : 
     660             :   /// Handler for [TextSelectionGestureDetector.onTapDown].
     661             :   ///
     662             :   /// By default, it forwards the tap to [MongolRenderEditable.handleTapDown] and sets
     663             :   /// [shouldShowSelectionToolbar] to true if the tap was initiated by a finger or stylus.
     664             :   ///
     665             :   /// See also:
     666             :   ///
     667             :   ///  * [TextSelectionGestureDetector.onTapDown], which triggers this callback.
     668           1 :   @protected
     669             :   void onTapDown(TapDownDetails details) {
     670           2 :     renderEditable.handleTapDown(details);
     671             :     // The selection overlay should only be shown when the user is interacting
     672             :     // through a touch screen (via either a finger or a stylus). A mouse shouldn't
     673             :     // trigger the selection overlay.
     674             :     // For backwards-compatibility, we treat a null kind the same as touch.
     675           1 :     final kind = details.kind;
     676           1 :     _shouldShowSelectionToolbar = kind == null ||
     677           1 :         kind == PointerDeviceKind.touch ||
     678           1 :         kind == PointerDeviceKind.stylus;
     679             :   }
     680             : 
     681             :   /// Handler for [TextSelectionGestureDetector.onForcePressStart].
     682             :   ///
     683             :   /// By default, it selects the word at the position of the force press,
     684             :   /// if selection is enabled.
     685             :   ///
     686             :   /// This callback is only applicable when force press is enabled.
     687             :   ///
     688             :   /// See also:
     689             :   ///
     690             :   ///  * [TextSelectionGestureDetector.onForcePressStart], which triggers this
     691             :   ///    callback.
     692           0 :   @protected
     693             :   void onForcePressStart(ForcePressDetails details) {
     694           0 :     assert(delegate.forcePressEnabled);
     695           0 :     _shouldShowSelectionToolbar = true;
     696           0 :     if (delegate.selectionEnabled) {
     697           0 :       renderEditable.selectWordsInRange(
     698           0 :         from: details.globalPosition,
     699             :         cause: SelectionChangedCause.forcePress,
     700             :       );
     701             :     }
     702             :   }
     703             : 
     704             :   /// Handler for [TextSelectionGestureDetector.onForcePressEnd].
     705             :   ///
     706             :   /// By default, it selects words in the range specified in [details] and shows
     707             :   /// toolbar if it is necessary.
     708             :   ///
     709             :   /// This callback is only applicable when force press is enabled.
     710             :   ///
     711             :   /// See also:
     712             :   ///
     713             :   ///  * [TextSelectionGestureDetector.onForcePressEnd], which triggers this
     714             :   ///    callback.
     715           0 :   @protected
     716             :   void onForcePressEnd(ForcePressDetails details) {
     717           0 :     assert(delegate.forcePressEnabled);
     718           0 :     renderEditable.selectWordsInRange(
     719           0 :       from: details.globalPosition,
     720             :       cause: SelectionChangedCause.forcePress,
     721             :     );
     722           0 :     if (shouldShowSelectionToolbar) editableText.showToolbar();
     723             :   }
     724             : 
     725             :   /// Handler for [TextSelectionGestureDetector.onSingleTapUp].
     726             :   ///
     727             :   /// By default, it selects word edge if selection is enabled.
     728             :   ///
     729             :   /// See also:
     730             :   ///
     731             :   ///  * [TextSelectionGestureDetector.onSingleTapUp], which triggers
     732             :   ///    this callback.
     733           0 :   @protected
     734             :   void onSingleTapUp(TapUpDetails details) {
     735           0 :     if (delegate.selectionEnabled) {
     736           0 :       renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
     737             :     }
     738             :   }
     739             : 
     740             :   /// Handler for [TextSelectionGestureDetector.onSingleTapCancel].
     741             :   ///
     742             :   /// By default, it services as place holder to enable subclass override.
     743             :   ///
     744             :   /// See also:
     745             :   ///
     746             :   ///  * [TextSelectionGestureDetector.onSingleTapCancel], which triggers
     747             :   ///    this callback.
     748           1 :   @protected
     749             :   void onSingleTapCancel() {
     750             :     /* Subclass should override this method if needed. */
     751             :   }
     752             : 
     753             :   /// Handler for [TextSelectionGestureDetector.onSingleLongTapStart].
     754             :   ///
     755             :   /// By default, it selects text position specified in [details] if selection
     756             :   /// is enabled.
     757             :   ///
     758             :   /// See also:
     759             :   ///
     760             :   ///  * [TextSelectionGestureDetector.onSingleLongTapStart], which triggers
     761             :   ///    this callback.
     762           0 :   @protected
     763             :   void onSingleLongTapStart(LongPressStartDetails details) {
     764           0 :     if (delegate.selectionEnabled) {
     765           0 :       renderEditable.selectPositionAt(
     766           0 :         from: details.globalPosition,
     767             :         cause: SelectionChangedCause.longPress,
     768             :       );
     769             :     }
     770             :   }
     771             : 
     772             :   /// Handler for [TextSelectionGestureDetector.onSingleLongTapMoveUpdate].
     773             :   ///
     774             :   /// By default, it updates the selection location specified in [details] if
     775             :   /// selection is enabled.
     776             :   ///
     777             :   /// See also:
     778             :   ///
     779             :   ///  * [TextSelectionGestureDetector.onSingleLongTapMoveUpdate], which
     780             :   ///    triggers this callback.
     781           0 :   @protected
     782             :   void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
     783           0 :     if (delegate.selectionEnabled) {
     784           0 :       renderEditable.selectPositionAt(
     785           0 :         from: details.globalPosition,
     786             :         cause: SelectionChangedCause.longPress,
     787             :       );
     788             :     }
     789             :   }
     790             : 
     791             :   /// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd].
     792             :   ///
     793             :   /// By default, it shows toolbar if necessary.
     794             :   ///
     795             :   /// See also:
     796             :   ///
     797             :   ///  * [TextSelectionGestureDetector.onSingleLongTapEnd], which triggers this
     798             :   ///    callback.
     799           1 :   @protected
     800             :   void onSingleLongTapEnd(LongPressEndDetails details) {
     801           3 :     if (shouldShowSelectionToolbar) editableText.showToolbar();
     802             :   }
     803             : 
     804             :   /// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
     805             :   ///
     806             :   /// By default, it selects a word through [MongolRenderEditable.selectWord] if
     807             :   /// selectionEnabled and shows toolbar if necessary.
     808             :   ///
     809             :   /// See also:
     810             :   ///
     811             :   ///  * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this
     812             :   ///    callback.
     813           1 :   @protected
     814             :   void onDoubleTapDown(TapDownDetails details) {
     815           2 :     if (delegate.selectionEnabled) {
     816           2 :       renderEditable.selectWord(cause: SelectionChangedCause.tap);
     817           3 :       if (shouldShowSelectionToolbar) editableText.showToolbar();
     818             :     }
     819             :   }
     820             : 
     821             :   /// Handler for [TextSelectionGestureDetector.onDragSelectionStart].
     822             :   ///
     823             :   /// By default, it selects a text position specified in [details].
     824             :   ///
     825             :   /// See also:
     826             :   ///
     827             :   ///  * [TextSelectionGestureDetector.onDragSelectionStart], which triggers
     828             :   ///    this callback.
     829           0 :   @protected
     830             :   void onDragSelectionStart(DragStartDetails details) {
     831           0 :     final kind = details.kind;
     832           0 :     _shouldShowSelectionToolbar = kind == null ||
     833           0 :         kind == PointerDeviceKind.touch ||
     834           0 :         kind == PointerDeviceKind.stylus;
     835             : 
     836           0 :     renderEditable.selectPositionAt(
     837           0 :       from: details.globalPosition,
     838             :       cause: SelectionChangedCause.drag,
     839             :     );
     840             :   }
     841             : 
     842             :   /// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate].
     843             :   ///
     844             :   /// By default, it updates the selection location specified in the provided
     845             :   /// details objects.
     846             :   ///
     847             :   /// See also:
     848             :   ///
     849             :   ///  * [TextSelectionGestureDetector.onDragSelectionUpdate], which triggers
     850             :   ///    this callback./lib/src/material/text_field.dart
     851           0 :   @protected
     852             :   void onDragSelectionUpdate(
     853             :       DragStartDetails startDetails, DragUpdateDetails updateDetails) {
     854           0 :     renderEditable.selectPositionAt(
     855           0 :       from: startDetails.globalPosition,
     856           0 :       to: updateDetails.globalPosition,
     857             :       cause: SelectionChangedCause.drag,
     858             :     );
     859             :   }
     860             : 
     861             :   /// Handler for [TextSelectionGestureDetector.onDragSelectionEnd].
     862             :   ///
     863             :   /// By default, it services as place holder to enable subclass override.
     864             :   ///
     865             :   /// See also:
     866             :   ///
     867             :   ///  * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this
     868             :   ///    callback.
     869           0 :   @protected
     870             :   void onDragSelectionEnd(DragEndDetails details) {
     871             :     /* Subclass should override this method if needed. */
     872             :   }
     873             : 
     874             :   /// Returns a [TextSelectionGestureDetector] configured with the handlers
     875             :   /// provided by this builder.
     876             :   ///
     877             :   /// The [child] or its subtree should contain [EditableText].
     878           1 :   Widget buildGestureDetector({
     879             :     Key? key,
     880             :     HitTestBehavior? behavior,
     881             :     required Widget child,
     882             :   }) {
     883           1 :     return TextSelectionGestureDetector(
     884             :       key: key,
     885           1 :       onTapDown: onTapDown,
     886           3 :       onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
     887           3 :       onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
     888           1 :       onSingleTapUp: onSingleTapUp,
     889           1 :       onSingleTapCancel: onSingleTapCancel,
     890           1 :       onSingleLongTapStart: onSingleLongTapStart,
     891           1 :       onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
     892           1 :       onSingleLongTapEnd: onSingleLongTapEnd,
     893           1 :       onDoubleTapDown: onDoubleTapDown,
     894           1 :       onDragSelectionStart: onDragSelectionStart,
     895           1 :       onDragSelectionUpdate: onDragSelectionUpdate,
     896           1 :       onDragSelectionEnd: onDragSelectionEnd,
     897             :       behavior: behavior,
     898             :       child: child,
     899             :     );
     900             :   }
     901             : }

Generated by: LCOV version 1.15