// 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.

/// @docImport 'package:flutter/material.dart';
/// @docImport 'package:flutter_test/flutter_test.dart';
///
/// @docImport 'editable_text.dart';
/// @docImport 'text.dart';
library;

import 'dart:async';
import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart';

import 'actions.dart';
import 'basic.dart';
import 'context_menu_button_item.dart';
import 'debug.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'magnifier.dart';
import 'media_query.dart';
import 'overlay.dart';
import 'platform_selectable_region_context_menu.dart';
import 'selection_container.dart';
import 'tap_region.dart';
import 'text_editing_intents.dart';
import 'text_selection.dart';
import 'text_selection_toolbar_anchors.dart';

// Examples can assume:
// late GlobalKey key;

const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{
  PointerDeviceKind.touch,
  PointerDeviceKind.stylus,
  PointerDeviceKind.invertedStylus,
};

// In practice some selectables like widgetspan shift several pixels. So when
// the vertical position diff is within the threshold, compare the horizontal
// position to make the compareScreenOrder function more robust.
const double _kSelectableVerticalComparingThreshold = 3.0;

/// 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 <Widget>[
///           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<SelectedContent?>? 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<ContextMenuButtonItem> 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 <ContextMenuButtonItem>[
      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<StatefulWidget> createState() => SelectableRegionState();
}

