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