[go: nahoru, domu]

blob: 57182d58d727b250c2630c1efbbb0f089b891449 [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 =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700195 { layoutCoordinates, position, selectionMode ->
196 val positionInContainer = convertToContainerCoordinates(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100197 layoutCoordinates,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700198 position
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100199 )
200
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700201 if (positionInContainer != null) {
202 startSelection(
203 position = positionInContainer,
204 isStartHandle = false,
205 adjustment = selectionMode
206 )
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100207
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700208 focusRequester.requestFocus()
209 hideSelectionToolbar()
210 }
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100211 }
haoyue6d80a12020-12-02 16:04:52 -0800212
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700213 selectionRegistrar.onSelectionUpdateSelectAll =
214 { selectableId ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700215 val (newSelection, newSubselection) = selectAll(
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700216 selectableId = selectableId,
217 previousSelection = selection,
218 )
219 if (newSelection != selection) {
220 selectionRegistrar.subselections = newSubselection
221 onSelectionChange(newSelection)
222 }
223
224 focusRequester.requestFocus()
225 hideSelectionToolbar()
226 }
227
haoyue6d80a12020-12-02 16:04:52 -0800228 selectionRegistrar.onSelectionUpdateCallback =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700229 { layoutCoordinates, newPosition, previousPosition, isStartHandle, selectionMode ->
230 val newPositionInContainer =
231 convertToContainerCoordinates(layoutCoordinates, newPosition)
232 val previousPositionInContainer =
233 convertToContainerCoordinates(layoutCoordinates, previousPosition)
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100234
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700235 updateSelection(
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700236 newPosition = newPositionInContainer,
237 previousPosition = previousPositionInContainer,
238 isStartHandle = isStartHandle,
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
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700340 internal fun selectAll(
341 selectableId: Long,
342 previousSelection: Selection?
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700343 ): Pair<Selection?, Map<Long, Selection>> {
344 val subselections = mutableMapOf<Long, Selection>()
345 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
346 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
347 val selection = if (selectable.selectableId == selectableId)
348 selectable.getSelectAllSelection() else null
349 selection?.let { subselections[selectable.selectableId] = it }
350 merge(mergedSelection, selection)
351 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700352 if (newSelection != previousSelection) {
353 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
354 }
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700355 return Pair(newSelection, subselections)
356 }
357
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800358 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100359 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800360 var selectedText: AnnotatedString? = null
361
362 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000363 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800364 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800365 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800366 if (selectable.selectableId != it.start.selectableId &&
367 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800368 selectedText == null
369 ) continue
370
371 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800372 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800373 selection = it
374 )
375 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
376
377 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800378 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
379 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800380 ) break
381 }
382 }
383 return selectedText
384 }
385
Qingqing Dengde023cc2020-04-24 14:23:41 -0700386 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700387 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700388 selectedText?.let { clipboardManager?.setText(it) }
389 }
390
391 /**
392 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
393 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
394 * the copy method as a callback when "copy" is clicked.
395 */
396 internal fun showSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800397 if (hasFocus) {
398 selection?.let {
399 textToolbar?.showMenu(
400 getContentRect(),
401 onCopyRequested = {
402 copy()
403 onRelease()
404 }
405 )
406 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700407 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700408 }
409
haoyue6d80a12020-12-02 16:04:52 -0800410 internal fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800411 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800412 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700413 }
414 }
415
416 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800417 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700418 showSelectionToolbar()
419 }
420 }
421
422 /**
423 * Calculate selected region as [Rect]. The top is the top of the first selected
424 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
425 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
426 */
427 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700428 val selection = selection ?: return Rect.Zero
haoyue04245e2021-03-08 14:52:56 -0800429 val startSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
430 val endSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
431 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
432 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700433
434 val localLayoutCoordinates = containerLayoutCoordinates
435 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000436 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700437 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800438 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700439 selection = selection,
440 isStartHandle = true
441 )
442 )
George Mount77ca2a22020-12-11 17:46:19 +0000443 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700444 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800445 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700446 selection = selection,
447 isStartHandle = false
448 )
449 )
450
451 startOffset = localLayoutCoordinates.localToRoot(startOffset)
452 endOffset = localLayoutCoordinates.localToRoot(endOffset)
453
454 val left = min(startOffset.x, endOffset.x)
455 val right = max(startOffset.x, endOffset.x)
456
George Mount77ca2a22020-12-11 17:46:19 +0000457 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700458 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700459 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700460 0f,
haoyue04245e2021-03-08 14:52:56 -0800461 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700462 )
463 )
464
George Mount77ca2a22020-12-11 17:46:19 +0000465 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700466 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700467 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700468 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800469 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700470 )
471 )
472
473 startTop = localLayoutCoordinates.localToRoot(startTop)
474 endTop = localLayoutCoordinates.localToRoot(endTop)
475
476 val top = min(startTop.y, endTop.y)
Haoyu Zhangd47f1fe2021-07-13 03:10:55 -0700477 val bottom = max(startOffset.y, endOffset.y) + (HandleHeight.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700478
479 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700480 left,
481 top,
482 right,
483 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700484 )
485 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700486 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700487 }
488
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800489 // This is for PressGestureDetector to cancel the selection.
490 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800491 selectionRegistrar.subselections = emptyMap()
haoyue6d80a12020-12-02 16:04:52 -0800492 hideSelectionToolbar()
haoyue04245e2021-03-08 14:52:56 -0800493 if (selection != null) {
494 onSelectionChange(null)
495 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
496 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800497 }
498
Matvei Malkovf770a912021-03-24 18:12:41 +0000499 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver {
500 return object : TextDragObserver {
501 override fun onStart(startPoint: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800502 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700503 val selection = selection!!
haoyue04245e2021-03-08 14:52:56 -0800504 val startSelectable =
505 selectionRegistrar.selectableMap[selection.start.selectableId]
506 val endSelectable =
507 selectionRegistrar.selectableMap[selection.end.selectableId]
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100508 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700509 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100510 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700511 val beginLayoutCoordinates = if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800512 startSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700513 } else {
haoyue04245e2021-03-08 14:52:56 -0800514 endSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700515 }
516
Qingqing Deng6f56a912019-05-13 10:10:37 -0700517 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100518 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700519 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800520 if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800521 startSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800522 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800523 )
524 } else {
haoyue04245e2021-03-08 14:52:56 -0800525 endSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800526 selection = selection, isStartHandle = false
527 )
haoyue6d80a12020-12-02 16:04:52 -0800528 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700529 )
530
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100531 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700532 // selection container coordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000533 dragBeginPosition = requireContainerCoordinates().localPositionOf(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700534 beginLayoutCoordinates,
535 beginCoordinates
536 )
537
538 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700539 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700540 }
541
Matvei Malkovf770a912021-03-24 18:12:41 +0000542 override fun onDrag(delta: Offset) {
Matvei Malkovf770a912021-03-24 18:12:41 +0000543 dragTotalDistance += delta
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700544 val endPosition = dragBeginPosition + dragTotalDistance
545 val consumed = updateSelection(
546 newPosition = endPosition,
547 previousPosition = dragBeginPosition,
Haoyu Zhang5740343b2021-07-15 11:29:27 -0700548 isStartHandle = isStartHandle,
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700549 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
Qingqing Denga5d80952019-10-11 16:46:52 -0700550 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700551 if (consumed) {
552 dragBeginPosition = endPosition
553 dragTotalDistance = Offset.Zero
554 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700555 }
haoyue6d80a12020-12-02 16:04:52 -0800556
Matvei Malkovf770a912021-03-24 18:12:41 +0000557 override fun onStop() {
haoyue6d80a12020-12-02 16:04:52 -0800558 showSelectionToolbar()
559 }
560
561 override fun onCancel() {
562 showSelectionToolbar()
563 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700564 }
565 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700566
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700567 /**
568 * Detect tap without consuming the up event.
569 */
570 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
571 forEachGesture {
572 coroutineScope {
573 awaitPointerEventScope {
574 waitForUpOrCancellation()?.let {
575 onTap(it.position)
576 }
577 }
578 }
579 }
580 }
581
582 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
583 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
584 }
585
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700586 private fun convertToContainerCoordinates(
587 layoutCoordinates: LayoutCoordinates,
588 offset: Offset
589 ): Offset? {
590 val coordinates = containerLayoutCoordinates
591 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000592 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700593 }
594
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700595 /**
596 * Cancel the previous selection and start a new selection at the given [position].
597 * It's used for long-press, double-click, triple-click and so on to start selection.
598 *
599 * @param position initial position of the selection. Both start and end handle is considered
600 * at this position.
601 * @param isStartHandle whether it's considered as the start handle moving. This parameter
602 * will influence the [SelectionAdjustment]'s behavior. For example,
603 * [SelectionAdjustment.Character] only adjust the moving handle.
604 * @param adjustment the selection adjustment.
605 */
606 private fun startSelection(
607 position: Offset,
608 isStartHandle: Boolean,
609 adjustment: SelectionAdjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700610 ) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700611 updateSelection(
612 startHandlePosition = position,
613 endHandlePosition = position,
614 previousHandlePosition = null,
615 isStartHandle = isStartHandle,
616 adjustment = adjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700617 )
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700618 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700619
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700620 /**
621 * Updates the selection after one of the selection handle moved.
622 *
623 * @param newPosition the new position of the moving selection handle.
624 * @param previousPosition the previous position of the moving selection handle.
625 * @param isStartHandle whether the moving selection handle is the start handle.
626 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
627 * produce the final selection range.
628 *
629 * @return a boolean representing whether the movement is consumed.
630 *
631 * @see SelectionAdjustment
632 */
633 internal fun updateSelection(
634 newPosition: Offset?,
635 previousPosition: Offset?,
636 isStartHandle: Boolean,
637 adjustment: SelectionAdjustment,
638 ): Boolean {
639 if (newPosition == null) return false
640 val otherHandlePosition = selection?.let { selection ->
641 val otherSelectableId = if (isStartHandle) {
642 selection.end.selectableId
643 } else {
644 selection.start.selectableId
645 }
646 val otherSelectable =
647 selectionRegistrar.selectableMap[otherSelectableId] ?: return@let null
648 convertToContainerCoordinates(
649 otherSelectable.getLayoutCoordinates()!!,
650 getAdjustedCoordinates(
651 otherSelectable.getHandlePosition(selection, !isStartHandle)
652 )
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700653 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700654 } ?: return false
655
656 val startHandlePosition = if (isStartHandle) newPosition else otherHandlePosition
657 val endHandlePosition = if (isStartHandle) otherHandlePosition else newPosition
658
659 return updateSelection(
660 startHandlePosition = startHandlePosition,
661 endHandlePosition = endHandlePosition,
662 previousHandlePosition = previousPosition,
663 isStartHandle = isStartHandle,
664 adjustment = adjustment
665 )
666 }
667
668 /**
669 * Updates the selection after one of the selection handle moved.
670 *
671 * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
672 * selection handle is updated each time. The only exception is that when a new selection is
673 * started. In this case, [previousHandlePosition] is always null.
674 *
675 * @param startHandlePosition the position of the start selection handle.
676 * @param endHandlePosition the position of the end selection handle.
677 * @param previousHandlePosition the position of the moving handle before the update.
678 * @param isStartHandle whether the moving selection handle is the start handle.
679 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
680 * produce the final selection range.
681 *
682 * @return a boolean representing whether the movement is consumed. It's useful for the case
683 * where a selection handle is updating consecutively. When the return value is true, it's
684 * expected that the caller will update the [startHandlePosition] to be the given
685 * [endHandlePosition] in following calls.
686 *
687 * @see SelectionAdjustment
688 */
689 internal fun updateSelection(
690 startHandlePosition: Offset,
691 endHandlePosition: Offset,
692 previousHandlePosition: Offset?,
693 isStartHandle: Boolean,
694 adjustment: SelectionAdjustment,
695 ): Boolean {
696 val newSubselections = mutableMapOf<Long, Selection>()
697 var moveConsumed = false
698 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
699 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
700 val previousSubselection =
701 selectionRegistrar.subselections[selectable.selectableId]
702 val (selection, consumed) = selectable.updateSelection(
703 startHandlePosition = startHandlePosition,
704 endHandlePosition = endHandlePosition,
705 previousHandlePosition = previousHandlePosition,
706 isStartHandle = isStartHandle,
707 containerLayoutCoordinates = requireContainerCoordinates(),
708 adjustment = adjustment,
709 previousSelection = previousSubselection,
710 )
711
712 moveConsumed = moveConsumed || consumed
713 selection?.let { newSubselections[selectable.selectableId] = it }
714 merge(mergedSelection, selection)
715 }
716 if (newSelection != selection) {
717 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
718 selectionRegistrar.subselections = newSubselections
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700719 onSelectionChange(newSelection)
720 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700721 return moveConsumed
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700722 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700723}
724
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200725internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700726 return lhs?.merge(rhs) ?: rhs
727}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800728
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100729internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
730
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200731internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800732 selectable: Selectable,
733 selection: Selection
734): AnnotatedString {
735 val currentText = selectable.getText()
736
737 return if (
haoyue04245e2021-03-08 14:52:56 -0800738 selectable.selectableId != selection.start.selectableId &&
739 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800740 ) {
741 // Select the full text content if the current selectable is between the
742 // start and the end selectables.
743 currentText
744 } else if (
haoyue04245e2021-03-08 14:52:56 -0800745 selectable.selectableId == selection.start.selectableId &&
746 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800747 ) {
748 // Select partial text content if the current selectable is the start and
749 // the end selectable.
750 if (selection.handlesCrossed) {
751 currentText.subSequence(selection.end.offset, selection.start.offset)
752 } else {
753 currentText.subSequence(selection.start.offset, selection.end.offset)
754 }
haoyue04245e2021-03-08 14:52:56 -0800755 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800756 // Select partial text content if the current selectable is the start
757 // selectable.
758 if (selection.handlesCrossed) {
759 currentText.subSequence(0, selection.start.offset)
760 } else {
761 currentText.subSequence(selection.start.offset, currentText.length)
762 }
763 } else {
764 // Selectable partial text content if the current selectable is the end
765 // selectable.
766 if (selection.handlesCrossed) {
767 currentText.subSequence(selection.end.offset, currentText.length)
768 } else {
769 currentText.subSequence(0, selection.end.offset)
770 }
771 }
772}
haoyue2678c62020-12-09 08:39:12 -0800773
774/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800775internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800776 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
777 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
778 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000779 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800780 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000781 windowToLocal(boundsInWindow.topLeft),
782 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800783 )
784}
785
haoyuac341f02021-01-22 22:01:56 -0800786internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800787 offset.x in left..right && offset.y in top..bottom