/// State for a [SelectableRegion].
class SelectableRegionState extends State<SelectableRegion>
    with TextSelectionDelegate
    implements SelectionRegistrar {
  late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
    SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
    CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
    ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(
      _GranularlyExtendSelectionAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(
        this,
        granularity: TextGranularity.word,
      ),
    ),
    ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(
      _GranularlyExtendSelectionAction<ExpandSelectionToDocumentBoundaryIntent>(
        this,
        granularity: TextGranularity.document,
      ),
    ),
    ExpandSelectionToLineBreakIntent: _makeOverridable(
      _GranularlyExtendSelectionAction<ExpandSelectionToLineBreakIntent>(
        this,
        granularity: TextGranularity.line,
      ),
    ),
    ExtendSelectionByCharacterIntent: _makeOverridable(
      _GranularlyExtendCaretSelectionAction<ExtendSelectionByCharacterIntent>(
        this,
        granularity: TextGranularity.character,
      ),
    ),
    ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(
      _GranularlyExtendCaretSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(
        this,
        granularity: TextGranularity.word,
      ),
    ),
    ExtendSelectionToLineBreakIntent: _makeOverridable(
      _GranularlyExtendCaretSelectionAction<ExtendSelectionToLineBreakIntent>(
        this,
        granularity: TextGranularity.line,
      ),
    ),
    ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(
      _DirectionallyExtendCaretSelectionAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this),
    ),
    ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(
      _GranularlyExtendCaretSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(
        this,
        granularity: TextGranularity.document,
      ),
    ),
  };

  final Map<Type, GestureRecognizerFactory> _gestureRecognizers =
      <Type, GestureRecognizerFactory>{};
  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<ProcessTextAction> _processTextActions = <ProcessTextAction>[];

  // 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>(
          () => 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<void> _initProcessTextActions() async {
    _processTextActions.clear();
    _processTextActions.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<T> _makeOverridable<T extends Intent>(Action<T> defaultAction) {
    return Action<T>.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>(
          () => TapAndPanGestureRecognizer(
            debugOwner: this,
            supportedDevices: <PointerDeviceKind>{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>{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>(
          () => 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<void> _copy() async {
    final SelectedContent? data = _selectable?.getSelectedContent();
    if (data == null) {
      return;
    }
    await Clipboard.setData(ClipboardData(text: data.plainText));
  }

  Future<void> _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<ContextMenuButtonItem> 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<ContextMenuButtonItem> get _textProcessingActionButtonItems {
    final buttonItems = <ContextMenuButtonItem>[];
    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<TextSelectionPoint> get selectionEndpoints {
    final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
    final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
    late List<TextSelectionPoint> points;
    final Offset startLocalPosition = start?.localPosition ?? end!.localPosition;
    final Offset endLocalPosition = end?.localPosition ?? start!.localPosition;
    if (startLocalPosition.dy > endLocalPosition.dy) {
      points = <TextSelectionPoint>[
        TextSelectionPoint(endLocalPosition, TextDirection.ltr),
        TextSelectionPoint(startLocalPosition, TextDirection.ltr),
      ];
    } else {
      points = <TextSelectionPoint>[
        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<void> 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 TapRegion(
      groupId: SelectableRegion,
      onTapOutside: (PointerDownEvent event) {
        // To match the native web behavior, this selectable region is
        // unfocused when tapping outside of it causing the selection to
        // be dismissed.
        //
        // Tapping outside the selectable region does not unfocus
        // the region on non-web platforms.
        if (kIsWeb) {
          _focusNode.unfocus();
        }
      },
      child: 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<T extends Intent> extends ContextAction<T> {
  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<SelectAllTextIntent> {
  _SelectAllAction(this.state);

  final SelectableRegionState state;

  @override
  void invokeAction(SelectAllTextIntent intent, [BuildContext? context]) {
    state.selectAll(SelectionChangedCause.keyboard);
  }
}

class _CopySelectionAction extends _NonOverrideAction<CopySelectionTextIntent> {
  _CopySelectionAction(this.state);

  final SelectableRegionState state;

  @override
  void invokeAction(CopySelectionTextIntent intent, [BuildContext? context]) {
    state._copy();
  }
}

class _GranularlyExtendSelectionAction<T extends DirectionalTextEditingIntent>
    extends _NonOverrideAction<T> {
  _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<T> {
  _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<T> {
  _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);
  }
}

/// A delegate that manages updating multiple [Selectable] children where the
/// [Selectable]s do not change or move around frequently.
///
/// This delegate keeps track of the [Selectable]s that received start or end
/// [SelectionEvent]s and the global locations of those events to accurately
/// synthesize [SelectionEvent]s for children [Selectable]s when needed.
///
/// When a new [SelectionEdgeUpdateEvent] is dispatched to a [Selectable], this
/// delegate checks whether the [Selectable] has already received a selection
/// update for each edge that currently exists, and synthesizes an event for the
/// edges that have not yet received an update. This synthesized event is dispatched
/// before dispatching the new event.
///
/// For example, if we have an existing start edge for this delegate and a [Selectable]
/// child receives an end [SelectionEdgeUpdateEvent] and the child hasn't received a start
/// [SelectionEdgeUpdateEvent], we synthesize a start [SelectionEdgeUpdateEvent] for the
/// child [Selectable] and dispatch it before dispatching the original end [SelectionEdgeUpdateEvent].
///
/// See also:
///
///  * [MultiSelectableSelectionContainerDelegate], for the class that provides
///  the main implementation details of this [SelectionContainerDelegate].
class StaticSelectionContainerDelegate extends MultiSelectableSelectionContainerDelegate {
  /// The set of [Selectable]s that have received start events.
  final Set<Selectable> _hasReceivedStartEvent = <Selectable>{};

  /// The set of [Selectable]s that have received end events.
  final Set<Selectable> _hasReceivedEndEvent = <Selectable>{};

  /// The global position of the last selection start edge update.
  Offset? _lastStartEdgeUpdateGlobalPosition;

  /// The global position of the last selection end edge update.
  Offset? _lastEndEdgeUpdateGlobalPosition;

  /// Tracks whether a selection edge update event for a given [Selectable] was received.
  ///
  /// When `forEnd` is true, the [Selectable] will be registered as having received
  /// an end event. When false, the [Selectable] is registered as having received
  /// a start event.
  ///
  /// When `forEnd` is null, the [Selectable] will be registered as having received both
  /// start and end events.
  ///
  /// Call this method when a [SelectionEvent] is dispatched to a child selectable managed
  /// by this delegate.
  ///
  /// Subclasses should call [clearInternalSelectionStateForSelectable] to clean up any state
  /// added by this method, for example when removing a [Selectable] from this delegate.
  @protected
  void didReceiveSelectionEventFor({required Selectable selectable, bool? forEnd}) {
    switch (forEnd) {
      case true:
        _hasReceivedEndEvent.add(selectable);
      case false:
        _hasReceivedStartEvent.add(selectable);
      case null:
        _hasReceivedStartEvent.add(selectable);
        _hasReceivedEndEvent.add(selectable);
    }
  }

  /// Updates the internal selection state after a [SelectionEvent] that
  /// selects a boundary such as: [SelectWordSelectionEvent],
  /// [SelectParagraphSelectionEvent], and [SelectAllSelectionEvent].
  ///
  /// Call this method after determining the new selection as a result of
  /// a [SelectionEvent] that selects a boundary. The [currentSelectionStartIndex]
  /// and [currentSelectionEndIndex] should be set to valid values at the time
  /// this method is called.
  ///
  /// Subclasses should call [clearInternalSelectionStateForSelectable] to clean up any state
  /// added by this method, for example when removing a [Selectable] from this delegate.
  @protected
  void didReceiveSelectionBoundaryEvents() {
    if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
      return;
    }
    final int start = min(currentSelectionStartIndex, currentSelectionEndIndex);
    final int end = max(currentSelectionStartIndex, currentSelectionEndIndex);
    for (var index = start; index <= end; index += 1) {
      didReceiveSelectionEventFor(selectable: selectables[index]);
    }
    _updateLastSelectionEdgeLocationsFromGeometries();
  }

  /// Updates the last selection edge location of the edge specified by `forEnd`
  /// to the provided `globalSelectionEdgeLocation`.
  @protected
  void updateLastSelectionEdgeLocation({
    required Offset globalSelectionEdgeLocation,
    required bool forEnd,
  }) {
    if (forEnd) {
      _lastEndEdgeUpdateGlobalPosition = globalSelectionEdgeLocation;
    } else {
      _lastStartEdgeUpdateGlobalPosition = globalSelectionEdgeLocation;
    }
  }

  /// Updates the last selection edge locations of both start and end selection
  /// edges based on their [SelectionGeometry].
  void _updateLastSelectionEdgeLocationsFromGeometries() {
    if (currentSelectionStartIndex != -1 &&
        selectables[currentSelectionStartIndex].value.hasSelection) {
      final Selectable start = selectables[currentSelectionStartIndex];
      final Offset localStartEdge =
          start.value.startSelectionPoint!.localPosition +
          Offset(0, -start.value.startSelectionPoint!.lineHeight / 2);
      updateLastSelectionEdgeLocation(
        globalSelectionEdgeLocation: MatrixUtils.transformPoint(
          start.getTransformTo(null),
          localStartEdge,
        ),
        forEnd: false,
      );
    }
    if (currentSelectionEndIndex != -1 &&
        selectables[currentSelectionEndIndex].value.hasSelection) {
      final Selectable end = selectables[currentSelectionEndIndex];
      final Offset localEndEdge =
          end.value.endSelectionPoint!.localPosition +
          Offset(0, -end.value.endSelectionPoint!.lineHeight / 2);
      updateLastSelectionEdgeLocation(
        globalSelectionEdgeLocation: MatrixUtils.transformPoint(
          end.getTransformTo(null),
          localEndEdge,
        ),
        forEnd: true,
      );
    }
  }

  /// Clears the internal selection state.
  ///
  /// This indicates that no [Selectable] child under this delegate
  /// has received start or end events, and resets any tracked global
  /// locations for start and end [SelectionEdgeUpdateEvent]s.
  @protected
  void clearInternalSelectionState() {
    selectables.forEach(clearInternalSelectionStateForSelectable);
    _lastStartEdgeUpdateGlobalPosition = null;
    _lastEndEdgeUpdateGlobalPosition = null;
  }

  /// Clears the internal selection state for a given [Selectable].
  ///
  /// This indicates that the given `selectable` has neither received a
  /// start or end [SelectionEdgeUpdateEvent]s.
  ///
  /// Subclasses should call this method to clean up state added in
  /// [didReceiveSelectionEventFor] and [didReceiveSelectionBoundaryEvents].
  @protected
  void clearInternalSelectionStateForSelectable(Selectable selectable) {
    _hasReceivedStartEvent.remove(selectable);
    _hasReceivedEndEvent.remove(selectable);
  }

  @override
  void remove(Selectable selectable) {
    clearInternalSelectionStateForSelectable(selectable);
    super.remove(selectable);
  }

  @override
  SelectionResult handleSelectAll(SelectAllSelectionEvent event) {
    final SelectionResult result = super.handleSelectAll(event);
    didReceiveSelectionBoundaryEvents();
    return result;
  }

  @override
  SelectionResult handleSelectWord(SelectWordSelectionEvent event) {
    final SelectionResult result = super.handleSelectWord(event);
    didReceiveSelectionBoundaryEvents();
    return result;
  }

  @override
  SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
    final SelectionResult result = super.handleSelectParagraph(event);
    didReceiveSelectionBoundaryEvents();
    return result;
  }

  @override
  SelectionResult handleClearSelection(ClearSelectionEvent event) {
    final SelectionResult result = super.handleClearSelection(event);
    clearInternalSelectionState();
    return result;
  }

  @override
  SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
    updateLastSelectionEdgeLocation(
      globalSelectionEdgeLocation: event.globalPosition,
      forEnd: event.type == SelectionEventType.endEdgeUpdate,
    );
    return super.handleSelectionEdgeUpdate(event);
  }

  @override
  void dispose() {
    clearInternalSelectionState();
    super.dispose();
  }

  @override
  SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
    switch (event.type) {
      case SelectionEventType.startEdgeUpdate:
        didReceiveSelectionEventFor(selectable: selectable, forEnd: false);
        ensureChildUpdated(selectable);
      case SelectionEventType.endEdgeUpdate:
        didReceiveSelectionEventFor(selectable: selectable, forEnd: true);
        ensureChildUpdated(selectable);
      case SelectionEventType.clear:
        clearInternalSelectionStateForSelectable(selectable);
      case SelectionEventType.selectAll:
      case SelectionEventType.selectWord:
      case SelectionEventType.selectParagraph:
        break;
      case SelectionEventType.granularlyExtendSelection:
      case SelectionEventType.directionallyExtendSelection:
        didReceiveSelectionEventFor(selectable: selectable);
        ensureChildUpdated(selectable);
    }
    return super.dispatchSelectionEventToChild(selectable, event);
  }

  /// Ensures the `selectable` child has received the most up to date selection events.
  ///
  /// This method is called when:
  ///   1. A new [Selectable] is added to the delegate, and its screen location
  ///   falls into the previous selection.
  ///   2. Before a [SelectionEvent] of type
  ///   [SelectionEventType.startEdgeUpdate], [SelectionEventType.endEdgeUpdate],
  ///   [SelectionEventType.granularlyExtendSelection], or
  ///   [SelectionEventType.directionallyExtendSelection] is dispatched
  ///   to a [Selectable] child.
  @override
  void ensureChildUpdated(Selectable selectable) {
    if (_lastEndEdgeUpdateGlobalPosition != null && _hasReceivedEndEvent.add(selectable)) {
      final synthesizedEvent = SelectionEdgeUpdateEvent.forEnd(
        globalPosition: _lastEndEdgeUpdateGlobalPosition!,
      );
      if (currentSelectionEndIndex == -1) {
        handleSelectionEdgeUpdate(synthesizedEvent);
      }
      selectable.dispatchSelectionEvent(synthesizedEvent);
    }
    if (_lastStartEdgeUpdateGlobalPosition != null && _hasReceivedStartEvent.add(selectable)) {
      final synthesizedEvent = SelectionEdgeUpdateEvent.forStart(
        globalPosition: _lastStartEdgeUpdateGlobalPosition!,
      );
      if (currentSelectionStartIndex == -1) {
        handleSelectionEdgeUpdate(synthesizedEvent);
      }
      selectable.dispatchSelectionEvent(synthesizedEvent);
    }
  }

  @override
  void didChangeSelectables() {
    if (_lastEndEdgeUpdateGlobalPosition != null) {
      handleSelectionEdgeUpdate(
        SelectionEdgeUpdateEvent.forEnd(globalPosition: _lastEndEdgeUpdateGlobalPosition!),
      );
    }
    if (_lastStartEdgeUpdateGlobalPosition != null) {
      handleSelectionEdgeUpdate(
        SelectionEdgeUpdateEvent.forStart(globalPosition: _lastStartEdgeUpdateGlobalPosition!),
      );
    }
    final Set<Selectable> selectableSet = selectables.toSet();
    _hasReceivedEndEvent.removeWhere(
      (Selectable selectable) => !selectableSet.contains(selectable),
    );
    _hasReceivedStartEvent.removeWhere(
      (Selectable selectable) => !selectableSet.contains(selectable),
    );
    super.didChangeSelectables();
  }
}

/// A delegate that handles events and updates for multiple [Selectable]
/// children.
///
/// Updates are optimized by tracking which [Selectable]s reside on the edges of
/// a selection. Subclasses should implement [ensureChildUpdated] to describe
/// how a [Selectable] should behave when added to a selection.
abstract class MultiSelectableSelectionContainerDelegate extends SelectionContainerDelegate
    with ChangeNotifier {
  /// Creates an instance of [MultiSelectableSelectionContainerDelegate].
  MultiSelectableSelectionContainerDelegate() {
    if (kFlutterMemoryAllocationsEnabled) {
      ChangeNotifier.maybeDispatchObjectCreation(this);
    }
  }

  /// Gets the list of [Selectable]s this delegate is managing.
  List<Selectable> selectables = <Selectable>[];

  /// The number of additional pixels added to the selection handle drawable
  /// area.
  ///
  /// Selection handles that are outside of the drawable area will be hidden.
  /// That logic prevents handles that get scrolled off the viewport from being
  /// drawn on the screen.
  ///
  /// The drawable area = current rectangle of [SelectionContainer] +
  /// _kSelectionHandleDrawableAreaPadding on each side.
  ///
  /// This was an eyeballed value to create smooth user experiences.
  static const double _kSelectionHandleDrawableAreaPadding = 5.0;

  /// The current [Selectable] that contains the selection end edge.
  @protected
  int currentSelectionEndIndex = -1;

  /// The current [Selectable] that contains the selection start edge.
  @protected
  int currentSelectionStartIndex = -1;

  LayerLink? _startHandleLayer;
  Selectable? _startHandleLayerOwner;
  LayerLink? _endHandleLayer;
  Selectable? _endHandleLayerOwner;

  bool _isHandlingSelectionEvent = false;
  bool _scheduledSelectableUpdate = false;
  bool _selectionInProgress = false;
  Set<Selectable> _additions = <Selectable>{};

  bool _extendSelectionInProgress = false;

  @override
  void add(Selectable selectable) {
    assert(!selectables.contains(selectable));
    _additions.add(selectable);
    _scheduleSelectableUpdate();
  }

  @override
  void remove(Selectable selectable) {
    if (_additions.remove(selectable)) {
      // The same selectable was added in the same frame and is not yet
      // incorporated into the selectables.
      //
      // Removing such selectable doesn't require selection geometry update.
      return;
    }
    _removeSelectable(selectable);
    _scheduleSelectableUpdate();
  }

  /// Notifies this delegate that layout of the container has changed.
  void layoutDidChange() {
    _updateSelectionGeometry();
  }

  void _scheduleSelectableUpdate() {
    if (!_scheduledSelectableUpdate) {
      _scheduledSelectableUpdate = true;
      void runScheduledTask([Duration? duration]) {
        if (!_scheduledSelectableUpdate) {
          return;
        }
        _scheduledSelectableUpdate = false;
        _updateSelectables();
      }

      if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.postFrameCallbacks) {
        // A new task can be scheduled as a result of running the scheduled task
        // from another MultiSelectableSelectionContainerDelegate. This can
        // happen if nesting two SelectionContainers. The selectable can be
        // safely updated in the same frame in this case.
        scheduleMicrotask(runScheduledTask);
      } else {
        SchedulerBinding.instance.addPostFrameCallback(
          runScheduledTask,
          debugLabel: 'SelectionContainer.runScheduledTask',
        );
      }
    }
  }

  void _updateSelectables() {
    // Remove offScreen selectable.
    if (_additions.isNotEmpty) {
      _flushAdditions();
    }
    didChangeSelectables();
  }

  void _flushAdditions() {
    final List<Selectable> mergingSelectables = _additions.toList()..sort(compareOrder);
    final List<Selectable> existingSelectables = selectables;
    selectables = <Selectable>[];
    var mergingIndex = 0;
    var existingIndex = 0;
    int selectionStartIndex = currentSelectionStartIndex;
    int selectionEndIndex = currentSelectionEndIndex;
    // Merge two sorted lists.
    while (mergingIndex < mergingSelectables.length || existingIndex < existingSelectables.length) {
      if (mergingIndex >= mergingSelectables.length ||
          (existingIndex < existingSelectables.length &&
              compareOrder(existingSelectables[existingIndex], mergingSelectables[mergingIndex]) <
                  0)) {
        if (existingIndex == currentSelectionStartIndex) {
          selectionStartIndex = selectables.length;
        }
        if (existingIndex == currentSelectionEndIndex) {
          selectionEndIndex = selectables.length;
        }
        selectables.add(existingSelectables[existingIndex]);
        existingIndex += 1;
        continue;
      }

      // If the merging selectable falls in the selection range, their selection
      // needs to be updated.
      final Selectable mergingSelectable = mergingSelectables[mergingIndex];
      if (existingIndex < max(currentSelectionStartIndex, currentSelectionEndIndex) &&
          existingIndex > min(currentSelectionStartIndex, currentSelectionEndIndex)) {
        ensureChildUpdated(mergingSelectable);
      }
      mergingSelectable.addListener(_handleSelectableGeometryChange);
      selectables.add(mergingSelectable);
      mergingIndex += 1;
    }
    assert(
      mergingIndex == mergingSelectables.length &&
          existingIndex == existingSelectables.length &&
          selectables.length == existingIndex + mergingIndex,
    );
    assert(selectionStartIndex >= -1 || selectionStartIndex < selectables.length);
    assert(selectionEndIndex >= -1 || selectionEndIndex < selectables.length);
    // selection indices should not be set to -1 unless they originally were.
    assert((currentSelectionStartIndex == -1) == (selectionStartIndex == -1));
    assert((currentSelectionEndIndex == -1) == (selectionEndIndex == -1));
    currentSelectionEndIndex = selectionEndIndex;
    currentSelectionStartIndex = selectionStartIndex;
    _additions = <Selectable>{};
  }

  void _removeSelectable(Selectable selectable) {
    assert(selectables.contains(selectable), 'The selectable is not in this registrar.');
    final int index = selectables.indexOf(selectable);
    selectables.removeAt(index);
    if (index <= currentSelectionEndIndex) {
      currentSelectionEndIndex -= 1;
    }
    if (index <= currentSelectionStartIndex) {
      currentSelectionStartIndex -= 1;
    }
    selectable.removeListener(_handleSelectableGeometryChange);
  }

  /// Called when this delegate finishes updating the [Selectable]s.
  @protected
  @mustCallSuper
  void didChangeSelectables() {
    _updateSelectionGeometry();
  }

  @override
  SelectionGeometry get value => _selectionGeometry;
  SelectionGeometry _selectionGeometry = const SelectionGeometry(
    hasContent: false,
    status: SelectionStatus.none,
  );

  /// Updates the [value] in this class and notifies listeners if necessary.
  void _updateSelectionGeometry() {
    final SelectionGeometry newValue = getSelectionGeometry();
    if (_selectionGeometry != newValue) {
      _selectionGeometry = newValue;
      notifyListeners();
    }
    _updateHandleLayersAndOwners();
  }

  static Rect _getBoundingBox(Selectable selectable) {
    Rect result = selectable.boundingBoxes.first;
    for (var index = 1; index < selectable.boundingBoxes.length; index += 1) {
      result = result.expandToInclude(selectable.boundingBoxes[index]);
    }
    return result;
  }

  /// The compare function this delegate used for determining the selection
  /// order of the selectables.
  ///
  /// Defaults to screen order.
  @protected
  Comparator<Selectable> get compareOrder => _compareScreenOrder;

  static int _compareScreenOrder(Selectable a, Selectable b) {
    final Rect rectA = MatrixUtils.transformRect(a.getTransformTo(null), _getBoundingBox(a));
    final Rect rectB = MatrixUtils.transformRect(b.getTransformTo(null), _getBoundingBox(b));
    final int result = _compareVertically(rectA, rectB);
    if (result != 0) {
      return result;
    }
    return _compareHorizontally(rectA, rectB);
  }

  /// Compares two rectangles in the screen order solely by their vertical
  /// positions.
  ///
  /// Returns positive if a is lower, negative if a is higher, 0 if their
  /// order can't be determine solely by their vertical position.
  static int _compareVertically(Rect a, Rect b) {
    // The rectangles overlap so defer to horizontal comparison.
    if ((a.top - b.top < _kSelectableVerticalComparingThreshold &&
            a.bottom - b.bottom > -_kSelectableVerticalComparingThreshold) ||
        (b.top - a.top < _kSelectableVerticalComparingThreshold &&
            b.bottom - a.bottom > -_kSelectableVerticalComparingThreshold)) {
      return 0;
    }
    if ((a.top - b.top).abs() > _kSelectableVerticalComparingThreshold) {
      return a.top > b.top ? 1 : -1;
    }
    return a.bottom > b.bottom ? 1 : -1;
  }

  /// Compares two rectangles in the screen order by their horizontal positions
  /// assuming one of the rectangles enclose the other rect vertically.
  ///
  /// Returns positive if a is lower, negative if a is higher.
  static int _compareHorizontally(Rect a, Rect b) {
    // a encloses b.
    if (a.left - b.left < precisionErrorTolerance && a.right - b.right > -precisionErrorTolerance) {
      return -1;
    }
    // b encloses a.
    if (b.left - a.left < precisionErrorTolerance && b.right - a.right > -precisionErrorTolerance) {
      return 1;
    }
    if ((a.left - b.left).abs() > precisionErrorTolerance) {
      return a.left > b.left ? 1 : -1;
    }
    return a.right > b.right ? 1 : -1;
  }

  void _handleSelectableGeometryChange() {
    // Geometries of selectable children may change multiple times when handling
    // selection events. Ignore these updates since the selection geometry of
    // this delegate will be updated after handling the selection events.
    if (_isHandlingSelectionEvent) {
      return;
    }
    _updateSelectionGeometry();
  }

  /// Gets the combined [SelectionGeometry] for child [Selectable]s.
  @protected
  SelectionGeometry getSelectionGeometry() {
    if (currentSelectionEndIndex == -1 || currentSelectionStartIndex == -1 || selectables.isEmpty) {
      // There is no valid selection.
      return SelectionGeometry(status: SelectionStatus.none, hasContent: selectables.isNotEmpty);
    }

    if (!_extendSelectionInProgress) {
      currentSelectionStartIndex = _adjustSelectionIndexBasedOnSelectionGeometry(
        currentSelectionStartIndex,
        currentSelectionEndIndex,
      );
      currentSelectionEndIndex = _adjustSelectionIndexBasedOnSelectionGeometry(
        currentSelectionEndIndex,
        currentSelectionStartIndex,
      );
    }

    // Need to find the non-null start selection point.
    SelectionGeometry startGeometry = selectables[currentSelectionStartIndex].value;
    final bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex;
    int startIndexWalker = currentSelectionStartIndex;
    while (startIndexWalker != currentSelectionEndIndex &&
        startGeometry.startSelectionPoint == null) {
      startIndexWalker += forwardSelection ? 1 : -1;
      startGeometry = selectables[startIndexWalker].value;
    }

    SelectionPoint? startPoint;
    if (startGeometry.startSelectionPoint != null) {
      final Matrix4 startTransform = getTransformFrom(selectables[startIndexWalker]);
      final Offset start = MatrixUtils.transformPoint(
        startTransform,
        startGeometry.startSelectionPoint!.localPosition,
      );
      // It can be NaN if it is detached or off-screen.
      if (start.isFinite) {
        startPoint = SelectionPoint(
          localPosition: start,
          lineHeight: startGeometry.startSelectionPoint!.lineHeight,
          handleType: startGeometry.startSelectionPoint!.handleType,
        );
      }
    }

    // Need to find the non-null end selection point.
    SelectionGeometry endGeometry = selectables[currentSelectionEndIndex].value;
    int endIndexWalker = currentSelectionEndIndex;
    while (endIndexWalker != currentSelectionStartIndex && endGeometry.endSelectionPoint == null) {
      endIndexWalker += forwardSelection ? -1 : 1;
      endGeometry = selectables[endIndexWalker].value;
    }
    SelectionPoint? endPoint;
    if (endGeometry.endSelectionPoint != null) {
      final Matrix4 endTransform = getTransformFrom(selectables[endIndexWalker]);
      final Offset end = MatrixUtils.transformPoint(
        endTransform,
        endGeometry.endSelectionPoint!.localPosition,
      );
      // It can be NaN if it is detached or off-screen.
      if (end.isFinite) {
        endPoint = SelectionPoint(
          localPosition: end,
          lineHeight: endGeometry.endSelectionPoint!.lineHeight,
          handleType: endGeometry.endSelectionPoint!.handleType,
        );
      }
    }

    // Need to collect selection rects from selectables ranging from the
    // currentSelectionStartIndex to the currentSelectionEndIndex.
    final selectionRects = <Rect>[];
    final Rect? drawableArea = hasSize
        ? Rect.fromLTWH(0, 0, containerSize.width, containerSize.height)
        : null;
    for (int index = currentSelectionStartIndex; index <= currentSelectionEndIndex; index++) {
      final List<Rect> currSelectableSelectionRects = selectables[index].value.selectionRects;
      final List<Rect> selectionRectsWithinDrawableArea = currSelectableSelectionRects
          .map((Rect selectionRect) {
            final Matrix4 transform = getTransformFrom(selectables[index]);
            final Rect localRect = MatrixUtils.transformRect(transform, selectionRect);
            return drawableArea?.intersect(localRect) ?? localRect;
          })
          .where((Rect selectionRect) {
            return selectionRect.isFinite && !selectionRect.isEmpty;
          })
          .toList();
      selectionRects.addAll(selectionRectsWithinDrawableArea);
    }

    return SelectionGeometry(
      startSelectionPoint: startPoint,
      endSelectionPoint: endPoint,
      selectionRects: selectionRects,
      status: startGeometry != endGeometry ? SelectionStatus.uncollapsed : startGeometry.status,
      // Would have at least one selectable child.
      hasContent: true,
    );
  }

  // The currentSelectionStartIndex or currentSelectionEndIndex may not be
  // the current index that contains selection edges. This can happen if the
  // selection edge is in between two selectables. One of the selectable will
  // have its selection collapsed at the index 0 or contentLength depends on
  // whether the selection is reversed or not. The current selection index can
  // be point to either one.
  //
  // This method adjusts the index to point to selectable with valid selection.
  int _adjustSelectionIndexBasedOnSelectionGeometry(int currentIndex, int towardIndex) {
    final bool forward = towardIndex > currentIndex;
    while (currentIndex != towardIndex &&
        selectables[currentIndex].value.status != SelectionStatus.uncollapsed) {
      currentIndex += forward ? 1 : -1;
    }
    return currentIndex;
  }

  @override
  void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
    if (_startHandleLayer == startHandle && _endHandleLayer == endHandle) {
      return;
    }
    _startHandleLayer = startHandle;
    _endHandleLayer = endHandle;
    _updateHandleLayersAndOwners();
  }

  /// Pushes both handle layers to the selectables that contain selection edges.
  ///
  /// This method needs to be called every time the selectables that contain the
  /// selection edges change, i.e. [currentSelectionStartIndex] or
  /// [currentSelectionEndIndex] changes. Otherwise, the handle may be painted
  /// in the wrong place.
  void _updateHandleLayersAndOwners() {
    LayerLink? effectiveStartHandle = _startHandleLayer;
    LayerLink? effectiveEndHandle = _endHandleLayer;
    if (effectiveStartHandle != null || effectiveEndHandle != null) {
      final Rect? drawableArea = hasSize
          ? Rect.fromLTWH(
              0,
              0,
              containerSize.width,
              containerSize.height,
            ).inflate(_kSelectionHandleDrawableAreaPadding)
          : null;
      final bool hideStartHandle =
          value.startSelectionPoint == null ||
          drawableArea == null ||
          !drawableArea.contains(value.startSelectionPoint!.localPosition);
      final bool hideEndHandle =
          value.endSelectionPoint == null ||
          drawableArea == null ||
          !drawableArea.contains(value.endSelectionPoint!.localPosition);
      effectiveStartHandle = hideStartHandle ? null : _startHandleLayer;
      effectiveEndHandle = hideEndHandle ? null : _endHandleLayer;
    }
    if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
      // No valid selection.
      if (_startHandleLayerOwner != null) {
        _startHandleLayerOwner!.pushHandleLayers(null, null);
        _startHandleLayerOwner = null;
      }
      if (_endHandleLayerOwner != null) {
        _endHandleLayerOwner!.pushHandleLayers(null, null);
        _endHandleLayerOwner = null;
      }
      return;
    }

    if (selectables[currentSelectionStartIndex] != _startHandleLayerOwner) {
      _startHandleLayerOwner?.pushHandleLayers(null, null);
    }
    if (selectables[currentSelectionEndIndex] != _endHandleLayerOwner) {
      _endHandleLayerOwner?.pushHandleLayers(null, null);
    }

    _startHandleLayerOwner = selectables[currentSelectionStartIndex];

    if (currentSelectionStartIndex == currentSelectionEndIndex) {
      // Selection edges is on the same selectable.
      _endHandleLayerOwner = _startHandleLayerOwner;
      _startHandleLayerOwner!.pushHandleLayers(effectiveStartHandle, effectiveEndHandle);
      return;
    }

    _startHandleLayerOwner!.pushHandleLayers(effectiveStartHandle, null);
    _endHandleLayerOwner = selectables[currentSelectionEndIndex];
    _endHandleLayerOwner!.pushHandleLayers(null, effectiveEndHandle);
  }

  /// Copies the selected contents of all [Selectable]s.
  @override
  SelectedContent? getSelectedContent() {
    final selections = <SelectedContent>[
      for (final Selectable selectable in selectables)
        if (selectable.getSelectedContent() case final SelectedContent data) data,
    ];
    if (selections.isEmpty) {
      return null;
    }
    final buffer = StringBuffer();
    for (final selection in selections) {
      buffer.write(selection.plainText);
    }
    return SelectedContent(plainText: buffer.toString());
  }

  /// The total length of the content under this [SelectionContainerDelegate].
  ///
  /// This value is derived from the [Selectable.contentLength] of each [Selectable]
  /// managed by this delegate.
  @override
  int get contentLength =>
      selectables.fold<int>(0, (int sum, Selectable selectable) => sum + selectable.contentLength);

  /// This method calculates a local [SelectedContentRange] based on the list
  /// of [selections] that are accumulated from the [Selectable] children under this
  /// delegate. This calculation takes into account the accumulated content
  /// length before the active selection, and returns null when either selection
  /// edge has not been set.
  SelectedContentRange? _calculateLocalRange(List<_SelectionInfo> selections) {
    if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
      return null;
    }
    var startOffset = 0;
    var endOffset = 0;
    var foundStart = false;
    bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex;
    if (currentSelectionEndIndex == currentSelectionStartIndex) {
      // Determining selection direction is inaccurate if currentSelectionStartIndex == currentSelectionEndIndex.
      // Use the range from the selectable within the selection as the source of truth for selection direction.
      final SelectedContentRange rangeAtSelectableInSelection =
          selectables[currentSelectionStartIndex].getSelection()!;
      forwardSelection =
          rangeAtSelectableInSelection.endOffset >= rangeAtSelectableInSelection.startOffset;
    }
    for (var index = 0; index < selections.length; index++) {
      final _SelectionInfo selection = selections[index];
      if (selection.range == null) {
        if (foundStart) {
          return SelectedContentRange(
            startOffset: forwardSelection ? startOffset : endOffset,
            endOffset: forwardSelection ? endOffset : startOffset,
          );
        }
        startOffset += selection.contentLength;
        endOffset = startOffset;
        continue;
      }
      final int selectionStartNormalized = min(
        selection.range!.startOffset,
        selection.range!.endOffset,
      );
      final int selectionEndNormalized = max(
        selection.range!.startOffset,
        selection.range!.endOffset,
      );
      if (!foundStart) {
        startOffset += selectionStartNormalized;
        endOffset = startOffset + (selectionEndNormalized - selectionStartNormalized).abs();
        foundStart = true;
      } else {
        endOffset += (selectionEndNormalized - selectionStartNormalized).abs();
      }
    }
    assert(
      foundStart,
      'The start of the selection has not been found despite this selection delegate having an existing currentSelectionStartIndex and currentSelectionEndIndex.',
    );
    return SelectedContentRange(
      startOffset: forwardSelection ? startOffset : endOffset,
      endOffset: forwardSelection ? endOffset : startOffset,
    );
  }

  /// Returns a [SelectedContentRange] considering the [SelectedContentRange]
  /// from each [Selectable] child managed under this delegate.
  ///
  /// When nothing is selected or either selection edge has not been set,
  /// this method will return `null`.
  @override
  SelectedContentRange? getSelection() {
    final selections = <_SelectionInfo>[
      for (final Selectable selectable in selectables)
        (contentLength: selectable.contentLength, range: selectable.getSelection()),
    ];
    return _calculateLocalRange(selections);
  }

  // Clears the selection on all selectables not in the range of
  // currentSelectionStartIndex..currentSelectionEndIndex.
  //
  // If one of the edges does not exist, then this method will clear the selection
  // in all selectables except the existing edge.
  //
  // If neither of the edges exist this method immediately returns.
  void _flushInactiveSelections() {
    if (currentSelectionStartIndex == -1 && currentSelectionEndIndex == -1) {
      return;
    }
    if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
      final int skipIndex = currentSelectionStartIndex == -1
          ? currentSelectionEndIndex
          : currentSelectionStartIndex;
      selectables
          .where((Selectable target) => target != selectables[skipIndex])
          .forEach(
            (Selectable target) =>
                dispatchSelectionEventToChild(target, const ClearSelectionEvent()),
          );
      return;
    }
    final int skipStart = min(currentSelectionStartIndex, currentSelectionEndIndex);
    final int skipEnd = max(currentSelectionStartIndex, currentSelectionEndIndex);
    for (var index = 0; index < selectables.length; index += 1) {
      if (index >= skipStart && index <= skipEnd) {
        continue;
      }
      dispatchSelectionEventToChild(selectables[index], const ClearSelectionEvent());
    }
  }

  /// Selects all contents of all [Selectable]s.
  @protected
  SelectionResult handleSelectAll(SelectAllSelectionEvent event) {
    for (final Selectable selectable in selectables) {
      dispatchSelectionEventToChild(selectable, event);
    }
    currentSelectionStartIndex = 0;
    currentSelectionEndIndex = selectables.length - 1;
    return SelectionResult.none;
  }

  SelectionResult _handleSelectBoundary(SelectionEvent event) {
    assert(
      event is SelectWordSelectionEvent || event is SelectParagraphSelectionEvent,
      'This method should only be given selection events that select text boundaries.',
    );
    late final Offset effectiveGlobalPosition;
    if (event.type == SelectionEventType.selectWord) {
      effectiveGlobalPosition = (event as SelectWordSelectionEvent).globalPosition;
    } else if (event.type == SelectionEventType.selectParagraph) {
      effectiveGlobalPosition = (event as SelectParagraphSelectionEvent).globalPosition;
    }
    SelectionResult? lastSelectionResult;
    for (var index = 0; index < selectables.length; index += 1) {
      var globalRectsContainPosition = false;
      if (selectables[index].boundingBoxes.isNotEmpty) {
        for (final Rect rect in selectables[index].boundingBoxes) {
          final Rect globalRect = MatrixUtils.transformRect(
            selectables[index].getTransformTo(null),
            rect,
          );
          if (globalRect.contains(effectiveGlobalPosition)) {
            globalRectsContainPosition = true;
            break;
          }
        }
      }
      if (globalRectsContainPosition) {
        final SelectionGeometry existingGeometry = selectables[index].value;
        lastSelectionResult = dispatchSelectionEventToChild(selectables[index], event);
        if (index == selectables.length - 1 && lastSelectionResult == SelectionResult.next) {
          return SelectionResult.next;
        }
        if (lastSelectionResult == SelectionResult.next) {
          continue;
        }
        if (index == 0 && lastSelectionResult == SelectionResult.previous) {
          return SelectionResult.previous;
        }
        if (selectables[index].value != existingGeometry) {
          // Geometry has changed as a result of select word, need to clear the
          // selection of other selectables to keep selection in sync.
          selectables
              .where((Selectable target) => target != selectables[index])
              .forEach(
                (Selectable target) =>
                    dispatchSelectionEventToChild(target, const ClearSelectionEvent()),
              );
          currentSelectionStartIndex = currentSelectionEndIndex = index;
        }
        return SelectionResult.end;
      } else {
        if (lastSelectionResult == SelectionResult.next) {
          currentSelectionStartIndex = currentSelectionEndIndex = index - 1;
          return SelectionResult.end;
        }
      }
    }
    assert(lastSelectionResult == null);
    return SelectionResult.end;
  }

  /// Selects a word in a [Selectable] at the location
  /// [SelectWordSelectionEvent.globalPosition].
  @protected
  SelectionResult handleSelectWord(SelectWordSelectionEvent event) {
    return _handleSelectBoundary(event);
  }

  /// Selects a paragraph in a [Selectable] at the location
  /// [SelectParagraphSelectionEvent.globalPosition].
  @protected
  SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
    return _handleSelectBoundary(event);
  }

  /// Removes the selection of all [Selectable]s this delegate manages.
  @protected
  SelectionResult handleClearSelection(ClearSelectionEvent event) {
    for (final Selectable selectable in selectables) {
      dispatchSelectionEventToChild(selectable, event);
    }
    currentSelectionEndIndex = -1;
    currentSelectionStartIndex = -1;
    return SelectionResult.none;
  }

  /// Extend current selection in a certain [TextGranularity].
  @protected
  SelectionResult handleGranularlyExtendSelection(GranularlyExtendSelectionEvent event) {
    assert((currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1));
    if (currentSelectionStartIndex == -1) {
      if (event.forward) {
        currentSelectionStartIndex = currentSelectionEndIndex = 0;
      } else {
        currentSelectionStartIndex = currentSelectionEndIndex = selectables.length - 1;
      }
    }
    int targetIndex = event.isEnd ? currentSelectionEndIndex : currentSelectionStartIndex;
    SelectionResult result = dispatchSelectionEventToChild(selectables[targetIndex], event);
    if (event.forward) {
      assert(result != SelectionResult.previous);
      while (targetIndex < selectables.length - 1 && result == SelectionResult.next) {
        targetIndex += 1;
        result = dispatchSelectionEventToChild(selectables[targetIndex], event);
        assert(result != SelectionResult.previous);
      }
    } else {
      assert(result != SelectionResult.next);
      while (targetIndex > 0 && result == SelectionResult.previous) {
        targetIndex -= 1;
        result = dispatchSelectionEventToChild(selectables[targetIndex], event);
        assert(result != SelectionResult.next);
      }
    }
    if (event.isEnd) {
      currentSelectionEndIndex = targetIndex;
    } else {
      currentSelectionStartIndex = targetIndex;
    }
    return result;
  }

  /// Extend current selection in a certain [TextGranularity].
  @protected
  SelectionResult handleDirectionallyExtendSelection(DirectionallyExtendSelectionEvent event) {
    assert((currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1));
    if (currentSelectionStartIndex == -1) {
      currentSelectionStartIndex = currentSelectionEndIndex = switch (event.direction) {
        SelectionExtendDirection.previousLine ||
        SelectionExtendDirection.backward => selectables.length - 1,
        SelectionExtendDirection.nextLine || SelectionExtendDirection.forward => 0,
      };
    }
    int targetIndex = event.isEnd ? currentSelectionEndIndex : currentSelectionStartIndex;
    SelectionResult result = dispatchSelectionEventToChild(selectables[targetIndex], event);
    switch (event.direction) {
      case SelectionExtendDirection.previousLine:
        assert(result == SelectionResult.end || result == SelectionResult.previous);
        if (result == SelectionResult.previous) {
          if (targetIndex > 0) {
            targetIndex -= 1;
            result = dispatchSelectionEventToChild(
              selectables[targetIndex],
              event.copyWith(direction: SelectionExtendDirection.backward),
            );
            assert(result == SelectionResult.end);
          }
        }
      case SelectionExtendDirection.nextLine:
        assert(result == SelectionResult.end || result == SelectionResult.next);
        if (result == SelectionResult.next) {
          if (targetIndex < selectables.length - 1) {
            targetIndex += 1;
            result = dispatchSelectionEventToChild(
              selectables[targetIndex],
              event.copyWith(direction: SelectionExtendDirection.forward),
            );
            assert(result == SelectionResult.end);
          }
        }
      case SelectionExtendDirection.forward:
      case SelectionExtendDirection.backward:
        assert(result == SelectionResult.end);
    }
    if (event.isEnd) {
      currentSelectionEndIndex = targetIndex;
    } else {
      currentSelectionStartIndex = targetIndex;
    }
    return result;
  }

  /// Updates the selection edges.
  @protected
  SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
    if (event.type == SelectionEventType.endEdgeUpdate) {
      return currentSelectionEndIndex == -1
          ? _initSelection(event, isEnd: true)
          : _adjustSelection(event, isEnd: true);
    }
    return currentSelectionStartIndex == -1
        ? _initSelection(event, isEnd: false)
        : _adjustSelection(event, isEnd: false);
  }

  @override
  SelectionResult dispatchSelectionEvent(SelectionEvent event) {
    final selectionWillBeInProgress = event is! ClearSelectionEvent;
    if (!_selectionInProgress && selectionWillBeInProgress) {
      // Sort the selectable every time a selection start.
      selectables.sort(compareOrder);
    }
    _selectionInProgress = selectionWillBeInProgress;
    _isHandlingSelectionEvent = true;
    late SelectionResult result;
    switch (event.type) {
      case SelectionEventType.startEdgeUpdate:
      case SelectionEventType.endEdgeUpdate:
        _extendSelectionInProgress = false;
        result = handleSelectionEdgeUpdate(event as SelectionEdgeUpdateEvent);
      case SelectionEventType.clear:
        _extendSelectionInProgress = false;
        result = handleClearSelection(event as ClearSelectionEvent);
      case SelectionEventType.selectAll:
        _extendSelectionInProgress = false;
        result = handleSelectAll(event as SelectAllSelectionEvent);
      case SelectionEventType.selectWord:
        _extendSelectionInProgress = false;
        result = handleSelectWord(event as SelectWordSelectionEvent);
      case SelectionEventType.selectParagraph:
        _extendSelectionInProgress = false;
        result = handleSelectParagraph(event as SelectParagraphSelectionEvent);
      case SelectionEventType.granularlyExtendSelection:
        _extendSelectionInProgress = true;
        result = handleGranularlyExtendSelection(event as GranularlyExtendSelectionEvent);
      case SelectionEventType.directionallyExtendSelection:
        _extendSelectionInProgress = true;
        result = handleDirectionallyExtendSelection(event as DirectionallyExtendSelectionEvent);
    }
    _isHandlingSelectionEvent = false;
    _updateSelectionGeometry();
    return result;
  }

  @override
  void dispose() {
    for (final Selectable selectable in selectables) {
      selectable.removeListener(_handleSelectableGeometryChange);
    }
    selectables = const <Selectable>[];
    _scheduledSelectableUpdate = false;
    super.dispose();
  }

  /// Ensures the [Selectable] child has received up to date selection event.
  ///
  /// This method is called when a new [Selectable] is added to the delegate,
  /// and its screen location falls into the previous selection.
  ///
  /// Subclasses are responsible for updating the selection of this newly added
  /// [Selectable].
  @protected
  void ensureChildUpdated(Selectable selectable);

  /// Dispatches a selection event to a specific [Selectable].
  ///
  /// Override this method if subclasses need to generate additional events or
  /// treatments prior to sending the [SelectionEvent].
  @protected
  SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
    return selectable.dispatchSelectionEvent(event);
  }

  /// Initializes the selection of the selectable children.
  ///
  /// The goal is to find the selectable child that contains the selection edge.
  /// Returns [SelectionResult.end] if the selection edge ends on any of the
  /// children. Otherwise, it returns [SelectionResult.previous] if the selection
  /// does not reach any of its children. Returns [SelectionResult.next]
  /// if the selection reaches the end of its children.
  ///
  /// Ideally, this method should only be called twice at the beginning of the
  /// drag selection, once for start edge update event, once for end edge update
  /// event.
  SelectionResult _initSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) {
    assert(
      (isEnd && currentSelectionEndIndex == -1) || (!isEnd && currentSelectionStartIndex == -1),
    );
    var newIndex = -1;
    var hasFoundEdgeIndex = false;
    SelectionResult? result;
    for (var index = 0; index < selectables.length && !hasFoundEdgeIndex; index += 1) {
      final Selectable child = selectables[index];
      final SelectionResult childResult = dispatchSelectionEventToChild(child, event);
      switch (childResult) {
        case SelectionResult.next:
        case SelectionResult.none:
          newIndex = index;
        case SelectionResult.end:
          newIndex = index;
          result = SelectionResult.end;
          hasFoundEdgeIndex = true;
        case SelectionResult.previous:
          hasFoundEdgeIndex = true;
          if (index == 0) {
            newIndex = 0;
            result = SelectionResult.previous;
          }
          result ??= SelectionResult.end;
        case SelectionResult.pending:
          newIndex = index;
          result = SelectionResult.pending;
          hasFoundEdgeIndex = true;
      }
    }

    if (newIndex == -1) {
      assert(selectables.isEmpty);
      return SelectionResult.none;
    }
    if (isEnd) {
      currentSelectionEndIndex = newIndex;
    } else {
      currentSelectionStartIndex = newIndex;
    }
    _flushInactiveSelections();
    // The result can only be null if the loop went through the entire list
    // without any of the selection returned end or previous. In this case, the
    // caller of this method needs to find the next selectable in their list.
    return result ?? SelectionResult.next;
  }

  /// Adjusts the selection based on the drag selection update event if there
  /// is already a selectable child that contains the selection edge.
  ///
  /// This method starts by sending the selection event to the current
  /// selectable that contains the selection edge, and finds forward or backward
  /// if that selectable no longer contains the selection edge.
  SelectionResult _adjustSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) {
    assert(() {
      if (isEnd) {
        assert(currentSelectionEndIndex < selectables.length && currentSelectionEndIndex >= 0);
        return true;
      }
      assert(currentSelectionStartIndex < selectables.length && currentSelectionStartIndex >= 0);
      return true;
    }());
    SelectionResult? finalResult;
    // Determines if the edge being adjusted is within the current viewport.
    //  - If so, we begin the search for the new selection edge position at the
    //    currentSelectionEndIndex/currentSelectionStartIndex.
    //  - If not, we attempt to locate the new selection edge starting from
    //    the opposite end.
    //  - If neither edge is in the current viewport, the search for the new
    //    selection edge position begins at 0.
    //
    // This can happen when there is a scrollable child and the edge being adjusted
    // has been scrolled out of view.
    final isCurrentEdgeWithinViewport = isEnd
        ? _selectionGeometry.endSelectionPoint != null
        : _selectionGeometry.startSelectionPoint != null;
    final isOppositeEdgeWithinViewport = isEnd
        ? _selectionGeometry.startSelectionPoint != null
        : _selectionGeometry.endSelectionPoint != null;
    int newIndex = switch ((isEnd, isCurrentEdgeWithinViewport, isOppositeEdgeWithinViewport)) {
      (true, true, true) => currentSelectionEndIndex,
      (true, true, false) => currentSelectionEndIndex,
      (true, false, true) => currentSelectionStartIndex,
      (true, false, false) => 0,
      (false, true, true) => currentSelectionStartIndex,
      (false, true, false) => currentSelectionStartIndex,
      (false, false, true) => currentSelectionEndIndex,
      (false, false, false) => 0,
    };
    bool? forward;
    late SelectionResult currentSelectableResult;
    // This loop sends the selection event to one of the following to determine
    // the direction of the search.
    //  - currentSelectionEndIndex/currentSelectionStartIndex if the current edge
    //    is in the current viewport.
    //  - The opposite edge index if the current edge is not in the current viewport.
    //  - Index 0 if neither edge is in the current viewport.
    //
    // If the result is `SelectionResult.next`, this loop look backward.
    // Otherwise, it looks forward.
    //
    // The terminate condition are:
    // 1. the selectable returns end, pending, none.
    // 2. the selectable returns previous when looking forward.
    // 2. the selectable returns next when looking backward.
    while (newIndex < selectables.length && newIndex >= 0 && finalResult == null) {
      currentSelectableResult = dispatchSelectionEventToChild(selectables[newIndex], event);
      switch (currentSelectableResult) {
        case SelectionResult.end:
        case SelectionResult.pending:
        case SelectionResult.none:
          finalResult = currentSelectableResult;
        case SelectionResult.next:
          if (forward == false) {
            newIndex += 1;
            finalResult = SelectionResult.end;
          } else if (newIndex == selectables.length - 1) {
            finalResult = currentSelectableResult;
          } else {
            forward = true;
            newIndex += 1;
          }
        case SelectionResult.previous:
          if (forward ?? false) {
            newIndex -= 1;
            finalResult = SelectionResult.end;
          } else if (newIndex == 0) {
            finalResult = currentSelectableResult;
          } else {
            forward = false;
            newIndex -= 1;
          }
      }
    }
    if (isEnd) {
      currentSelectionEndIndex = newIndex;
    } else {
      currentSelectionStartIndex = newIndex;
    }
    _flushInactiveSelections();
    return finalResult!;
  }
}

