[go: nahoru, domu]

blob: 1a2ec052860359869fdec1228877dc5620849341 [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 /**
Zach Klippenstein63870892022-01-14 12:45:18 -0800189 * The handle that is currently being dragged, or null when no handle is being dragged. To get
190 * the position of the last drag event, use [currentDragPosition].
Zach Klippensteinadabe342021-11-11 16:38:13 -0800191 */
192 var draggingHandle: Handle? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100193 private set
194
Zach Klippenstein63870892022-01-14 12:45:18 -0800195 /**
196 * When a handle is being dragged (i.e. [draggingHandle] is non-null), this is the last position
197 * of the actual drag event. It is not clamped to handle positions. Null when not being dragged.
198 */
199 var currentDragPosition: Offset? by mutableStateOf(null)
200 private set
201
Zach Klippenstein4688a462021-12-08 08:28:07 -0800202 private val shouldShowMagnifier get() = draggingHandle != null
203
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100204 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700205 selectionRegistrar.onPositionChangeCallback = { selectableId ->
206 if (
207 selectableId == selection?.start?.selectableId ||
208 selectableId == selection?.end?.selectableId
209 ) {
210 updateHandleOffsets()
211 updateSelectionToolbarPosition()
212 }
haoyue6d80a12020-12-02 16:04:52 -0800213 }
214
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100215 selectionRegistrar.onSelectionUpdateStartCallback =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700216 { layoutCoordinates, position, selectionMode ->
217 val positionInContainer = convertToContainerCoordinates(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100218 layoutCoordinates,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700219 position
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100220 )
221
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700222 if (positionInContainer != null) {
223 startSelection(
224 position = positionInContainer,
225 isStartHandle = false,
226 adjustment = selectionMode
227 )
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100228
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700229 focusRequester.requestFocus()
230 hideSelectionToolbar()
231 }
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100232 }
haoyue6d80a12020-12-02 16:04:52 -0800233
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700234 selectionRegistrar.onSelectionUpdateSelectAll =
235 { selectableId ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700236 val (newSelection, newSubselection) = selectAll(
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700237 selectableId = selectableId,
238 previousSelection = selection,
239 )
240 if (newSelection != selection) {
241 selectionRegistrar.subselections = newSubselection
242 onSelectionChange(newSelection)
243 }
244
245 focusRequester.requestFocus()
246 hideSelectionToolbar()
247 }
248
haoyue6d80a12020-12-02 16:04:52 -0800249 selectionRegistrar.onSelectionUpdateCallback =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700250 { layoutCoordinates, newPosition, previousPosition, isStartHandle, selectionMode ->
251 val newPositionInContainer =
252 convertToContainerCoordinates(layoutCoordinates, newPosition)
253 val previousPositionInContainer =
254 convertToContainerCoordinates(layoutCoordinates, previousPosition)
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100255
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700256 updateSelection(
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700257 newPosition = newPositionInContainer,
258 previousPosition = previousPositionInContainer,
259 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700260 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700261 )
262 }
haoyue6d80a12020-12-02 16:04:52 -0800263
264 selectionRegistrar.onSelectionUpdateEndCallback = {
265 showSelectionToolbar()
Zach Klippensteinadabe342021-11-11 16:38:13 -0800266 // This property is set by updateSelection while dragging, so we need to clear it after
267 // the original selection drag.
268 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800269 currentDragPosition = null
haoyue6d80a12020-12-02 16:04:52 -0800270 }
haoyu9085c882020-12-08 12:01:06 -0800271
haoyue04245e2021-03-08 14:52:56 -0800272 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
273 if (selectableKey in selectionRegistrar.subselections) {
haoyu9085c882020-12-08 12:01:06 -0800274 // clear the selection range of each Selectable.
275 onRelease()
276 selection = null
277 }
278 }
haoyue04245e2021-03-08 14:52:56 -0800279
280 selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
281 if (
282 selectableKey == selection?.start?.selectableId ||
283 selectableKey == selection?.end?.selectableId
284 ) {
285 // The selectable that contains a selection handle just unsubscribed.
286 // Hide selection handles for now
287 startHandlePosition = null
288 endHandlePosition = null
289 }
290 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100291 }
292
Zach Klippensteinadabe342021-11-11 16:38:13 -0800293 /**
294 * Returns the [Selectable] responsible for managing the given [AnchorInfo], or null if the
295 * anchor is not from a currently-registered [Selectable].
296 */
297 internal fun getAnchorSelectable(anchor: AnchorInfo): Selectable? {
298 return selectionRegistrar.selectableMap[anchor.selectableId]
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100299 }
300
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100301 private fun updateHandleOffsets() {
302 val selection = selection
303 val containerCoordinates = containerLayoutCoordinates
Zach Klippensteinadabe342021-11-11 16:38:13 -0800304 val startSelectable = selection?.start?.let(::getAnchorSelectable)
305 val endSelectable = selection?.end?.let(::getAnchorSelectable)
haoyue04245e2021-03-08 14:52:56 -0800306 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
307 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
haoyue2678c62020-12-09 08:39:12 -0800308 if (
309 selection == null ||
310 containerCoordinates == null ||
311 !containerCoordinates.isAttached ||
312 startLayoutCoordinates == null ||
313 endLayoutCoordinates == null
314 ) {
315 this.startHandlePosition = null
316 this.endHandlePosition = null
317 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100318 }
haoyue2678c62020-12-09 08:39:12 -0800319
George Mount77ca2a22020-12-11 17:46:19 +0000320 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800321 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800322 startSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800323 selection = selection,
324 isStartHandle = true
325 )
326 )
George Mount77ca2a22020-12-11 17:46:19 +0000327 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800328 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800329 endSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800330 selection = selection,
331 isStartHandle = false
332 )
333 )
334
335 val visibleBounds = containerCoordinates.visibleBounds()
336 this.startHandlePosition =
337 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
338 this.endHandlePosition =
339 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100340 }
341
342 /**
343 * Returns non-nullable [containerLayoutCoordinates].
344 */
345 internal fun requireContainerCoordinates(): LayoutCoordinates {
346 val coordinates = containerLayoutCoordinates
347 require(coordinates != null)
348 require(coordinates.isAttached)
349 return coordinates
350 }
351
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700352 internal fun selectAll(
353 selectableId: Long,
354 previousSelection: Selection?
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700355 ): Pair<Selection?, Map<Long, Selection>> {
356 val subselections = mutableMapOf<Long, Selection>()
357 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
358 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
359 val selection = if (selectable.selectableId == selectableId)
360 selectable.getSelectAllSelection() else null
361 selection?.let { subselections[selectable.selectableId] = it }
362 merge(mergedSelection, selection)
363 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700364 if (newSelection != previousSelection) {
365 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
366 }
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700367 return Pair(newSelection, subselections)
368 }
369
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800370 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100371 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800372 var selectedText: AnnotatedString? = null
373
374 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000375 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800376 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800377 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800378 if (selectable.selectableId != it.start.selectableId &&
379 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800380 selectedText == null
381 ) continue
382
383 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800384 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800385 selection = it
386 )
387 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
388
389 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800390 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
391 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800392 ) break
393 }
394 }
395 return selectedText
396 }
397
Qingqing Dengde023cc2020-04-24 14:23:41 -0700398 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700399 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700400 selectedText?.let { clipboardManager?.setText(it) }
401 }
402
403 /**
404 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
405 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
406 * the copy method as a callback when "copy" is clicked.
407 */
408 internal fun showSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800409 if (hasFocus) {
410 selection?.let {
411 textToolbar?.showMenu(
412 getContentRect(),
413 onCopyRequested = {
414 copy()
415 onRelease()
416 }
417 )
418 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700419 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700420 }
421
haoyue6d80a12020-12-02 16:04:52 -0800422 internal fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800423 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800424 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700425 }
426 }
427
428 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800429 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700430 showSelectionToolbar()
431 }
432 }
433
434 /**
435 * Calculate selected region as [Rect]. The top is the top of the first selected
436 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
437 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
438 */
439 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700440 val selection = selection ?: return Rect.Zero
Zach Klippensteinadabe342021-11-11 16:38:13 -0800441 val startSelectable = getAnchorSelectable(selection.start)
442 val endSelectable = getAnchorSelectable(selection.end)
haoyue04245e2021-03-08 14:52:56 -0800443 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
444 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700445
446 val localLayoutCoordinates = containerLayoutCoordinates
447 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000448 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700449 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800450 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700451 selection = selection,
452 isStartHandle = true
453 )
454 )
George Mount77ca2a22020-12-11 17:46:19 +0000455 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700456 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800457 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700458 selection = selection,
459 isStartHandle = false
460 )
461 )
462
463 startOffset = localLayoutCoordinates.localToRoot(startOffset)
464 endOffset = localLayoutCoordinates.localToRoot(endOffset)
465
466 val left = min(startOffset.x, endOffset.x)
467 val right = max(startOffset.x, endOffset.x)
468
George Mount77ca2a22020-12-11 17:46:19 +0000469 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700470 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700471 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700472 0f,
haoyue04245e2021-03-08 14:52:56 -0800473 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700474 )
475 )
476
George Mount77ca2a22020-12-11 17:46:19 +0000477 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700478 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700479 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700480 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800481 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700482 )
483 )
484
485 startTop = localLayoutCoordinates.localToRoot(startTop)
486 endTop = localLayoutCoordinates.localToRoot(endTop)
487
488 val top = min(startTop.y, endTop.y)
Haoyu Zhangd47f1fe2021-07-13 03:10:55 -0700489 val bottom = max(startOffset.y, endOffset.y) + (HandleHeight.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700490
491 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700492 left,
493 top,
494 right,
495 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700496 )
497 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700498 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700499 }
500
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800501 // This is for PressGestureDetector to cancel the selection.
502 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800503 selectionRegistrar.subselections = emptyMap()
haoyue6d80a12020-12-02 16:04:52 -0800504 hideSelectionToolbar()
haoyue04245e2021-03-08 14:52:56 -0800505 if (selection != null) {
506 onSelectionChange(null)
507 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
508 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800509 }
510
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800511 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver = object : TextDragObserver {
512 override fun onDown(point: Offset) {
Zach Klippenstein63870892022-01-14 12:45:18 -0800513 val selection = selection ?: return
514 val anchor = if (isStartHandle) selection.start else selection.end
515 val selectable = getAnchorSelectable(anchor) ?: return
516 // The LayoutCoordinates of the composable where the drag gesture should begin. This
517 // is used to convert the position of the beginning of the drag gesture from the
518 // composable coordinates to selection container coordinates.
519 val beginLayoutCoordinates = selectable.getLayoutCoordinates() ?: return
520
521 // The position of the character where the drag gesture should begin. This is in
522 // the composable coordinates.
523 val beginCoordinates = getAdjustedCoordinates(
524 selectable.getHandlePosition(
525 selection = selection, isStartHandle = isStartHandle
526 )
527 )
528
529 // Convert the position where drag gesture begins from composable coordinates to
530 // selection container coordinates.
531 currentDragPosition = requireContainerCoordinates().localPositionOf(
532 beginLayoutCoordinates,
533 beginCoordinates
534 )
535 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800536 }
537
538 override fun onUp() {
Zach Klippenstein63870892022-01-14 12:45:18 -0800539 draggingHandle = null
540 currentDragPosition = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800541 }
542
543 override fun onStart(startPoint: Offset) {
544 hideSelectionToolbar()
545 val selection = selection!!
546 val startSelectable =
547 selectionRegistrar.selectableMap[selection.start.selectableId]
548 val endSelectable =
549 selectionRegistrar.selectableMap[selection.end.selectableId]
550 // The LayoutCoordinates of the composable where the drag gesture should begin. This
551 // is used to convert the position of the beginning of the drag gesture from the
552 // composable coordinates to selection container coordinates.
553 val beginLayoutCoordinates = if (isStartHandle) {
554 startSelectable?.getLayoutCoordinates()!!
555 } else {
556 endSelectable?.getLayoutCoordinates()!!
557 }
558
559 // The position of the character where the drag gesture should begin. This is in
560 // the composable coordinates.
561 val beginCoordinates = getAdjustedCoordinates(
562 if (isStartHandle) {
563 startSelectable!!.getHandlePosition(
564 selection = selection, isStartHandle = true
565 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700566 } else {
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800567 endSelectable!!.getHandlePosition(
568 selection = selection, isStartHandle = false
569 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700570 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800571 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700572
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800573 // Convert the position where drag gesture begins from composable coordinates to
574 // selection container coordinates.
575 dragBeginPosition = requireContainerCoordinates().localPositionOf(
576 beginLayoutCoordinates,
577 beginCoordinates
578 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700579
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800580 // Zero out the total distance that being dragged.
581 dragTotalDistance = Offset.Zero
582 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700583
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800584 override fun onDrag(delta: Offset) {
585 dragTotalDistance += delta
586 val endPosition = dragBeginPosition + dragTotalDistance
587 val consumed = updateSelection(
588 newPosition = endPosition,
589 previousPosition = dragBeginPosition,
590 isStartHandle = isStartHandle,
591 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
592 )
593 if (consumed) {
594 dragBeginPosition = endPosition
Nader Jawad6df06122020-06-03 15:27:08 -0700595 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700596 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800597 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700598
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800599 override fun onStop() {
600 showSelectionToolbar()
601 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800602 currentDragPosition = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800603 }
haoyue6d80a12020-12-02 16:04:52 -0800604
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800605 override fun onCancel() {
606 showSelectionToolbar()
607 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800608 currentDragPosition = null
Qingqing Deng6f56a912019-05-13 10:10:37 -0700609 }
610 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700611
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700612 /**
613 * Detect tap without consuming the up event.
614 */
615 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
616 forEachGesture {
617 coroutineScope {
618 awaitPointerEventScope {
619 waitForUpOrCancellation()?.let {
620 onTap(it.position)
621 }
622 }
623 }
624 }
625 }
626
627 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
628 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
629 }
630
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700631 private fun convertToContainerCoordinates(
632 layoutCoordinates: LayoutCoordinates,
633 offset: Offset
634 ): Offset? {
635 val coordinates = containerLayoutCoordinates
636 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000637 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700638 }
639
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700640 /**
641 * Cancel the previous selection and start a new selection at the given [position].
642 * It's used for long-press, double-click, triple-click and so on to start selection.
643 *
644 * @param position initial position of the selection. Both start and end handle is considered
645 * at this position.
646 * @param isStartHandle whether it's considered as the start handle moving. This parameter
647 * will influence the [SelectionAdjustment]'s behavior. For example,
648 * [SelectionAdjustment.Character] only adjust the moving handle.
649 * @param adjustment the selection adjustment.
650 */
651 private fun startSelection(
652 position: Offset,
653 isStartHandle: Boolean,
654 adjustment: SelectionAdjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700655 ) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700656 updateSelection(
657 startHandlePosition = position,
658 endHandlePosition = position,
659 previousHandlePosition = null,
660 isStartHandle = isStartHandle,
661 adjustment = adjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700662 )
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700663 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700664
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700665 /**
666 * Updates the selection after one of the selection handle moved.
667 *
668 * @param newPosition the new position of the moving selection handle.
669 * @param previousPosition the previous position of the moving selection handle.
670 * @param isStartHandle whether the moving selection handle is the start handle.
671 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
672 * produce the final selection range.
673 *
674 * @return a boolean representing whether the movement is consumed.
675 *
676 * @see SelectionAdjustment
677 */
678 internal fun updateSelection(
679 newPosition: Offset?,
680 previousPosition: Offset?,
681 isStartHandle: Boolean,
682 adjustment: SelectionAdjustment,
683 ): Boolean {
684 if (newPosition == null) return false
685 val otherHandlePosition = selection?.let { selection ->
686 val otherSelectableId = if (isStartHandle) {
687 selection.end.selectableId
688 } else {
689 selection.start.selectableId
690 }
691 val otherSelectable =
692 selectionRegistrar.selectableMap[otherSelectableId] ?: return@let null
693 convertToContainerCoordinates(
694 otherSelectable.getLayoutCoordinates()!!,
695 getAdjustedCoordinates(
696 otherSelectable.getHandlePosition(selection, !isStartHandle)
697 )
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700698 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700699 } ?: return false
700
701 val startHandlePosition = if (isStartHandle) newPosition else otherHandlePosition
702 val endHandlePosition = if (isStartHandle) otherHandlePosition else newPosition
703
704 return updateSelection(
705 startHandlePosition = startHandlePosition,
706 endHandlePosition = endHandlePosition,
707 previousHandlePosition = previousPosition,
708 isStartHandle = isStartHandle,
709 adjustment = adjustment
710 )
711 }
712
713 /**
714 * Updates the selection after one of the selection handle moved.
715 *
716 * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
717 * selection handle is updated each time. The only exception is that when a new selection is
718 * started. In this case, [previousHandlePosition] is always null.
719 *
720 * @param startHandlePosition the position of the start selection handle.
721 * @param endHandlePosition the position of the end selection handle.
722 * @param previousHandlePosition the position of the moving handle before the update.
723 * @param isStartHandle whether the moving selection handle is the start handle.
724 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
725 * produce the final selection range.
726 *
727 * @return a boolean representing whether the movement is consumed. It's useful for the case
728 * where a selection handle is updating consecutively. When the return value is true, it's
729 * expected that the caller will update the [startHandlePosition] to be the given
730 * [endHandlePosition] in following calls.
731 *
732 * @see SelectionAdjustment
733 */
734 internal fun updateSelection(
735 startHandlePosition: Offset,
736 endHandlePosition: Offset,
737 previousHandlePosition: Offset?,
738 isStartHandle: Boolean,
739 adjustment: SelectionAdjustment,
740 ): Boolean {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800741 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Zach Klippenstein63870892022-01-14 12:45:18 -0800742 currentDragPosition = if (isStartHandle) startHandlePosition else endHandlePosition
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700743 val newSubselections = mutableMapOf<Long, Selection>()
744 var moveConsumed = false
745 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
746 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
747 val previousSubselection =
748 selectionRegistrar.subselections[selectable.selectableId]
749 val (selection, consumed) = selectable.updateSelection(
750 startHandlePosition = startHandlePosition,
751 endHandlePosition = endHandlePosition,
752 previousHandlePosition = previousHandlePosition,
753 isStartHandle = isStartHandle,
754 containerLayoutCoordinates = requireContainerCoordinates(),
755 adjustment = adjustment,
756 previousSelection = previousSubselection,
757 )
758
759 moveConsumed = moveConsumed || consumed
760 selection?.let { newSubselections[selectable.selectableId] = it }
761 merge(mergedSelection, selection)
762 }
763 if (newSelection != selection) {
764 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
765 selectionRegistrar.subselections = newSubselections
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700766 onSelectionChange(newSelection)
767 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700768 return moveConsumed
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700769 }
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200770
771 fun contextMenuOpenAdjustment(position: Offset) {
772 val isEmptySelection = selection?.toTextRange()?.collapsed ?: true
773 // TODO(b/209483184) the logic should be more complex here, it should check that current
774 // selection doesn't include click position
775 if (isEmptySelection) {
776 startSelection(
777 position = position,
778 isStartHandle = true,
779 adjustment = SelectionAdjustment.Word
780 )
781 }
782 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700783}
784
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200785internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700786 return lhs?.merge(rhs) ?: rhs
787}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800788
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100789internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
790
Zach Klippensteinadabe342021-11-11 16:38:13 -0800791internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
792
793internal fun calculateSelectionMagnifierCenterAndroid(manager: SelectionManager): Offset {
794 fun getMagnifierCenter(anchor: AnchorInfo, isStartHandle: Boolean): Offset {
795 // TODO(b/206833278) Clamp x to the end of the selectable area, hide after a threshold.
796 // TODO(b/206833278) Animate x when jumping lines.
797 // TODO(b/206833278) X should track drag exactly, not clamp to cursor positions.
798
799 // Let the selectable determine the vertical position of the magnifier, since it should be
800 // clamped to the center of text lines.
801 val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
802 // The end offset is exclusive.
803 val selectionOffset = if (isStartHandle) anchor.offset else anchor.offset - 1
804 val bounds = selectable.getBoundingBox(selectionOffset)
805 // Don't need to account for Rtl here because the bounds will already be adjusted for that.
806 // (i.e. in Rtl, left > right)
807 val localCenter = if (isStartHandle) bounds.centerLeft else bounds.centerRight
808 val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
809 return containerCoordinates.localPositionOf(
810 sourceCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified,
811 relativeToSource = localCenter
812 )
813 }
814
815 val selection = manager.selection ?: return Offset.Unspecified
816 return when (manager.draggingHandle) {
817 null -> return Offset.Unspecified
818 Handle.SelectionStart -> getMagnifierCenter(selection.start, isStartHandle = true)
819 Handle.SelectionEnd -> getMagnifierCenter(selection.end, isStartHandle = false)
820 Handle.Cursor -> error("SelectionContainer does not support cursor")
821 }
822}
823
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200824internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800825 selectable: Selectable,
826 selection: Selection
827): AnnotatedString {
828 val currentText = selectable.getText()
829
830 return if (
haoyue04245e2021-03-08 14:52:56 -0800831 selectable.selectableId != selection.start.selectableId &&
832 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800833 ) {
834 // Select the full text content if the current selectable is between the
835 // start and the end selectables.
836 currentText
837 } else if (
haoyue04245e2021-03-08 14:52:56 -0800838 selectable.selectableId == selection.start.selectableId &&
839 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800840 ) {
841 // Select partial text content if the current selectable is the start and
842 // the end selectable.
843 if (selection.handlesCrossed) {
844 currentText.subSequence(selection.end.offset, selection.start.offset)
845 } else {
846 currentText.subSequence(selection.start.offset, selection.end.offset)
847 }
haoyue04245e2021-03-08 14:52:56 -0800848 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800849 // Select partial text content if the current selectable is the start
850 // selectable.
851 if (selection.handlesCrossed) {
852 currentText.subSequence(0, selection.start.offset)
853 } else {
854 currentText.subSequence(selection.start.offset, currentText.length)
855 }
856 } else {
857 // Selectable partial text content if the current selectable is the end
858 // selectable.
859 if (selection.handlesCrossed) {
860 currentText.subSequence(selection.end.offset, currentText.length)
861 } else {
862 currentText.subSequence(0, selection.end.offset)
863 }
864 }
865}
haoyue2678c62020-12-09 08:39:12 -0800866
867/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800868internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800869 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
870 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
871 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000872 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800873 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000874 windowToLocal(boundsInWindow.topLeft),
875 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800876 )
877}
878
haoyuac341f02021-01-22 22:01:56 -0800879internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800880 offset.x in left..right && offset.y in top..bottom