[go: nahoru, domu]

blob: 09c3e37a040482c337dde1825510881898746e0f [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
Nader Jawade6a9b332020-05-21 13:49:20 -070053import kotlin.math.max
54import kotlin.math.min
Zach Klippenstein72e3e512022-01-14 12:24:09 -080055import kotlinx.coroutines.coroutineScope
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 */
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800116 val modifier
117 get() = Modifier
118 .onClearSelectionRequested { onRelease() }
119 .onGloballyPositioned { containerLayoutCoordinates = it }
120 .focusRequester(focusRequester)
121 .onFocusChanged { focusState ->
122 if (!focusState.isFocused && hasFocus) {
123 onRelease()
124 }
125 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800126 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800127 .focusable()
128 .onKeyEvent {
129 if (isCopyKeyEvent(it)) {
130 copy()
131 true
132 } else {
133 false
134 }
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100135 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800136 .then(if (shouldShowMagnifier) Modifier.selectionMagnifier(this) else Modifier)
haoyuc40d02752021-01-25 17:32:47 -0800137
haoyu7ad5ea32021-03-22 10:36:35 -0700138 private var previousPosition: Offset? = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800139
haoyuc40d02752021-01-25 17:32:47 -0800140 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700141 * Layout Coordinates of the selection container.
142 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100143 var containerLayoutCoordinates: LayoutCoordinates? = null
144 set(value) {
145 field = value
haoyu2c6e9842021-03-30 17:39:04 -0700146 if (hasFocus && selection != null) {
147 val positionInWindow = value?.positionInWindow()
148 if (previousPosition != positionInWindow) {
149 previousPosition = positionInWindow
150 updateHandleOffsets()
151 updateSelectionToolbarPosition()
152 }
haoyu3c3fb452021-02-18 01:01:14 -0800153 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100154 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700155
156 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700157 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
158 * recalculated.
159 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800160 internal var dragBeginPosition by mutableStateOf(Offset.Zero)
161 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700162
163 /**
164 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
165 * it will be zeroed out.
166 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800167 internal var dragTotalDistance by mutableStateOf(Offset.Zero)
168 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700169
170 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100171 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
172 * is null when handle shouldn't be displayed.
173 * It is a [State] so reading it during the composition will cause recomposition every time
174 * the position has been changed.
175 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800176 var startHandlePosition: Offset? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100177 private set
178
179 /**
180 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
181 * is null when handle shouldn't be displayed.
182 * It is a [State] so reading it during the composition will cause recomposition every time
183 * the position has been changed.
184 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800185 var endHandlePosition: Offset? by mutableStateOf(null)
186 private set
187
188 /**
189 * The handle that is currently being dragged, or null when no handle is being dragged.
190 */
191 var draggingHandle: Handle? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100192 private set
193
Zach Klippenstein4688a462021-12-08 08:28:07 -0800194 private val shouldShowMagnifier get() = draggingHandle != null
195
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100196 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700197 selectionRegistrar.onPositionChangeCallback = { selectableId ->
198 if (
199 selectableId == selection?.start?.selectableId ||
200 selectableId == selection?.end?.selectableId
201 ) {
202 updateHandleOffsets()
203 updateSelectionToolbarPosition()
204 }
haoyue6d80a12020-12-02 16:04:52 -0800205 }
206
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100207 selectionRegistrar.onSelectionUpdateStartCallback =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700208 { layoutCoordinates, position, selectionMode ->
209 val positionInContainer = convertToContainerCoordinates(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100210 layoutCoordinates,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700211 position
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100212 )
213
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700214 if (positionInContainer != null) {
215 startSelection(
216 position = positionInContainer,
217 isStartHandle = false,
218 adjustment = selectionMode
219 )
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100220
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700221 focusRequester.requestFocus()
222 hideSelectionToolbar()
223 }
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100224 }
haoyue6d80a12020-12-02 16:04:52 -0800225
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700226 selectionRegistrar.onSelectionUpdateSelectAll =
227 { selectableId ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700228 val (newSelection, newSubselection) = selectAll(
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700229 selectableId = selectableId,
230 previousSelection = selection,
231 )
232 if (newSelection != selection) {
233 selectionRegistrar.subselections = newSubselection
234 onSelectionChange(newSelection)
235 }
236
237 focusRequester.requestFocus()
238 hideSelectionToolbar()
239 }
240
haoyue6d80a12020-12-02 16:04:52 -0800241 selectionRegistrar.onSelectionUpdateCallback =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700242 { layoutCoordinates, newPosition, previousPosition, isStartHandle, selectionMode ->
243 val newPositionInContainer =
244 convertToContainerCoordinates(layoutCoordinates, newPosition)
245 val previousPositionInContainer =
246 convertToContainerCoordinates(layoutCoordinates, previousPosition)
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100247
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700248 updateSelection(
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700249 newPosition = newPositionInContainer,
250 previousPosition = previousPositionInContainer,
251 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700252 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700253 )
254 }
haoyue6d80a12020-12-02 16:04:52 -0800255
256 selectionRegistrar.onSelectionUpdateEndCallback = {
257 showSelectionToolbar()
Zach Klippensteinadabe342021-11-11 16:38:13 -0800258 // This property is set by updateSelection while dragging, so we need to clear it after
259 // the original selection drag.
260 draggingHandle = null
haoyue6d80a12020-12-02 16:04:52 -0800261 }
haoyu9085c882020-12-08 12:01:06 -0800262
haoyue04245e2021-03-08 14:52:56 -0800263 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
264 if (selectableKey in selectionRegistrar.subselections) {
haoyu9085c882020-12-08 12:01:06 -0800265 // clear the selection range of each Selectable.
266 onRelease()
267 selection = null
268 }
269 }
haoyue04245e2021-03-08 14:52:56 -0800270
271 selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
272 if (
273 selectableKey == selection?.start?.selectableId ||
274 selectableKey == selection?.end?.selectableId
275 ) {
276 // The selectable that contains a selection handle just unsubscribed.
277 // Hide selection handles for now
278 startHandlePosition = null
279 endHandlePosition = null
280 }
281 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100282 }
283
Zach Klippensteinadabe342021-11-11 16:38:13 -0800284 /**
285 * Returns the [Selectable] responsible for managing the given [AnchorInfo], or null if the
286 * anchor is not from a currently-registered [Selectable].
287 */
288 internal fun getAnchorSelectable(anchor: AnchorInfo): Selectable? {
289 return selectionRegistrar.selectableMap[anchor.selectableId]
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100290 }
291
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100292 private fun updateHandleOffsets() {
293 val selection = selection
294 val containerCoordinates = containerLayoutCoordinates
Zach Klippensteinadabe342021-11-11 16:38:13 -0800295 val startSelectable = selection?.start?.let(::getAnchorSelectable)
296 val endSelectable = selection?.end?.let(::getAnchorSelectable)
haoyue04245e2021-03-08 14:52:56 -0800297 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
298 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
haoyue2678c62020-12-09 08:39:12 -0800299 if (
300 selection == null ||
301 containerCoordinates == null ||
302 !containerCoordinates.isAttached ||
303 startLayoutCoordinates == null ||
304 endLayoutCoordinates == null
305 ) {
306 this.startHandlePosition = null
307 this.endHandlePosition = null
308 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100309 }
haoyue2678c62020-12-09 08:39:12 -0800310
George Mount77ca2a22020-12-11 17:46:19 +0000311 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800312 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800313 startSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800314 selection = selection,
315 isStartHandle = true
316 )
317 )
George Mount77ca2a22020-12-11 17:46:19 +0000318 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800319 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800320 endSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800321 selection = selection,
322 isStartHandle = false
323 )
324 )
325
326 val visibleBounds = containerCoordinates.visibleBounds()
327 this.startHandlePosition =
328 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
329 this.endHandlePosition =
330 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100331 }
332
333 /**
334 * Returns non-nullable [containerLayoutCoordinates].
335 */
336 internal fun requireContainerCoordinates(): LayoutCoordinates {
337 val coordinates = containerLayoutCoordinates
338 require(coordinates != null)
339 require(coordinates.isAttached)
340 return coordinates
341 }
342
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700343 internal fun selectAll(
344 selectableId: Long,
345 previousSelection: Selection?
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700346 ): Pair<Selection?, Map<Long, Selection>> {
347 val subselections = mutableMapOf<Long, Selection>()
348 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
349 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
350 val selection = if (selectable.selectableId == selectableId)
351 selectable.getSelectAllSelection() else null
352 selection?.let { subselections[selectable.selectableId] = it }
353 merge(mergedSelection, selection)
354 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700355 if (newSelection != previousSelection) {
356 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
357 }
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700358 return Pair(newSelection, subselections)
359 }
360
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800361 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100362 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800363 var selectedText: AnnotatedString? = null
364
365 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000366 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800367 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800368 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800369 if (selectable.selectableId != it.start.selectableId &&
370 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800371 selectedText == null
372 ) continue
373
374 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800375 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800376 selection = it
377 )
378 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
379
380 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800381 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
382 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800383 ) break
384 }
385 }
386 return selectedText
387 }
388
Qingqing Dengde023cc2020-04-24 14:23:41 -0700389 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700390 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700391 selectedText?.let { clipboardManager?.setText(it) }
392 }
393
394 /**
395 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
396 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
397 * the copy method as a callback when "copy" is clicked.
398 */
399 internal fun showSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800400 if (hasFocus) {
401 selection?.let {
402 textToolbar?.showMenu(
403 getContentRect(),
404 onCopyRequested = {
405 copy()
406 onRelease()
407 }
408 )
409 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700410 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700411 }
412
haoyue6d80a12020-12-02 16:04:52 -0800413 internal fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800414 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800415 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700416 }
417 }
418
419 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800420 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700421 showSelectionToolbar()
422 }
423 }
424
425 /**
426 * Calculate selected region as [Rect]. The top is the top of the first selected
427 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
428 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
429 */
430 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700431 val selection = selection ?: return Rect.Zero
Zach Klippensteinadabe342021-11-11 16:38:13 -0800432 val startSelectable = getAnchorSelectable(selection.start)
433 val endSelectable = getAnchorSelectable(selection.end)
haoyue04245e2021-03-08 14:52:56 -0800434 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
435 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700436
437 val localLayoutCoordinates = containerLayoutCoordinates
438 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000439 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700440 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800441 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700442 selection = selection,
443 isStartHandle = true
444 )
445 )
George Mount77ca2a22020-12-11 17:46:19 +0000446 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700447 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800448 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700449 selection = selection,
450 isStartHandle = false
451 )
452 )
453
454 startOffset = localLayoutCoordinates.localToRoot(startOffset)
455 endOffset = localLayoutCoordinates.localToRoot(endOffset)
456
457 val left = min(startOffset.x, endOffset.x)
458 val right = max(startOffset.x, endOffset.x)
459
George Mount77ca2a22020-12-11 17:46:19 +0000460 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700461 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700462 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700463 0f,
haoyue04245e2021-03-08 14:52:56 -0800464 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700465 )
466 )
467
George Mount77ca2a22020-12-11 17:46:19 +0000468 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700469 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700470 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700471 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800472 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700473 )
474 )
475
476 startTop = localLayoutCoordinates.localToRoot(startTop)
477 endTop = localLayoutCoordinates.localToRoot(endTop)
478
479 val top = min(startTop.y, endTop.y)
Haoyu Zhangd47f1fe2021-07-13 03:10:55 -0700480 val bottom = max(startOffset.y, endOffset.y) + (HandleHeight.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700481
482 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700483 left,
484 top,
485 right,
486 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700487 )
488 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700489 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700490 }
491
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800492 // This is for PressGestureDetector to cancel the selection.
493 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800494 selectionRegistrar.subselections = emptyMap()
haoyue6d80a12020-12-02 16:04:52 -0800495 hideSelectionToolbar()
haoyue04245e2021-03-08 14:52:56 -0800496 if (selection != null) {
497 onSelectionChange(null)
498 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
499 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800500 }
501
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800502 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver = object : TextDragObserver {
503 override fun onDown(point: Offset) {
504 // TODO(b/206833278) Show magnifier on down.
505 }
506
507 override fun onUp() {
508 // TODO(b/206833278) Show magnifier on down.
509 }
510
511 override fun onStart(startPoint: Offset) {
512 hideSelectionToolbar()
513 val selection = selection!!
514 val startSelectable =
515 selectionRegistrar.selectableMap[selection.start.selectableId]
516 val endSelectable =
517 selectionRegistrar.selectableMap[selection.end.selectableId]
518 // The LayoutCoordinates of the composable where the drag gesture should begin. This
519 // is used to convert the position of the beginning of the drag gesture from the
520 // composable coordinates to selection container coordinates.
521 val beginLayoutCoordinates = if (isStartHandle) {
522 startSelectable?.getLayoutCoordinates()!!
523 } else {
524 endSelectable?.getLayoutCoordinates()!!
525 }
526
527 // The position of the character where the drag gesture should begin. This is in
528 // the composable coordinates.
529 val beginCoordinates = getAdjustedCoordinates(
530 if (isStartHandle) {
531 startSelectable!!.getHandlePosition(
532 selection = selection, isStartHandle = true
533 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700534 } else {
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800535 endSelectable!!.getHandlePosition(
536 selection = selection, isStartHandle = false
537 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700538 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800539 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700540
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800541 // Convert the position where drag gesture begins from composable coordinates to
542 // selection container coordinates.
543 dragBeginPosition = requireContainerCoordinates().localPositionOf(
544 beginLayoutCoordinates,
545 beginCoordinates
546 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700547
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800548 // Zero out the total distance that being dragged.
549 dragTotalDistance = Offset.Zero
550 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700551
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800552 override fun onDrag(delta: Offset) {
553 dragTotalDistance += delta
554 val endPosition = dragBeginPosition + dragTotalDistance
555 val consumed = updateSelection(
556 newPosition = endPosition,
557 previousPosition = dragBeginPosition,
558 isStartHandle = isStartHandle,
559 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
560 )
561 if (consumed) {
562 dragBeginPosition = endPosition
Nader Jawad6df06122020-06-03 15:27:08 -0700563 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700564 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800565 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700566
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800567 override fun onStop() {
568 showSelectionToolbar()
569 draggingHandle = null
570 }
haoyue6d80a12020-12-02 16:04:52 -0800571
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800572 override fun onCancel() {
573 showSelectionToolbar()
574 draggingHandle = null
Qingqing Deng6f56a912019-05-13 10:10:37 -0700575 }
576 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700577
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700578 /**
579 * Detect tap without consuming the up event.
580 */
581 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
582 forEachGesture {
583 coroutineScope {
584 awaitPointerEventScope {
585 waitForUpOrCancellation()?.let {
586 onTap(it.position)
587 }
588 }
589 }
590 }
591 }
592
593 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
594 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
595 }
596
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700597 private fun convertToContainerCoordinates(
598 layoutCoordinates: LayoutCoordinates,
599 offset: Offset
600 ): Offset? {
601 val coordinates = containerLayoutCoordinates
602 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000603 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700604 }
605
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700606 /**
607 * Cancel the previous selection and start a new selection at the given [position].
608 * It's used for long-press, double-click, triple-click and so on to start selection.
609 *
610 * @param position initial position of the selection. Both start and end handle is considered
611 * at this position.
612 * @param isStartHandle whether it's considered as the start handle moving. This parameter
613 * will influence the [SelectionAdjustment]'s behavior. For example,
614 * [SelectionAdjustment.Character] only adjust the moving handle.
615 * @param adjustment the selection adjustment.
616 */
617 private fun startSelection(
618 position: Offset,
619 isStartHandle: Boolean,
620 adjustment: SelectionAdjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700621 ) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700622 updateSelection(
623 startHandlePosition = position,
624 endHandlePosition = position,
625 previousHandlePosition = null,
626 isStartHandle = isStartHandle,
627 adjustment = adjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700628 )
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700629 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700630
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700631 /**
632 * Updates the selection after one of the selection handle moved.
633 *
634 * @param newPosition the new position of the moving selection handle.
635 * @param previousPosition the previous position of the moving selection handle.
636 * @param isStartHandle whether the moving selection handle is the start handle.
637 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
638 * produce the final selection range.
639 *
640 * @return a boolean representing whether the movement is consumed.
641 *
642 * @see SelectionAdjustment
643 */
644 internal fun updateSelection(
645 newPosition: Offset?,
646 previousPosition: Offset?,
647 isStartHandle: Boolean,
648 adjustment: SelectionAdjustment,
649 ): Boolean {
650 if (newPosition == null) return false
651 val otherHandlePosition = selection?.let { selection ->
652 val otherSelectableId = if (isStartHandle) {
653 selection.end.selectableId
654 } else {
655 selection.start.selectableId
656 }
657 val otherSelectable =
658 selectionRegistrar.selectableMap[otherSelectableId] ?: return@let null
659 convertToContainerCoordinates(
660 otherSelectable.getLayoutCoordinates()!!,
661 getAdjustedCoordinates(
662 otherSelectable.getHandlePosition(selection, !isStartHandle)
663 )
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700664 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700665 } ?: return false
666
667 val startHandlePosition = if (isStartHandle) newPosition else otherHandlePosition
668 val endHandlePosition = if (isStartHandle) otherHandlePosition else newPosition
669
670 return updateSelection(
671 startHandlePosition = startHandlePosition,
672 endHandlePosition = endHandlePosition,
673 previousHandlePosition = previousPosition,
674 isStartHandle = isStartHandle,
675 adjustment = adjustment
676 )
677 }
678
679 /**
680 * Updates the selection after one of the selection handle moved.
681 *
682 * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
683 * selection handle is updated each time. The only exception is that when a new selection is
684 * started. In this case, [previousHandlePosition] is always null.
685 *
686 * @param startHandlePosition the position of the start selection handle.
687 * @param endHandlePosition the position of the end selection handle.
688 * @param previousHandlePosition the position of the moving handle before the update.
689 * @param isStartHandle whether the moving selection handle is the start handle.
690 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
691 * produce the final selection range.
692 *
693 * @return a boolean representing whether the movement is consumed. It's useful for the case
694 * where a selection handle is updating consecutively. When the return value is true, it's
695 * expected that the caller will update the [startHandlePosition] to be the given
696 * [endHandlePosition] in following calls.
697 *
698 * @see SelectionAdjustment
699 */
700 internal fun updateSelection(
701 startHandlePosition: Offset,
702 endHandlePosition: Offset,
703 previousHandlePosition: Offset?,
704 isStartHandle: Boolean,
705 adjustment: SelectionAdjustment,
706 ): Boolean {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800707 // TODO(b/206833278) This should be set as soon as down event happens, not wait for drag.
708 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700709 val newSubselections = mutableMapOf<Long, Selection>()
710 var moveConsumed = false
711 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
712 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
713 val previousSubselection =
714 selectionRegistrar.subselections[selectable.selectableId]
715 val (selection, consumed) = selectable.updateSelection(
716 startHandlePosition = startHandlePosition,
717 endHandlePosition = endHandlePosition,
718 previousHandlePosition = previousHandlePosition,
719 isStartHandle = isStartHandle,
720 containerLayoutCoordinates = requireContainerCoordinates(),
721 adjustment = adjustment,
722 previousSelection = previousSubselection,
723 )
724
725 moveConsumed = moveConsumed || consumed
726 selection?.let { newSubselections[selectable.selectableId] = it }
727 merge(mergedSelection, selection)
728 }
729 if (newSelection != selection) {
730 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
731 selectionRegistrar.subselections = newSubselections
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700732 onSelectionChange(newSelection)
733 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700734 return moveConsumed
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700735 }
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200736
737 fun contextMenuOpenAdjustment(position: Offset) {
738 val isEmptySelection = selection?.toTextRange()?.collapsed ?: true
739 // TODO(b/209483184) the logic should be more complex here, it should check that current
740 // selection doesn't include click position
741 if (isEmptySelection) {
742 startSelection(
743 position = position,
744 isStartHandle = true,
745 adjustment = SelectionAdjustment.Word
746 )
747 }
748 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700749}
750
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200751internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700752 return lhs?.merge(rhs) ?: rhs
753}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800754
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100755internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
756
Zach Klippensteinadabe342021-11-11 16:38:13 -0800757internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
758
759internal fun calculateSelectionMagnifierCenterAndroid(manager: SelectionManager): Offset {
760 fun getMagnifierCenter(anchor: AnchorInfo, isStartHandle: Boolean): Offset {
761 // TODO(b/206833278) Clamp x to the end of the selectable area, hide after a threshold.
762 // TODO(b/206833278) Animate x when jumping lines.
763 // TODO(b/206833278) X should track drag exactly, not clamp to cursor positions.
764
765 // Let the selectable determine the vertical position of the magnifier, since it should be
766 // clamped to the center of text lines.
767 val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
768 // The end offset is exclusive.
769 val selectionOffset = if (isStartHandle) anchor.offset else anchor.offset - 1
770 val bounds = selectable.getBoundingBox(selectionOffset)
771 // Don't need to account for Rtl here because the bounds will already be adjusted for that.
772 // (i.e. in Rtl, left > right)
773 val localCenter = if (isStartHandle) bounds.centerLeft else bounds.centerRight
774 val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
775 return containerCoordinates.localPositionOf(
776 sourceCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified,
777 relativeToSource = localCenter
778 )
779 }
780
781 val selection = manager.selection ?: return Offset.Unspecified
782 return when (manager.draggingHandle) {
783 null -> return Offset.Unspecified
784 Handle.SelectionStart -> getMagnifierCenter(selection.start, isStartHandle = true)
785 Handle.SelectionEnd -> getMagnifierCenter(selection.end, isStartHandle = false)
786 Handle.Cursor -> error("SelectionContainer does not support cursor")
787 }
788}
789
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200790internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800791 selectable: Selectable,
792 selection: Selection
793): AnnotatedString {
794 val currentText = selectable.getText()
795
796 return if (
haoyue04245e2021-03-08 14:52:56 -0800797 selectable.selectableId != selection.start.selectableId &&
798 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800799 ) {
800 // Select the full text content if the current selectable is between the
801 // start and the end selectables.
802 currentText
803 } else if (
haoyue04245e2021-03-08 14:52:56 -0800804 selectable.selectableId == selection.start.selectableId &&
805 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800806 ) {
807 // Select partial text content if the current selectable is the start and
808 // the end selectable.
809 if (selection.handlesCrossed) {
810 currentText.subSequence(selection.end.offset, selection.start.offset)
811 } else {
812 currentText.subSequence(selection.start.offset, selection.end.offset)
813 }
haoyue04245e2021-03-08 14:52:56 -0800814 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800815 // Select partial text content if the current selectable is the start
816 // selectable.
817 if (selection.handlesCrossed) {
818 currentText.subSequence(0, selection.start.offset)
819 } else {
820 currentText.subSequence(selection.start.offset, currentText.length)
821 }
822 } else {
823 // Selectable partial text content if the current selectable is the end
824 // selectable.
825 if (selection.handlesCrossed) {
826 currentText.subSequence(selection.end.offset, currentText.length)
827 } else {
828 currentText.subSequence(0, selection.end.offset)
829 }
830 }
831}
haoyue2678c62020-12-09 08:39:12 -0800832
833/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800834internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800835 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
836 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
837 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000838 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800839 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000840 windowToLocal(boundsInWindow.topLeft),
841 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800842 )
843}
844
haoyuac341f02021-01-22 22:01:56 -0800845internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800846 offset.x in left..right && offset.y in top..bottom