diff --git a/lib/common/widgets/flutter/selectable_text/selectable_region.dart b/lib/common/widgets/flutter/selectable_text/selectable_region.dart deleted file mode 100644 index 1d9feac6a..000000000 --- a/lib/common/widgets/flutter/selectable_text/selectable_region.dart +++ /dev/null @@ -1,2259 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:math'; - -import 'package:PiliPlus/common/widgets/flutter/selectable_text/tap_and_drag.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart' - hide - BaseTapAndDragGestureRecognizer, - TapAndHorizontalDragGestureRecognizer, - TapAndPanGestureRecognizer; -import 'package:flutter/material.dart' hide SelectableRegion; -import 'package:flutter/rendering.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:vector_math/vector_math_64.dart'; - -// Examples can assume: -// late GlobalKey key; - -const Set _kLongPressSelectionDevices = { - PointerDeviceKind.touch, - PointerDeviceKind.stylus, - PointerDeviceKind.invertedStylus, -}; - -/// A widget that introduces an area for user selections. -/// -/// Flutter widgets are not selectable by default. Wrapping a widget subtree -/// with a [SelectableRegion] widget enables selection within that subtree (for -/// example, [Text] widgets automatically look for selectable regions to enable -/// selection). The wrapped subtree can be selected by users using mouse or -/// touch gestures, e.g. users can select widgets by holding the mouse -/// left-click and dragging across widgets, or they can use long press gestures -/// to select words on touch devices. -/// -/// A [SelectableRegion] widget requires configuration; in particular specific -/// [selectionControls] must be provided. -/// -/// The [SelectionArea] widget from the [material] library configures a -/// [SelectableRegion] in a platform-specific manner (e.g. using a Material -/// toolbar on Android, a Cupertino toolbar on iOS), and it may therefore be -/// simpler to use that widget rather than using [SelectableRegion] directly. -/// -/// ## An overview of the selection system. -/// -/// Every [Selectable] under the [SelectableRegion] can be selected. They form a -/// selection tree structure to handle the selection. -/// -/// The [SelectableRegion] is a wrapper over [SelectionContainer]. It listens to -/// user gestures and sends corresponding [SelectionEvent]s to the -/// [SelectionContainer] it creates. -/// -/// A [SelectionContainer] is a single [Selectable] that handles -/// [SelectionEvent]s on behalf of child [Selectable]s in the subtree. It -/// creates a [SelectionRegistrarScope] with its [SelectionContainer.delegate] -/// to collect child [Selectable]s and sends the [SelectionEvent]s it receives -/// from the parent [SelectionRegistrar] to the appropriate child [Selectable]s. -/// It creates an abstraction for the parent [SelectionRegistrar] as if it is -/// interacting with a single [Selectable]. -/// -/// The [SelectionContainer] created by [SelectableRegion] is the root node of a -/// selection tree. Each non-leaf node in the tree is a [SelectionContainer], -/// and the leaf node is a leaf widget whose render object implements -/// [Selectable]. They are connected through [SelectionRegistrarScope]s created -/// by [SelectionContainer]s. -/// -/// Both [SelectionContainer]s and the leaf [Selectable]s need to register -/// themselves to the [SelectionRegistrar] from the -/// [SelectionContainer.maybeOf] if they want to participate in the -/// selection. -/// -/// An example selection tree will look like: -/// -/// {@tool snippet} -/// -/// ```dart -/// MaterialApp( -/// home: SelectableRegion( -/// selectionControls: materialTextSelectionControls, -/// child: Scaffold( -/// appBar: AppBar(title: const Text('Flutter Code Sample')), -/// body: ListView( -/// children: const [ -/// Text('Item 0', style: TextStyle(fontSize: 50.0)), -/// Text('Item 1', style: TextStyle(fontSize: 50.0)), -/// ], -/// ), -/// ), -/// ), -/// ) -/// ``` -/// {@end-tool} -/// -/// -/// SelectionContainer -/// (SelectableRegion) -/// / \ -/// / \ -/// / \ -/// Selectable \ -/// ("Flutter Code Sample") \ -/// \ -/// SelectionContainer -/// (ListView) -/// / \ -/// / \ -/// / \ -/// Selectable Selectable -/// ("Item 0") ("Item 1") -/// -/// -/// ## Making a widget selectable -/// -/// Some leaf widgets, such as [Text], have all of the selection logic wired up -/// automatically and can be selected as long as they are under a -/// [SelectableRegion]. -/// -/// To make a custom selectable widget, its render object needs to mix in -/// [Selectable] and implement the required APIs to handle [SelectionEvent]s -/// as well as paint appropriate selection highlights. -/// -/// The render object also needs to register itself to a [SelectionRegistrar]. -/// For the most cases, one can use [SelectionRegistrant] to auto-register -/// itself with the register returned from [SelectionContainer.maybeOf] as -/// seen in the example below. -/// -/// {@tool dartpad} -/// This sample demonstrates how to create an adapter widget that makes any -/// child widget selectable. -/// -/// ** See code in examples/api/lib/material/selectable_region/selectable_region.0.dart ** -/// {@end-tool} -/// -/// ## Complex layout -/// -/// By default, the screen order is used as the selection order. If a group of -/// [Selectable]s needs to select differently, consider wrapping them with a -/// [SelectionContainer] to customize its selection behavior. -/// -/// {@tool dartpad} -/// This sample demonstrates how to create a [SelectionContainer] that only -/// allows selecting everything or nothing with no partial selection. -/// -/// ** See code in examples/api/lib/material/selection_container/selection_container.0.dart ** -/// {@end-tool} -/// -/// In the case where a group of widgets should be excluded from selection under -/// a [SelectableRegion], consider wrapping that group of widgets using -/// [SelectionContainer.disabled]. -/// -/// {@tool dartpad} -/// This sample demonstrates how to disable selection for a Text in a Column. -/// -/// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart ** -/// {@end-tool} -/// -/// To create a separate selection system from its parent selection area, -/// wrap part of the subtree with another [SelectableRegion]. The selection of the -/// child selection area can not extend past its subtree, and the selection of -/// the parent selection area can not extend inside the child selection area. -/// -/// ## Selection status -/// -/// A [SelectableRegion]s [SelectableRegionSelectionStatus] is used to indicate whether -/// the [SelectableRegion] is actively changing the selection, or has finalized it. For -/// example, during a mouse click + drag, the [SelectableRegionSelectionStatus] will be -/// set to [SelectableRegionSelectionStatus.changing], and when the mouse click is released -/// the status will be set to [SelectableRegionSelectionStatus.finalized]. -/// -/// The default value of [SelectableRegion]s selection status -/// is [SelectableRegionSelectionStatus.finalized]. -/// -/// To access the [SelectableRegionSelectionStatus] of a parent [SelectableRegion] -/// use [SelectableRegionSelectionStatusScope.maybeOf] and retrieve the value from -/// the [ValueListenable]. -/// -/// One can also listen for changes to the [SelectableRegionSelectionStatus] by -/// adding a listener to the [ValueListenable] retrieved from [SelectableRegionSelectionStatusScope.maybeOf] -/// through [ValueListenable.addListener]. In Stateful widgets this is typically -/// done in [State.didChangeDependencies]. Remove the listener when no longer -/// needed, typically in your Stateful widgets [State.dispose] method through -/// [ValueListenable.removeListener]. -/// -/// ## Tests -/// -/// In a test, a region can be selected either by faking drag events (e.g. using -/// [WidgetTester.dragFrom]) or by sending intents to a widget inside the region -/// that has been given a [GlobalKey], e.g.: -/// -/// ```dart -/// Actions.invoke(key.currentContext!, const SelectAllTextIntent(SelectionChangedCause.keyboard)); -/// ``` -/// -/// See also: -/// -/// * [SelectionArea], which creates a [SelectableRegion] with -/// platform-adaptive selection controls. -/// * [SelectableText], which enables selection on a single run of text. -/// * [SelectionHandler], which contains APIs to handle selection events from the -/// [SelectableRegion]. -/// * [Selectable], which provides API to participate in the selection system. -/// * [SelectionRegistrar], which [Selectable] needs to subscribe to receive -/// selection events. -/// * [SelectionContainer], which collects selectable widgets in the subtree -/// and provides api to dispatch selection event to the collected widget. -/// * [SelectionListener], which enables accessing the [SelectionDetails] of -/// the selectable subtree it wraps. -class SelectableRegion extends StatefulWidget { - /// Create a new [SelectableRegion] widget. - /// - /// The [selectionControls] are used for building the selection handles and - /// toolbar for mobile devices. - const SelectableRegion({ - super.key, - this.contextMenuBuilder, - this.focusNode, - this.magnifierConfiguration = TextMagnifierConfiguration.disabled, - this.onSelectionChanged, - required this.selectionControls, - required this.child, - }); - - /// The configuration for the magnifier used with selections in this region. - /// - /// By default, [SelectableRegion]'s [TextMagnifierConfiguration] is disabled. - /// For a version of [SelectableRegion] that adapts automatically to the - /// current platform, consider [SelectionArea]. - /// - /// {@macro flutter.widgets.magnifier.intro} - final TextMagnifierConfiguration magnifierConfiguration; - - /// {@macro flutter.widgets.Focus.focusNode} - final FocusNode? focusNode; - - /// The child widget this selection area applies to. - /// - /// {@macro flutter.widgets.ProxyWidget.child} - final Widget child; - - /// {@macro flutter.widgets.EditableText.contextMenuBuilder} - final SelectableRegionContextMenuBuilder? contextMenuBuilder; - - /// The delegate to build the selection handles and toolbar for mobile - /// devices. - /// - /// The [emptyTextSelectionControls] global variable provides a default - /// [TextSelectionControls] implementation with no controls. - final TextSelectionControls selectionControls; - - /// Called when the selected content changes. - final ValueChanged? onSelectionChanged; - - /// Returns the [ContextMenuButtonItem]s representing the buttons in this - /// platform's default selection menu. - /// - /// For example, [SelectableRegion] uses this to generate the default buttons - /// for its context menu. - /// - /// See also: - /// - /// * [SelectableRegionState.contextMenuButtonItems], which gives the - /// [ContextMenuButtonItem]s for a specific SelectableRegion. - /// * [EditableText.getEditableButtonItems], which performs a similar role but - /// for content that is both selectable and editable. - /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can - /// take a list of [ContextMenuButtonItem]s with - /// [AdaptiveTextSelectionToolbar.buttonItems]. - /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button - /// Widgets for the current platform given [ContextMenuButtonItem]s. - static List getSelectableButtonItems({ - required final SelectionGeometry selectionGeometry, - required final VoidCallback onCopy, - required final VoidCallback onSelectAll, - required final VoidCallback? onShare, - }) { - final canCopy = selectionGeometry.status == SelectionStatus.uncollapsed; - final bool canSelectAll = selectionGeometry.hasContent; - // The share button is not supported on the web. - final bool platformCanShare = - !kIsWeb && - switch (defaultTargetPlatform) { - TargetPlatform.android => - selectionGeometry.status == SelectionStatus.uncollapsed, - TargetPlatform.macOS || - TargetPlatform.fuchsia || - TargetPlatform.linux || - TargetPlatform.windows => false, - // TODO(bleroux): the share button should be shown on iOS but the share - // functionality requires some changes on the engine side because, on iPad, - // it needs an anchor for the popup. - // See: https://github.com/flutter/flutter/issues/141775. - TargetPlatform.iOS => false, - }; - final bool canShare = onShare != null && platformCanShare; - - // On Android, the share button is before the select all button. - final showShareBeforeSelectAll = - defaultTargetPlatform == TargetPlatform.android; - - // Determine which buttons will appear so that the order and total number is - // known. A button's position in the menu can slightly affect its - // appearance. - return [ - if (canCopy) - ContextMenuButtonItem( - onPressed: onCopy, - type: ContextMenuButtonType.copy, - ), - if (canShare && showShareBeforeSelectAll) - ContextMenuButtonItem( - onPressed: onShare, - type: ContextMenuButtonType.share, - ), - if (canSelectAll) - ContextMenuButtonItem( - onPressed: onSelectAll, - type: ContextMenuButtonType.selectAll, - ), - if (canShare && !showShareBeforeSelectAll) - ContextMenuButtonItem( - onPressed: onShare, - type: ContextMenuButtonType.share, - ), - ]; - } - - @override - State createState() => SelectableRegionState(); -} - -/// State for a [SelectableRegion]. -class SelectableRegionState extends State - with TextSelectionDelegate - implements SelectionRegistrar { - late final Map> _actions = >{ - SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)), - CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)), - ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( - _GranularlyExtendSelectionAction< - ExtendSelectionToNextWordBoundaryOrCaretLocationIntent - >( - this, - granularity: TextGranularity.word, - ), - ), - ExpandSelectionToDocumentBoundaryIntent: _makeOverridable( - _GranularlyExtendSelectionAction( - this, - granularity: TextGranularity.document, - ), - ), - ExpandSelectionToLineBreakIntent: _makeOverridable( - _GranularlyExtendSelectionAction( - this, - granularity: TextGranularity.line, - ), - ), - ExtendSelectionByCharacterIntent: _makeOverridable( - _GranularlyExtendCaretSelectionAction( - this, - granularity: TextGranularity.character, - ), - ), - ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( - _GranularlyExtendCaretSelectionAction< - ExtendSelectionToNextWordBoundaryIntent - >( - this, - granularity: TextGranularity.word, - ), - ), - ExtendSelectionToLineBreakIntent: _makeOverridable( - _GranularlyExtendCaretSelectionAction( - this, - granularity: TextGranularity.line, - ), - ), - ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable( - _DirectionallyExtendCaretSelectionAction< - ExtendSelectionVerticallyToAdjacentLineIntent - >(this), - ), - ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( - _GranularlyExtendCaretSelectionAction< - ExtendSelectionToDocumentBoundaryIntent - >( - this, - granularity: TextGranularity.document, - ), - ), - }; - - final Map _gestureRecognizers = - {}; - SelectionOverlay? _selectionOverlay; - final LayerLink _startHandleLayerLink = LayerLink(); - final LayerLink _endHandleLayerLink = LayerLink(); - final LayerLink _toolbarLayerLink = LayerLink(); - final StaticSelectionContainerDelegate _selectionDelegate = - StaticSelectionContainerDelegate(); - // there should only ever be one selectable, which is the SelectionContainer. - Selectable? _selectable; - - bool get _hasSelectionOverlayGeometry => - _selectionDelegate.value.startSelectionPoint != null || - _selectionDelegate.value.endSelectionPoint != null; - - Orientation? _lastOrientation; - SelectedContent? _lastSelectedContent; - - /// Whether the native browser context menu is enabled. - // TODO(Renzo-Olivares): Re-enable web context menu for Android - // and iOS when https://github.com/flutter/flutter/issues/177123 - // is resolved. - bool get _webContextMenuEnabled => - kIsWeb && - BrowserContextMenu.enabled && - defaultTargetPlatform != TargetPlatform.android && - defaultTargetPlatform != TargetPlatform.iOS; - - /// The [SelectionOverlay] that is currently visible on the screen. - /// - /// Can be null if there is no visible [SelectionOverlay]. - @visibleForTesting - SelectionOverlay? get selectionOverlay => _selectionOverlay; - - /// The text processing service used to retrieve the native text processing actions. - final ProcessTextService _processTextService = DefaultProcessTextService(); - - /// The list of native text processing actions provided by the engine. - final List _processTextActions = []; - - // The focus node to use if the widget didn't supply one. - FocusNode? _localFocusNode; - FocusNode get _focusNode => - widget.focusNode ?? - (_localFocusNode ??= FocusNode(debugLabel: 'SelectableRegion')); - - /// Notifies its listeners when the selection state in this [SelectableRegion] changes. - final _SelectableRegionSelectionStatusNotifier _selectionStatusNotifier = - _SelectableRegionSelectionStatusNotifier._(); - - @protected - @override - void initState() { - super.initState(); - _focusNode.addListener(_handleFocusChanged); - _initMouseGestureRecognizer(); - _initTouchGestureRecognizer(); - // Right clicks. - _gestureRecognizers[TapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(debugOwner: this), - (TapGestureRecognizer instance) { - instance.onSecondaryTapDown = _handleRightClickDown; - }, - ); - _initProcessTextActions(); - } - - /// Query the engine to initialize the list of text processing actions to show - /// in the text selection toolbar. - Future _initProcessTextActions() async { - _processTextActions - ..clear() - ..addAll(await _processTextService.queryTextActions()); - } - - @protected - @override - void didChangeDependencies() { - super.didChangeDependencies(); - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.iOS: - break; - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - return; - } - - // Hide the text selection toolbar on mobile when orientation changes. - final Orientation orientation = MediaQuery.orientationOf(context); - if (_lastOrientation == null) { - _lastOrientation = orientation; - return; - } - if (orientation != _lastOrientation) { - _lastOrientation = orientation; - hideToolbar(defaultTargetPlatform == TargetPlatform.android); - } - } - - @protected - @override - void didUpdateWidget(SelectableRegion oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.focusNode != oldWidget.focusNode) { - if (oldWidget.focusNode == null && widget.focusNode != null) { - _localFocusNode?.removeListener(_handleFocusChanged); - _localFocusNode?.dispose(); - _localFocusNode = null; - } else if (widget.focusNode == null && oldWidget.focusNode != null) { - oldWidget.focusNode!.removeListener(_handleFocusChanged); - } - _focusNode.addListener(_handleFocusChanged); - if (_focusNode.hasFocus != oldWidget.focusNode?.hasFocus) { - _handleFocusChanged(); - } - } - } - - Action _makeOverridable(Action defaultAction) { - return Action.overridable( - context: context, - defaultAction: defaultAction, - ); - } - - void _handleFocusChanged() { - if (!_focusNode.hasFocus) { - if (_webContextMenuEnabled) { - PlatformSelectableRegionContextMenu.detach(_selectionDelegate); - } - if (SchedulerBinding.instance.lifecycleState == - AppLifecycleState.resumed) { - // We should only clear the selection when this SelectableRegion loses - // focus while the application is currently running. It is possible - // that the application is not currently running, for example on desktop - // platforms, clicking on a different window switches the focus to - // the new window causing the Flutter application to go inactive. In this - // case we want to retain the selection so it remains when we return to - // the Flutter application. - clearSelection(); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - _finalizeSelectableRegionStatus(); - } - } - if (_webContextMenuEnabled) { - PlatformSelectableRegionContextMenu.attach(_selectionDelegate); - } - } - - void _updateSelectionStatus() { - final SelectionGeometry geometry = _selectionDelegate.value; - final TextSelection selection = switch (geometry.status) { - SelectionStatus.uncollapsed || SelectionStatus.collapsed => - const TextSelection(baseOffset: 0, extentOffset: 1), - SelectionStatus.none => const TextSelection.collapsed(offset: 1), - }; - textEditingValue = TextEditingValue(text: '__', selection: selection); - if (_hasSelectionOverlayGeometry) { - _updateSelectionOverlay(); - } else { - _selectionOverlay?.dispose(); - _selectionOverlay = null; - } - } - - // gestures. - - /// Whether the Shift key was pressed when the most recent [PointerDownEvent] - /// was tracked by the [BaseTapAndDragGestureRecognizer]. - bool _isShiftPressed = false; - - // The position of the most recent secondary tap down event on this - // SelectableRegion. - Offset? _lastSecondaryTapDownPosition; - - // The device kind for the pointer of the most recent tap down event on this - // SelectableRegion. - PointerDeviceKind? _lastPointerDeviceKind; - - static bool _isPrecisePointerDevice(PointerDeviceKind pointerDeviceKind) { - switch (pointerDeviceKind) { - case PointerDeviceKind.mouse: - return true; - case PointerDeviceKind.trackpad: - case PointerDeviceKind.stylus: - case PointerDeviceKind.invertedStylus: - case PointerDeviceKind.touch: - case PointerDeviceKind.unknown: - return false; - } - } - - void _finalizeSelectableRegionStatus() { - if (_selectionStatusNotifier.value != - SelectableRegionSelectionStatus.changing) { - // Don't finalize the selection again if it is not currently changing. - return; - } - _selectionStatusNotifier.value = SelectableRegionSelectionStatus.finalized; - } - - // Converts the details.consecutiveTapCount from a TapAndDrag*Details object, - // which can grow to be infinitely large, to a value between 1 and the supported - // max consecutive tap count. The value that the raw count is converted to is - // based on the default observed behavior on the native platforms. - // - // This method should be used in all instances when details.consecutiveTapCount - // would be used. - int _getEffectiveConsecutiveTapCount(int rawCount) { - var maxConsecutiveTap = 3; - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - if (_lastPointerDeviceKind != null && - _lastPointerDeviceKind != PointerDeviceKind.mouse) { - // When the pointer device kind is not precise like a mouse, native - // Android resets the tap count at 2. For example, this is so the - // selection can collapse on the third tap. - maxConsecutiveTap = 2; - } - // From observation, these platforms reset their tap count to 0 when - // the number of consecutive taps exceeds the max consecutive tap supported. - // For example on native Android, when going past a triple click, - // on the fourth click the selection is moved to the precise click - // position, on the fifth click the word at the position is selected, and - // on the sixth click the paragraph at the position is selected. - return rawCount <= maxConsecutiveTap - ? rawCount - : (rawCount % maxConsecutiveTap == 0 - ? maxConsecutiveTap - : rawCount % maxConsecutiveTap); - case TargetPlatform.linux: - // From observation, these platforms reset their tap count to 0 when - // the number of consecutive taps exceeds the max consecutive tap supported. - // For example on Debian Linux with GTK, when going past a triple click, - // on the fourth click the selection is moved to the precise click - // position, on the fifth click the word at the position is selected, and - // on the sixth click the paragraph at the position is selected. - return rawCount <= maxConsecutiveTap - ? rawCount - : (rawCount % maxConsecutiveTap == 0 - ? maxConsecutiveTap - : rawCount % maxConsecutiveTap); - case TargetPlatform.iOS: - case TargetPlatform.macOS: - case TargetPlatform.windows: - // From observation, these platforms hold their tap count at the max - // consecutive tap supported. For example on macOS, when going past a triple - // click, the selection should be retained at the paragraph that was first - // selected on triple click. - return min(rawCount, maxConsecutiveTap); - } - } - - void _initMouseGestureRecognizer() { - _gestureRecognizers[TapAndPanGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapAndPanGestureRecognizer( - debugOwner: this, - supportedDevices: {PointerDeviceKind.mouse}, - ), - (TapAndPanGestureRecognizer instance) { - instance - ..onTapTrackStart = _onTapTrackStart - ..onTapTrackReset = _onTapTrackReset - ..onTapDown = _startNewMouseSelectionGesture - ..onTapUp = _handleMouseTapUp - ..onDragStart = _handleMouseDragStart - ..onDragUpdate = _handleMouseDragUpdate - ..onDragEnd = _handleMouseDragEnd - ..onCancel = clearSelection - ..dragStartBehavior = DragStartBehavior.down; - }, - ); - } - - void _onTapTrackStart() { - _isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed.intersection( - { - LogicalKeyboardKey.shiftLeft, - LogicalKeyboardKey.shiftRight, - }, - ).isNotEmpty; - } - - void _onTapTrackReset() { - _isShiftPressed = false; - } - - void _initTouchGestureRecognizer() { - // A [TapAndHorizontalDragGestureRecognizer] is used on non-precise pointer devices - // like PointerDeviceKind.touch so [SelectableRegion] gestures do not conflict with - // ancestor Scrollable gestures in common scenarios like a vertically scrolling list view. - _gestureRecognizers[TapAndHorizontalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers< - TapAndHorizontalDragGestureRecognizer - >( - () => TapAndHorizontalDragGestureRecognizer( - debugOwner: this, - supportedDevices: PointerDeviceKind.values.where(( - PointerDeviceKind device, - ) { - return device != PointerDeviceKind.mouse; - }).toSet(), - ), - (TapAndHorizontalDragGestureRecognizer instance) { - instance - // iOS does not provide a device specific touch slop - // unlike Android (~8.0), so the touch slop for a [Scrollable] - // always default to kTouchSlop which is 18.0. When - // [SelectableRegion] is the child of a horizontal - // scrollable that means the [SelectableRegion] will - // always win the gesture arena when competing with - // the ancestor scrollable because they both have - // the same touch slop threshold and the child receives - // the [PointerEvent] first. To avoid this conflict - // and ensure a smooth scrolling experience, on - // iOS the [TapAndHorizontalDragGestureRecognizer] - // will wait for all other gestures to lose before - // declaring victory. - ..eagerVictoryOnDrag = defaultTargetPlatform != TargetPlatform.iOS - ..onTapDown = _startNewMouseSelectionGesture - ..onTapUp = _handleMouseTapUp - ..onDragStart = _handleMouseDragStart - ..onDragUpdate = _handleMouseDragUpdate - ..onDragEnd = _handleMouseDragEnd - ..onCancel = clearSelection - ..dragStartBehavior = DragStartBehavior.down; - }, - ); - _gestureRecognizers[LongPressGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer( - debugOwner: this, - supportedDevices: _kLongPressSelectionDevices, - ), - (LongPressGestureRecognizer instance) { - instance - ..onLongPressStart = _handleTouchLongPressStart - ..onLongPressMoveUpdate = _handleTouchLongPressMoveUpdate - ..onLongPressEnd = _handleTouchLongPressEnd; - }, - ); - } - - Offset? _doubleTapOffset; - void _startNewMouseSelectionGesture(TapDragDownDetails details) { - _lastPointerDeviceKind = details.kind; - switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { - case 1: - _focusNode.requestFocus(); - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.iOS: - // On mobile platforms the selection is set on tap up for the first - // tap. - break; - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - hideToolbar(); - // It is impossible to extend the selection when the shift key is - // pressed and the start of the selection has not been initialized. - // In this case we fallback on collapsing the selection to first - // initialize the selection. - final bool isShiftPressedValid = - _isShiftPressed && - _selectionDelegate.value.startSelectionPoint != null; - if (isShiftPressedValid) { - _selectEndTo(offset: details.globalPosition); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - break; - } - clearSelection(); - _collapseSelectionAt(offset: details.globalPosition); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - } - case 2: - switch (defaultTargetPlatform) { - case TargetPlatform.iOS: - if (kIsWeb && - details.kind != null && - !_isPrecisePointerDevice(details.kind!)) { - // Double tap on iOS web triggers when a drag begins after the double tap. - _doubleTapOffset = details.globalPosition; - break; - } - _selectWordAt(offset: details.globalPosition); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - if (details.kind != null && - !_isPrecisePointerDevice(details.kind!)) { - _showHandles(); - } - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - _selectWordAt(offset: details.globalPosition); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - } - case 3: - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.iOS: - if (details.kind != null && - _isPrecisePointerDevice(details.kind!)) { - // Triple tap on static text is only supported on mobile - // platforms using a precise pointer device. - _selectParagraphAt(offset: details.globalPosition); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - } - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - _selectParagraphAt(offset: details.globalPosition); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - } - } - _updateSelectedContentIfNeeded(); - } - - void _handleMouseDragStart(TapDragStartDetails details) { - switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { - case 1: - if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) { - // Drag to select is only enabled with a precise pointer device. - return; - } - _selectStartTo(offset: details.globalPosition); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - } - _updateSelectedContentIfNeeded(); - } - - void _handleMouseDragUpdate(TapDragUpdateDetails details) { - switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { - case 1: - if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) { - // Drag to select is only enabled with a precise pointer device. - return; - } - _selectEndTo(offset: details.globalPosition, continuous: true); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - case 2: - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - // Double tap + drag is only supported on Android when using a precise - // pointer device or when not on the web. - if (!kIsWeb || - details.kind != null && - _isPrecisePointerDevice(details.kind!)) { - _selectEndTo( - offset: details.globalPosition, - continuous: true, - textGranularity: TextGranularity.word, - ); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - } - case TargetPlatform.iOS: - if (kIsWeb && - details.kind != null && - !_isPrecisePointerDevice(details.kind!) && - _doubleTapOffset != null) { - // On iOS web a double tap does not select the word at the position, - // until the drag has begun. - _selectWordAt(offset: _doubleTapOffset!); - _doubleTapOffset = null; - } - _selectEndTo( - offset: details.globalPosition, - continuous: true, - textGranularity: TextGranularity.word, - ); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - if (details.kind != null && - !_isPrecisePointerDevice(details.kind!)) { - _showHandles(); - } - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - _selectEndTo( - offset: details.globalPosition, - continuous: true, - textGranularity: TextGranularity.word, - ); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - } - case 3: - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.iOS: - // Triple tap + drag is only supported on mobile devices when using - // a precise pointer device. - if (details.kind != null && - _isPrecisePointerDevice(details.kind!)) { - _selectEndTo( - offset: details.globalPosition, - continuous: true, - textGranularity: TextGranularity.paragraph, - ); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - } - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - _selectEndTo( - offset: details.globalPosition, - continuous: true, - textGranularity: TextGranularity.paragraph, - ); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - } - } - _updateSelectedContentIfNeeded(); - } - - void _handleMouseDragEnd(TapDragEndDetails details) { - assert(_lastPointerDeviceKind != null); - final bool isPointerPrecise = _isPrecisePointerDevice( - _lastPointerDeviceKind!, - ); - // On mobile platforms like android, fuchsia, and iOS, a drag gesture will - // only show the selection overlay when the drag has finished and the pointer - // device kind is not precise, for example at the end of a double tap + drag - // to select on native iOS. - final bool shouldShowSelectionOverlayOnMobile = !isPointerPrecise; - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - if (shouldShowSelectionOverlayOnMobile) { - _showHandles(); - _showToolbar(); - } - case TargetPlatform.iOS: - if (shouldShowSelectionOverlayOnMobile) { - _showToolbar(); - } - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - // The selection overlay is not shown on desktop platforms after a drag. - break; - } - _finalizeSelection(); - _updateSelectedContentIfNeeded(); - _finalizeSelectableRegionStatus(); - } - - void _handleMouseTapUp(TapDragUpDetails details) { - if (defaultTargetPlatform == TargetPlatform.iOS && - _positionIsOnActiveSelection(globalPosition: details.globalPosition)) { - // On iOS when the tap occurs on the previous selection, instead of - // moving the selection, the context menu will be toggled. - final bool toolbarIsVisible = - _selectionOverlay?.toolbarIsVisible ?? false; - if (toolbarIsVisible) { - hideToolbar(false); - } else { - _showToolbar(); - } - return; - } - switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) { - case 1: - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.iOS: - hideToolbar(); - _collapseSelectionAt(offset: details.globalPosition); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - // On desktop platforms the selection is set on tap down. - } - case 2: - final bool isPointerPrecise = _isPrecisePointerDevice(details.kind); - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - if (!isPointerPrecise) { - // On Android, a double tap will only show the selection overlay after - // the following tap up when the pointer device kind is not precise. - _showHandles(); - _showToolbar(); - } - case TargetPlatform.iOS: - if (!isPointerPrecise) { - if (kIsWeb) { - // Double tap on iOS web only triggers when a drag begins after the double tap. - break; - } - // On iOS, a double tap will only show the selection toolbar after - // the following tap up when the pointer device kind is not precise. - _showToolbar(); - } - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - // The selection overlay is not shown on desktop platforms - // on a double click. - break; - } - } - _finalizeSelectableRegionStatus(); - _updateSelectedContentIfNeeded(); - } - - void _updateSelectedContentIfNeeded() { - if (widget.onSelectionChanged == null) { - return; - } - final SelectedContent? content = _selectable?.getSelectedContent(); - if (_lastSelectedContent?.plainText != content?.plainText) { - _lastSelectedContent = content; - widget.onSelectionChanged!.call(_lastSelectedContent); - } - } - - void _handleTouchLongPressStart(LongPressStartDetails details) { - HapticFeedback.selectionClick(); - _focusNode.requestFocus(); - _selectWordAt(offset: details.globalPosition); - _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; - // Platforms besides Android will show the text selection handles when - // the long press is initiated. Android shows the text selection handles when - // the long press has ended, usually after a pointer up event is received. - if (defaultTargetPlatform != TargetPlatform.android) { - _showHandles(); - } - _updateSelectedContentIfNeeded(); - } - - void _handleTouchLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - _selectEndTo( - offset: details.globalPosition, - textGranularity: TextGranularity.word, - ); - _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; - _updateSelectedContentIfNeeded(); - } - - void _handleTouchLongPressEnd(LongPressEndDetails details) { - _finalizeSelection(); - _updateSelectedContentIfNeeded(); - _finalizeSelectableRegionStatus(); - _showToolbar(); - if (defaultTargetPlatform == TargetPlatform.android) { - _showHandles(); - } - } - - bool _positionIsOnActiveSelection({required Offset globalPosition}) { - for (final Rect selectionRect in _selectionDelegate.value.selectionRects) { - final Matrix4 transform = _selectable!.getTransformTo(null); - final Rect globalRect = MatrixUtils.transformRect( - transform, - selectionRect, - ); - if (globalRect.contains(globalPosition)) { - return true; - } - } - return false; - } - - void _handleRightClickDown(TapDownDetails details) { - final Offset? previousSecondaryTapDownPosition = - _lastSecondaryTapDownPosition; - final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false; - _lastSecondaryTapDownPosition = details.globalPosition; - _focusNode.requestFocus(); - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.windows: - // If _lastSecondaryTapDownPosition is within the current selection then - // keep the current selection, if not then collapse it. - final bool lastSecondaryTapDownPositionWasOnActiveSelection = - _positionIsOnActiveSelection( - globalPosition: details.globalPosition, - ); - if (lastSecondaryTapDownPositionWasOnActiveSelection) { - // Restore _lastSecondaryTapDownPosition since it may be cleared if a user - // accesses contextMenuAnchors. - _lastSecondaryTapDownPosition = details.globalPosition; - _showHandles(); - _showToolbar(location: _lastSecondaryTapDownPosition); - _updateSelectedContentIfNeeded(); - return; - } - _collapseSelectionAt(offset: _lastSecondaryTapDownPosition!); - case TargetPlatform.iOS: - _selectWordAt(offset: _lastSecondaryTapDownPosition!); - case TargetPlatform.macOS: - if (previousSecondaryTapDownPosition == _lastSecondaryTapDownPosition && - toolbarIsVisible) { - hideToolbar(); - return; - } - _selectWordAt(offset: _lastSecondaryTapDownPosition!); - case TargetPlatform.linux: - if (toolbarIsVisible) { - hideToolbar(); - return; - } - // If _lastSecondaryTapDownPosition is within the current selection then - // keep the current selection, if not then collapse it. - final bool lastSecondaryTapDownPositionWasOnActiveSelection = - _positionIsOnActiveSelection( - globalPosition: details.globalPosition, - ); - if (!lastSecondaryTapDownPositionWasOnActiveSelection) { - _collapseSelectionAt(offset: _lastSecondaryTapDownPosition!); - } - } - _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; - _finalizeSelectableRegionStatus(); - // Restore _lastSecondaryTapDownPosition since it may be cleared if a user - // accesses contextMenuAnchors. - _lastSecondaryTapDownPosition = details.globalPosition; - _showHandles(); - _showToolbar(location: _lastSecondaryTapDownPosition); - _updateSelectedContentIfNeeded(); - } - - // Selection update helper methods. - - Offset? _selectionEndPosition; - bool get _userDraggingSelectionEnd => _selectionEndPosition != null; - bool _scheduledSelectionEndEdgeUpdate = false; - - /// Sends end [SelectionEdgeUpdateEvent] to the selectable subtree. - /// - /// If the selectable subtree returns a [SelectionResult.pending], this method - /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result - /// is not pending or users end their gestures. - void _triggerSelectionEndEdgeUpdate({TextGranularity? textGranularity}) { - // This method can be called when the drag is not in progress. This can - // happen if the child scrollable returns SelectionResult.pending, and - // the selection area scheduled a selection update for the next frame, but - // the drag is lifted before the scheduled selection update is run. - if (_scheduledSelectionEndEdgeUpdate || !_userDraggingSelectionEnd) { - return; - } - if (_selectable?.dispatchSelectionEvent( - SelectionEdgeUpdateEvent.forEnd( - globalPosition: _selectionEndPosition!, - granularity: textGranularity, - ), - ) == - SelectionResult.pending) { - _scheduledSelectionEndEdgeUpdate = true; - SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { - if (!_scheduledSelectionEndEdgeUpdate) { - return; - } - _scheduledSelectionEndEdgeUpdate = false; - _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity); - }, debugLabel: 'SelectableRegion.endEdgeUpdate'); - return; - } - } - - void _onAnyDragEnd(DragEndDetails details) { - final bool draggingHandles = - _selectionOverlay != null && - (_selectionOverlay!.isDraggingStartHandle || - _selectionOverlay!.isDraggingEndHandle); - if (!draggingHandles) { - _selectionOverlay!.hideMagnifier(); - _showToolbar(); - } - _finalizeSelection(); - _updateSelectedContentIfNeeded(); - _finalizeSelectableRegionStatus(); - } - - void _stopSelectionEndEdgeUpdate() { - _scheduledSelectionEndEdgeUpdate = false; - _selectionEndPosition = null; - } - - Offset? _selectionStartPosition; - bool get _userDraggingSelectionStart => _selectionStartPosition != null; - bool _scheduledSelectionStartEdgeUpdate = false; - - /// Sends start [SelectionEdgeUpdateEvent] to the selectable subtree. - /// - /// If the selectable subtree returns a [SelectionResult.pending], this method - /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result - /// is not pending or users end their gestures. - void _triggerSelectionStartEdgeUpdate({TextGranularity? textGranularity}) { - // This method can be called when the drag is not in progress. This can - // happen if the child scrollable returns SelectionResult.pending, and - // the selection area scheduled a selection update for the next frame, but - // the drag is lifted before the scheduled selection update is run. - if (_scheduledSelectionStartEdgeUpdate || !_userDraggingSelectionStart) { - return; - } - if (_selectable?.dispatchSelectionEvent( - SelectionEdgeUpdateEvent.forStart( - globalPosition: _selectionStartPosition!, - granularity: textGranularity, - ), - ) == - SelectionResult.pending) { - _scheduledSelectionStartEdgeUpdate = true; - SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { - if (!_scheduledSelectionStartEdgeUpdate) { - return; - } - _scheduledSelectionStartEdgeUpdate = false; - _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity); - }, debugLabel: 'SelectableRegion.startEdgeUpdate'); - return; - } - } - - void _stopSelectionStartEdgeUpdate() { - _scheduledSelectionStartEdgeUpdate = false; - _selectionEndPosition = null; - } - - // SelectionOverlay helper methods. - - late Offset _selectionStartHandleDragPosition; - late Offset _selectionEndHandleDragPosition; - - void _handleSelectionStartHandleDragStart(DragStartDetails details) { - assert(_selectionDelegate.value.startSelectionPoint != null); - - final Offset localPosition = - _selectionDelegate.value.startSelectionPoint!.localPosition; - final Matrix4 globalTransform = _selectable!.getTransformTo(null); - _selectionStartHandleDragPosition = MatrixUtils.transformPoint( - globalTransform, - localPosition, - ); - - _selectionOverlay!.showMagnifier( - _buildInfoForMagnifier( - details.globalPosition, - _selectionDelegate.value.startSelectionPoint!, - ), - ); - _updateSelectedContentIfNeeded(); - } - - void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) { - _selectionStartHandleDragPosition = - _selectionStartHandleDragPosition + details.delta; - // The value corresponds to the paint origin of the selection handle. - // Offset it to the center of the line to make it feel more natural. - _selectionStartPosition = - _selectionStartHandleDragPosition - - Offset(0, _selectionDelegate.value.startSelectionPoint!.lineHeight / 2); - _triggerSelectionStartEdgeUpdate(); - - _selectionOverlay!.updateMagnifier( - _buildInfoForMagnifier( - details.globalPosition, - _selectionDelegate.value.startSelectionPoint!, - ), - ); - _updateSelectedContentIfNeeded(); - _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; - } - - void _handleSelectionEndHandleDragStart(DragStartDetails details) { - assert(_selectionDelegate.value.endSelectionPoint != null); - final Offset localPosition = - _selectionDelegate.value.endSelectionPoint!.localPosition; - final Matrix4 globalTransform = _selectable!.getTransformTo(null); - _selectionEndHandleDragPosition = MatrixUtils.transformPoint( - globalTransform, - localPosition, - ); - - _selectionOverlay!.showMagnifier( - _buildInfoForMagnifier( - details.globalPosition, - _selectionDelegate.value.endSelectionPoint!, - ), - ); - _updateSelectedContentIfNeeded(); - } - - void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) { - _selectionEndHandleDragPosition = - _selectionEndHandleDragPosition + details.delta; - // The value corresponds to the paint origin of the selection handle. - // Offset it to the center of the line to make it feel more natural. - _selectionEndPosition = - _selectionEndHandleDragPosition - - Offset(0, _selectionDelegate.value.endSelectionPoint!.lineHeight / 2); - _triggerSelectionEndEdgeUpdate(); - - _selectionOverlay!.updateMagnifier( - _buildInfoForMagnifier( - details.globalPosition, - _selectionDelegate.value.endSelectionPoint!, - ), - ); - _updateSelectedContentIfNeeded(); - _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; - } - - MagnifierInfo _buildInfoForMagnifier( - Offset globalGesturePosition, - SelectionPoint selectionPoint, - ) { - final Vector3 globalTransform = _selectable! - .getTransformTo(null) - .getTranslation(); - final globalTransformAsOffset = Offset( - globalTransform.x, - globalTransform.y, - ); - final Offset globalSelectionPointPosition = - selectionPoint.localPosition + globalTransformAsOffset; - final caretRect = Rect.fromLTWH( - globalSelectionPointPosition.dx, - globalSelectionPointPosition.dy - selectionPoint.lineHeight, - 0, - selectionPoint.lineHeight, - ); - - return MagnifierInfo( - globalGesturePosition: globalGesturePosition, - caretRect: caretRect, - fieldBounds: globalTransformAsOffset & _selectable!.size, - currentLineBoundaries: globalTransformAsOffset & _selectable!.size, - ); - } - - void _createSelectionOverlay() { - assert(_hasSelectionOverlayGeometry); - if (_selectionOverlay != null) { - return; - } - final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint; - final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint; - _selectionOverlay = SelectionOverlay( - context: context, - debugRequiredFor: widget, - startHandleType: start?.handleType ?? TextSelectionHandleType.collapsed, - lineHeightAtStart: start?.lineHeight ?? end!.lineHeight, - onStartHandleDragStart: _handleSelectionStartHandleDragStart, - onStartHandleDragUpdate: _handleSelectionStartHandleDragUpdate, - onStartHandleDragEnd: _onAnyDragEnd, - endHandleType: end?.handleType ?? TextSelectionHandleType.collapsed, - lineHeightAtEnd: end?.lineHeight ?? start!.lineHeight, - onEndHandleDragStart: _handleSelectionEndHandleDragStart, - onEndHandleDragUpdate: _handleSelectionEndHandleDragUpdate, - onEndHandleDragEnd: _onAnyDragEnd, - selectionEndpoints: selectionEndpoints, - selectionControls: widget.selectionControls, - selectionDelegate: this, - clipboardStatus: null, - startHandleLayerLink: _startHandleLayerLink, - endHandleLayerLink: _endHandleLayerLink, - toolbarLayerLink: _toolbarLayerLink, - magnifierConfiguration: widget.magnifierConfiguration, - ); - } - - void _updateSelectionOverlay() { - if (_selectionOverlay == null) { - return; - } - assert(_hasSelectionOverlayGeometry); - final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint; - final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint; - _selectionOverlay! - ..startHandleType = start?.handleType ?? TextSelectionHandleType.left - ..lineHeightAtStart = start?.lineHeight ?? end!.lineHeight - ..endHandleType = end?.handleType ?? TextSelectionHandleType.right - ..lineHeightAtEnd = end?.lineHeight ?? start!.lineHeight - ..selectionEndpoints = selectionEndpoints; - } - - /// Shows the selection handles. - /// - /// Returns true if the handles are shown, false if the handles can't be - /// shown. - bool _showHandles() { - if (_selectionOverlay != null) { - _selectionOverlay!.showHandles(); - return true; - } - - if (!_hasSelectionOverlayGeometry) { - return false; - } - - _createSelectionOverlay(); - _selectionOverlay!.showHandles(); - return true; - } - - /// Shows the text selection toolbar. - /// - /// If the parameter `location` is set, the toolbar will be shown at the - /// location. Otherwise, the toolbar location will be calculated based on the - /// handles' locations. The `location` is in the coordinates system of the - /// [Overlay]. - /// - /// Returns true if the toolbar is shown, false if the toolbar can't be shown. - bool _showToolbar({Offset? location}) { - if (!_hasSelectionOverlayGeometry && _selectionOverlay == null) { - return false; - } - - // Web is using native dom elements to enable clipboard functionality of the - // context menu: copy, paste, select, cut. It might also provide additional - // functionality depending on the browser (such as translate). Due to this, - // we should not show a Flutter toolbar for the editable text elements - // unless the browser's context menu is explicitly disabled. - if (_webContextMenuEnabled) { - return false; - } - - if (_selectionOverlay == null) { - _createSelectionOverlay(); - } - - _selectionOverlay!.toolbarLocation = location; - // TODO(Renzo-Olivares): Remove the logic below that does a runtimeType - // check for TextSelectionHandleControls when TextSelectionHandleControls - // is fully removed, see: https://github.com/flutter/flutter/pull/124262. - if (widget.selectionControls is! TextSelectionHandleControls) { - _selectionOverlay!.showToolbar(); - return true; - } - - _selectionOverlay!.hideToolbar(); - - _selectionOverlay!.showToolbar( - context: context, - contextMenuBuilder: (BuildContext context) { - return widget.contextMenuBuilder!(context, this); - }, - ); - return true; - } - - /// Sets or updates selection end edge to the `offset` location. - /// - /// A selection always contains a select start edge and selection end edge. - /// They can be created by calling both [_selectStartTo] and [_selectEndTo], or - /// use other selection APIs, such as [_selectWordAt] or [selectAll]. - /// - /// This method sets or updates the selection end edge by sending - /// [SelectionEdgeUpdateEvent]s to the child [Selectable]s. - /// - /// If `continuous` is set to true and the update causes scrolling, the - /// method will continue sending the same [SelectionEdgeUpdateEvent]s to the - /// child [Selectable]s every frame until the scrolling finishes or a - /// [_finalizeSelection] is called. - /// - /// The `continuous` argument defaults to false. - /// - /// The `offset` is in global coordinates. - /// - /// Provide the `textGranularity` if the selection should not move by the default - /// [TextGranularity.character]. Only [TextGranularity.character] and - /// [TextGranularity.word] are currently supported. - /// - /// See also: - /// * [_selectStartTo], which sets or updates selection start edge. - /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [clearSelection], which clears the ongoing selection. - /// * [_selectWordAt], which selects a whole word at the location. - /// * [_selectParagraphAt], which selects an entire paragraph at the location. - /// * [_collapseSelectionAt], which collapses the selection at the location. - /// * [selectAll], which selects the entire content. - void _selectEndTo({ - required Offset offset, - bool continuous = false, - TextGranularity? textGranularity, - }) { - if (!continuous) { - _selectable?.dispatchSelectionEvent( - SelectionEdgeUpdateEvent.forEnd( - globalPosition: offset, - granularity: textGranularity, - ), - ); - return; - } - if (_selectionEndPosition != offset) { - _selectionEndPosition = offset; - _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity); - } - } - - /// Sets or updates selection start edge to the `offset` location. - /// - /// A selection always contains a select start edge and selection end edge. - /// They can be created by calling both [_selectStartTo] and [_selectEndTo], or - /// use other selection APIs, such as [_selectWordAt] or [selectAll]. - /// - /// This method sets or updates the selection start edge by sending - /// [SelectionEdgeUpdateEvent]s to the child [Selectable]s. - /// - /// If `continuous` is set to true and the update causes scrolling, the - /// method will continue sending the same [SelectionEdgeUpdateEvent]s to the - /// child [Selectable]s every frame until the scrolling finishes or a - /// [_finalizeSelection] is called. - /// - /// The `continuous` argument defaults to false. - /// - /// The `offset` is in global coordinates. - /// - /// Provide the `textGranularity` if the selection should not move by the default - /// [TextGranularity.character]. Only [TextGranularity.character] and - /// [TextGranularity.word] are currently supported. - /// - /// See also: - /// * [_selectEndTo], which sets or updates selection end edge. - /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [clearSelection], which clears the ongoing selection. - /// * [_selectWordAt], which selects a whole word at the location. - /// * [_selectParagraphAt], which selects an entire paragraph at the location. - /// * [_collapseSelectionAt], which collapses the selection at the location. - /// * [selectAll], which selects the entire content. - void _selectStartTo({ - required Offset offset, - bool continuous = false, - TextGranularity? textGranularity, - }) { - if (!continuous) { - _selectable?.dispatchSelectionEvent( - SelectionEdgeUpdateEvent.forStart( - globalPosition: offset, - granularity: textGranularity, - ), - ); - return; - } - if (_selectionStartPosition != offset) { - _selectionStartPosition = offset; - _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity); - } - } - - /// Collapses the selection at the given `offset` location. - /// - /// The `offset` is in global coordinates. - /// - /// See also: - /// * [_selectStartTo], which sets or updates selection start edge. - /// * [_selectEndTo], which sets or updates selection end edge. - /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [clearSelection], which clears the ongoing selection. - /// * [_selectWordAt], which selects a whole word at the location. - /// * [_selectParagraphAt], which selects an entire paragraph at the location. - /// * [selectAll], which selects the entire content. - void _collapseSelectionAt({required Offset offset}) { - // There may be other selection ongoing. - _finalizeSelection(); - _selectStartTo(offset: offset); - _selectEndTo(offset: offset); - } - - /// Selects a whole word at the `offset` location. - /// - /// The `offset` is in global coordinates. - /// - /// If the whole word is already in the current selection, selection won't - /// change. One call [clearSelection] first if the selection needs to be - /// updated even if the word is already covered by the current selection. - /// - /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection - /// edges after calling this method. - /// - /// See also: - /// * [_selectStartTo], which sets or updates selection start edge. - /// * [_selectEndTo], which sets or updates selection end edge. - /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [clearSelection], which clears the ongoing selection. - /// * [_collapseSelectionAt], which collapses the selection at the location. - /// * [_selectParagraphAt], which selects an entire paragraph at the location. - /// * [selectAll], which selects the entire content. - void _selectWordAt({required Offset offset}) { - // There may be other selection ongoing. - _finalizeSelection(); - _selectable?.dispatchSelectionEvent( - SelectWordSelectionEvent(globalPosition: offset), - ); - } - - /// Selects the entire paragraph at the `offset` location. - /// - /// The `offset` is in global coordinates. - /// - /// If the paragraph is already in the current selection, selection won't - /// change. One call [clearSelection] first if the selection needs to be - /// updated even if the paragraph is already covered by the current selection. - /// - /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection - /// edges after calling this method. - /// - /// See also: - /// * [_selectStartTo], which sets or updates selection start edge. - /// * [_selectEndTo], which sets or updates selection end edge. - /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [clearSelection], which clear the ongoing selection. - /// * [_selectWordAt], which selects a whole word at the location. - /// * [selectAll], which selects the entire content. - void _selectParagraphAt({required Offset offset}) { - // There may be other selection ongoing. - _finalizeSelection(); - _selectable?.dispatchSelectionEvent( - SelectParagraphSelectionEvent(globalPosition: offset), - ); - } - - /// Stops any ongoing selection updates. - /// - /// This method is different from [clearSelection] that it does not remove - /// the current selection. It only stops the continuous updates. - /// - /// A continuous update can happen as result of calling [_selectStartTo] or - /// [_selectEndTo] with `continuous` sets to true which causes a [Selectable] - /// to scroll. Calling this method will stop the update as well as the - /// scrolling. - void _finalizeSelection() { - _stopSelectionEndEdgeUpdate(); - _stopSelectionStartEdgeUpdate(); - } - - /// Removes the ongoing selection for this [SelectableRegion]. - void clearSelection() { - _finalizeSelection(); - _directionalHorizontalBaseline = null; - _adjustingSelectionEnd = null; - _selectable?.dispatchSelectionEvent(const ClearSelectionEvent()); - _updateSelectedContentIfNeeded(); - } - - Future _copy() async { - final SelectedContent? data = _selectable?.getSelectedContent(); - if (data == null) { - return; - } - await Clipboard.setData(ClipboardData(text: data.plainText)); - } - - Future _share() async { - final SelectedContent? data = _selectable?.getSelectedContent(); - if (data == null) { - return; - } - await SystemChannels.platform.invokeMethod('Share.invoke', data.plainText); - } - - /// {@macro flutter.widgets.EditableText.getAnchors} - /// - /// See also: - /// - /// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s - /// for the default context menu buttons. - TextSelectionToolbarAnchors get contextMenuAnchors { - if (_lastSecondaryTapDownPosition != null) { - final anchors = TextSelectionToolbarAnchors( - primaryAnchor: _lastSecondaryTapDownPosition!, - ); - // Clear the state of _lastSecondaryTapDownPosition after use since a user may - // access contextMenuAnchors and receive invalid anchors for their context menu. - _lastSecondaryTapDownPosition = null; - return anchors; - } - final renderBox = context.findRenderObject()! as RenderBox; - return TextSelectionToolbarAnchors.fromSelection( - renderBox: renderBox, - startGlyphHeight: startGlyphHeight, - endGlyphHeight: endGlyphHeight, - selectionEndpoints: selectionEndpoints, - ); - } - - bool? _adjustingSelectionEnd; - bool _determineIsAdjustingSelectionEnd(bool forward) { - if (_adjustingSelectionEnd != null) { - return _adjustingSelectionEnd!; - } - final bool isReversed; - final SelectionPoint start = _selectionDelegate.value.startSelectionPoint!; - final SelectionPoint end = _selectionDelegate.value.endSelectionPoint!; - if (start.localPosition.dy > end.localPosition.dy) { - isReversed = true; - } else if (start.localPosition.dy < end.localPosition.dy) { - isReversed = false; - } else { - isReversed = start.localPosition.dx > end.localPosition.dx; - } - // Always move the selection edge that increases the selection range. - return _adjustingSelectionEnd = forward != isReversed; - } - - void _granularlyExtendSelection(TextGranularity granularity, bool forward) { - _directionalHorizontalBaseline = null; - if (!_selectionDelegate.value.hasSelection) { - return; - } - _selectable?.dispatchSelectionEvent( - GranularlyExtendSelectionEvent( - forward: forward, - isEnd: _determineIsAdjustingSelectionEnd(forward), - granularity: granularity, - ), - ); - _updateSelectedContentIfNeeded(); - _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; - _finalizeSelectableRegionStatus(); - } - - double? _directionalHorizontalBaseline; - - void _directionallyExtendSelection(bool forward) { - if (!_selectionDelegate.value.hasSelection) { - return; - } - final bool adjustingSelectionExtend = _determineIsAdjustingSelectionEnd( - forward, - ); - final SelectionPoint baseLinePoint = adjustingSelectionExtend - ? _selectionDelegate.value.endSelectionPoint! - : _selectionDelegate.value.startSelectionPoint!; - _directionalHorizontalBaseline ??= baseLinePoint.localPosition.dx; - final Offset globalSelectionPointOffset = MatrixUtils.transformPoint( - context.findRenderObject()!.getTransformTo(null), - Offset(_directionalHorizontalBaseline!, 0), - ); - _selectable?.dispatchSelectionEvent( - DirectionallyExtendSelectionEvent( - isEnd: _adjustingSelectionEnd!, - direction: forward - ? SelectionExtendDirection.nextLine - : SelectionExtendDirection.previousLine, - dx: globalSelectionPointOffset.dx, - ), - ); - _updateSelectedContentIfNeeded(); - _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; - _finalizeSelectableRegionStatus(); - } - - // [TextSelectionDelegate] overrides. - - /// Returns the [ContextMenuButtonItem]s representing the buttons in this - /// platform's default selection menu. - /// - /// See also: - /// - /// * [SelectableRegion.getSelectableButtonItems], which performs a similar role, - /// but for any selectable text, not just specifically SelectableRegion. - /// * [EditableTextState.contextMenuButtonItems], which performs a similar role - /// but for content that is not just selectable but also editable. - /// * [contextMenuAnchors], which provides the anchor points for the default - /// context menu. - /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can - /// take a list of [ContextMenuButtonItem]s with - /// [AdaptiveTextSelectionToolbar.buttonItems]. - /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the - /// button Widgets for the current platform given [ContextMenuButtonItem]s. - List get contextMenuButtonItems { - return SelectableRegion.getSelectableButtonItems( - selectionGeometry: _selectionDelegate.value, - onCopy: () { - _copy(); - - // On Android copy should clear the selection. - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - clearSelection(); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - _finalizeSelectableRegionStatus(); - case TargetPlatform.iOS: - hideToolbar(false); - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - hideToolbar(); - } - }, - onSelectAll: () { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.iOS: - case TargetPlatform.fuchsia: - selectAll(SelectionChangedCause.toolbar); - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - selectAll(); - hideToolbar(); - } - }, - onShare: () { - _share(); - - // On Android, share should clear the selection. - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - clearSelection(); - _selectionStatusNotifier.value = - SelectableRegionSelectionStatus.changing; - _finalizeSelectableRegionStatus(); - case TargetPlatform.iOS: - hideToolbar(false); - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - hideToolbar(); - } - }, - )..addAll(_textProcessingActionButtonItems); - } - - List get _textProcessingActionButtonItems { - final buttonItems = []; - final SelectedContent? data = _selectable?.getSelectedContent(); - if (data == null) { - return buttonItems; - } - - for (final ProcessTextAction action in _processTextActions) { - buttonItems.add( - ContextMenuButtonItem( - label: action.label, - onPressed: () async { - final String selectedText = data.plainText; - if (selectedText.isNotEmpty) { - await _processTextService.processTextAction( - action.id, - selectedText, - true, - ); - hideToolbar(); - } - }, - ), - ); - } - return buttonItems; - } - - /// The line height at the start of the current selection. - double get startGlyphHeight { - return _selectionDelegate.value.startSelectionPoint!.lineHeight; - } - - /// The line height at the end of the current selection. - double get endGlyphHeight { - return _selectionDelegate.value.endSelectionPoint!.lineHeight; - } - - /// Returns the local coordinates of the endpoints of the current selection. - List get selectionEndpoints { - final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint; - final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint; - late List points; - final Offset startLocalPosition = - start?.localPosition ?? end!.localPosition; - final Offset endLocalPosition = end?.localPosition ?? start!.localPosition; - if (startLocalPosition.dy > endLocalPosition.dy) { - points = [ - TextSelectionPoint(endLocalPosition, TextDirection.ltr), - TextSelectionPoint(startLocalPosition, TextDirection.ltr), - ]; - } else { - points = [ - TextSelectionPoint(startLocalPosition, TextDirection.ltr), - TextSelectionPoint(endLocalPosition, TextDirection.ltr), - ]; - } - return points; - } - - // [TextSelectionDelegate] overrides. - // TODO(justinmc): After deprecations have been removed, remove - // TextSelectionDelegate from this class. - // https://github.com/flutter/flutter/issues/111213 - - @Deprecated( - 'Use `contextMenuBuilder` instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', - ) - @override - bool get cutEnabled => false; - - @Deprecated( - 'Use `contextMenuBuilder` instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', - ) - @override - bool get pasteEnabled => false; - - @override - void hideToolbar([bool hideHandles = true]) { - _selectionOverlay?.hideToolbar(); - if (hideHandles) { - _selectionOverlay?.hideHandles(); - } - } - - @override - void selectAll([SelectionChangedCause? cause]) { - clearSelection(); - _selectable?.dispatchSelectionEvent(const SelectAllSelectionEvent()); - if (cause == SelectionChangedCause.toolbar) { - _showToolbar(); - _showHandles(); - } - _updateSelectedContentIfNeeded(); - _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; - _finalizeSelectableRegionStatus(); - } - - @Deprecated( - 'Use `contextMenuBuilder` instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', - ) - @override - void copySelection(SelectionChangedCause cause) { - _copy(); - clearSelection(); - _selectionStatusNotifier.value = SelectableRegionSelectionStatus.changing; - _finalizeSelectableRegionStatus(); - } - - @Deprecated( - 'Use `contextMenuBuilder` instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', - ) - @override - TextEditingValue textEditingValue = const TextEditingValue(text: '_'); - - @Deprecated( - 'Use `contextMenuBuilder` instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', - ) - @override - void bringIntoView(TextPosition position) { - /* SelectableRegion must be in view at this point. */ - } - - @Deprecated( - 'Use `contextMenuBuilder` instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', - ) - @override - void cutSelection(SelectionChangedCause cause) { - assert(false); - } - - @Deprecated( - 'Use `contextMenuBuilder` instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', - ) - @override - void userUpdateTextEditingValue( - TextEditingValue value, - SelectionChangedCause cause, - ) { - /* SelectableRegion maintains its own state */ - } - - @Deprecated( - 'Use `contextMenuBuilder` instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', - ) - @override - Future pasteText(SelectionChangedCause cause) async { - assert(false); - } - - // [SelectionRegistrar] override. - - @override - void add(Selectable selectable) { - assert(_selectable == null); - _selectable = selectable; - _selectable!.addListener(_updateSelectionStatus); - _selectable!.pushHandleLayers(_startHandleLayerLink, _endHandleLayerLink); - } - - @override - void remove(Selectable selectable) { - assert(_selectable == selectable); - _selectable!.removeListener(_updateSelectionStatus); - _selectable!.pushHandleLayers(null, null); - _selectable = null; - } - - @protected - @override - void dispose() { - _selectable?.removeListener(_updateSelectionStatus); - _selectable?.pushHandleLayers(null, null); - _selectionDelegate.dispose(); - _selectionStatusNotifier.dispose(); - // In case dispose was triggered before gesture end, remove the magnifier - // so it doesn't remain stuck in the overlay forever. - _selectionOverlay?.hideMagnifier(); - _selectionOverlay?.dispose(); - _selectionOverlay = null; - widget.focusNode?.removeListener(_handleFocusChanged); - _localFocusNode?.removeListener(_handleFocusChanged); - _localFocusNode?.dispose(); - super.dispose(); - } - - @protected - @override - Widget build(BuildContext context) { - assert(debugCheckHasOverlay(context)); - Widget result = SelectableRegionSelectionStatusScope._( - selectionStatusNotifier: _selectionStatusNotifier, - child: SelectionContainer( - registrar: this, - delegate: _selectionDelegate, - child: widget.child, - ), - ); - if (_webContextMenuEnabled) { - result = PlatformSelectableRegionContextMenu(child: result); - } - return CompositedTransformTarget( - link: _toolbarLayerLink, - child: RawGestureDetector( - gestures: _gestureRecognizers, - behavior: HitTestBehavior.translucent, - excludeFromSemantics: true, - child: Actions( - actions: _actions, - child: Focus.withExternalFocusNode( - includeSemantics: false, - focusNode: _focusNode, - child: result, - ), - ), - ), - ); - } -} - -/// An action that does not override any [Action.overridable] in the subtree. -/// -/// If this action is invoked by an [Action.overridable], it will immediately -/// invoke the [Action.overridable] and do nothing else. Otherwise, it will call -/// [invokeAction]. -abstract class _NonOverrideAction extends ContextAction { - Object? invokeAction(T intent, [BuildContext? context]); - - @override - Object? invoke(T intent, [BuildContext? context]) { - if (callingAction != null) { - return callingAction!.invoke(intent); - } - return invokeAction(intent, context); - } -} - -class _SelectAllAction extends _NonOverrideAction { - _SelectAllAction(this.state); - - final SelectableRegionState state; - - @override - void invokeAction(SelectAllTextIntent intent, [BuildContext? context]) { - state.selectAll(SelectionChangedCause.keyboard); - } -} - -class _CopySelectionAction extends _NonOverrideAction { - _CopySelectionAction(this.state); - - final SelectableRegionState state; - - @override - void invokeAction(CopySelectionTextIntent intent, [BuildContext? context]) { - state._copy(); - } -} - -class _GranularlyExtendSelectionAction - extends _NonOverrideAction { - _GranularlyExtendSelectionAction(this.state, {required this.granularity}); - - final SelectableRegionState state; - final TextGranularity granularity; - - @override - void invokeAction(T intent, [BuildContext? context]) { - state._granularlyExtendSelection(granularity, intent.forward); - } -} - -class _GranularlyExtendCaretSelectionAction< - T extends DirectionalCaretMovementIntent -> - extends _NonOverrideAction { - _GranularlyExtendCaretSelectionAction( - this.state, { - required this.granularity, - }); - - final SelectableRegionState state; - final TextGranularity granularity; - - @override - void invokeAction(T intent, [BuildContext? context]) { - if (intent.collapseSelection) { - // Selectable region never collapses selection. - return; - } - state._granularlyExtendSelection(granularity, intent.forward); - } -} - -class _DirectionallyExtendCaretSelectionAction< - T extends DirectionalCaretMovementIntent -> - extends _NonOverrideAction { - _DirectionallyExtendCaretSelectionAction(this.state); - - final SelectableRegionState state; - - @override - void invokeAction(T intent, [BuildContext? context]) { - if (intent.collapseSelection) { - // Selectable region never collapses selection. - return; - } - state._directionallyExtendSelection(intent.forward); - } -} - -/// Signature for a widget builder that builds a context menu for the given -/// [SelectableRegionState]. -/// -/// See also: -/// -/// * [EditableTextContextMenuBuilder], which performs the same role for -/// [EditableText]. -typedef SelectableRegionContextMenuBuilder = - Widget Function( - BuildContext context, - SelectableRegionState selectableRegionState, - ); - -/// Notifies its listeners when the [SelectableRegion] that created this object -/// is changing or finalizes its selection. -/// -/// To access the [_SelectableRegionSelectionStatusNotifier] from the nearest [SelectableRegion] -/// ancestor, use [SelectableRegionSelectionStatusScope.maybeOf]. -final class _SelectableRegionSelectionStatusNotifier extends ChangeNotifier - implements ValueListenable { - _SelectableRegionSelectionStatusNotifier._(); - - SelectableRegionSelectionStatus _selectableRegionSelectionStatus = - SelectableRegionSelectionStatus.finalized; - - /// The current value of the [SelectableRegionSelectionStatus] of the [SelectableRegion] - /// that owns this object. - /// - /// Defaults to [SelectableRegionSelectionStatus.finalized]. - @override - SelectableRegionSelectionStatus get value => _selectableRegionSelectionStatus; - - /// Sets the [SelectableRegionSelectionStatus] for the [SelectableRegion] that - /// owns this object. - /// - /// Listeners are notified even if the value did not change. - @protected - set value(SelectableRegionSelectionStatus newStatus) { - assert( - newStatus == SelectableRegionSelectionStatus.finalized && - value == SelectableRegionSelectionStatus.changing || - newStatus == SelectableRegionSelectionStatus.changing, - 'Attempting to finalize the selection when it is already finalized.', - ); - _selectableRegionSelectionStatus = newStatus; - notifyListeners(); - } -} - -/// Notifies its listeners when the selection under a [SelectableRegion] or -/// [SelectionArea] is being changed or finalized. -/// -/// Use [SelectableRegionSelectionStatusScope.maybeOf], to access the [ValueListenable] of type -/// [SelectableRegionSelectionStatus] under a [SelectableRegion]. Its listeners -/// will be called even when the value of the [SelectableRegionSelectionStatus] -/// does not change. -final class SelectableRegionSelectionStatusScope extends InheritedWidget { - const SelectableRegionSelectionStatusScope._({ - required this.selectionStatusNotifier, - required super.child, - }); - - /// Tracks updates to the [SelectableRegionSelectionStatus] of the owning - /// [SelectableRegion]. - /// - /// Listeners will be called even when the value of the [SelectableRegionSelectionStatus] - /// does not change. The selection under the [SelectableRegion] still may have changed. - final ValueListenable - selectionStatusNotifier; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [SelectableRegion] or [SelectionArea] widget, then null is - /// returned. - /// - /// Calling this method will create a dependency on the closest - /// [SelectableRegionSelectionStatusScope] in the [context], if there is one. - static ValueListenable? maybeOf( - BuildContext context, - ) { - return context - .dependOnInheritedWidgetOfExactType< - SelectableRegionSelectionStatusScope - >() - ?.selectionStatusNotifier; - } - - @override - bool updateShouldNotify(SelectableRegionSelectionStatusScope oldWidget) { - return selectionStatusNotifier != oldWidget.selectionStatusNotifier; - } -} diff --git a/lib/common/widgets/flutter/selectable_text/selectable_text.dart b/lib/common/widgets/flutter/selectable_text/selectable_text.dart deleted file mode 100644 index 0ef7989ee..000000000 --- a/lib/common/widgets/flutter/selectable_text/selectable_text.dart +++ /dev/null @@ -1,903 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; - -import 'package:PiliPlus/common/widgets/flutter/selectable_text/text_selection.dart'; -import 'package:flutter/cupertino.dart' - hide TextSelectionGestureDetectorBuilder; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' - hide SelectableText, TextSelectionGestureDetectorBuilder; -import 'package:flutter/rendering.dart'; -import 'package:flutter/scheduler.dart'; - -class _TextSpanEditingController extends TextEditingController { - _TextSpanEditingController({required TextSpan textSpan}) - : _textSpan = textSpan, - super(text: textSpan.toPlainText(includeSemanticsLabels: false)); - - final TextSpan _textSpan; - - @override - TextSpan buildTextSpan({ - required BuildContext context, - TextStyle? style, - required bool withComposing, - }) { - // This does not care about composing. - return TextSpan(style: style, children: [_textSpan]); - } - - @override - set text(String? newText) { - // This should never be reached. - throw UnimplementedError(); - } -} - -class _SelectableTextSelectionGestureDetectorBuilder - extends CustomTextSelectionGestureDetectorBuilder { - _SelectableTextSelectionGestureDetectorBuilder({ - required _SelectableTextState state, - }) : _state = state, - super(delegate: state); - - final _SelectableTextState _state; - - @override - void onSingleTapUp(TapDragUpDetails details) { - if (!delegate.selectionEnabled) { - return; - } - super.onSingleTapUp(details); - _state.widget.onTap?.call(); - } -} - -/// A run of selectable text with a single style. -/// -/// Consider using [SelectionArea] or [SelectableRegion] instead, which enable -/// selection on a widget subtree, including but not limited to [Text] widgets. -/// -/// The [SelectableText] widget displays a string of text with a single style. -/// The string might break across multiple lines or might all be displayed on -/// the same line depending on the layout constraints. -/// -/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc} -/// -/// The [style] argument is optional. When omitted, the text will use the style -/// from the closest enclosing [DefaultTextStyle]. If the given style's -/// [TextStyle.inherit] property is true (the default), the given style will -/// be merged with the closest enclosing [DefaultTextStyle]. This merging -/// behavior is useful, for example, to make the text bold while using the -/// default font family and size. -/// -/// {@macro flutter.material.textfield.wantKeepAlive} -/// -/// {@tool snippet} -/// -/// ```dart -/// const SelectableText( -/// 'Hello! How are you?', -/// textAlign: TextAlign.center, -/// style: TextStyle(fontWeight: FontWeight.bold), -/// ) -/// ``` -/// {@end-tool} -/// -/// Using the [SelectableText.rich] constructor, the [SelectableText] widget can -/// display a paragraph with differently styled [TextSpan]s. The sample -/// that follows displays "Hello beautiful world" with different styles -/// for each word. -/// -/// {@tool snippet} -/// -/// ```dart -/// const SelectableText.rich( -/// TextSpan( -/// text: 'Hello', // default text style -/// children: [ -/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)), -/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)), -/// ], -/// ), -/// ) -/// ``` -/// {@end-tool} -/// -/// ## Interactivity -/// -/// To make [SelectableText] react to touch events, use callback [onTap] to achieve -/// the desired behavior. -/// -/// ## Scrolling Considerations -/// -/// If this [SelectableText] is not a descendant of [Scaffold] and is being used -/// within a [Scrollable] or nested [Scrollable]s, consider placing a -/// [ScrollNotificationObserver] above the root [Scrollable] that contains this -/// [SelectableText] to ensure proper scroll coordination for [SelectableText] -/// and its components like [TextSelectionOverlay]. -/// -/// See also: -/// -/// * [Text], which is the non selectable version of this widget. -/// * [TextField], which is the editable version of this widget. -/// * [SelectionArea], which enables the selection of multiple [Text] widgets -/// and of other widgets. -class SelectableText extends StatefulWidget { - /// Creates a selectable text widget. - /// - /// If the [style] argument is null, the text will use the style from the - /// closest enclosing [DefaultTextStyle]. - /// - - /// If the [showCursor], [autofocus], [dragStartBehavior], - /// [selectionHeightStyle], [selectionWidthStyle] and [data] arguments are - /// specified, the [maxLines] argument must be greater than zero. - const SelectableText( - String this.data, { - super.key, - this.focusNode, - this.style, - this.strutStyle, - this.textAlign, - this.textDirection, - @Deprecated( - 'Use textScaler instead. ' - 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' - 'This feature was deprecated after v3.12.0-2.0.pre.', - ) - this.textScaleFactor, - this.textScaler, - this.showCursor = false, - this.autofocus = false, - @Deprecated( - 'Use `contextMenuBuilder` instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', - ) - this.toolbarOptions, - this.minLines, - this.maxLines, - this.cursorWidth = 2.0, - this.cursorHeight, - this.cursorRadius, - this.cursorColor, - this.selectionColor, - this.selectionHeightStyle, - this.selectionWidthStyle, - this.dragStartBehavior = DragStartBehavior.start, - this.enableInteractiveSelection = true, - this.selectionControls, - this.onTap, - this.scrollPhysics, - this.scrollBehavior, - this.semanticsLabel, - this.textHeightBehavior, - this.textWidthBasis, - this.onSelectionChanged, - this.contextMenuBuilder = _defaultContextMenuBuilder, - this.magnifierConfiguration, - }) : assert(maxLines == null || maxLines > 0), - assert(minLines == null || minLines > 0), - assert( - (maxLines == null) || (minLines == null) || (maxLines >= minLines), - "minLines can't be greater than maxLines", - ), - assert( - textScaler == null || textScaleFactor == null, - 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', - ), - textSpan = null; - - /// Creates a selectable text widget with a [TextSpan]. - /// - /// The [TextSpan.children] attribute of the [textSpan] parameter must only - /// contain [TextSpan]s. Other types of [InlineSpan] are not allowed. - const SelectableText.rich( - TextSpan this.textSpan, { - super.key, - this.focusNode, - this.style, - this.strutStyle, - this.textAlign, - this.textDirection, - @Deprecated( - 'Use textScaler instead. ' - 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' - 'This feature was deprecated after v3.12.0-2.0.pre.', - ) - this.textScaleFactor, - this.textScaler, - this.showCursor = false, - this.autofocus = false, - @Deprecated( - 'Use `contextMenuBuilder` instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', - ) - this.toolbarOptions, - this.minLines, - this.maxLines, - this.cursorWidth = 2.0, - this.cursorHeight, - this.cursorRadius, - this.cursorColor, - this.selectionColor, - this.selectionHeightStyle, - this.selectionWidthStyle, - this.dragStartBehavior = DragStartBehavior.start, - this.enableInteractiveSelection = true, - this.selectionControls, - this.onTap, - this.scrollPhysics, - this.scrollBehavior, - this.semanticsLabel, - this.textHeightBehavior, - this.textWidthBasis, - this.onSelectionChanged, - this.contextMenuBuilder = _defaultContextMenuBuilder, - this.magnifierConfiguration, - }) : assert(maxLines == null || maxLines > 0), - assert(minLines == null || minLines > 0), - assert( - (maxLines == null) || (minLines == null) || (maxLines >= minLines), - "minLines can't be greater than maxLines", - ), - assert( - textScaler == null || textScaleFactor == null, - 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', - ), - data = null; - - /// The text to display. - /// - /// This will be null if a [textSpan] is provided instead. - final String? data; - - /// The text to display as a [TextSpan]. - /// - /// This will be null if [data] is provided instead. - final TextSpan? textSpan; - - /// Defines the focus for this widget. - /// - /// Text is only selectable when widget is focused. - /// - /// The [focusNode] is a long-lived object that's typically managed by a - /// [StatefulWidget] parent. See [FocusNode] for more information. - /// - /// To give the focus to this widget, provide a [focusNode] and then - /// use the current [FocusScope] to request the focus: - /// - /// ```dart - /// FocusScope.of(context).requestFocus(myFocusNode); - /// ``` - /// - /// This happens automatically when the widget is tapped. - /// - /// To be notified when the widget gains or loses the focus, add a listener - /// to the [focusNode]: - /// - /// ```dart - /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); }); - /// ``` - /// - /// If null, this widget will create its own [FocusNode] with - /// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget - /// to be skipped over during focus traversal. - final FocusNode? focusNode; - - /// The style to use for the text. - /// - /// If null, defaults [DefaultTextStyle] of context. - final TextStyle? style; - - /// {@macro flutter.widgets.editableText.strutStyle} - final StrutStyle? strutStyle; - - /// {@macro flutter.widgets.editableText.textAlign} - final TextAlign? textAlign; - - /// {@macro flutter.widgets.editableText.textDirection} - final TextDirection? textDirection; - - /// {@macro flutter.widgets.editableText.textScaleFactor} - @Deprecated( - 'Use textScaler instead. ' - 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' - 'This feature was deprecated after v3.12.0-2.0.pre.', - ) - final double? textScaleFactor; - - /// {@macro flutter.painting.textPainter.textScaler} - final TextScaler? textScaler; - - /// {@macro flutter.widgets.editableText.autofocus} - final bool autofocus; - - /// {@macro flutter.widgets.editableText.minLines} - final int? minLines; - - /// {@macro flutter.widgets.editableText.maxLines} - final int? maxLines; - - /// {@macro flutter.widgets.editableText.showCursor} - final bool showCursor; - - /// {@macro flutter.widgets.editableText.cursorWidth} - final double cursorWidth; - - /// {@macro flutter.widgets.editableText.cursorHeight} - final double? cursorHeight; - - /// {@macro flutter.widgets.editableText.cursorRadius} - final Radius? cursorRadius; - - /// The color of the cursor. - /// - /// The cursor indicates the current text insertion point. - /// - /// If null then [DefaultSelectionStyle.cursorColor] is used. If that is also - /// null and [ThemeData.platform] is [TargetPlatform.iOS] or - /// [TargetPlatform.macOS], then [CupertinoThemeData.primaryColor] is used. - /// Otherwise [ColorScheme.primary] of [ThemeData.colorScheme] is used. - final Color? cursorColor; - - /// The color to use when painting the selection. - /// - /// If this property is null, this widget gets the selection color from the - /// inherited [DefaultSelectionStyle] (if any); if none, the selection - /// color is derived from the [CupertinoThemeData.primaryColor] on - /// Apple platforms and [ColorScheme.primary] of [ThemeData.colorScheme] on - /// other platforms. - final Color? selectionColor; - - /// Controls how tall the selection highlight boxes are computed to be. - /// - /// See [ui.BoxHeightStyle] for details on available styles. - final ui.BoxHeightStyle? selectionHeightStyle; - - /// Controls how wide the selection highlight boxes are computed to be. - /// - /// See [ui.BoxWidthStyle] for details on available styles. - final ui.BoxWidthStyle? selectionWidthStyle; - - /// {@macro flutter.widgets.editableText.enableInteractiveSelection} - final bool enableInteractiveSelection; - - /// {@macro flutter.widgets.editableText.selectionControls} - final TextSelectionControls? selectionControls; - - /// {@macro flutter.widgets.scrollable.dragStartBehavior} - final DragStartBehavior dragStartBehavior; - - /// Configuration of toolbar options. - /// - /// Paste and cut will be disabled regardless. - /// - /// If not set, select all and copy will be enabled by default. - @Deprecated( - 'Use `contextMenuBuilder` instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', - ) - final ToolbarOptions? toolbarOptions; - - /// {@macro flutter.widgets.editableText.selectionEnabled} - bool get selectionEnabled => enableInteractiveSelection; - - /// Called when the user taps on this selectable text. - /// - /// The selectable text builds a [GestureDetector] to handle input events like tap, - /// to trigger focus requests, to move the caret, adjust the selection, etc. - /// Handling some of those events by wrapping the selectable text with a competing - /// GestureDetector is problematic. - /// - /// To unconditionally handle taps, without interfering with the selectable text's - /// internal gesture detector, provide this callback. - /// - /// To be notified when the text field gains or loses the focus, provide a - /// [focusNode] and add a listener to that. - /// - /// To listen to arbitrary pointer events without competing with the - /// selectable text's internal gesture detector, use a [Listener]. - final GestureTapCallback? onTap; - - /// {@macro flutter.widgets.editableText.scrollPhysics} - final ScrollPhysics? scrollPhysics; - - /// {@macro flutter.widgets.editableText.scrollBehavior} - final ScrollBehavior? scrollBehavior; - - /// {@macro flutter.widgets.Text.semanticsLabel} - final String? semanticsLabel; - - /// {@macro dart.ui.textHeightBehavior} - final TextHeightBehavior? textHeightBehavior; - - /// {@macro flutter.painting.textPainter.textWidthBasis} - final TextWidthBasis? textWidthBasis; - - /// {@macro flutter.widgets.editableText.onSelectionChanged} - final SelectionChangedCallback? onSelectionChanged; - - /// {@macro flutter.widgets.EditableText.contextMenuBuilder} - final EditableTextContextMenuBuilder? contextMenuBuilder; - - static Widget _defaultContextMenuBuilder( - BuildContext context, - EditableTextState editableTextState, - ) { - return AdaptiveTextSelectionToolbar.editableText( - editableTextState: editableTextState, - ); - } - - /// The configuration for the magnifier used when the text is selected. - /// - /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] - /// on Android, and builds nothing on all other platforms. To suppress the - /// magnifier, consider passing [TextMagnifierConfiguration.disabled]. - /// - /// {@macro flutter.widgets.magnifier.intro} - final TextMagnifierConfiguration? magnifierConfiguration; - - @override - State createState() => _SelectableTextState(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add( - DiagnosticsProperty('data', data, defaultValue: null), - ) - ..add( - DiagnosticsProperty( - 'semanticsLabel', - semanticsLabel, - defaultValue: null, - ), - ) - ..add( - DiagnosticsProperty( - 'focusNode', - focusNode, - defaultValue: null, - ), - ) - ..add( - DiagnosticsProperty('style', style, defaultValue: null), - ) - ..add( - DiagnosticsProperty('autofocus', autofocus, defaultValue: false), - ) - ..add( - DiagnosticsProperty( - 'showCursor', - showCursor, - defaultValue: false, - ), - ) - ..add(IntProperty('minLines', minLines, defaultValue: null)) - ..add(IntProperty('maxLines', maxLines, defaultValue: null)) - ..add( - EnumProperty('textAlign', textAlign, defaultValue: null), - ) - ..add( - EnumProperty( - 'textDirection', - textDirection, - defaultValue: null, - ), - ) - ..add( - DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null), - ) - ..add( - DiagnosticsProperty( - 'textScaler', - textScaler, - defaultValue: null, - ), - ) - ..add( - DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0), - ) - ..add( - DoubleProperty('cursorHeight', cursorHeight, defaultValue: null), - ) - ..add( - DiagnosticsProperty( - 'cursorRadius', - cursorRadius, - defaultValue: null, - ), - ) - ..add( - DiagnosticsProperty( - 'cursorColor', - cursorColor, - defaultValue: null, - ), - ) - ..add( - DiagnosticsProperty( - 'selectionColor', - selectionColor, - defaultValue: null, - ), - ) - ..add( - FlagProperty( - 'selectionEnabled', - value: selectionEnabled, - defaultValue: true, - ifFalse: 'selection disabled', - ), - ) - ..add( - DiagnosticsProperty( - 'selectionControls', - selectionControls, - defaultValue: null, - ), - ) - ..add( - DiagnosticsProperty( - 'scrollPhysics', - scrollPhysics, - defaultValue: null, - ), - ) - ..add( - DiagnosticsProperty( - 'scrollBehavior', - scrollBehavior, - defaultValue: null, - ), - ) - ..add( - DiagnosticsProperty( - 'textHeightBehavior', - textHeightBehavior, - defaultValue: null, - ), - ); - } -} - -class _SelectableTextState extends State - implements TextSelectionGestureDetectorBuilderDelegate { - EditableTextState? get _editableText => editableTextKey.currentState; - - late _TextSpanEditingController _controller; - - FocusNode? _focusNode; - FocusNode get _effectiveFocusNode => - widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true)); - - bool _showSelectionHandles = false; - - late _SelectableTextSelectionGestureDetectorBuilder - _selectionGestureDetectorBuilder; - - // API for TextSelectionGestureDetectorBuilderDelegate. - @override - late bool forcePressEnabled; - - @override - final GlobalKey editableTextKey = - GlobalKey(); - - @override - bool get selectionEnabled => widget.selectionEnabled; - // End of API for TextSelectionGestureDetectorBuilderDelegate. - - @override - void initState() { - super.initState(); - _selectionGestureDetectorBuilder = - _SelectableTextSelectionGestureDetectorBuilder(state: this); - _controller = _TextSpanEditingController( - textSpan: widget.textSpan ?? TextSpan(text: widget.data), - ); - _controller.addListener(_onControllerChanged); - _effectiveFocusNode.addListener(_handleFocusChanged); - } - - @override - void didUpdateWidget(SelectableText oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.data != oldWidget.data || - widget.textSpan != oldWidget.textSpan) { - _controller - ..removeListener(_onControllerChanged) - ..dispose(); - _controller = _TextSpanEditingController( - textSpan: widget.textSpan ?? TextSpan(text: widget.data), - ); - _controller.addListener(_onControllerChanged); - } - if (widget.focusNode != oldWidget.focusNode) { - (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); - (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged); - } - if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) { - _showSelectionHandles = false; - } else { - _showSelectionHandles = true; - } - } - - @override - void dispose() { - _effectiveFocusNode.removeListener(_handleFocusChanged); - _focusNode?.dispose(); - _controller.dispose(); - super.dispose(); - } - - void _onControllerChanged() { - final bool showSelectionHandles = - !_effectiveFocusNode.hasFocus || !_controller.selection.isCollapsed; - if (showSelectionHandles == _showSelectionHandles) { - return; - } - setState(() { - _showSelectionHandles = showSelectionHandles; - }); - } - - void _handleFocusChanged() { - if (!_effectiveFocusNode.hasFocus && - SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) { - // We should only clear the selection when this SelectableText loses - // focus while the application is currently running. It is possible - // that the application is not currently running, for example on desktop - // platforms, clicking on a different window switches the focus to - // the new window causing the Flutter application to go inactive. In this - // case we want to retain the selection so it remains when we return to - // the Flutter application. - _controller.value = TextEditingValue(text: _controller.value.text); - } - } - - void _handleSelectionChanged( - TextSelection selection, - SelectionChangedCause? cause, - ) { - final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); - if (willShowSelectionHandles != _showSelectionHandles) { - setState(() { - _showSelectionHandles = willShowSelectionHandles; - }); - } - - widget.onSelectionChanged?.call(selection, cause); - - switch (Theme.of(context).platform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - if (cause == SelectionChangedCause.longPress) { - _editableText?.bringIntoView(selection.base); - } - return; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - // Do nothing. - } - } - - /// Toggle the toolbar when a selection handle is tapped. - void _handleSelectionHandleTapped() { - if (_controller.selection.isCollapsed) { - _editableText!.toggleToolbar(); - } - } - - 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 (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) { - return false; - } - - if (_controller.selection.isCollapsed) { - return false; - } - - if (cause == SelectionChangedCause.keyboard) { - return false; - } - - if (cause == SelectionChangedCause.longPress) { - return true; - } - - if (_controller.text.isNotEmpty) { - return true; - } - - return false; - } - - @override - Widget build(BuildContext context) { - // TODO(garyq): Assert to block WidgetSpans from being used here are removed, - // but we still do not yet have nice handling of things like carets, clipboard, - // and other features. We should add proper support. Currently, caret handling - // is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010 - // should be landed in SkParagraph after the switch is complete. - assert(debugCheckHasMediaQuery(context)); - assert(debugCheckHasDirectionality(context)); - assert( - !(widget.style != null && - !widget.style!.inherit && - (widget.style!.fontSize == null || - widget.style!.textBaseline == null)), - 'inherit false style must supply fontSize and textBaseline', - ); - - final ThemeData theme = Theme.of(context); - final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of( - context, - ); - final FocusNode focusNode = _effectiveFocusNode; - - TextSelectionControls? textSelectionControls = widget.selectionControls; - final bool paintCursorAboveText; - final bool cursorOpacityAnimates; - Offset? cursorOffset; - final Color cursorColor; - final Color selectionColor; - Radius? cursorRadius = widget.cursorRadius; - - switch (theme.platform) { - case TargetPlatform.iOS: - final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); - forcePressEnabled = true; - textSelectionControls ??= cupertinoTextSelectionHandleControls; - paintCursorAboveText = true; - cursorOpacityAnimates = true; - cursorColor = - widget.cursorColor ?? - selectionStyle.cursorColor ?? - cupertinoTheme.primaryColor; - selectionColor = - selectionStyle.selectionColor ?? - cupertinoTheme.primaryColor.withValues(alpha: 0.40); - cursorRadius ??= const Radius.circular(2.0); - cursorOffset = Offset( - iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), - 0, - ); - - case TargetPlatform.macOS: - final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); - forcePressEnabled = false; - textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls; - paintCursorAboveText = true; - cursorOpacityAnimates = true; - cursorColor = - widget.cursorColor ?? - selectionStyle.cursorColor ?? - cupertinoTheme.primaryColor; - selectionColor = - selectionStyle.selectionColor ?? - cupertinoTheme.primaryColor.withValues(alpha: 0.40); - cursorRadius ??= const Radius.circular(2.0); - cursorOffset = Offset( - iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), - 0, - ); - - case TargetPlatform.android: - case TargetPlatform.fuchsia: - forcePressEnabled = false; - textSelectionControls ??= materialTextSelectionHandleControls; - paintCursorAboveText = false; - cursorOpacityAnimates = false; - cursorColor = - widget.cursorColor ?? - selectionStyle.cursorColor ?? - theme.colorScheme.primary; - selectionColor = - selectionStyle.selectionColor ?? - theme.colorScheme.primary.withValues(alpha: 0.40); - - case TargetPlatform.linux: - case TargetPlatform.windows: - forcePressEnabled = false; - textSelectionControls ??= desktopTextSelectionHandleControls; - paintCursorAboveText = false; - cursorOpacityAnimates = false; - cursorColor = - widget.cursorColor ?? - selectionStyle.cursorColor ?? - theme.colorScheme.primary; - selectionColor = - selectionStyle.selectionColor ?? - theme.colorScheme.primary.withValues(alpha: 0.40); - } - - final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); - TextStyle? effectiveTextStyle = widget.style; - if (effectiveTextStyle == null || effectiveTextStyle.inherit) { - effectiveTextStyle = defaultTextStyle.style.merge( - widget.style ?? _controller._textSpan.style, - ); - } - final TextScaler? effectiveScaler = - widget.textScaler ?? - switch (widget.textScaleFactor) { - null => null, - final double textScaleFactor => TextScaler.linear(textScaleFactor), - }; - final Widget child = RepaintBoundary( - child: EditableText( - key: editableTextKey, - style: effectiveTextStyle, - readOnly: true, - toolbarOptions: widget.toolbarOptions, - textWidthBasis: - widget.textWidthBasis ?? defaultTextStyle.textWidthBasis, - textHeightBehavior: - widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior, - showSelectionHandles: _showSelectionHandles, - showCursor: widget.showCursor, - controller: _controller, - focusNode: focusNode, - strutStyle: widget.strutStyle ?? const StrutStyle(), - textAlign: - widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, - textDirection: widget.textDirection, - textScaler: effectiveScaler, - autofocus: widget.autofocus, - forceLine: false, - minLines: widget.minLines, - maxLines: widget.maxLines ?? defaultTextStyle.maxLines, - selectionColor: widget.selectionColor ?? selectionColor, - selectionControls: widget.selectionEnabled - ? textSelectionControls - : null, - onSelectionChanged: _handleSelectionChanged, - onSelectionHandleTapped: _handleSelectionHandleTapped, - rendererIgnoresPointer: true, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - cursorRadius: cursorRadius, - cursorColor: cursorColor, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - cursorOpacityAnimates: cursorOpacityAnimates, - cursorOffset: cursorOffset, - paintCursorAboveText: paintCursorAboveText, - backgroundCursorColor: CupertinoColors.inactiveGray, - enableInteractiveSelection: widget.enableInteractiveSelection, - magnifierConfiguration: - widget.magnifierConfiguration ?? - TextMagnifier.adaptiveMagnifierConfiguration, - dragStartBehavior: widget.dragStartBehavior, - scrollPhysics: widget.scrollPhysics, - scrollBehavior: widget.scrollBehavior, - autofillHints: null, - contextMenuBuilder: widget.contextMenuBuilder, - ), - ); - - return Semantics( - label: widget.semanticsLabel, - excludeSemantics: widget.semanticsLabel != null, - onLongPress: () { - _effectiveFocusNode.requestFocus(); - }, - child: _selectionGestureDetectorBuilder.buildGestureDetector( - behavior: HitTestBehavior.translucent, - child: child, - ), - ); - } -} diff --git a/lib/common/widgets/flutter/selectable_text/selection_area.dart b/lib/common/widgets/flutter/selectable_text/selection_area.dart deleted file mode 100644 index d001462c8..000000000 --- a/lib/common/widgets/flutter/selectable_text/selection_area.dart +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_region.dart'; -import 'package:flutter/cupertino.dart' - hide - SelectableRegion, - SelectableRegionState, - SelectableRegionContextMenuBuilder; -import 'package:flutter/material.dart' - hide - SelectionArea, - SelectableRegion, - SelectableRegionState, - SelectableRegionContextMenuBuilder; -import 'package:flutter/rendering.dart'; - -/// A widget that introduces an area for user selections with adaptive selection -/// controls. -/// -/// This widget creates a [SelectableRegion] with platform-adaptive selection -/// controls. -/// -/// Flutter widgets are not selectable by default. To enable selection for -/// a specific screen, consider wrapping the body of the [Route] with a -/// [SelectionArea]. -/// -/// The [SelectionArea] widget must have a [Localizations] ancestor that -/// contains a [MaterialLocalizations] delegate; using the [MaterialApp] widget -/// ensures that such an ancestor is present. -/// -/// {@tool dartpad} -/// This example shows how to make a screen selectable. -/// -/// ** See code in examples/api/lib/material/selection_area/selection_area.0.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [SelectableRegion], which provides an overview of the selection system. -/// * [SelectableText], which enables selection on a single run of text. -/// * [SelectionListener], which enables accessing the [SelectionDetails] of -/// the selectable subtree it wraps. -class SelectionArea extends StatefulWidget { - /// Creates a [SelectionArea]. - /// - /// If [selectionControls] is null, a platform specific one is used. - const SelectionArea({ - super.key, - this.focusNode, - this.selectionControls, - this.contextMenuBuilder = _defaultContextMenuBuilder, - this.magnifierConfiguration, - this.onSelectionChanged, - required this.child, - }); - - /// The configuration for the magnifier in the selection region. - /// - /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] - /// on Android, and builds nothing on all other platforms. To suppress the - /// magnifier, consider passing [TextMagnifierConfiguration.disabled]. - /// - /// {@macro flutter.widgets.magnifier.intro} - final TextMagnifierConfiguration? magnifierConfiguration; - - /// {@macro flutter.widgets.Focus.focusNode} - final FocusNode? focusNode; - - /// The delegate to build the selection handles and toolbar. - /// - /// If it is null, the platform specific selection control is used. - final TextSelectionControls? selectionControls; - - /// {@macro flutter.widgets.EditableText.contextMenuBuilder} - /// - /// If not provided, will build a default menu based on the ambient - /// [ThemeData.platform]. - /// - /// {@tool dartpad} - /// This example shows how to build a custom context menu for any selected - /// content in a SelectionArea. - /// - /// ** See code in examples/api/lib/material/context_menu/selectable_region_toolbar_builder.0.dart ** - /// {@end-tool} - /// - /// See also: - /// - /// * [AdaptiveTextSelectionToolbar], which is built by default. - final SelectableRegionContextMenuBuilder? contextMenuBuilder; - - /// Called when the selected content changes. - final ValueChanged? onSelectionChanged; - - /// The child widget this selection area applies to. - /// - /// {@macro flutter.widgets.ProxyWidget.child} - final Widget child; - - static Widget _defaultContextMenuBuilder( - BuildContext context, - SelectableRegionState selectableRegionState, - ) => AdaptiveTextSelectionToolbar.buttonItems( - buttonItems: selectableRegionState.contextMenuButtonItems, - anchors: selectableRegionState.contextMenuAnchors, - ); - - @override - State createState() => SelectionAreaState(); -} - -/// State for a [SelectionArea]. -class SelectionAreaState extends State { - final GlobalKey _selectableRegionKey = - GlobalKey(); - - /// The [State] of the [SelectableRegion] for which this [SelectionArea] wraps. - SelectableRegionState get selectableRegion => - _selectableRegionKey.currentState!; - - @protected - @override - Widget build(BuildContext context) { - assert(debugCheckHasMaterialLocalizations(context)); - final TextSelectionControls controls = - widget.selectionControls ?? - switch (Theme.of(context).platform) { - TargetPlatform.android || - TargetPlatform.fuchsia => materialTextSelectionHandleControls, - TargetPlatform.linux || - TargetPlatform.windows => desktopTextSelectionHandleControls, - TargetPlatform.iOS => cupertinoTextSelectionHandleControls, - TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls, - }; - return SelectableRegion( - key: _selectableRegionKey, - selectionControls: controls, - focusNode: widget.focusNode, - contextMenuBuilder: widget.contextMenuBuilder, - magnifierConfiguration: - widget.magnifierConfiguration ?? - TextMagnifier.adaptiveMagnifierConfiguration, - onSelectionChanged: widget.onSelectionChanged, - child: widget.child, - ); - } -} diff --git a/lib/common/widgets/flutter/selectable_text/tap_and_drag.dart b/lib/common/widgets/flutter/selectable_text/tap_and_drag.dart deleted file mode 100644 index 7884f7469..000000000 --- a/lib/common/widgets/flutter/selectable_text/tap_and_drag.dart +++ /dev/null @@ -1,1125 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart' - hide TapAndHorizontalDragGestureRecognizer; - -// Examples can assume: -// void setState(VoidCallback fn) { } -// late String _last; - -double _getGlobalDistance(PointerEvent event, OffsetPair? originPosition) { - assert(originPosition != null); - final Offset offset = event.position - originPosition!.global; - return offset.distance; -} - -// The possible states of a [BaseTapAndDragGestureRecognizer]. -// -// The recognizer advances from [ready] to [possible] when it starts tracking -// a pointer in [BaseTapAndDragGestureRecognizer.addAllowedPointer]. Where it advances -// from there depends on the sequence of pointer events that is tracked by the -// recognizer, following the initial [PointerDownEvent]: -// -// * If a [PointerUpEvent] has not been tracked, the recognizer stays in the [possible] -// state as long as it continues to track a pointer. -// * If a [PointerMoveEvent] is tracked that has moved a sufficient global distance -// from the initial [PointerDownEvent] and it came before a [PointerUpEvent], then -// this recognizer moves from the [possible] state to [accepted]. -// * If a [PointerUpEvent] is tracked before the pointer has moved a sufficient global -// distance to be considered a drag, then this recognizer moves from the [possible] -// state to [ready]. -// * If a [PointerCancelEvent] is tracked then this recognizer moves from its current -// state to [ready]. -// -// Once the recognizer has stopped tracking any remaining pointers, the recognizer -// returns to the [ready] state. -enum _DragState { - // The recognizer is ready to start recognizing a drag. - ready, - - // The sequence of pointer events seen thus far is consistent with a drag but - // it has not been accepted definitively. - possible, - - // The sequence of pointer events has been accepted definitively as a drag. - accepted, -} - -// A mixin for [OneSequenceGestureRecognizer] that tracks the number of taps -// that occur in a series of [PointerEvent]s and the most recent set of -// [LogicalKeyboardKey]s pressed on the most recent tap down. -// -// A tap is tracked as part of a series of taps if: -// -// 1. The elapsed time between when a [PointerUpEvent] and the subsequent -// [PointerDownEvent] does not exceed [kDoubleTapTimeout]. -// 2. The delta between the position tapped in the global coordinate system -// and the position that was tapped previously must be less than or equal -// to [kDoubleTapSlop]. -// -// This mixin's state, i.e. the series of taps being tracked is reset when -// a tap is tracked that does not meet any of the specifications stated above. -mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { - // Public state available to [OneSequenceGestureRecognizer]. - - // The [PointerDownEvent] that was most recently tracked in [addAllowedPointer]. - // - // This value will be null if a [PointerDownEvent] has not been tracked yet in - // [addAllowedPointer] or the timer between two taps has elapsed. - // - // This value is only reset when the timer between a [PointerUpEvent] and the - // [PointerDownEvent] times out or when a new [PointerDownEvent] is tracked in - // [addAllowedPointer]. - PointerDownEvent? get currentDown => _down; - - // The [PointerUpEvent] that was most recently tracked in [handleEvent]. - // - // This value will be null if a [PointerUpEvent] has not been tracked yet in - // [handleEvent] or the timer between two taps has elapsed. - // - // This value is only reset when the timer between a [PointerUpEvent] and the - // [PointerDownEvent] times out or when a new [PointerDownEvent] is tracked in - // [addAllowedPointer]. - PointerUpEvent? get currentUp => _up; - - // The number of consecutive taps that the most recently tracked [PointerDownEvent] - // in [currentDown] represents. - // - // This value defaults to zero, meaning a tap series is not currently being tracked. - // - // When this value is greater than zero it means [addAllowedPointer] has run - // and at least one [PointerDownEvent] belongs to the current series of taps - // being tracked. - // - // [addAllowedPointer] will either increment this value by `1` or set the value to `1` - // depending if the new [PointerDownEvent] is determined to be in the same series as the - // tap that preceded it. If too much time has elapsed between two taps, the recognizer has lost - // in the arena, the gesture has been cancelled, or the recognizer is being disposed then - // this value will be set to `0`, and a new series will begin. - int get consecutiveTapCount => _consecutiveTapCount; - - // The upper limit for the [consecutiveTapCount]. When this limit is reached - // all tap related state is reset and a new tap series is tracked. - // - // If this value is null, [consecutiveTapCount] can grow infinitely large. - int? get maxConsecutiveTap; - - // Private tap state tracked. - PointerDownEvent? _down; - PointerUpEvent? _up; - int _consecutiveTapCount = 0; - - OffsetPair? _originPosition; - int? _previousButtons; - - // For timing taps. - Timer? _consecutiveTapTimer; - Offset? _lastTapOffset; - - /// {@macro flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackStart} - VoidCallback? onTapTrackStart; - - /// {@macro flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackReset} - VoidCallback? onTapTrackReset; - - // When tracking a tap, the [consecutiveTapCount] is incremented if the given tap - // falls under the tolerance specifications and reset to 1 if not. - @override - void addAllowedPointer(PointerDownEvent event) { - super.addAllowedPointer(event); - if (_consecutiveTapTimer != null && !_consecutiveTapTimer!.isActive) { - _tapTrackerReset(); - } - if (maxConsecutiveTap == _consecutiveTapCount) { - _tapTrackerReset(); - } - _up = null; - if (_down != null && !_representsSameSeries(event)) { - // The given tap does not match the specifications of the series of taps being tracked, - // reset the tap count and related state. - _consecutiveTapCount = 1; - } else { - _consecutiveTapCount += 1; - } - _consecutiveTapTimerStop(); - // `_down` must be assigned in this method instead of [handleEvent], - // because [acceptGesture] might be called before [handleEvent], - // which may rely on `_down` to initiate a callback. - _trackTap(event); - } - - @override - void handleEvent(PointerEvent event) { - if (event is PointerMoveEvent) { - final double computedSlop = computeHitSlop(event.kind, gestureSettings); - final bool isSlopPastTolerance = - _getGlobalDistance(event, _originPosition) > computedSlop; - - if (isSlopPastTolerance) { - _consecutiveTapTimerStop(); - _previousButtons = null; - _lastTapOffset = null; - } - } else if (event is PointerUpEvent) { - _up = event; - if (_down != null) { - _consecutiveTapTimerStop(); - _consecutiveTapTimerStart(); - } - } else if (event is PointerCancelEvent) { - _tapTrackerReset(); - } - } - - @override - void rejectGesture(int pointer) { - _tapTrackerReset(); - } - - @override - void dispose() { - _tapTrackerReset(); - super.dispose(); - } - - void _trackTap(PointerDownEvent event) { - _down = event; - _previousButtons = event.buttons; - _lastTapOffset = event.position; - _originPosition = OffsetPair( - local: event.localPosition, - global: event.position, - ); - onTapTrackStart?.call(); - } - - bool _hasSameButton(int buttons) { - assert(_previousButtons != null); - if (buttons == _previousButtons!) { - return true; - } else { - return false; - } - } - - bool _isWithinConsecutiveTapTolerance(Offset secondTapOffset) { - if (_lastTapOffset == null) { - return false; - } - - final Offset difference = secondTapOffset - _lastTapOffset!; - return difference.distance <= kDoubleTapSlop; - } - - bool _representsSameSeries(PointerDownEvent event) { - return _consecutiveTapTimer != null && - _isWithinConsecutiveTapTolerance(event.position) && - _hasSameButton(event.buttons); - } - - void _consecutiveTapTimerStart() { - _consecutiveTapTimer ??= Timer( - kDoubleTapTimeout, - _consecutiveTapTimerTimeout, - ); - } - - void _consecutiveTapTimerStop() { - if (_consecutiveTapTimer != null) { - _consecutiveTapTimer!.cancel(); - _consecutiveTapTimer = null; - } - } - - void _consecutiveTapTimerTimeout() { - // The consecutive tap timer may time out before a tap down/tap up event is - // fired. In this case we should not reset the tap tracker state immediately. - // Instead we should reset the tap tracker on the next call to [addAllowedPointer], - // if the timer is no longer active. - } - - void _tapTrackerReset() { - // The timer has timed out, i.e. the time between a [PointerUpEvent] and the subsequent - // [PointerDownEvent] exceeded the duration of [kDoubleTapTimeout], so the tap belonging - // to the [PointerDownEvent] cannot be considered part of the same tap series as the - // previous [PointerUpEvent]. - _consecutiveTapTimerStop(); - _previousButtons = null; - _originPosition = null; - _lastTapOffset = null; - _consecutiveTapCount = 0; - _down = null; - _up = null; - onTapTrackReset?.call(); - } -} - -/// A base class for gesture recognizers that recognize taps and movements. -/// -/// Takes on the responsibilities of [TapGestureRecognizer] and -/// [DragGestureRecognizer] in one [GestureRecognizer]. -/// -/// ### Gesture arena behavior -/// -/// [BaseTapAndDragGestureRecognizer] competes on the pointer events of -/// [kPrimaryButton] only when it has at least one non-null `onTap*` -/// or `onDrag*` callback. -/// -/// It will declare defeat if it determines that a gesture is not a -/// tap (e.g. if the pointer is dragged too far while it's contacting the -/// screen) or a drag (e.g. if the pointer was not dragged far enough to -/// be considered a drag. -/// -/// This recognizer will not immediately declare victory for every tap that it -/// recognizes, but it declares victory for every drag. -/// -/// The recognizer will declare victory when all other recognizer's in -/// the arena have lost, if the timer of [kPressTimeout] elapses and a tap -/// series greater than 1 is being tracked, or until the pointer has moved -/// a sufficient global distance from the origin to be considered a drag. -/// -/// If this recognizer loses the arena (either by declaring defeat or by -/// another recognizer declaring victory) while the pointer is contacting the -/// screen, it will fire [onCancel] instead of [onTapUp] or [onDragEnd]. -/// -/// ### When competing with `TapGestureRecognizer` and `DragGestureRecognizer` -/// -/// Similar to [TapGestureRecognizer] and [DragGestureRecognizer], -/// [BaseTapAndDragGestureRecognizer] will not aggressively declare victory when -/// it detects a tap, so when it is competing with those gesture recognizers and -/// others it has a chance of losing. Similarly, when `eagerVictoryOnDrag` is set -/// to `false`, this recognizer will not aggressively declare victory when it -/// detects a drag. By default, `eagerVictoryOnDrag` is set to `true`, so this -/// recognizer will aggressively declare victory when it detects a drag. -/// -/// When competing against [TapGestureRecognizer], if the pointer does not move past the tap -/// tolerance, then the recognizer that entered the arena first will win. In this case the -/// gesture detected is a tap. If the pointer does travel past the tap tolerance then this -/// recognizer will be declared winner by default. The gesture detected in this case is a drag. -/// -/// When competing against [DragGestureRecognizer], if the pointer does not move a sufficient -/// global distance to be considered a drag, the recognizers will tie in the arena. If the -/// pointer does travel enough distance then the recognizer that entered the arena -/// first will win. The gesture detected in this case is a drag. -/// -/// {@tool dartpad} -/// This example shows how to use the [TapAndPanGestureRecognizer] along with a -/// [RawGestureDetector] to scale a Widget. -/// -/// ** See code in examples/api/lib/gestures/tap_and_drag/tap_and_drag.0.dart ** -/// {@end-tool} -/// -/// {@tool snippet} -/// -/// This example shows how to hook up [TapAndPanGestureRecognizer]s' to nested -/// [RawGestureDetector]s'. It assumes that the code is being used inside a [State] -/// object with a `_last` field that is then displayed as the child of the gesture detector. -/// -/// In this example, if the pointer has moved past the drag threshold, then the -/// the first [TapAndPanGestureRecognizer] instance to receive the [PointerEvent] -/// will win the arena because the recognizer will immediately declare victory. -/// -/// The first one to receive the event in the example will depend on where on both -/// containers the pointer lands first. If your pointer begins in the overlapping -/// area of both containers, then the inner-most widget will receive the event first. -/// If your pointer begins in the yellow container then it will be the first to -/// receive the event. -/// -/// If the pointer has not moved past the drag threshold, then the first recognizer -/// to enter the arena will win (i.e. they both tie and the gesture arena will call -/// [GestureArenaManager.sweep] so the first member of the arena will win). -/// -/// ```dart -/// RawGestureDetector( -/// gestures: { -/// TapAndPanGestureRecognizer: GestureRecognizerFactoryWithHandlers( -/// () => TapAndPanGestureRecognizer(), -/// (TapAndPanGestureRecognizer instance) { -/// instance -/// ..onTapDown = (TapDragDownDetails details) { setState(() { _last = 'down_a'; }); } -/// ..onDragStart = (TapDragStartDetails details) { setState(() { _last = 'drag_start_a'; }); } -/// ..onDragUpdate = (TapDragUpdateDetails details) { setState(() { _last = 'drag_update_a'; }); } -/// ..onDragEnd = (TapDragEndDetails details) { setState(() { _last = 'drag_end_a'; }); } -/// ..onTapUp = (TapDragUpDetails details) { setState(() { _last = 'up_a'; }); } -/// ..onCancel = () { setState(() { _last = 'cancel_a'; }); }; -/// }, -/// ), -/// }, -/// child: Container( -/// width: 300.0, -/// height: 300.0, -/// color: Colors.yellow, -/// alignment: Alignment.center, -/// child: RawGestureDetector( -/// gestures: { -/// TapAndPanGestureRecognizer: GestureRecognizerFactoryWithHandlers( -/// () => TapAndPanGestureRecognizer(), -/// (TapAndPanGestureRecognizer instance) { -/// instance -/// ..onTapDown = (TapDragDownDetails details) { setState(() { _last = 'down_b'; }); } -/// ..onDragStart = (TapDragStartDetails details) { setState(() { _last = 'drag_start_b'; }); } -/// ..onDragUpdate = (TapDragUpdateDetails details) { setState(() { _last = 'drag_update_b'; }); } -/// ..onDragEnd = (TapDragEndDetails details) { setState(() { _last = 'drag_end_b'; }); } -/// ..onTapUp = (TapDragUpDetails details) { setState(() { _last = 'up_b'; }); } -/// ..onCancel = () { setState(() { _last = 'cancel_b'; }); }; -/// }, -/// ), -/// }, -/// child: Container( -/// width: 150.0, -/// height: 150.0, -/// color: Colors.blue, -/// child: Text(_last), -/// ), -/// ), -/// ), -/// ) -/// ``` -/// {@end-tool} -sealed class BaseTapAndDragGestureRecognizer - extends OneSequenceGestureRecognizer - with _TapStatusTrackerMixin { - /// Creates a tap and drag gesture recognizer. - /// - /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} - BaseTapAndDragGestureRecognizer({ - super.debugOwner, - super.supportedDevices, - super.allowedButtonsFilter, - this.eagerVictoryOnDrag = true, - }) : _deadline = kPressTimeout, - dragStartBehavior = DragStartBehavior.start; - - /// Configure the behavior of offsets passed to [onDragStart]. - /// - /// If set to [DragStartBehavior.start], the [onDragStart] callback will be called - /// with the position of the pointer at the time this gesture recognizer won - /// the arena. If [DragStartBehavior.down], [onDragStart] will be called with - /// the position of the first detected down event for the pointer. When there - /// are no other gestures competing with this gesture in the arena, there's - /// no difference in behavior between the two settings. - /// - /// For more information about the gesture arena: - /// https://flutter.dev/to/gesture-disambiguation - /// - /// By default, the drag start behavior is [DragStartBehavior.start]. - /// - /// See also: - /// - /// * [DragGestureRecognizer.dragStartBehavior], which includes more details and an example. - DragStartBehavior dragStartBehavior; - - /// The frequency at which the [onDragUpdate] callback is called. - /// - /// The value defaults to null, meaning there is no delay for [onDragUpdate] callback. - Duration? dragUpdateThrottleFrequency; - - /// An upper bound for the amount of taps that can belong to one tap series. - /// - /// When this limit is reached the series of taps being tracked by this - /// recognizer will be reset. - @override - int? maxConsecutiveTap; - - /// Whether this recognizer eagerly declares victory when it has detected - /// a drag. - /// - /// When this value is `false`, this recognizer will wait until it is the last - /// recognizer in the gesture arena before declaring victory on a drag. - /// - /// Defaults to `true`. - bool eagerVictoryOnDrag; - - /// {@macro flutter.gestures.tap.TapGestureRecognizer.onTapDown} - /// - /// This triggers after the down event, once a short timeout ([kPressTimeout]) has - /// elapsed, or once the gestures has won the arena, whichever comes first. - /// - /// The position of the pointer is provided in the callback's `details` - /// argument, which is a [TapDragDownDetails] object. - /// - /// {@template flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData} - /// The number of consecutive taps, and the keys that were pressed on tap down - /// are also provided in the callback's `details` argument. - /// {@endtemplate} - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - /// * [TapDragDownDetails], which is passed as an argument to this callback. - GestureTapDragDownCallback? onTapDown; - - /// {@macro flutter.gestures.tap.TapGestureRecognizer.onTapUp} - /// - /// This triggers on the up event, if the recognizer wins the arena with it - /// or has previously won. - /// - /// The position of the pointer is provided in the callback's `details` - /// argument, which is a [TapDragUpDetails] object. - /// - /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData} - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - /// * [TapDragUpDetails], which is passed as an argument to this callback. - GestureTapDragUpCallback? onTapUp; - - /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onStart} - /// - /// The position of the pointer is provided in the callback's `details` - /// argument, which is a [TapDragStartDetails] object. The [dragStartBehavior] - /// determines this position. - /// - /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData} - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - /// * [TapDragStartDetails], which is passed as an argument to this callback. - GestureTapDragStartCallback? onDragStart; - - /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onUpdate} - /// - /// The distance traveled by the pointer since the last update is provided in - /// the callback's `details` argument, which is a [TapDragUpdateDetails] object. - /// - /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData} - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - /// * [TapDragUpdateDetails], which is passed as an argument to this callback. - GestureTapDragUpdateCallback? onDragUpdate; - - /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onEnd} - /// - /// The velocity is provided in the callback's `details` argument, which is a - /// [TapDragEndDetails] object. - /// - /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData} - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - /// * [TapDragEndDetails], which is passed as an argument to this callback. - GestureTapDragEndCallback? onDragEnd; - - /// The pointer that previously triggered [onTapDown] did not complete. - /// - /// This is called when a [PointerCancelEvent] is tracked when the [onTapDown] callback - /// was previously called. - /// - /// It may also be called if a [PointerUpEvent] is tracked after the pointer has moved - /// past the tap tolerance but not past the drag tolerance, and the recognizer has not - /// yet won the arena. - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - GestureCancelCallback? onCancel; - - // Tap related state. - bool _pastSlopTolerance = false; - bool _sentTapDown = false; - bool _wonArenaForPrimaryPointer = false; - - // Primary pointer being tracked by this recognizer. - int? _primaryPointer; - Timer? _deadlineTimer; - // The recognizer will call [onTapDown] after this amount of time has elapsed - // since starting to track the primary pointer. - // - // [onTapDown] will not be called if the primary pointer is - // accepted, rejected, or all pointers are up or canceled before [_deadline]. - final Duration _deadline; - - // Drag related state. - _DragState _dragState = _DragState.ready; - PointerEvent? _start; - late OffsetPair _initialPosition; - late OffsetPair _currentPosition; - // late double _globalDistanceMoved; - late double _globalDistanceMovedAllAxes; - - // For drag update throttle. - TapDragUpdateDetails? _lastDragUpdateDetails; - Timer? _dragUpdateThrottleTimer; - - final Set _acceptedActivePointers = {}; - - // Offset _getDeltaForDetails(Offset delta); - // double? _getPrimaryValueFromOffset(Offset value); - bool _hasSufficientGlobalDistanceToAccept( - PointerDeviceKind pointerDeviceKind, - ); - - // Drag updates may require throttling to avoid excessive updating, such as for text layouts in text - // fields. The frequency of invocations is controlled by the [dragUpdateThrottleFrequency]. - // - // Once the drag gesture ends, any pending drag update will be fired - // immediately. See [_checkDragEnd]. - void _handleDragUpdateThrottled() { - assert(_lastDragUpdateDetails != null); - if (onDragUpdate != null) { - invokeCallback( - 'onDragUpdate', - () => onDragUpdate!(_lastDragUpdateDetails!), - ); - } - _dragUpdateThrottleTimer = null; - _lastDragUpdateDetails = null; - } - - @override - bool isPointerAllowed(PointerEvent event) { - if (_primaryPointer == null) { - switch (event.buttons) { - case kPrimaryButton: - if (onTapDown == null && - onDragStart == null && - onDragUpdate == null && - onDragEnd == null && - onTapUp == null && - onCancel == null) { - return false; - } - default: - return false; - } - } else { - if (event.pointer != _primaryPointer) { - return false; - } - } - - return super.isPointerAllowed(event as PointerDownEvent); - } - - @override - void addAllowedPointer(PointerDownEvent event) { - if (_dragState == _DragState.ready) { - super.addAllowedPointer(event); - _primaryPointer = event.pointer; - // _globalDistanceMoved = 0.0; - _globalDistanceMovedAllAxes = 0.0; - _dragState = _DragState.possible; - _initialPosition = OffsetPair( - global: event.position, - local: event.localPosition, - ); - _currentPosition = _initialPosition; - _deadlineTimer = Timer( - _deadline, - () => _didExceedDeadlineWithEvent(event), - ); - } - } - - @override - void handleNonAllowedPointer(PointerDownEvent event) { - // There can be multiple drags simultaneously. Their effects are combined. - if (event.buttons != kPrimaryButton) { - if (!_wonArenaForPrimaryPointer) { - super.handleNonAllowedPointer(event); - } - } - } - - @override - void acceptGesture(int pointer) { - if (pointer != _primaryPointer) { - return; - } - - _stopDeadlineTimer(); - - assert(!_acceptedActivePointers.contains(pointer)); - _acceptedActivePointers.add(pointer); - - // Called when this recognizer is accepted by the [GestureArena]. - if (currentDown != null) { - _checkTapDown(currentDown!); - } - - _wonArenaForPrimaryPointer = true; - - // resolve(GestureDisposition.accepted) will be called when the [PointerMoveEvent] - // has moved a sufficient global distance to be considered a drag and - // `eagerVictoryOnDrag` is set to `true`. - if (_start != null && eagerVictoryOnDrag) { - assert(_dragState == _DragState.accepted); - assert(currentUp == null); - _acceptDrag(_start!); - } - - // This recognizer will wait until it is the last one in the gesture arena - // before accepting a drag when `eagerVictoryOnDrag` is set to `false`. - if (_start != null && !eagerVictoryOnDrag) { - assert(_dragState == _DragState.possible); - assert(currentUp == null); - _dragState = _DragState.accepted; - _acceptDrag(_start!); - } - - if (currentUp != null) { - _checkTapUp(currentUp!); - } - } - - @override - void didStopTrackingLastPointer(int pointer) { - switch (_dragState) { - case _DragState.ready: - _checkCancel(); - resolve(GestureDisposition.rejected); - - case _DragState.possible: - if (_pastSlopTolerance) { - // This means the pointer was not accepted as a tap. - if (_wonArenaForPrimaryPointer) { - // If the recognizer has already won the arena for the primary pointer being tracked - // but the pointer has exceeded the tap tolerance, then the pointer is accepted as a - // drag gesture. - if (currentDown != null) { - if (!_acceptedActivePointers.remove(pointer)) { - resolvePointer(pointer, GestureDisposition.rejected); - } - _dragState = _DragState.accepted; - _acceptDrag(currentDown!); - _checkDragEnd(); - } - } else { - _checkCancel(); - resolve(GestureDisposition.rejected); - } - } else { - // The pointer is accepted as a tap. - if (currentUp != null) { - _checkTapUp(currentUp!); - } - } - - case _DragState.accepted: - // For the case when the pointer has been accepted as a drag. - // Meaning [_checkTapDown] and [_checkDragStart] have already ran. - _checkDragEnd(); - } - - _stopDeadlineTimer(); - _start = null; - _dragState = _DragState.ready; - _pastSlopTolerance = false; - } - - @override - void handleEvent(PointerEvent event) { - if (event.pointer != _primaryPointer) { - return; - } - super.handleEvent(event); - if (event is PointerMoveEvent) { - // Receiving a [PointerMoveEvent], does not automatically mean the pointer - // being tracked is doing a drag gesture. There is some drift that can happen - // between the initial [PointerDownEvent] and subsequent [PointerMoveEvent]s. - // Accessing [_pastSlopTolerance] lets us know if our tap has moved past the - // acceptable tolerance. If the pointer does not move past this tolerance than - // it is not considered a drag. - // - // To be recognized as a drag, the [PointerMoveEvent] must also have moved - // a sufficient global distance from the initial [PointerDownEvent] to be - // accepted as a drag. This logic is handled in [_hasSufficientGlobalDistanceToAccept]. - // - // The recognizer will also detect the gesture as a drag when the pointer - // has been accepted and it has moved past the [slopTolerance] but has not moved - // a sufficient global distance from the initial position to be considered a drag. - // In this case since the gesture cannot be a tap, it defaults to a drag. - final double computedSlop = computeHitSlop(event.kind, gestureSettings); - _pastSlopTolerance = - _pastSlopTolerance || - _getGlobalDistance(event, _initialPosition) > computedSlop; - - if (_dragState == _DragState.accepted) { - _currentPosition = OffsetPair.fromEventPosition(event); - _checkDragUpdate(event); - } else if (_dragState == _DragState.possible) { - if (_start == null) { - // Only check for a drag if the start of a drag was not already identified. - _checkDrag(event); - } - - // This can occur when the recognizer is accepted before a [PointerMoveEvent] has been - // received that moves the pointer a sufficient global distance to be considered a drag. - if (_start != null && _wonArenaForPrimaryPointer) { - _dragState = _DragState.accepted; - _acceptDrag(_start!); - } - } - } else if (event is PointerUpEvent) { - if (_dragState == _DragState.possible) { - // The drag has not been accepted before a [PointerUpEvent], therefore the recognizer - // attempts to recognize a tap. - stopTrackingIfPointerNoLongerDown(event); - } else if (_dragState == _DragState.accepted) { - _giveUpPointer(event.pointer); - } - } else if (event is PointerCancelEvent) { - _dragState = _DragState.ready; - _giveUpPointer(event.pointer); - } - } - - @override - void rejectGesture(int pointer) { - if (pointer != _primaryPointer) { - return; - } - super.rejectGesture(pointer); - - _stopDeadlineTimer(); - _giveUpPointer(pointer); - _resetTaps(); - _resetDragUpdateThrottle(); - } - - @override - void dispose() { - _stopDeadlineTimer(); - _resetDragUpdateThrottle(); - super.dispose(); - } - - @override - String get debugDescription => 'tap_and_drag'; - - void _acceptDrag(PointerEvent event) { - assert(_dragState == _DragState.accepted); - - if (!_wonArenaForPrimaryPointer) { - return; - } - - if (dragStartBehavior == DragStartBehavior.start) { - _initialPosition += OffsetPair( - global: event.delta, - local: event.localDelta, - ); - _currentPosition = _initialPosition; - } - _checkDragStart(event); - final Offset localDelta = event.localDelta; - if (localDelta != Offset.zero) { - _currentPosition = OffsetPair.fromEventPosition(event); - final Offset correctedLocalPosition = _initialPosition.local + localDelta; - final Matrix4? localToGlobalTransform = event.transform == null - ? null - : Matrix4.tryInvert(event.transform!); - final Offset globalUpdateDelta = PointerEvent.transformDeltaViaPositions( - transform: localToGlobalTransform, - untransformedDelta: localDelta, - untransformedEndPosition: correctedLocalPosition, - ); - final updateDelta = OffsetPair( - local: localDelta, - global: globalUpdateDelta, - ); - // Only adds delta for down behaviour - _checkDragUpdate(event, corrected: _initialPosition + updateDelta); - } - } - - void _checkDrag(PointerMoveEvent event) { - final Matrix4? localToGlobalTransform = event.transform == null - ? null - : Matrix4.tryInvert(event.transform!); - // final Offset movedLocally = _getDeltaForDetails(event.localDelta); - // _globalDistanceMoved += - // PointerEvent.transformDeltaViaPositions( - // transform: localToGlobalTransform, - // untransformedDelta: movedLocally, - // untransformedEndPosition: event.localPosition, - // ).distance * - // (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign; - _globalDistanceMovedAllAxes += - PointerEvent.transformDeltaViaPositions( - transform: localToGlobalTransform, - untransformedDelta: event.localDelta, - untransformedEndPosition: event.localPosition, - ).distance * - 1.sign; - if (_hasSufficientGlobalDistanceToAccept(event.kind) || - (_wonArenaForPrimaryPointer && - _globalDistanceMovedAllAxes.abs() > - computePanSlop(event.kind, gestureSettings))) { - _start = event; - if (eagerVictoryOnDrag) { - _dragState = _DragState.accepted; - if (!_wonArenaForPrimaryPointer) { - resolve(GestureDisposition.accepted); - } - } - } - } - - void _checkTapDown(PointerDownEvent event) { - if (_sentTapDown) { - return; - } - - final details = TapDragDownDetails( - globalPosition: event.position, - localPosition: event.localPosition, - kind: getKindForPointer(event.pointer), - consecutiveTapCount: consecutiveTapCount, - ); - - if (onTapDown != null) { - invokeCallback('onTapDown', () => onTapDown!(details)); - } - - _sentTapDown = true; - } - - void _checkTapUp(PointerUpEvent event) { - if (!_wonArenaForPrimaryPointer) { - return; - } - - final upDetails = TapDragUpDetails( - kind: event.kind, - globalPosition: event.position, - localPosition: event.localPosition, - consecutiveTapCount: consecutiveTapCount, - ); - - if (onTapUp != null) { - invokeCallback('onTapUp', () => onTapUp!(upDetails)); - } - - _resetTaps(); - if (!_acceptedActivePointers.remove(event.pointer)) { - resolvePointer(event.pointer, GestureDisposition.rejected); - } - } - - void _checkDragStart(PointerEvent event) { - if (onDragStart != null) { - final details = TapDragStartDetails( - sourceTimeStamp: event.timeStamp, - globalPosition: _initialPosition.global, - localPosition: _initialPosition.local, - kind: getKindForPointer(event.pointer), - consecutiveTapCount: consecutiveTapCount, - ); - - invokeCallback('onDragStart', () => onDragStart!(details)); - } - - _start = null; - } - - void _checkDragUpdate(PointerEvent event, {OffsetPair? corrected}) { - final Offset globalPosition = corrected?.global ?? event.position; - final Offset localPosition = corrected?.local ?? event.localPosition; - - final details = TapDragUpdateDetails( - sourceTimeStamp: event.timeStamp, - delta: event.localDelta, - globalPosition: globalPosition, - kind: getKindForPointer(event.pointer), - localPosition: localPosition, - offsetFromOrigin: globalPosition - _initialPosition.global, - localOffsetFromOrigin: localPosition - _initialPosition.local, - consecutiveTapCount: consecutiveTapCount, - ); - - if (dragUpdateThrottleFrequency != null) { - _lastDragUpdateDetails = details; - // Only schedule a new timer if there's not one pending. - _dragUpdateThrottleTimer ??= Timer( - dragUpdateThrottleFrequency!, - _handleDragUpdateThrottled, - ); - } else { - if (onDragUpdate != null) { - invokeCallback('onDragUpdate', () => onDragUpdate!(details)); - } - } - } - - void _checkDragEnd() { - final Offset globalPosition = _currentPosition.global; - final Offset localPosition = _currentPosition.local; - - if (_dragUpdateThrottleTimer != null) { - // If there's already an update scheduled, trigger it immediately and - // cancel the timer. - _dragUpdateThrottleTimer!.cancel(); - _handleDragUpdateThrottled(); - } - - final endDetails = TapDragEndDetails( - globalPosition: globalPosition, - localPosition: localPosition, - primaryVelocity: 0.0, - consecutiveTapCount: consecutiveTapCount, - ); - - if (onDragEnd != null) { - invokeCallback('onDragEnd', () => onDragEnd!(endDetails)); - } - - _resetTaps(); - _resetDragUpdateThrottle(); - } - - void _checkCancel() { - if (!_sentTapDown) { - // Do not fire tap cancel if [onTapDown] was never called. - return; - } - if (onCancel != null) { - invokeCallback('onCancel', onCancel!); - } - _resetDragUpdateThrottle(); - _resetTaps(); - } - - void _didExceedDeadlineWithEvent(PointerDownEvent event) { - _didExceedDeadline(); - } - - void _didExceedDeadline() { - if (currentDown != null) { - _checkTapDown(currentDown!); - - if (consecutiveTapCount > 1) { - // If our consecutive tap count is greater than 1, i.e. is a double tap or greater, - // then this recognizer declares victory to prevent the [LongPressGestureRecognizer] - // from declaring itself the winner if a double tap is held for too long. - resolve(GestureDisposition.accepted); - } - } - } - - void _giveUpPointer(int pointer) { - stopTrackingPointer(pointer); - // If the pointer was never accepted, then it is rejected since this recognizer is no longer - // interested in winning the gesture arena for it. - if (!_acceptedActivePointers.remove(pointer)) { - resolvePointer(pointer, GestureDisposition.rejected); - } - } - - void _resetTaps() { - _sentTapDown = false; - _wonArenaForPrimaryPointer = false; - _primaryPointer = null; - } - - void _resetDragUpdateThrottle() { - if (dragUpdateThrottleFrequency == null) { - return; - } - _lastDragUpdateDetails = null; - if (_dragUpdateThrottleTimer != null) { - _dragUpdateThrottleTimer!.cancel(); - _dragUpdateThrottleTimer = null; - } - } - - void _stopDeadlineTimer() { - if (_deadlineTimer != null) { - _deadlineTimer!.cancel(); - _deadlineTimer = null; - } - } -} - -/// Recognizes taps along with movement in the horizontal direction. -/// -/// Before this recognizer has won the arena for the primary pointer being tracked, -/// it will only accept a drag on the horizontal axis. If a drag is detected after -/// this recognizer has won the arena then it will accept a drag on any axis. -/// -/// See also: -/// -/// * [BaseTapAndDragGestureRecognizer], for the class that provides the main -/// implementation details of this recognizer. -/// * [TapAndPanGestureRecognizer], for a similar recognizer that accepts a drag -/// on any axis regardless if the recognizer has won the arena for the primary -/// pointer being tracked. -/// * [HorizontalDragGestureRecognizer], for a similar recognizer that only recognizes -/// horizontal movement. -class TapAndHorizontalDragGestureRecognizer - extends BaseTapAndDragGestureRecognizer { - /// Create a gesture recognizer for interactions in the horizontal axis. - /// - /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} - TapAndHorizontalDragGestureRecognizer({ - super.debugOwner, - super.supportedDevices, - }); - - @override - bool _hasSufficientGlobalDistanceToAccept( - PointerDeviceKind pointerDeviceKind, - ) { - return false; - // return _globalDistanceMoved.abs() > - // computeHitSlop(pointerDeviceKind, gestureSettings); - } - - // @override - // Offset _getDeltaForDetails(Offset delta) => Offset(delta.dx, 0.0); - - // @override - // double _getPrimaryValueFromOffset(Offset value) => value.dx; - - @override - String get debugDescription => 'tap and horizontal drag'; -} - -/// {@template flutter.gestures.selectionrecognizers.TapAndPanGestureRecognizer} -/// Recognizes taps along with both horizontal and vertical movement. -/// -/// This recognizer will accept a drag on any axis, regardless if it has won the -/// arena for the primary pointer being tracked. -/// -/// See also: -/// -/// * [BaseTapAndDragGestureRecognizer], for the class that provides the main -/// implementation details of this recognizer. -/// * [TapAndHorizontalDragGestureRecognizer], for a similar recognizer that -/// only accepts horizontal drags before it has won the arena for the primary -/// pointer being tracked. -/// * [PanGestureRecognizer], for a similar recognizer that only recognizes -/// movement. -/// {@endtemplate} -class TapAndPanGestureRecognizer extends BaseTapAndDragGestureRecognizer { - /// Create a gesture recognizer for interactions on a plane. - TapAndPanGestureRecognizer({super.debugOwner, super.supportedDevices}); - - @override - bool _hasSufficientGlobalDistanceToAccept( - PointerDeviceKind pointerDeviceKind, - ) { - return true; - // return _globalDistanceMoved.abs() > - // computePanSlop(pointerDeviceKind, gestureSettings); - } - - // @override - // Offset _getDeltaForDetails(Offset delta) => delta; - - // @override - // double? _getPrimaryValueFromOffset(Offset value) => null; - - @override - String get debugDescription => 'tap and pan'; -} diff --git a/lib/common/widgets/flutter/selectable_text/text_selection.dart b/lib/common/widgets/flutter/selectable_text/text_selection.dart deleted file mode 100644 index 794d2cd27..000000000 --- a/lib/common/widgets/flutter/selectable_text/text_selection.dart +++ /dev/null @@ -1,421 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:math' as math; - -import 'package:PiliPlus/common/widgets/flutter/selectable_text/tap_and_drag.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart' - hide - BaseTapAndDragGestureRecognizer, - TapAndHorizontalDragGestureRecognizer, - TapAndPanGestureRecognizer; -import 'package:flutter/material.dart' hide TextSelectionGestureDetector; - -class CustomTextSelectionGestureDetectorBuilder - extends TextSelectionGestureDetectorBuilder { - CustomTextSelectionGestureDetectorBuilder({required super.delegate}); - - @override - Widget buildGestureDetector({ - Key? key, - HitTestBehavior? behavior, - required Widget child, - }) { - return TextSelectionGestureDetector( - key: key, - onTapTrackStart: onTapTrackStart, - onTapTrackReset: onTapTrackReset, - onTapDown: onTapDown, - onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null, - onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null, - onSecondaryTap: onSecondaryTap, - onSecondaryTapDown: onSecondaryTapDown, - onSingleTapUp: onSingleTapUp, - onSingleTapCancel: onSingleTapCancel, - onUserTap: onUserTap, - onSingleLongTapStart: onSingleLongTapStart, - onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, - onSingleLongTapEnd: onSingleLongTapEnd, - onSingleLongTapCancel: onSingleLongTapCancel, - onDoubleTapDown: onDoubleTapDown, - onTripleTapDown: onTripleTapDown, - onDragSelectionStart: onDragSelectionStart, - onDragSelectionUpdate: onDragSelectionUpdate, - onDragSelectionEnd: onDragSelectionEnd, - onUserTapAlwaysCalled: onUserTapAlwaysCalled, - 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 -/// double tap will only recognize one or the other. This widget detects both: -/// the first tap and then any subsequent taps that occurs within a time limit -/// after the first. -/// -/// See also: -/// -/// * [TextField], a Material text field which uses this gesture detector. -/// * [CupertinoTextField], a Cupertino text field which uses this gesture -/// detector. -class TextSelectionGestureDetector extends StatefulWidget { - /// Create a [TextSelectionGestureDetector]. - /// - /// Multiple callbacks can be called for one sequence of input gesture. - const TextSelectionGestureDetector({ - super.key, - this.onTapTrackStart, - this.onTapTrackReset, - this.onTapDown, - this.onForcePressStart, - this.onForcePressEnd, - this.onSecondaryTap, - this.onSecondaryTapDown, - this.onSingleTapUp, - this.onSingleTapCancel, - this.onUserTap, - this.onSingleLongTapStart, - this.onSingleLongTapMoveUpdate, - this.onSingleLongTapEnd, - this.onSingleLongTapCancel, - this.onDoubleTapDown, - this.onTripleTapDown, - this.onDragSelectionStart, - this.onDragSelectionUpdate, - this.onDragSelectionEnd, - this.onUserTapAlwaysCalled = false, - this.behavior, - required this.child, - }); - - /// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackStart} - /// Callback used to indicate that a tap tracking has started upon - /// a [PointerDownEvent]. - /// {@endtemplate} - final VoidCallback? onTapTrackStart; - - /// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackReset} - /// Callback used to indicate that a tap tracking has been reset which - /// happens on the next [PointerDownEvent] after the timer between two taps - /// elapses, the recognizer loses the arena, the gesture is cancelled or - /// the recognizer is disposed of. - /// {@endtemplate} - final VoidCallback? onTapTrackReset; - - /// Called for every tap down including every tap down that's part of a - /// double click or a long press, except touches that include enough movement - /// to not qualify as taps (e.g. pans and flings). - final GestureTapDragDownCallback? onTapDown; - - /// Called when a pointer has tapped down and the force of the pointer has - /// just become greater than [ForcePressGestureRecognizer.startPressure]. - final GestureForcePressStartCallback? onForcePressStart; - - /// Called when a pointer that had previously triggered [onForcePressStart] is - /// lifted off the screen. - final GestureForcePressEndCallback? onForcePressEnd; - - /// Called for a tap event with the secondary mouse button. - final GestureTapCallback? onSecondaryTap; - - /// Called for a tap down event with the secondary mouse button. - final GestureTapDownCallback? onSecondaryTapDown; - - /// Called for the first tap in a series of taps, consecutive taps do not call - /// this method. - /// - /// For example, if the detector was configured with [onTapDown] and - /// [onDoubleTapDown], three quick taps would be recognized as a single tap - /// down, followed by a tap up, then a double tap down, followed by a single tap down. - final GestureTapDragUpCallback? onSingleTapUp; - - /// Called for each touch that becomes recognized as a gesture that is not a - /// short tap, such as a long tap or drag. It is called at the moment when - /// another gesture from the touch is recognized. - final GestureCancelCallback? onSingleTapCancel; - - /// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is - /// disabled, which is the default behavior. - /// - /// When [onUserTapAlwaysCalled] is enabled, this is called for every tap, - /// including consecutive taps. - final GestureTapCallback? onUserTap; - - /// Called for a single long tap that's sustained for longer than - /// [kLongPressTimeout] but not necessarily lifted. Not called for a - /// double-tap-hold, which calls [onDoubleTapDown] instead. - final GestureLongPressStartCallback? onSingleLongTapStart; - - /// Called after [onSingleLongTapStart] when the pointer is dragged. - final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate; - - /// Called after [onSingleLongTapStart] when the pointer is lifted. - final GestureLongPressEndCallback? onSingleLongTapEnd; - - /// Called after [onSingleLongTapStart] when the pointer is canceled. - final GestureLongPressCancelCallback? onSingleLongTapCancel; - - /// Called after a momentary hold or a short tap that is close in space and - /// time (within [kDoubleTapTimeout]) to a previous short tap. - final GestureTapDragDownCallback? onDoubleTapDown; - - /// Called after a momentary hold or a short tap that is close in space and - /// time (within [kDoubleTapTimeout]) to a previous double-tap. - final GestureTapDragDownCallback? onTripleTapDown; - - /// Called when a mouse starts dragging to select text. - final GestureTapDragStartCallback? onDragSelectionStart; - - /// Called repeatedly as a mouse moves while dragging. - final GestureTapDragUpdateCallback? onDragSelectionUpdate; - - /// Called when a mouse that was previously dragging is released. - final GestureTapDragEndCallback? onDragSelectionEnd; - - /// Whether [onUserTap] will be called for all taps including consecutive taps. - /// - /// Defaults to false, so [onUserTap] is only called for each distinct tap. - final bool onUserTapAlwaysCalled; - - /// How this gesture detector should behave during hit testing. - /// - /// This defaults to [HitTestBehavior.deferToChild]. - final HitTestBehavior? behavior; - - /// Child below this widget. - final Widget child; - - @override - State createState() => _TextSelectionGestureDetectorState(); -} - -class _TextSelectionGestureDetectorState - extends State { - // Converts the details.consecutiveTapCount from a TapAndDrag*Details object, - // which can grow to be infinitely large, to a value between 1 and 3. The value - // that the raw count is converted to is based on the default observed behavior - // on the native platforms. - // - // This method should be used in all instances when details.consecutiveTapCount - // would be used. - static int _getEffectiveConsecutiveTapCount(int rawCount) { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - // From observation, these platform's reset their tap count to 0 when - // the number of consecutive taps exceeds 3. For example on Debian Linux - // with GTK, when going past a triple click, on the fourth click the - // selection is moved to the precise click position, on the fifth click - // the word at the position is selected, and on the sixth click the - // paragraph at the position is selected. - return rawCount <= 3 - ? rawCount - : (rawCount % 3 == 0 ? 3 : rawCount % 3); - case TargetPlatform.iOS: - case TargetPlatform.macOS: - // From observation, these platform's either hold their tap count at 3. - // For example on macOS, when going past a triple click, the selection - // should be retained at the paragraph that was first selected on triple - // click. - return math.min(rawCount, 3); - case TargetPlatform.windows: - // From observation, this platform's consecutive tap actions alternate - // between double click and triple click actions. For example, after a - // triple click has selected a paragraph, on the next click the word at - // the clicked position will be selected, and on the next click the - // paragraph at the position is selected. - return rawCount < 2 ? rawCount : 2 + rawCount % 2; - } - } - - void _handleTapTrackStart() { - widget.onTapTrackStart?.call(); - } - - void _handleTapTrackReset() { - widget.onTapTrackReset?.call(); - } - - // The down handler is force-run on success of a single tap and optimistically - // run before a long press success. - void _handleTapDown(TapDragDownDetails details) { - widget.onTapDown?.call(details); - // This isn't detected as a double tap gesture in the gesture recognizer - // because it's 2 single taps, each of which may do different things depending - // on whether it's a single tap, the first tap of a double tap, the second - // tap held down, a clean double tap etc. - if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) { - return widget.onDoubleTapDown?.call(details); - } - - if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 3) { - return widget.onTripleTapDown?.call(details); - } - } - - void _handleTapUp(TapDragUpDetails details) { - if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) { - widget.onSingleTapUp?.call(details); - widget.onUserTap?.call(); - } else if (widget.onUserTapAlwaysCalled) { - widget.onUserTap?.call(); - } - } - - void _handleTapCancel() { - widget.onSingleTapCancel?.call(); - } - - void _handleDragStart(TapDragStartDetails details) { - widget.onDragSelectionStart?.call(details); - } - - void _handleDragUpdate(TapDragUpdateDetails details) { - widget.onDragSelectionUpdate?.call(details); - } - - void _handleDragEnd(TapDragEndDetails details) { - widget.onDragSelectionEnd?.call(details); - } - - void _forcePressStarted(ForcePressDetails details) { - widget.onForcePressStart?.call(details); - } - - void _forcePressEnded(ForcePressDetails details) { - widget.onForcePressEnd?.call(details); - } - - void _handleLongPressStart(LongPressStartDetails details) { - widget.onSingleLongTapStart?.call(details); - } - - void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - widget.onSingleLongTapMoveUpdate?.call(details); - } - - void _handleLongPressEnd(LongPressEndDetails details) { - widget.onSingleLongTapEnd?.call(details); - } - - void _handleLongPressCancel() { - widget.onSingleLongTapCancel?.call(); - } - - @override - Widget build(BuildContext context) { - final gestures = {}; - - gestures[TapGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(debugOwner: this), - (TapGestureRecognizer instance) { - instance - ..onSecondaryTap = widget.onSecondaryTap - ..onSecondaryTapDown = widget.onSecondaryTapDown; - }, - ); - - if (widget.onSingleLongTapStart != null || - widget.onSingleLongTapMoveUpdate != null || - widget.onSingleLongTapEnd != null || - widget.onSingleLongTapCancel != null) { - gestures[LongPressGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer( - debugOwner: this, - supportedDevices: {PointerDeviceKind.touch}, - ), - (LongPressGestureRecognizer instance) { - instance - ..onLongPressStart = _handleLongPressStart - ..onLongPressMoveUpdate = _handleLongPressMoveUpdate - ..onLongPressEnd = _handleLongPressEnd - ..onLongPressCancel = _handleLongPressCancel; - }, - ); - } - - if (widget.onDragSelectionStart != null || - widget.onDragSelectionUpdate != null || - widget.onDragSelectionEnd != null) { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.iOS: - gestures[TapAndHorizontalDragGestureRecognizer] = - GestureRecognizerFactoryWithHandlers< - TapAndHorizontalDragGestureRecognizer - >( - () => TapAndHorizontalDragGestureRecognizer(debugOwner: this), - (TapAndHorizontalDragGestureRecognizer instance) { - instance - // Text selection should start from the position of the first pointer - // down event. - ..dragStartBehavior = DragStartBehavior.down - ..eagerVictoryOnDrag = - defaultTargetPlatform != TargetPlatform.iOS - ..onTapTrackStart = _handleTapTrackStart - ..onTapTrackReset = _handleTapTrackReset - ..onTapDown = _handleTapDown - ..onDragStart = _handleDragStart - ..onDragUpdate = _handleDragUpdate - ..onDragEnd = _handleDragEnd - ..onTapUp = _handleTapUp - ..onCancel = _handleTapCancel; - }, - ); - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - gestures[TapAndPanGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => TapAndPanGestureRecognizer(debugOwner: this), - (TapAndPanGestureRecognizer instance) { - instance - // Text selection should start from the position of the first pointer - // down event. - ..dragStartBehavior = DragStartBehavior.down - ..onTapTrackStart = _handleTapTrackStart - ..onTapTrackReset = _handleTapTrackReset - ..onTapDown = _handleTapDown - ..onDragStart = _handleDragStart - ..onDragUpdate = _handleDragUpdate - ..onDragEnd = _handleDragEnd - ..onTapUp = _handleTapUp - ..onCancel = _handleTapCancel; - }, - ); - } - } - - if (widget.onForcePressStart != null || widget.onForcePressEnd != null) { - gestures[ForcePressGestureRecognizer] = - GestureRecognizerFactoryWithHandlers( - () => ForcePressGestureRecognizer(debugOwner: this), - (ForcePressGestureRecognizer instance) { - instance - ..onStart = widget.onForcePressStart != null - ? _forcePressStarted - : null - ..onEnd = widget.onForcePressEnd != null - ? _forcePressEnded - : null; - }, - ); - } - - return RawGestureDetector( - gestures: gestures, - excludeFromSemantics: true, - behavior: widget.behavior, - child: widget.child, - ); - } -} diff --git a/lib/common/widgets/flutter/selectable_text/text.dart b/lib/common/widgets/selectable_text.dart similarity index 74% rename from lib/common/widgets/flutter/selectable_text/text.dart rename to lib/common/widgets/selectable_text.dart index 24be9f726..235abda4f 100644 --- a/lib/common/widgets/flutter/selectable_text/text.dart +++ b/lib/common/widgets/selectable_text.dart @@ -1,7 +1,5 @@ -import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_text.dart'; -import 'package:PiliPlus/common/widgets/flutter/selectable_text/selection_area.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide SelectableText, SelectionArea; +import 'package:flutter/material.dart'; Widget selectableText( String text, { diff --git a/lib/pages/live_room/superchat/superchat_card.dart b/lib/pages/live_room/superchat/superchat_card.dart index 6b0e6d41f..5a542d283 100644 --- a/lib/pages/live_room/superchat/superchat_card.dart +++ b/lib/pages/live_room/superchat/superchat_card.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:PiliPlus/common/widgets/flutter/selectable_text/selection_area.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/live/live_superchat/item.dart'; @@ -11,7 +10,7 @@ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart' show kDebugMode; -import 'package:flutter/material.dart' hide SelectionArea; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class SuperChatCard extends StatefulWidget { diff --git a/lib/pages/pgc_review/child/view.dart b/lib/pages/pgc_review/child/view.dart index 7660e3c61..a8536acef 100644 --- a/lib/pages/pgc_review/child/view.dart +++ b/lib/pages/pgc_review/child/view.dart @@ -3,7 +3,6 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; -import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_text.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; @@ -19,7 +18,7 @@ import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:flutter/material.dart' hide SelectableText; +import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; diff --git a/lib/pages/video/ai_conclusion/view.dart b/lib/pages/video/ai_conclusion/view.dart index f3ae11c13..d91ce23b9 100644 --- a/lib/pages/video/ai_conclusion/view.dart +++ b/lib/pages/video/ai_conclusion/view.dart @@ -1,5 +1,5 @@ -import 'package:PiliPlus/common/widgets/flutter/selectable_text/text.dart'; import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; +import 'package:PiliPlus/common/widgets/selectable_text.dart'; import 'package:PiliPlus/models_new/video/video_ai_conclusion/model_result.dart'; import 'package:PiliPlus/pages/common/slide/common_slide_page.dart'; import 'package:PiliPlus/pages/video/controller.dart'; diff --git a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart index c8619e1e2..514609431 100644 --- a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart +++ b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart @@ -1,7 +1,7 @@ import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; -import 'package:PiliPlus/common/widgets/flutter/selectable_text/text.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/selectable_text.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart'; diff --git a/lib/pages/video/introduction/ugc/view.dart b/lib/pages/video/introduction/ugc/view.dart index fb2a88e0b..32c642bf0 100644 --- a/lib/pages/video/introduction/ugc/view.dart +++ b/lib/pages/video/introduction/ugc/view.dart @@ -2,12 +2,11 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; -import 'package:PiliPlus/common/widgets/flutter/selectable_text/selection_area.dart'; -import 'package:PiliPlus/common/widgets/flutter/selectable_text/text.dart'; import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/selectable_text.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/http/sponsor_block.dart'; import 'package:PiliPlus/models/common/image_type.dart'; @@ -39,7 +38,7 @@ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:expandable/expandable.dart'; -import 'package:flutter/material.dart' hide SelectionArea; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; diff --git a/lib/scripts/patch.ps1 b/lib/scripts/patch.ps1 index 9873a4fb8..2717ee15b 100644 --- a/lib/scripts/patch.ps1 +++ b/lib/scripts/patch.ps1 @@ -14,6 +14,8 @@ $BottomSheetPatch = "lib/scripts/bottom_sheet.patch" $ScrollViewPatch = "lib/scripts/scroll_view.patch" +$TextSelectionPatch = "lib/scripts/text_selection.patch" + # TODO: remove # https://github.com/flutter/flutter/issues/90223 $ModalBarrierPatch = "lib/scripts/modal_barrier.patch" @@ -26,7 +28,7 @@ Set-Location $env:FLUTTER_ROOT $picks = @() $reverts = @() -$patches = @($ModalBarrierPatch, $MouseCursorPatch) +$patches = @($ModalBarrierPatch, $TextSelectionPatch, $MouseCursorPatch) switch ($platform.ToLower()) { "android" { diff --git a/lib/scripts/text_selection.patch b/lib/scripts/text_selection.patch new file mode 100644 index 000000000..5abf357dc --- /dev/null +++ b/lib/scripts/text_selection.patch @@ -0,0 +1,20 @@ +diff --git a/packages/flutter/lib/src/gestures/tap_and_drag.dart b/packages/flutter/lib/src/gestures/tap_and_drag.dart +index 2409d19eabd..9460b61c00a 100644 +--- a/packages/flutter/lib/src/gestures/tap_and_drag.dart ++++ b/packages/flutter/lib/src/gestures/tap_and_drag.dart +@@ -1407,6 +1407,7 @@ class TapAndHorizontalDragGestureRecognizer extends BaseTapAndDragGestureRecogni + + @override + bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind) { ++ return false; + return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings); + } + +@@ -1442,6 +1443,7 @@ class TapAndPanGestureRecognizer extends BaseTapAndDragGestureRecognizer { + + @override + bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind) { ++ return true; + return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings); + } +