/// The length of the content that can be selected, and the range that is
/// selected.
typedef _SelectionInfo = ({int contentLength, SelectedContentRange? range});

/// 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);

/// The status of the selection under a [SelectableRegion].
///
/// This value can be accessed for a [SelectableRegion] by using
/// [SelectableRegionSelectionStatusScope.maybeOf].
///
/// This value under a [SelectableRegion] is updated frequently
/// during selection gestures such as clicks and taps to select
/// and keyboard shortcuts.
enum SelectableRegionSelectionStatus {
  /// Indicates that the selection under a [SelectableRegion] is changing.
  ///
  /// A [SelectableRegion]s selection is changing when it is being
  /// updated by user through selection gestures and keyboard shortcuts.
  /// For example, during a text selection drag with a click + drag,
  /// a [SelectableRegion]s selection is considered changing until
  /// the user releases the click, then it will be considered finalized.
  changing,

  /// Indicates that the selection under a [SelectableRegion] is finalized.
  ///
  /// A [SelectableRegion]s selection is finalized when it is no longer
  /// being updated by the user through selection gestures or keyboard
  /// shortcuts. For example, the selection will be finalized on a mouse
  /// drag end, touch long press drag end, a single click to collapse the
  /// selection, a double click/tap to select a word, ctrl + A / cmd + A to
  /// select all, or a triple click/tap to select a paragraph.
  finalized,
}

