1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/material.dart';
6/// @docImport 'package:flutter_test/flutter_test.dart';
7///
8/// @docImport 'editable_text.dart';
9/// @docImport 'text.dart';
10library;
11
12import 'dart:async';
13import 'dart:math';
14
15import 'package:flutter/foundation.dart';
16import 'package:flutter/gestures.dart';
17import 'package:flutter/rendering.dart';
18import 'package:flutter/scheduler.dart';
19import 'package:flutter/services.dart';
20import 'package:vector_math/vector_math_64.dart';
21
22import 'actions.dart';
23import 'basic.dart';
24import 'context_menu_button_item.dart';
25import 'debug.dart';
26import 'focus_manager.dart';
27import 'focus_scope.dart';
28import 'framework.dart';
29import 'gesture_detector.dart';
30import 'magnifier.dart';
31import 'media_query.dart';
32import 'overlay.dart';
33import 'platform_selectable_region_context_menu.dart';
34import 'selection_container.dart';
35import 'text_editing_intents.dart';
36import 'text_selection.dart';
37import 'text_selection_toolbar_anchors.dart';
38
39// Examples can assume:
40// FocusNode _focusNode = FocusNode();
41// late GlobalKey key;
42
43const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{
44 PointerDeviceKind.touch,
45 PointerDeviceKind.stylus,
46 PointerDeviceKind.invertedStylus,
47};
48
49// In practice some selectables like widgetspan shift several pixels. So when
50// the vertical position diff is within the threshold, compare the horizontal
51// position to make the compareScreenOrder function more robust.
52const double _kSelectableVerticalComparingThreshold = 3.0;
53
54/// A widget that introduces an area for user selections.
55///
56/// Flutter widgets are not selectable by default. Wrapping a widget subtree
57/// with a [SelectableRegion] widget enables selection within that subtree (for
58/// example, [Text] widgets automatically look for selectable regions to enable
59/// selection). The wrapped subtree can be selected by users using mouse or
60/// touch gestures, e.g. users can select widgets by holding the mouse
61/// left-click and dragging across widgets, or they can use long press gestures
62/// to select words on touch devices.
63///
64/// A [SelectableRegion] widget requires configuration; in particular specific
65/// [selectionControls] must be provided.
66///
67/// The [SelectionArea] widget from the [material] library configures a
68/// [SelectableRegion] in a platform-specific manner (e.g. using a Material
69/// toolbar on Android, a Cupertino toolbar on iOS), and it may therefore be
70/// simpler to use that widget rather than using [SelectableRegion] directly.
71///
72/// ## An overview of the selection system.
73///
74/// Every [Selectable] under the [SelectableRegion] can be selected. They form a
75/// selection tree structure to handle the selection.
76///
77/// The [SelectableRegion] is a wrapper over [SelectionContainer]. It listens to
78/// user gestures and sends corresponding [SelectionEvent]s to the
79/// [SelectionContainer] it creates.
80///
81/// A [SelectionContainer] is a single [Selectable] that handles
82/// [SelectionEvent]s on behalf of child [Selectable]s in the subtree. It
83/// creates a [SelectionRegistrarScope] with its [SelectionContainer.delegate]
84/// to collect child [Selectable]s and sends the [SelectionEvent]s it receives
85/// from the parent [SelectionRegistrar] to the appropriate child [Selectable]s.
86/// It creates an abstraction for the parent [SelectionRegistrar] as if it is
87/// interacting with a single [Selectable].
88///
89/// The [SelectionContainer] created by [SelectableRegion] is the root node of a
90/// selection tree. Each non-leaf node in the tree is a [SelectionContainer],
91/// and the leaf node is a leaf widget whose render object implements
92/// [Selectable]. They are connected through [SelectionRegistrarScope]s created
93/// by [SelectionContainer]s.
94///
95/// Both [SelectionContainer]s and the leaf [Selectable]s need to register
96/// themselves to the [SelectionRegistrar] from the
97/// [SelectionContainer.maybeOf] if they want to participate in the
98/// selection.
99///
100/// An example selection tree will look like:
101///
102/// {@tool snippet}
103///
104/// ```dart
105/// MaterialApp(
106/// home: SelectableRegion(
107/// selectionControls: materialTextSelectionControls,
108/// focusNode: _focusNode, // initialized to FocusNode()
109/// child: Scaffold(
110/// appBar: AppBar(title: const Text('Flutter Code Sample')),
111/// body: ListView(
112/// children: const <Widget>[
113/// Text('Item 0', style: TextStyle(fontSize: 50.0)),
114/// Text('Item 1', style: TextStyle(fontSize: 50.0)),
115/// ],
116/// ),
117/// ),
118/// ),
119/// )
120/// ```
121/// {@end-tool}
122///
123///
124/// SelectionContainer
125/// (SelectableRegion)
126/// / \
127/// / \
128/// / \
129/// Selectable \
130/// ("Flutter Code Sample") \
131/// \
132/// SelectionContainer
133/// (ListView)
134/// / \
135/// / \
136/// / \
137/// Selectable Selectable
138/// ("Item 0") ("Item 1")
139///
140///
141/// ## Making a widget selectable
142///
143/// Some leaf widgets, such as [Text], have all of the selection logic wired up
144/// automatically and can be selected as long as they are under a
145/// [SelectableRegion].
146///
147/// To make a custom selectable widget, its render object needs to mix in
148/// [Selectable] and implement the required APIs to handle [SelectionEvent]s
149/// as well as paint appropriate selection highlights.
150///
151/// The render object also needs to register itself to a [SelectionRegistrar].
152/// For the most cases, one can use [SelectionRegistrant] to auto-register
153/// itself with the register returned from [SelectionContainer.maybeOf] as
154/// seen in the example below.
155///
156/// {@tool dartpad}
157/// This sample demonstrates how to create an adapter widget that makes any
158/// child widget selectable.
159///
160/// ** See code in examples/api/lib/material/selectable_region/selectable_region.0.dart **
161/// {@end-tool}
162///
163/// ## Complex layout
164///
165/// By default, the screen order is used as the selection order. If a group of
166/// [Selectable]s needs to select differently, consider wrapping them with a
167/// [SelectionContainer] to customize its selection behavior.
168///
169/// {@tool dartpad}
170/// This sample demonstrates how to create a [SelectionContainer] that only
171/// allows selecting everything or nothing with no partial selection.
172///
173/// ** See code in examples/api/lib/material/selection_container/selection_container.0.dart **
174/// {@end-tool}
175///
176/// In the case where a group of widgets should be excluded from selection under
177/// a [SelectableRegion], consider wrapping that group of widgets using
178/// [SelectionContainer.disabled].
179///
180/// {@tool dartpad}
181/// This sample demonstrates how to disable selection for a Text in a Column.
182///
183/// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart **
184/// {@end-tool}
185///
186/// To create a separate selection system from its parent selection area,
187/// wrap part of the subtree with another [SelectableRegion]. The selection of the
188/// child selection area can not extend past its subtree, and the selection of
189/// the parent selection area can not extend inside the child selection area.
190///
191/// ## Tests
192///
193/// In a test, a region can be selected either by faking drag events (e.g. using
194/// [WidgetTester.dragFrom]) or by sending intents to a widget inside the region
195/// that has been given a [GlobalKey], e.g.:
196///
197/// ```dart
198/// Actions.invoke(key.currentContext!, const SelectAllTextIntent(SelectionChangedCause.keyboard));
199/// ```
200///
201/// See also:
202///
203/// * [SelectionArea], which creates a [SelectableRegion] with
204/// platform-adaptive selection controls.
205/// * [SelectableText], which enables selection on a single run of text.
206/// * [SelectionHandler], which contains APIs to handle selection events from the
207/// [SelectableRegion].
208/// * [Selectable], which provides API to participate in the selection system.
209/// * [SelectionRegistrar], which [Selectable] needs to subscribe to receive
210/// selection events.
211/// * [SelectionContainer], which collects selectable widgets in the subtree
212/// and provides api to dispatch selection event to the collected widget.
213class SelectableRegion extends StatefulWidget {
214 /// Create a new [SelectableRegion] widget.
215 ///
216 /// The [selectionControls] are used for building the selection handles and
217 /// toolbar for mobile devices.
218 const SelectableRegion({
219 super.key,
220 this.contextMenuBuilder,
221 required this.focusNode,
222 required this.selectionControls,
223 required this.child,
224 this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
225 this.onSelectionChanged,
226 });
227
228 /// The configuration for the magnifier used with selections in this region.
229 ///
230 /// By default, [SelectableRegion]'s [TextMagnifierConfiguration] is disabled.
231 /// For a version of [SelectableRegion] that adapts automatically to the
232 /// current platform, consider [SelectionArea].
233 ///
234 /// {@macro flutter.widgets.magnifier.intro}
235 final TextMagnifierConfiguration magnifierConfiguration;
236
237 /// {@macro flutter.widgets.Focus.focusNode}
238 final FocusNode focusNode;
239
240 /// The child widget this selection area applies to.
241 ///
242 /// {@macro flutter.widgets.ProxyWidget.child}
243 final Widget child;
244
245 /// {@macro flutter.widgets.EditableText.contextMenuBuilder}
246 final SelectableRegionContextMenuBuilder? contextMenuBuilder;
247
248 /// The delegate to build the selection handles and toolbar for mobile
249 /// devices.
250 ///
251 /// The [emptyTextSelectionControls] global variable provides a default
252 /// [TextSelectionControls] implementation with no controls.
253 final TextSelectionControls selectionControls;
254
255 /// Called when the selected content changes.
256 final ValueChanged<SelectedContent?>? onSelectionChanged;
257
258 /// Returns the [ContextMenuButtonItem]s representing the buttons in this
259 /// platform's default selection menu.
260 ///
261 /// For example, [SelectableRegion] uses this to generate the default buttons
262 /// for its context menu.
263 ///
264 /// See also:
265 ///
266 /// * [SelectableRegionState.contextMenuButtonItems], which gives the
267 /// [ContextMenuButtonItem]s for a specific SelectableRegion.
268 /// * [EditableText.getEditableButtonItems], which performs a similar role but
269 /// for content that is both selectable and editable.
270 /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can
271 /// take a list of [ContextMenuButtonItem]s with
272 /// [AdaptiveTextSelectionToolbar.buttonItems].
273 /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button
274 /// Widgets for the current platform given [ContextMenuButtonItem]s.
275 static List<ContextMenuButtonItem> getSelectableButtonItems({
276 required final SelectionGeometry selectionGeometry,
277 required final VoidCallback onCopy,
278 required final VoidCallback onSelectAll,
279 required final VoidCallback? onShare,
280 }) {
281 final bool canCopy = selectionGeometry.status == SelectionStatus.uncollapsed;
282 final bool canSelectAll = selectionGeometry.hasContent;
283 final bool platformCanShare = switch (defaultTargetPlatform) {
284 TargetPlatform.android
285 => selectionGeometry.status == SelectionStatus.uncollapsed,
286 TargetPlatform.macOS
287 || TargetPlatform.fuchsia
288 || TargetPlatform.linux
289 || TargetPlatform.windows
290 => false,
291 // TODO(bleroux): the share button should be shown on iOS but the share
292 // functionality requires some changes on the engine side because, on iPad,
293 // it needs an anchor for the popup.
294 // See: https://github.com/flutter/flutter/issues/141775.
295 TargetPlatform.iOS
296 => false,
297 };
298 final bool canShare = onShare != null && platformCanShare;
299
300 // On Android, the share button is before the select all button.
301 final bool showShareBeforeSelectAll = defaultTargetPlatform == TargetPlatform.android;
302
303 // Determine which buttons will appear so that the order and total number is
304 // known. A button's position in the menu can slightly affect its
305 // appearance.
306 return <ContextMenuButtonItem>[
307 if (canCopy)
308 ContextMenuButtonItem(
309 onPressed: onCopy,
310 type: ContextMenuButtonType.copy,
311 ),
312 if (canShare && showShareBeforeSelectAll)
313 ContextMenuButtonItem(
314 onPressed: onShare,
315 type: ContextMenuButtonType.share,
316 ),
317 if (canSelectAll)
318 ContextMenuButtonItem(
319 onPressed: onSelectAll,
320 type: ContextMenuButtonType.selectAll,
321 ),
322 if (canShare && !showShareBeforeSelectAll)
323 ContextMenuButtonItem(
324 onPressed: onShare,
325 type: ContextMenuButtonType.share,
326 ),
327 ];
328 }
329
330 @override
331 State<StatefulWidget> createState() => SelectableRegionState();
332}
333
334/// State for a [SelectableRegion].
335class SelectableRegionState extends State<SelectableRegion> with TextSelectionDelegate implements SelectionRegistrar {
336 late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
337 SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
338 CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
339 ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_GranularlyExtendSelectionAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(this, granularity: TextGranularity.word)),
340 ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(_GranularlyExtendSelectionAction<ExpandSelectionToDocumentBoundaryIntent>(this, granularity: TextGranularity.document)),
341 ExpandSelectionToLineBreakIntent: _makeOverridable(_GranularlyExtendSelectionAction<ExpandSelectionToLineBreakIntent>(this, granularity: TextGranularity.line)),
342 ExtendSelectionByCharacterIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionByCharacterIntent>(this, granularity: TextGranularity.character)),
343 ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, granularity: TextGranularity.word)),
344 ExtendSelectionToLineBreakIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionToLineBreakIntent>(this, granularity: TextGranularity.line)),
345 ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_DirectionallyExtendCaretSelectionAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this)),
346 ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_GranularlyExtendCaretSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, granularity: TextGranularity.document)),
347 };
348
349 final Map<Type, GestureRecognizerFactory> _gestureRecognizers = <Type, GestureRecognizerFactory>{};
350 SelectionOverlay? _selectionOverlay;
351 final LayerLink _startHandleLayerLink = LayerLink();
352 final LayerLink _endHandleLayerLink = LayerLink();
353 final LayerLink _toolbarLayerLink = LayerLink();
354 final _SelectableRegionContainerDelegate _selectionDelegate = _SelectableRegionContainerDelegate();
355 // there should only ever be one selectable, which is the SelectionContainer.
356 Selectable? _selectable;
357
358 bool get _hasSelectionOverlayGeometry => _selectionDelegate.value.startSelectionPoint != null
359 || _selectionDelegate.value.endSelectionPoint != null;
360
361 Orientation? _lastOrientation;
362 SelectedContent? _lastSelectedContent;
363
364 /// The [SelectionOverlay] that is currently visible on the screen.
365 ///
366 /// Can be null if there is no visible [SelectionOverlay].
367 @visibleForTesting
368 SelectionOverlay? get selectionOverlay => _selectionOverlay;
369
370 /// The text processing service used to retrieve the native text processing actions.
371 final ProcessTextService _processTextService = DefaultProcessTextService();
372
373 /// The list of native text processing actions provided by the engine.
374 final List<ProcessTextAction> _processTextActions = <ProcessTextAction>[];
375
376 @override
377 void initState() {
378 super.initState();
379 widget.focusNode.addListener(_handleFocusChanged);
380 _initMouseGestureRecognizer();
381 _initTouchGestureRecognizer();
382 // Right clicks.
383 _gestureRecognizers[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
384 () => TapGestureRecognizer(debugOwner: this),
385 (TapGestureRecognizer instance) {
386 instance.onSecondaryTapDown = _handleRightClickDown;
387 },
388 );
389 _initProcessTextActions();
390 }
391
392 /// Query the engine to initialize the list of text processing actions to show
393 /// in the text selection toolbar.
394 Future<void> _initProcessTextActions() async {
395 _processTextActions.clear();
396 _processTextActions.addAll(await _processTextService.queryTextActions());
397 }
398
399 @override
400 void didChangeDependencies() {
401 super.didChangeDependencies();
402 switch (defaultTargetPlatform) {
403 case TargetPlatform.android:
404 case TargetPlatform.iOS:
405 break;
406 case TargetPlatform.fuchsia:
407 case TargetPlatform.linux:
408 case TargetPlatform.macOS:
409 case TargetPlatform.windows:
410 return;
411 }
412
413 // Hide the text selection toolbar on mobile when orientation changes.
414 final Orientation orientation = MediaQuery.orientationOf(context);
415 if (_lastOrientation == null) {
416 _lastOrientation = orientation;
417 return;
418 }
419 if (orientation != _lastOrientation) {
420 _lastOrientation = orientation;
421 hideToolbar(defaultTargetPlatform == TargetPlatform.android);
422 }
423 }
424
425 @override
426 void didUpdateWidget(SelectableRegion oldWidget) {
427 super.didUpdateWidget(oldWidget);
428 if (widget.focusNode != oldWidget.focusNode) {
429 oldWidget.focusNode.removeListener(_handleFocusChanged);
430 widget.focusNode.addListener(_handleFocusChanged);
431 if (widget.focusNode.hasFocus != oldWidget.focusNode.hasFocus) {
432 _handleFocusChanged();
433 }
434 }
435 }
436
437 Action<T> _makeOverridable<T extends Intent>(Action<T> defaultAction) {
438 return Action<T>.overridable(context: context, defaultAction: defaultAction);
439 }
440
441 void _handleFocusChanged() {
442 if (!widget.focusNode.hasFocus) {
443 if (kIsWeb) {
444 PlatformSelectableRegionContextMenu.detach(_selectionDelegate);
445 }
446 if (SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) {
447 // We should only clear the selection when this SelectableRegion loses
448 // focus while the application is currently running. It is possible
449 // that the application is not currently running, for example on desktop
450 // platforms, clicking on a different window switches the focus to
451 // the new window causing the Flutter application to go inactive. In this
452 // case we want to retain the selection so it remains when we return to
453 // the Flutter application.
454 clearSelection();
455 }
456 }
457 if (kIsWeb) {
458 PlatformSelectableRegionContextMenu.attach(_selectionDelegate);
459 }
460 }
461
462 void _updateSelectionStatus() {
463 final SelectionGeometry geometry = _selectionDelegate.value;
464 final TextSelection selection = switch (geometry.status) {
465 SelectionStatus.uncollapsed || SelectionStatus.collapsed => const TextSelection(baseOffset: 0, extentOffset: 1),
466 SelectionStatus.none => const TextSelection.collapsed(offset: 1),
467 };
468 textEditingValue = TextEditingValue(text: '__', selection: selection);
469 if (_hasSelectionOverlayGeometry) {
470 _updateSelectionOverlay();
471 } else {
472 _selectionOverlay?.dispose();
473 _selectionOverlay = null;
474 }
475 }
476
477 // gestures.
478
479 /// Whether the Shift key was pressed when the most recent [PointerDownEvent]
480 /// was tracked by the [BaseTapAndDragGestureRecognizer].
481 bool _isShiftPressed = false;
482
483 // The position of the most recent secondary tap down event on this
484 // SelectableRegion.
485 Offset? _lastSecondaryTapDownPosition;
486
487 // The device kind for the pointer of the most recent tap down event on this
488 // SelectableRegion.
489 PointerDeviceKind? _lastPointerDeviceKind;
490
491 static bool _isPrecisePointerDevice(PointerDeviceKind pointerDeviceKind) {
492 switch (pointerDeviceKind) {
493 case PointerDeviceKind.mouse:
494 return true;
495 case PointerDeviceKind.trackpad:
496 case PointerDeviceKind.stylus:
497 case PointerDeviceKind.invertedStylus:
498 case PointerDeviceKind.touch:
499 case PointerDeviceKind.unknown:
500 return false;
501 }
502 }
503
504 // Converts the details.consecutiveTapCount from a TapAndDrag*Details object,
505 // which can grow to be infinitely large, to a value between 1 and the supported
506 // max consecutive tap count. The value that the raw count is converted to is
507 // based on the default observed behavior on the native platforms.
508 //
509 // This method should be used in all instances when details.consecutiveTapCount
510 // would be used.
511 int _getEffectiveConsecutiveTapCount(int rawCount) {
512 int maxConsecutiveTap = 3;
513 switch (defaultTargetPlatform) {
514 case TargetPlatform.android:
515 case TargetPlatform.fuchsia:
516 if (_lastPointerDeviceKind != null && _lastPointerDeviceKind != PointerDeviceKind.mouse) {
517 // When the pointer device kind is not precise like a mouse, native
518 // Android resets the tap count at 2. For example, this is so the
519 // selection can collapse on the third tap.
520 maxConsecutiveTap = 2;
521 }
522 // From observation, these platforms reset their tap count to 0 when
523 // the number of consecutive taps exceeds the max consecutive tap supported.
524 // For example on native Android, when going past a triple click,
525 // on the fourth click the selection is moved to the precise click
526 // position, on the fifth click the word at the position is selected, and
527 // on the sixth click the paragraph at the position is selected.
528 return rawCount <= maxConsecutiveTap ? rawCount : (rawCount % maxConsecutiveTap == 0 ? maxConsecutiveTap : rawCount % maxConsecutiveTap);
529 case TargetPlatform.linux:
530 // From observation, these platforms reset their tap count to 0 when
531 // the number of consecutive taps exceeds the max consecutive tap supported.
532 // For example on Debian Linux with GTK, when going past a triple click,
533 // on the fourth click the selection is moved to the precise click
534 // position, on the fifth click the word at the position is selected, and
535 // on the sixth click the paragraph at the position is selected.
536 return rawCount <= maxConsecutiveTap ? rawCount : (rawCount % maxConsecutiveTap == 0 ? maxConsecutiveTap : rawCount % maxConsecutiveTap);
537 case TargetPlatform.iOS:
538 case TargetPlatform.macOS:
539 case TargetPlatform.windows:
540 // From observation, these platforms hold their tap count at the max
541 // consecutive tap supported. For example on macOS, when going past a triple
542 // click, the selection should be retained at the paragraph that was first
543 // selected on triple click.
544 return min(rawCount, maxConsecutiveTap);
545 }
546 }
547
548 void _initMouseGestureRecognizer() {
549 _gestureRecognizers[TapAndPanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
550 () => TapAndPanGestureRecognizer(
551 debugOwner:this,
552 supportedDevices: <PointerDeviceKind>{ PointerDeviceKind.mouse },
553 ),
554 (TapAndPanGestureRecognizer instance) {
555 instance
556 ..onTapTrackStart = _onTapTrackStart
557 ..onTapTrackReset = _onTapTrackReset
558 ..onTapDown = _startNewMouseSelectionGesture
559 ..onTapUp = _handleMouseTapUp
560 ..onDragStart = _handleMouseDragStart
561 ..onDragUpdate = _handleMouseDragUpdate
562 ..onDragEnd = _handleMouseDragEnd
563 ..onCancel = clearSelection
564 ..dragStartBehavior = DragStartBehavior.down;
565 },
566 );
567 }
568
569 void _onTapTrackStart() {
570 _isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed
571 .intersection(<LogicalKeyboardKey>{LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight})
572 .isNotEmpty;
573 }
574
575 void _onTapTrackReset() {
576 _isShiftPressed = false;
577 }
578
579 void _initTouchGestureRecognizer() {
580 // A [TapAndHorizontalDragGestureRecognizer] is used on non-precise pointer devices
581 // like PointerDeviceKind.touch so [SelectableRegion] gestures do not conflict with
582 // ancestor Scrollable gestures in common scenarios like a vertically scrolling list view.
583 _gestureRecognizers[TapAndHorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndHorizontalDragGestureRecognizer>(
584 () => TapAndHorizontalDragGestureRecognizer(
585 debugOwner:this,
586 supportedDevices: PointerDeviceKind.values.where((PointerDeviceKind device) {
587 return device != PointerDeviceKind.mouse;
588 }).toSet(),
589 ),
590 (TapAndHorizontalDragGestureRecognizer instance) {
591 instance
592 // iOS does not provide a device specific touch slop
593 // unlike Android (~8.0), so the touch slop for a [Scrollable]
594 // always default to kTouchSlop which is 18.0. When
595 // [SelectableRegion] is the child of a horizontal
596 // scrollable that means the [SelectableRegion] will
597 // always win the gesture arena when competing with
598 // the ancestor scrollable because they both have
599 // the same touch slop threshold and the child receives
600 // the [PointerEvent] first. To avoid this conflict
601 // and ensure a smooth scrolling experience, on
602 // iOS the [TapAndHorizontalDragGestureRecognizer]
603 // will wait for all other gestures to lose before
604 // declaring victory.
605 ..eagerVictoryOnDrag = defaultTargetPlatform != TargetPlatform.iOS
606 ..onTapDown = _startNewMouseSelectionGesture
607 ..onTapUp = _handleMouseTapUp
608 ..onDragStart = _handleMouseDragStart
609 ..onDragUpdate = _handleMouseDragUpdate
610 ..onDragEnd = _handleMouseDragEnd
611 ..onCancel = clearSelection
612 ..dragStartBehavior = DragStartBehavior.down;
613 },
614 );
615 _gestureRecognizers[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
616 () => LongPressGestureRecognizer(debugOwner: this, supportedDevices: _kLongPressSelectionDevices),
617 (LongPressGestureRecognizer instance) {
618 instance
619 ..onLongPressStart = _handleTouchLongPressStart
620 ..onLongPressMoveUpdate = _handleTouchLongPressMoveUpdate
621 ..onLongPressEnd = _handleTouchLongPressEnd;
622 },
623 );
624 }
625
626 Offset? _doubleTapOffset;
627 void _startNewMouseSelectionGesture(TapDragDownDetails details) {
628 _lastPointerDeviceKind = details.kind;
629 switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
630 case 1:
631 widget.focusNode.requestFocus();
632 switch (defaultTargetPlatform) {
633 case TargetPlatform.android:
634 case TargetPlatform.fuchsia:
635 case TargetPlatform.iOS:
636 // On mobile platforms the selection is set on tap up for the first
637 // tap.
638 break;
639 case TargetPlatform.macOS:
640 case TargetPlatform.linux:
641 case TargetPlatform.windows:
642 hideToolbar();
643 // It is impossible to extend the selection when the shift key is
644 // pressed and the start of the selection has not been initialized.
645 // In this case we fallback on collapsing the selection to first
646 // initialize the selection.
647 final bool isShiftPressedValid = _isShiftPressed && _selectionDelegate.value.startSelectionPoint != null;
648 if (isShiftPressedValid) {
649 _selectEndTo(offset: details.globalPosition);
650 return;
651 }
652 _collapseSelectionAt(offset: details.globalPosition);
653 }
654 case 2:
655 switch (defaultTargetPlatform) {
656 case TargetPlatform.iOS:
657 if (kIsWeb && details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
658 // Double tap on iOS web triggers when a drag begins after the double tap.
659 _doubleTapOffset = details.globalPosition;
660 break;
661 }
662 _selectWordAt(offset: details.globalPosition);
663 if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
664 _showHandles();
665 }
666 case TargetPlatform.android:
667 case TargetPlatform.fuchsia:
668 case TargetPlatform.macOS:
669 case TargetPlatform.linux:
670 case TargetPlatform.windows:
671 _selectWordAt(offset: details.globalPosition);
672 }
673 case 3:
674 switch (defaultTargetPlatform) {
675 case TargetPlatform.android:
676 case TargetPlatform.fuchsia:
677 case TargetPlatform.iOS:
678 if (details.kind != null && _isPrecisePointerDevice(details.kind!)) {
679 // Triple tap on static text is only supported on mobile
680 // platforms using a precise pointer device.
681 _selectParagraphAt(offset: details.globalPosition);
682 }
683 case TargetPlatform.macOS:
684 case TargetPlatform.linux:
685 case TargetPlatform.windows:
686 _selectParagraphAt(offset: details.globalPosition);
687 }
688 }
689 _updateSelectedContentIfNeeded();
690 }
691
692 void _handleMouseDragStart(TapDragStartDetails details) {
693 switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
694 case 1:
695 if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
696 // Drag to select is only enabled with a precise pointer device.
697 return;
698 }
699 _selectStartTo(offset: details.globalPosition);
700 }
701 _updateSelectedContentIfNeeded();
702 }
703
704 void _handleMouseDragUpdate(TapDragUpdateDetails details) {
705 switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
706 case 1:
707 if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
708 // Drag to select is only enabled with a precise pointer device.
709 return;
710 }
711 _selectEndTo(offset: details.globalPosition, continuous: true);
712 case 2:
713 switch (defaultTargetPlatform) {
714 case TargetPlatform.android:
715 case TargetPlatform.fuchsia:
716 // Double tap + drag is only supported on Android when using a precise
717 // pointer device or when not on the web.
718 if (!kIsWeb || details.kind != null && _isPrecisePointerDevice(details.kind!)) {
719 _selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word);
720 }
721 case TargetPlatform.iOS:
722 if (kIsWeb && details.kind != null && !_isPrecisePointerDevice(details.kind!) && _doubleTapOffset != null) {
723 // On iOS web a double tap does not select the word at the position,
724 // until the drag has begun.
725 _selectWordAt(offset: _doubleTapOffset!);
726 _doubleTapOffset = null;
727 }
728 _selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word);
729 if (details.kind != null && !_isPrecisePointerDevice(details.kind!)) {
730 _showHandles();
731 }
732 case TargetPlatform.macOS:
733 case TargetPlatform.linux:
734 case TargetPlatform.windows:
735 _selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word);
736 }
737 case 3:
738 switch (defaultTargetPlatform) {
739 case TargetPlatform.android:
740 case TargetPlatform.fuchsia:
741 case TargetPlatform.iOS:
742 // Triple tap + drag is only supported on mobile devices when using
743 // a precise pointer device.
744 if (details.kind != null && _isPrecisePointerDevice(details.kind!)) {
745 _selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.paragraph);
746 }
747 case TargetPlatform.macOS:
748 case TargetPlatform.linux:
749 case TargetPlatform.windows:
750 _selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.paragraph);
751 }
752 }
753 _updateSelectedContentIfNeeded();
754 }
755
756 void _handleMouseDragEnd(TapDragEndDetails details) {
757 final bool isPointerPrecise = _lastPointerDeviceKind != null && _lastPointerDeviceKind == PointerDeviceKind.mouse;
758 switch (defaultTargetPlatform) {
759 case TargetPlatform.android:
760 case TargetPlatform.fuchsia:
761 if (!isPointerPrecise) {
762 // On Android, a drag gesture will only show the selection overlay when
763 // the drag has finished and the pointer device kind is not precise.
764 _showHandles();
765 _showToolbar();
766 }
767 case TargetPlatform.iOS:
768 if (!isPointerPrecise) {
769 // On iOS, a drag gesture will only show the selection toolbar when
770 // the drag has finished and the pointer device kind is not precise.
771 _showToolbar();
772 }
773 case TargetPlatform.macOS:
774 case TargetPlatform.linux:
775 case TargetPlatform.windows:
776 // The selection overlay is not shown on desktop platforms after a drag.
777 break;
778 }
779 _finalizeSelection();
780 _updateSelectedContentIfNeeded();
781 }
782
783 void _handleMouseTapUp(TapDragUpDetails details) {
784 if (defaultTargetPlatform == TargetPlatform.iOS && _positionIsOnActiveSelection(globalPosition: details.globalPosition)) {
785 // On iOS when the tap occurs on the previous selection, instead of
786 // moving the selection, the context menu will be toggled.
787 final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
788 if (toolbarIsVisible) {
789 hideToolbar(false);
790 } else {
791 _showToolbar();
792 }
793 return;
794 }
795 switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
796 case 1:
797 switch (defaultTargetPlatform) {
798 case TargetPlatform.android:
799 case TargetPlatform.fuchsia:
800 case TargetPlatform.iOS:
801 hideToolbar();
802 _collapseSelectionAt(offset: details.globalPosition);
803 case TargetPlatform.macOS:
804 case TargetPlatform.linux:
805 case TargetPlatform.windows:
806 // On desktop platforms the selection is set on tap down.
807 break;
808 }
809 case 2:
810 final bool isPointerPrecise = _isPrecisePointerDevice(details.kind);
811 switch (defaultTargetPlatform) {
812 case TargetPlatform.android:
813 case TargetPlatform.fuchsia:
814 if (!isPointerPrecise) {
815 // On Android, a double tap will only show the selection overlay after
816 // the following tap up when the pointer device kind is not precise.
817 _showHandles();
818 _showToolbar();
819 }
820 case TargetPlatform.iOS:
821 if (!isPointerPrecise) {
822 // On iOS, a double tap will only show the selection toolbar after
823 // the following tap up when the pointer device kind is not precise.
824 _showToolbar();
825 }
826 case TargetPlatform.macOS:
827 case TargetPlatform.linux:
828 case TargetPlatform.windows:
829 // The selection overlay is not shown on desktop platforms
830 // on a double click.
831 break;
832 }
833 }
834 _updateSelectedContentIfNeeded();
835 }
836
837 void _updateSelectedContentIfNeeded() {
838 if (_lastSelectedContent?.plainText != _selectable?.getSelectedContent()?.plainText) {
839 _lastSelectedContent = _selectable?.getSelectedContent();
840 widget.onSelectionChanged?.call(_lastSelectedContent);
841 }
842 }
843
844 void _handleTouchLongPressStart(LongPressStartDetails details) {
845 HapticFeedback.selectionClick();
846 widget.focusNode.requestFocus();
847 _selectWordAt(offset: details.globalPosition);
848 // Platforms besides Android will show the text selection handles when
849 // the long press is initiated. Android shows the text selection handles when
850 // the long press has ended, usually after a pointer up event is received.
851 if (defaultTargetPlatform != TargetPlatform.android) {
852 _showHandles();
853 }
854 _updateSelectedContentIfNeeded();
855 }
856
857 void _handleTouchLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
858 _selectEndTo(offset: details.globalPosition, textGranularity: TextGranularity.word);
859 _updateSelectedContentIfNeeded();
860 }
861
862 void _handleTouchLongPressEnd(LongPressEndDetails details) {
863 _finalizeSelection();
864 _updateSelectedContentIfNeeded();
865 _showToolbar();
866 if (defaultTargetPlatform == TargetPlatform.android) {
867 _showHandles();
868 }
869 }
870
871 bool _positionIsOnActiveSelection({required Offset globalPosition}) {
872 for (final Rect selectionRect in _selectionDelegate.value.selectionRects) {
873 final Matrix4 transform = _selectable!.getTransformTo(null);
874 final Rect globalRect = MatrixUtils.transformRect(transform, selectionRect);
875 if (globalRect.contains(globalPosition)) {
876 return true;
877 }
878 }
879 return false;
880 }
881
882 void _handleRightClickDown(TapDownDetails details) {
883 final Offset? previousSecondaryTapDownPosition = _lastSecondaryTapDownPosition;
884 final bool toolbarIsVisible = _selectionOverlay?.toolbarIsVisible ?? false;
885 _lastSecondaryTapDownPosition = details.globalPosition;
886 widget.focusNode.requestFocus();
887 switch (defaultTargetPlatform) {
888 case TargetPlatform.android:
889 case TargetPlatform.fuchsia:
890 case TargetPlatform.windows:
891 // If _lastSecondaryTapDownPosition is within the current selection then
892 // keep the current selection, if not then collapse it.
893 final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition);
894 if (!lastSecondaryTapDownPositionWasOnActiveSelection) {
895 _collapseSelectionAt(offset: _lastSecondaryTapDownPosition!);
896 }
897 _showHandles();
898 _showToolbar(location: _lastSecondaryTapDownPosition);
899 case TargetPlatform.iOS:
900 _selectWordAt(offset: _lastSecondaryTapDownPosition!);
901 _showHandles();
902 _showToolbar(location: _lastSecondaryTapDownPosition);
903 case TargetPlatform.macOS:
904 if (previousSecondaryTapDownPosition == _lastSecondaryTapDownPosition && toolbarIsVisible) {
905 hideToolbar();
906 return;
907 }
908 _selectWordAt(offset: _lastSecondaryTapDownPosition!);
909 _showHandles();
910 _showToolbar(location: _lastSecondaryTapDownPosition);
911 case TargetPlatform.linux:
912 if (toolbarIsVisible) {
913 hideToolbar();
914 return;
915 }
916 // If _lastSecondaryTapDownPosition is within the current selection then
917 // keep the current selection, if not then collapse it.
918 final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection(globalPosition: details.globalPosition);
919 if (!lastSecondaryTapDownPositionWasOnActiveSelection) {
920 _collapseSelectionAt(offset: _lastSecondaryTapDownPosition!);
921 }
922 _showHandles();
923 _showToolbar(location: _lastSecondaryTapDownPosition);
924 }
925 _updateSelectedContentIfNeeded();
926 }
927
928 // Selection update helper methods.
929
930 Offset? _selectionEndPosition;
931 bool get _userDraggingSelectionEnd => _selectionEndPosition != null;
932 bool _scheduledSelectionEndEdgeUpdate = false;
933
934 /// Sends end [SelectionEdgeUpdateEvent] to the selectable subtree.
935 ///
936 /// If the selectable subtree returns a [SelectionResult.pending], this method
937 /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result
938 /// is not pending or users end their gestures.
939 void _triggerSelectionEndEdgeUpdate({TextGranularity? textGranularity}) {
940 // This method can be called when the drag is not in progress. This can
941 // happen if the child scrollable returns SelectionResult.pending, and
942 // the selection area scheduled a selection update for the next frame, but
943 // the drag is lifted before the scheduled selection update is run.
944 if (_scheduledSelectionEndEdgeUpdate || !_userDraggingSelectionEnd) {
945 return;
946 }
947 if (_selectable?.dispatchSelectionEvent(
948 SelectionEdgeUpdateEvent.forEnd(globalPosition: _selectionEndPosition!, granularity: textGranularity)) == SelectionResult.pending) {
949 _scheduledSelectionEndEdgeUpdate = true;
950 SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
951 if (!_scheduledSelectionEndEdgeUpdate) {
952 return;
953 }
954 _scheduledSelectionEndEdgeUpdate = false;
955 _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity);
956 }, debugLabel: 'SelectableRegion.endEdgeUpdate');
957 return;
958 }
959 }
960
961 void _onAnyDragEnd(DragEndDetails details) {
962 if (widget.selectionControls is! TextSelectionHandleControls) {
963 _selectionOverlay!.hideMagnifier();
964 _selectionOverlay!.showToolbar();
965 } else {
966 _selectionOverlay!.hideMagnifier();
967 _selectionOverlay!.showToolbar(
968 context: context,
969 contextMenuBuilder: (BuildContext context) {
970 return widget.contextMenuBuilder!(context, this);
971 },
972 );
973 }
974 _stopSelectionStartEdgeUpdate();
975 _stopSelectionEndEdgeUpdate();
976 _updateSelectedContentIfNeeded();
977 }
978
979 void _stopSelectionEndEdgeUpdate() {
980 _scheduledSelectionEndEdgeUpdate = false;
981 _selectionEndPosition = null;
982 }
983
984 Offset? _selectionStartPosition;
985 bool get _userDraggingSelectionStart => _selectionStartPosition != null;
986 bool _scheduledSelectionStartEdgeUpdate = false;
987
988 /// Sends start [SelectionEdgeUpdateEvent] to the selectable subtree.
989 ///
990 /// If the selectable subtree returns a [SelectionResult.pending], this method
991 /// continues to send [SelectionEdgeUpdateEvent]s every frame until the result
992 /// is not pending or users end their gestures.
993 void _triggerSelectionStartEdgeUpdate({TextGranularity? textGranularity}) {
994 // This method can be called when the drag is not in progress. This can
995 // happen if the child scrollable returns SelectionResult.pending, and
996 // the selection area scheduled a selection update for the next frame, but
997 // the drag is lifted before the scheduled selection update is run.
998 if (_scheduledSelectionStartEdgeUpdate || !_userDraggingSelectionStart) {
999 return;
1000 }
1001 if (_selectable?.dispatchSelectionEvent(
1002 SelectionEdgeUpdateEvent.forStart(globalPosition: _selectionStartPosition!, granularity: textGranularity)) == SelectionResult.pending) {
1003 _scheduledSelectionStartEdgeUpdate = true;
1004 SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
1005 if (!_scheduledSelectionStartEdgeUpdate) {
1006 return;
1007 }
1008 _scheduledSelectionStartEdgeUpdate = false;
1009 _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity);
1010 }, debugLabel: 'SelectableRegion.startEdgeUpdate');
1011 return;
1012 }
1013 }
1014
1015 void _stopSelectionStartEdgeUpdate() {
1016 _scheduledSelectionStartEdgeUpdate = false;
1017 _selectionEndPosition = null;
1018 }
1019
1020 // SelectionOverlay helper methods.
1021
1022 late Offset _selectionStartHandleDragPosition;
1023 late Offset _selectionEndHandleDragPosition;
1024
1025 void _handleSelectionStartHandleDragStart(DragStartDetails details) {
1026 assert(_selectionDelegate.value.startSelectionPoint != null);
1027
1028 final Offset localPosition = _selectionDelegate.value.startSelectionPoint!.localPosition;
1029 final Matrix4 globalTransform = _selectable!.getTransformTo(null);
1030 _selectionStartHandleDragPosition = MatrixUtils.transformPoint(globalTransform, localPosition);
1031
1032 _selectionOverlay!.showMagnifier(_buildInfoForMagnifier(
1033 details.globalPosition,
1034 _selectionDelegate.value.startSelectionPoint!,
1035 ));
1036 _updateSelectedContentIfNeeded();
1037 }
1038
1039 void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) {
1040 _selectionStartHandleDragPosition = _selectionStartHandleDragPosition + details.delta;
1041 // The value corresponds to the paint origin of the selection handle.
1042 // Offset it to the center of the line to make it feel more natural.
1043 _selectionStartPosition = _selectionStartHandleDragPosition - Offset(0, _selectionDelegate.value.startSelectionPoint!.lineHeight / 2);
1044 _triggerSelectionStartEdgeUpdate();
1045
1046 _selectionOverlay!.updateMagnifier(_buildInfoForMagnifier(
1047 details.globalPosition,
1048 _selectionDelegate.value.startSelectionPoint!,
1049 ));
1050 _updateSelectedContentIfNeeded();
1051 }
1052
1053 void _handleSelectionEndHandleDragStart(DragStartDetails details) {
1054 assert(_selectionDelegate.value.endSelectionPoint != null);
1055 final Offset localPosition = _selectionDelegate.value.endSelectionPoint!.localPosition;
1056 final Matrix4 globalTransform = _selectable!.getTransformTo(null);
1057 _selectionEndHandleDragPosition = MatrixUtils.transformPoint(globalTransform, localPosition);
1058
1059 _selectionOverlay!.showMagnifier(_buildInfoForMagnifier(
1060 details.globalPosition,
1061 _selectionDelegate.value.endSelectionPoint!,
1062 ));
1063 _updateSelectedContentIfNeeded();
1064 }
1065
1066 void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) {
1067 _selectionEndHandleDragPosition = _selectionEndHandleDragPosition + details.delta;
1068 // The value corresponds to the paint origin of the selection handle.
1069 // Offset it to the center of the line to make it feel more natural.
1070 _selectionEndPosition = _selectionEndHandleDragPosition - Offset(0, _selectionDelegate.value.endSelectionPoint!.lineHeight / 2);
1071 _triggerSelectionEndEdgeUpdate();
1072
1073 _selectionOverlay!.updateMagnifier(_buildInfoForMagnifier(
1074 details.globalPosition,
1075 _selectionDelegate.value.endSelectionPoint!,
1076 ));
1077 _updateSelectedContentIfNeeded();
1078 }
1079
1080 MagnifierInfo _buildInfoForMagnifier(Offset globalGesturePosition, SelectionPoint selectionPoint) {
1081 final Vector3 globalTransform = _selectable!.getTransformTo(null).getTranslation();
1082 final Offset globalTransformAsOffset = Offset(globalTransform.x, globalTransform.y);
1083 final Offset globalSelectionPointPosition = selectionPoint.localPosition + globalTransformAsOffset;
1084 final Rect caretRect = Rect.fromLTWH(
1085 globalSelectionPointPosition.dx,
1086 globalSelectionPointPosition.dy - selectionPoint.lineHeight,
1087 0,
1088 selectionPoint.lineHeight
1089 );
1090
1091 return MagnifierInfo(
1092 globalGesturePosition: globalGesturePosition,
1093 caretRect: caretRect,
1094 fieldBounds: globalTransformAsOffset & _selectable!.size,
1095 currentLineBoundaries: globalTransformAsOffset & _selectable!.size,
1096 );
1097 }
1098
1099 void _createSelectionOverlay() {
1100 assert(_hasSelectionOverlayGeometry);
1101 if (_selectionOverlay != null) {
1102 return;
1103 }
1104 final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
1105 final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
1106 _selectionOverlay = SelectionOverlay(
1107 context: context,
1108 debugRequiredFor: widget,
1109 startHandleType: start?.handleType ?? TextSelectionHandleType.left,
1110 lineHeightAtStart: start?.lineHeight ?? end!.lineHeight,
1111 onStartHandleDragStart: _handleSelectionStartHandleDragStart,
1112 onStartHandleDragUpdate: _handleSelectionStartHandleDragUpdate,
1113 onStartHandleDragEnd: _onAnyDragEnd,
1114 endHandleType: end?.handleType ?? TextSelectionHandleType.right,
1115 lineHeightAtEnd: end?.lineHeight ?? start!.lineHeight,
1116 onEndHandleDragStart: _handleSelectionEndHandleDragStart,
1117 onEndHandleDragUpdate: _handleSelectionEndHandleDragUpdate,
1118 onEndHandleDragEnd: _onAnyDragEnd,
1119 selectionEndpoints: selectionEndpoints,
1120 selectionControls: widget.selectionControls,
1121 selectionDelegate: this,
1122 clipboardStatus: null,
1123 startHandleLayerLink: _startHandleLayerLink,
1124 endHandleLayerLink: _endHandleLayerLink,
1125 toolbarLayerLink: _toolbarLayerLink,
1126 magnifierConfiguration: widget.magnifierConfiguration
1127 );
1128 }
1129
1130 void _updateSelectionOverlay() {
1131 if (_selectionOverlay == null) {
1132 return;
1133 }
1134 assert(_hasSelectionOverlayGeometry);
1135 final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
1136 final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
1137 _selectionOverlay!
1138 ..startHandleType = start?.handleType ?? TextSelectionHandleType.left
1139 ..lineHeightAtStart = start?.lineHeight ?? end!.lineHeight
1140 ..endHandleType = end?.handleType ?? TextSelectionHandleType.right
1141 ..lineHeightAtEnd = end?.lineHeight ?? start!.lineHeight
1142 ..selectionEndpoints = selectionEndpoints;
1143 }
1144
1145 /// Shows the selection handles.
1146 ///
1147 /// Returns true if the handles are shown, false if the handles can't be
1148 /// shown.
1149 bool _showHandles() {
1150 if (_selectionOverlay != null) {
1151 _selectionOverlay!.showHandles();
1152 return true;
1153 }
1154
1155 if (!_hasSelectionOverlayGeometry) {
1156 return false;
1157 }
1158
1159 _createSelectionOverlay();
1160 _selectionOverlay!.showHandles();
1161 return true;
1162 }
1163
1164 /// Shows the text selection toolbar.
1165 ///
1166 /// If the parameter `location` is set, the toolbar will be shown at the
1167 /// location. Otherwise, the toolbar location will be calculated based on the
1168 /// handles' locations. The `location` is in the coordinates system of the
1169 /// [Overlay].
1170 ///
1171 /// Returns true if the toolbar is shown, false if the toolbar can't be shown.
1172 bool _showToolbar({Offset? location}) {
1173 if (!_hasSelectionOverlayGeometry && _selectionOverlay == null) {
1174 return false;
1175 }
1176
1177 // Web is using native dom elements to enable clipboard functionality of the
1178 // context menu: copy, paste, select, cut. It might also provide additional
1179 // functionality depending on the browser (such as translate). Due to this,
1180 // we should not show a Flutter toolbar for the editable text elements
1181 // unless the browser's context menu is explicitly disabled.
1182 if (kIsWeb && BrowserContextMenu.enabled) {
1183 return false;
1184 }
1185
1186 if (_selectionOverlay == null) {
1187 _createSelectionOverlay();
1188 }
1189
1190 _selectionOverlay!.toolbarLocation = location;
1191 if (widget.selectionControls is! TextSelectionHandleControls) {
1192 _selectionOverlay!.showToolbar();
1193 return true;
1194 }
1195
1196 _selectionOverlay!.hideToolbar();
1197
1198 _selectionOverlay!.showToolbar(
1199 context: context,
1200 contextMenuBuilder: (BuildContext context) {
1201 return widget.contextMenuBuilder!(context, this);
1202 },
1203 );
1204 return true;
1205 }
1206
1207 /// Sets or updates selection end edge to the `offset` location.
1208 ///
1209 /// A selection always contains a select start edge and selection end edge.
1210 /// They can be created by calling both [_selectStartTo] and [_selectEndTo], or
1211 /// use other selection APIs, such as [_selectWordAt] or [selectAll].
1212 ///
1213 /// This method sets or updates the selection end edge by sending
1214 /// [SelectionEdgeUpdateEvent]s to the child [Selectable]s.
1215 ///
1216 /// If `continuous` is set to true and the update causes scrolling, the
1217 /// method will continue sending the same [SelectionEdgeUpdateEvent]s to the
1218 /// child [Selectable]s every frame until the scrolling finishes or a
1219 /// [_finalizeSelection] is called.
1220 ///
1221 /// The `continuous` argument defaults to false.
1222 ///
1223 /// The `offset` is in global coordinates.
1224 ///
1225 /// Provide the `textGranularity` if the selection should not move by the default
1226 /// [TextGranularity.character]. Only [TextGranularity.character] and
1227 /// [TextGranularity.word] are currently supported.
1228 ///
1229 /// See also:
1230 /// * [_selectStartTo], which sets or updates selection start edge.
1231 /// * [_finalizeSelection], which stops the `continuous` updates.
1232 /// * [clearSelection], which clears the ongoing selection.
1233 /// * [_selectWordAt], which selects a whole word at the location.
1234 /// * [_selectParagraphAt], which selects an entire paragraph at the location.
1235 /// * [_collapseSelectionAt], which collapses the selection at the location.
1236 /// * [selectAll], which selects the entire content.
1237 void _selectEndTo({required Offset offset, bool continuous = false, TextGranularity? textGranularity}) {
1238 if (!continuous) {
1239 _selectable?.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forEnd(globalPosition: offset, granularity: textGranularity));
1240 return;
1241 }
1242 if (_selectionEndPosition != offset) {
1243 _selectionEndPosition = offset;
1244 _triggerSelectionEndEdgeUpdate(textGranularity: textGranularity);
1245 }
1246 }
1247
1248 /// Sets or updates selection start edge to the `offset` location.
1249 ///
1250 /// A selection always contains a select start edge and selection end edge.
1251 /// They can be created by calling both [_selectStartTo] and [_selectEndTo], or
1252 /// use other selection APIs, such as [_selectWordAt] or [selectAll].
1253 ///
1254 /// This method sets or updates the selection start edge by sending
1255 /// [SelectionEdgeUpdateEvent]s to the child [Selectable]s.
1256 ///
1257 /// If `continuous` is set to true and the update causes scrolling, the
1258 /// method will continue sending the same [SelectionEdgeUpdateEvent]s to the
1259 /// child [Selectable]s every frame until the scrolling finishes or a
1260 /// [_finalizeSelection] is called.
1261 ///
1262 /// The `continuous` argument defaults to false.
1263 ///
1264 /// The `offset` is in global coordinates.
1265 ///
1266 /// Provide the `textGranularity` if the selection should not move by the default
1267 /// [TextGranularity.character]. Only [TextGranularity.character] and
1268 /// [TextGranularity.word] are currently supported.
1269 ///
1270 /// See also:
1271 /// * [_selectEndTo], which sets or updates selection end edge.
1272 /// * [_finalizeSelection], which stops the `continuous` updates.
1273 /// * [clearSelection], which clears the ongoing selection.
1274 /// * [_selectWordAt], which selects a whole word at the location.
1275 /// * [_selectParagraphAt], which selects an entire paragraph at the location.
1276 /// * [_collapseSelectionAt], which collapses the selection at the location.
1277 /// * [selectAll], which selects the entire content.
1278 void _selectStartTo({required Offset offset, bool continuous = false, TextGranularity? textGranularity}) {
1279 if (!continuous) {
1280 _selectable?.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forStart(globalPosition: offset, granularity: textGranularity));
1281 return;
1282 }
1283 if (_selectionStartPosition != offset) {
1284 _selectionStartPosition = offset;
1285 _triggerSelectionStartEdgeUpdate(textGranularity: textGranularity);
1286 }
1287 }
1288
1289 /// Collapses the selection at the given `offset` location.
1290 ///
1291 /// The `offset` is in global coordinates.
1292 ///
1293 /// See also:
1294 /// * [_selectStartTo], which sets or updates selection start edge.
1295 /// * [_selectEndTo], which sets or updates selection end edge.
1296 /// * [_finalizeSelection], which stops the `continuous` updates.
1297 /// * [clearSelection], which clears the ongoing selection.
1298 /// * [_selectWordAt], which selects a whole word at the location.
1299 /// * [_selectParagraphAt], which selects an entire paragraph at the location.
1300 /// * [selectAll], which selects the entire content.
1301 void _collapseSelectionAt({required Offset offset}) {
1302 _selectStartTo(offset: offset);
1303 _selectEndTo(offset: offset);
1304 }
1305
1306 /// Selects a whole word at the `offset` location.
1307 ///
1308 /// The `offset` is in global coordinates.
1309 ///
1310 /// If the whole word is already in the current selection, selection won't
1311 /// change. One call [clearSelection] first if the selection needs to be
1312 /// updated even if the word is already covered by the current selection.
1313 ///
1314 /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection
1315 /// edges after calling this method.
1316 ///
1317 /// See also:
1318 /// * [_selectStartTo], which sets or updates selection start edge.
1319 /// * [_selectEndTo], which sets or updates selection end edge.
1320 /// * [_finalizeSelection], which stops the `continuous` updates.
1321 /// * [clearSelection], which clears the ongoing selection.
1322 /// * [_collapseSelectionAt], which collapses the selection at the location.
1323 /// * [_selectParagraphAt], which selects an entire paragraph at the location.
1324 /// * [selectAll], which selects the entire content.
1325 void _selectWordAt({required Offset offset}) {
1326 // There may be other selection ongoing.
1327 _finalizeSelection();
1328 _selectable?.dispatchSelectionEvent(SelectWordSelectionEvent(globalPosition: offset));
1329 }
1330
1331 /// Selects the entire paragraph at the `offset` location.
1332 ///
1333 /// The `offset` is in global coordinates.
1334 ///
1335 /// If the paragraph is already in the current selection, selection won't
1336 /// change. One call [clearSelection] first if the selection needs to be
1337 /// updated even if the paragraph is already covered by the current selection.
1338 ///
1339 /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection
1340 /// edges after calling this method.
1341 ///
1342 /// See also:
1343 /// * [_selectStartTo], which sets or updates selection start edge.
1344 /// * [_selectEndTo], which sets or updates selection end edge.
1345 /// * [_finalizeSelection], which stops the `continuous` updates.
1346 /// * [clearSelection], which clear the ongoing selection.
1347 /// * [_selectWordAt], which selects a whole word at the location.
1348 /// * [selectAll], which selects the entire content.
1349 void _selectParagraphAt({required Offset offset}) {
1350 // There may be other selection ongoing.
1351 _finalizeSelection();
1352 _selectable?.dispatchSelectionEvent(SelectParagraphSelectionEvent(globalPosition: offset));
1353 }
1354
1355 /// Stops any ongoing selection updates.
1356 ///
1357 /// This method is different from [clearSelection] that it does not remove
1358 /// the current selection. It only stops the continuous updates.
1359 ///
1360 /// A continuous update can happen as result of calling [_selectStartTo] or
1361 /// [_selectEndTo] with `continuous` sets to true which causes a [Selectable]
1362 /// to scroll. Calling this method will stop the update as well as the
1363 /// scrolling.
1364 void _finalizeSelection() {
1365 _stopSelectionEndEdgeUpdate();
1366 _stopSelectionStartEdgeUpdate();
1367 }
1368
1369 /// Removes the ongoing selection for this [SelectableRegion].
1370 void clearSelection() {
1371 _finalizeSelection();
1372 _directionalHorizontalBaseline = null;
1373 _adjustingSelectionEnd = null;
1374 _selectable?.dispatchSelectionEvent(const ClearSelectionEvent());
1375 _updateSelectedContentIfNeeded();
1376 }
1377
1378 Future<void> _copy() async {
1379 final SelectedContent? data = _selectable?.getSelectedContent();
1380 if (data == null) {
1381 return;
1382 }
1383 await Clipboard.setData(ClipboardData(text: data.plainText));
1384 }
1385
1386 Future<void> _share() async {
1387 final SelectedContent? data = _selectable?.getSelectedContent();
1388 if (data == null) {
1389 return;
1390 }
1391 await SystemChannels.platform.invokeMethod('Share.invoke', data.plainText);
1392 }
1393
1394 /// {@macro flutter.widgets.EditableText.getAnchors}
1395 ///
1396 /// See also:
1397 ///
1398 /// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s
1399 /// for the default context menu buttons.
1400 TextSelectionToolbarAnchors get contextMenuAnchors {
1401 if (_lastSecondaryTapDownPosition != null) {
1402 return TextSelectionToolbarAnchors(
1403 primaryAnchor: _lastSecondaryTapDownPosition!,
1404 );
1405 }
1406 final RenderBox renderBox = context.findRenderObject()! as RenderBox;
1407 return TextSelectionToolbarAnchors.fromSelection(
1408 renderBox: renderBox,
1409 startGlyphHeight: startGlyphHeight,
1410 endGlyphHeight: endGlyphHeight,
1411 selectionEndpoints: selectionEndpoints,
1412 );
1413 }
1414
1415 bool? _adjustingSelectionEnd;
1416 bool _determineIsAdjustingSelectionEnd(bool forward) {
1417 if (_adjustingSelectionEnd != null) {
1418 return _adjustingSelectionEnd!;
1419 }
1420 final bool isReversed;
1421 final SelectionPoint start = _selectionDelegate.value
1422 .startSelectionPoint!;
1423 final SelectionPoint end = _selectionDelegate.value.endSelectionPoint!;
1424 if (start.localPosition.dy > end.localPosition.dy) {
1425 isReversed = true;
1426 } else if (start.localPosition.dy < end.localPosition.dy) {
1427 isReversed = false;
1428 } else {
1429 isReversed = start.localPosition.dx > end.localPosition.dx;
1430 }
1431 // Always move the selection edge that increases the selection range.
1432 return _adjustingSelectionEnd = forward != isReversed;
1433 }
1434
1435 void _granularlyExtendSelection(TextGranularity granularity, bool forward) {
1436 _directionalHorizontalBaseline = null;
1437 if (!_selectionDelegate.value.hasSelection) {
1438 return;
1439 }
1440 _selectable?.dispatchSelectionEvent(
1441 GranularlyExtendSelectionEvent(
1442 forward: forward,
1443 isEnd: _determineIsAdjustingSelectionEnd(forward),
1444 granularity: granularity,
1445 ),
1446 );
1447 _updateSelectedContentIfNeeded();
1448 }
1449
1450 double? _directionalHorizontalBaseline;
1451
1452 void _directionallyExtendSelection(bool forward) {
1453 if (!_selectionDelegate.value.hasSelection) {
1454 return;
1455 }
1456 final bool adjustingSelectionExtend = _determineIsAdjustingSelectionEnd(forward);
1457 final SelectionPoint baseLinePoint = adjustingSelectionExtend
1458 ? _selectionDelegate.value.endSelectionPoint!
1459 : _selectionDelegate.value.startSelectionPoint!;
1460 _directionalHorizontalBaseline ??= baseLinePoint.localPosition.dx;
1461 final Offset globalSelectionPointOffset = MatrixUtils.transformPoint(context.findRenderObject()!.getTransformTo(null), Offset(_directionalHorizontalBaseline!, 0));
1462 _selectable?.dispatchSelectionEvent(
1463 DirectionallyExtendSelectionEvent(
1464 isEnd: _adjustingSelectionEnd!,
1465 direction: forward ? SelectionExtendDirection.nextLine : SelectionExtendDirection.previousLine,
1466 dx: globalSelectionPointOffset.dx,
1467 ),
1468 );
1469 _updateSelectedContentIfNeeded();
1470 }
1471
1472 // [TextSelectionDelegate] overrides.
1473
1474 /// Returns the [ContextMenuButtonItem]s representing the buttons in this
1475 /// platform's default selection menu.
1476 ///
1477 /// See also:
1478 ///
1479 /// * [SelectableRegion.getSelectableButtonItems], which performs a similar role,
1480 /// but for any selectable text, not just specifically SelectableRegion.
1481 /// * [EditableTextState.contextMenuButtonItems], which performs a similar role
1482 /// but for content that is not just selectable but also editable.
1483 /// * [contextMenuAnchors], which provides the anchor points for the default
1484 /// context menu.
1485 /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can
1486 /// take a list of [ContextMenuButtonItem]s with
1487 /// [AdaptiveTextSelectionToolbar.buttonItems].
1488 /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the
1489 /// button Widgets for the current platform given [ContextMenuButtonItem]s.
1490 List<ContextMenuButtonItem> get contextMenuButtonItems {
1491 return SelectableRegion.getSelectableButtonItems(
1492 selectionGeometry: _selectionDelegate.value,
1493 onCopy: () {
1494 _copy();
1495
1496 // On Android copy should clear the selection.
1497 switch (defaultTargetPlatform) {
1498 case TargetPlatform.android:
1499 case TargetPlatform.fuchsia:
1500 clearSelection();
1501 case TargetPlatform.iOS:
1502 hideToolbar(false);
1503 case TargetPlatform.linux:
1504 case TargetPlatform.macOS:
1505 case TargetPlatform.windows:
1506 hideToolbar();
1507 }
1508 },
1509 onSelectAll: () {
1510 switch (defaultTargetPlatform) {
1511 case TargetPlatform.android:
1512 case TargetPlatform.iOS:
1513 case TargetPlatform.fuchsia:
1514 selectAll(SelectionChangedCause.toolbar);
1515 case TargetPlatform.linux:
1516 case TargetPlatform.macOS:
1517 case TargetPlatform.windows:
1518 selectAll();
1519 hideToolbar();
1520 }
1521 },
1522 onShare: () {
1523 _share();
1524
1525 // On Android, share should clear the selection.
1526 switch (defaultTargetPlatform) {
1527 case TargetPlatform.android:
1528 case TargetPlatform.fuchsia:
1529 clearSelection();
1530 case TargetPlatform.iOS:
1531 hideToolbar(false);
1532 case TargetPlatform.linux:
1533 case TargetPlatform.macOS:
1534 case TargetPlatform.windows:
1535 hideToolbar();
1536 }
1537 },
1538 )..addAll(_textProcessingActionButtonItems);
1539 }
1540
1541 List<ContextMenuButtonItem> get _textProcessingActionButtonItems {
1542 final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
1543 final SelectedContent? data = _selectable?.getSelectedContent();
1544 if (data == null) {
1545 return buttonItems;
1546 }
1547
1548 for (final ProcessTextAction action in _processTextActions) {
1549 buttonItems.add(ContextMenuButtonItem(
1550 label: action.label,
1551 onPressed: () async {
1552 final String selectedText = data.plainText;
1553 if (selectedText.isNotEmpty) {
1554 await _processTextService.processTextAction(action.id, selectedText, true);
1555 hideToolbar();
1556 }
1557 },
1558 ));
1559 }
1560 return buttonItems;
1561 }
1562
1563 /// The line height at the start of the current selection.
1564 double get startGlyphHeight {
1565 return _selectionDelegate.value.startSelectionPoint!.lineHeight;
1566 }
1567
1568 /// The line height at the end of the current selection.
1569 double get endGlyphHeight {
1570 return _selectionDelegate.value.endSelectionPoint!.lineHeight;
1571 }
1572
1573 /// Returns the local coordinates of the endpoints of the current selection.
1574 List<TextSelectionPoint> get selectionEndpoints {
1575 final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
1576 final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
1577 late List<TextSelectionPoint> points;
1578 final Offset startLocalPosition = start?.localPosition ?? end!.localPosition;
1579 final Offset endLocalPosition = end?.localPosition ?? start!.localPosition;
1580 if (startLocalPosition.dy > endLocalPosition.dy) {
1581 points = <TextSelectionPoint>[
1582 TextSelectionPoint(endLocalPosition, TextDirection.ltr),
1583 TextSelectionPoint(startLocalPosition, TextDirection.ltr),
1584 ];
1585 } else {
1586 points = <TextSelectionPoint>[
1587 TextSelectionPoint(startLocalPosition, TextDirection.ltr),
1588 TextSelectionPoint(endLocalPosition, TextDirection.ltr),
1589 ];
1590 }
1591 return points;
1592 }
1593
1594 // [TextSelectionDelegate] overrides.
1595 // TODO(justinmc): After deprecations have been removed, remove
1596 // TextSelectionDelegate from this class.
1597 // https://github.com/flutter/flutter/issues/111213
1598
1599 @Deprecated(
1600 'Use `contextMenuBuilder` instead. '
1601 'This feature was deprecated after v3.3.0-0.5.pre.',
1602 )
1603 @override
1604 bool get cutEnabled => false;
1605
1606 @Deprecated(
1607 'Use `contextMenuBuilder` instead. '
1608 'This feature was deprecated after v3.3.0-0.5.pre.',
1609 )
1610 @override
1611 bool get pasteEnabled => false;
1612
1613 @override
1614 void hideToolbar([bool hideHandles = true]) {
1615 _selectionOverlay?.hideToolbar();
1616 if (hideHandles) {
1617 _selectionOverlay?.hideHandles();
1618 }
1619 }
1620
1621 @override
1622 void selectAll([SelectionChangedCause? cause]) {
1623 clearSelection();
1624 _selectable?.dispatchSelectionEvent(const SelectAllSelectionEvent());
1625 if (cause == SelectionChangedCause.toolbar) {
1626 _showToolbar();
1627 _showHandles();
1628 }
1629 _updateSelectedContentIfNeeded();
1630 }
1631
1632 @Deprecated(
1633 'Use `contextMenuBuilder` instead. '
1634 'This feature was deprecated after v3.3.0-0.5.pre.',
1635 )
1636 @override
1637 void copySelection(SelectionChangedCause cause) {
1638 _copy();
1639 clearSelection();
1640 }
1641
1642 @Deprecated(
1643 'Use `contextMenuBuilder` instead. '
1644 'This feature was deprecated after v3.3.0-0.5.pre.',
1645 )
1646 @override
1647 TextEditingValue textEditingValue = const TextEditingValue(text: '_');
1648
1649 @Deprecated(
1650 'Use `contextMenuBuilder` instead. '
1651 'This feature was deprecated after v3.3.0-0.5.pre.',
1652 )
1653 @override
1654 void bringIntoView(TextPosition position) {/* SelectableRegion must be in view at this point. */}
1655
1656 @Deprecated(
1657 'Use `contextMenuBuilder` instead. '
1658 'This feature was deprecated after v3.3.0-0.5.pre.',
1659 )
1660 @override
1661 void cutSelection(SelectionChangedCause cause) {
1662 assert(false);
1663 }
1664
1665 @Deprecated(
1666 'Use `contextMenuBuilder` instead. '
1667 'This feature was deprecated after v3.3.0-0.5.pre.',
1668 )
1669 @override
1670 void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause) {/* SelectableRegion maintains its own state */}
1671
1672 @Deprecated(
1673 'Use `contextMenuBuilder` instead. '
1674 'This feature was deprecated after v3.3.0-0.5.pre.',
1675 )
1676 @override
1677 Future<void> pasteText(SelectionChangedCause cause) async {
1678 assert(false);
1679 }
1680
1681 // [SelectionRegistrar] override.
1682
1683 @override
1684 void add(Selectable selectable) {
1685 assert(_selectable == null);
1686 _selectable = selectable;
1687 _selectable!.addListener(_updateSelectionStatus);
1688 _selectable!.pushHandleLayers(_startHandleLayerLink, _endHandleLayerLink);
1689 }
1690
1691 @override
1692 void remove(Selectable selectable) {
1693 assert(_selectable == selectable);
1694 _selectable!.removeListener(_updateSelectionStatus);
1695 _selectable!.pushHandleLayers(null, null);
1696 _selectable = null;
1697 }
1698
1699 @override
1700 void dispose() {
1701 _selectable?.removeListener(_updateSelectionStatus);
1702 _selectable?.pushHandleLayers(null, null);
1703 _selectionDelegate.dispose();
1704 // In case dispose was triggered before gesture end, remove the magnifier
1705 // so it doesn't remain stuck in the overlay forever.
1706 _selectionOverlay?.hideMagnifier();
1707 _selectionOverlay?.dispose();
1708 _selectionOverlay = null;
1709 super.dispose();
1710 }
1711
1712 @override
1713 Widget build(BuildContext context) {
1714 assert(debugCheckHasOverlay(context));
1715 Widget result = SelectionContainer(
1716 registrar: this,
1717 delegate: _selectionDelegate,
1718 child: widget.child,
1719 );
1720 if (kIsWeb) {
1721 result = PlatformSelectableRegionContextMenu(
1722 child: result,
1723 );
1724 }
1725 return CompositedTransformTarget(
1726 link: _toolbarLayerLink,
1727 child: RawGestureDetector(
1728 gestures: _gestureRecognizers,
1729 behavior: HitTestBehavior.translucent,
1730 excludeFromSemantics: true,
1731 child: Actions(
1732 actions: _actions,
1733 child: Focus(
1734 includeSemantics: false,
1735 focusNode: widget.focusNode,
1736 child: result,
1737 ),
1738 ),
1739 ),
1740 );
1741 }
1742}
1743
1744/// An action that does not override any [Action.overridable] in the subtree.
1745///
1746/// If this action is invoked by an [Action.overridable], it will immediately
1747/// invoke the [Action.overridable] and do nothing else. Otherwise, it will call
1748/// [invokeAction].
1749abstract class _NonOverrideAction<T extends Intent> extends ContextAction<T> {
1750 Object? invokeAction(T intent, [BuildContext? context]);
1751
1752 @override
1753 Object? invoke(T intent, [BuildContext? context]) {
1754 if (callingAction != null) {
1755 return callingAction!.invoke(intent);
1756 }
1757 return invokeAction(intent, context);
1758 }
1759}
1760
1761class _SelectAllAction extends _NonOverrideAction<SelectAllTextIntent> {
1762 _SelectAllAction(this.state);
1763
1764 final SelectableRegionState state;
1765
1766 @override
1767 void invokeAction(SelectAllTextIntent intent, [BuildContext? context]) {
1768 state.selectAll(SelectionChangedCause.keyboard);
1769 }
1770}
1771
1772class _CopySelectionAction extends _NonOverrideAction<CopySelectionTextIntent> {
1773 _CopySelectionAction(this.state);
1774
1775 final SelectableRegionState state;
1776
1777 @override
1778 void invokeAction(CopySelectionTextIntent intent, [BuildContext? context]) {
1779 state._copy();
1780 }
1781}
1782
1783class _GranularlyExtendSelectionAction<T extends DirectionalTextEditingIntent> extends _NonOverrideAction<T> {
1784 _GranularlyExtendSelectionAction(this.state, {required this.granularity});
1785
1786 final SelectableRegionState state;
1787 final TextGranularity granularity;
1788
1789 @override
1790 void invokeAction(T intent, [BuildContext? context]) {
1791 state._granularlyExtendSelection(granularity, intent.forward);
1792 }
1793}
1794
1795class _GranularlyExtendCaretSelectionAction<T extends DirectionalCaretMovementIntent> extends _NonOverrideAction<T> {
1796 _GranularlyExtendCaretSelectionAction(this.state, {required this.granularity});
1797
1798 final SelectableRegionState state;
1799 final TextGranularity granularity;
1800
1801 @override
1802 void invokeAction(T intent, [BuildContext? context]) {
1803 if (intent.collapseSelection) {
1804 // Selectable region never collapses selection.
1805 return;
1806 }
1807 state._granularlyExtendSelection(granularity, intent.forward);
1808 }
1809}
1810
1811class _DirectionallyExtendCaretSelectionAction<T extends DirectionalCaretMovementIntent> extends _NonOverrideAction<T> {
1812 _DirectionallyExtendCaretSelectionAction(this.state);
1813
1814 final SelectableRegionState state;
1815
1816 @override
1817 void invokeAction(T intent, [BuildContext? context]) {
1818 if (intent.collapseSelection) {
1819 // Selectable region never collapses selection.
1820 return;
1821 }
1822 state._directionallyExtendSelection(intent.forward);
1823 }
1824}
1825
1826class _SelectableRegionContainerDelegate extends MultiSelectableSelectionContainerDelegate {
1827 final Set<Selectable> _hasReceivedStartEvent = <Selectable>{};
1828 final Set<Selectable> _hasReceivedEndEvent = <Selectable>{};
1829
1830 Offset? _lastStartEdgeUpdateGlobalPosition;
1831 Offset? _lastEndEdgeUpdateGlobalPosition;
1832
1833 @override
1834 void remove(Selectable selectable) {
1835 _hasReceivedStartEvent.remove(selectable);
1836 _hasReceivedEndEvent.remove(selectable);
1837 super.remove(selectable);
1838 }
1839
1840 void _updateLastEdgeEventsFromGeometries() {
1841 if (currentSelectionStartIndex != -1 && selectables[currentSelectionStartIndex].value.hasSelection) {
1842 final Selectable start = selectables[currentSelectionStartIndex];
1843 final Offset localStartEdge = start.value.startSelectionPoint!.localPosition +
1844 Offset(0, - start.value.startSelectionPoint!.lineHeight / 2);
1845 _lastStartEdgeUpdateGlobalPosition = MatrixUtils.transformPoint(start.getTransformTo(null), localStartEdge);
1846 }
1847 if (currentSelectionEndIndex != -1 && selectables[currentSelectionEndIndex].value.hasSelection) {
1848 final Selectable end = selectables[currentSelectionEndIndex];
1849 final Offset localEndEdge = end.value.endSelectionPoint!.localPosition +
1850 Offset(0, -end.value.endSelectionPoint!.lineHeight / 2);
1851 _lastEndEdgeUpdateGlobalPosition = MatrixUtils.transformPoint(end.getTransformTo(null), localEndEdge);
1852 }
1853 }
1854
1855 @override
1856 SelectionResult handleSelectAll(SelectAllSelectionEvent event) {
1857 final SelectionResult result = super.handleSelectAll(event);
1858 for (final Selectable selectable in selectables) {
1859 _hasReceivedStartEvent.add(selectable);
1860 _hasReceivedEndEvent.add(selectable);
1861 }
1862 // Synthesize last update event so the edge updates continue to work.
1863 _updateLastEdgeEventsFromGeometries();
1864 return result;
1865 }
1866
1867 /// Selects a word in a [Selectable] at the location
1868 /// [SelectWordSelectionEvent.globalPosition].
1869 @override
1870 SelectionResult handleSelectWord(SelectWordSelectionEvent event) {
1871 final SelectionResult result = super.handleSelectWord(event);
1872 if (currentSelectionStartIndex != -1) {
1873 _hasReceivedStartEvent.add(selectables[currentSelectionStartIndex]);
1874 }
1875 if (currentSelectionEndIndex != -1) {
1876 _hasReceivedEndEvent.add(selectables[currentSelectionEndIndex]);
1877 }
1878 _updateLastEdgeEventsFromGeometries();
1879 return result;
1880 }
1881
1882 /// Selects a paragraph in a [Selectable] at the location
1883 /// [SelectParagraphSelectionEvent.globalPosition].
1884 @override
1885 SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
1886 final SelectionResult result = super.handleSelectParagraph(event);
1887 if (currentSelectionStartIndex != -1) {
1888 _hasReceivedStartEvent.add(selectables[currentSelectionStartIndex]);
1889 }
1890 if (currentSelectionEndIndex != -1) {
1891 _hasReceivedEndEvent.add(selectables[currentSelectionEndIndex]);
1892 }
1893 _updateLastEdgeEventsFromGeometries();
1894 return result;
1895 }
1896
1897 @override
1898 SelectionResult handleClearSelection(ClearSelectionEvent event) {
1899 final SelectionResult result = super.handleClearSelection(event);
1900 _hasReceivedStartEvent.clear();
1901 _hasReceivedEndEvent.clear();
1902 _lastStartEdgeUpdateGlobalPosition = null;
1903 _lastEndEdgeUpdateGlobalPosition = null;
1904 return result;
1905 }
1906
1907 @override
1908 SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
1909 if (event.type == SelectionEventType.endEdgeUpdate) {
1910 _lastEndEdgeUpdateGlobalPosition = event.globalPosition;
1911 } else {
1912 _lastStartEdgeUpdateGlobalPosition = event.globalPosition;
1913 }
1914 return super.handleSelectionEdgeUpdate(event);
1915 }
1916
1917 @override
1918 void dispose() {
1919 _hasReceivedStartEvent.clear();
1920 _hasReceivedEndEvent.clear();
1921 super.dispose();
1922 }
1923
1924 @override
1925 SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
1926 switch (event.type) {
1927 case SelectionEventType.startEdgeUpdate:
1928 _hasReceivedStartEvent.add(selectable);
1929 ensureChildUpdated(selectable);
1930 case SelectionEventType.endEdgeUpdate:
1931 _hasReceivedEndEvent.add(selectable);
1932 ensureChildUpdated(selectable);
1933 case SelectionEventType.clear:
1934 _hasReceivedStartEvent.remove(selectable);
1935 _hasReceivedEndEvent.remove(selectable);
1936 case SelectionEventType.selectAll:
1937 case SelectionEventType.selectWord:
1938 case SelectionEventType.selectParagraph:
1939 break;
1940 case SelectionEventType.granularlyExtendSelection:
1941 case SelectionEventType.directionallyExtendSelection:
1942 _hasReceivedStartEvent.add(selectable);
1943 _hasReceivedEndEvent.add(selectable);
1944 ensureChildUpdated(selectable);
1945 }
1946 return super.dispatchSelectionEventToChild(selectable, event);
1947 }
1948
1949 @override
1950 void ensureChildUpdated(Selectable selectable) {
1951 if (_lastEndEdgeUpdateGlobalPosition != null && _hasReceivedEndEvent.add(selectable)) {
1952 final SelectionEdgeUpdateEvent synthesizedEvent = SelectionEdgeUpdateEvent.forEnd(
1953 globalPosition: _lastEndEdgeUpdateGlobalPosition!,
1954 );
1955 if (currentSelectionEndIndex == -1) {
1956 handleSelectionEdgeUpdate(synthesizedEvent);
1957 }
1958 selectable.dispatchSelectionEvent(synthesizedEvent);
1959 }
1960 if (_lastStartEdgeUpdateGlobalPosition != null && _hasReceivedStartEvent.add(selectable)) {
1961 final SelectionEdgeUpdateEvent synthesizedEvent = SelectionEdgeUpdateEvent.forStart(
1962 globalPosition: _lastStartEdgeUpdateGlobalPosition!,
1963 );
1964 if (currentSelectionStartIndex == -1) {
1965 handleSelectionEdgeUpdate(synthesizedEvent);
1966 }
1967 selectable.dispatchSelectionEvent(synthesizedEvent);
1968 }
1969 }
1970
1971 @override
1972 void didChangeSelectables() {
1973 if (_lastEndEdgeUpdateGlobalPosition != null) {
1974 handleSelectionEdgeUpdate(
1975 SelectionEdgeUpdateEvent.forEnd(
1976 globalPosition: _lastEndEdgeUpdateGlobalPosition!,
1977 ),
1978 );
1979 }
1980 if (_lastStartEdgeUpdateGlobalPosition != null) {
1981 handleSelectionEdgeUpdate(
1982 SelectionEdgeUpdateEvent.forStart(
1983 globalPosition: _lastStartEdgeUpdateGlobalPosition!,
1984 ),
1985 );
1986 }
1987 final Set<Selectable> selectableSet = selectables.toSet();
1988 _hasReceivedEndEvent.removeWhere((Selectable selectable) => !selectableSet.contains(selectable));
1989 _hasReceivedStartEvent.removeWhere((Selectable selectable) => !selectableSet.contains(selectable));
1990 super.didChangeSelectables();
1991 }
1992}
1993
1994/// A delegate that handles events and updates for multiple [Selectable]
1995/// children.
1996///
1997/// Updates are optimized by tracking which [Selectable]s reside on the edges of
1998/// a selection. Subclasses should implement [ensureChildUpdated] to describe
1999/// how a [Selectable] should behave when added to a selection.
2000abstract class MultiSelectableSelectionContainerDelegate extends SelectionContainerDelegate with ChangeNotifier {
2001 /// Creates an instance of [MultiSelectableSelectionContainerDelegate].
2002 MultiSelectableSelectionContainerDelegate() {
2003 if (kFlutterMemoryAllocationsEnabled) {
2004 ChangeNotifier.maybeDispatchObjectCreation(this);
2005 }
2006 }
2007
2008 /// Gets the list of [Selectable]s this delegate is managing.
2009 List<Selectable> selectables = <Selectable>[];
2010
2011 /// The number of additional pixels added to the selection handle drawable
2012 /// area.
2013 ///
2014 /// Selection handles that are outside of the drawable area will be hidden.
2015 /// That logic prevents handles that get scrolled off the viewport from being
2016 /// drawn on the screen.
2017 ///
2018 /// The drawable area = current rectangle of [SelectionContainer] +
2019 /// _kSelectionHandleDrawableAreaPadding on each side.
2020 ///
2021 /// This was an eyeballed value to create smooth user experiences.
2022 static const double _kSelectionHandleDrawableAreaPadding = 5.0;
2023
2024 /// The current [Selectable] that contains the selection end edge.
2025 @protected
2026 int currentSelectionEndIndex = -1;
2027
2028 /// The current [Selectable] that contains the selection start edge.
2029 @protected
2030 int currentSelectionStartIndex = -1;
2031
2032 LayerLink? _startHandleLayer;
2033 Selectable? _startHandleLayerOwner;
2034 LayerLink? _endHandleLayer;
2035 Selectable? _endHandleLayerOwner;
2036
2037 bool _isHandlingSelectionEvent = false;
2038 bool _scheduledSelectableUpdate = false;
2039 bool _selectionInProgress = false;
2040 Set<Selectable> _additions = <Selectable>{};
2041
2042 bool _extendSelectionInProgress = false;
2043
2044 @override
2045 void add(Selectable selectable) {
2046 assert(!selectables.contains(selectable));
2047 _additions.add(selectable);
2048 _scheduleSelectableUpdate();
2049 }
2050
2051 @override
2052 void remove(Selectable selectable) {
2053 if (_additions.remove(selectable)) {
2054 // The same selectable was added in the same frame and is not yet
2055 // incorporated into the selectables.
2056 //
2057 // Removing such selectable doesn't require selection geometry update.
2058 return;
2059 }
2060 _removeSelectable(selectable);
2061 _scheduleSelectableUpdate();
2062 }
2063
2064 /// Notifies this delegate that layout of the container has changed.
2065 void layoutDidChange() {
2066 _updateSelectionGeometry();
2067 }
2068
2069 void _scheduleSelectableUpdate() {
2070 if (!_scheduledSelectableUpdate) {
2071 _scheduledSelectableUpdate = true;
2072 void runScheduledTask([Duration? duration]) {
2073 if (!_scheduledSelectableUpdate) {
2074 return;
2075 }
2076 _scheduledSelectableUpdate = false;
2077 _updateSelectables();
2078 }
2079
2080 if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.postFrameCallbacks) {
2081 // A new task can be scheduled as a result of running the scheduled task
2082 // from another MultiSelectableSelectionContainerDelegate. This can
2083 // happen if nesting two SelectionContainers. The selectable can be
2084 // safely updated in the same frame in this case.
2085 scheduleMicrotask(runScheduledTask);
2086 } else {
2087 SchedulerBinding.instance.addPostFrameCallback(
2088 runScheduledTask,
2089 debugLabel: 'SelectionContainer.runScheduledTask',
2090 );
2091 }
2092 }
2093 }
2094
2095 void _updateSelectables() {
2096 // Remove offScreen selectable.
2097 if (_additions.isNotEmpty) {
2098 _flushAdditions();
2099 }
2100 didChangeSelectables();
2101 }
2102
2103 void _flushAdditions() {
2104 final List<Selectable> mergingSelectables = _additions.toList()..sort(compareOrder);
2105 final List<Selectable> existingSelectables = selectables;
2106 selectables = <Selectable>[];
2107 int mergingIndex = 0;
2108 int existingIndex = 0;
2109 int selectionStartIndex = currentSelectionStartIndex;
2110 int selectionEndIndex = currentSelectionEndIndex;
2111 // Merge two sorted lists.
2112 while (mergingIndex < mergingSelectables.length || existingIndex < existingSelectables.length) {
2113 if (mergingIndex >= mergingSelectables.length ||
2114 (existingIndex < existingSelectables.length &&
2115 compareOrder(existingSelectables[existingIndex], mergingSelectables[mergingIndex]) < 0)) {
2116 if (existingIndex == currentSelectionStartIndex) {
2117 selectionStartIndex = selectables.length;
2118 }
2119 if (existingIndex == currentSelectionEndIndex) {
2120 selectionEndIndex = selectables.length;
2121 }
2122 selectables.add(existingSelectables[existingIndex]);
2123 existingIndex += 1;
2124 continue;
2125 }
2126
2127 // If the merging selectable falls in the selection range, their selection
2128 // needs to be updated.
2129 final Selectable mergingSelectable = mergingSelectables[mergingIndex];
2130 if (existingIndex < max(currentSelectionStartIndex, currentSelectionEndIndex) &&
2131 existingIndex > min(currentSelectionStartIndex, currentSelectionEndIndex)) {
2132 ensureChildUpdated(mergingSelectable);
2133 }
2134 mergingSelectable.addListener(_handleSelectableGeometryChange);
2135 selectables.add(mergingSelectable);
2136 mergingIndex += 1;
2137 }
2138 assert(mergingIndex == mergingSelectables.length &&
2139 existingIndex == existingSelectables.length &&
2140 selectables.length == existingIndex + mergingIndex);
2141 assert(selectionStartIndex >= -1 || selectionStartIndex < selectables.length);
2142 assert(selectionEndIndex >= -1 || selectionEndIndex < selectables.length);
2143 // selection indices should not be set to -1 unless they originally were.
2144 assert((currentSelectionStartIndex == -1) == (selectionStartIndex == -1));
2145 assert((currentSelectionEndIndex == -1) == (selectionEndIndex == -1));
2146 currentSelectionEndIndex = selectionEndIndex;
2147 currentSelectionStartIndex = selectionStartIndex;
2148 _additions = <Selectable>{};
2149 }
2150
2151 void _removeSelectable(Selectable selectable) {
2152 assert(selectables.contains(selectable), 'The selectable is not in this registrar.');
2153 final int index = selectables.indexOf(selectable);
2154 selectables.removeAt(index);
2155 if (index <= currentSelectionEndIndex) {
2156 currentSelectionEndIndex -= 1;
2157 }
2158 if (index <= currentSelectionStartIndex) {
2159 currentSelectionStartIndex -= 1;
2160 }
2161 selectable.removeListener(_handleSelectableGeometryChange);
2162 }
2163
2164 /// Called when this delegate finishes updating the [Selectable]s.
2165 @protected
2166 @mustCallSuper
2167 void didChangeSelectables() {
2168 _updateSelectionGeometry();
2169 }
2170
2171 @override
2172 SelectionGeometry get value => _selectionGeometry;
2173 SelectionGeometry _selectionGeometry = const SelectionGeometry(
2174 hasContent: false,
2175 status: SelectionStatus.none,
2176 );
2177
2178 /// Updates the [value] in this class and notifies listeners if necessary.
2179 void _updateSelectionGeometry() {
2180 final SelectionGeometry newValue = getSelectionGeometry();
2181 if (_selectionGeometry != newValue) {
2182 _selectionGeometry = newValue;
2183 notifyListeners();
2184 }
2185 _updateHandleLayersAndOwners();
2186 }
2187
2188 static Rect _getBoundingBox(Selectable selectable) {
2189 Rect result = selectable.boundingBoxes.first;
2190 for (int index = 1; index < selectable.boundingBoxes.length; index += 1) {
2191 result = result.expandToInclude(selectable.boundingBoxes[index]);
2192 }
2193 return result;
2194 }
2195
2196 /// The compare function this delegate used for determining the selection
2197 /// order of the selectables.
2198 ///
2199 /// Defaults to screen order.
2200 @protected
2201 Comparator<Selectable> get compareOrder => _compareScreenOrder;
2202
2203 static int _compareScreenOrder(Selectable a, Selectable b) {
2204 final Rect rectA = MatrixUtils.transformRect(
2205 a.getTransformTo(null),
2206 _getBoundingBox(a),
2207 );
2208 final Rect rectB = MatrixUtils.transformRect(
2209 b.getTransformTo(null),
2210 _getBoundingBox(b),
2211 );
2212 final int result = _compareVertically(rectA, rectB);
2213 if (result != 0) {
2214 return result;
2215 }
2216 return _compareHorizontally(rectA, rectB);
2217 }
2218
2219 /// Compares two rectangles in the screen order solely by their vertical
2220 /// positions.
2221 ///
2222 /// Returns positive if a is lower, negative if a is higher, 0 if their
2223 /// order can't be determine solely by their vertical position.
2224 static int _compareVertically(Rect a, Rect b) {
2225 // The rectangles overlap so defer to horizontal comparison.
2226 if ((a.top - b.top < _kSelectableVerticalComparingThreshold && a.bottom - b.bottom > - _kSelectableVerticalComparingThreshold) ||
2227 (b.top - a.top < _kSelectableVerticalComparingThreshold && b.bottom - a.bottom > - _kSelectableVerticalComparingThreshold)) {
2228 return 0;
2229 }
2230 if ((a.top - b.top).abs() > _kSelectableVerticalComparingThreshold) {
2231 return a.top > b.top ? 1 : -1;
2232 }
2233 return a.bottom > b.bottom ? 1 : -1;
2234 }
2235
2236 /// Compares two rectangles in the screen order by their horizontal positions
2237 /// assuming one of the rectangles enclose the other rect vertically.
2238 ///
2239 /// Returns positive if a is lower, negative if a is higher.
2240 static int _compareHorizontally(Rect a, Rect b) {
2241 // a encloses b.
2242 if (a.left - b.left < precisionErrorTolerance && a.right - b.right > - precisionErrorTolerance) {
2243 return -1;
2244 }
2245 // b encloses a.
2246 if (b.left - a.left < precisionErrorTolerance && b.right - a.right > - precisionErrorTolerance) {
2247 return 1;
2248 }
2249 if ((a.left - b.left).abs() > precisionErrorTolerance) {
2250 return a.left > b.left ? 1 : -1;
2251 }
2252 return a.right > b.right ? 1 : -1;
2253 }
2254
2255 void _handleSelectableGeometryChange() {
2256 // Geometries of selectable children may change multiple times when handling
2257 // selection events. Ignore these updates since the selection geometry of
2258 // this delegate will be updated after handling the selection events.
2259 if (_isHandlingSelectionEvent) {
2260 return;
2261 }
2262 _updateSelectionGeometry();
2263 }
2264
2265 /// Gets the combined [SelectionGeometry] for child [Selectable]s.
2266 @protected
2267 SelectionGeometry getSelectionGeometry() {
2268 if (currentSelectionEndIndex == -1 ||
2269 currentSelectionStartIndex == -1 ||
2270 selectables.isEmpty) {
2271 // There is no valid selection.
2272 return SelectionGeometry(
2273 status: SelectionStatus.none,
2274 hasContent: selectables.isNotEmpty,
2275 );
2276 }
2277
2278 if (!_extendSelectionInProgress) {
2279 currentSelectionStartIndex = _adjustSelectionIndexBasedOnSelectionGeometry(
2280 currentSelectionStartIndex,
2281 currentSelectionEndIndex,
2282 );
2283 currentSelectionEndIndex = _adjustSelectionIndexBasedOnSelectionGeometry(
2284 currentSelectionEndIndex,
2285 currentSelectionStartIndex,
2286 );
2287 }
2288
2289 // Need to find the non-null start selection point.
2290 SelectionGeometry startGeometry = selectables[currentSelectionStartIndex].value;
2291 final bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex;
2292 int startIndexWalker = currentSelectionStartIndex;
2293 while (startIndexWalker != currentSelectionEndIndex && startGeometry.startSelectionPoint == null) {
2294 startIndexWalker += forwardSelection ? 1 : -1;
2295 startGeometry = selectables[startIndexWalker].value;
2296 }
2297
2298 SelectionPoint? startPoint;
2299 if (startGeometry.startSelectionPoint != null) {
2300 final Matrix4 startTransform = getTransformFrom(selectables[startIndexWalker]);
2301 final Offset start = MatrixUtils.transformPoint(startTransform, startGeometry.startSelectionPoint!.localPosition);
2302 // It can be NaN if it is detached or off-screen.
2303 if (start.isFinite) {
2304 startPoint = SelectionPoint(
2305 localPosition: start,
2306 lineHeight: startGeometry.startSelectionPoint!.lineHeight,
2307 handleType: startGeometry.startSelectionPoint!.handleType,
2308 );
2309 }
2310 }
2311
2312 // Need to find the non-null end selection point.
2313 SelectionGeometry endGeometry = selectables[currentSelectionEndIndex].value;
2314 int endIndexWalker = currentSelectionEndIndex;
2315 while (endIndexWalker != currentSelectionStartIndex && endGeometry.endSelectionPoint == null) {
2316 endIndexWalker += forwardSelection ? -1 : 1;
2317 endGeometry = selectables[endIndexWalker].value;
2318 }
2319 SelectionPoint? endPoint;
2320 if (endGeometry.endSelectionPoint != null) {
2321 final Matrix4 endTransform = getTransformFrom(selectables[endIndexWalker]);
2322 final Offset end = MatrixUtils.transformPoint(endTransform, endGeometry.endSelectionPoint!.localPosition);
2323 // It can be NaN if it is detached or off-screen.
2324 if (end.isFinite) {
2325 endPoint = SelectionPoint(
2326 localPosition: end,
2327 lineHeight: endGeometry.endSelectionPoint!.lineHeight,
2328 handleType: endGeometry.endSelectionPoint!.handleType,
2329 );
2330 }
2331 }
2332
2333 // Need to collect selection rects from selectables ranging from the
2334 // currentSelectionStartIndex to the currentSelectionEndIndex.
2335 final List<Rect> selectionRects = <Rect>[];
2336 final Rect? drawableArea = hasSize ? Rect
2337 .fromLTWH(0, 0, containerSize.width, containerSize.height) : null;
2338 for (int index = currentSelectionStartIndex; index <= currentSelectionEndIndex; index++) {
2339 final List<Rect> currSelectableSelectionRects = selectables[index].value.selectionRects;
2340 final List<Rect> selectionRectsWithinDrawableArea = currSelectableSelectionRects.map((Rect selectionRect) {
2341 final Matrix4 transform = getTransformFrom(selectables[index]);
2342 final Rect localRect = MatrixUtils.transformRect(transform, selectionRect);
2343 return drawableArea?.intersect(localRect) ?? localRect;
2344 }).where((Rect selectionRect) {
2345 return selectionRect.isFinite && !selectionRect.isEmpty;
2346 }).toList();
2347 selectionRects.addAll(selectionRectsWithinDrawableArea);
2348 }
2349
2350 return SelectionGeometry(
2351 startSelectionPoint: startPoint,
2352 endSelectionPoint: endPoint,
2353 selectionRects: selectionRects,
2354 status: startGeometry != endGeometry
2355 ? SelectionStatus.uncollapsed
2356 : startGeometry.status,
2357 // Would have at least one selectable child.
2358 hasContent: true,
2359 );
2360 }
2361
2362 // The currentSelectionStartIndex or currentSelectionEndIndex may not be
2363 // the current index that contains selection edges. This can happen if the
2364 // selection edge is in between two selectables. One of the selectable will
2365 // have its selection collapsed at the index 0 or contentLength depends on
2366 // whether the selection is reversed or not. The current selection index can
2367 // be point to either one.
2368 //
2369 // This method adjusts the index to point to selectable with valid selection.
2370 int _adjustSelectionIndexBasedOnSelectionGeometry(int currentIndex, int towardIndex) {
2371 final bool forward = towardIndex > currentIndex;
2372 while (currentIndex != towardIndex &&
2373 selectables[currentIndex].value.status != SelectionStatus.uncollapsed) {
2374 currentIndex += forward ? 1 : -1;
2375 }
2376 return currentIndex;
2377 }
2378
2379 @override
2380 void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
2381 if (_startHandleLayer == startHandle && _endHandleLayer == endHandle) {
2382 return;
2383 }
2384 _startHandleLayer = startHandle;
2385 _endHandleLayer = endHandle;
2386 _updateHandleLayersAndOwners();
2387 }
2388
2389 /// Pushes both handle layers to the selectables that contain selection edges.
2390 ///
2391 /// This method needs to be called every time the selectables that contain the
2392 /// selection edges change, i.e. [currentSelectionStartIndex] or
2393 /// [currentSelectionEndIndex] changes. Otherwise, the handle may be painted
2394 /// in the wrong place.
2395 void _updateHandleLayersAndOwners() {
2396 LayerLink? effectiveStartHandle = _startHandleLayer;
2397 LayerLink? effectiveEndHandle = _endHandleLayer;
2398 if (effectiveStartHandle != null || effectiveEndHandle != null) {
2399 final Rect? drawableArea = hasSize ? Rect
2400 .fromLTWH(0, 0, containerSize.width, containerSize.height)
2401 .inflate(_kSelectionHandleDrawableAreaPadding) : null;
2402 final bool hideStartHandle = value.startSelectionPoint == null || drawableArea == null || !drawableArea.contains(value.startSelectionPoint!.localPosition);
2403 final bool hideEndHandle = value.endSelectionPoint == null || drawableArea == null|| !drawableArea.contains(value.endSelectionPoint!.localPosition);
2404 effectiveStartHandle = hideStartHandle ? null : _startHandleLayer;
2405 effectiveEndHandle = hideEndHandle ? null : _endHandleLayer;
2406 }
2407 if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
2408 // No valid selection.
2409 if (_startHandleLayerOwner != null) {
2410 _startHandleLayerOwner!.pushHandleLayers(null, null);
2411 _startHandleLayerOwner = null;
2412 }
2413 if (_endHandleLayerOwner != null) {
2414 _endHandleLayerOwner!.pushHandleLayers(null, null);
2415 _endHandleLayerOwner = null;
2416 }
2417 return;
2418 }
2419
2420 if (selectables[currentSelectionStartIndex] != _startHandleLayerOwner) {
2421 _startHandleLayerOwner?.pushHandleLayers(null, null);
2422 }
2423 if (selectables[currentSelectionEndIndex] != _endHandleLayerOwner) {
2424 _endHandleLayerOwner?.pushHandleLayers(null, null);
2425 }
2426
2427 _startHandleLayerOwner = selectables[currentSelectionStartIndex];
2428
2429 if (currentSelectionStartIndex == currentSelectionEndIndex) {
2430 // Selection edges is on the same selectable.
2431 _endHandleLayerOwner = _startHandleLayerOwner;
2432 _startHandleLayerOwner!.pushHandleLayers(effectiveStartHandle, effectiveEndHandle);
2433 return;
2434 }
2435
2436 _startHandleLayerOwner!.pushHandleLayers(effectiveStartHandle, null);
2437 _endHandleLayerOwner = selectables[currentSelectionEndIndex];
2438 _endHandleLayerOwner!.pushHandleLayers(null, effectiveEndHandle);
2439 }
2440
2441 /// Copies the selected contents of all [Selectable]s.
2442 @override
2443 SelectedContent? getSelectedContent() {
2444 final List<SelectedContent> selections = <SelectedContent>[
2445 for (final Selectable selectable in selectables)
2446 if (selectable.getSelectedContent() case final SelectedContent data) data,
2447 ];
2448 if (selections.isEmpty) {
2449 return null;
2450 }
2451 final StringBuffer buffer = StringBuffer();
2452 for (final SelectedContent selection in selections) {
2453 buffer.write(selection.plainText);
2454 }
2455 return SelectedContent(
2456 plainText: buffer.toString(),
2457 );
2458 }
2459
2460 // Clears the selection on all selectables not in the range of
2461 // currentSelectionStartIndex..currentSelectionEndIndex.
2462 //
2463 // If one of the edges does not exist, then this method will clear the selection
2464 // in all selectables except the existing edge.
2465 //
2466 // If neither of the edges exist this method immediately returns.
2467 void _flushInactiveSelections() {
2468 if (currentSelectionStartIndex == -1 && currentSelectionEndIndex == -1) {
2469 return;
2470 }
2471 if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
2472 final int skipIndex = currentSelectionStartIndex == -1 ? currentSelectionEndIndex : currentSelectionStartIndex;
2473 selectables
2474 .where((Selectable target) => target != selectables[skipIndex])
2475 .forEach((Selectable target) => dispatchSelectionEventToChild(target, const ClearSelectionEvent()));
2476 return;
2477 }
2478 final int skipStart = min(currentSelectionStartIndex, currentSelectionEndIndex);
2479 final int skipEnd = max(currentSelectionStartIndex, currentSelectionEndIndex);
2480 for (int index = 0; index < selectables.length; index += 1) {
2481 if (index >= skipStart && index <= skipEnd) {
2482 continue;
2483 }
2484 dispatchSelectionEventToChild(selectables[index], const ClearSelectionEvent());
2485 }
2486 }
2487
2488 /// Selects all contents of all [Selectable]s.
2489 @protected
2490 SelectionResult handleSelectAll(SelectAllSelectionEvent event) {
2491 for (final Selectable selectable in selectables) {
2492 dispatchSelectionEventToChild(selectable, event);
2493 }
2494 currentSelectionStartIndex = 0;
2495 currentSelectionEndIndex = selectables.length - 1;
2496 return SelectionResult.none;
2497 }
2498
2499 SelectionResult _handleSelectBoundary(SelectionEvent event) {
2500 assert(event is SelectWordSelectionEvent || event is SelectParagraphSelectionEvent, 'This method should only be given selection events that select text boundaries.');
2501 late final Offset effectiveGlobalPosition;
2502 if (event.type == SelectionEventType.selectWord) {
2503 effectiveGlobalPosition = (event as SelectWordSelectionEvent).globalPosition;
2504 } else if (event.type == SelectionEventType.selectParagraph) {
2505 effectiveGlobalPosition = (event as SelectParagraphSelectionEvent).globalPosition;
2506 }
2507 SelectionResult? lastSelectionResult;
2508 for (int index = 0; index < selectables.length; index += 1) {
2509 bool globalRectsContainPosition = false;
2510 if (selectables[index].boundingBoxes.isNotEmpty) {
2511 for (final Rect rect in selectables[index].boundingBoxes) {
2512 final Rect globalRect = MatrixUtils.transformRect(selectables[index].getTransformTo(null), rect);
2513 if (globalRect.contains(effectiveGlobalPosition)) {
2514 globalRectsContainPosition = true;
2515 break;
2516 }
2517 }
2518 }
2519 if (globalRectsContainPosition) {
2520 final SelectionGeometry existingGeometry = selectables[index].value;
2521 lastSelectionResult = dispatchSelectionEventToChild(selectables[index], event);
2522 if (index == selectables.length - 1 && lastSelectionResult == SelectionResult.next) {
2523 return SelectionResult.next;
2524 }
2525 if (lastSelectionResult == SelectionResult.next) {
2526 continue;
2527 }
2528 if (index == 0 && lastSelectionResult == SelectionResult.previous) {
2529 return SelectionResult.previous;
2530 }
2531 if (selectables[index].value != existingGeometry) {
2532 // Geometry has changed as a result of select word, need to clear the
2533 // selection of other selectables to keep selection in sync.
2534 selectables
2535 .where((Selectable target) => target != selectables[index])
2536 .forEach((Selectable target) => dispatchSelectionEventToChild(target, const ClearSelectionEvent()));
2537 currentSelectionStartIndex = currentSelectionEndIndex = index;
2538 }
2539 return SelectionResult.end;
2540 } else {
2541 if (lastSelectionResult == SelectionResult.next) {
2542 currentSelectionStartIndex = currentSelectionEndIndex = index - 1;
2543 return SelectionResult.end;
2544 }
2545 }
2546 }
2547 assert(lastSelectionResult == null);
2548 return SelectionResult.end;
2549 }
2550
2551 /// Selects a word in a [Selectable] at the location
2552 /// [SelectWordSelectionEvent.globalPosition].
2553 @protected
2554 SelectionResult handleSelectWord(SelectWordSelectionEvent event) {
2555 return _handleSelectBoundary(event);
2556 }
2557
2558 /// Selects a paragraph in a [Selectable] at the location
2559 /// [SelectParagraphSelectionEvent.globalPosition].
2560 @protected
2561 SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
2562 return _handleSelectBoundary(event);
2563 }
2564
2565 /// Removes the selection of all [Selectable]s this delegate manages.
2566 @protected
2567 SelectionResult handleClearSelection(ClearSelectionEvent event) {
2568 for (final Selectable selectable in selectables) {
2569 dispatchSelectionEventToChild(selectable, event);
2570 }
2571 currentSelectionEndIndex = -1;
2572 currentSelectionStartIndex = -1;
2573 return SelectionResult.none;
2574 }
2575
2576 /// Extend current selection in a certain [TextGranularity].
2577 @protected
2578 SelectionResult handleGranularlyExtendSelection(GranularlyExtendSelectionEvent event) {
2579 assert((currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1));
2580 if (currentSelectionStartIndex == -1) {
2581 if (event.forward) {
2582 currentSelectionStartIndex = currentSelectionEndIndex = 0;
2583 } else {
2584 currentSelectionStartIndex = currentSelectionEndIndex = selectables.length;
2585 }
2586 }
2587 int targetIndex = event.isEnd ? currentSelectionEndIndex : currentSelectionStartIndex;
2588 SelectionResult result = dispatchSelectionEventToChild(selectables[targetIndex], event);
2589 if (event.forward) {
2590 assert(result != SelectionResult.previous);
2591 while (targetIndex < selectables.length - 1 && result == SelectionResult.next) {
2592 targetIndex += 1;
2593 result = dispatchSelectionEventToChild(selectables[targetIndex], event);
2594 assert(result != SelectionResult.previous);
2595 }
2596 } else {
2597 assert(result != SelectionResult.next);
2598 while (targetIndex > 0 && result == SelectionResult.previous) {
2599 targetIndex -= 1;
2600 result = dispatchSelectionEventToChild(selectables[targetIndex], event);
2601 assert(result != SelectionResult.next);
2602 }
2603 }
2604 if (event.isEnd) {
2605 currentSelectionEndIndex = targetIndex;
2606 } else {
2607 currentSelectionStartIndex = targetIndex;
2608 }
2609 return result;
2610 }
2611
2612 /// Extend current selection in a certain [TextGranularity].
2613 @protected
2614 SelectionResult handleDirectionallyExtendSelection(DirectionallyExtendSelectionEvent event) {
2615 assert((currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1));
2616 if (currentSelectionStartIndex == -1) {
2617 currentSelectionStartIndex = currentSelectionEndIndex = switch (event.direction) {
2618 SelectionExtendDirection.previousLine || SelectionExtendDirection.backward => selectables.length - 1,
2619 SelectionExtendDirection.nextLine || SelectionExtendDirection.forward => 0,
2620 };
2621 }
2622 int targetIndex = event.isEnd ? currentSelectionEndIndex : currentSelectionStartIndex;
2623 SelectionResult result = dispatchSelectionEventToChild(selectables[targetIndex], event);
2624 switch (event.direction) {
2625 case SelectionExtendDirection.previousLine:
2626 assert(result == SelectionResult.end || result == SelectionResult.previous);
2627 if (result == SelectionResult.previous) {
2628 if (targetIndex > 0) {
2629 targetIndex -= 1;
2630 result = dispatchSelectionEventToChild(
2631 selectables[targetIndex],
2632 event.copyWith(direction: SelectionExtendDirection.backward),
2633 );
2634 assert(result == SelectionResult.end);
2635 }
2636 }
2637 case SelectionExtendDirection.nextLine:
2638 assert(result == SelectionResult.end || result == SelectionResult.next);
2639 if (result == SelectionResult.next) {
2640 if (targetIndex < selectables.length - 1) {
2641 targetIndex += 1;
2642 result = dispatchSelectionEventToChild(
2643 selectables[targetIndex],
2644 event.copyWith(direction: SelectionExtendDirection.forward),
2645 );
2646 assert(result == SelectionResult.end);
2647 }
2648 }
2649 case SelectionExtendDirection.forward:
2650 case SelectionExtendDirection.backward:
2651 assert(result == SelectionResult.end);
2652 }
2653 if (event.isEnd) {
2654 currentSelectionEndIndex = targetIndex;
2655 } else {
2656 currentSelectionStartIndex = targetIndex;
2657 }
2658 return result;
2659 }
2660
2661 /// Updates the selection edges.
2662 @protected
2663 SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
2664 if (event.type == SelectionEventType.endEdgeUpdate) {
2665 return currentSelectionEndIndex == -1 ? _initSelection(event, isEnd: true) : _adjustSelection(event, isEnd: true);
2666 }
2667 return currentSelectionStartIndex == -1 ? _initSelection(event, isEnd: false) : _adjustSelection(event, isEnd: false);
2668 }
2669
2670 @override
2671 SelectionResult dispatchSelectionEvent(SelectionEvent event) {
2672 final bool selectionWillBeInProgress = event is! ClearSelectionEvent;
2673 if (!_selectionInProgress && selectionWillBeInProgress) {
2674 // Sort the selectable every time a selection start.
2675 selectables.sort(compareOrder);
2676 }
2677 _selectionInProgress = selectionWillBeInProgress;
2678 _isHandlingSelectionEvent = true;
2679 late SelectionResult result;
2680 switch (event.type) {
2681 case SelectionEventType.startEdgeUpdate:
2682 case SelectionEventType.endEdgeUpdate:
2683 _extendSelectionInProgress = false;
2684 result = handleSelectionEdgeUpdate(event as SelectionEdgeUpdateEvent);
2685 case SelectionEventType.clear:
2686 _extendSelectionInProgress = false;
2687 result = handleClearSelection(event as ClearSelectionEvent);
2688 case SelectionEventType.selectAll:
2689 _extendSelectionInProgress = false;
2690 result = handleSelectAll(event as SelectAllSelectionEvent);
2691 case SelectionEventType.selectWord:
2692 _extendSelectionInProgress = false;
2693 result = handleSelectWord(event as SelectWordSelectionEvent);
2694 case SelectionEventType.selectParagraph:
2695 _extendSelectionInProgress = false;
2696 result = handleSelectParagraph(event as SelectParagraphSelectionEvent);
2697 case SelectionEventType.granularlyExtendSelection:
2698 _extendSelectionInProgress = true;
2699 result = handleGranularlyExtendSelection(event as GranularlyExtendSelectionEvent);
2700 case SelectionEventType.directionallyExtendSelection:
2701 _extendSelectionInProgress = true;
2702 result = handleDirectionallyExtendSelection(event as DirectionallyExtendSelectionEvent);
2703 }
2704 _isHandlingSelectionEvent = false;
2705 _updateSelectionGeometry();
2706 return result;
2707 }
2708
2709 @override
2710 void dispose() {
2711 for (final Selectable selectable in selectables) {
2712 selectable.removeListener(_handleSelectableGeometryChange);
2713 }
2714 selectables = const <Selectable>[];
2715 _scheduledSelectableUpdate = false;
2716 super.dispose();
2717 }
2718
2719 /// Ensures the [Selectable] child has received up to date selection event.
2720 ///
2721 /// This method is called when a new [Selectable] is added to the delegate,
2722 /// and its screen location falls into the previous selection.
2723 ///
2724 /// Subclasses are responsible for updating the selection of this newly added
2725 /// [Selectable].
2726 @protected
2727 void ensureChildUpdated(Selectable selectable);
2728
2729 /// Dispatches a selection event to a specific [Selectable].
2730 ///
2731 /// Override this method if subclasses need to generate additional events or
2732 /// treatments prior to sending the [SelectionEvent].
2733 @protected
2734 SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
2735 return selectable.dispatchSelectionEvent(event);
2736 }
2737
2738 /// Initializes the selection of the selectable children.
2739 ///
2740 /// The goal is to find the selectable child that contains the selection edge.
2741 /// Returns [SelectionResult.end] if the selection edge ends on any of the
2742 /// children. Otherwise, it returns [SelectionResult.previous] if the selection
2743 /// does not reach any of its children. Returns [SelectionResult.next]
2744 /// if the selection reaches the end of its children.
2745 ///
2746 /// Ideally, this method should only be called twice at the beginning of the
2747 /// drag selection, once for start edge update event, once for end edge update
2748 /// event.
2749 SelectionResult _initSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) {
2750 assert((isEnd && currentSelectionEndIndex == -1) || (!isEnd && currentSelectionStartIndex == -1));
2751 int newIndex = -1;
2752 bool hasFoundEdgeIndex = false;
2753 SelectionResult? result;
2754 for (int index = 0; index < selectables.length && !hasFoundEdgeIndex; index += 1) {
2755 final Selectable child = selectables[index];
2756 final SelectionResult childResult = dispatchSelectionEventToChild(child, event);
2757 switch (childResult) {
2758 case SelectionResult.next:
2759 case SelectionResult.none:
2760 newIndex = index;
2761 case SelectionResult.end:
2762 newIndex = index;
2763 result = SelectionResult.end;
2764 hasFoundEdgeIndex = true;
2765 case SelectionResult.previous:
2766 hasFoundEdgeIndex = true;
2767 if (index == 0) {
2768 newIndex = 0;
2769 result = SelectionResult.previous;
2770 }
2771 result ??= SelectionResult.end;
2772 case SelectionResult.pending:
2773 newIndex = index;
2774 result = SelectionResult.pending;
2775 hasFoundEdgeIndex = true;
2776 }
2777 }
2778
2779 if (newIndex == -1) {
2780 assert(selectables.isEmpty);
2781 return SelectionResult.none;
2782 }
2783 if (isEnd) {
2784 currentSelectionEndIndex = newIndex;
2785 } else {
2786 currentSelectionStartIndex = newIndex;
2787 }
2788 _flushInactiveSelections();
2789 // The result can only be null if the loop went through the entire list
2790 // without any of the selection returned end or previous. In this case, the
2791 // caller of this method needs to find the next selectable in their list.
2792 return result ?? SelectionResult.next;
2793 }
2794
2795 /// Adjusts the selection based on the drag selection update event if there
2796 /// is already a selectable child that contains the selection edge.
2797 ///
2798 /// This method starts by sending the selection event to the current
2799 /// selectable that contains the selection edge, and finds forward or backward
2800 /// if that selectable no longer contains the selection edge.
2801 SelectionResult _adjustSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) {
2802 assert(() {
2803 if (isEnd) {
2804 assert(currentSelectionEndIndex < selectables.length && currentSelectionEndIndex >= 0);
2805 return true;
2806 }
2807 assert(currentSelectionStartIndex < selectables.length && currentSelectionStartIndex >= 0);
2808 return true;
2809 }());
2810 SelectionResult? finalResult;
2811 // Determines if the edge being adjusted is within the current viewport.
2812 // - If so, we begin the search for the new selection edge position at the
2813 // currentSelectionEndIndex/currentSelectionStartIndex.
2814 // - If not, we attempt to locate the new selection edge starting from
2815 // the opposite end.
2816 // - If neither edge is in the current viewport, the search for the new
2817 // selection edge position begins at 0.
2818 //
2819 // This can happen when there is a scrollable child and the edge being adjusted
2820 // has been scrolled out of view.
2821 final bool isCurrentEdgeWithinViewport = isEnd ? _selectionGeometry.endSelectionPoint != null : _selectionGeometry.startSelectionPoint != null;
2822 final bool isOppositeEdgeWithinViewport = isEnd ? _selectionGeometry.startSelectionPoint != null : _selectionGeometry.endSelectionPoint != null;
2823 int newIndex = switch ((isEnd, isCurrentEdgeWithinViewport, isOppositeEdgeWithinViewport)) {
2824 (true, true, true) => currentSelectionEndIndex,
2825 (true, true, false) => currentSelectionEndIndex,
2826 (true, false, true) => currentSelectionStartIndex,
2827 (true, false, false) => 0,
2828 (false, true, true) => currentSelectionStartIndex,
2829 (false, true, false) => currentSelectionStartIndex,
2830 (false, false, true) => currentSelectionEndIndex,
2831 (false, false, false) => 0,
2832 };
2833 bool? forward;
2834 late SelectionResult currentSelectableResult;
2835 // This loop sends the selection event to one of the following to determine
2836 // the direction of the search.
2837 // - currentSelectionEndIndex/currentSelectionStartIndex if the current edge
2838 // is in the current viewport.
2839 // - The opposite edge index if the current edge is not in the current viewport.
2840 // - Index 0 if neither edge is in the current viewport.
2841 //
2842 // If the result is `SelectionResult.next`, this loop look backward.
2843 // Otherwise, it looks forward.
2844 //
2845 // The terminate condition are:
2846 // 1. the selectable returns end, pending, none.
2847 // 2. the selectable returns previous when looking forward.
2848 // 2. the selectable returns next when looking backward.
2849 while (newIndex < selectables.length && newIndex >= 0 && finalResult == null) {
2850 currentSelectableResult = dispatchSelectionEventToChild(selectables[newIndex], event);
2851 switch (currentSelectableResult) {
2852 case SelectionResult.end:
2853 case SelectionResult.pending:
2854 case SelectionResult.none:
2855 finalResult = currentSelectableResult;
2856 case SelectionResult.next:
2857 if (forward == false) {
2858 newIndex += 1;
2859 finalResult = SelectionResult.end;
2860 } else if (newIndex == selectables.length - 1) {
2861 finalResult = currentSelectableResult;
2862 } else {
2863 forward = true;
2864 newIndex += 1;
2865 }
2866 case SelectionResult.previous:
2867 if (forward ?? false) {
2868 newIndex -= 1;
2869 finalResult = SelectionResult.end;
2870 } else if (newIndex == 0) {
2871 finalResult = currentSelectableResult;
2872 } else {
2873 forward = false;
2874 newIndex -= 1;
2875 }
2876 }
2877 }
2878 if (isEnd) {
2879 currentSelectionEndIndex = newIndex;
2880 } else {
2881 currentSelectionStartIndex = newIndex;
2882 }
2883 _flushInactiveSelections();
2884 return finalResult!;
2885 }
2886}
2887
2888/// Signature for a widget builder that builds a context menu for the given
2889/// [SelectableRegionState].
2890///
2891/// See also:
2892///
2893/// * [EditableTextContextMenuBuilder], which performs the same role for
2894/// [EditableText].
2895typedef SelectableRegionContextMenuBuilder = Widget Function(
2896 BuildContext context,
2897 SelectableRegionState selectableRegionState,
2898);
2899