[go: nahoru, domu]

blob: dd8f4ec5e309a0d113e524cd0e8645449b8926b2 [file] [log] [blame]
Qingqing Deng6f56a912019-05-13 10:10:37 -07001/*
haoyuac341f02021-01-22 22:01:56 -08002 * Copyright 2021 The Android Open Source Project
Qingqing Deng6f56a912019-05-13 10:10:37 -07003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Matvei Malkovc287d1b2021-01-26 15:04:34 +000017@file:Suppress("DEPRECATION")
18
haoyuac341f02021-01-22 22:01:56 -080019package androidx.compose.foundation.text.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070020
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +000021import androidx.compose.foundation.fastFold
haoyuc40d02752021-01-25 17:32:47 -080022import androidx.compose.foundation.focusable
Ralston Da Silvade62bc62021-06-02 17:46:44 -070023import androidx.compose.foundation.gestures.forEachGesture
24import androidx.compose.foundation.gestures.waitForUpOrCancellation
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010025import androidx.compose.runtime.State
26import androidx.compose.runtime.getValue
27import androidx.compose.runtime.mutableStateOf
28import androidx.compose.runtime.setValue
29import androidx.compose.runtime.structuralEqualityPolicy
haoyuc40d02752021-01-25 17:32:47 -080030import androidx.compose.ui.Modifier
31import androidx.compose.ui.focus.FocusRequester
32import androidx.compose.ui.focus.focusRequester
haoyuc40d02752021-01-25 17:32:47 -080033import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010034import androidx.compose.ui.geometry.Offset
35import androidx.compose.ui.geometry.Rect
Matvei Malkovf770a912021-03-24 18:12:41 +000036import androidx.compose.foundation.text.TextDragObserver
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010037import androidx.compose.ui.hapticfeedback.HapticFeedback
38import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010039import androidx.compose.ui.input.key.KeyEvent
40import androidx.compose.ui.input.key.onKeyEvent
Ralston Da Silvade62bc62021-06-02 17:46:44 -070041import androidx.compose.ui.input.pointer.PointerInputScope
42import androidx.compose.ui.input.pointer.pointerInput
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010043import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000044import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080045import androidx.compose.ui.layout.onGloballyPositioned
haoyu7ad5ea32021-03-22 10:36:35 -070046import androidx.compose.ui.layout.positionInWindow
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010047import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010048import androidx.compose.ui.platform.TextToolbar
49import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010050import androidx.compose.ui.text.AnnotatedString
Ralston Da Silvade62bc62021-06-02 17:46:44 -070051import kotlinx.coroutines.coroutineScope
Nader Jawade6a9b332020-05-21 13:49:20 -070052import kotlin.math.max
53import kotlin.math.min
Qingqing Deng6f56a912019-05-13 10:10:37 -070054
Qingqing Deng35f97ea2019-09-18 19:24:37 -070055/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010056 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070057 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070058internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070059 /**
60 * The current selection.
61 */
62 var selection: Selection? = null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010063 set(value) {
64 field = value
haoyu9085c882020-12-08 12:01:06 -080065 if (value != null) {
66 updateHandleOffsets()
67 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010068 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070069
70 /**
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010071 * Is touch mode active
72 */
73 var touchMode: Boolean = true
74
75 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070076 * The manager will invoke this every time it comes to the conclusion that the selection should
77 * change. The expectation is that this callback will end up causing `setSelection` to get
78 * called. This is what makes this a "controlled component".
79 */
80 var onSelectionChange: (Selection?) -> Unit = {}
81
82 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080083 * [HapticFeedback] handle to perform haptic feedback.
84 */
85 var hapticFeedBack: HapticFeedback? = null
86
87 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070088 * [ClipboardManager] to perform clipboard features.
89 */
90 var clipboardManager: ClipboardManager? = null
91
92 /**
Qingqing Denga89450d2020-04-03 19:11:49 -070093 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
94 */
95 var textToolbar: TextToolbar? = null
96
97 /**
haoyuc40d02752021-01-25 17:32:47 -080098 * Focus requester used to request focus when selection becomes active.
99 */
100 var focusRequester: FocusRequester = FocusRequester()
101
102 /**
haoyu3c3fb452021-02-18 01:01:14 -0800103 * Return true if the corresponding SelectionContainer is focused.
haoyuc40d02752021-01-25 17:32:47 -0800104 */
haoyu3c3fb452021-02-18 01:01:14 -0800105 var hasFocus: Boolean by mutableStateOf(false)
haoyuc40d02752021-01-25 17:32:47 -0800106
107 /**
108 * Modifier for selection container.
109 */
110 val modifier get() = Modifier
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700111 .onClearSelectionRequested { onRelease() }
haoyuc40d02752021-01-25 17:32:47 -0800112 .onGloballyPositioned { containerLayoutCoordinates = it }
113 .focusRequester(focusRequester)
114 .onFocusChanged { focusState ->
haoyu3c3fb452021-02-18 01:01:14 -0800115 if (!focusState.isFocused && hasFocus) {
haoyuc40d02752021-01-25 17:32:47 -0800116 onRelease()
117 }
haoyu3c3fb452021-02-18 01:01:14 -0800118 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800119 }
haoyu3c3fb452021-02-18 01:01:14 -0800120 .focusable()
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100121 .onKeyEvent {
122 if (isCopyKeyEvent(it)) {
123 copy()
124 true
125 } else {
126 false
127 }
128 }
haoyuc40d02752021-01-25 17:32:47 -0800129
haoyu7ad5ea32021-03-22 10:36:35 -0700130 private var previousPosition: Offset? = null
haoyuc40d02752021-01-25 17:32:47 -0800131 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700132 * Layout Coordinates of the selection container.
133 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100134 var containerLayoutCoordinates: LayoutCoordinates? = null
135 set(value) {
136 field = value
haoyu2c6e9842021-03-30 17:39:04 -0700137 if (hasFocus && selection != null) {
138 val positionInWindow = value?.positionInWindow()
139 if (previousPosition != positionInWindow) {
140 previousPosition = positionInWindow
141 updateHandleOffsets()
142 updateSelectionToolbarPosition()
143 }
haoyu3c3fb452021-02-18 01:01:14 -0800144 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100145 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700146
147 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700148 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
149 * recalculated.
150 */
Nader Jawad6df06122020-06-03 15:27:08 -0700151 private var dragBeginPosition = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700152
153 /**
154 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
155 * it will be zeroed out.
156 */
Nader Jawad6df06122020-06-03 15:27:08 -0700157 private var dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700158
159 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100160 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
161 * is null when handle shouldn't be displayed.
162 * It is a [State] so reading it during the composition will cause recomposition every time
163 * the position has been changed.
164 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700165 var startHandlePosition by mutableStateOf<Offset?>(
166 null,
167 policy = structuralEqualityPolicy()
168 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100169 private set
170
171 /**
172 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
173 * is null when handle shouldn't be displayed.
174 * It is a [State] so reading it during the composition will cause recomposition every time
175 * the position has been changed.
176 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700177 var endHandlePosition by mutableStateOf<Offset?>(
178 null,
179 policy = structuralEqualityPolicy()
180 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100181 private set
182
183 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700184 selectionRegistrar.onPositionChangeCallback = { selectableId ->
185 if (
186 selectableId == selection?.start?.selectableId ||
187 selectableId == selection?.end?.selectableId
188 ) {
189 updateHandleOffsets()
190 updateSelectionToolbarPosition()
191 }
haoyue6d80a12020-12-02 16:04:52 -0800192 }
193
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100194 selectionRegistrar.onSelectionUpdateStartCallback =
195 { layoutCoordinates, startPosition, selectionMode ->
196 val startPositionInContainer = convertToContainerCoordinates(
197 layoutCoordinates,
198 startPosition
199 )
200
201 updateSelection(
202 startPosition = startPositionInContainer,
203 endPosition = startPositionInContainer,
204 isStartHandle = true,
205 adjustment = selectionMode
206 )
207
208 focusRequester.requestFocus()
209 hideSelectionToolbar()
210 }
haoyue6d80a12020-12-02 16:04:52 -0800211
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700212 selectionRegistrar.onSelectionUpdateSelectAll =
213 { selectableId ->
214 val (newSelection, newSubselection) = mergeSelections(
215 selectableId = selectableId,
216 previousSelection = selection,
217 )
218 if (newSelection != selection) {
219 selectionRegistrar.subselections = newSubselection
220 onSelectionChange(newSelection)
221 }
222
223 focusRequester.requestFocus()
224 hideSelectionToolbar()
225 }
226
haoyue6d80a12020-12-02 16:04:52 -0800227 selectionRegistrar.onSelectionUpdateCallback =
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100228 { layoutCoordinates, startPosition, endPosition, selectionMode ->
229 val startPositionOrCurrent = if (startPosition == null) {
230 currentSelectionStartPosition()
231 } else {
232 convertToContainerCoordinates(layoutCoordinates, startPosition)
233 }
234
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700235 updateSelection(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100236 startPosition = startPositionOrCurrent,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700237 endPosition = convertToContainerCoordinates(layoutCoordinates, endPosition),
haoyu70aed732020-12-15 07:10:56 -0800238 isStartHandle = false,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700239 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700240 )
241 }
haoyue6d80a12020-12-02 16:04:52 -0800242
243 selectionRegistrar.onSelectionUpdateEndCallback = {
244 showSelectionToolbar()
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700245 finishSelectionUpdate()
haoyue6d80a12020-12-02 16:04:52 -0800246 }
haoyu9085c882020-12-08 12:01:06 -0800247
haoyue04245e2021-03-08 14:52:56 -0800248 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
249 if (selectableKey in selectionRegistrar.subselections) {
haoyu9085c882020-12-08 12:01:06 -0800250 // clear the selection range of each Selectable.
251 onRelease()
252 selection = null
253 }
254 }
haoyue04245e2021-03-08 14:52:56 -0800255
256 selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
257 if (
258 selectableKey == selection?.start?.selectableId ||
259 selectableKey == selection?.end?.selectableId
260 ) {
261 // The selectable that contains a selection handle just unsubscribed.
262 // Hide selection handles for now
263 startHandlePosition = null
264 endHandlePosition = null
265 }
266 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100267 }
268
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100269 private fun currentSelectionStartPosition(): Offset? {
270 return selection?.let { selection ->
271 val startSelectable =
272 selectionRegistrar.selectableMap[selection.start.selectableId]
273
274 requireContainerCoordinates().localPositionOf(
275 startSelectable?.getLayoutCoordinates()!!,
276 getAdjustedCoordinates(
277 startSelectable.getHandlePosition(
278 selection = selection,
279 isStartHandle = true
280 )
281 )
282 )
283 }
284 }
285
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100286 private fun updateHandleOffsets() {
287 val selection = selection
288 val containerCoordinates = containerLayoutCoordinates
haoyue04245e2021-03-08 14:52:56 -0800289 val startSelectable = selection?.start?.selectableId?.let {
290 selectionRegistrar.selectableMap[it]
291 }
292 val endSelectable = selection?.end?.selectableId?.let {
293 selectionRegistrar.selectableMap[it]
294 }
295 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
341 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700342 * Iterates over the handlers, gets the selection for each Composable, and merges all the
343 * returned [Selection]s.
344 *
Nader Jawad6df06122020-06-03 15:27:08 -0700345 * @param startPosition [Offset] for the start of the selection
346 * @param endPosition [Offset] for the end of the selection
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800347 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700348 *
haoyue04245e2021-03-08 14:52:56 -0800349 * @return a [Pair] of a [Selection] object which is constructed by combining all
350 * composables that are selected and a [Map] from selectable key to [Selection]s on the
351 * [Selectable] corresponding to the that key.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700352 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800353 // This function is internal for testing purposes.
354 internal fun mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700355 startPosition: Offset,
356 endPosition: Offset,
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700357 adjustment: SelectionAdjustment = SelectionAdjustment.None,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800358 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800359 isStartHandle: Boolean = true
haoyue04245e2021-03-08 14:52:56 -0800360 ): Pair<Selection?, Map<Long, Selection>> {
361 val subselections = mutableMapOf<Long, Selection>()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100362 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
haoyue04245e2021-03-08 14:52:56 -0800363 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700364 val subselection =
365 selectionRegistrar.subselections[selectable.selectableId]
haoyue04245e2021-03-08 14:52:56 -0800366 val selection = selectable.getSelection(
367 startPosition = startPosition,
368 endPosition = endPosition,
369 containerLayoutCoordinates = requireContainerCoordinates(),
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700370 adjustment = adjustment,
371 previousSelection = subselection,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100372 isStartHandle = isStartHandle,
Siyamed Sinir700df8452019-10-22 20:23:58 -0700373 )
haoyue04245e2021-03-08 14:52:56 -0800374 selection?.let { subselections[selectable.selectableId] = it }
375 merge(mergedSelection, selection)
Qingqing Deng247f2b42019-12-12 19:48:37 -0800376 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700377 performHapticFeedbackIfNeeded(newSelection, previousSelection, hapticFeedBack)
haoyue04245e2021-03-08 14:52:56 -0800378 return Pair(newSelection, subselections)
Siyamed Sinir472c3162019-10-21 23:41:00 -0700379 }
380
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700381 internal fun mergeSelections(
382 previousSelection: Selection? = null,
383 selectableId: Long
384 ): Pair<Selection?, Map<Long, Selection>> {
385 val subselections = mutableMapOf<Long, Selection>()
386 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
387 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
388 val selection = if (selectable.selectableId == selectableId)
389 selectable.getSelectAllSelection() else null
390 selection?.let { subselections[selectable.selectableId] = it }
391 merge(mergedSelection, selection)
392 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700393 performHapticFeedbackIfNeeded(previousSelection, newSelection, hapticFeedBack)
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700394 return Pair(newSelection, subselections)
395 }
396
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800397 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100398 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800399 var selectedText: AnnotatedString? = null
400
401 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000402 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800403 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800404 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800405 if (selectable.selectableId != it.start.selectableId &&
406 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800407 selectedText == null
408 ) continue
409
410 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800411 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800412 selection = it
413 )
414 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
415
416 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800417 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
418 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800419 ) break
420 }
421 }
422 return selectedText
423 }
424
Qingqing Dengde023cc2020-04-24 14:23:41 -0700425 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700426 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700427 selectedText?.let { clipboardManager?.setText(it) }
428 }
429
430 /**
431 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
432 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
433 * the copy method as a callback when "copy" is clicked.
434 */
435 internal fun showSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800436 if (hasFocus) {
437 selection?.let {
438 textToolbar?.showMenu(
439 getContentRect(),
440 onCopyRequested = {
441 copy()
442 onRelease()
443 }
444 )
445 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700446 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700447 }
448
haoyue6d80a12020-12-02 16:04:52 -0800449 internal fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800450 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800451 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700452 }
453 }
454
455 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800456 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700457 showSelectionToolbar()
458 }
459 }
460
461 /**
462 * Calculate selected region as [Rect]. The top is the top of the first selected
463 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
464 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
465 */
466 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700467 val selection = selection ?: return Rect.Zero
haoyue04245e2021-03-08 14:52:56 -0800468 val startSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
469 val endSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
470 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
471 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700472
473 val localLayoutCoordinates = containerLayoutCoordinates
474 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000475 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700476 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800477 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700478 selection = selection,
479 isStartHandle = true
480 )
481 )
George Mount77ca2a22020-12-11 17:46:19 +0000482 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700483 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800484 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700485 selection = selection,
486 isStartHandle = false
487 )
488 )
489
490 startOffset = localLayoutCoordinates.localToRoot(startOffset)
491 endOffset = localLayoutCoordinates.localToRoot(endOffset)
492
493 val left = min(startOffset.x, endOffset.x)
494 val right = max(startOffset.x, endOffset.x)
495
George Mount77ca2a22020-12-11 17:46:19 +0000496 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700497 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700498 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700499 0f,
haoyue04245e2021-03-08 14:52:56 -0800500 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700501 )
502 )
503
George Mount77ca2a22020-12-11 17:46:19 +0000504 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700505 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700506 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700507 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800508 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700509 )
510 )
511
512 startTop = localLayoutCoordinates.localToRoot(startTop)
513 endTop = localLayoutCoordinates.localToRoot(endTop)
514
515 val top = min(startTop.y, endTop.y)
Haoyu Zhangd47f1fe2021-07-13 03:10:55 -0700516 val bottom = max(startOffset.y, endOffset.y) + (HandleHeight.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700517
518 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700519 left,
520 top,
521 right,
522 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700523 )
524 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700525 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700526 }
527
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800528 // This is for PressGestureDetector to cancel the selection.
529 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800530 selectionRegistrar.subselections = emptyMap()
haoyue6d80a12020-12-02 16:04:52 -0800531 hideSelectionToolbar()
haoyue04245e2021-03-08 14:52:56 -0800532 if (selection != null) {
533 onSelectionChange(null)
534 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
535 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800536 }
537
Matvei Malkovf770a912021-03-24 18:12:41 +0000538 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver {
539 return object : TextDragObserver {
540 override fun onStart(startPoint: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800541 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700542 val selection = selection!!
haoyue04245e2021-03-08 14:52:56 -0800543 val startSelectable =
544 selectionRegistrar.selectableMap[selection.start.selectableId]
545 val endSelectable =
546 selectionRegistrar.selectableMap[selection.end.selectableId]
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100547 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700548 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100549 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700550 val beginLayoutCoordinates = if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800551 startSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700552 } else {
haoyue04245e2021-03-08 14:52:56 -0800553 endSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700554 }
555
Qingqing Deng6f56a912019-05-13 10:10:37 -0700556 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100557 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700558 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800559 if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800560 startSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800561 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800562 )
563 } else {
haoyue04245e2021-03-08 14:52:56 -0800564 endSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800565 selection = selection, isStartHandle = false
566 )
haoyue6d80a12020-12-02 16:04:52 -0800567 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700568 )
569
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100570 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700571 // selection container coordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000572 dragBeginPosition = requireContainerCoordinates().localPositionOf(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700573 beginLayoutCoordinates,
574 beginCoordinates
575 )
576
577 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700578 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700579 }
580
Matvei Malkovf770a912021-03-24 18:12:41 +0000581 override fun onDrag(delta: Offset) {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700582 val selection = selection!!
Matvei Malkovf770a912021-03-24 18:12:41 +0000583 dragTotalDistance += delta
haoyue04245e2021-03-08 14:52:56 -0800584 val startSelectable =
585 selectionRegistrar.selectableMap[selection.start.selectableId]
586 val endSelectable =
587 selectionRegistrar.selectableMap[selection.end.selectableId]
Siyamed Sinir472c3162019-10-21 23:41:00 -0700588 val currentStart = if (isStartHandle) {
589 dragBeginPosition + dragTotalDistance
590 } else {
George Mount77ca2a22020-12-11 17:46:19 +0000591 requireContainerCoordinates().localPositionOf(
haoyue04245e2021-03-08 14:52:56 -0800592 startSelectable?.getLayoutCoordinates()!!,
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800593 getAdjustedCoordinates(
haoyue04245e2021-03-08 14:52:56 -0800594 startSelectable.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800595 selection = selection,
596 isStartHandle = true
597 )
598 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700599 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700600 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700601
602 val currentEnd = if (isStartHandle) {
George Mount77ca2a22020-12-11 17:46:19 +0000603 requireContainerCoordinates().localPositionOf(
haoyue04245e2021-03-08 14:52:56 -0800604 endSelectable?.getLayoutCoordinates()!!,
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800605 getAdjustedCoordinates(
haoyue04245e2021-03-08 14:52:56 -0800606 endSelectable.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800607 selection = selection,
608 isStartHandle = false
609 )
610 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700611 )
612 } else {
613 dragBeginPosition + dragTotalDistance
614 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700615 updateSelection(
Qingqing Denga5d80952019-10-11 16:46:52 -0700616 startPosition = currentStart,
617 endPosition = currentEnd,
Haoyu Zhang5740343b2021-07-15 11:29:27 -0700618 isStartHandle = isStartHandle,
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700619 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
Qingqing Denga5d80952019-10-11 16:46:52 -0700620 )
Matvei Malkovf770a912021-03-24 18:12:41 +0000621 return
Qingqing Deng6f56a912019-05-13 10:10:37 -0700622 }
haoyue6d80a12020-12-02 16:04:52 -0800623
Matvei Malkovf770a912021-03-24 18:12:41 +0000624 override fun onStop() {
haoyue6d80a12020-12-02 16:04:52 -0800625 showSelectionToolbar()
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700626 finishSelectionUpdate()
haoyue6d80a12020-12-02 16:04:52 -0800627 }
628
629 override fun onCancel() {
630 showSelectionToolbar()
631 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700632 }
633 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700634
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700635 /**
636 * Detect tap without consuming the up event.
637 */
638 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
639 forEachGesture {
640 coroutineScope {
641 awaitPointerEventScope {
642 waitForUpOrCancellation()?.let {
643 onTap(it.position)
644 }
645 }
646 }
647 }
648 }
649
650 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
651 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
652 }
653
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700654 private fun convertToContainerCoordinates(
655 layoutCoordinates: LayoutCoordinates,
656 offset: Offset
657 ): Offset? {
658 val coordinates = containerLayoutCoordinates
659 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000660 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700661 }
662
663 private fun updateSelection(
664 startPosition: Offset?,
665 endPosition: Offset?,
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700666 adjustment: SelectionAdjustment = SelectionAdjustment.None,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700667 isStartHandle: Boolean = true
668 ) {
669 if (startPosition == null || endPosition == null) return
haoyue04245e2021-03-08 14:52:56 -0800670 val (newSelection, newSubselection) = mergeSelections(
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700671 startPosition = startPosition,
672 endPosition = endPosition,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100673 adjustment = adjustment,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100674 previousSelection = selection,
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700675 isStartHandle = isStartHandle
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700676 )
haoyue04245e2021-03-08 14:52:56 -0800677 if (newSelection != selection) {
678 selectionRegistrar.subselections = newSubselection
679 onSelectionChange(newSelection)
680 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700681 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700682
683 private fun finishSelectionUpdate() {
684 selection?.let {
685 val newSelection = Selection(
686 start = it.start.copy(rawOffset = it.start.offset),
687 end = it.end.copy(rawOffset = it.end.offset),
688 handlesCrossed = it.handlesCrossed
689 )
690 onSelectionChange(newSelection)
691 }
692 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700693}
694
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200695internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700696 return lhs?.merge(rhs) ?: rhs
697}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800698
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100699internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
700
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200701internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800702 selectable: Selectable,
703 selection: Selection
704): AnnotatedString {
705 val currentText = selectable.getText()
706
707 return if (
haoyue04245e2021-03-08 14:52:56 -0800708 selectable.selectableId != selection.start.selectableId &&
709 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800710 ) {
711 // Select the full text content if the current selectable is between the
712 // start and the end selectables.
713 currentText
714 } else if (
haoyue04245e2021-03-08 14:52:56 -0800715 selectable.selectableId == selection.start.selectableId &&
716 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800717 ) {
718 // Select partial text content if the current selectable is the start and
719 // the end selectable.
720 if (selection.handlesCrossed) {
721 currentText.subSequence(selection.end.offset, selection.start.offset)
722 } else {
723 currentText.subSequence(selection.start.offset, selection.end.offset)
724 }
haoyue04245e2021-03-08 14:52:56 -0800725 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800726 // Select partial text content if the current selectable is the start
727 // selectable.
728 if (selection.handlesCrossed) {
729 currentText.subSequence(0, selection.start.offset)
730 } else {
731 currentText.subSequence(selection.start.offset, currentText.length)
732 }
733 } else {
734 // Selectable partial text content if the current selectable is the end
735 // selectable.
736 if (selection.handlesCrossed) {
737 currentText.subSequence(selection.end.offset, currentText.length)
738 } else {
739 currentText.subSequence(0, selection.end.offset)
740 }
741 }
742}
haoyue2678c62020-12-09 08:39:12 -0800743
744/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800745internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800746 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
747 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
748 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000749 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800750 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000751 windowToLocal(boundsInWindow.topLeft),
752 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800753 )
754}
755
haoyuac341f02021-01-22 22:01:56 -0800756internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800757 offset.x in left..right && offset.y in top..bottom
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700758
759private fun performHapticFeedbackIfNeeded(
760 previousSelection: Selection?,
761 newSelection: Selection?,
762 hapticFeedback: HapticFeedback?,
763) {
764 if (hapticFeedback == null) {
765 return
766 }
767 if (previousSelection == null) {
768 if (newSelection != null) {
769 hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
770 }
771 return
772 }
773 if (newSelection == null) {
774 // We know previousSelection is not null, so selection is updated.
775 hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
776 return
777 }
778 // We don't care if rawOffset of Selection.AnchorInfo is different, so we only compare the
779 // other fields. If any of them changes, we need to perform hapticFeedback.
780 if (previousSelection.start.offset != newSelection.start.offset ||
781 previousSelection.start.selectableId != newSelection.start.selectableId ||
782 previousSelection.end.offset != newSelection.end.offset ||
783 previousSelection.end.selectableId != previousSelection.end.selectableId
784 ) {
785 hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
786 }
787}