[go: nahoru, domu]

blob: 353a734a6f34425a35535b42a593edf639839c4f [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
Zach Klippensteinadabe342021-11-11 16:38:13 -080025import androidx.compose.foundation.text.Handle
26import androidx.compose.foundation.text.TextDragObserver
27import androidx.compose.foundation.text.selection.Selection.AnchorInfo
28import androidx.compose.runtime.MutableState
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010029import androidx.compose.runtime.State
30import androidx.compose.runtime.getValue
31import androidx.compose.runtime.mutableStateOf
32import androidx.compose.runtime.setValue
haoyuc40d02752021-01-25 17:32:47 -080033import androidx.compose.ui.Modifier
34import androidx.compose.ui.focus.FocusRequester
35import androidx.compose.ui.focus.focusRequester
haoyuc40d02752021-01-25 17:32:47 -080036import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010037import androidx.compose.ui.geometry.Offset
38import androidx.compose.ui.geometry.Rect
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010039import androidx.compose.ui.hapticfeedback.HapticFeedback
40import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010041import androidx.compose.ui.input.key.KeyEvent
42import androidx.compose.ui.input.key.onKeyEvent
Ralston Da Silvade62bc62021-06-02 17:46:44 -070043import androidx.compose.ui.input.pointer.PointerInputScope
44import androidx.compose.ui.input.pointer.pointerInput
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010045import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000046import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080047import androidx.compose.ui.layout.onGloballyPositioned
haoyu7ad5ea32021-03-22 10:36:35 -070048import androidx.compose.ui.layout.positionInWindow
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010049import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010050import androidx.compose.ui.platform.TextToolbar
51import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010052import androidx.compose.ui.text.AnnotatedString
Ralston Da Silvade62bc62021-06-02 17:46:44 -070053import kotlinx.coroutines.coroutineScope
Nader Jawade6a9b332020-05-21 13:49:20 -070054import kotlin.math.max
55import kotlin.math.min
Qingqing Deng6f56a912019-05-13 10:10:37 -070056
Qingqing Deng35f97ea2019-09-18 19:24:37 -070057/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010058 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070059 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070060internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080061
62 private val _selection: MutableState<Selection?> = mutableStateOf(null)
63
Qingqing Deng6f56a912019-05-13 10:10:37 -070064 /**
65 * The current selection.
66 */
Zach Klippensteinadabe342021-11-11 16:38:13 -080067 var selection: Selection?
68 get() = _selection.value
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010069 set(value) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080070 _selection.value = value
haoyu9085c882020-12-08 12:01:06 -080071 if (value != null) {
72 updateHandleOffsets()
73 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010074 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070075
76 /**
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010077 * Is touch mode active
78 */
79 var touchMode: Boolean = true
80
81 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070082 * The manager will invoke this every time it comes to the conclusion that the selection should
83 * change. The expectation is that this callback will end up causing `setSelection` to get
84 * called. This is what makes this a "controlled component".
85 */
86 var onSelectionChange: (Selection?) -> Unit = {}
87
88 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080089 * [HapticFeedback] handle to perform haptic feedback.
90 */
91 var hapticFeedBack: HapticFeedback? = null
92
93 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070094 * [ClipboardManager] to perform clipboard features.
95 */
96 var clipboardManager: ClipboardManager? = null
97
98 /**
Qingqing Denga89450d2020-04-03 19:11:49 -070099 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
100 */
101 var textToolbar: TextToolbar? = null
102
103 /**
haoyuc40d02752021-01-25 17:32:47 -0800104 * Focus requester used to request focus when selection becomes active.
105 */
106 var focusRequester: FocusRequester = FocusRequester()
107
108 /**
haoyu3c3fb452021-02-18 01:01:14 -0800109 * Return true if the corresponding SelectionContainer is focused.
haoyuc40d02752021-01-25 17:32:47 -0800110 */
haoyu3c3fb452021-02-18 01:01:14 -0800111 var hasFocus: Boolean by mutableStateOf(false)
haoyuc40d02752021-01-25 17:32:47 -0800112
113 /**
114 * Modifier for selection container.
115 */
116 val modifier get() = Modifier
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700117 .onClearSelectionRequested { onRelease() }
haoyuc40d02752021-01-25 17:32:47 -0800118 .onGloballyPositioned { containerLayoutCoordinates = it }
119 .focusRequester(focusRequester)
120 .onFocusChanged { focusState ->
haoyu3c3fb452021-02-18 01:01:14 -0800121 if (!focusState.isFocused && hasFocus) {
haoyuc40d02752021-01-25 17:32:47 -0800122 onRelease()
123 }
haoyu3c3fb452021-02-18 01:01:14 -0800124 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800125 }
haoyu3c3fb452021-02-18 01:01:14 -0800126 .focusable()
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100127 .onKeyEvent {
128 if (isCopyKeyEvent(it)) {
129 copy()
130 true
131 } else {
132 false
133 }
134 }
Zach Klippensteinadabe342021-11-11 16:38:13 -0800135 .selectionMagnifier(this)
haoyuc40d02752021-01-25 17:32:47 -0800136
haoyu7ad5ea32021-03-22 10:36:35 -0700137 private var previousPosition: Offset? = null
haoyuc40d02752021-01-25 17:32:47 -0800138 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700139 * Layout Coordinates of the selection container.
140 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100141 var containerLayoutCoordinates: LayoutCoordinates? = null
142 set(value) {
143 field = value
haoyu2c6e9842021-03-30 17:39:04 -0700144 if (hasFocus && selection != null) {
145 val positionInWindow = value?.positionInWindow()
146 if (previousPosition != positionInWindow) {
147 previousPosition = positionInWindow
148 updateHandleOffsets()
149 updateSelectionToolbarPosition()
150 }
haoyu3c3fb452021-02-18 01:01:14 -0800151 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100152 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700153
154 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700155 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
156 * recalculated.
157 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800158 internal var dragBeginPosition by mutableStateOf(Offset.Zero)
159 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700160
161 /**
162 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
163 * it will be zeroed out.
164 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800165 internal var dragTotalDistance by mutableStateOf(Offset.Zero)
166 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700167
168 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100169 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
170 * is null when handle shouldn't be displayed.
171 * It is a [State] so reading it during the composition will cause recomposition every time
172 * the position has been changed.
173 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800174 var startHandlePosition: Offset? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100175 private set
176
177 /**
178 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
179 * is null when handle shouldn't be displayed.
180 * It is a [State] so reading it during the composition will cause recomposition every time
181 * the position has been changed.
182 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800183 var endHandlePosition: Offset? by mutableStateOf(null)
184 private set
185
186 /**
187 * The handle that is currently being dragged, or null when no handle is being dragged.
188 */
189 var draggingHandle: Handle? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100190 private set
191
192 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700193 selectionRegistrar.onPositionChangeCallback = { selectableId ->
194 if (
195 selectableId == selection?.start?.selectableId ||
196 selectableId == selection?.end?.selectableId
197 ) {
198 updateHandleOffsets()
199 updateSelectionToolbarPosition()
200 }
haoyue6d80a12020-12-02 16:04:52 -0800201 }
202
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100203 selectionRegistrar.onSelectionUpdateStartCallback =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700204 { layoutCoordinates, position, selectionMode ->
205 val positionInContainer = convertToContainerCoordinates(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100206 layoutCoordinates,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700207 position
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100208 )
209
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700210 if (positionInContainer != null) {
211 startSelection(
212 position = positionInContainer,
213 isStartHandle = false,
214 adjustment = selectionMode
215 )
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100216
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700217 focusRequester.requestFocus()
218 hideSelectionToolbar()
219 }
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100220 }
haoyue6d80a12020-12-02 16:04:52 -0800221
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700222 selectionRegistrar.onSelectionUpdateSelectAll =
223 { selectableId ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700224 val (newSelection, newSubselection) = selectAll(
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700225 selectableId = selectableId,
226 previousSelection = selection,
227 )
228 if (newSelection != selection) {
229 selectionRegistrar.subselections = newSubselection
230 onSelectionChange(newSelection)
231 }
232
233 focusRequester.requestFocus()
234 hideSelectionToolbar()
235 }
236
haoyue6d80a12020-12-02 16:04:52 -0800237 selectionRegistrar.onSelectionUpdateCallback =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700238 { layoutCoordinates, newPosition, previousPosition, isStartHandle, selectionMode ->
239 val newPositionInContainer =
240 convertToContainerCoordinates(layoutCoordinates, newPosition)
241 val previousPositionInContainer =
242 convertToContainerCoordinates(layoutCoordinates, previousPosition)
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100243
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700244 updateSelection(
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700245 newPosition = newPositionInContainer,
246 previousPosition = previousPositionInContainer,
247 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700248 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700249 )
250 }
haoyue6d80a12020-12-02 16:04:52 -0800251
252 selectionRegistrar.onSelectionUpdateEndCallback = {
253 showSelectionToolbar()
Zach Klippensteinadabe342021-11-11 16:38:13 -0800254 // This property is set by updateSelection while dragging, so we need to clear it after
255 // the original selection drag.
256 draggingHandle = null
haoyue6d80a12020-12-02 16:04:52 -0800257 }
haoyu9085c882020-12-08 12:01:06 -0800258
haoyue04245e2021-03-08 14:52:56 -0800259 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
260 if (selectableKey in selectionRegistrar.subselections) {
haoyu9085c882020-12-08 12:01:06 -0800261 // clear the selection range of each Selectable.
262 onRelease()
263 selection = null
264 }
265 }
haoyue04245e2021-03-08 14:52:56 -0800266
267 selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
268 if (
269 selectableKey == selection?.start?.selectableId ||
270 selectableKey == selection?.end?.selectableId
271 ) {
272 // The selectable that contains a selection handle just unsubscribed.
273 // Hide selection handles for now
274 startHandlePosition = null
275 endHandlePosition = null
276 }
277 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100278 }
279
Zach Klippensteinadabe342021-11-11 16:38:13 -0800280 /**
281 * Returns the [Selectable] responsible for managing the given [AnchorInfo], or null if the
282 * anchor is not from a currently-registered [Selectable].
283 */
284 internal fun getAnchorSelectable(anchor: AnchorInfo): Selectable? {
285 return selectionRegistrar.selectableMap[anchor.selectableId]
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100286 }
287
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100288 private fun updateHandleOffsets() {
289 val selection = selection
290 val containerCoordinates = containerLayoutCoordinates
Zach Klippensteinadabe342021-11-11 16:38:13 -0800291 val startSelectable = selection?.start?.let(::getAnchorSelectable)
292 val endSelectable = selection?.end?.let(::getAnchorSelectable)
haoyue04245e2021-03-08 14:52:56 -0800293 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
294 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
haoyue2678c62020-12-09 08:39:12 -0800295 if (
296 selection == null ||
297 containerCoordinates == null ||
298 !containerCoordinates.isAttached ||
299 startLayoutCoordinates == null ||
300 endLayoutCoordinates == null
301 ) {
302 this.startHandlePosition = null
303 this.endHandlePosition = null
304 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100305 }
haoyue2678c62020-12-09 08:39:12 -0800306
George Mount77ca2a22020-12-11 17:46:19 +0000307 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800308 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800309 startSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800310 selection = selection,
311 isStartHandle = true
312 )
313 )
George Mount77ca2a22020-12-11 17:46:19 +0000314 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800315 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800316 endSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800317 selection = selection,
318 isStartHandle = false
319 )
320 )
321
322 val visibleBounds = containerCoordinates.visibleBounds()
323 this.startHandlePosition =
324 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
325 this.endHandlePosition =
326 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100327 }
328
329 /**
330 * Returns non-nullable [containerLayoutCoordinates].
331 */
332 internal fun requireContainerCoordinates(): LayoutCoordinates {
333 val coordinates = containerLayoutCoordinates
334 require(coordinates != null)
335 require(coordinates.isAttached)
336 return coordinates
337 }
338
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700339 internal fun selectAll(
340 selectableId: Long,
341 previousSelection: Selection?
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700342 ): Pair<Selection?, Map<Long, Selection>> {
343 val subselections = mutableMapOf<Long, Selection>()
344 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
345 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
346 val selection = if (selectable.selectableId == selectableId)
347 selectable.getSelectAllSelection() else null
348 selection?.let { subselections[selectable.selectableId] = it }
349 merge(mergedSelection, selection)
350 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700351 if (newSelection != previousSelection) {
352 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
353 }
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700354 return Pair(newSelection, subselections)
355 }
356
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800357 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100358 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800359 var selectedText: AnnotatedString? = null
360
361 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000362 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800363 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800364 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800365 if (selectable.selectableId != it.start.selectableId &&
366 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800367 selectedText == null
368 ) continue
369
370 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800371 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800372 selection = it
373 )
374 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
375
376 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800377 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
378 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800379 ) break
380 }
381 }
382 return selectedText
383 }
384
Qingqing Dengde023cc2020-04-24 14:23:41 -0700385 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700386 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700387 selectedText?.let { clipboardManager?.setText(it) }
388 }
389
390 /**
391 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
392 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
393 * the copy method as a callback when "copy" is clicked.
394 */
395 internal fun showSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800396 if (hasFocus) {
397 selection?.let {
398 textToolbar?.showMenu(
399 getContentRect(),
400 onCopyRequested = {
401 copy()
402 onRelease()
403 }
404 )
405 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700406 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700407 }
408
haoyue6d80a12020-12-02 16:04:52 -0800409 internal fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800410 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800411 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700412 }
413 }
414
415 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800416 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700417 showSelectionToolbar()
418 }
419 }
420
421 /**
422 * Calculate selected region as [Rect]. The top is the top of the first selected
423 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
424 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
425 */
426 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700427 val selection = selection ?: return Rect.Zero
Zach Klippensteinadabe342021-11-11 16:38:13 -0800428 val startSelectable = getAnchorSelectable(selection.start)
429 val endSelectable = getAnchorSelectable(selection.end)
haoyue04245e2021-03-08 14:52:56 -0800430 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
431 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700432
433 val localLayoutCoordinates = containerLayoutCoordinates
434 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000435 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700436 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800437 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700438 selection = selection,
439 isStartHandle = true
440 )
441 )
George Mount77ca2a22020-12-11 17:46:19 +0000442 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700443 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800444 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700445 selection = selection,
446 isStartHandle = false
447 )
448 )
449
450 startOffset = localLayoutCoordinates.localToRoot(startOffset)
451 endOffset = localLayoutCoordinates.localToRoot(endOffset)
452
453 val left = min(startOffset.x, endOffset.x)
454 val right = max(startOffset.x, endOffset.x)
455
George Mount77ca2a22020-12-11 17:46:19 +0000456 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700457 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700458 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700459 0f,
haoyue04245e2021-03-08 14:52:56 -0800460 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700461 )
462 )
463
George Mount77ca2a22020-12-11 17:46:19 +0000464 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700465 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700466 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700467 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800468 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700469 )
470 )
471
472 startTop = localLayoutCoordinates.localToRoot(startTop)
473 endTop = localLayoutCoordinates.localToRoot(endTop)
474
475 val top = min(startTop.y, endTop.y)
Haoyu Zhangd47f1fe2021-07-13 03:10:55 -0700476 val bottom = max(startOffset.y, endOffset.y) + (HandleHeight.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700477
478 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700479 left,
480 top,
481 right,
482 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700483 )
484 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700485 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700486 }
487
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800488 // This is for PressGestureDetector to cancel the selection.
489 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800490 selectionRegistrar.subselections = emptyMap()
haoyue6d80a12020-12-02 16:04:52 -0800491 hideSelectionToolbar()
haoyue04245e2021-03-08 14:52:56 -0800492 if (selection != null) {
493 onSelectionChange(null)
494 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
495 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800496 }
497
Matvei Malkovf770a912021-03-24 18:12:41 +0000498 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver {
499 return object : TextDragObserver {
500 override fun onStart(startPoint: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800501 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700502 val selection = selection!!
haoyue04245e2021-03-08 14:52:56 -0800503 val startSelectable =
504 selectionRegistrar.selectableMap[selection.start.selectableId]
505 val endSelectable =
506 selectionRegistrar.selectableMap[selection.end.selectableId]
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100507 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700508 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100509 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700510 val beginLayoutCoordinates = if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800511 startSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700512 } else {
haoyue04245e2021-03-08 14:52:56 -0800513 endSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700514 }
515
Qingqing Deng6f56a912019-05-13 10:10:37 -0700516 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100517 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700518 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800519 if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800520 startSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800521 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800522 )
523 } else {
haoyue04245e2021-03-08 14:52:56 -0800524 endSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800525 selection = selection, isStartHandle = false
526 )
haoyue6d80a12020-12-02 16:04:52 -0800527 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700528 )
529
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100530 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700531 // selection container coordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000532 dragBeginPosition = requireContainerCoordinates().localPositionOf(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700533 beginLayoutCoordinates,
534 beginCoordinates
535 )
536
537 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700538 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700539 }
540
Matvei Malkovf770a912021-03-24 18:12:41 +0000541 override fun onDrag(delta: Offset) {
Matvei Malkovf770a912021-03-24 18:12:41 +0000542 dragTotalDistance += delta
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700543 val endPosition = dragBeginPosition + dragTotalDistance
544 val consumed = updateSelection(
545 newPosition = endPosition,
546 previousPosition = dragBeginPosition,
Haoyu Zhang5740343b2021-07-15 11:29:27 -0700547 isStartHandle = isStartHandle,
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700548 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
Qingqing Denga5d80952019-10-11 16:46:52 -0700549 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700550 if (consumed) {
551 dragBeginPosition = endPosition
552 dragTotalDistance = Offset.Zero
553 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700554 }
haoyue6d80a12020-12-02 16:04:52 -0800555
Matvei Malkovf770a912021-03-24 18:12:41 +0000556 override fun onStop() {
haoyue6d80a12020-12-02 16:04:52 -0800557 showSelectionToolbar()
Zach Klippensteinadabe342021-11-11 16:38:13 -0800558 draggingHandle = null
haoyue6d80a12020-12-02 16:04:52 -0800559 }
560
561 override fun onCancel() {
562 showSelectionToolbar()
Zach Klippensteinadabe342021-11-11 16:38:13 -0800563 draggingHandle = null
haoyue6d80a12020-12-02 16:04:52 -0800564 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700565 }
566 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700567
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700568 /**
569 * Detect tap without consuming the up event.
570 */
571 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
572 forEachGesture {
573 coroutineScope {
574 awaitPointerEventScope {
575 waitForUpOrCancellation()?.let {
576 onTap(it.position)
577 }
578 }
579 }
580 }
581 }
582
583 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
584 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
585 }
586
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700587 private fun convertToContainerCoordinates(
588 layoutCoordinates: LayoutCoordinates,
589 offset: Offset
590 ): Offset? {
591 val coordinates = containerLayoutCoordinates
592 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000593 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700594 }
595
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700596 /**
597 * Cancel the previous selection and start a new selection at the given [position].
598 * It's used for long-press, double-click, triple-click and so on to start selection.
599 *
600 * @param position initial position of the selection. Both start and end handle is considered
601 * at this position.
602 * @param isStartHandle whether it's considered as the start handle moving. This parameter
603 * will influence the [SelectionAdjustment]'s behavior. For example,
604 * [SelectionAdjustment.Character] only adjust the moving handle.
605 * @param adjustment the selection adjustment.
606 */
607 private fun startSelection(
608 position: Offset,
609 isStartHandle: Boolean,
610 adjustment: SelectionAdjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700611 ) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700612 updateSelection(
613 startHandlePosition = position,
614 endHandlePosition = position,
615 previousHandlePosition = null,
616 isStartHandle = isStartHandle,
617 adjustment = adjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700618 )
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700619 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700620
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700621 /**
622 * Updates the selection after one of the selection handle moved.
623 *
624 * @param newPosition the new position of the moving selection handle.
625 * @param previousPosition the previous position of the moving selection handle.
626 * @param isStartHandle whether the moving selection handle is the start handle.
627 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
628 * produce the final selection range.
629 *
630 * @return a boolean representing whether the movement is consumed.
631 *
632 * @see SelectionAdjustment
633 */
634 internal fun updateSelection(
635 newPosition: Offset?,
636 previousPosition: Offset?,
637 isStartHandle: Boolean,
638 adjustment: SelectionAdjustment,
639 ): Boolean {
640 if (newPosition == null) return false
641 val otherHandlePosition = selection?.let { selection ->
642 val otherSelectableId = if (isStartHandle) {
643 selection.end.selectableId
644 } else {
645 selection.start.selectableId
646 }
647 val otherSelectable =
648 selectionRegistrar.selectableMap[otherSelectableId] ?: return@let null
649 convertToContainerCoordinates(
650 otherSelectable.getLayoutCoordinates()!!,
651 getAdjustedCoordinates(
652 otherSelectable.getHandlePosition(selection, !isStartHandle)
653 )
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700654 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700655 } ?: return false
656
657 val startHandlePosition = if (isStartHandle) newPosition else otherHandlePosition
658 val endHandlePosition = if (isStartHandle) otherHandlePosition else newPosition
659
660 return updateSelection(
661 startHandlePosition = startHandlePosition,
662 endHandlePosition = endHandlePosition,
663 previousHandlePosition = previousPosition,
664 isStartHandle = isStartHandle,
665 adjustment = adjustment
666 )
667 }
668
669 /**
670 * Updates the selection after one of the selection handle moved.
671 *
672 * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
673 * selection handle is updated each time. The only exception is that when a new selection is
674 * started. In this case, [previousHandlePosition] is always null.
675 *
676 * @param startHandlePosition the position of the start selection handle.
677 * @param endHandlePosition the position of the end selection handle.
678 * @param previousHandlePosition the position of the moving handle before the update.
679 * @param isStartHandle whether the moving selection handle is the start handle.
680 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
681 * produce the final selection range.
682 *
683 * @return a boolean representing whether the movement is consumed. It's useful for the case
684 * where a selection handle is updating consecutively. When the return value is true, it's
685 * expected that the caller will update the [startHandlePosition] to be the given
686 * [endHandlePosition] in following calls.
687 *
688 * @see SelectionAdjustment
689 */
690 internal fun updateSelection(
691 startHandlePosition: Offset,
692 endHandlePosition: Offset,
693 previousHandlePosition: Offset?,
694 isStartHandle: Boolean,
695 adjustment: SelectionAdjustment,
696 ): Boolean {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800697 // TODO(b/206833278) This should be set as soon as down event happens, not wait for drag.
698 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700699 val newSubselections = mutableMapOf<Long, Selection>()
700 var moveConsumed = false
701 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
702 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
703 val previousSubselection =
704 selectionRegistrar.subselections[selectable.selectableId]
705 val (selection, consumed) = selectable.updateSelection(
706 startHandlePosition = startHandlePosition,
707 endHandlePosition = endHandlePosition,
708 previousHandlePosition = previousHandlePosition,
709 isStartHandle = isStartHandle,
710 containerLayoutCoordinates = requireContainerCoordinates(),
711 adjustment = adjustment,
712 previousSelection = previousSubselection,
713 )
714
715 moveConsumed = moveConsumed || consumed
716 selection?.let { newSubselections[selectable.selectableId] = it }
717 merge(mergedSelection, selection)
718 }
719 if (newSelection != selection) {
720 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
721 selectionRegistrar.subselections = newSubselections
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700722 onSelectionChange(newSelection)
723 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700724 return moveConsumed
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700725 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700726}
727
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200728internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700729 return lhs?.merge(rhs) ?: rhs
730}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800731
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100732internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
733
Zach Klippensteinadabe342021-11-11 16:38:13 -0800734internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
735
736internal fun calculateSelectionMagnifierCenterAndroid(manager: SelectionManager): Offset {
737 fun getMagnifierCenter(anchor: AnchorInfo, isStartHandle: Boolean): Offset {
738 // TODO(b/206833278) Clamp x to the end of the selectable area, hide after a threshold.
739 // TODO(b/206833278) Animate x when jumping lines.
740 // TODO(b/206833278) X should track drag exactly, not clamp to cursor positions.
741
742 // Let the selectable determine the vertical position of the magnifier, since it should be
743 // clamped to the center of text lines.
744 val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
745 // The end offset is exclusive.
746 val selectionOffset = if (isStartHandle) anchor.offset else anchor.offset - 1
747 val bounds = selectable.getBoundingBox(selectionOffset)
748 // Don't need to account for Rtl here because the bounds will already be adjusted for that.
749 // (i.e. in Rtl, left > right)
750 val localCenter = if (isStartHandle) bounds.centerLeft else bounds.centerRight
751 val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
752 return containerCoordinates.localPositionOf(
753 sourceCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified,
754 relativeToSource = localCenter
755 )
756 }
757
758 val selection = manager.selection ?: return Offset.Unspecified
759 return when (manager.draggingHandle) {
760 null -> return Offset.Unspecified
761 Handle.SelectionStart -> getMagnifierCenter(selection.start, isStartHandle = true)
762 Handle.SelectionEnd -> getMagnifierCenter(selection.end, isStartHandle = false)
763 Handle.Cursor -> error("SelectionContainer does not support cursor")
764 }
765}
766
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200767internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800768 selectable: Selectable,
769 selection: Selection
770): AnnotatedString {
771 val currentText = selectable.getText()
772
773 return if (
haoyue04245e2021-03-08 14:52:56 -0800774 selectable.selectableId != selection.start.selectableId &&
775 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800776 ) {
777 // Select the full text content if the current selectable is between the
778 // start and the end selectables.
779 currentText
780 } else if (
haoyue04245e2021-03-08 14:52:56 -0800781 selectable.selectableId == selection.start.selectableId &&
782 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800783 ) {
784 // Select partial text content if the current selectable is the start and
785 // the end selectable.
786 if (selection.handlesCrossed) {
787 currentText.subSequence(selection.end.offset, selection.start.offset)
788 } else {
789 currentText.subSequence(selection.start.offset, selection.end.offset)
790 }
haoyue04245e2021-03-08 14:52:56 -0800791 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800792 // Select partial text content if the current selectable is the start
793 // selectable.
794 if (selection.handlesCrossed) {
795 currentText.subSequence(0, selection.start.offset)
796 } else {
797 currentText.subSequence(selection.start.offset, currentText.length)
798 }
799 } else {
800 // Selectable partial text content if the current selectable is the end
801 // selectable.
802 if (selection.handlesCrossed) {
803 currentText.subSequence(selection.end.offset, currentText.length)
804 } else {
805 currentText.subSequence(0, selection.end.offset)
806 }
807 }
808}
haoyue2678c62020-12-09 08:39:12 -0800809
810/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800811internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800812 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
813 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
814 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000815 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800816 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000817 windowToLocal(boundsInWindow.topLeft),
818 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800819 )
820}
821
haoyuac341f02021-01-22 22:01:56 -0800822internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800823 offset.x in left..right && offset.y in top..bottom