[go: nahoru, domu]

blob: 8d516e3acf886a29486007f997d20bb3901cb21f [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
Matvei Malkovc287d1b2021-01-26 15:04:34 +000017@file:Suppress("DEPRECATION")
18
haoyuac341f02021-01-22 22:01:56 -080019package androidx.compose.foundation.text.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070020
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +000021import androidx.compose.foundation.fastFold
haoyuc40d02752021-01-25 17:32:47 -080022import androidx.compose.foundation.focusable
Ralston Da Silvade62bc62021-06-02 17:46:44 -070023import androidx.compose.foundation.gestures.forEachGesture
24import androidx.compose.foundation.gestures.waitForUpOrCancellation
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010025import androidx.compose.runtime.State
26import androidx.compose.runtime.getValue
27import androidx.compose.runtime.mutableStateOf
28import androidx.compose.runtime.setValue
29import androidx.compose.runtime.structuralEqualityPolicy
haoyuc40d02752021-01-25 17:32:47 -080030import androidx.compose.ui.Modifier
31import androidx.compose.ui.focus.FocusRequester
32import androidx.compose.ui.focus.focusRequester
haoyuc40d02752021-01-25 17:32:47 -080033import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010034import androidx.compose.ui.geometry.Offset
35import androidx.compose.ui.geometry.Rect
Matvei Malkovf770a912021-03-24 18:12:41 +000036import androidx.compose.foundation.text.TextDragObserver
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010037import androidx.compose.ui.hapticfeedback.HapticFeedback
38import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010039import androidx.compose.ui.input.key.KeyEvent
40import androidx.compose.ui.input.key.onKeyEvent
Ralston Da Silvade62bc62021-06-02 17:46:44 -070041import androidx.compose.ui.input.pointer.PointerInputScope
42import androidx.compose.ui.input.pointer.pointerInput
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010043import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000044import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080045import androidx.compose.ui.layout.onGloballyPositioned
haoyu7ad5ea32021-03-22 10:36:35 -070046import androidx.compose.ui.layout.positionInWindow
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010047import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010048import androidx.compose.ui.platform.TextToolbar
49import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010050import androidx.compose.ui.text.AnnotatedString
Ralston Da Silvade62bc62021-06-02 17:46:44 -070051import kotlinx.coroutines.coroutineScope
Nader Jawade6a9b332020-05-21 13:49:20 -070052import kotlin.math.max
53import kotlin.math.min
Qingqing Deng6f56a912019-05-13 10:10:37 -070054
Qingqing Deng35f97ea2019-09-18 19:24:37 -070055/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010056 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070057 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070058internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070059 /**
60 * The current selection.
61 */
62 var selection: Selection? = null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010063 set(value) {
64 field = value
haoyu9085c882020-12-08 12:01:06 -080065 if (value != null) {
66 updateHandleOffsets()
67 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010068 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070069
70 /**
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010071 * Is touch mode active
72 */
73 var touchMode: Boolean = true
74
75 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070076 * The manager will invoke this every time it comes to the conclusion that the selection should
77 * change. The expectation is that this callback will end up causing `setSelection` to get
78 * called. This is what makes this a "controlled component".
79 */
80 var onSelectionChange: (Selection?) -> Unit = {}
81
82 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080083 * [HapticFeedback] handle to perform haptic feedback.
84 */
85 var hapticFeedBack: HapticFeedback? = null
86
87 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070088 * [ClipboardManager] to perform clipboard features.
89 */
90 var clipboardManager: ClipboardManager? = null
91
92 /**
Qingqing Denga89450d2020-04-03 19:11:49 -070093 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
94 */
95 var textToolbar: TextToolbar? = null
96
97 /**
haoyuc40d02752021-01-25 17:32:47 -080098 * Focus requester used to request focus when selection becomes active.
99 */
100 var focusRequester: FocusRequester = FocusRequester()
101
102 /**
haoyu3c3fb452021-02-18 01:01:14 -0800103 * Return true if the corresponding SelectionContainer is focused.
haoyuc40d02752021-01-25 17:32:47 -0800104 */
haoyu3c3fb452021-02-18 01:01:14 -0800105 var hasFocus: Boolean by mutableStateOf(false)
haoyuc40d02752021-01-25 17:32:47 -0800106
107 /**
108 * Modifier for selection container.
109 */
110 val modifier get() = Modifier
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700111 .onClearSelectionRequested { onRelease() }
haoyuc40d02752021-01-25 17:32:47 -0800112 .onGloballyPositioned { containerLayoutCoordinates = it }
113 .focusRequester(focusRequester)
114 .onFocusChanged { focusState ->
haoyu3c3fb452021-02-18 01:01:14 -0800115 if (!focusState.isFocused && hasFocus) {
haoyuc40d02752021-01-25 17:32:47 -0800116 onRelease()
117 }
haoyu3c3fb452021-02-18 01:01:14 -0800118 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800119 }
haoyu3c3fb452021-02-18 01:01:14 -0800120 .focusable()
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100121 .onKeyEvent {
122 if (isCopyKeyEvent(it)) {
123 copy()
124 true
125 } else {
126 false
127 }
128 }
haoyuc40d02752021-01-25 17:32:47 -0800129
haoyu7ad5ea32021-03-22 10:36:35 -0700130 private var previousPosition: Offset? = null
haoyuc40d02752021-01-25 17:32:47 -0800131 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700132 * Layout Coordinates of the selection container.
133 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100134 var containerLayoutCoordinates: LayoutCoordinates? = null
135 set(value) {
136 field = value
haoyu2c6e9842021-03-30 17:39:04 -0700137 if (hasFocus && selection != null) {
138 val positionInWindow = value?.positionInWindow()
139 if (previousPosition != positionInWindow) {
140 previousPosition = positionInWindow
141 updateHandleOffsets()
142 updateSelectionToolbarPosition()
143 }
haoyu3c3fb452021-02-18 01:01:14 -0800144 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100145 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700146
147 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700148 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
149 * recalculated.
150 */
Nader Jawad6df06122020-06-03 15:27:08 -0700151 private var dragBeginPosition = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700152
153 /**
154 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
155 * it will be zeroed out.
156 */
Nader Jawad6df06122020-06-03 15:27:08 -0700157 private var dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700158
159 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100160 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
161 * is null when handle shouldn't be displayed.
162 * It is a [State] so reading it during the composition will cause recomposition every time
163 * the position has been changed.
164 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700165 var startHandlePosition by mutableStateOf<Offset?>(
166 null,
167 policy = structuralEqualityPolicy()
168 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100169 private set
170
171 /**
172 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
173 * is null when handle shouldn't be displayed.
174 * It is a [State] so reading it during the composition will cause recomposition every time
175 * the position has been changed.
176 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700177 var endHandlePosition by mutableStateOf<Offset?>(
178 null,
179 policy = structuralEqualityPolicy()
180 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100181 private set
182
183 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700184 selectionRegistrar.onPositionChangeCallback = { selectableId ->
185 if (
186 selectableId == selection?.start?.selectableId ||
187 selectableId == selection?.end?.selectableId
188 ) {
189 updateHandleOffsets()
190 updateSelectionToolbarPosition()
191 }
haoyue6d80a12020-12-02 16:04:52 -0800192 }
193
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100194 selectionRegistrar.onSelectionUpdateStartCallback =
195 { layoutCoordinates, startPosition, selectionMode ->
196 val startPositionInContainer = convertToContainerCoordinates(
197 layoutCoordinates,
198 startPosition
199 )
200
201 updateSelection(
202 startPosition = startPositionInContainer,
203 endPosition = startPositionInContainer,
204 isStartHandle = true,
205 adjustment = selectionMode
206 )
207
208 focusRequester.requestFocus()
209 hideSelectionToolbar()
210 }
haoyue6d80a12020-12-02 16:04:52 -0800211
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700212 selectionRegistrar.onSelectionUpdateSelectAll =
213 { selectableId ->
214 val (newSelection, newSubselection) = mergeSelections(
215 selectableId = selectableId,
216 previousSelection = selection,
217 )
218 if (newSelection != selection) {
219 selectionRegistrar.subselections = newSubselection
220 onSelectionChange(newSelection)
221 }
222
223 focusRequester.requestFocus()
224 hideSelectionToolbar()
225 }
226
haoyue6d80a12020-12-02 16:04:52 -0800227 selectionRegistrar.onSelectionUpdateCallback =
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100228 { layoutCoordinates, startPosition, endPosition, selectionMode ->
229 val startPositionOrCurrent = if (startPosition == null) {
230 currentSelectionStartPosition()
231 } else {
232 convertToContainerCoordinates(layoutCoordinates, startPosition)
233 }
234
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700235 updateSelection(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100236 startPosition = startPositionOrCurrent,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700237 endPosition = convertToContainerCoordinates(layoutCoordinates, endPosition),
haoyu70aed732020-12-15 07:10:56 -0800238 isStartHandle = false,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700239 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700240 )
241 }
haoyue6d80a12020-12-02 16:04:52 -0800242
243 selectionRegistrar.onSelectionUpdateEndCallback = {
244 showSelectionToolbar()
245 }
haoyu9085c882020-12-08 12:01:06 -0800246
haoyue04245e2021-03-08 14:52:56 -0800247 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
248 if (selectableKey in selectionRegistrar.subselections) {
haoyu9085c882020-12-08 12:01:06 -0800249 // clear the selection range of each Selectable.
250 onRelease()
251 selection = null
252 }
253 }
haoyue04245e2021-03-08 14:52:56 -0800254
255 selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
256 if (
257 selectableKey == selection?.start?.selectableId ||
258 selectableKey == selection?.end?.selectableId
259 ) {
260 // The selectable that contains a selection handle just unsubscribed.
261 // Hide selection handles for now
262 startHandlePosition = null
263 endHandlePosition = null
264 }
265 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100266 }
267
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100268 private fun currentSelectionStartPosition(): Offset? {
269 return selection?.let { selection ->
270 val startSelectable =
271 selectionRegistrar.selectableMap[selection.start.selectableId]
272
273 requireContainerCoordinates().localPositionOf(
274 startSelectable?.getLayoutCoordinates()!!,
275 getAdjustedCoordinates(
276 startSelectable.getHandlePosition(
277 selection = selection,
278 isStartHandle = true
279 )
280 )
281 )
282 }
283 }
284
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100285 private fun updateHandleOffsets() {
286 val selection = selection
287 val containerCoordinates = containerLayoutCoordinates
haoyue04245e2021-03-08 14:52:56 -0800288 val startSelectable = selection?.start?.selectableId?.let {
289 selectionRegistrar.selectableMap[it]
290 }
291 val endSelectable = selection?.end?.selectableId?.let {
292 selectionRegistrar.selectableMap[it]
293 }
294 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
295 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
haoyue2678c62020-12-09 08:39:12 -0800296 if (
297 selection == null ||
298 containerCoordinates == null ||
299 !containerCoordinates.isAttached ||
300 startLayoutCoordinates == null ||
301 endLayoutCoordinates == null
302 ) {
303 this.startHandlePosition = null
304 this.endHandlePosition = null
305 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100306 }
haoyue2678c62020-12-09 08:39:12 -0800307
George Mount77ca2a22020-12-11 17:46:19 +0000308 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800309 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800310 startSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800311 selection = selection,
312 isStartHandle = true
313 )
314 )
George Mount77ca2a22020-12-11 17:46:19 +0000315 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800316 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800317 endSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800318 selection = selection,
319 isStartHandle = false
320 )
321 )
322
323 val visibleBounds = containerCoordinates.visibleBounds()
324 this.startHandlePosition =
325 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
326 this.endHandlePosition =
327 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100328 }
329
330 /**
331 * Returns non-nullable [containerLayoutCoordinates].
332 */
333 internal fun requireContainerCoordinates(): LayoutCoordinates {
334 val coordinates = containerLayoutCoordinates
335 require(coordinates != null)
336 require(coordinates.isAttached)
337 return coordinates
338 }
339
340 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700341 * Iterates over the handlers, gets the selection for each Composable, and merges all the
342 * returned [Selection]s.
343 *
Nader Jawad6df06122020-06-03 15:27:08 -0700344 * @param startPosition [Offset] for the start of the selection
345 * @param endPosition [Offset] for the end of the selection
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800346 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700347 *
haoyue04245e2021-03-08 14:52:56 -0800348 * @return a [Pair] of a [Selection] object which is constructed by combining all
349 * composables that are selected and a [Map] from selectable key to [Selection]s on the
350 * [Selectable] corresponding to the that key.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700351 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800352 // This function is internal for testing purposes.
353 internal fun mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700354 startPosition: Offset,
355 endPosition: Offset,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100356 adjustment: SelectionAdjustment = SelectionAdjustment.NONE,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800357 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800358 isStartHandle: Boolean = true
haoyue04245e2021-03-08 14:52:56 -0800359 ): Pair<Selection?, Map<Long, Selection>> {
360 val subselections = mutableMapOf<Long, Selection>()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100361 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
haoyue04245e2021-03-08 14:52:56 -0800362 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
363 val selection = selectable.getSelection(
364 startPosition = startPosition,
365 endPosition = endPosition,
366 containerLayoutCoordinates = requireContainerCoordinates(),
haoyue04245e2021-03-08 14:52:56 -0800367 previousSelection = previousSelection,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100368 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700369 adjustment = adjustment
Siyamed Sinir700df8452019-10-22 20:23:58 -0700370 )
haoyue04245e2021-03-08 14:52:56 -0800371 selection?.let { subselections[selectable.selectableId] = it }
372 merge(mergedSelection, selection)
Qingqing Deng247f2b42019-12-12 19:48:37 -0800373 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800374 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
375 HapticFeedbackType.TextHandleMove
376 )
haoyue04245e2021-03-08 14:52:56 -0800377 return Pair(newSelection, subselections)
Siyamed Sinir472c3162019-10-21 23:41:00 -0700378 }
379
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700380 internal fun mergeSelections(
381 previousSelection: Selection? = null,
382 selectableId: Long
383 ): Pair<Selection?, Map<Long, Selection>> {
384 val subselections = mutableMapOf<Long, Selection>()
385 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
386 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
387 val selection = if (selectable.selectableId == selectableId)
388 selectable.getSelectAllSelection() else null
389 selection?.let { subselections[selectable.selectableId] = it }
390 merge(mergedSelection, selection)
391 }
392 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
393 HapticFeedbackType.TextHandleMove
394 )
395 return Pair(newSelection, subselections)
396 }
397
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800398 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100399 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800400 var selectedText: AnnotatedString? = null
401
402 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000403 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800404 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800405 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800406 if (selectable.selectableId != it.start.selectableId &&
407 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800408 selectedText == null
409 ) continue
410
411 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800412 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800413 selection = it
414 )
415 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
416
417 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800418 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
419 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800420 ) break
421 }
422 }
423 return selectedText
424 }
425
Qingqing Dengde023cc2020-04-24 14:23:41 -0700426 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700427 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700428 selectedText?.let { clipboardManager?.setText(it) }
429 }
430
431 /**
432 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
433 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
434 * the copy method as a callback when "copy" is clicked.
435 */
436 internal fun showSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800437 if (hasFocus) {
438 selection?.let {
439 textToolbar?.showMenu(
440 getContentRect(),
441 onCopyRequested = {
442 copy()
443 onRelease()
444 }
445 )
446 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700447 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700448 }
449
haoyue6d80a12020-12-02 16:04:52 -0800450 internal fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800451 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800452 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700453 }
454 }
455
456 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800457 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700458 showSelectionToolbar()
459 }
460 }
461
462 /**
463 * Calculate selected region as [Rect]. The top is the top of the first selected
464 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
465 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
466 */
467 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700468 val selection = selection ?: return Rect.Zero
haoyue04245e2021-03-08 14:52:56 -0800469 val startSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
470 val endSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
471 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
472 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700473
474 val localLayoutCoordinates = containerLayoutCoordinates
475 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000476 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700477 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800478 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700479 selection = selection,
480 isStartHandle = true
481 )
482 )
George Mount77ca2a22020-12-11 17:46:19 +0000483 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700484 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800485 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700486 selection = selection,
487 isStartHandle = false
488 )
489 )
490
491 startOffset = localLayoutCoordinates.localToRoot(startOffset)
492 endOffset = localLayoutCoordinates.localToRoot(endOffset)
493
494 val left = min(startOffset.x, endOffset.x)
495 val right = max(startOffset.x, endOffset.x)
496
George Mount77ca2a22020-12-11 17:46:19 +0000497 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700498 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700499 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700500 0f,
haoyue04245e2021-03-08 14:52:56 -0800501 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700502 )
503 )
504
George Mount77ca2a22020-12-11 17:46:19 +0000505 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700506 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700507 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700508 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800509 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700510 )
511 )
512
513 startTop = localLayoutCoordinates.localToRoot(startTop)
514 endTop = localLayoutCoordinates.localToRoot(endTop)
515
516 val top = min(startTop.y, endTop.y)
Nader Jawad16e330a2020-05-21 21:21:01 -0700517 val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700518
519 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700520 left,
521 top,
522 right,
523 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700524 )
525 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700526 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700527 }
528
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800529 // This is for PressGestureDetector to cancel the selection.
530 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800531 selectionRegistrar.subselections = emptyMap()
haoyue6d80a12020-12-02 16:04:52 -0800532 hideSelectionToolbar()
haoyue04245e2021-03-08 14:52:56 -0800533 if (selection != null) {
534 onSelectionChange(null)
535 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
536 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800537 }
538
Matvei Malkovf770a912021-03-24 18:12:41 +0000539 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver {
540 return object : TextDragObserver {
541 override fun onStart(startPoint: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800542 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700543 val selection = selection!!
haoyue04245e2021-03-08 14:52:56 -0800544 val startSelectable =
545 selectionRegistrar.selectableMap[selection.start.selectableId]
546 val endSelectable =
547 selectionRegistrar.selectableMap[selection.end.selectableId]
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100548 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700549 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100550 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700551 val beginLayoutCoordinates = if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800552 startSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700553 } else {
haoyue04245e2021-03-08 14:52:56 -0800554 endSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700555 }
556
Qingqing Deng6f56a912019-05-13 10:10:37 -0700557 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100558 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700559 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800560 if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800561 startSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800562 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800563 )
564 } else {
haoyue04245e2021-03-08 14:52:56 -0800565 endSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800566 selection = selection, isStartHandle = false
567 )
haoyue6d80a12020-12-02 16:04:52 -0800568 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700569 )
570
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100571 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700572 // selection container coordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000573 dragBeginPosition = requireContainerCoordinates().localPositionOf(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700574 beginLayoutCoordinates,
575 beginCoordinates
576 )
577
578 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700579 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700580 }
581
Matvei Malkovf770a912021-03-24 18:12:41 +0000582 override fun onDrag(delta: Offset) {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700583 val selection = selection!!
Matvei Malkovf770a912021-03-24 18:12:41 +0000584 dragTotalDistance += delta
haoyue04245e2021-03-08 14:52:56 -0800585 val startSelectable =
586 selectionRegistrar.selectableMap[selection.start.selectableId]
587 val endSelectable =
588 selectionRegistrar.selectableMap[selection.end.selectableId]
Siyamed Sinir472c3162019-10-21 23:41:00 -0700589 val currentStart = if (isStartHandle) {
590 dragBeginPosition + dragTotalDistance
591 } else {
George Mount77ca2a22020-12-11 17:46:19 +0000592 requireContainerCoordinates().localPositionOf(
haoyue04245e2021-03-08 14:52:56 -0800593 startSelectable?.getLayoutCoordinates()!!,
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800594 getAdjustedCoordinates(
haoyue04245e2021-03-08 14:52:56 -0800595 startSelectable.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800596 selection = selection,
597 isStartHandle = true
598 )
599 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700600 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700601 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700602
603 val currentEnd = if (isStartHandle) {
George Mount77ca2a22020-12-11 17:46:19 +0000604 requireContainerCoordinates().localPositionOf(
haoyue04245e2021-03-08 14:52:56 -0800605 endSelectable?.getLayoutCoordinates()!!,
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800606 getAdjustedCoordinates(
haoyue04245e2021-03-08 14:52:56 -0800607 endSelectable.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800608 selection = selection,
609 isStartHandle = false
610 )
611 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700612 )
613 } else {
614 dragBeginPosition + dragTotalDistance
615 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700616 updateSelection(
Qingqing Denga5d80952019-10-11 16:46:52 -0700617 startPosition = currentStart,
618 endPosition = currentEnd,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800619 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700620 )
Matvei Malkovf770a912021-03-24 18:12:41 +0000621 return
Qingqing Deng6f56a912019-05-13 10:10:37 -0700622 }
haoyue6d80a12020-12-02 16:04:52 -0800623
Matvei Malkovf770a912021-03-24 18:12:41 +0000624 override fun onStop() {
haoyue6d80a12020-12-02 16:04:52 -0800625 showSelectionToolbar()
626 }
627
628 override fun onCancel() {
629 showSelectionToolbar()
630 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700631 }
632 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700633
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700634 /**
635 * Detect tap without consuming the up event.
636 */
637 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
638 forEachGesture {
639 coroutineScope {
640 awaitPointerEventScope {
641 waitForUpOrCancellation()?.let {
642 onTap(it.position)
643 }
644 }
645 }
646 }
647 }
648
649 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
650 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
651 }
652
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700653 private fun convertToContainerCoordinates(
654 layoutCoordinates: LayoutCoordinates,
655 offset: Offset
656 ): Offset? {
657 val coordinates = containerLayoutCoordinates
658 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000659 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700660 }
661
662 private fun updateSelection(
663 startPosition: Offset?,
664 endPosition: Offset?,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100665 adjustment: SelectionAdjustment = SelectionAdjustment.NONE,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700666 isStartHandle: Boolean = true
667 ) {
668 if (startPosition == null || endPosition == null) return
haoyue04245e2021-03-08 14:52:56 -0800669 val (newSelection, newSubselection) = mergeSelections(
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700670 startPosition = startPosition,
671 endPosition = endPosition,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100672 adjustment = adjustment,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700673 isStartHandle = isStartHandle,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100674 previousSelection = selection,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700675 )
haoyue04245e2021-03-08 14:52:56 -0800676 if (newSelection != selection) {
677 selectionRegistrar.subselections = newSubselection
678 onSelectionChange(newSelection)
679 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700680 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700681}
682
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200683internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700684 return lhs?.merge(rhs) ?: rhs
685}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800686
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100687internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
688
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200689internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800690 selectable: Selectable,
691 selection: Selection
692): AnnotatedString {
693 val currentText = selectable.getText()
694
695 return if (
haoyue04245e2021-03-08 14:52:56 -0800696 selectable.selectableId != selection.start.selectableId &&
697 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800698 ) {
699 // Select the full text content if the current selectable is between the
700 // start and the end selectables.
701 currentText
702 } else if (
haoyue04245e2021-03-08 14:52:56 -0800703 selectable.selectableId == selection.start.selectableId &&
704 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800705 ) {
706 // Select partial text content if the current selectable is the start and
707 // the end selectable.
708 if (selection.handlesCrossed) {
709 currentText.subSequence(selection.end.offset, selection.start.offset)
710 } else {
711 currentText.subSequence(selection.start.offset, selection.end.offset)
712 }
haoyue04245e2021-03-08 14:52:56 -0800713 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800714 // Select partial text content if the current selectable is the start
715 // selectable.
716 if (selection.handlesCrossed) {
717 currentText.subSequence(0, selection.start.offset)
718 } else {
719 currentText.subSequence(selection.start.offset, currentText.length)
720 }
721 } else {
722 // Selectable partial text content if the current selectable is the end
723 // selectable.
724 if (selection.handlesCrossed) {
725 currentText.subSequence(selection.end.offset, currentText.length)
726 } else {
727 currentText.subSequence(0, selection.end.offset)
728 }
729 }
730}
haoyue2678c62020-12-09 08:39:12 -0800731
732/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800733internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800734 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
735 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
736 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000737 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800738 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000739 windowToLocal(boundsInWindow.topLeft),
740 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800741 )
742}
743
haoyuac341f02021-01-22 22:01:56 -0800744internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800745 offset.x in left..right && offset.y in top..bottom