@@ -352,7 +352,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
352352 _showToolbar (location: details.globalPosition);
353353 }
354354 } else {
355- _clearSelection ();
355+ hideToolbar ();
356+ _collapseSelectionAt (offset: details.globalPosition);
356357 }
357358 };
358359 instance.onSecondaryTapDown = _handleRightClickDown;
@@ -472,6 +473,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
472473 (TapAndPanGestureRecognizer instance) {
473474 instance
474475 ..onTapDown = _startNewMouseSelectionGesture
476+ ..onTapUp = _handleMouseTapUp
475477 ..onDragStart = _handleMouseDragStart
476478 ..onDragUpdate = _handleMouseDragUpdate
477479 ..onDragEnd = _handleMouseDragEnd
@@ -498,7 +500,17 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
498500 case 1 :
499501 widget.focusNode.requestFocus ();
500502 hideToolbar ();
501- _clearSelection ();
503+ switch (defaultTargetPlatform) {
504+ case TargetPlatform .android:
505+ case TargetPlatform .fuchsia:
506+ case TargetPlatform .iOS:
507+ // On mobile platforms the selection is set on tap up.
508+ break ;
509+ case TargetPlatform .macOS:
510+ case TargetPlatform .linux:
511+ case TargetPlatform .windows:
512+ _collapseSelectionAt (offset: details.globalPosition);
513+ }
502514 case 2 :
503515 _selectWordAt (offset: details.globalPosition);
504516 }
@@ -528,6 +540,24 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
528540 _updateSelectedContentIfNeeded ();
529541 }
530542
543+ void _handleMouseTapUp (TapDragUpDetails details) {
544+ switch (_getEffectiveConsecutiveTapCount (details.consecutiveTapCount)) {
545+ case 1 :
546+ switch (defaultTargetPlatform) {
547+ case TargetPlatform .android:
548+ case TargetPlatform .fuchsia:
549+ case TargetPlatform .iOS:
550+ _collapseSelectionAt (offset: details.globalPosition);
551+ case TargetPlatform .macOS:
552+ case TargetPlatform .linux:
553+ case TargetPlatform .windows:
554+ // On desktop platforms the selection is set on tap down.
555+ break ;
556+ }
557+ }
558+ _updateSelectedContentIfNeeded ();
559+ }
560+
531561 void _updateSelectedContentIfNeeded () {
532562 if (_lastSelectedContent? .plainText != _selectable? .getSelectedContent ()? .plainText) {
533563 _lastSelectedContent = _selectable? .getSelectedContent ();
@@ -586,8 +616,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
586616 // keep the current selection, if not then collapse it.
587617 final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection (globalPosition: details.globalPosition);
588618 if (! lastSecondaryTapDownPositionWasOnActiveSelection) {
589- _selectStartTo (offset: lastSecondaryTapDownPosition! );
590- _selectEndTo (offset: lastSecondaryTapDownPosition! );
619+ _collapseSelectionAt (offset: lastSecondaryTapDownPosition! );
591620 }
592621 _showHandles ();
593622 _showToolbar (location: lastSecondaryTapDownPosition);
@@ -612,8 +641,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
612641 // keep the current selection, if not then collapse it.
613642 final bool lastSecondaryTapDownPositionWasOnActiveSelection = _positionIsOnActiveSelection (globalPosition: details.globalPosition);
614643 if (! lastSecondaryTapDownPositionWasOnActiveSelection) {
615- _selectStartTo (offset: lastSecondaryTapDownPosition! );
616- _selectEndTo (offset: lastSecondaryTapDownPosition! );
644+ _collapseSelectionAt (offset: lastSecondaryTapDownPosition! );
617645 }
618646 _showHandles ();
619647 _showToolbar (location: lastSecondaryTapDownPosition);
@@ -925,8 +953,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
925953 /// See also:
926954 /// * [_selectStartTo] , which sets or updates selection start edge.
927955 /// * [_finalizeSelection] , which stops the `continuous` updates.
928- /// * [_clearSelection] , which clear the ongoing selection.
956+ /// * [_clearSelection] , which clears the ongoing selection.
929957 /// * [_selectWordAt] , which selects a whole word at the location.
958+ /// * [_collapseSelectionAt] , which collapses the selection at the location.
930959 /// * [selectAll] , which selects the entire content.
931960 void _selectEndTo ({required Offset offset, bool continuous = false , TextGranularity ? textGranularity}) {
932961 if (! continuous) {
@@ -964,8 +993,9 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
964993 /// See also:
965994 /// * [_selectEndTo] , which sets or updates selection end edge.
966995 /// * [_finalizeSelection] , which stops the `continuous` updates.
967- /// * [_clearSelection] , which clear the ongoing selection.
996+ /// * [_clearSelection] , which clears the ongoing selection.
968997 /// * [_selectWordAt] , which selects a whole word at the location.
998+ /// * [_collapseSelectionAt] , which collapses the selection at the location.
969999 /// * [selectAll] , which selects the entire content.
9701000 void _selectStartTo ({required Offset offset, bool continuous = false , TextGranularity ? textGranularity}) {
9711001 if (! continuous) {
@@ -978,6 +1008,20 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
9781008 }
9791009 }
9801010
1011+ /// Collapses the selection at the given `offset` location.
1012+ ///
1013+ /// See also:
1014+ /// * [_selectStartTo] , which sets or updates selection start edge.
1015+ /// * [_selectEndTo] , which sets or updates selection end edge.
1016+ /// * [_finalizeSelection] , which stops the `continuous` updates.
1017+ /// * [_clearSelection] , which clears the ongoing selection.
1018+ /// * [_selectWordAt] , which selects a whole word at the location.
1019+ /// * [selectAll] , which selects the entire content.
1020+ void _collapseSelectionAt ({required Offset offset}) {
1021+ _selectStartTo (offset: offset);
1022+ _selectEndTo (offset: offset);
1023+ }
1024+
9811025 /// Selects a whole word at the `offset` location.
9821026 ///
9831027 /// If the whole word is already in the current selection, selection won't
@@ -991,7 +1035,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
9911035 /// * [_selectStartTo] , which sets or updates selection start edge.
9921036 /// * [_selectEndTo] , which sets or updates selection end edge.
9931037 /// * [_finalizeSelection] , which stops the `continuous` updates.
994- /// * [_clearSelection] , which clear the ongoing selection.
1038+ /// * [_clearSelection] , which clears the ongoing selection.
1039+ /// * [_collapseSelectionAt] , which collapses the selection at the location.
9951040 /// * [selectAll] , which selects the entire content.
9961041 void _selectWordAt ({required Offset offset}) {
9971042 // There may be other selection ongoing.
@@ -1881,7 +1926,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
18811926
18821927 SelectionPoint ? startPoint;
18831928 if (startGeometry.startSelectionPoint != null ) {
1884- final Matrix4 startTransform = getTransformFrom (selectables[startIndexWalker]);
1929+ final Matrix4 startTransform = getTransformFrom (selectables[startIndexWalker]);
18851930 final Offset start = MatrixUtils .transformPoint (startTransform, startGeometry.startSelectionPoint! .localPosition);
18861931 // It can be NaN if it is detached or off-screen.
18871932 if (start.isFinite) {
@@ -1902,7 +1947,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
19021947 }
19031948 SelectionPoint ? endPoint;
19041949 if (endGeometry.endSelectionPoint != null ) {
1905- final Matrix4 endTransform = getTransformFrom (selectables[endIndexWalker]);
1950+ final Matrix4 endTransform = getTransformFrom (selectables[endIndexWalker]);
19061951 final Offset end = MatrixUtils .transformPoint (endTransform, endGeometry.endSelectionPoint! .localPosition);
19071952 // It can be NaN if it is detached or off-screen.
19081953 if (end.isFinite) {
@@ -1986,8 +2031,8 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
19862031 final Rect ? drawableArea = hasSize ? Rect
19872032 .fromLTWH (0 , 0 , containerSize.width, containerSize.height)
19882033 .inflate (_kSelectionHandleDrawableAreaPadding) : null ;
1989- final bool hideStartHandle = value.startSelectionPoint == null || drawableArea == null || ! drawableArea.contains (value.startSelectionPoint! .localPosition);
1990- final bool hideEndHandle = value.endSelectionPoint == null || drawableArea == null || ! drawableArea.contains (value.endSelectionPoint! .localPosition);
2034+ final bool hideStartHandle = value.startSelectionPoint == null || drawableArea == null || ! drawableArea.contains (value.startSelectionPoint! .localPosition);
2035+ final bool hideEndHandle = value.endSelectionPoint == null || drawableArea == null || ! drawableArea.contains (value.endSelectionPoint! .localPosition);
19912036 effectiveStartHandle = hideStartHandle ? null : _startHandleLayer;
19922037 effectiveEndHandle = hideEndHandle ? null : _endHandleLayer;
19932038 }
@@ -2047,6 +2092,34 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
20472092 );
20482093 }
20492094
2095+ // Clears the selection on all selectables not in the range of
2096+ // currentSelectionStartIndex..currentSelectionEndIndex.
2097+ //
2098+ // If one of the edges does not exist, then this method will clear the selection
2099+ // in all selectables except the existing edge.
2100+ //
2101+ // If neither of the edges exist this method immediately returns.
2102+ void _flushInactiveSelections () {
2103+ if (currentSelectionStartIndex == - 1 && currentSelectionEndIndex == - 1 ) {
2104+ return ;
2105+ }
2106+ if (currentSelectionStartIndex == - 1 || currentSelectionEndIndex == - 1 ) {
2107+ final int skipIndex = currentSelectionStartIndex == - 1 ? currentSelectionEndIndex : currentSelectionStartIndex;
2108+ selectables
2109+ .where ((Selectable target) => target != selectables[skipIndex])
2110+ .forEach ((Selectable target) => dispatchSelectionEventToChild (target, const ClearSelectionEvent ()));
2111+ return ;
2112+ }
2113+ final int skipStart = min (currentSelectionStartIndex, currentSelectionEndIndex);
2114+ final int skipEnd = max (currentSelectionStartIndex, currentSelectionEndIndex);
2115+ for (int index = 0 ; index < selectables.length; index += 1 ) {
2116+ if (index >= skipStart && index <= skipEnd) {
2117+ continue ;
2118+ }
2119+ dispatchSelectionEventToChild (selectables[index], const ClearSelectionEvent ());
2120+ }
2121+ }
2122+
20502123 /// Selects all contents of all selectables.
20512124 @protected
20522125 SelectionResult handleSelectAll (SelectAllSelectionEvent event) {
@@ -2290,7 +2363,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
22902363 bool hasFoundEdgeIndex = false ;
22912364 SelectionResult ? result;
22922365 for (int index = 0 ; index < selectables.length && ! hasFoundEdgeIndex; index += 1 ) {
2293- final Selectable child = selectables[index];
2366+ final Selectable child = selectables[index];
22942367 final SelectionResult childResult = dispatchSelectionEventToChild (child, event);
22952368 switch (childResult) {
22962369 case SelectionResult .next:
@@ -2323,6 +2396,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
23232396 } else {
23242397 currentSelectionStartIndex = newIndex;
23252398 }
2399+ _flushInactiveSelections ();
23262400 // The result can only be null if the loop went through the entire list
23272401 // without any of the selection returned end or previous. In this case, the
23282402 // caller of this method needs to find the next selectable in their list.
@@ -2345,13 +2419,39 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
23452419 return true ;
23462420 }());
23472421 SelectionResult ? finalResult;
2348- int newIndex = isEnd ? currentSelectionEndIndex : currentSelectionStartIndex;
2422+ // Determines if the edge being adjusted is within the current viewport.
2423+ // - If so, we begin the search for the new selection edge position at the
2424+ // currentSelectionEndIndex/currentSelectionStartIndex.
2425+ // - If not, we attempt to locate the new selection edge starting from
2426+ // the opposite end.
2427+ // - If neither edge is in the current viewport, the search for the new
2428+ // selection edge position begins at 0.
2429+ //
2430+ // This can happen when there is a scrollable child and the edge being adjusted
2431+ // has been scrolled out of view.
2432+ final bool isCurrentEdgeWithinViewport = isEnd ? _selectionGeometry.endSelectionPoint != null : _selectionGeometry.startSelectionPoint != null ;
2433+ final bool isOppositeEdgeWithinViewport = isEnd ? _selectionGeometry.startSelectionPoint != null : _selectionGeometry.endSelectionPoint != null ;
2434+ int newIndex = switch ((isEnd, isCurrentEdgeWithinViewport, isOppositeEdgeWithinViewport)) {
2435+ (true , true , true ) => currentSelectionEndIndex,
2436+ (true , true , false ) => currentSelectionEndIndex,
2437+ (true , false , true ) => currentSelectionStartIndex,
2438+ (true , false , false ) => 0 ,
2439+ (false , true , true ) => currentSelectionStartIndex,
2440+ (false , true , false ) => currentSelectionStartIndex,
2441+ (false , false , true ) => currentSelectionEndIndex,
2442+ (false , false , false ) => 0 ,
2443+ };
23492444 bool ? forward;
23502445 late SelectionResult currentSelectableResult;
2351- // This loop sends the selection event to the
2352- // currentSelectionEndIndex/currentSelectionStartIndex to determine the
2353- // direction of the search. If the result is `SelectionResult.next`, this
2354- // loop look backward. Otherwise, it looks forward.
2446+ // This loop sends the selection event to one of the following to determine
2447+ // the direction of the search.
2448+ // - currentSelectionEndIndex/currentSelectionStartIndex if the current edge
2449+ // is in the current viewport.
2450+ // - The opposite edge index if the current edge is not in the current viewport.
2451+ // - Index 0 if neither edge is in the current viewport.
2452+ //
2453+ // If the result is `SelectionResult.next`, this loop look backward.
2454+ // Otherwise, it looks forward.
23552455 //
23562456 // The terminate condition are:
23572457 // 1. the selectable returns end, pending, none.
@@ -2391,6 +2491,7 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
23912491 } else {
23922492 currentSelectionStartIndex = newIndex;
23932493 }
2494+ _flushInactiveSelections ();
23942495 return finalResult! ;
23952496 }
23962497}
0 commit comments