[go: nahoru, domu]

blob: 4fad5b61dca0c743e53db562ca2b74b938fb157d [file] [log] [blame]
Qingqing Deng6f56a912019-05-13 10:10:37 -07001/*
haoyuac341f02021-01-22 22:01:56 -08002 * Copyright 2021 The Android Open Source Project
Qingqing Deng6f56a912019-05-13 10:10:37 -07003 *
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
haoyuac341f02021-01-22 22:01:56 -080017package androidx.compose.foundation.text.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070018
Grant Toepferd681b5c2023-06-14 11:24:02 -070019import androidx.annotation.VisibleForTesting
Grant Toepfer39043c82024-04-22 11:33:23 -070020import androidx.collection.LongObjectMap
21import androidx.collection.emptyLongObjectMap
22import androidx.collection.mutableLongIntMapOf
23import androidx.collection.mutableLongObjectMapOf
haoyuc40d02752021-01-25 17:32:47 -080024import androidx.compose.foundation.focusable
George Mount32de9dd2022-10-05 14:51:06 -070025import androidx.compose.foundation.gestures.awaitEachGesture
Ralston Da Silvade62bc62021-06-02 17:46:44 -070026import androidx.compose.foundation.gestures.waitForUpOrCancellation
Zach Klippensteinadabe342021-11-11 16:38:13 -080027import androidx.compose.foundation.text.Handle
28import androidx.compose.foundation.text.TextDragObserver
Halil Ozercan325cadb2024-02-07 15:14:35 +000029import androidx.compose.foundation.text.input.internal.coerceIn
Zach Klippensteinadabe342021-11-11 16:38:13 -080030import androidx.compose.foundation.text.selection.Selection.AnchorInfo
31import androidx.compose.runtime.MutableState
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010032import androidx.compose.runtime.State
33import androidx.compose.runtime.getValue
34import androidx.compose.runtime.mutableStateOf
35import androidx.compose.runtime.setValue
haoyuc40d02752021-01-25 17:32:47 -080036import androidx.compose.ui.Modifier
37import androidx.compose.ui.focus.FocusRequester
38import androidx.compose.ui.focus.focusRequester
haoyuc40d02752021-01-25 17:32:47 -080039import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010040import androidx.compose.ui.geometry.Offset
41import androidx.compose.ui.geometry.Rect
Grant Toepferd681b5c2023-06-14 11:24:02 -070042import androidx.compose.ui.geometry.isSpecified
43import androidx.compose.ui.geometry.isUnspecified
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010044import androidx.compose.ui.hapticfeedback.HapticFeedback
45import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010046import androidx.compose.ui.input.key.KeyEvent
47import androidx.compose.ui.input.key.onKeyEvent
Ralston Da Silvade62bc62021-06-02 17:46:44 -070048import androidx.compose.ui.input.pointer.PointerInputScope
49import androidx.compose.ui.input.pointer.pointerInput
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010050import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000051import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080052import androidx.compose.ui.layout.onGloballyPositioned
Grant Toepferef76a142023-11-07 11:56:49 -070053import androidx.compose.ui.layout.positionInRoot
haoyu7ad5ea32021-03-22 10:36:35 -070054import androidx.compose.ui.layout.positionInWindow
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010055import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010056import androidx.compose.ui.platform.TextToolbar
57import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010058import androidx.compose.ui.text.AnnotatedString
Grant Toepfer427dbdf2023-09-05 18:38:42 -070059import androidx.compose.ui.text.buildAnnotatedString
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -080060import androidx.compose.ui.unit.IntSize
Grant Toepferd681b5c2023-06-14 11:24:02 -070061import androidx.compose.ui.util.fastAny
Louis Pullen-Freilich1ffdbfc2023-08-24 18:35:34 +010062import androidx.compose.ui.util.fastFold
Grant Toepferfefbd7a2023-03-03 15:02:55 -080063import androidx.compose.ui.util.fastForEach
Grant Toepferd681b5c2023-06-14 11:24:02 -070064import androidx.compose.ui.util.fastForEachIndexed
Grant Toepferef76a142023-11-07 11:56:49 -070065import androidx.compose.ui.util.fastMapNotNull
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -080066import kotlin.math.absoluteValue
Qingqing Deng6f56a912019-05-13 10:10:37 -070067
Qingqing Deng35f97ea2019-09-18 19:24:37 -070068/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010069 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070070 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070071internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080072
73 private val _selection: MutableState<Selection?> = mutableStateOf(null)
74
Qingqing Deng6f56a912019-05-13 10:10:37 -070075 /**
76 * The current selection.
77 */
Zach Klippensteinadabe342021-11-11 16:38:13 -080078 var selection: Selection?
79 get() = _selection.value
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010080 set(value) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080081 _selection.value = value
haoyu9085c882020-12-08 12:01:06 -080082 if (value != null) {
83 updateHandleOffsets()
84 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010085 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070086
87 /**
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010088 * Is touch mode active
89 */
Grant Toepferfefbd7a2023-03-03 15:02:55 -080090 private val _isInTouchMode = mutableStateOf(true)
91 var isInTouchMode: Boolean
92 get() = _isInTouchMode.value
93 set(value) {
94 if (_isInTouchMode.value != value) {
95 _isInTouchMode.value = value
Grant Toepfer427dbdf2023-09-05 18:38:42 -070096 updateSelectionToolbar()
Grant Toepferfefbd7a2023-03-03 15:02:55 -080097 }
98 }
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010099
100 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700101 * The manager will invoke this every time it comes to the conclusion that the selection should
102 * change. The expectation is that this callback will end up causing `setSelection` to get
103 * called. This is what makes this a "controlled component".
104 */
Grant Toepfer4e6fa502024-03-05 15:49:57 -0800105 var onSelectionChange: (Selection?) -> Unit = { selection = it }
106 set(newOnSelectionChange) {
107 // Wrap the given lambda with one that sets the selection immediately.
108 // The onSelectionChange loop requires a composition to happen for the selection
109 // to be updated, so we want to shorten that loop for gesture use cases where
110 // multiple selection changing events can be acted on within a single composition
111 // loop. Previous selection is used as part of that loop so keeping it up to date
112 // is important.
113 field = { newSelection ->
114 selection = newSelection
115 newOnSelectionChange(newSelection)
116 }
117 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700118
119 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800120 * [HapticFeedback] handle to perform haptic feedback.
121 */
122 var hapticFeedBack: HapticFeedback? = null
123
124 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700125 * [ClipboardManager] to perform clipboard features.
126 */
127 var clipboardManager: ClipboardManager? = null
128
129 /**
Qingqing Denga89450d2020-04-03 19:11:49 -0700130 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
131 */
132 var textToolbar: TextToolbar? = null
133
134 /**
haoyuc40d02752021-01-25 17:32:47 -0800135 * Focus requester used to request focus when selection becomes active.
136 */
137 var focusRequester: FocusRequester = FocusRequester()
138
139 /**
haoyu3c3fb452021-02-18 01:01:14 -0800140 * Return true if the corresponding SelectionContainer is focused.
haoyuc40d02752021-01-25 17:32:47 -0800141 */
haoyu3c3fb452021-02-18 01:01:14 -0800142 var hasFocus: Boolean by mutableStateOf(false)
haoyuc40d02752021-01-25 17:32:47 -0800143
144 /**
145 * Modifier for selection container.
146 */
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800147 val modifier
148 get() = Modifier
149 .onClearSelectionRequested { onRelease() }
150 .onGloballyPositioned { containerLayoutCoordinates = it }
151 .focusRequester(focusRequester)
152 .onFocusChanged { focusState ->
153 if (!focusState.isFocused && hasFocus) {
154 onRelease()
155 }
156 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800157 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800158 .focusable()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800159 .updateSelectionTouchMode { isInTouchMode = it }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800160 .onKeyEvent {
161 if (isCopyKeyEvent(it)) {
162 copy()
163 true
164 } else {
165 false
166 }
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100167 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800168 .then(if (shouldShowMagnifier) Modifier.selectionMagnifier(this) else Modifier)
haoyuc40d02752021-01-25 17:32:47 -0800169
haoyu7ad5ea32021-03-22 10:36:35 -0700170 private var previousPosition: Offset? = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800171
haoyuc40d02752021-01-25 17:32:47 -0800172 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700173 * Layout Coordinates of the selection container.
174 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100175 var containerLayoutCoordinates: LayoutCoordinates? = null
176 set(value) {
177 field = value
haoyu2c6e9842021-03-30 17:39:04 -0700178 if (hasFocus && selection != null) {
179 val positionInWindow = value?.positionInWindow()
180 if (previousPosition != positionInWindow) {
181 previousPosition = positionInWindow
182 updateHandleOffsets()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700183 updateSelectionToolbar()
haoyu2c6e9842021-03-30 17:39:04 -0700184 }
haoyu3c3fb452021-02-18 01:01:14 -0800185 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100186 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700187
188 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700189 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
190 * recalculated.
191 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800192 internal var dragBeginPosition by mutableStateOf(Offset.Zero)
193 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700194
195 /**
196 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
197 * it will be zeroed out.
198 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800199 internal var dragTotalDistance by mutableStateOf(Offset.Zero)
200 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700201
202 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100203 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
204 * is null when handle shouldn't be displayed.
205 * It is a [State] so reading it during the composition will cause recomposition every time
206 * the position has been changed.
207 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800208 var startHandlePosition: Offset? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100209 private set
210
211 /**
212 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
213 * is null when handle shouldn't be displayed.
214 * It is a [State] so reading it during the composition will cause recomposition every time
215 * the position has been changed.
216 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800217 var endHandlePosition: Offset? by mutableStateOf(null)
218 private set
219
220 /**
Zach Klippenstein63870892022-01-14 12:45:18 -0800221 * The handle that is currently being dragged, or null when no handle is being dragged. To get
222 * the position of the last drag event, use [currentDragPosition].
Zach Klippensteinadabe342021-11-11 16:38:13 -0800223 */
224 var draggingHandle: Handle? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100225 private set
226
Zach Klippenstein63870892022-01-14 12:45:18 -0800227 /**
228 * When a handle is being dragged (i.e. [draggingHandle] is non-null), this is the last position
229 * of the actual drag event. It is not clamped to handle positions. Null when not being dragged.
230 */
231 var currentDragPosition: Offset? by mutableStateOf(null)
232 private set
233
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800234 private val shouldShowMagnifier
Grant Toepferd681b5c2023-06-14 11:24:02 -0700235 get() = draggingHandle != null && isInTouchMode && !isTriviallyCollapsedSelection()
236
237 @VisibleForTesting
238 internal var previousSelectionLayout: SelectionLayout? = null
Zach Klippenstein4688a462021-12-08 08:28:07 -0800239
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100240 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700241 selectionRegistrar.onPositionChangeCallback = { selectableId ->
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700242 if (selectableId in selectionRegistrar.subselections) {
haoyu7ad5ea32021-03-22 10:36:35 -0700243 updateHandleOffsets()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700244 updateSelectionToolbar()
haoyu7ad5ea32021-03-22 10:36:35 -0700245 }
haoyue6d80a12020-12-02 16:04:52 -0800246 }
247
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100248 selectionRegistrar.onSelectionUpdateStartCallback =
Grant Toepferc7d1a4e2023-09-18 23:06:37 +0000249 { isInTouchMode, layoutCoordinates, rawPosition, selectionMode ->
250 val textRect = with(layoutCoordinates.size) {
251 Rect(0f, 0f, width.toFloat(), height.toFloat())
252 }
253
254 val position = if (textRect.containsInclusive(rawPosition)) {
255 rawPosition
256 } else {
257 rawPosition.coerceIn(textRect)
258 }
259
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700260 val positionInContainer = convertToContainerCoordinates(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100261 layoutCoordinates,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700262 position
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100263 )
264
Grant Toepferd681b5c2023-06-14 11:24:02 -0700265 if (positionInContainer.isSpecified) {
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800266 this.isInTouchMode = isInTouchMode
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700267 startSelection(
268 position = positionInContainer,
269 isStartHandle = false,
270 adjustment = selectionMode
271 )
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100272
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700273 focusRequester.requestFocus()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800274 showToolbar = false
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700275 }
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100276 }
haoyue6d80a12020-12-02 16:04:52 -0800277
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700278 selectionRegistrar.onSelectionUpdateSelectAll =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800279 { isInTouchMode, selectableId ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700280 val (newSelection, newSubselection) = selectAll(
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700281 selectableId = selectableId,
282 previousSelection = selection,
283 )
284 if (newSelection != selection) {
285 selectionRegistrar.subselections = newSubselection
286 onSelectionChange(newSelection)
287 }
288
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800289 this.isInTouchMode = isInTouchMode
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700290 focusRequester.requestFocus()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800291 showToolbar = false
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700292 }
293
haoyue6d80a12020-12-02 16:04:52 -0800294 selectionRegistrar.onSelectionUpdateCallback =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800295 { isInTouchMode,
296 layoutCoordinates,
297 newPosition,
298 previousPosition,
299 isStartHandle,
300 selectionMode ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700301 val newPositionInContainer =
302 convertToContainerCoordinates(layoutCoordinates, newPosition)
303 val previousPositionInContainer =
304 convertToContainerCoordinates(layoutCoordinates, previousPosition)
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100305
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800306 this.isInTouchMode = isInTouchMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700307 updateSelection(
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700308 newPosition = newPositionInContainer,
309 previousPosition = previousPositionInContainer,
310 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700311 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700312 )
313 }
haoyue6d80a12020-12-02 16:04:52 -0800314
315 selectionRegistrar.onSelectionUpdateEndCallback = {
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800316 showToolbar = true
Zach Klippensteinadabe342021-11-11 16:38:13 -0800317 // This property is set by updateSelection while dragging, so we need to clear it after
318 // the original selection drag.
319 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800320 currentDragPosition = null
haoyue6d80a12020-12-02 16:04:52 -0800321 }
haoyu9085c882020-12-08 12:01:06 -0800322
Grant Toepfer2ad83dc2024-02-07 13:50:56 -0800323 // This function is meant to handle changes in the selectable content,
324 // such as the text changing.
haoyue04245e2021-03-08 14:52:56 -0800325 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
326 if (selectableKey in selectionRegistrar.subselections) {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700327 // Clear the selection range of each Selectable.
haoyu9085c882020-12-08 12:01:06 -0800328 onRelease()
329 selection = null
330 }
331 }
haoyue04245e2021-03-08 14:52:56 -0800332
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700333 selectionRegistrar.afterSelectableUnsubscribe = { selectableId ->
334 if (selectableId == selection?.start?.selectableId) {
haoyue04245e2021-03-08 14:52:56 -0800335 // The selectable that contains a selection handle just unsubscribed.
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700336 // Hide the associated selection handle
haoyue04245e2021-03-08 14:52:56 -0800337 startHandlePosition = null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700338 }
339 if (selectableId == selection?.end?.selectableId) {
haoyue04245e2021-03-08 14:52:56 -0800340 endHandlePosition = null
341 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700342
343 if (selectableId in selectionRegistrar.subselections) {
344 // Unsubscribing the selectable may make the selection empty, which would hide it.
345 updateSelectionToolbar()
346 }
haoyue04245e2021-03-08 14:52:56 -0800347 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100348 }
349
Zach Klippensteinadabe342021-11-11 16:38:13 -0800350 /**
351 * Returns the [Selectable] responsible for managing the given [AnchorInfo], or null if the
352 * anchor is not from a currently-registered [Selectable].
353 */
354 internal fun getAnchorSelectable(anchor: AnchorInfo): Selectable? {
355 return selectionRegistrar.selectableMap[anchor.selectableId]
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100356 }
357
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100358 private fun updateHandleOffsets() {
359 val selection = selection
360 val containerCoordinates = containerLayoutCoordinates
Zach Klippensteinadabe342021-11-11 16:38:13 -0800361 val startSelectable = selection?.start?.let(::getAnchorSelectable)
362 val endSelectable = selection?.end?.let(::getAnchorSelectable)
haoyue04245e2021-03-08 14:52:56 -0800363 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
364 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700365
haoyue2678c62020-12-09 08:39:12 -0800366 if (
367 selection == null ||
368 containerCoordinates == null ||
369 !containerCoordinates.isAttached ||
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700370 (startLayoutCoordinates == null && endLayoutCoordinates == null)
haoyue2678c62020-12-09 08:39:12 -0800371 ) {
372 this.startHandlePosition = null
373 this.endHandlePosition = null
374 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100375 }
haoyue2678c62020-12-09 08:39:12 -0800376
haoyue2678c62020-12-09 08:39:12 -0800377 val visibleBounds = containerCoordinates.visibleBounds()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700378 this.startHandlePosition = startLayoutCoordinates?.let { handleCoordinates ->
379 // Set the new handle position only if the handle is in visible bounds or
380 // the handle is still dragging. If handle goes out of visible bounds during drag,
381 // handle popup is also removed from composition, halting the drag gesture. This
382 // affects multiple text selection when selected text is configured with maxLines=1
383 // and overflow=clip.
384 val handlePosition = startSelectable.getHandlePosition(selection, isStartHandle = true)
Grant Toepfer0880f742023-10-11 16:34:51 -0700385 if (handlePosition.isUnspecified) return@let null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700386 val position = containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
387 position.takeIf {
388 draggingHandle == Handle.SelectionStart || visibleBounds.containsInclusive(it)
389 }
Halil Ozercanb2486f12023-01-09 07:40:22 +0000390 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700391
392 this.endHandlePosition = endLayoutCoordinates?.let { handleCoordinates ->
393 val handlePosition = endSelectable.getHandlePosition(selection, isStartHandle = false)
Grant Toepfer0880f742023-10-11 16:34:51 -0700394 if (handlePosition.isUnspecified) return@let null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700395 val position = containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
396 position.takeIf {
397 draggingHandle == Handle.SelectionEnd || visibleBounds.containsInclusive(it)
398 }
Halil Ozercanb2486f12023-01-09 07:40:22 +0000399 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100400 }
401
402 /**
403 * Returns non-nullable [containerLayoutCoordinates].
404 */
405 internal fun requireContainerCoordinates(): LayoutCoordinates {
406 val coordinates = containerLayoutCoordinates
Ralston Da Silvaf24ae422023-05-30 12:59:24 -0700407 requireNotNull(coordinates) { "null coordinates" }
408 require(coordinates.isAttached) { "unattached coordinates" }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100409 return coordinates
410 }
411
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700412 internal fun selectAll(
413 selectableId: Long,
414 previousSelection: Selection?
Grant Toepfer39043c82024-04-22 11:33:23 -0700415 ): Pair<Selection?, LongObjectMap<Selection>> {
416 val subselections = mutableLongObjectMapOf<Selection>()
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700417 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
418 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
419 val selection = if (selectable.selectableId == selectableId)
420 selectable.getSelectAllSelection() else null
421 selection?.let { subselections[selectable.selectableId] = it }
422 merge(mergedSelection, selection)
423 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800424 if (isInTouchMode && newSelection != previousSelection) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700425 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
426 }
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700427 return Pair(newSelection, subselections)
428 }
429
Grant Toepferd681b5c2023-06-14 11:24:02 -0700430 /**
431 * Returns whether the start and end anchors are equal.
432 *
433 * It is possible that this returns true, but the selection is still empty because it has
434 * multiple collapsed selections across multiple selectables. To test for that case, use
435 * [isNonEmptySelection].
436 */
437 internal fun isTriviallyCollapsedSelection(): Boolean {
438 val selection = selection ?: return true
439 return selection.start == selection.end
440 }
441
442 /**
443 * Returns whether the selection selects zero characters.
444 *
445 * It is possible that the selection anchors are different but still result in a zero-width
446 * selection. In this case, you may want to still show the selection anchors, but not allow for
447 * a user to try and copy zero characters. To test for whether the anchors are equal, use
448 * [isTriviallyCollapsedSelection].
449 */
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800450 internal fun isNonEmptySelection(): Boolean {
451 val selection = selection ?: return false
Grant Toepferd681b5c2023-06-14 11:24:02 -0700452 if (selection.start == selection.end) {
453 return false
454 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800455
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700456 if (selection.start.selectableId == selection.end.selectableId) {
457 // Selection is in the same selectable, but not the same anchors,
458 // so there must be some selected text.
459 return true
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800460 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700461
462 // All subselections associated with a selectable must be an empty selection.
463 return selectionRegistrar.sort(requireContainerCoordinates()).fastAny { selectable ->
464 selectionRegistrar.subselections[selectable.selectableId]
465 ?.run { start.offset != end.offset }
466 ?: false
467 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800468 }
469
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800470 internal fun getSelectedText(): AnnotatedString? {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700471 if (selection == null || selectionRegistrar.subselections.isEmpty()) {
472 return null
473 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800474
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700475 return buildAnnotatedString {
476 selectionRegistrar.sort(requireContainerCoordinates()).fastForEach { selectable ->
477 selectionRegistrar.subselections[selectable.selectableId]?.let { subSelection ->
478 val currentText = selectable.getText()
479 val currentSelectedText = if (subSelection.handlesCrossed) {
480 currentText.subSequence(
481 subSelection.end.offset,
482 subSelection.start.offset
483 )
484 } else {
485 currentText.subSequence(
486 subSelection.start.offset,
487 subSelection.end.offset
488 )
489 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800490
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700491 append(currentSelectedText)
492 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800493 }
494 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800495 }
496
Qingqing Dengde023cc2020-04-24 14:23:41 -0700497 internal fun copy() {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700498 getSelectedText()?.takeIf { it.isNotEmpty() }?.let { clipboardManager?.setText(it) }
Qingqing Dengde023cc2020-04-24 14:23:41 -0700499 }
500
501 /**
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800502 * Whether toolbar should be shown right now.
503 * Examples: Show toolbar after user finishes selection.
504 * Hide it during selection.
505 * Hide it when no selection exists.
506 */
507 internal var showToolbar = false
508 internal set(value) {
509 field = value
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700510 updateSelectionToolbar()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800511 }
512
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700513 private fun toolbarCopy() {
514 copy()
515 onRelease()
516 }
517
518 private fun updateSelectionToolbar() {
519 if (!hasFocus) {
520 return
521 }
522
523 val textToolbar = textToolbar ?: return
524 if (showToolbar && isInTouchMode && isNonEmptySelection()) {
525 val rect = getContentRect() ?: return
526 textToolbar.showMenu(rect = rect, onCopyRequested = ::toolbarCopy)
527 } else if (textToolbar.status == TextToolbarStatus.Shown) {
528 textToolbar.hide()
529 }
530 }
531
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800532 /**
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700533 * Calculate selected region as [Rect].
534 * The result is the smallest [Rect] that encapsulates the entire selection,
535 * coerced into visible bounds.
Qingqing Dengde023cc2020-04-24 14:23:41 -0700536 */
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700537 private fun getContentRect(): Rect? {
538 selection ?: return null
539 val containerCoordinates = containerLayoutCoordinates ?: return null
540 if (!containerCoordinates.isAttached) return null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700541
Grant Toepferef76a142023-11-07 11:56:49 -0700542 val selectableSubSelections = selectionRegistrar.sort(requireContainerCoordinates())
543 .fastMapNotNull { selectable ->
544 selectionRegistrar.subselections[selectable.selectableId]
545 ?.let { selectable to it }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700546 }
Grant Toepferef76a142023-11-07 11:56:49 -0700547 .firstAndLast()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700548
Grant Toepferef76a142023-11-07 11:56:49 -0700549 if (selectableSubSelections.isEmpty()) return null
550 val selectedRegionRect =
551 getSelectedRegionRect(selectableSubSelections, containerCoordinates)
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700552
Grant Toepferef76a142023-11-07 11:56:49 -0700553 if (selectedRegionRect == invertedInfiniteRect) return null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700554
Grant Toepferef76a142023-11-07 11:56:49 -0700555 val visibleRect = containerCoordinates.visibleBounds().intersect(selectedRegionRect)
556 // if the rectangles do not at least touch at the edges, we shouldn't show the toolbar
557 if (visibleRect.width < 0 || visibleRect.height < 0) return null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700558
Grant Toepferef76a142023-11-07 11:56:49 -0700559 val rootRect = visibleRect.translate(containerCoordinates.positionInRoot())
Albert Changc8462ae2024-04-04 21:46:31 +0900560 return rootRect.copy(bottom = rootRect.bottom + HandleHeight.value * 4)
Qingqing Dengde023cc2020-04-24 14:23:41 -0700561 }
562
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800563 // This is for PressGestureDetector to cancel the selection.
564 fun onRelease() {
Grant Toepfer39043c82024-04-22 11:33:23 -0700565 selectionRegistrar.subselections = emptyLongObjectMap()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800566 showToolbar = false
haoyue04245e2021-03-08 14:52:56 -0800567 if (selection != null) {
568 onSelectionChange(null)
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800569 if (isInTouchMode) {
570 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
571 }
haoyue04245e2021-03-08 14:52:56 -0800572 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800573 }
574
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800575 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver = object : TextDragObserver {
576 override fun onDown(point: Offset) {
Grant Toepferc9a7da32023-10-19 14:53:25 -0700577 // if the handle position is null, then it is invisible, so ignore the gesture
578 (if (isStartHandle) startHandlePosition else endHandlePosition) ?: return
579
Zach Klippenstein63870892022-01-14 12:45:18 -0800580 val selection = selection ?: return
581 val anchor = if (isStartHandle) selection.start else selection.end
582 val selectable = getAnchorSelectable(anchor) ?: return
583 // The LayoutCoordinates of the composable where the drag gesture should begin. This
584 // is used to convert the position of the beginning of the drag gesture from the
585 // composable coordinates to selection container coordinates.
586 val beginLayoutCoordinates = selectable.getLayoutCoordinates() ?: return
587
588 // The position of the character where the drag gesture should begin. This is in
589 // the composable coordinates.
Grant Toepferc9a7da32023-10-19 14:53:25 -0700590 val handlePosition = selectable.getHandlePosition(selection, isStartHandle)
591 if (handlePosition.isUnspecified) return
592 val beginCoordinates = getAdjustedCoordinates(handlePosition)
Zach Klippenstein63870892022-01-14 12:45:18 -0800593
594 // Convert the position where drag gesture begins from composable coordinates to
595 // selection container coordinates.
596 currentDragPosition = requireContainerCoordinates().localPositionOf(
597 beginLayoutCoordinates,
598 beginCoordinates
599 )
600 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800601 showToolbar = false
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800602 }
603
604 override fun onStart(startPoint: Offset) {
Grant Toepferc9a7da32023-10-19 14:53:25 -0700605 draggingHandle ?: return
606
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800607 val selection = selection!!
Grant Toepferc9a7da32023-10-19 14:53:25 -0700608 val anchor = if (isStartHandle) selection.start else selection.end
609 val selectable = checkNotNull(selectionRegistrar.selectableMap[anchor.selectableId]) {
610 "SelectionRegistrar should contain the current selection's selectableIds"
611 }
612
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800613 // The LayoutCoordinates of the composable where the drag gesture should begin. This
614 // is used to convert the position of the beginning of the drag gesture from the
615 // composable coordinates to selection container coordinates.
Grant Toepferc9a7da32023-10-19 14:53:25 -0700616 val beginLayoutCoordinates = checkNotNull(selectable.getLayoutCoordinates()) {
617 "Current selectable should have layout coordinates."
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800618 }
619
620 // The position of the character where the drag gesture should begin. This is in
621 // the composable coordinates.
Grant Toepferc9a7da32023-10-19 14:53:25 -0700622 val handlePosition = selectable.getHandlePosition(selection, isStartHandle)
623 if (handlePosition.isUnspecified) return
624 val beginCoordinates = getAdjustedCoordinates(handlePosition)
Siyamed Sinir472c3162019-10-21 23:41:00 -0700625
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800626 // Convert the position where drag gesture begins from composable coordinates to
627 // selection container coordinates.
628 dragBeginPosition = requireContainerCoordinates().localPositionOf(
629 beginLayoutCoordinates,
630 beginCoordinates
631 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700632
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800633 // Zero out the total distance that being dragged.
634 dragTotalDistance = Offset.Zero
635 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700636
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800637 override fun onDrag(delta: Offset) {
Grant Toepferc9a7da32023-10-19 14:53:25 -0700638 draggingHandle ?: return
639
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800640 dragTotalDistance += delta
641 val endPosition = dragBeginPosition + dragTotalDistance
642 val consumed = updateSelection(
643 newPosition = endPosition,
644 previousPosition = dragBeginPosition,
645 isStartHandle = isStartHandle,
646 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
647 )
648 if (consumed) {
649 dragBeginPosition = endPosition
Nader Jawad6df06122020-06-03 15:27:08 -0700650 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700651 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800652 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700653
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800654 private fun done() {
655 showToolbar = true
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800656 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800657 currentDragPosition = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800658 }
haoyue6d80a12020-12-02 16:04:52 -0800659
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800660 override fun onUp() = done()
661 override fun onStop() = done()
662 override fun onCancel() = done()
Qingqing Deng6f56a912019-05-13 10:10:37 -0700663 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700664
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700665 /**
666 * Detect tap without consuming the up event.
667 */
668 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
George Mount32de9dd2022-10-05 14:51:06 -0700669 awaitEachGesture {
670 waitForUpOrCancellation()?.let {
671 onTap(it.position)
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700672 }
673 }
674 }
675
676 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
677 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
678 }
679
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700680 private fun convertToContainerCoordinates(
681 layoutCoordinates: LayoutCoordinates,
682 offset: Offset
Grant Toepferd681b5c2023-06-14 11:24:02 -0700683 ): Offset {
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700684 val coordinates = containerLayoutCoordinates
Grant Toepferd681b5c2023-06-14 11:24:02 -0700685 if (coordinates == null || !coordinates.isAttached) return Offset.Unspecified
George Mount77ca2a22020-12-11 17:46:19 +0000686 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700687 }
688
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700689 /**
690 * Cancel the previous selection and start a new selection at the given [position].
691 * It's used for long-press, double-click, triple-click and so on to start selection.
692 *
693 * @param position initial position of the selection. Both start and end handle is considered
694 * at this position.
695 * @param isStartHandle whether it's considered as the start handle moving. This parameter
696 * will influence the [SelectionAdjustment]'s behavior. For example,
697 * [SelectionAdjustment.Character] only adjust the moving handle.
698 * @param adjustment the selection adjustment.
699 */
700 private fun startSelection(
701 position: Offset,
702 isStartHandle: Boolean,
703 adjustment: SelectionAdjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700704 ) {
Grant Toepferd681b5c2023-06-14 11:24:02 -0700705 previousSelectionLayout = null
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700706 updateSelection(
Grant Toepfer3fb01402023-10-13 12:44:44 -0700707 position = position,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700708 previousHandlePosition = Offset.Unspecified,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700709 isStartHandle = isStartHandle,
710 adjustment = adjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700711 )
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700712 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700713
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700714 /**
715 * Updates the selection after one of the selection handle moved.
716 *
717 * @param newPosition the new position of the moving selection handle.
718 * @param previousPosition the previous position of the moving selection handle.
719 * @param isStartHandle whether the moving selection handle is the start handle.
720 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
721 * produce the final selection range.
722 *
723 * @return a boolean representing whether the movement is consumed.
724 *
725 * @see SelectionAdjustment
726 */
727 internal fun updateSelection(
728 newPosition: Offset?,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700729 previousPosition: Offset,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700730 isStartHandle: Boolean,
731 adjustment: SelectionAdjustment,
732 ): Boolean {
733 if (newPosition == null) return false
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700734 return updateSelection(
Grant Toepfer3fb01402023-10-13 12:44:44 -0700735 position = newPosition,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700736 previousHandlePosition = previousPosition,
737 isStartHandle = isStartHandle,
738 adjustment = adjustment
739 )
740 }
741
742 /**
743 * Updates the selection after one of the selection handle moved.
744 *
745 * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
746 * selection handle is updated each time. The only exception is that when a new selection is
747 * started. In this case, [previousHandlePosition] is always null.
748 *
Grant Toepfer3fb01402023-10-13 12:44:44 -0700749 * @param position the position of the current gesture.
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700750 * @param previousHandlePosition the position of the moving handle before the update.
751 * @param isStartHandle whether the moving selection handle is the start handle.
752 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
753 * produce the final selection range.
754 *
755 * @return a boolean representing whether the movement is consumed. It's useful for the case
756 * where a selection handle is updating consecutively. When the return value is true, it's
757 * expected that the caller will update the [startHandlePosition] to be the given
758 * [endHandlePosition] in following calls.
759 *
760 * @see SelectionAdjustment
761 */
762 internal fun updateSelection(
Grant Toepfer3fb01402023-10-13 12:44:44 -0700763 position: Offset,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700764 previousHandlePosition: Offset,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700765 isStartHandle: Boolean,
766 adjustment: SelectionAdjustment,
767 ): Boolean {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800768 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Grant Toepfer3fb01402023-10-13 12:44:44 -0700769 currentDragPosition = position
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700770
Grant Toepfer3fb01402023-10-13 12:44:44 -0700771 val selectionLayout = getSelectionLayout(position, previousHandlePosition, isStartHandle)
Grant Toepferd681b5c2023-06-14 11:24:02 -0700772 if (!selectionLayout.shouldRecomputeSelection(previousSelectionLayout)) {
773 return false
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700774 }
Grant Toepferd681b5c2023-06-14 11:24:02 -0700775
776 val newSelection = adjustment.adjust(selectionLayout)
777 if (newSelection != selection) {
778 selectionChanged(selectionLayout, newSelection)
779 }
780 previousSelectionLayout = selectionLayout
781 return true
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700782 }
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200783
Grant Toepferd681b5c2023-06-14 11:24:02 -0700784 private fun getSelectionLayout(
Grant Toepfer3fb01402023-10-13 12:44:44 -0700785 position: Offset,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700786 previousHandlePosition: Offset,
787 isStartHandle: Boolean,
788 ): SelectionLayout {
789 val containerCoordinates = requireContainerCoordinates()
790 val sortedSelectables = selectionRegistrar.sort(containerCoordinates)
791
Grant Toepfer39043c82024-04-22 11:33:23 -0700792 val idToIndexMap = mutableLongIntMapOf()
Grant Toepferd681b5c2023-06-14 11:24:02 -0700793 sortedSelectables.fastForEachIndexed { index, selectable ->
794 idToIndexMap[selectable.selectableId] = index
795 }
796
797 val selectableIdOrderingComparator = compareBy<Long> { idToIndexMap[it] }
798
799 // if previous handle is null, then treat this as a new selection.
800 val previousSelection = if (previousHandlePosition.isUnspecified) null else selection
801 val builder = SelectionLayoutBuilder(
Grant Toepfer3fb01402023-10-13 12:44:44 -0700802 currentPosition = position,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700803 previousHandlePosition = previousHandlePosition,
804 containerCoordinates = containerCoordinates,
805 isStartHandle = isStartHandle,
806 previousSelection = previousSelection,
807 selectableIdOrderingComparator = selectableIdOrderingComparator,
808 )
809
810 sortedSelectables.fastForEach {
811 it.appendSelectableInfoToBuilder(builder)
812 }
813
814 return builder.build()
815 }
816
817 private fun selectionChanged(selectionLayout: SelectionLayout, newSelection: Selection) {
818 if (shouldPerformHaptics()) {
819 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
820 }
821 selectionRegistrar.subselections = selectionLayout.createSubSelections(newSelection)
822 onSelectionChange(newSelection)
823 }
824
825 @VisibleForTesting
826 internal fun shouldPerformHaptics(): Boolean =
827 isInTouchMode && selectionRegistrar.selectables.fastAny { it.getText().isNotEmpty() }
828
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200829 fun contextMenuOpenAdjustment(position: Offset) {
830 val isEmptySelection = selection?.toTextRange()?.collapsed ?: true
831 // TODO(b/209483184) the logic should be more complex here, it should check that current
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800832 // selection doesn't include click position
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200833 if (isEmptySelection) {
834 startSelection(
835 position = position,
836 isStartHandle = true,
837 adjustment = SelectionAdjustment.Word
838 )
839 }
840 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700841}
842
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200843internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700844 return lhs?.merge(rhs) ?: rhs
845}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800846
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100847internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
848
Zach Klippensteinadabe342021-11-11 16:38:13 -0800849internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
850
Grant Toepferef76a142023-11-07 11:56:49 -0700851private val invertedInfiniteRect = Rect(
852 left = Float.POSITIVE_INFINITY,
853 top = Float.POSITIVE_INFINITY,
854 right = Float.NEGATIVE_INFINITY,
855 bottom = Float.NEGATIVE_INFINITY
856)
857
858private fun <T> List<T>.firstAndLast(): List<T> = when (size) {
859 0, 1 -> this
860 else -> listOf(first(), last())
861}
862
863/**
864 * Get the selected region rect in the given [containerCoordinates].
865 * This will compute the smallest rect that contains every first/last
866 * character bounding box of each selectable. If for any reason there are no
867 * bounding boxes, then the [invertedInfiniteRect] is returned.
868 */
869@VisibleForTesting
870internal fun getSelectedRegionRect(
871 selectableSubSelectionPairs: List<Pair<Selectable, Selection>>,
872 containerCoordinates: LayoutCoordinates,
873): Rect {
874 if (selectableSubSelectionPairs.isEmpty()) return invertedInfiniteRect
875 var (containerLeft, containerTop, containerRight, containerBottom) = invertedInfiniteRect
876 selectableSubSelectionPairs.fastForEach { (selectable, subSelection) ->
877 val startOffset = subSelection.start.offset
878 val endOffset = subSelection.end.offset
879 if (startOffset == endOffset) return@fastForEach
880 val localCoordinates = selectable.getLayoutCoordinates() ?: return@fastForEach
881
882 val minOffset = minOf(startOffset, endOffset)
883 val maxOffset = maxOf(startOffset, endOffset)
884 val offsets = if (minOffset == maxOffset - 1) {
885 intArrayOf(minOffset)
886 } else {
887 intArrayOf(minOffset, maxOffset - 1)
888 }
889 var (left, top, right, bottom) = invertedInfiniteRect
890 for (i in offsets) {
891 val rect = selectable.getBoundingBox(i)
892 left = minOf(left, rect.left)
893 top = minOf(top, rect.top)
894 right = maxOf(right, rect.right)
895 bottom = maxOf(bottom, rect.bottom)
896 }
897
898 val localTopLeft = Offset(left, top)
899 val localBottomRight = Offset(right, bottom)
900
901 val containerTopLeft =
902 containerCoordinates.localPositionOf(localCoordinates, localTopLeft)
903 val containerBottomRight =
904 containerCoordinates.localPositionOf(localCoordinates, localBottomRight)
905
906 containerLeft = minOf(containerLeft, containerTopLeft.x)
907 containerTop = minOf(containerTop, containerTopLeft.y)
908 containerRight = maxOf(containerRight, containerBottomRight.x)
909 containerBottom = maxOf(containerBottom, containerBottomRight.y)
910 }
911 return Rect(containerLeft, containerTop, containerRight, containerBottom)
912}
913
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800914internal fun calculateSelectionMagnifierCenterAndroid(
915 manager: SelectionManager,
916 magnifierSize: IntSize
917): Offset {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800918 val selection = manager.selection ?: return Offset.Unspecified
919 return when (manager.draggingHandle) {
920 null -> return Offset.Unspecified
Grant Toepferd681b5c2023-06-14 11:24:02 -0700921 Handle.SelectionStart -> getMagnifierCenter(manager, magnifierSize, selection.start)
922 Handle.SelectionEnd -> getMagnifierCenter(manager, magnifierSize, selection.end)
Zach Klippensteinadabe342021-11-11 16:38:13 -0800923 Handle.Cursor -> error("SelectionContainer does not support cursor")
924 }
925}
926
Grant Toepferd681b5c2023-06-14 11:24:02 -0700927private fun getMagnifierCenter(
928 manager: SelectionManager,
929 magnifierSize: IntSize,
930 anchor: AnchorInfo
931): Offset {
932 val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
933 val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
934 val selectableCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified
935 val offset = anchor.offset
936
937 if (offset > selectable.getLastVisibleOffset()) return Offset.Unspecified
938
939 // The horizontal position doesn't snap to cursor positions but should directly track the
940 // actual drag.
941 val localDragPosition = selectableCoordinates.localPositionOf(
942 containerCoordinates,
943 manager.currentDragPosition!!
944 )
945 val dragX = localDragPosition.x
946
947 // But it is constrained by the horizontal bounds of the current line.
948 val lineRange = selectable.getRangeOfLineContaining(offset)
949 val textConstrainedX = if (lineRange.collapsed) {
950 // A collapsed range implies the text is empty.
951 // line left and right are equal for this offset, so use either
952 selectable.getLineLeft(offset)
953 } else {
954 val lineStartX = selectable.getLineLeft(lineRange.start)
955 val lineEndX = selectable.getLineRight(lineRange.end - 1)
956 // in RTL/BiDi, lineStartX may be larger than lineEndX
957 val minX = minOf(lineStartX, lineEndX)
958 val maxX = maxOf(lineStartX, lineEndX)
959 dragX.coerceIn(minX, maxX)
960 }
961
962 // selectable couldn't determine horizontals
963 if (textConstrainedX == -1f) return Offset.Unspecified
964
965 // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
966 // magnifier actually is). See
967 // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
968 if ((dragX - textConstrainedX).absoluteValue > magnifierSize.width / 2) {
969 return Offset.Unspecified
970 }
971
972 val lineCenterY = selectable.getCenterYForOffset(offset)
973
974 // selectable couldn't determine the line center
975 if (lineCenterY == -1f) return Offset.Unspecified
976
977 return containerCoordinates.localPositionOf(
978 sourceCoordinates = selectableCoordinates,
979 relativeToSource = Offset(textConstrainedX, lineCenterY)
980 )
981}
982
haoyue2678c62020-12-09 08:39:12 -0800983/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800984internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800985 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
986 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
987 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000988 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800989 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000990 windowToLocal(boundsInWindow.topLeft),
991 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800992 )
993}
994
haoyuac341f02021-01-22 22:01:56 -0800995internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800996 offset.x in left..right && offset.y in top..bottom