1 /*
<lambda>null2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.foundation.text.selection
18 
19 import androidx.annotation.VisibleForTesting
20 import androidx.collection.LongObjectMap
21 import androidx.collection.emptyLongObjectMap
22 import androidx.collection.mutableLongIntMapOf
23 import androidx.collection.mutableLongObjectMapOf
24 import androidx.compose.foundation.ComposeFoundationFlags
25 import androidx.compose.foundation.ExperimentalFoundationApi
26 import androidx.compose.foundation.focusable
27 import androidx.compose.foundation.gestures.awaitAllPointersUpWithSlopDetection
28 import androidx.compose.foundation.gestures.awaitEachGesture
29 import androidx.compose.foundation.gestures.awaitPrimaryFirstDown
30 import androidx.compose.foundation.internal.checkPreconditionNotNull
31 import androidx.compose.foundation.internal.requirePrecondition
32 import androidx.compose.foundation.internal.requirePreconditionNotNull
33 import androidx.compose.foundation.text.Handle
34 import androidx.compose.foundation.text.TextDragObserver
35 import androidx.compose.foundation.text.input.internal.coerceIn
36 import androidx.compose.foundation.text.isPositionInsideSelection
37 import androidx.compose.foundation.text.selection.Selection.AnchorInfo
38 import androidx.compose.runtime.MutableState
39 import androidx.compose.runtime.State
40 import androidx.compose.runtime.getValue
41 import androidx.compose.runtime.mutableStateOf
42 import androidx.compose.runtime.setValue
43 import androidx.compose.ui.Modifier
44 import androidx.compose.ui.focus.FocusRequester
45 import androidx.compose.ui.focus.focusRequester
46 import androidx.compose.ui.focus.onFocusChanged
47 import androidx.compose.ui.geometry.Offset
48 import androidx.compose.ui.geometry.Rect
49 import androidx.compose.ui.geometry.isSpecified
50 import androidx.compose.ui.geometry.isUnspecified
51 import androidx.compose.ui.hapticfeedback.HapticFeedback
52 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
53 import androidx.compose.ui.input.key.KeyEvent
54 import androidx.compose.ui.input.key.onKeyEvent
55 import androidx.compose.ui.input.pointer.PointerEventPass
56 import androidx.compose.ui.input.pointer.pointerInput
57 import androidx.compose.ui.layout.LayoutCoordinates
58 import androidx.compose.ui.layout.boundsInWindow
59 import androidx.compose.ui.layout.onGloballyPositioned
60 import androidx.compose.ui.layout.positionInRoot
61 import androidx.compose.ui.layout.positionInWindow
62 import androidx.compose.ui.platform.TextToolbar
63 import androidx.compose.ui.platform.TextToolbarStatus
64 import androidx.compose.ui.text.AnnotatedString
65 import androidx.compose.ui.text.buildAnnotatedString
66 import androidx.compose.ui.unit.IntSize
67 import androidx.compose.ui.util.fastAll
68 import androidx.compose.ui.util.fastAny
69 import androidx.compose.ui.util.fastFold
70 import androidx.compose.ui.util.fastForEach
71 import androidx.compose.ui.util.fastForEachIndexed
72 import androidx.compose.ui.util.fastMapNotNull
73 import kotlin.math.absoluteValue
74 
75 /** A bridge class between user interaction to the text composables for text selection. */
76 internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
77 
78     private val _selection: MutableState<Selection?> = mutableStateOf(null)
79 
80     /** The current selection. */
81     var selection: Selection?
82         get() = _selection.value
83         set(value) {
84             _selection.value = value
85             if (value != null) {
86                 updateHandleOffsets()
87             }
88         }
89 
90     /** Is touch mode active */
91     private val _isInTouchMode = mutableStateOf(true)
92     var isInTouchMode: Boolean
93         get() = _isInTouchMode.value
94         set(value) {
95             if (_isInTouchMode.value != value) {
96                 _isInTouchMode.value = value
97                 updateSelectionToolbar()
98             }
99         }
100 
101     /**
102      * The manager will invoke this every time it comes to the conclusion that the selection should
103      * change. The expectation is that this callback will end up causing `setSelection` to get
104      * called. This is what makes this a "controlled component".
105      */
106     var onSelectionChange: (Selection?) -> Unit = { selection = it }
107         set(newOnSelectionChange) {
108             // Wrap the given lambda with one that sets the selection immediately.
109             // The onSelectionChange loop requires a composition to happen for the selection
110             // to be updated, so we want to shorten that loop for gesture use cases where
111             // multiple selection changing events can be acted on within a single composition
112             // loop. Previous selection is used as part of that loop so keeping it up to date
113             // is important.
114             field = { newSelection ->
115                 selection = newSelection
116                 newOnSelectionChange(newSelection)
117             }
118         }
119 
120     /** [HapticFeedback] handle to perform haptic feedback. */
121     var hapticFeedBack: HapticFeedback? = null
122 
123     /** A handler to perform a Copy action */
124     var onCopyHandler: ((AnnotatedString) -> Unit)? = null
125 
126     /** [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M). */
127     var textToolbar: TextToolbar? = null
128 
129     /** Focus requester used to request focus when selection becomes active. */
130     var focusRequester: FocusRequester = FocusRequester()
131 
132     /** Return true if the corresponding SelectionContainer has a child that is focused. */
133     var hasFocus: Boolean by mutableStateOf(false)
134 
135     /** Return true if dragging gesture is currently in process. */
136     private val isDraggingInProgress
137         get() = draggingHandle != null
138 
139     /** Modifier for selection container. */
140     val modifier
141         get() =
142             Modifier.onClearSelectionRequested { onRelease() }
143                 .onGloballyPositioned { containerLayoutCoordinates = it }
144                 .focusRequester(focusRequester)
145                 .onFocusChanged { focusState ->
146                     if (!focusState.hasFocus && hasFocus) {
147                         onRelease()
148                     }
149                     this.hasFocus = focusState.hasFocus
150                 }
151                 .focusable()
152                 .updateSelectionTouchMode { isInTouchMode = it }
153                 .onKeyEvent {
154                     if (isCopyKeyEvent(it)) {
155                         copy()
156                         true
157                     } else {
158                         false
159                     }
160                 }
161                 .then(if (shouldShowMagnifier) Modifier.selectionMagnifier(this) else Modifier)
162                 .addContextMenuComponents()
163 
164     private var previousPosition: Offset? = null
165 
166     /** Layout Coordinates of the selection container. */
167     var containerLayoutCoordinates: LayoutCoordinates? = null
168         set(value) {
169             field = value
170             if (hasFocus && selection != null) {
171                 val positionInWindow = value?.positionInWindow()
172                 if (previousPosition != positionInWindow) {
173                     previousPosition = positionInWindow
174                     updateHandleOffsets()
175                     updateSelectionToolbar()
176                 }
177             }
178         }
179 
180     /**
181      * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
182      * recalculated.
183      */
184     internal var dragBeginPosition by mutableStateOf(Offset.Zero)
185         private set
186 
187     /**
188      * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
189      * it will be zeroed out.
190      */
191     internal var dragTotalDistance by mutableStateOf(Offset.Zero)
192         private set
193 
194     /**
195      * The calculated position of the start handle in the [SelectionContainer] coordinates. It is
196      * null when handle shouldn't be displayed. It is a [State] so reading it during the composition
197      * will cause recomposition every time the position has been changed.
198      */
199     var startHandlePosition: Offset? by mutableStateOf(null)
200         private set
201 
202     /**
203      * The calculated position of the end handle in the [SelectionContainer] coordinates. It is null
204      * when handle shouldn't be displayed. It is a [State] so reading it during the composition will
205      * cause recomposition every time the position has been changed.
206      */
207     var endHandlePosition: Offset? by mutableStateOf(null)
208         private set
209 
210     /**
211      * The handle that is currently being dragged, or null when no handle is being dragged. To get
212      * the position of the last drag event, use [currentDragPosition].
213      */
214     var draggingHandle: Handle? by mutableStateOf(null)
215         private set
216 
217     /** Line height on start handle position */
218     val startHandleLineHeight: Float
219         get() {
220             val selection = this.selection ?: return 0f
221             val selectable = selection.start.let(::getAnchorSelectable) ?: return 0f
222             return selectable.getLineHeight(selection.start.offset)
223         }
224 
225     /** Line height on end handle position */
226     val endHandleLineHeight: Float
227         get() {
228             val selection = this.selection ?: return 0f
229             val selectable = selection.end.let(::getAnchorSelectable) ?: return 0f
230             return selectable.getLineHeight(selection.end.offset)
231         }
232 
233     /**
234      * When a handle is being dragged (i.e. [draggingHandle] is non-null), this is the last position
235      * of the actual drag event. It is not clamped to handle positions. Null when not being dragged.
236      */
237     var currentDragPosition: Offset? by mutableStateOf(null)
238         private set
239 
240     private val shouldShowMagnifier
241         get() = isDraggingInProgress && isInTouchMode && !isTriviallyCollapsedSelection()
242 
243     @VisibleForTesting internal var previousSelectionLayout: SelectionLayout? = null
244 
245     init {
246         selectionRegistrar.onPositionChangeCallback = { selectableId ->
247             if (selectableId in selectionRegistrar.subselections) {
248                 updateHandleOffsets()
249                 updateSelectionToolbar()
250             }
251         }
252 
253         selectionRegistrar.onSelectionUpdateStartCallback =
254             { isInTouchMode, layoutCoordinates, rawPosition, selectionMode ->
255                 val textRect =
256                     with(layoutCoordinates.size) { Rect(0f, 0f, width.toFloat(), height.toFloat()) }
257 
258                 val position =
259                     if (textRect.containsInclusive(rawPosition)) {
260                         rawPosition
261                     } else {
262                         rawPosition.coerceIn(textRect)
263                     }
264 
265                 val positionInContainer = convertToContainerCoordinates(layoutCoordinates, position)
266 
267                 if (positionInContainer.isSpecified) {
268                     this.isInTouchMode = isInTouchMode
269                     startSelection(
270                         position = positionInContainer,
271                         isStartHandle = false,
272                         adjustment = selectionMode
273                     )
274 
275                     focusRequester.requestFocus()
276                     showToolbar = false
277                 }
278             }
279 
280         selectionRegistrar.onSelectionUpdateSelectAll = { isInTouchMode, selectableId ->
281             val (newSelection, newSubselection) =
282                 selectAllInSelectable(
283                     selectableId = selectableId,
284                     previousSelection = selection,
285                 )
286             if (newSelection != selection) {
287                 selectionRegistrar.subselections = newSubselection
288                 onSelectionChange(newSelection)
289             }
290 
291             this.isInTouchMode = isInTouchMode
292             focusRequester.requestFocus()
293             showToolbar = false
294         }
295 
296         selectionRegistrar.onSelectionUpdateCallback =
297             {
298                 isInTouchMode,
299                 layoutCoordinates,
300                 newPosition,
301                 previousPosition,
302                 isStartHandle,
303                 selectionMode ->
304                 val newPositionInContainer =
305                     convertToContainerCoordinates(layoutCoordinates, newPosition)
306                 val previousPositionInContainer =
307                     convertToContainerCoordinates(layoutCoordinates, previousPosition)
308 
309                 this.isInTouchMode = isInTouchMode
310                 updateSelection(
311                     newPosition = newPositionInContainer,
312                     previousPosition = previousPositionInContainer,
313                     isStartHandle = isStartHandle,
314                     adjustment = selectionMode
315                 )
316             }
317 
318         selectionRegistrar.onSelectionUpdateEndCallback = {
319             showToolbar = true
320             // This property is set by updateSelection while dragging, so we need to clear it after
321             // the original selection drag.
322             draggingHandle = null
323             currentDragPosition = null
324         }
325 
326         // This function is meant to handle changes in the selectable content,
327         // such as the text changing.
328         selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
329             if (selectableKey in selectionRegistrar.subselections) {
330                 // Clear the selection range of each Selectable.
331                 onRelease()
332                 selection = null
333             }
334         }
335 
336         selectionRegistrar.afterSelectableUnsubscribe = { selectableId ->
337             if (selectableId == selection?.start?.selectableId) {
338                 // The selectable that contains a selection handle just unsubscribed.
339                 // Hide the associated selection handle
340                 startHandlePosition = null
341             }
342             if (selectableId == selection?.end?.selectableId) {
343                 endHandlePosition = null
344             }
345 
346             if (selectableId in selectionRegistrar.subselections) {
347                 // Unsubscribing the selectable may make the selection empty, which would hide it.
348                 updateSelectionToolbar()
349             }
350         }
351     }
352 
353     /**
354      * Returns the [Selectable] responsible for managing the given [AnchorInfo], or null if the
355      * anchor is not from a currently-registered [Selectable].
356      */
357     internal fun getAnchorSelectable(anchor: AnchorInfo): Selectable? {
358         return selectionRegistrar.selectableMap[anchor.selectableId]
359     }
360 
361     private fun updateHandleOffsets() {
362         val selection = selection
363         val containerCoordinates = containerLayoutCoordinates
364         val startSelectable = selection?.start?.let(::getAnchorSelectable)
365         val endSelectable = selection?.end?.let(::getAnchorSelectable)
366         val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
367         val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
368 
369         if (
370             selection == null ||
371                 containerCoordinates == null ||
372                 !containerCoordinates.isAttached ||
373                 (startLayoutCoordinates == null && endLayoutCoordinates == null)
374         ) {
375             this.startHandlePosition = null
376             this.endHandlePosition = null
377             return
378         }
379 
380         val visibleBounds = containerCoordinates.visibleBounds()
381         this.startHandlePosition =
382             startLayoutCoordinates?.let { handleCoordinates ->
383                 // Set the new handle position only if the handle is in visible bounds or
384                 // the handle is still dragging. If handle goes out of visible bounds during drag,
385                 // handle popup is also removed from composition, halting the drag gesture. This
386                 // affects multiple text selection when selected text is configured with maxLines=1
387                 // and overflow=clip.
388                 val handlePosition =
389                     startSelectable.getHandlePosition(selection, isStartHandle = true)
390                 if (handlePosition.isUnspecified) return@let null
391                 val position =
392                     containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
393                 position.takeIf {
394                     draggingHandle == Handle.SelectionStart || visibleBounds.containsInclusive(it)
395                 }
396             }
397 
398         this.endHandlePosition =
399             endLayoutCoordinates?.let { handleCoordinates ->
400                 val handlePosition =
401                     endSelectable.getHandlePosition(selection, isStartHandle = false)
402                 if (handlePosition.isUnspecified) return@let null
403                 val position =
404                     containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
405                 position.takeIf {
406                     draggingHandle == Handle.SelectionEnd || visibleBounds.containsInclusive(it)
407                 }
408             }
409     }
410 
411     /** Returns non-nullable [containerLayoutCoordinates]. */
412     internal fun requireContainerCoordinates(): LayoutCoordinates {
413         val coordinates = containerLayoutCoordinates
414         requirePreconditionNotNull(coordinates) { "null coordinates" }
415         requirePrecondition(coordinates.isAttached) { "unattached coordinates" }
416         return coordinates
417     }
418 
419     internal fun selectAllInSelectable(
420         selectableId: Long,
421         previousSelection: Selection?
422     ): Pair<Selection?, LongObjectMap<Selection>> {
423         val subselections = mutableLongObjectMapOf<Selection>()
424         val newSelection =
425             selectionRegistrar.sort(requireContainerCoordinates()).fastFold(null) {
426                 mergedSelection: Selection?,
427                 selectable: Selectable ->
428                 val selection =
429                     if (selectable.selectableId == selectableId) selectable.getSelectAllSelection()
430                     else null
431                 selection?.let { subselections[selectable.selectableId] = it }
432                 merge(mergedSelection, selection)
433             }
434         if (isInTouchMode && newSelection != previousSelection) {
435             hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
436         }
437         return Pair(newSelection, subselections)
438     }
439 
440     /** Returns whether the selection encompasses the entire container. */
441     internal fun isEntireContainerSelected(): Boolean {
442         val selectables = selectionRegistrar.sort(requireContainerCoordinates())
443 
444         // If there are no selectables, then an empty selection spans the entire container.
445         if (selectables.isEmpty()) return true
446 
447         // Since some text exists, we must make sure that every selectable is fully selected.
448         return selectables.fastAll {
449             val text = it.getText()
450             if (text.isEmpty()) return@fastAll true // empty text is inherently fully selected
451 
452             // If a non-empty selectable isn't included in the sub-selections,
453             // then some text in the container is not selected.
454             val subSelection =
455                 selectionRegistrar.subselections[it.selectableId] ?: return@fastAll false
456 
457             val selectionStart = subSelection.start.offset
458             val selectionEnd = subSelection.end.offset
459 
460             // The selection could be reversed,
461             // so just verify that the difference between the two offsets matches the text length
462             (selectionStart - selectionEnd).absoluteValue == text.length
463         }
464     }
465 
466     /** Creates and sets a selection spanning the entire container. */
467     internal fun selectAll() {
468         val selectables = selectionRegistrar.sort(requireContainerCoordinates())
469         if (selectables.isEmpty()) return
470 
471         var firstSubSelection: Selection? = null
472         var lastSubSelection: Selection? = null
473         val newSubSelections =
474             mutableLongObjectMapOf<Selection>().apply {
475                 selectables.fastForEach { selectable ->
476                     val subSelection = selectable.getSelectAllSelection() ?: return@fastForEach
477                     if (firstSubSelection == null) firstSubSelection = subSelection
478                     lastSubSelection = subSelection
479                     put(selectable.selectableId, subSelection)
480                 }
481             }
482 
483         if (newSubSelections.isEmpty()) return
484 
485         // first/last sub selections are implied to be non-null from here on out
486         val newSelection =
487             if (firstSubSelection === lastSubSelection) {
488                 firstSubSelection
489             } else {
490                 Selection(
491                     start = firstSubSelection!!.start,
492                     end = lastSubSelection!!.end,
493                     handlesCrossed = false,
494                 )
495             }
496 
497         selectionRegistrar.subselections = newSubSelections
498         onSelectionChange(newSelection)
499         previousSelectionLayout = null
500     }
501 
502     /**
503      * Returns whether the start and end anchors are equal.
504      *
505      * It is possible that this returns true, but the selection is still empty because it has
506      * multiple collapsed selections across multiple selectables. To test for that case, use
507      * [isNonEmptySelection].
508      */
509     internal fun isTriviallyCollapsedSelection(): Boolean {
510         val selection = selection ?: return true
511         return selection.start == selection.end
512     }
513 
514     /**
515      * Returns whether the selection selects zero characters.
516      *
517      * It is possible that the selection anchors are different but still result in a zero-width
518      * selection. In this case, you may want to still show the selection anchors, but not allow for
519      * a user to try and copy zero characters. To test for whether the anchors are equal, use
520      * [isTriviallyCollapsedSelection].
521      */
522     internal fun isNonEmptySelection(): Boolean {
523         val selection = selection ?: return false
524         if (selection.start == selection.end) {
525             return false
526         }
527 
528         if (selection.start.selectableId == selection.end.selectableId) {
529             // Selection is in the same selectable, but not the same anchors,
530             // so there must be some selected text.
531             return true
532         }
533 
534         // All subselections associated with a selectable must be an empty selection.
535         return selectionRegistrar.sort(requireContainerCoordinates()).fastAny { selectable ->
536             selectionRegistrar.subselections[selectable.selectableId]?.run {
537                 start.offset != end.offset
538             } ?: false
539         }
540     }
541 
542     internal fun getSelectedText(): AnnotatedString? {
543         if (selection == null || selectionRegistrar.subselections.isEmpty()) {
544             return null
545         }
546 
547         return buildAnnotatedString {
548             selectionRegistrar.sort(requireContainerCoordinates()).fastForEach { selectable ->
549                 selectionRegistrar.subselections[selectable.selectableId]?.let { subSelection ->
550                     val currentText = selectable.getText()
551                     val currentSelectedText =
552                         if (subSelection.handlesCrossed) {
553                             currentText.subSequence(
554                                 subSelection.end.offset,
555                                 subSelection.start.offset
556                             )
557                         } else {
558                             currentText.subSequence(
559                                 subSelection.start.offset,
560                                 subSelection.end.offset
561                             )
562                         }
563 
564                     append(currentSelectedText)
565                 }
566             }
567         }
568     }
569 
570     internal fun copy() {
571         getSelectedText()
572             ?.takeIf { it.isNotEmpty() }
573             ?.let { textToCopy -> onCopyHandler?.invoke(textToCopy) }
574     }
575 
576     /**
577      * Whether toolbar should be shown right now. Examples: Show toolbar after user finishes
578      * selection. Hide it during selection. Hide it when no selection exists.
579      */
580     internal var showToolbar = false
581         internal set(value) {
582             field = value
583             updateSelectionToolbar()
584         }
585 
586     private fun toolbarCopy() {
587         copy()
588         onRelease()
589     }
590 
591     private fun updateSelectionToolbar() {
592         if (!hasFocus) {
593             return
594         }
595 
596         val textToolbar = textToolbar ?: return
597         if (showToolbar && isInTouchMode) {
598             val rect = getContentRect() ?: return
599             textToolbar.showMenu(
600                 rect = rect,
601                 onCopyRequested = if (isNonEmptySelection()) ::toolbarCopy else null,
602                 onSelectAllRequested = if (isEntireContainerSelected()) null else ::selectAll,
603                 onAutofillRequested = null
604             )
605         } else if (textToolbar.status == TextToolbarStatus.Shown) {
606             textToolbar.hide()
607         }
608     }
609 
610     /**
611      * Calculate selected region as [Rect]. The result is the smallest [Rect] that encapsulates the
612      * entire selection, coerced into visible bounds.
613      */
614     private fun getContentRect(): Rect? {
615         selection ?: return null
616         val containerCoordinates = containerLayoutCoordinates ?: return null
617         if (!containerCoordinates.isAttached) return null
618 
619         val selectableSubSelections =
620             selectionRegistrar
621                 .sort(requireContainerCoordinates())
622                 .fastMapNotNull { selectable ->
623                     selectionRegistrar.subselections[selectable.selectableId]?.let {
624                         selectable to it
625                     }
626                 }
627                 .firstAndLast()
628 
629         if (selectableSubSelections.isEmpty()) return null
630         val selectedRegionRect =
631             getSelectedRegionRect(selectableSubSelections, containerCoordinates)
632 
633         if (selectedRegionRect == invertedInfiniteRect) return null
634 
635         val visibleRect = containerCoordinates.visibleBounds().intersect(selectedRegionRect)
636         // if the rectangles do not at least touch at the edges, we shouldn't show the toolbar
637         if (visibleRect.width < 0 || visibleRect.height < 0) return null
638 
639         val rootRect = visibleRect.translate(containerCoordinates.positionInRoot())
640         return rootRect.copy(bottom = rootRect.bottom + HandleHeight.value * 4)
641     }
642 
643     // This is for PressGestureDetector to cancel the selection.
644     fun onRelease() {
645         selectionRegistrar.subselections = emptyLongObjectMap()
646         showToolbar = false
647         if (selection != null) {
648             onSelectionChange(null)
649             if (isInTouchMode) {
650                 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
651             }
652         }
653     }
654 
655     fun handleDragObserver(isStartHandle: Boolean): TextDragObserver =
656         object : TextDragObserver {
657             override fun onDown(point: Offset) {
658                 // if the handle position is null, then it is invisible, so ignore the gesture
659                 (if (isStartHandle) startHandlePosition else endHandlePosition) ?: return
660 
661                 val selection = selection ?: return
662                 val anchor = if (isStartHandle) selection.start else selection.end
663                 val selectable = getAnchorSelectable(anchor) ?: return
664                 // The LayoutCoordinates of the composable where the drag gesture should begin. This
665                 // is used to convert the position of the beginning of the drag gesture from the
666                 // composable coordinates to selection container coordinates.
667                 val beginLayoutCoordinates = selectable.getLayoutCoordinates() ?: return
668 
669                 // The position of the character where the drag gesture should begin. This is in
670                 // the composable coordinates.
671                 val handlePosition = selectable.getHandlePosition(selection, isStartHandle)
672                 if (handlePosition.isUnspecified) return
673                 val beginCoordinates = getAdjustedCoordinates(handlePosition)
674 
675                 // Convert the position where drag gesture begins from composable coordinates to
676                 // selection container coordinates.
677                 currentDragPosition =
678                     requireContainerCoordinates()
679                         .localPositionOf(beginLayoutCoordinates, beginCoordinates)
680                 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
681                 showToolbar = false
682             }
683 
684             override fun onStart(startPoint: Offset) {
685                 draggingHandle ?: return
686 
687                 val selection = selection!!
688                 val anchor = if (isStartHandle) selection.start else selection.end
689                 val selectable =
690                     checkPreconditionNotNull(
691                         selectionRegistrar.selectableMap[anchor.selectableId]
692                     ) {
693                         "SelectionRegistrar should contain the current selection's selectableIds"
694                     }
695 
696                 // The LayoutCoordinates of the composable where the drag gesture should begin. This
697                 // is used to convert the position of the beginning of the drag gesture from the
698                 // composable coordinates to selection container coordinates.
699                 val beginLayoutCoordinates =
700                     checkPreconditionNotNull(selectable.getLayoutCoordinates()) {
701                         "Current selectable should have layout coordinates."
702                     }
703 
704                 // The position of the character where the drag gesture should begin. This is in
705                 // the composable coordinates.
706                 val handlePosition = selectable.getHandlePosition(selection, isStartHandle)
707                 if (handlePosition.isUnspecified) return
708                 val beginCoordinates = getAdjustedCoordinates(handlePosition)
709 
710                 // Convert the position where drag gesture begins from composable coordinates to
711                 // selection container coordinates.
712                 dragBeginPosition =
713                     requireContainerCoordinates()
714                         .localPositionOf(beginLayoutCoordinates, beginCoordinates)
715 
716                 // Zero out the total distance that being dragged.
717                 dragTotalDistance = Offset.Zero
718             }
719 
720             override fun onDrag(delta: Offset) {
721                 draggingHandle ?: return
722 
723                 dragTotalDistance += delta
724                 val endPosition = dragBeginPosition + dragTotalDistance
725                 val consumed =
726                     updateSelection(
727                         newPosition = endPosition,
728                         previousPosition = dragBeginPosition,
729                         isStartHandle = isStartHandle,
730                         adjustment = SelectionAdjustment.CharacterWithWordAccelerate
731                     )
732                 if (consumed) {
733                     dragBeginPosition = endPosition
734                     dragTotalDistance = Offset.Zero
735                 }
736             }
737 
738             private fun done() {
739                 showToolbar = true
740                 draggingHandle = null
741                 currentDragPosition = null
742             }
743 
744             override fun onUp() = done()
745 
746             override fun onStop() = done()
747 
748             override fun onCancel() = done()
749         }
750 
751     @OptIn(ExperimentalFoundationApi::class)
752     private fun Modifier.addContextMenuComponents(): Modifier =
753         if (ComposeFoundationFlags.isNewContextMenuEnabled)
754             addSelectionContainerTextContextMenuComponents(this@SelectionManager)
755         else this
756 
757     /** Clear the selection on up event that isn't a drag-end. */
758     private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier =
759         pointerInput(Unit) {
760             awaitEachGesture {
761                 // Wait for primary pointer to be down. It's required to explicitly filter
762                 // secondary mouse button to make context menu work correctly.
763                 val primaryFirstDown = awaitPrimaryFirstDown(requireUnconsumed = false)
764 
765                 // Wait for all pointers to be up, and if we're not dragging, clear the selection.
766                 // Do it in the initial phase so that when this happens while dragging, we check
767                 // isDraggingInProgress before the drag-end event clears it.
768                 val pointerSlopReached =
769                     awaitAllPointersUpWithSlopDetection(primaryFirstDown, PointerEventPass.Initial)
770                 if (!pointerSlopReached && !isDraggingInProgress) {
771                     block()
772                 }
773             }
774         }
775 
776     private fun convertToContainerCoordinates(
777         layoutCoordinates: LayoutCoordinates,
778         offset: Offset
779     ): Offset {
780         val coordinates = containerLayoutCoordinates
781         if (coordinates == null || !coordinates.isAttached) return Offset.Unspecified
782         return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
783     }
784 
785     /**
786      * Cancel the previous selection and start a new selection at the given [position]. It's used
787      * for long-press, double-click, triple-click and so on to start selection.
788      *
789      * @param position initial position of the selection. Both start and end handle is considered at
790      *   this position.
791      * @param isStartHandle whether it's considered as the start handle moving. This parameter will
792      *   influence the [SelectionAdjustment]'s behavior. For example,
793      *   [SelectionAdjustment.Character] only adjust the moving handle.
794      * @param adjustment the selection adjustment.
795      */
796     private fun startSelection(
797         position: Offset,
798         isStartHandle: Boolean,
799         adjustment: SelectionAdjustment
800     ) {
801         previousSelectionLayout = null
802         updateSelection(
803             position = position,
804             previousHandlePosition = Offset.Unspecified,
805             isStartHandle = isStartHandle,
806             adjustment = adjustment
807         )
808     }
809 
810     /**
811      * Updates the selection after one of the selection handle moved.
812      *
813      * @param newPosition the new position of the moving selection handle.
814      * @param previousPosition the previous position of the moving selection handle.
815      * @param isStartHandle whether the moving selection handle is the start handle.
816      * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
817      *   produce the final selection range.
818      * @return a boolean representing whether the movement is consumed.
819      * @see SelectionAdjustment
820      */
821     internal fun updateSelection(
822         newPosition: Offset?,
823         previousPosition: Offset,
824         isStartHandle: Boolean,
825         adjustment: SelectionAdjustment,
826     ): Boolean {
827         if (newPosition == null) return false
828         return updateSelection(
829             position = newPosition,
830             previousHandlePosition = previousPosition,
831             isStartHandle = isStartHandle,
832             adjustment = adjustment
833         )
834     }
835 
836     /**
837      * Updates the selection after one of the selection handle moved.
838      *
839      * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
840      * selection handle is updated each time. The only exception is that when a new selection is
841      * started. In this case, [previousHandlePosition] is always null.
842      *
843      * @param position the position of the current gesture.
844      * @param previousHandlePosition the position of the moving handle before the update.
845      * @param isStartHandle whether the moving selection handle is the start handle.
846      * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
847      *   produce the final selection range.
848      * @return a boolean representing whether the movement is consumed. It's useful for the case
849      *   where a selection handle is updating consecutively. When the return value is true, it's
850      *   expected that the caller will update the [startHandlePosition] to be the given
851      *   [endHandlePosition] in following calls.
852      * @see SelectionAdjustment
853      */
854     internal fun updateSelection(
855         position: Offset,
856         previousHandlePosition: Offset,
857         isStartHandle: Boolean,
858         adjustment: SelectionAdjustment,
859     ): Boolean {
860         draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
861         currentDragPosition = position
862 
863         val selectionLayout =
864             getSelectionLayout(position, previousHandlePosition, isStartHandle) ?: return false
865         if (!selectionLayout.shouldRecomputeSelection(previousSelectionLayout)) {
866             return false
867         }
868 
869         val newSelection = adjustment.adjust(selectionLayout)
870         if (newSelection != selection) {
871             selectionChanged(selectionLayout, newSelection)
872         }
873         previousSelectionLayout = selectionLayout
874         return true
875     }
876 
877     private fun getSelectionLayout(
878         position: Offset,
879         previousHandlePosition: Offset,
880         isStartHandle: Boolean,
881     ): SelectionLayout? {
882         val containerCoordinates = requireContainerCoordinates()
883         val sortedSelectables = selectionRegistrar.sort(containerCoordinates)
884 
885         val idToIndexMap = mutableLongIntMapOf()
886         sortedSelectables.fastForEachIndexed { index, selectable ->
887             idToIndexMap[selectable.selectableId] = index
888         }
889 
890         val selectableIdOrderingComparator = compareBy<Long> { idToIndexMap[it] }
891 
892         // if previous handle is null, then treat this as a new selection.
893         val previousSelection = if (previousHandlePosition.isUnspecified) null else selection
894         val builder =
895             SelectionLayoutBuilder(
896                 currentPosition = position,
897                 previousHandlePosition = previousHandlePosition,
898                 containerCoordinates = containerCoordinates,
899                 isStartHandle = isStartHandle,
900                 previousSelection = previousSelection,
901                 selectableIdOrderingComparator = selectableIdOrderingComparator,
902             )
903 
904         sortedSelectables.fastForEach { it.appendSelectableInfoToBuilder(builder) }
905 
906         return builder.build()
907     }
908 
909     private fun selectionChanged(selectionLayout: SelectionLayout, newSelection: Selection) {
910         if (shouldPerformHaptics()) {
911             hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
912         }
913         selectionRegistrar.subselections = selectionLayout.createSubSelections(newSelection)
914         onSelectionChange(newSelection)
915     }
916 
917     @VisibleForTesting
918     internal fun shouldPerformHaptics(): Boolean =
919         isInTouchMode && selectionRegistrar.selectables.fastAny { it.getText().isNotEmpty() }
920 
921     /**
922      * Implements the macOS select-word-on-right-click behavior.
923      *
924      * If the current selection does not already include [position], select the word at [position].
925      */
926     fun selectWordAtPositionIfNotAlreadySelected(position: Offset) {
927         val containerCoordinates = containerLayoutCoordinates ?: return
928         if (!containerCoordinates.isAttached) return
929 
930         val isClickedPositionInsideSelection =
931             selectionRegistrar.selectables.fastAny { selectable ->
932                 val selection =
933                     selectionRegistrar.subselections[selectable.selectableId]
934                         ?: return@fastAny false
935                 val selectableLayoutCoords =
936                     selectable.getLayoutCoordinates() ?: return@fastAny false
937                 val positionInSelectable =
938                     selectableLayoutCoords.localPositionOf(containerCoordinates, position)
939                 val textLayoutResult = selectable.textLayoutResult() ?: return@fastAny false
940                 textLayoutResult.isPositionInsideSelection(
941                     position = positionInSelectable,
942                     selectionRange = selection.toTextRange()
943                 )
944             }
945         if (!isClickedPositionInsideSelection) {
946             startSelection(
947                 position = position,
948                 isStartHandle = true,
949                 adjustment = SelectionAdjustment.Word
950             )
951         }
952     }
953 }
954 
mergenull955 internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
956     return lhs?.merge(rhs) ?: rhs
957 }
958 
isCopyKeyEventnull959 internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
960 
961 internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
962 
963 internal expect fun Modifier.addSelectionContainerTextContextMenuComponents(
964     selectionManager: SelectionManager
965 ): Modifier
966 
967 private val invertedInfiniteRect =
968     Rect(
969         left = Float.POSITIVE_INFINITY,
970         top = Float.POSITIVE_INFINITY,
971         right = Float.NEGATIVE_INFINITY,
972         bottom = Float.NEGATIVE_INFINITY
973     )
974 
975 private fun <T> List<T>.firstAndLast(): List<T> =
976     when (size) {
977         0,
978         1 -> this
979         else -> listOf(first(), last())
980     }
981 
982 /**
983  * Get the selected region rect in the given [containerCoordinates]. This will compute the smallest
984  * rect that contains every first/last character bounding box of each selectable. If for any reason
985  * there are no bounding boxes, then the [invertedInfiniteRect] is returned.
986  */
987 @VisibleForTesting
getSelectedRegionRectnull988 internal fun getSelectedRegionRect(
989     selectableSubSelectionPairs: List<Pair<Selectable, Selection>>,
990     containerCoordinates: LayoutCoordinates,
991 ): Rect {
992     if (selectableSubSelectionPairs.isEmpty()) return invertedInfiniteRect
993     var (containerLeft, containerTop, containerRight, containerBottom) = invertedInfiniteRect
994     selectableSubSelectionPairs.fastForEach { (selectable, subSelection) ->
995         val startOffset = subSelection.start.offset
996         val endOffset = subSelection.end.offset
997         if (startOffset == endOffset) return@fastForEach
998         val localCoordinates = selectable.getLayoutCoordinates() ?: return@fastForEach
999 
1000         val minOffset = minOf(startOffset, endOffset)
1001         val maxOffset = maxOf(startOffset, endOffset)
1002         val offsets =
1003             if (minOffset == maxOffset - 1) {
1004                 intArrayOf(minOffset)
1005             } else {
1006                 intArrayOf(minOffset, maxOffset - 1)
1007             }
1008         var (left, top, right, bottom) = invertedInfiniteRect
1009         for (i in offsets) {
1010             val rect = selectable.getBoundingBox(i)
1011             left = minOf(left, rect.left)
1012             top = minOf(top, rect.top)
1013             right = maxOf(right, rect.right)
1014             bottom = maxOf(bottom, rect.bottom)
1015         }
1016 
1017         val localTopLeft = Offset(left, top)
1018         val localBottomRight = Offset(right, bottom)
1019 
1020         val containerTopLeft = containerCoordinates.localPositionOf(localCoordinates, localTopLeft)
1021         val containerBottomRight =
1022             containerCoordinates.localPositionOf(localCoordinates, localBottomRight)
1023 
1024         containerLeft = minOf(containerLeft, containerTopLeft.x)
1025         containerTop = minOf(containerTop, containerTopLeft.y)
1026         containerRight = maxOf(containerRight, containerBottomRight.x)
1027         containerBottom = maxOf(containerBottom, containerBottomRight.y)
1028     }
1029     return Rect(containerLeft, containerTop, containerRight, containerBottom)
1030 }
1031 
calculateSelectionMagnifierCenterAndroidnull1032 internal fun calculateSelectionMagnifierCenterAndroid(
1033     manager: SelectionManager,
1034     magnifierSize: IntSize
1035 ): Offset {
1036     val selection = manager.selection ?: return Offset.Unspecified
1037     return when (manager.draggingHandle) {
1038         null -> return Offset.Unspecified
1039         Handle.SelectionStart -> getMagnifierCenter(manager, magnifierSize, selection.start)
1040         Handle.SelectionEnd -> getMagnifierCenter(manager, magnifierSize, selection.end)
1041         Handle.Cursor -> error("SelectionContainer does not support cursor")
1042     }
1043 }
1044 
getMagnifierCenternull1045 private fun getMagnifierCenter(
1046     manager: SelectionManager,
1047     magnifierSize: IntSize,
1048     anchor: AnchorInfo
1049 ): Offset {
1050     val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
1051     val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
1052     val selectableCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified
1053     val offset = anchor.offset
1054 
1055     if (offset > selectable.getLastVisibleOffset()) return Offset.Unspecified
1056 
1057     // The horizontal position doesn't snap to cursor positions but should directly track the
1058     // actual drag.
1059     val localDragPosition =
1060         selectableCoordinates.localPositionOf(containerCoordinates, manager.currentDragPosition!!)
1061     val dragX = localDragPosition.x
1062 
1063     // But it is constrained by the horizontal bounds of the current line.
1064     val lineRange = selectable.getRangeOfLineContaining(offset)
1065     val textConstrainedX =
1066         if (lineRange.collapsed) {
1067             // A collapsed range implies the text is empty.
1068             // line left and right are equal for this offset, so use either
1069             selectable.getLineLeft(offset)
1070         } else {
1071             val lineStartX = selectable.getLineLeft(lineRange.start)
1072             val lineEndX = selectable.getLineRight(lineRange.end - 1)
1073             // in RTL/BiDi, lineStartX may be larger than lineEndX
1074             val minX = minOf(lineStartX, lineEndX)
1075             val maxX = maxOf(lineStartX, lineEndX)
1076             dragX.coerceIn(minX, maxX)
1077         }
1078 
1079     // selectable couldn't determine horizontals
1080     if (textConstrainedX == -1f) return Offset.Unspecified
1081 
1082     // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
1083     // magnifier actually is). See
1084     // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
1085     // Also check whether magnifierSize is calculated. A platform magnifier instance is not
1086     // created until it's requested for the first time. So the size will only be calculated after we
1087     // return a specified offset from this function.
1088     // It is very unlikely that this behavior would cause a flicker since magnifier immediately
1089     // shows up where the pointer is being dragged. The pointer needs to drag further than the half
1090     // of magnifier's width to hide by the following logic.
1091     if (
1092         magnifierSize != IntSize.Zero &&
1093             (dragX - textConstrainedX).absoluteValue > magnifierSize.width / 2
1094     ) {
1095         return Offset.Unspecified
1096     }
1097 
1098     val lineCenterY = selectable.getCenterYForOffset(offset)
1099 
1100     // selectable couldn't determine the line center
1101     if (lineCenterY == -1f) return Offset.Unspecified
1102 
1103     return containerCoordinates.localPositionOf(
1104         sourceCoordinates = selectableCoordinates,
1105         relativeToSource = Offset(textConstrainedX, lineCenterY)
1106     )
1107 }
1108 
1109 /** Returns the boundary of the visible area in this [LayoutCoordinates]. */
visibleBoundsnull1110 internal fun LayoutCoordinates.visibleBounds(): Rect {
1111     // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
1112     // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
1113     // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
1114     val boundsInWindow = boundsInWindow()
1115     return Rect(windowToLocal(boundsInWindow.topLeft), windowToLocal(boundsInWindow.bottomRight))
1116 }
1117 
containsInclusivenull1118 internal fun Rect.containsInclusive(offset: Offset): Boolean =
1119     offset.x in left..right && offset.y in top..bottom
1120