/// 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<SelectableRegionSelectionStatus> {
  _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<SelectableRegionSelectionStatus> 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<SelectableRegionSelectionStatus>? maybeOf(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<SelectableRegionSelectionStatusScope>()
        ?.selectionStatusNotifier;
  }

  @override
  bool updateShouldNotify(SelectableRegionSelectionStatusScope oldWidget) {
    return selectionStatusNotifier != oldWidget.selectionStatusNotifier;
  }
}

/// A [SelectionContainer] that allows the user to access the [SelectionDetails] and
/// listen to selection changes for the child subtree it wraps under a [SelectionArea]
/// or [SelectableRegion].
///
/// The selection updates are provided through the [selectionNotifier], to listen
/// to these updates attach a listener through [SelectionListenerNotifier.addListener].
///
/// This widget does not listen to selection changes of nested [SelectionArea]s
/// or [SelectableRegion]s in its subtree because those widgets are self-contained
/// and do not bubble up their selection. To listen to selection changes of a
/// [SelectionArea] or [SelectableRegion] under this [SelectionListener], add
/// an additional [SelectionListener] under each one.
///
/// {@tool dartpad}
/// This example shows how to use [SelectionListener] to access the [SelectionDetails]
/// under a [SelectionArea] or [SelectableRegion].
///
/// ** See code in examples/api/lib/material/selection_area/selection_area.1.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows how to color the active selection red under a
/// [SelectionArea] or [SelectableRegion].
///
/// ** See code in examples/api/lib/material/selection_area/selection_area.2.dart **
/// {@end-tool}
///
/// See also:
///
///   * [SelectableRegion], which provides an overview of the selection system.
class SelectionListener extends StatefulWidget {
  /// Create a new [SelectionListener] widget.
  const SelectionListener({super.key, required this.selectionNotifier, required this.child});

