[go: nahoru, domu]

blob: ec504b57bdf1c8f1aca2c2222c37cbba3b9bd22b [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
haoyuc40d02752021-01-25 17:32:47 -080020import androidx.compose.foundation.focusable
George Mount32de9dd2022-10-05 14:51:06 -070021import androidx.compose.foundation.gestures.awaitEachGesture
Ralston Da Silvade62bc62021-06-02 17:46:44 -070022import androidx.compose.foundation.gestures.waitForUpOrCancellation
Zach Klippensteinadabe342021-11-11 16:38:13 -080023import androidx.compose.foundation.text.Handle
24import androidx.compose.foundation.text.TextDragObserver
25import androidx.compose.foundation.text.selection.Selection.AnchorInfo
Grant Toepfer427dbdf2023-09-05 18:38:42 -070026import androidx.compose.foundation.text2.input.internal.coerceIn
Zach Klippensteinadabe342021-11-11 16:38:13 -080027import androidx.compose.runtime.MutableState
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010028import androidx.compose.runtime.State
29import androidx.compose.runtime.getValue
30import androidx.compose.runtime.mutableStateOf
31import androidx.compose.runtime.setValue
haoyuc40d02752021-01-25 17:32:47 -080032import androidx.compose.ui.Modifier
33import androidx.compose.ui.focus.FocusRequester
34import androidx.compose.ui.focus.focusRequester
haoyuc40d02752021-01-25 17:32:47 -080035import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010036import androidx.compose.ui.geometry.Offset
37import androidx.compose.ui.geometry.Rect
Grant Toepferd681b5c2023-06-14 11:24:02 -070038import androidx.compose.ui.geometry.isSpecified
39import androidx.compose.ui.geometry.isUnspecified
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010040import androidx.compose.ui.hapticfeedback.HapticFeedback
41import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010042import androidx.compose.ui.input.key.KeyEvent
43import androidx.compose.ui.input.key.onKeyEvent
Ralston Da Silvade62bc62021-06-02 17:46:44 -070044import androidx.compose.ui.input.pointer.PointerInputScope
45import androidx.compose.ui.input.pointer.pointerInput
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010046import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000047import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080048import androidx.compose.ui.layout.onGloballyPositioned
Grant Toepferef76a142023-11-07 11:56:49 -070049import androidx.compose.ui.layout.positionInRoot
haoyu7ad5ea32021-03-22 10:36:35 -070050import androidx.compose.ui.layout.positionInWindow
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010051import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010052import androidx.compose.ui.platform.TextToolbar
53import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010054import androidx.compose.ui.text.AnnotatedString
Grant Toepfer427dbdf2023-09-05 18:38:42 -070055import androidx.compose.ui.text.buildAnnotatedString
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -080056import androidx.compose.ui.unit.IntSize
Grant Toepferd681b5c2023-06-14 11:24:02 -070057import androidx.compose.ui.util.fastAny
Louis Pullen-Freilich1ffdbfc2023-08-24 18:35:34 +010058import androidx.compose.ui.util.fastFold
Grant Toepferfefbd7a2023-03-03 15:02:55 -080059import androidx.compose.ui.util.fastForEach
Grant Toepferd681b5c2023-06-14 11:24:02 -070060import androidx.compose.ui.util.fastForEachIndexed
Grant Toepferef76a142023-11-07 11:56:49 -070061import androidx.compose.ui.util.fastMapNotNull
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -080062import kotlin.math.absoluteValue
Qingqing Deng6f56a912019-05-13 10:10:37 -070063
Qingqing Deng35f97ea2019-09-18 19:24:37 -070064/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010065 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070066 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070067internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080068
69 private val _selection: MutableState<Selection?> = mutableStateOf(null)
70
Qingqing Deng6f56a912019-05-13 10:10:37 -070071 /**
72 * The current selection.
73 */
Zach Klippensteinadabe342021-11-11 16:38:13 -080074 var selection: Selection?
75 get() = _selection.value
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010076 set(value) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080077 _selection.value = value
haoyu9085c882020-12-08 12:01:06 -080078 if (value != null) {
79 updateHandleOffsets()
80 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010081 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070082
83 /**
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010084 * Is touch mode active
85 */
Grant Toepferfefbd7a2023-03-03 15:02:55 -080086 private val _isInTouchMode = mutableStateOf(true)
87 var isInTouchMode: Boolean
88 get() = _isInTouchMode.value
89 set(value) {
90 if (_isInTouchMode.value != value) {
91 _isInTouchMode.value = value
Grant Toepfer427dbdf2023-09-05 18:38:42 -070092 updateSelectionToolbar()
Grant Toepferfefbd7a2023-03-03 15:02:55 -080093 }
94 }
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010095
96 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070097 * The manager will invoke this every time it comes to the conclusion that the selection should
98 * change. The expectation is that this callback will end up causing `setSelection` to get
99 * called. This is what makes this a "controlled component".
100 */
101 var onSelectionChange: (Selection?) -> Unit = {}
102
103 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800104 * [HapticFeedback] handle to perform haptic feedback.
105 */
106 var hapticFeedBack: HapticFeedback? = null
107
108 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700109 * [ClipboardManager] to perform clipboard features.
110 */
111 var clipboardManager: ClipboardManager? = null
112
113 /**
Qingqing Denga89450d2020-04-03 19:11:49 -0700114 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
115 */
116 var textToolbar: TextToolbar? = null
117
118 /**
haoyuc40d02752021-01-25 17:32:47 -0800119 * Focus requester used to request focus when selection becomes active.
120 */
121 var focusRequester: FocusRequester = FocusRequester()
122
123 /**
haoyu3c3fb452021-02-18 01:01:14 -0800124 * Return true if the corresponding SelectionContainer is focused.
haoyuc40d02752021-01-25 17:32:47 -0800125 */
haoyu3c3fb452021-02-18 01:01:14 -0800126 var hasFocus: Boolean by mutableStateOf(false)
haoyuc40d02752021-01-25 17:32:47 -0800127
128 /**
129 * Modifier for selection container.
130 */
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800131 val modifier
132 get() = Modifier
133 .onClearSelectionRequested { onRelease() }
134 .onGloballyPositioned { containerLayoutCoordinates = it }
135 .focusRequester(focusRequester)
136 .onFocusChanged { focusState ->
137 if (!focusState.isFocused && hasFocus) {
138 onRelease()
139 }
140 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800141 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800142 .focusable()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800143 .updateSelectionTouchMode { isInTouchMode = it }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800144 .onKeyEvent {
145 if (isCopyKeyEvent(it)) {
146 copy()
147 true
148 } else {
149 false
150 }
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100151 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800152 .then(if (shouldShowMagnifier) Modifier.selectionMagnifier(this) else Modifier)
haoyuc40d02752021-01-25 17:32:47 -0800153
haoyu7ad5ea32021-03-22 10:36:35 -0700154 private var previousPosition: Offset? = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800155
haoyuc40d02752021-01-25 17:32:47 -0800156 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700157 * Layout Coordinates of the selection container.
158 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100159 var containerLayoutCoordinates: LayoutCoordinates? = null
160 set(value) {
161 field = value
haoyu2c6e9842021-03-30 17:39:04 -0700162 if (hasFocus && selection != null) {
163 val positionInWindow = value?.positionInWindow()
164 if (previousPosition != positionInWindow) {
165 previousPosition = positionInWindow
166 updateHandleOffsets()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700167 updateSelectionToolbar()
haoyu2c6e9842021-03-30 17:39:04 -0700168 }
haoyu3c3fb452021-02-18 01:01:14 -0800169 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100170 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700171
172 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700173 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
174 * recalculated.
175 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800176 internal var dragBeginPosition by mutableStateOf(Offset.Zero)
177 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700178
179 /**
180 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
181 * it will be zeroed out.
182 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800183 internal var dragTotalDistance by mutableStateOf(Offset.Zero)
184 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700185
186 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100187 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
188 * is null when handle shouldn't be displayed.
189 * It is a [State] so reading it during the composition will cause recomposition every time
190 * the position has been changed.
191 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800192 var startHandlePosition: Offset? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100193 private set
194
195 /**
196 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
197 * is null when handle shouldn't be displayed.
198 * It is a [State] so reading it during the composition will cause recomposition every time
199 * the position has been changed.
200 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800201 var endHandlePosition: Offset? by mutableStateOf(null)
202 private set
203
204 /**
Zach Klippenstein63870892022-01-14 12:45:18 -0800205 * The handle that is currently being dragged, or null when no handle is being dragged. To get
206 * the position of the last drag event, use [currentDragPosition].
Zach Klippensteinadabe342021-11-11 16:38:13 -0800207 */
208 var draggingHandle: Handle? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100209 private set
210
Zach Klippenstein63870892022-01-14 12:45:18 -0800211 /**
212 * When a handle is being dragged (i.e. [draggingHandle] is non-null), this is the last position
213 * of the actual drag event. It is not clamped to handle positions. Null when not being dragged.
214 */
215 var currentDragPosition: Offset? by mutableStateOf(null)
216 private set
217
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800218 private val shouldShowMagnifier
Grant Toepferd681b5c2023-06-14 11:24:02 -0700219 get() = draggingHandle != null && isInTouchMode && !isTriviallyCollapsedSelection()
220
221 @VisibleForTesting
222 internal var previousSelectionLayout: SelectionLayout? = null
Zach Klippenstein4688a462021-12-08 08:28:07 -0800223
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100224 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700225 selectionRegistrar.onPositionChangeCallback = { selectableId ->
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700226 if (selectableId in selectionRegistrar.subselections) {
haoyu7ad5ea32021-03-22 10:36:35 -0700227 updateHandleOffsets()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700228 updateSelectionToolbar()
haoyu7ad5ea32021-03-22 10:36:35 -0700229 }
haoyue6d80a12020-12-02 16:04:52 -0800230 }
231
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100232 selectionRegistrar.onSelectionUpdateStartCallback =
Grant Toepferc7d1a4e2023-09-18 23:06:37 +0000233 { isInTouchMode, layoutCoordinates, rawPosition, selectionMode ->
234 val textRect = with(layoutCoordinates.size) {
235 Rect(0f, 0f, width.toFloat(), height.toFloat())
236 }
237
238 val position = if (textRect.containsInclusive(rawPosition)) {
239 rawPosition
240 } else {
241 rawPosition.coerceIn(textRect)
242 }
243
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700244 val positionInContainer = convertToContainerCoordinates(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100245 layoutCoordinates,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700246 position
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100247 )
248
Grant Toepferd681b5c2023-06-14 11:24:02 -0700249 if (positionInContainer.isSpecified) {
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800250 this.isInTouchMode = isInTouchMode
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700251 startSelection(
252 position = positionInContainer,
253 isStartHandle = false,
254 adjustment = selectionMode
255 )
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100256
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700257 focusRequester.requestFocus()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800258 showToolbar = false
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700259 }
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100260 }
haoyue6d80a12020-12-02 16:04:52 -0800261
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700262 selectionRegistrar.onSelectionUpdateSelectAll =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800263 { isInTouchMode, selectableId ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700264 val (newSelection, newSubselection) = selectAll(
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700265 selectableId = selectableId,
266 previousSelection = selection,
267 )
268 if (newSelection != selection) {
269 selectionRegistrar.subselections = newSubselection
270 onSelectionChange(newSelection)
271 }
272
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800273 this.isInTouchMode = isInTouchMode
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700274 focusRequester.requestFocus()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800275 showToolbar = false
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700276 }
277
haoyue6d80a12020-12-02 16:04:52 -0800278 selectionRegistrar.onSelectionUpdateCallback =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800279 { isInTouchMode,
280 layoutCoordinates,
281 newPosition,
282 previousPosition,
283 isStartHandle,
284 selectionMode ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700285 val newPositionInContainer =
286 convertToContainerCoordinates(layoutCoordinates, newPosition)
287 val previousPositionInContainer =
288 convertToContainerCoordinates(layoutCoordinates, previousPosition)
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100289
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800290 this.isInTouchMode = isInTouchMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700291 updateSelection(
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700292 newPosition = newPositionInContainer,
293 previousPosition = previousPositionInContainer,
294 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700295 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700296 )
297 }
haoyue6d80a12020-12-02 16:04:52 -0800298
299 selectionRegistrar.onSelectionUpdateEndCallback = {
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800300 showToolbar = true
Zach Klippensteinadabe342021-11-11 16:38:13 -0800301 // This property is set by updateSelection while dragging, so we need to clear it after
302 // the original selection drag.
303 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800304 currentDragPosition = null
haoyue6d80a12020-12-02 16:04:52 -0800305 }
haoyu9085c882020-12-08 12:01:06 -0800306
haoyue04245e2021-03-08 14:52:56 -0800307 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
308 if (selectableKey in selectionRegistrar.subselections) {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700309 // Clear the selection range of each Selectable.
haoyu9085c882020-12-08 12:01:06 -0800310 onRelease()
311 selection = null
312 }
313 }
haoyue04245e2021-03-08 14:52:56 -0800314
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700315 selectionRegistrar.afterSelectableUnsubscribe = { selectableId ->
316 if (selectableId == selection?.start?.selectableId) {
haoyue04245e2021-03-08 14:52:56 -0800317 // The selectable that contains a selection handle just unsubscribed.
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700318 // Hide the associated selection handle
haoyue04245e2021-03-08 14:52:56 -0800319 startHandlePosition = null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700320 }
321 if (selectableId == selection?.end?.selectableId) {
haoyue04245e2021-03-08 14:52:56 -0800322 endHandlePosition = null
323 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700324
325 if (selectableId in selectionRegistrar.subselections) {
326 // Unsubscribing the selectable may make the selection empty, which would hide it.
327 updateSelectionToolbar()
328 }
haoyue04245e2021-03-08 14:52:56 -0800329 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100330 }
331
Zach Klippensteinadabe342021-11-11 16:38:13 -0800332 /**
333 * Returns the [Selectable] responsible for managing the given [AnchorInfo], or null if the
334 * anchor is not from a currently-registered [Selectable].
335 */
336 internal fun getAnchorSelectable(anchor: AnchorInfo): Selectable? {
337 return selectionRegistrar.selectableMap[anchor.selectableId]
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100338 }
339
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100340 private fun updateHandleOffsets() {
341 val selection = selection
342 val containerCoordinates = containerLayoutCoordinates
Zach Klippensteinadabe342021-11-11 16:38:13 -0800343 val startSelectable = selection?.start?.let(::getAnchorSelectable)
344 val endSelectable = selection?.end?.let(::getAnchorSelectable)
haoyue04245e2021-03-08 14:52:56 -0800345 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
346 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700347
haoyue2678c62020-12-09 08:39:12 -0800348 if (
349 selection == null ||
350 containerCoordinates == null ||
351 !containerCoordinates.isAttached ||
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700352 (startLayoutCoordinates == null && endLayoutCoordinates == null)
haoyue2678c62020-12-09 08:39:12 -0800353 ) {
354 this.startHandlePosition = null
355 this.endHandlePosition = null
356 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100357 }
haoyue2678c62020-12-09 08:39:12 -0800358
haoyue2678c62020-12-09 08:39:12 -0800359 val visibleBounds = containerCoordinates.visibleBounds()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700360 this.startHandlePosition = startLayoutCoordinates?.let { handleCoordinates ->
361 // Set the new handle position only if the handle is in visible bounds or
362 // the handle is still dragging. If handle goes out of visible bounds during drag,
363 // handle popup is also removed from composition, halting the drag gesture. This
364 // affects multiple text selection when selected text is configured with maxLines=1
365 // and overflow=clip.
366 val handlePosition = startSelectable.getHandlePosition(selection, isStartHandle = true)
Grant Toepfer0880f742023-10-11 16:34:51 -0700367 if (handlePosition.isUnspecified) return@let null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700368 val position = containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
369 position.takeIf {
370 draggingHandle == Handle.SelectionStart || visibleBounds.containsInclusive(it)
371 }
Halil Ozercanb2486f12023-01-09 07:40:22 +0000372 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700373
374 this.endHandlePosition = endLayoutCoordinates?.let { handleCoordinates ->
375 val handlePosition = endSelectable.getHandlePosition(selection, isStartHandle = false)
Grant Toepfer0880f742023-10-11 16:34:51 -0700376 if (handlePosition.isUnspecified) return@let null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700377 val position = containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
378 position.takeIf {
379 draggingHandle == Handle.SelectionEnd || visibleBounds.containsInclusive(it)
380 }
Halil Ozercanb2486f12023-01-09 07:40:22 +0000381 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100382 }
383
384 /**
385 * Returns non-nullable [containerLayoutCoordinates].
386 */
387 internal fun requireContainerCoordinates(): LayoutCoordinates {
388 val coordinates = containerLayoutCoordinates
Ralston Da Silvaf24ae422023-05-30 12:59:24 -0700389 requireNotNull(coordinates) { "null coordinates" }
390 require(coordinates.isAttached) { "unattached coordinates" }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100391 return coordinates
392 }
393
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700394 internal fun selectAll(
395 selectableId: Long,
396 previousSelection: Selection?
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700397 ): Pair<Selection?, Map<Long, Selection>> {
398 val subselections = mutableMapOf<Long, Selection>()
399 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
400 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
401 val selection = if (selectable.selectableId == selectableId)
402 selectable.getSelectAllSelection() else null
403 selection?.let { subselections[selectable.selectableId] = it }
404 merge(mergedSelection, selection)
405 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800406 if (isInTouchMode && newSelection != previousSelection) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700407 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
408 }
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700409 return Pair(newSelection, subselections)
410 }
411
Grant Toepferd681b5c2023-06-14 11:24:02 -0700412 /**
413 * Returns whether the start and end anchors are equal.
414 *
415 * It is possible that this returns true, but the selection is still empty because it has
416 * multiple collapsed selections across multiple selectables. To test for that case, use
417 * [isNonEmptySelection].
418 */
419 internal fun isTriviallyCollapsedSelection(): Boolean {
420 val selection = selection ?: return true
421 return selection.start == selection.end
422 }
423
424 /**
425 * Returns whether the selection selects zero characters.
426 *
427 * It is possible that the selection anchors are different but still result in a zero-width
428 * selection. In this case, you may want to still show the selection anchors, but not allow for
429 * a user to try and copy zero characters. To test for whether the anchors are equal, use
430 * [isTriviallyCollapsedSelection].
431 */
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800432 internal fun isNonEmptySelection(): Boolean {
433 val selection = selection ?: return false
Grant Toepferd681b5c2023-06-14 11:24:02 -0700434 if (selection.start == selection.end) {
435 return false
436 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800437
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700438 if (selection.start.selectableId == selection.end.selectableId) {
439 // Selection is in the same selectable, but not the same anchors,
440 // so there must be some selected text.
441 return true
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800442 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700443
444 // All subselections associated with a selectable must be an empty selection.
445 return selectionRegistrar.sort(requireContainerCoordinates()).fastAny { selectable ->
446 selectionRegistrar.subselections[selectable.selectableId]
447 ?.run { start.offset != end.offset }
448 ?: false
449 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800450 }
451
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800452 internal fun getSelectedText(): AnnotatedString? {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700453 if (selection == null || selectionRegistrar.subselections.isEmpty()) {
454 return null
455 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800456
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700457 return buildAnnotatedString {
458 selectionRegistrar.sort(requireContainerCoordinates()).fastForEach { selectable ->
459 selectionRegistrar.subselections[selectable.selectableId]?.let { subSelection ->
460 val currentText = selectable.getText()
461 val currentSelectedText = if (subSelection.handlesCrossed) {
462 currentText.subSequence(
463 subSelection.end.offset,
464 subSelection.start.offset
465 )
466 } else {
467 currentText.subSequence(
468 subSelection.start.offset,
469 subSelection.end.offset
470 )
471 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800472
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700473 append(currentSelectedText)
474 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800475 }
476 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800477 }
478
Qingqing Dengde023cc2020-04-24 14:23:41 -0700479 internal fun copy() {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700480 getSelectedText()?.takeIf { it.isNotEmpty() }?.let { clipboardManager?.setText(it) }
Qingqing Dengde023cc2020-04-24 14:23:41 -0700481 }
482
483 /**
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800484 * Whether toolbar should be shown right now.
485 * Examples: Show toolbar after user finishes selection.
486 * Hide it during selection.
487 * Hide it when no selection exists.
488 */
489 internal var showToolbar = false
490 internal set(value) {
491 field = value
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700492 updateSelectionToolbar()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800493 }
494
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700495 private fun toolbarCopy() {
496 copy()
497 onRelease()
498 }
499
500 private fun updateSelectionToolbar() {
501 if (!hasFocus) {
502 return
503 }
504
505 val textToolbar = textToolbar ?: return
506 if (showToolbar && isInTouchMode && isNonEmptySelection()) {
507 val rect = getContentRect() ?: return
508 textToolbar.showMenu(rect = rect, onCopyRequested = ::toolbarCopy)
509 } else if (textToolbar.status == TextToolbarStatus.Shown) {
510 textToolbar.hide()
511 }
512 }
513
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800514 /**
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700515 * Calculate selected region as [Rect].
516 * The result is the smallest [Rect] that encapsulates the entire selection,
517 * coerced into visible bounds.
Qingqing Dengde023cc2020-04-24 14:23:41 -0700518 */
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700519 private fun getContentRect(): Rect? {
520 selection ?: return null
521 val containerCoordinates = containerLayoutCoordinates ?: return null
522 if (!containerCoordinates.isAttached) return null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700523
Grant Toepferef76a142023-11-07 11:56:49 -0700524 val selectableSubSelections = selectionRegistrar.sort(requireContainerCoordinates())
525 .fastMapNotNull { selectable ->
526 selectionRegistrar.subselections[selectable.selectableId]
527 ?.let { selectable to it }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700528 }
Grant Toepferef76a142023-11-07 11:56:49 -0700529 .firstAndLast()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700530
Grant Toepferef76a142023-11-07 11:56:49 -0700531 if (selectableSubSelections.isEmpty()) return null
532 val selectedRegionRect =
533 getSelectedRegionRect(selectableSubSelections, containerCoordinates)
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700534
Grant Toepferef76a142023-11-07 11:56:49 -0700535 if (selectedRegionRect == invertedInfiniteRect) return null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700536
Grant Toepferef76a142023-11-07 11:56:49 -0700537 val visibleRect = containerCoordinates.visibleBounds().intersect(selectedRegionRect)
538 // if the rectangles do not at least touch at the edges, we shouldn't show the toolbar
539 if (visibleRect.width < 0 || visibleRect.height < 0) return null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700540
Grant Toepferef76a142023-11-07 11:56:49 -0700541 val rootRect = visibleRect.translate(containerCoordinates.positionInRoot())
542 return rootRect.copy(bottom = visibleRect.bottom + HandleHeight.value * 4)
Qingqing Dengde023cc2020-04-24 14:23:41 -0700543 }
544
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800545 // This is for PressGestureDetector to cancel the selection.
546 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800547 selectionRegistrar.subselections = emptyMap()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800548 showToolbar = false
haoyue04245e2021-03-08 14:52:56 -0800549 if (selection != null) {
550 onSelectionChange(null)
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800551 if (isInTouchMode) {
552 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
553 }
haoyue04245e2021-03-08 14:52:56 -0800554 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800555 }
556
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800557 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver = object : TextDragObserver {
558 override fun onDown(point: Offset) {
Grant Toepferc9a7da32023-10-19 14:53:25 -0700559 // if the handle position is null, then it is invisible, so ignore the gesture
560 (if (isStartHandle) startHandlePosition else endHandlePosition) ?: return
561
Zach Klippenstein63870892022-01-14 12:45:18 -0800562 val selection = selection ?: return
563 val anchor = if (isStartHandle) selection.start else selection.end
564 val selectable = getAnchorSelectable(anchor) ?: return
565 // The LayoutCoordinates of the composable where the drag gesture should begin. This
566 // is used to convert the position of the beginning of the drag gesture from the
567 // composable coordinates to selection container coordinates.
568 val beginLayoutCoordinates = selectable.getLayoutCoordinates() ?: return
569
570 // The position of the character where the drag gesture should begin. This is in
571 // the composable coordinates.
Grant Toepferc9a7da32023-10-19 14:53:25 -0700572 val handlePosition = selectable.getHandlePosition(selection, isStartHandle)
573 if (handlePosition.isUnspecified) return
574 val beginCoordinates = getAdjustedCoordinates(handlePosition)
Zach Klippenstein63870892022-01-14 12:45:18 -0800575
576 // Convert the position where drag gesture begins from composable coordinates to
577 // selection container coordinates.
578 currentDragPosition = requireContainerCoordinates().localPositionOf(
579 beginLayoutCoordinates,
580 beginCoordinates
581 )
582 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800583 showToolbar = false
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800584 }
585
586 override fun onStart(startPoint: Offset) {
Grant Toepferc9a7da32023-10-19 14:53:25 -0700587 draggingHandle ?: return
588
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800589 val selection = selection!!
Grant Toepferc9a7da32023-10-19 14:53:25 -0700590 val anchor = if (isStartHandle) selection.start else selection.end
591 val selectable = checkNotNull(selectionRegistrar.selectableMap[anchor.selectableId]) {
592 "SelectionRegistrar should contain the current selection's selectableIds"
593 }
594
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800595 // The LayoutCoordinates of the composable where the drag gesture should begin. This
596 // is used to convert the position of the beginning of the drag gesture from the
597 // composable coordinates to selection container coordinates.
Grant Toepferc9a7da32023-10-19 14:53:25 -0700598 val beginLayoutCoordinates = checkNotNull(selectable.getLayoutCoordinates()) {
599 "Current selectable should have layout coordinates."
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800600 }
601
602 // The position of the character where the drag gesture should begin. This is in
603 // the composable coordinates.
Grant Toepferc9a7da32023-10-19 14:53:25 -0700604 val handlePosition = selectable.getHandlePosition(selection, isStartHandle)
605 if (handlePosition.isUnspecified) return
606 val beginCoordinates = getAdjustedCoordinates(handlePosition)
Siyamed Sinir472c3162019-10-21 23:41:00 -0700607
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800608 // Convert the position where drag gesture begins from composable coordinates to
609 // selection container coordinates.
610 dragBeginPosition = requireContainerCoordinates().localPositionOf(
611 beginLayoutCoordinates,
612 beginCoordinates
613 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700614
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800615 // Zero out the total distance that being dragged.
616 dragTotalDistance = Offset.Zero
617 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700618
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800619 override fun onDrag(delta: Offset) {
Grant Toepferc9a7da32023-10-19 14:53:25 -0700620 draggingHandle ?: return
621
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800622 dragTotalDistance += delta
623 val endPosition = dragBeginPosition + dragTotalDistance
624 val consumed = updateSelection(
625 newPosition = endPosition,
626 previousPosition = dragBeginPosition,
627 isStartHandle = isStartHandle,
628 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
629 )
630 if (consumed) {
631 dragBeginPosition = endPosition
Nader Jawad6df06122020-06-03 15:27:08 -0700632 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700633 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800634 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700635
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800636 private fun done() {
637 showToolbar = true
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800638 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800639 currentDragPosition = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800640 }
haoyue6d80a12020-12-02 16:04:52 -0800641
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800642 override fun onUp() = done()
643 override fun onStop() = done()
644 override fun onCancel() = done()
Qingqing Deng6f56a912019-05-13 10:10:37 -0700645 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700646
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700647 /**
648 * Detect tap without consuming the up event.
649 */
650 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
George Mount32de9dd2022-10-05 14:51:06 -0700651 awaitEachGesture {
652 waitForUpOrCancellation()?.let {
653 onTap(it.position)
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700654 }
655 }
656 }
657
658 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
659 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
660 }
661
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700662 private fun convertToContainerCoordinates(
663 layoutCoordinates: LayoutCoordinates,
664 offset: Offset
Grant Toepferd681b5c2023-06-14 11:24:02 -0700665 ): Offset {
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700666 val coordinates = containerLayoutCoordinates
Grant Toepferd681b5c2023-06-14 11:24:02 -0700667 if (coordinates == null || !coordinates.isAttached) return Offset.Unspecified
George Mount77ca2a22020-12-11 17:46:19 +0000668 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700669 }
670
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700671 /**
672 * Cancel the previous selection and start a new selection at the given [position].
673 * It's used for long-press, double-click, triple-click and so on to start selection.
674 *
675 * @param position initial position of the selection. Both start and end handle is considered
676 * at this position.
677 * @param isStartHandle whether it's considered as the start handle moving. This parameter
678 * will influence the [SelectionAdjustment]'s behavior. For example,
679 * [SelectionAdjustment.Character] only adjust the moving handle.
680 * @param adjustment the selection adjustment.
681 */
682 private fun startSelection(
683 position: Offset,
684 isStartHandle: Boolean,
685 adjustment: SelectionAdjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700686 ) {
Grant Toepferd681b5c2023-06-14 11:24:02 -0700687 previousSelectionLayout = null
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700688 updateSelection(
Grant Toepfer3fb01402023-10-13 12:44:44 -0700689 position = position,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700690 previousHandlePosition = Offset.Unspecified,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700691 isStartHandle = isStartHandle,
692 adjustment = adjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700693 )
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700694 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700695
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700696 /**
697 * Updates the selection after one of the selection handle moved.
698 *
699 * @param newPosition the new position of the moving selection handle.
700 * @param previousPosition the previous position of the moving selection handle.
701 * @param isStartHandle whether the moving selection handle is the start handle.
702 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
703 * produce the final selection range.
704 *
705 * @return a boolean representing whether the movement is consumed.
706 *
707 * @see SelectionAdjustment
708 */
709 internal fun updateSelection(
710 newPosition: Offset?,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700711 previousPosition: Offset,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700712 isStartHandle: Boolean,
713 adjustment: SelectionAdjustment,
714 ): Boolean {
715 if (newPosition == null) return false
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700716 return updateSelection(
Grant Toepfer3fb01402023-10-13 12:44:44 -0700717 position = newPosition,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700718 previousHandlePosition = previousPosition,
719 isStartHandle = isStartHandle,
720 adjustment = adjustment
721 )
722 }
723
724 /**
725 * Updates the selection after one of the selection handle moved.
726 *
727 * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
728 * selection handle is updated each time. The only exception is that when a new selection is
729 * started. In this case, [previousHandlePosition] is always null.
730 *
Grant Toepfer3fb01402023-10-13 12:44:44 -0700731 * @param position the position of the current gesture.
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700732 * @param previousHandlePosition the position of the moving handle before the update.
733 * @param isStartHandle whether the moving selection handle is the start handle.
734 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
735 * produce the final selection range.
736 *
737 * @return a boolean representing whether the movement is consumed. It's useful for the case
738 * where a selection handle is updating consecutively. When the return value is true, it's
739 * expected that the caller will update the [startHandlePosition] to be the given
740 * [endHandlePosition] in following calls.
741 *
742 * @see SelectionAdjustment
743 */
744 internal fun updateSelection(
Grant Toepfer3fb01402023-10-13 12:44:44 -0700745 position: Offset,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700746 previousHandlePosition: Offset,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700747 isStartHandle: Boolean,
748 adjustment: SelectionAdjustment,
749 ): Boolean {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800750 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Grant Toepfer3fb01402023-10-13 12:44:44 -0700751 currentDragPosition = position
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700752
Grant Toepfer3fb01402023-10-13 12:44:44 -0700753 val selectionLayout = getSelectionLayout(position, previousHandlePosition, isStartHandle)
Grant Toepferd681b5c2023-06-14 11:24:02 -0700754 if (!selectionLayout.shouldRecomputeSelection(previousSelectionLayout)) {
755 return false
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700756 }
Grant Toepferd681b5c2023-06-14 11:24:02 -0700757
758 val newSelection = adjustment.adjust(selectionLayout)
759 if (newSelection != selection) {
760 selectionChanged(selectionLayout, newSelection)
761 }
762 previousSelectionLayout = selectionLayout
763 return true
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700764 }
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200765
Grant Toepferd681b5c2023-06-14 11:24:02 -0700766 private fun getSelectionLayout(
Grant Toepfer3fb01402023-10-13 12:44:44 -0700767 position: Offset,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700768 previousHandlePosition: Offset,
769 isStartHandle: Boolean,
770 ): SelectionLayout {
771 val containerCoordinates = requireContainerCoordinates()
772 val sortedSelectables = selectionRegistrar.sort(containerCoordinates)
773
774 val idToIndexMap = mutableMapOf<Long, Int>()
775 sortedSelectables.fastForEachIndexed { index, selectable ->
776 idToIndexMap[selectable.selectableId] = index
777 }
778
779 val selectableIdOrderingComparator = compareBy<Long> { idToIndexMap[it] }
780
781 // if previous handle is null, then treat this as a new selection.
782 val previousSelection = if (previousHandlePosition.isUnspecified) null else selection
783 val builder = SelectionLayoutBuilder(
Grant Toepfer3fb01402023-10-13 12:44:44 -0700784 currentPosition = position,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700785 previousHandlePosition = previousHandlePosition,
786 containerCoordinates = containerCoordinates,
787 isStartHandle = isStartHandle,
788 previousSelection = previousSelection,
789 selectableIdOrderingComparator = selectableIdOrderingComparator,
790 )
791
792 sortedSelectables.fastForEach {
793 it.appendSelectableInfoToBuilder(builder)
794 }
795
796 return builder.build()
797 }
798
799 private fun selectionChanged(selectionLayout: SelectionLayout, newSelection: Selection) {
800 if (shouldPerformHaptics()) {
801 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
802 }
803 selectionRegistrar.subselections = selectionLayout.createSubSelections(newSelection)
804 onSelectionChange(newSelection)
805 }
806
807 @VisibleForTesting
808 internal fun shouldPerformHaptics(): Boolean =
809 isInTouchMode && selectionRegistrar.selectables.fastAny { it.getText().isNotEmpty() }
810
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200811 fun contextMenuOpenAdjustment(position: Offset) {
812 val isEmptySelection = selection?.toTextRange()?.collapsed ?: true
813 // TODO(b/209483184) the logic should be more complex here, it should check that current
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800814 // selection doesn't include click position
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200815 if (isEmptySelection) {
816 startSelection(
817 position = position,
818 isStartHandle = true,
819 adjustment = SelectionAdjustment.Word
820 )
821 }
822 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700823}
824
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200825internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700826 return lhs?.merge(rhs) ?: rhs
827}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800828
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100829internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
830
Zach Klippensteinadabe342021-11-11 16:38:13 -0800831internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
832
Grant Toepferef76a142023-11-07 11:56:49 -0700833private val invertedInfiniteRect = Rect(
834 left = Float.POSITIVE_INFINITY,
835 top = Float.POSITIVE_INFINITY,
836 right = Float.NEGATIVE_INFINITY,
837 bottom = Float.NEGATIVE_INFINITY
838)
839
840private fun <T> List<T>.firstAndLast(): List<T> = when (size) {
841 0, 1 -> this
842 else -> listOf(first(), last())
843}
844
845/**
846 * Get the selected region rect in the given [containerCoordinates].
847 * This will compute the smallest rect that contains every first/last
848 * character bounding box of each selectable. If for any reason there are no
849 * bounding boxes, then the [invertedInfiniteRect] is returned.
850 */
851@VisibleForTesting
852internal fun getSelectedRegionRect(
853 selectableSubSelectionPairs: List<Pair<Selectable, Selection>>,
854 containerCoordinates: LayoutCoordinates,
855): Rect {
856 if (selectableSubSelectionPairs.isEmpty()) return invertedInfiniteRect
857 var (containerLeft, containerTop, containerRight, containerBottom) = invertedInfiniteRect
858 selectableSubSelectionPairs.fastForEach { (selectable, subSelection) ->
859 val startOffset = subSelection.start.offset
860 val endOffset = subSelection.end.offset
861 if (startOffset == endOffset) return@fastForEach
862 val localCoordinates = selectable.getLayoutCoordinates() ?: return@fastForEach
863
864 val minOffset = minOf(startOffset, endOffset)
865 val maxOffset = maxOf(startOffset, endOffset)
866 val offsets = if (minOffset == maxOffset - 1) {
867 intArrayOf(minOffset)
868 } else {
869 intArrayOf(minOffset, maxOffset - 1)
870 }
871 var (left, top, right, bottom) = invertedInfiniteRect
872 for (i in offsets) {
873 val rect = selectable.getBoundingBox(i)
874 left = minOf(left, rect.left)
875 top = minOf(top, rect.top)
876 right = maxOf(right, rect.right)
877 bottom = maxOf(bottom, rect.bottom)
878 }
879
880 val localTopLeft = Offset(left, top)
881 val localBottomRight = Offset(right, bottom)
882
883 val containerTopLeft =
884 containerCoordinates.localPositionOf(localCoordinates, localTopLeft)
885 val containerBottomRight =
886 containerCoordinates.localPositionOf(localCoordinates, localBottomRight)
887
888 containerLeft = minOf(containerLeft, containerTopLeft.x)
889 containerTop = minOf(containerTop, containerTopLeft.y)
890 containerRight = maxOf(containerRight, containerBottomRight.x)
891 containerBottom = maxOf(containerBottom, containerBottomRight.y)
892 }
893 return Rect(containerLeft, containerTop, containerRight, containerBottom)
894}
895
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800896internal fun calculateSelectionMagnifierCenterAndroid(
897 manager: SelectionManager,
898 magnifierSize: IntSize
899): Offset {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800900 val selection = manager.selection ?: return Offset.Unspecified
901 return when (manager.draggingHandle) {
902 null -> return Offset.Unspecified
Grant Toepferd681b5c2023-06-14 11:24:02 -0700903 Handle.SelectionStart -> getMagnifierCenter(manager, magnifierSize, selection.start)
904 Handle.SelectionEnd -> getMagnifierCenter(manager, magnifierSize, selection.end)
Zach Klippensteinadabe342021-11-11 16:38:13 -0800905 Handle.Cursor -> error("SelectionContainer does not support cursor")
906 }
907}
908
Grant Toepferd681b5c2023-06-14 11:24:02 -0700909private fun getMagnifierCenter(
910 manager: SelectionManager,
911 magnifierSize: IntSize,
912 anchor: AnchorInfo
913): Offset {
914 val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
915 val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
916 val selectableCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified
917 val offset = anchor.offset
918
919 if (offset > selectable.getLastVisibleOffset()) return Offset.Unspecified
920
921 // The horizontal position doesn't snap to cursor positions but should directly track the
922 // actual drag.
923 val localDragPosition = selectableCoordinates.localPositionOf(
924 containerCoordinates,
925 manager.currentDragPosition!!
926 )
927 val dragX = localDragPosition.x
928
929 // But it is constrained by the horizontal bounds of the current line.
930 val lineRange = selectable.getRangeOfLineContaining(offset)
931 val textConstrainedX = if (lineRange.collapsed) {
932 // A collapsed range implies the text is empty.
933 // line left and right are equal for this offset, so use either
934 selectable.getLineLeft(offset)
935 } else {
936 val lineStartX = selectable.getLineLeft(lineRange.start)
937 val lineEndX = selectable.getLineRight(lineRange.end - 1)
938 // in RTL/BiDi, lineStartX may be larger than lineEndX
939 val minX = minOf(lineStartX, lineEndX)
940 val maxX = maxOf(lineStartX, lineEndX)
941 dragX.coerceIn(minX, maxX)
942 }
943
944 // selectable couldn't determine horizontals
945 if (textConstrainedX == -1f) return Offset.Unspecified
946
947 // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
948 // magnifier actually is). See
949 // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
950 if ((dragX - textConstrainedX).absoluteValue > magnifierSize.width / 2) {
951 return Offset.Unspecified
952 }
953
954 val lineCenterY = selectable.getCenterYForOffset(offset)
955
956 // selectable couldn't determine the line center
957 if (lineCenterY == -1f) return Offset.Unspecified
958
959 return containerCoordinates.localPositionOf(
960 sourceCoordinates = selectableCoordinates,
961 relativeToSource = Offset(textConstrainedX, lineCenterY)
962 )
963}
964
haoyue2678c62020-12-09 08:39:12 -0800965/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800966internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800967 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
968 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
969 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000970 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800971 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000972 windowToLocal(boundsInWindow.topLeft),
973 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800974 )
975}
976
haoyuac341f02021-01-22 22:01:56 -0800977internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800978 offset.x in left..right && offset.y in top..bottom