[go: nahoru, domu]

blob: a940422a674b04fdc314b6fffa8125ad26b6f28d [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 Klippenstein4688a462021-12-08 08:28:07 -0800135 .then(if (shouldShowMagnifier) Modifier.selectionMagnifier(this) else Modifier)
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
Zach Klippenstein4688a462021-12-08 08:28:07 -0800192 private val shouldShowMagnifier get() = draggingHandle != null
193
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100194 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700195 selectionRegistrar.onPositionChangeCallback = { selectableId ->
196 if (
197 selectableId == selection?.start?.selectableId ||
198 selectableId == selection?.end?.selectableId
199 ) {
200 updateHandleOffsets()
201 updateSelectionToolbarPosition()
202 }
haoyue6d80a12020-12-02 16:04:52 -0800203 }
204
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100205 selectionRegistrar.onSelectionUpdateStartCallback =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700206 { layoutCoordinates, position, selectionMode ->
207 val positionInContainer = convertToContainerCoordinates(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100208 layoutCoordinates,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700209 position
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100210 )
211
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700212 if (positionInContainer != null) {
213 startSelection(
214 position = positionInContainer,
215 isStartHandle = false,
216 adjustment = selectionMode
217 )
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100218
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700219 focusRequester.requestFocus()
220 hideSelectionToolbar()
221 }
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100222 }
haoyue6d80a12020-12-02 16:04:52 -0800223
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700224 selectionRegistrar.onSelectionUpdateSelectAll =
225 { selectableId ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700226 val (newSelection, newSubselection) = selectAll(
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700227 selectableId = selectableId,
228 previousSelection = selection,
229 )
230 if (newSelection != selection) {
231 selectionRegistrar.subselections = newSubselection
232 onSelectionChange(newSelection)
233 }
234
235 focusRequester.requestFocus()
236 hideSelectionToolbar()
237 }
238
haoyue6d80a12020-12-02 16:04:52 -0800239 selectionRegistrar.onSelectionUpdateCallback =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700240 { layoutCoordinates, newPosition, previousPosition, isStartHandle, selectionMode ->
241 val newPositionInContainer =
242 convertToContainerCoordinates(layoutCoordinates, newPosition)
243 val previousPositionInContainer =
244 convertToContainerCoordinates(layoutCoordinates, previousPosition)
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100245
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700246 updateSelection(
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700247 newPosition = newPositionInContainer,
248 previousPosition = previousPositionInContainer,
249 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700250 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700251 )
252 }
haoyue6d80a12020-12-02 16:04:52 -0800253
254 selectionRegistrar.onSelectionUpdateEndCallback = {
255 showSelectionToolbar()
Zach Klippensteinadabe342021-11-11 16:38:13 -0800256 // This property is set by updateSelection while dragging, so we need to clear it after
257 // the original selection drag.
258 draggingHandle = null
haoyue6d80a12020-12-02 16:04:52 -0800259 }
haoyu9085c882020-12-08 12:01:06 -0800260
haoyue04245e2021-03-08 14:52:56 -0800261 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
262 if (selectableKey in selectionRegistrar.subselections) {
haoyu9085c882020-12-08 12:01:06 -0800263 // clear the selection range of each Selectable.
264 onRelease()
265 selection = null
266 }
267 }
haoyue04245e2021-03-08 14:52:56 -0800268
269 selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
270 if (
271 selectableKey == selection?.start?.selectableId ||
272 selectableKey == selection?.end?.selectableId
273 ) {
274 // The selectable that contains a selection handle just unsubscribed.
275 // Hide selection handles for now
276 startHandlePosition = null
277 endHandlePosition = null
278 }
279 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100280 }
281
Zach Klippensteinadabe342021-11-11 16:38:13 -0800282 /**
283 * Returns the [Selectable] responsible for managing the given [AnchorInfo], or null if the
284 * anchor is not from a currently-registered [Selectable].
285 */
286 internal fun getAnchorSelectable(anchor: AnchorInfo): Selectable? {
287 return selectionRegistrar.selectableMap[anchor.selectableId]
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100288 }
289
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100290 private fun updateHandleOffsets() {
291 val selection = selection
292 val containerCoordinates = containerLayoutCoordinates
Zach Klippensteinadabe342021-11-11 16:38:13 -0800293 val startSelectable = selection?.start?.let(::getAnchorSelectable)
294 val endSelectable = selection?.end?.let(::getAnchorSelectable)
haoyue04245e2021-03-08 14:52:56 -0800295 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
296 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
haoyue2678c62020-12-09 08:39:12 -0800297 if (
298 selection == null ||
299 containerCoordinates == null ||
300 !containerCoordinates.isAttached ||
301 startLayoutCoordinates == null ||
302 endLayoutCoordinates == null
303 ) {
304 this.startHandlePosition = null
305 this.endHandlePosition = null
306 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100307 }
haoyue2678c62020-12-09 08:39:12 -0800308
George Mount77ca2a22020-12-11 17:46:19 +0000309 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800310 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800311 startSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800312 selection = selection,
313 isStartHandle = true
314 )
315 )
George Mount77ca2a22020-12-11 17:46:19 +0000316 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800317 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800318 endSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800319 selection = selection,
320 isStartHandle = false
321 )
322 )
323
324 val visibleBounds = containerCoordinates.visibleBounds()
325 this.startHandlePosition =
326 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
327 this.endHandlePosition =
328 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100329 }
330
331 /**
332 * Returns non-nullable [containerLayoutCoordinates].
333 */
334 internal fun requireContainerCoordinates(): LayoutCoordinates {
335 val coordinates = containerLayoutCoordinates
336 require(coordinates != null)
337 require(coordinates.isAttached)
338 return coordinates
339 }
340
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700341 internal fun selectAll(
342 selectableId: Long,
343 previousSelection: Selection?
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700344 ): Pair<Selection?, Map<Long, Selection>> {
345 val subselections = mutableMapOf<Long, Selection>()
346 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
347 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
348 val selection = if (selectable.selectableId == selectableId)
349 selectable.getSelectAllSelection() else null
350 selection?.let { subselections[selectable.selectableId] = it }
351 merge(mergedSelection, selection)
352 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700353 if (newSelection != previousSelection) {
354 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
355 }
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700356 return Pair(newSelection, subselections)
357 }
358
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800359 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100360 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800361 var selectedText: AnnotatedString? = null
362
363 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000364 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800365 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800366 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800367 if (selectable.selectableId != it.start.selectableId &&
368 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800369 selectedText == null
370 ) continue
371
372 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800373 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800374 selection = it
375 )
376 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
377
378 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800379 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
380 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800381 ) break
382 }
383 }
384 return selectedText
385 }
386
Qingqing Dengde023cc2020-04-24 14:23:41 -0700387 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700388 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700389 selectedText?.let { clipboardManager?.setText(it) }
390 }
391
392 /**
393 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
394 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
395 * the copy method as a callback when "copy" is clicked.
396 */
397 internal fun showSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800398 if (hasFocus) {
399 selection?.let {
400 textToolbar?.showMenu(
401 getContentRect(),
402 onCopyRequested = {
403 copy()
404 onRelease()
405 }
406 )
407 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700408 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700409 }
410
haoyue6d80a12020-12-02 16:04:52 -0800411 internal fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800412 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800413 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700414 }
415 }
416
417 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800418 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700419 showSelectionToolbar()
420 }
421 }
422
423 /**
424 * Calculate selected region as [Rect]. The top is the top of the first selected
425 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
426 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
427 */
428 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700429 val selection = selection ?: return Rect.Zero
Zach Klippensteinadabe342021-11-11 16:38:13 -0800430 val startSelectable = getAnchorSelectable(selection.start)
431 val endSelectable = getAnchorSelectable(selection.end)
haoyue04245e2021-03-08 14:52:56 -0800432 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
433 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700434
435 val localLayoutCoordinates = containerLayoutCoordinates
436 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000437 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700438 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800439 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700440 selection = selection,
441 isStartHandle = true
442 )
443 )
George Mount77ca2a22020-12-11 17:46:19 +0000444 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700445 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800446 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700447 selection = selection,
448 isStartHandle = false
449 )
450 )
451
452 startOffset = localLayoutCoordinates.localToRoot(startOffset)
453 endOffset = localLayoutCoordinates.localToRoot(endOffset)
454
455 val left = min(startOffset.x, endOffset.x)
456 val right = max(startOffset.x, endOffset.x)
457
George Mount77ca2a22020-12-11 17:46:19 +0000458 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700459 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700460 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700461 0f,
haoyue04245e2021-03-08 14:52:56 -0800462 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700463 )
464 )
465
George Mount77ca2a22020-12-11 17:46:19 +0000466 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700467 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700468 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700469 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800470 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700471 )
472 )
473
474 startTop = localLayoutCoordinates.localToRoot(startTop)
475 endTop = localLayoutCoordinates.localToRoot(endTop)
476
477 val top = min(startTop.y, endTop.y)
Haoyu Zhangd47f1fe2021-07-13 03:10:55 -0700478 val bottom = max(startOffset.y, endOffset.y) + (HandleHeight.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700479
480 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700481 left,
482 top,
483 right,
484 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700485 )
486 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700487 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700488 }
489
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800490 // This is for PressGestureDetector to cancel the selection.
491 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800492 selectionRegistrar.subselections = emptyMap()
haoyue6d80a12020-12-02 16:04:52 -0800493 hideSelectionToolbar()
haoyue04245e2021-03-08 14:52:56 -0800494 if (selection != null) {
495 onSelectionChange(null)
496 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
497 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800498 }
499
Matvei Malkovf770a912021-03-24 18:12:41 +0000500 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver {
501 return object : TextDragObserver {
502 override fun onStart(startPoint: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800503 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700504 val selection = selection!!
haoyue04245e2021-03-08 14:52:56 -0800505 val startSelectable =
506 selectionRegistrar.selectableMap[selection.start.selectableId]
507 val endSelectable =
508 selectionRegistrar.selectableMap[selection.end.selectableId]
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100509 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700510 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100511 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700512 val beginLayoutCoordinates = if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800513 startSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700514 } else {
haoyue04245e2021-03-08 14:52:56 -0800515 endSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700516 }
517
Qingqing Deng6f56a912019-05-13 10:10:37 -0700518 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100519 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700520 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800521 if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800522 startSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800523 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800524 )
525 } else {
haoyue04245e2021-03-08 14:52:56 -0800526 endSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800527 selection = selection, isStartHandle = false
528 )
haoyue6d80a12020-12-02 16:04:52 -0800529 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700530 )
531
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100532 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700533 // selection container coordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000534 dragBeginPosition = requireContainerCoordinates().localPositionOf(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700535 beginLayoutCoordinates,
536 beginCoordinates
537 )
538
539 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700540 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700541 }
542
Matvei Malkovf770a912021-03-24 18:12:41 +0000543 override fun onDrag(delta: Offset) {
Matvei Malkovf770a912021-03-24 18:12:41 +0000544 dragTotalDistance += delta
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700545 val endPosition = dragBeginPosition + dragTotalDistance
546 val consumed = updateSelection(
547 newPosition = endPosition,
548 previousPosition = dragBeginPosition,
Haoyu Zhang5740343b2021-07-15 11:29:27 -0700549 isStartHandle = isStartHandle,
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700550 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
Qingqing Denga5d80952019-10-11 16:46:52 -0700551 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700552 if (consumed) {
553 dragBeginPosition = endPosition
554 dragTotalDistance = Offset.Zero
555 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700556 }
haoyue6d80a12020-12-02 16:04:52 -0800557
Matvei Malkovf770a912021-03-24 18:12:41 +0000558 override fun onStop() {
haoyue6d80a12020-12-02 16:04:52 -0800559 showSelectionToolbar()
Zach Klippensteinadabe342021-11-11 16:38:13 -0800560 draggingHandle = null
haoyue6d80a12020-12-02 16:04:52 -0800561 }
562
563 override fun onCancel() {
564 showSelectionToolbar()
Zach Klippensteinadabe342021-11-11 16:38:13 -0800565 draggingHandle = null
haoyue6d80a12020-12-02 16:04:52 -0800566 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700567 }
568 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700569
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700570 /**
571 * Detect tap without consuming the up event.
572 */
573 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
574 forEachGesture {
575 coroutineScope {
576 awaitPointerEventScope {
577 waitForUpOrCancellation()?.let {
578 onTap(it.position)
579 }
580 }
581 }
582 }
583 }
584
585 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
586 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
587 }
588
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700589 private fun convertToContainerCoordinates(
590 layoutCoordinates: LayoutCoordinates,
591 offset: Offset
592 ): Offset? {
593 val coordinates = containerLayoutCoordinates
594 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000595 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700596 }
597
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700598 /**
599 * Cancel the previous selection and start a new selection at the given [position].
600 * It's used for long-press, double-click, triple-click and so on to start selection.
601 *
602 * @param position initial position of the selection. Both start and end handle is considered
603 * at this position.
604 * @param isStartHandle whether it's considered as the start handle moving. This parameter
605 * will influence the [SelectionAdjustment]'s behavior. For example,
606 * [SelectionAdjustment.Character] only adjust the moving handle.
607 * @param adjustment the selection adjustment.
608 */
609 private fun startSelection(
610 position: Offset,
611 isStartHandle: Boolean,
612 adjustment: SelectionAdjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700613 ) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700614 updateSelection(
615 startHandlePosition = position,
616 endHandlePosition = position,
617 previousHandlePosition = null,
618 isStartHandle = isStartHandle,
619 adjustment = adjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700620 )
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700621 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700622
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700623 /**
624 * Updates the selection after one of the selection handle moved.
625 *
626 * @param newPosition the new position of the moving selection handle.
627 * @param previousPosition the previous position of the moving selection handle.
628 * @param isStartHandle whether the moving selection handle is the start handle.
629 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
630 * produce the final selection range.
631 *
632 * @return a boolean representing whether the movement is consumed.
633 *
634 * @see SelectionAdjustment
635 */
636 internal fun updateSelection(
637 newPosition: Offset?,
638 previousPosition: Offset?,
639 isStartHandle: Boolean,
640 adjustment: SelectionAdjustment,
641 ): Boolean {
642 if (newPosition == null) return false
643 val otherHandlePosition = selection?.let { selection ->
644 val otherSelectableId = if (isStartHandle) {
645 selection.end.selectableId
646 } else {
647 selection.start.selectableId
648 }
649 val otherSelectable =
650 selectionRegistrar.selectableMap[otherSelectableId] ?: return@let null
651 convertToContainerCoordinates(
652 otherSelectable.getLayoutCoordinates()!!,
653 getAdjustedCoordinates(
654 otherSelectable.getHandlePosition(selection, !isStartHandle)
655 )
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700656 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700657 } ?: return false
658
659 val startHandlePosition = if (isStartHandle) newPosition else otherHandlePosition
660 val endHandlePosition = if (isStartHandle) otherHandlePosition else newPosition
661
662 return updateSelection(
663 startHandlePosition = startHandlePosition,
664 endHandlePosition = endHandlePosition,
665 previousHandlePosition = previousPosition,
666 isStartHandle = isStartHandle,
667 adjustment = adjustment
668 )
669 }
670
671 /**
672 * Updates the selection after one of the selection handle moved.
673 *
674 * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
675 * selection handle is updated each time. The only exception is that when a new selection is
676 * started. In this case, [previousHandlePosition] is always null.
677 *
678 * @param startHandlePosition the position of the start selection handle.
679 * @param endHandlePosition the position of the end selection handle.
680 * @param previousHandlePosition the position of the moving handle before the update.
681 * @param isStartHandle whether the moving selection handle is the start handle.
682 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
683 * produce the final selection range.
684 *
685 * @return a boolean representing whether the movement is consumed. It's useful for the case
686 * where a selection handle is updating consecutively. When the return value is true, it's
687 * expected that the caller will update the [startHandlePosition] to be the given
688 * [endHandlePosition] in following calls.
689 *
690 * @see SelectionAdjustment
691 */
692 internal fun updateSelection(
693 startHandlePosition: Offset,
694 endHandlePosition: Offset,
695 previousHandlePosition: Offset?,
696 isStartHandle: Boolean,
697 adjustment: SelectionAdjustment,
698 ): Boolean {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800699 // TODO(b/206833278) This should be set as soon as down event happens, not wait for drag.
700 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700701 val newSubselections = mutableMapOf<Long, Selection>()
702 var moveConsumed = false
703 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
704 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
705 val previousSubselection =
706 selectionRegistrar.subselections[selectable.selectableId]
707 val (selection, consumed) = selectable.updateSelection(
708 startHandlePosition = startHandlePosition,
709 endHandlePosition = endHandlePosition,
710 previousHandlePosition = previousHandlePosition,
711 isStartHandle = isStartHandle,
712 containerLayoutCoordinates = requireContainerCoordinates(),
713 adjustment = adjustment,
714 previousSelection = previousSubselection,
715 )
716
717 moveConsumed = moveConsumed || consumed
718 selection?.let { newSubselections[selectable.selectableId] = it }
719 merge(mergedSelection, selection)
720 }
721 if (newSelection != selection) {
722 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
723 selectionRegistrar.subselections = newSubselections
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700724 onSelectionChange(newSelection)
725 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700726 return moveConsumed
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700727 }
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200728
729 fun contextMenuOpenAdjustment(position: Offset) {
730 val isEmptySelection = selection?.toTextRange()?.collapsed ?: true
731 // TODO(b/209483184) the logic should be more complex here, it should check that current
732 // selection doesn't include click position
733 if (isEmptySelection) {
734 startSelection(
735 position = position,
736 isStartHandle = true,
737 adjustment = SelectionAdjustment.Word
738 )
739 }
740 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700741}
742
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200743internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700744 return lhs?.merge(rhs) ?: rhs
745}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800746
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100747internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
748
Zach Klippensteinadabe342021-11-11 16:38:13 -0800749internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
750
751internal fun calculateSelectionMagnifierCenterAndroid(manager: SelectionManager): Offset {
752 fun getMagnifierCenter(anchor: AnchorInfo, isStartHandle: Boolean): Offset {
753 // TODO(b/206833278) Clamp x to the end of the selectable area, hide after a threshold.
754 // TODO(b/206833278) Animate x when jumping lines.
755 // TODO(b/206833278) X should track drag exactly, not clamp to cursor positions.
756
757 // Let the selectable determine the vertical position of the magnifier, since it should be
758 // clamped to the center of text lines.
759 val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
760 // The end offset is exclusive.
761 val selectionOffset = if (isStartHandle) anchor.offset else anchor.offset - 1
762 val bounds = selectable.getBoundingBox(selectionOffset)
763 // Don't need to account for Rtl here because the bounds will already be adjusted for that.
764 // (i.e. in Rtl, left > right)
765 val localCenter = if (isStartHandle) bounds.centerLeft else bounds.centerRight
766 val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
767 return containerCoordinates.localPositionOf(
768 sourceCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified,
769 relativeToSource = localCenter
770 )
771 }
772
773 val selection = manager.selection ?: return Offset.Unspecified
774 return when (manager.draggingHandle) {
775 null -> return Offset.Unspecified
776 Handle.SelectionStart -> getMagnifierCenter(selection.start, isStartHandle = true)
777 Handle.SelectionEnd -> getMagnifierCenter(selection.end, isStartHandle = false)
778 Handle.Cursor -> error("SelectionContainer does not support cursor")
779 }
780}
781
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200782internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800783 selectable: Selectable,
784 selection: Selection
785): AnnotatedString {
786 val currentText = selectable.getText()
787
788 return if (
haoyue04245e2021-03-08 14:52:56 -0800789 selectable.selectableId != selection.start.selectableId &&
790 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800791 ) {
792 // Select the full text content if the current selectable is between the
793 // start and the end selectables.
794 currentText
795 } else if (
haoyue04245e2021-03-08 14:52:56 -0800796 selectable.selectableId == selection.start.selectableId &&
797 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800798 ) {
799 // Select partial text content if the current selectable is the start and
800 // the end selectable.
801 if (selection.handlesCrossed) {
802 currentText.subSequence(selection.end.offset, selection.start.offset)
803 } else {
804 currentText.subSequence(selection.start.offset, selection.end.offset)
805 }
haoyue04245e2021-03-08 14:52:56 -0800806 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800807 // Select partial text content if the current selectable is the start
808 // selectable.
809 if (selection.handlesCrossed) {
810 currentText.subSequence(0, selection.start.offset)
811 } else {
812 currentText.subSequence(selection.start.offset, currentText.length)
813 }
814 } else {
815 // Selectable partial text content if the current selectable is the end
816 // selectable.
817 if (selection.handlesCrossed) {
818 currentText.subSequence(selection.end.offset, currentText.length)
819 } else {
820 currentText.subSequence(0, selection.end.offset)
821 }
822 }
823}
haoyue2678c62020-12-09 08:39:12 -0800824
825/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800826internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800827 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
828 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
829 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000830 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800831 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000832 windowToLocal(boundsInWindow.topLeft),
833 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800834 )
835}
836
haoyuac341f02021-01-22 22:01:56 -0800837internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800838 offset.x in left..right && offset.y in top..bottom