  /// Notifies listeners when the selection has changed.
  final SelectionListenerNotifier selectionNotifier;

  /// The child widget this selection listener applies to.
  ///
  /// {@macro flutter.widgets.ProxyWidget.child}
  final Widget child;

  @override
  State<SelectionListener> createState() => _SelectionListenerState();
}

class _SelectionListenerState extends State<SelectionListener> {
  late final _SelectionListenerDelegate _selectionDelegate = _SelectionListenerDelegate(
    selectionNotifier: widget.selectionNotifier,
  );

  @override
  void didUpdateWidget(SelectionListener oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.selectionNotifier != widget.selectionNotifier) {
      _selectionDelegate._setNotifier(widget.selectionNotifier);
    }
  }

  @override
  void dispose() {
    _selectionDelegate.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SelectionContainer(delegate: _selectionDelegate, child: widget.child);
  }
}

final class _SelectionListenerDelegate extends StaticSelectionContainerDelegate
    implements SelectionDetails {
  _SelectionListenerDelegate({required SelectionListenerNotifier selectionNotifier})
    : _selectionNotifier = selectionNotifier {
    _selectionNotifier._registerSelectionListenerDelegate(this);
  }

  SelectionGeometry? _initialSelectionGeometry;

  SelectionListenerNotifier _selectionNotifier;
  void _setNotifier(SelectionListenerNotifier newNotifier) {
    _selectionNotifier._unregisterSelectionListenerDelegate();
    _selectionNotifier = newNotifier;
    _selectionNotifier._registerSelectionListenerDelegate(this);
  }

  @override
  void notifyListeners() {
    super.notifyListeners();
    // Skip initial notification if selection is not valid.
    if (_initialSelectionGeometry == null && !value.hasSelection) {
      _initialSelectionGeometry = value;
      return;
    }
    _selectionNotifier.notifyListeners();
  }

  @override
  void dispose() {
    _selectionNotifier._unregisterSelectionListenerDelegate();
    _initialSelectionGeometry = null;
    super.dispose();
  }

  @override
  SelectedContentRange? get range => getSelection();

  @override
  SelectionStatus get status => value.status;
}

