[go: nahoru, domu]

blob: 1774fd4b3106ec1310b06113147882bb534b4709 [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
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010023import androidx.compose.runtime.State
24import androidx.compose.runtime.getValue
25import androidx.compose.runtime.mutableStateOf
26import androidx.compose.runtime.setValue
27import androidx.compose.runtime.structuralEqualityPolicy
haoyuc40d02752021-01-25 17:32:47 -080028import androidx.compose.ui.Modifier
29import androidx.compose.ui.focus.FocusRequester
30import androidx.compose.ui.focus.focusRequester
haoyuc40d02752021-01-25 17:32:47 -080031import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010032import androidx.compose.ui.geometry.Offset
33import androidx.compose.ui.geometry.Rect
Matvei Malkovf770a912021-03-24 18:12:41 +000034import androidx.compose.foundation.text.TextDragObserver
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010035import androidx.compose.ui.hapticfeedback.HapticFeedback
36import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010037import androidx.compose.ui.input.key.KeyEvent
38import androidx.compose.ui.input.key.onKeyEvent
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010039import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000040import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080041import androidx.compose.ui.layout.onGloballyPositioned
haoyu7ad5ea32021-03-22 10:36:35 -070042import androidx.compose.ui.layout.positionInWindow
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010043import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010044import androidx.compose.ui.platform.TextToolbar
45import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010046import androidx.compose.ui.text.AnnotatedString
Nader Jawade6a9b332020-05-21 13:49:20 -070047import kotlin.math.max
48import kotlin.math.min
Qingqing Deng6f56a912019-05-13 10:10:37 -070049
Qingqing Deng35f97ea2019-09-18 19:24:37 -070050/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010051 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070052 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070053internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070054 /**
55 * The current selection.
56 */
57 var selection: Selection? = null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010058 set(value) {
59 field = value
haoyu9085c882020-12-08 12:01:06 -080060 if (value != null) {
61 updateHandleOffsets()
62 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010063 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070064
65 /**
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010066 * Is touch mode active
67 */
68 var touchMode: Boolean = true
69
70 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070071 * The manager will invoke this every time it comes to the conclusion that the selection should
72 * change. The expectation is that this callback will end up causing `setSelection` to get
73 * called. This is what makes this a "controlled component".
74 */
75 var onSelectionChange: (Selection?) -> Unit = {}
76
77 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080078 * [HapticFeedback] handle to perform haptic feedback.
79 */
80 var hapticFeedBack: HapticFeedback? = null
81
82 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070083 * [ClipboardManager] to perform clipboard features.
84 */
85 var clipboardManager: ClipboardManager? = null
86
87 /**
Qingqing Denga89450d2020-04-03 19:11:49 -070088 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
89 */
90 var textToolbar: TextToolbar? = null
91
92 /**
haoyuc40d02752021-01-25 17:32:47 -080093 * Focus requester used to request focus when selection becomes active.
94 */
95 var focusRequester: FocusRequester = FocusRequester()
96
97 /**
haoyu3c3fb452021-02-18 01:01:14 -080098 * Return true if the corresponding SelectionContainer is focused.
haoyuc40d02752021-01-25 17:32:47 -080099 */
haoyu3c3fb452021-02-18 01:01:14 -0800100 var hasFocus: Boolean by mutableStateOf(false)
haoyuc40d02752021-01-25 17:32:47 -0800101
102 /**
103 * Modifier for selection container.
104 */
105 val modifier get() = Modifier
106 .onGloballyPositioned { containerLayoutCoordinates = it }
107 .focusRequester(focusRequester)
108 .onFocusChanged { focusState ->
haoyu3c3fb452021-02-18 01:01:14 -0800109 if (!focusState.isFocused && hasFocus) {
haoyuc40d02752021-01-25 17:32:47 -0800110 onRelease()
111 }
haoyu3c3fb452021-02-18 01:01:14 -0800112 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800113 }
haoyu3c3fb452021-02-18 01:01:14 -0800114 .focusable()
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100115 .onKeyEvent {
116 if (isCopyKeyEvent(it)) {
117 copy()
118 true
119 } else {
120 false
121 }
122 }
haoyuc40d02752021-01-25 17:32:47 -0800123
haoyu7ad5ea32021-03-22 10:36:35 -0700124 private var previousPosition: Offset? = null
haoyuc40d02752021-01-25 17:32:47 -0800125 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700126 * Layout Coordinates of the selection container.
127 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100128 var containerLayoutCoordinates: LayoutCoordinates? = null
129 set(value) {
130 field = value
haoyu2c6e9842021-03-30 17:39:04 -0700131 if (hasFocus && selection != null) {
132 val positionInWindow = value?.positionInWindow()
133 if (previousPosition != positionInWindow) {
134 previousPosition = positionInWindow
135 updateHandleOffsets()
136 updateSelectionToolbarPosition()
137 }
haoyu3c3fb452021-02-18 01:01:14 -0800138 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100139 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700140
141 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700142 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
143 * recalculated.
144 */
Nader Jawad6df06122020-06-03 15:27:08 -0700145 private var dragBeginPosition = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700146
147 /**
148 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
149 * it will be zeroed out.
150 */
Nader Jawad6df06122020-06-03 15:27:08 -0700151 private var dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700152
153 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100154 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
155 * is null when handle shouldn't be displayed.
156 * It is a [State] so reading it during the composition will cause recomposition every time
157 * the position has been changed.
158 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700159 var startHandlePosition by mutableStateOf<Offset?>(
160 null,
161 policy = structuralEqualityPolicy()
162 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100163 private set
164
165 /**
166 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
167 * is null when handle shouldn't be displayed.
168 * It is a [State] so reading it during the composition will cause recomposition every time
169 * the position has been changed.
170 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700171 var endHandlePosition by mutableStateOf<Offset?>(
172 null,
173 policy = structuralEqualityPolicy()
174 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100175 private set
176
177 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700178 selectionRegistrar.onPositionChangeCallback = { selectableId ->
179 if (
180 selectableId == selection?.start?.selectableId ||
181 selectableId == selection?.end?.selectableId
182 ) {
183 updateHandleOffsets()
184 updateSelectionToolbarPosition()
185 }
haoyue6d80a12020-12-02 16:04:52 -0800186 }
187
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100188 selectionRegistrar.onSelectionUpdateStartCallback =
189 { layoutCoordinates, startPosition, selectionMode ->
190 val startPositionInContainer = convertToContainerCoordinates(
191 layoutCoordinates,
192 startPosition
193 )
194
195 updateSelection(
196 startPosition = startPositionInContainer,
197 endPosition = startPositionInContainer,
198 isStartHandle = true,
199 adjustment = selectionMode
200 )
201
202 focusRequester.requestFocus()
203 hideSelectionToolbar()
204 }
haoyue6d80a12020-12-02 16:04:52 -0800205
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700206 selectionRegistrar.onSelectionUpdateSelectAll =
207 { selectableId ->
208 val (newSelection, newSubselection) = mergeSelections(
209 selectableId = selectableId,
210 previousSelection = selection,
211 )
212 if (newSelection != selection) {
213 selectionRegistrar.subselections = newSubselection
214 onSelectionChange(newSelection)
215 }
216
217 focusRequester.requestFocus()
218 hideSelectionToolbar()
219 }
220
haoyue6d80a12020-12-02 16:04:52 -0800221 selectionRegistrar.onSelectionUpdateCallback =
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100222 { layoutCoordinates, startPosition, endPosition, selectionMode ->
223 val startPositionOrCurrent = if (startPosition == null) {
224 currentSelectionStartPosition()
225 } else {
226 convertToContainerCoordinates(layoutCoordinates, startPosition)
227 }
228
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700229 updateSelection(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100230 startPosition = startPositionOrCurrent,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700231 endPosition = convertToContainerCoordinates(layoutCoordinates, endPosition),
haoyu70aed732020-12-15 07:10:56 -0800232 isStartHandle = false,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700233 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700234 )
235 }
haoyue6d80a12020-12-02 16:04:52 -0800236
237 selectionRegistrar.onSelectionUpdateEndCallback = {
238 showSelectionToolbar()
239 }
haoyu9085c882020-12-08 12:01:06 -0800240
haoyue04245e2021-03-08 14:52:56 -0800241 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
242 if (selectableKey in selectionRegistrar.subselections) {
haoyu9085c882020-12-08 12:01:06 -0800243 // clear the selection range of each Selectable.
244 onRelease()
245 selection = null
246 }
247 }
haoyue04245e2021-03-08 14:52:56 -0800248
249 selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
250 if (
251 selectableKey == selection?.start?.selectableId ||
252 selectableKey == selection?.end?.selectableId
253 ) {
254 // The selectable that contains a selection handle just unsubscribed.
255 // Hide selection handles for now
256 startHandlePosition = null
257 endHandlePosition = null
258 }
259 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100260 }
261
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100262 private fun currentSelectionStartPosition(): Offset? {
263 return selection?.let { selection ->
264 val startSelectable =
265 selectionRegistrar.selectableMap[selection.start.selectableId]
266
267 requireContainerCoordinates().localPositionOf(
268 startSelectable?.getLayoutCoordinates()!!,
269 getAdjustedCoordinates(
270 startSelectable.getHandlePosition(
271 selection = selection,
272 isStartHandle = true
273 )
274 )
275 )
276 }
277 }
278
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100279 private fun updateHandleOffsets() {
280 val selection = selection
281 val containerCoordinates = containerLayoutCoordinates
haoyue04245e2021-03-08 14:52:56 -0800282 val startSelectable = selection?.start?.selectableId?.let {
283 selectionRegistrar.selectableMap[it]
284 }
285 val endSelectable = selection?.end?.selectableId?.let {
286 selectionRegistrar.selectableMap[it]
287 }
288 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
289 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
haoyue2678c62020-12-09 08:39:12 -0800290 if (
291 selection == null ||
292 containerCoordinates == null ||
293 !containerCoordinates.isAttached ||
294 startLayoutCoordinates == null ||
295 endLayoutCoordinates == null
296 ) {
297 this.startHandlePosition = null
298 this.endHandlePosition = null
299 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100300 }
haoyue2678c62020-12-09 08:39:12 -0800301
George Mount77ca2a22020-12-11 17:46:19 +0000302 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800303 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800304 startSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800305 selection = selection,
306 isStartHandle = true
307 )
308 )
George Mount77ca2a22020-12-11 17:46:19 +0000309 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800310 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800311 endSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800312 selection = selection,
313 isStartHandle = false
314 )
315 )
316
317 val visibleBounds = containerCoordinates.visibleBounds()
318 this.startHandlePosition =
319 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
320 this.endHandlePosition =
321 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100322 }
323
324 /**
325 * Returns non-nullable [containerLayoutCoordinates].
326 */
327 internal fun requireContainerCoordinates(): LayoutCoordinates {
328 val coordinates = containerLayoutCoordinates
329 require(coordinates != null)
330 require(coordinates.isAttached)
331 return coordinates
332 }
333
334 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700335 * Iterates over the handlers, gets the selection for each Composable, and merges all the
336 * returned [Selection]s.
337 *
Nader Jawad6df06122020-06-03 15:27:08 -0700338 * @param startPosition [Offset] for the start of the selection
339 * @param endPosition [Offset] for the end of the selection
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800340 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700341 *
haoyue04245e2021-03-08 14:52:56 -0800342 * @return a [Pair] of a [Selection] object which is constructed by combining all
343 * composables that are selected and a [Map] from selectable key to [Selection]s on the
344 * [Selectable] corresponding to the that key.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700345 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800346 // This function is internal for testing purposes.
347 internal fun mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700348 startPosition: Offset,
349 endPosition: Offset,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100350 adjustment: SelectionAdjustment = SelectionAdjustment.NONE,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800351 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800352 isStartHandle: Boolean = true
haoyue04245e2021-03-08 14:52:56 -0800353 ): Pair<Selection?, Map<Long, Selection>> {
354 val subselections = mutableMapOf<Long, Selection>()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100355 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
haoyue04245e2021-03-08 14:52:56 -0800356 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
357 val selection = selectable.getSelection(
358 startPosition = startPosition,
359 endPosition = endPosition,
360 containerLayoutCoordinates = requireContainerCoordinates(),
haoyue04245e2021-03-08 14:52:56 -0800361 previousSelection = previousSelection,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100362 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700363 adjustment = adjustment
Siyamed Sinir700df8452019-10-22 20:23:58 -0700364 )
haoyue04245e2021-03-08 14:52:56 -0800365 selection?.let { subselections[selectable.selectableId] = it }
366 merge(mergedSelection, selection)
Qingqing Deng247f2b42019-12-12 19:48:37 -0800367 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800368 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
369 HapticFeedbackType.TextHandleMove
370 )
haoyue04245e2021-03-08 14:52:56 -0800371 return Pair(newSelection, subselections)
Siyamed Sinir472c3162019-10-21 23:41:00 -0700372 }
373
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700374 internal fun mergeSelections(
375 previousSelection: Selection? = null,
376 selectableId: Long
377 ): Pair<Selection?, Map<Long, Selection>> {
378 val subselections = mutableMapOf<Long, Selection>()
379 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
380 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
381 val selection = if (selectable.selectableId == selectableId)
382 selectable.getSelectAllSelection() else null
383 selection?.let { subselections[selectable.selectableId] = it }
384 merge(mergedSelection, selection)
385 }
386 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
387 HapticFeedbackType.TextHandleMove
388 )
389 return Pair(newSelection, subselections)
390 }
391
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800392 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100393 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800394 var selectedText: AnnotatedString? = null
395
396 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000397 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800398 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800399 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800400 if (selectable.selectableId != it.start.selectableId &&
401 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800402 selectedText == null
403 ) continue
404
405 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800406 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800407 selection = it
408 )
409 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
410
411 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800412 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
413 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800414 ) break
415 }
416 }
417 return selectedText
418 }
419
Qingqing Dengde023cc2020-04-24 14:23:41 -0700420 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700421 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700422 selectedText?.let { clipboardManager?.setText(it) }
423 }
424
425 /**
426 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
427 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
428 * the copy method as a callback when "copy" is clicked.
429 */
430 internal fun showSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800431 if (hasFocus) {
432 selection?.let {
433 textToolbar?.showMenu(
434 getContentRect(),
435 onCopyRequested = {
436 copy()
437 onRelease()
438 }
439 )
440 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700441 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700442 }
443
haoyue6d80a12020-12-02 16:04:52 -0800444 internal fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800445 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800446 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700447 }
448 }
449
450 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800451 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700452 showSelectionToolbar()
453 }
454 }
455
456 /**
457 * Calculate selected region as [Rect]. The top is the top of the first selected
458 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
459 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
460 */
461 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700462 val selection = selection ?: return Rect.Zero
haoyue04245e2021-03-08 14:52:56 -0800463 val startSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
464 val endSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
465 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
466 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700467
468 val localLayoutCoordinates = containerLayoutCoordinates
469 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000470 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700471 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800472 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700473 selection = selection,
474 isStartHandle = true
475 )
476 )
George Mount77ca2a22020-12-11 17:46:19 +0000477 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700478 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800479 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700480 selection = selection,
481 isStartHandle = false
482 )
483 )
484
485 startOffset = localLayoutCoordinates.localToRoot(startOffset)
486 endOffset = localLayoutCoordinates.localToRoot(endOffset)
487
488 val left = min(startOffset.x, endOffset.x)
489 val right = max(startOffset.x, endOffset.x)
490
George Mount77ca2a22020-12-11 17:46:19 +0000491 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700492 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700493 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700494 0f,
haoyue04245e2021-03-08 14:52:56 -0800495 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700496 )
497 )
498
George Mount77ca2a22020-12-11 17:46:19 +0000499 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700500 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700501 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700502 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800503 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700504 )
505 )
506
507 startTop = localLayoutCoordinates.localToRoot(startTop)
508 endTop = localLayoutCoordinates.localToRoot(endTop)
509
510 val top = min(startTop.y, endTop.y)
Nader Jawad16e330a2020-05-21 21:21:01 -0700511 val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700512
513 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700514 left,
515 top,
516 right,
517 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700518 )
519 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700520 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700521 }
522
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800523 // This is for PressGestureDetector to cancel the selection.
524 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800525 selectionRegistrar.subselections = emptyMap()
haoyue6d80a12020-12-02 16:04:52 -0800526 hideSelectionToolbar()
haoyue04245e2021-03-08 14:52:56 -0800527 if (selection != null) {
528 onSelectionChange(null)
529 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
530 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800531 }
532
Matvei Malkovf770a912021-03-24 18:12:41 +0000533 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver {
534 return object : TextDragObserver {
535 override fun onStart(startPoint: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800536 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700537 val selection = selection!!
haoyue04245e2021-03-08 14:52:56 -0800538 val startSelectable =
539 selectionRegistrar.selectableMap[selection.start.selectableId]
540 val endSelectable =
541 selectionRegistrar.selectableMap[selection.end.selectableId]
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100542 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700543 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100544 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700545 val beginLayoutCoordinates = if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800546 startSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700547 } else {
haoyue04245e2021-03-08 14:52:56 -0800548 endSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700549 }
550
Qingqing Deng6f56a912019-05-13 10:10:37 -0700551 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100552 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700553 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800554 if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800555 startSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800556 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800557 )
558 } else {
haoyue04245e2021-03-08 14:52:56 -0800559 endSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800560 selection = selection, isStartHandle = false
561 )
haoyue6d80a12020-12-02 16:04:52 -0800562 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700563 )
564
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100565 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700566 // selection container coordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000567 dragBeginPosition = requireContainerCoordinates().localPositionOf(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700568 beginLayoutCoordinates,
569 beginCoordinates
570 )
571
572 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700573 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700574 }
575
Matvei Malkovf770a912021-03-24 18:12:41 +0000576 override fun onDrag(delta: Offset) {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700577 val selection = selection!!
Matvei Malkovf770a912021-03-24 18:12:41 +0000578 dragTotalDistance += delta
haoyue04245e2021-03-08 14:52:56 -0800579 val startSelectable =
580 selectionRegistrar.selectableMap[selection.start.selectableId]
581 val endSelectable =
582 selectionRegistrar.selectableMap[selection.end.selectableId]
Siyamed Sinir472c3162019-10-21 23:41:00 -0700583 val currentStart = if (isStartHandle) {
584 dragBeginPosition + dragTotalDistance
585 } else {
George Mount77ca2a22020-12-11 17:46:19 +0000586 requireContainerCoordinates().localPositionOf(
haoyue04245e2021-03-08 14:52:56 -0800587 startSelectable?.getLayoutCoordinates()!!,
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800588 getAdjustedCoordinates(
haoyue04245e2021-03-08 14:52:56 -0800589 startSelectable.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800590 selection = selection,
591 isStartHandle = true
592 )
593 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700594 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700595 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700596
597 val currentEnd = if (isStartHandle) {
George Mount77ca2a22020-12-11 17:46:19 +0000598 requireContainerCoordinates().localPositionOf(
haoyue04245e2021-03-08 14:52:56 -0800599 endSelectable?.getLayoutCoordinates()!!,
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800600 getAdjustedCoordinates(
haoyue04245e2021-03-08 14:52:56 -0800601 endSelectable.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800602 selection = selection,
603 isStartHandle = false
604 )
605 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700606 )
607 } else {
608 dragBeginPosition + dragTotalDistance
609 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700610 updateSelection(
Qingqing Denga5d80952019-10-11 16:46:52 -0700611 startPosition = currentStart,
612 endPosition = currentEnd,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800613 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700614 )
Matvei Malkovf770a912021-03-24 18:12:41 +0000615 return
Qingqing Deng6f56a912019-05-13 10:10:37 -0700616 }
haoyue6d80a12020-12-02 16:04:52 -0800617
Matvei Malkovf770a912021-03-24 18:12:41 +0000618 override fun onStop() {
haoyue6d80a12020-12-02 16:04:52 -0800619 showSelectionToolbar()
620 }
621
622 override fun onCancel() {
623 showSelectionToolbar()
624 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700625 }
626 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700627
628 private fun convertToContainerCoordinates(
629 layoutCoordinates: LayoutCoordinates,
630 offset: Offset
631 ): Offset? {
632 val coordinates = containerLayoutCoordinates
633 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000634 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700635 }
636
637 private fun updateSelection(
638 startPosition: Offset?,
639 endPosition: Offset?,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100640 adjustment: SelectionAdjustment = SelectionAdjustment.NONE,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700641 isStartHandle: Boolean = true
642 ) {
643 if (startPosition == null || endPosition == null) return
haoyue04245e2021-03-08 14:52:56 -0800644 val (newSelection, newSubselection) = mergeSelections(
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700645 startPosition = startPosition,
646 endPosition = endPosition,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100647 adjustment = adjustment,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700648 isStartHandle = isStartHandle,
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100649 previousSelection = selection,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700650 )
haoyue04245e2021-03-08 14:52:56 -0800651 if (newSelection != selection) {
652 selectionRegistrar.subselections = newSubselection
653 onSelectionChange(newSelection)
654 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700655 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700656}
657
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200658internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700659 return lhs?.merge(rhs) ?: rhs
660}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800661
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100662internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
663
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200664internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800665 selectable: Selectable,
666 selection: Selection
667): AnnotatedString {
668 val currentText = selectable.getText()
669
670 return if (
haoyue04245e2021-03-08 14:52:56 -0800671 selectable.selectableId != selection.start.selectableId &&
672 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800673 ) {
674 // Select the full text content if the current selectable is between the
675 // start and the end selectables.
676 currentText
677 } else if (
haoyue04245e2021-03-08 14:52:56 -0800678 selectable.selectableId == selection.start.selectableId &&
679 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800680 ) {
681 // Select partial text content if the current selectable is the start and
682 // the end selectable.
683 if (selection.handlesCrossed) {
684 currentText.subSequence(selection.end.offset, selection.start.offset)
685 } else {
686 currentText.subSequence(selection.start.offset, selection.end.offset)
687 }
haoyue04245e2021-03-08 14:52:56 -0800688 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800689 // Select partial text content if the current selectable is the start
690 // selectable.
691 if (selection.handlesCrossed) {
692 currentText.subSequence(0, selection.start.offset)
693 } else {
694 currentText.subSequence(selection.start.offset, currentText.length)
695 }
696 } else {
697 // Selectable partial text content if the current selectable is the end
698 // selectable.
699 if (selection.handlesCrossed) {
700 currentText.subSequence(selection.end.offset, currentText.length)
701 } else {
702 currentText.subSequence(0, selection.end.offset)
703 }
704 }
705}
haoyue2678c62020-12-09 08:39:12 -0800706
707/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800708internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800709 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
710 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
711 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000712 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800713 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000714 windowToLocal(boundsInWindow.topLeft),
715 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800716 )
717}
718
haoyuac341f02021-01-22 22:01:56 -0800719internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800720 offset.x in left..right && offset.y in top..bottom