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