/// A read-only interface for accessing the details of a selection under a [SelectionListener].
///
/// This includes information such as the status of the selection indicating
/// if it is collapsed or uncollapsed, the [SelectedContentRange] that includes
/// the start and end offsets of the selection local to the [SelectionListener]
/// that reports this object.
///
/// This object is typically accessed by providing a [SelectionListenerNotifier]
/// to a [SelectionListener] and retrieving the value from [SelectionListenerNotifier.selection].
abstract final class SelectionDetails {
  /// The computed selection range of the owning [SelectionListener]s subtree.
  ///
  /// Returns `null` if there is nothing selected.
  SelectedContentRange? get range;

  /// The status that indicates whether there is a selection and whether the selection is collapsed.
  SelectionStatus get status;
}

/// Notifies listeners when the selection under a [SelectionListener] has been
/// changed.
///
/// This object is typically provided to a [SelectionListener].
final class SelectionListenerNotifier extends ChangeNotifier {
  _SelectionListenerDelegate? _selectionDelegate;

  /// The details of the selection under the [SelectionListener] that owns this notifier.
  ///
  /// Throws an exception if this notifier has not been registered to a [SelectionListener].
  /// To check if a notifier has been registered to a [SelectionListener] use [registered].
  SelectionDetails get selection =>
      _selectionDelegate ??
      (throw Exception('Selection client has not been registered to this notifier.'));

  /// Whether this [SelectionListenerNotifier] has been registered to a [SelectionListener].
  bool get registered => _selectionDelegate != null;

  void _registerSelectionListenerDelegate(_SelectionListenerDelegate selectionDelegate) {
    assert(
      !registered,
      'This SelectionListenerNotifier is already registered to another SelectionListener. Try providing a new SelectionListenerNotifier.',
    );
    _selectionDelegate = selectionDelegate;
  }

  void _unregisterSelectionListenerDelegate() {
    _selectionDelegate = null;
  }

  // From ChangeNotifier.
  @override
  void dispose() {
    _unregisterSelectionListenerDelegate();
    super.dispose();
  }

  /// Calls the listener every time the [SelectionGeometry] of the selection changes under
  /// a [SelectionListener].
  ///
  /// Listeners can be removed with [removeListener].
  @override
  void addListener(VoidCallback listener) {
    super.addListener(listener);
  }
}
