[go: nahoru, domu]

blob: 1a0565e5587793631dc3c35319690a096c8bf110 [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
haoyuc40d02752021-01-25 17:32:47 -080021import androidx.compose.foundation.InteractionState
22import 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
31import androidx.compose.ui.focus.isFocused
32import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010033import androidx.compose.ui.geometry.Offset
34import androidx.compose.ui.geometry.Rect
Matvei Malkov0506aa72021-02-07 17:02:32 +000035import androidx.compose.foundation.legacygestures.DragObserver
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010036import androidx.compose.ui.hapticfeedback.HapticFeedback
37import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010038import androidx.compose.ui.input.key.KeyEvent
39import androidx.compose.ui.input.key.onKeyEvent
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010040import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000041import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080042import androidx.compose.ui.layout.onGloballyPositioned
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 /**
98 * InteractionState corresponds to the focusRequester, it will return trun.
99 */
100 val interactionState: InteractionState = InteractionState()
101
102 /**
103 * Modifier for selection container.
104 */
105 val modifier get() = Modifier
106 .onGloballyPositioned { containerLayoutCoordinates = it }
107 .focusRequester(focusRequester)
108 .onFocusChanged { focusState ->
109 if (!focusState.isFocused) {
110 onRelease()
111 }
112 }
113 .focusable(interactionState = interactionState)
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100114 .onKeyEvent {
115 if (isCopyKeyEvent(it)) {
116 copy()
117 true
118 } else {
119 false
120 }
121 }
haoyuc40d02752021-01-25 17:32:47 -0800122
123 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700124 * Layout Coordinates of the selection container.
125 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100126 var containerLayoutCoordinates: LayoutCoordinates? = null
127 set(value) {
128 field = value
129 updateHandleOffsets()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700130 updateSelectionToolbarPosition()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100131 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700132
133 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700134 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
135 * recalculated.
136 */
Nader Jawad6df06122020-06-03 15:27:08 -0700137 private var dragBeginPosition = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700138
139 /**
140 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
141 * it will be zeroed out.
142 */
Nader Jawad6df06122020-06-03 15:27:08 -0700143 private var dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700144
145 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100146 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
147 * is null when handle shouldn't be displayed.
148 * It is a [State] so reading it during the composition will cause recomposition every time
149 * the position has been changed.
150 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700151 var startHandlePosition by mutableStateOf<Offset?>(
152 null,
153 policy = structuralEqualityPolicy()
154 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100155 private set
156
157 /**
158 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
159 * is null when handle shouldn't be displayed.
160 * It is a [State] so reading it during the composition will cause recomposition every time
161 * the position has been changed.
162 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700163 var endHandlePosition by mutableStateOf<Offset?>(
164 null,
165 policy = structuralEqualityPolicy()
166 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100167 private set
168
169 init {
170 selectionRegistrar.onPositionChangeCallback = {
171 updateHandleOffsets()
haoyue6d80a12020-12-02 16:04:52 -0800172 updateSelectionToolbarPosition()
173 }
174
175 selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
176 updateSelection(
177 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
178 endPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
179 isStartHandle = true,
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100180 longPress = touchMode
haoyue6d80a12020-12-02 16:04:52 -0800181 )
Qingqing Dengde023cc2020-04-24 14:23:41 -0700182 hideSelectionToolbar()
haoyuc40d02752021-01-25 17:32:47 -0800183 focusRequester.requestFocus()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100184 }
haoyue6d80a12020-12-02 16:04:52 -0800185
186 selectionRegistrar.onSelectionUpdateCallback =
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700187 { layoutCoordinates, startPosition, endPosition ->
188 updateSelection(
189 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
190 endPosition = convertToContainerCoordinates(layoutCoordinates, endPosition),
haoyu70aed732020-12-15 07:10:56 -0800191 isStartHandle = false,
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100192 longPress = touchMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700193 )
194 }
haoyue6d80a12020-12-02 16:04:52 -0800195
196 selectionRegistrar.onSelectionUpdateEndCallback = {
197 showSelectionToolbar()
198 }
haoyu9085c882020-12-08 12:01:06 -0800199
200 selectionRegistrar.onSelectableChangeCallback = { selectable ->
201 if (selectable in selectionRegistrar.selectables) {
202 // clear the selection range of each Selectable.
203 onRelease()
204 selection = null
205 }
206 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100207 }
208
209 private fun updateHandleOffsets() {
210 val selection = selection
211 val containerCoordinates = containerLayoutCoordinates
haoyue2678c62020-12-09 08:39:12 -0800212 val startLayoutCoordinates = selection?.start?.selectable?.getLayoutCoordinates()
213 val endLayoutCoordinates = selection?.end?.selectable?.getLayoutCoordinates()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100214
haoyue2678c62020-12-09 08:39:12 -0800215 if (
216 selection == null ||
217 containerCoordinates == null ||
218 !containerCoordinates.isAttached ||
219 startLayoutCoordinates == null ||
220 endLayoutCoordinates == null
221 ) {
222 this.startHandlePosition = null
223 this.endHandlePosition = null
224 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100225 }
haoyue2678c62020-12-09 08:39:12 -0800226
George Mount77ca2a22020-12-11 17:46:19 +0000227 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800228 startLayoutCoordinates,
229 selection.start.selectable.getHandlePosition(
230 selection = selection,
231 isStartHandle = true
232 )
233 )
George Mount77ca2a22020-12-11 17:46:19 +0000234 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800235 endLayoutCoordinates,
236 selection.end.selectable.getHandlePosition(
237 selection = selection,
238 isStartHandle = false
239 )
240 )
241
242 val visibleBounds = containerCoordinates.visibleBounds()
243 this.startHandlePosition =
244 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
245 this.endHandlePosition =
246 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100247 }
248
249 /**
250 * Returns non-nullable [containerLayoutCoordinates].
251 */
252 internal fun requireContainerCoordinates(): LayoutCoordinates {
253 val coordinates = containerLayoutCoordinates
254 require(coordinates != null)
255 require(coordinates.isAttached)
256 return coordinates
257 }
258
259 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700260 * Iterates over the handlers, gets the selection for each Composable, and merges all the
261 * returned [Selection]s.
262 *
Nader Jawad6df06122020-06-03 15:27:08 -0700263 * @param startPosition [Offset] for the start of the selection
264 * @param endPosition [Offset] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800265 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800266 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700267 *
268 * @return [Selection] object which is constructed by combining all Composables that are
269 * selected.
270 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800271 // This function is internal for testing purposes.
272 internal fun mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700273 startPosition: Offset,
274 endPosition: Offset,
Siyamed Sinir0100f122019-11-16 00:23:12 -0800275 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800276 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800277 isStartHandle: Boolean = true
Siyamed Sinir472c3162019-10-21 23:41:00 -0700278 ): Selection? {
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800279
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100280 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
haoyuac341f02021-01-22 22:01:56 -0800281 .fold(null) { mergedSelection: Selection?, handler: Selectable ->
Qingqing Deng247f2b42019-12-12 19:48:37 -0800282 merge(
283 mergedSelection,
284 handler.getSelection(
285 startPosition = startPosition,
286 endPosition = endPosition,
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100287 containerLayoutCoordinates = requireContainerCoordinates(),
Qingqing Deng247f2b42019-12-12 19:48:37 -0800288 longPress = longPress,
289 previousSelection = previousSelection,
290 isStartHandle = isStartHandle
291 )
Siyamed Sinir700df8452019-10-22 20:23:58 -0700292 )
Qingqing Deng247f2b42019-12-12 19:48:37 -0800293 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800294 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
295 HapticFeedbackType.TextHandleMove
296 )
297 return newSelection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700298 }
299
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800300 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100301 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800302 var selectedText: AnnotatedString? = null
303
304 selection?.let {
305 for (handler in selectables) {
306 // Continue if the current selectable is before the selection starts.
307 if (handler != it.start.selectable && handler != it.end.selectable &&
308 selectedText == null
309 ) continue
310
311 val currentSelectedText = getCurrentSelectedText(
312 selectable = handler,
313 selection = it
314 )
315 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
316
317 // Break if the current selectable is the last selected selectable.
318 if (handler == it.end.selectable && !it.handlesCrossed ||
319 handler == it.start.selectable && it.handlesCrossed
320 ) break
321 }
322 }
323 return selectedText
324 }
325
Qingqing Dengde023cc2020-04-24 14:23:41 -0700326 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700327 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700328 selectedText?.let { clipboardManager?.setText(it) }
329 }
330
331 /**
332 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
333 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
334 * the copy method as a callback when "copy" is clicked.
335 */
336 internal fun showSelectionToolbar() {
337 selection?.let {
Qingqing Deng40519572020-07-10 13:25:36 -0700338 textToolbar?.showMenu(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700339 getContentRect(),
Qingqing Deng40519572020-07-10 13:25:36 -0700340 onCopyRequested = {
341 copy()
342 onRelease()
343 }
Qingqing Dengce140562020-04-24 14:46:22 -0700344 )
Qingqing Denga89450d2020-04-03 19:11:49 -0700345 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700346 }
347
haoyue6d80a12020-12-02 16:04:52 -0800348 internal fun hideSelectionToolbar() {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700349 if (textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800350 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700351 }
352 }
353
354 private fun updateSelectionToolbarPosition() {
355 if (textToolbar?.status == TextToolbarStatus.Shown) {
356 showSelectionToolbar()
357 }
358 }
359
360 /**
361 * Calculate selected region as [Rect]. The top is the top of the first selected
362 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
363 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
364 */
365 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700366 val selection = selection ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700367 val startLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700368 selection.start.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700369 val endLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700370 selection.end.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700371
372 val localLayoutCoordinates = containerLayoutCoordinates
373 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000374 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700375 startLayoutCoordinates,
376 selection.start.selectable.getHandlePosition(
377 selection = selection,
378 isStartHandle = true
379 )
380 )
George Mount77ca2a22020-12-11 17:46:19 +0000381 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700382 endLayoutCoordinates,
383 selection.end.selectable.getHandlePosition(
384 selection = selection,
385 isStartHandle = false
386 )
387 )
388
389 startOffset = localLayoutCoordinates.localToRoot(startOffset)
390 endOffset = localLayoutCoordinates.localToRoot(endOffset)
391
392 val left = min(startOffset.x, endOffset.x)
393 val right = max(startOffset.x, endOffset.x)
394
George Mount77ca2a22020-12-11 17:46:19 +0000395 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700396 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700397 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700398 0f,
399 selection.start.selectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700400 )
401 )
402
George Mount77ca2a22020-12-11 17:46:19 +0000403 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700404 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700405 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700406 0.0f,
407 selection.end.selectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700408 )
409 )
410
411 startTop = localLayoutCoordinates.localToRoot(startTop)
412 endTop = localLayoutCoordinates.localToRoot(endTop)
413
414 val top = min(startTop.y, endTop.y)
Nader Jawad16e330a2020-05-21 21:21:01 -0700415 val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700416
417 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700418 left,
419 top,
420 right,
421 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700422 )
423 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700424 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700425 }
426
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800427 // This is for PressGestureDetector to cancel the selection.
428 fun onRelease() {
haoyuc40d02752021-01-25 17:32:47 -0800429 if (containerLayoutCoordinates?.isAttached == true) {
430 // Call mergeSelections with an out of boundary input to inform all text widgets to
431 // cancel their individual selection.
432 mergeSelections(
433 startPosition = Offset(-1f, -1f),
434 endPosition = Offset(-1f, -1f),
435 previousSelection = selection
436 )
437 }
haoyue6d80a12020-12-02 16:04:52 -0800438 hideSelectionToolbar()
Siyamed Sinire810eab2019-11-22 12:36:38 -0800439 if (selection != null) onSelectionChange(null)
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800440 }
441
Siyamed Sinir472c3162019-10-21 23:41:00 -0700442 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700443 return object : DragObserver {
Nader Jawad6df06122020-06-03 15:27:08 -0700444 override fun onStart(downPosition: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800445 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700446 val selection = selection!!
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100447 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700448 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100449 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700450 val beginLayoutCoordinates = if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800451 selection.start.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700452 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800453 selection.end.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700454 }
455
Qingqing Deng6f56a912019-05-13 10:10:37 -0700456 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100457 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700458 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800459 if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800460 selection.start.selectable.getHandlePosition(
461 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800462 )
463 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800464 selection.end.selectable.getHandlePosition(
465 selection = selection, isStartHandle = false
466 )
haoyue6d80a12020-12-02 16:04:52 -0800467 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700468 )
469
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100470 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700471 // selection container coordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000472 dragBeginPosition = requireContainerCoordinates().localPositionOf(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700473 beginLayoutCoordinates,
474 beginCoordinates
475 )
476
477 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700478 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700479 }
480
Nader Jawad6df06122020-06-03 15:27:08 -0700481 override fun onDrag(dragDistance: Offset): Offset {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700482 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700483 dragTotalDistance += dragDistance
484
Siyamed Sinir472c3162019-10-21 23:41:00 -0700485 val currentStart = if (isStartHandle) {
486 dragBeginPosition + dragTotalDistance
487 } else {
George Mount77ca2a22020-12-11 17:46:19 +0000488 requireContainerCoordinates().localPositionOf(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800489 selection.start.selectable.getLayoutCoordinates()!!,
490 getAdjustedCoordinates(
491 selection.start.selectable.getHandlePosition(
492 selection = selection,
493 isStartHandle = true
494 )
495 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700496 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700497 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700498
499 val currentEnd = if (isStartHandle) {
George Mount77ca2a22020-12-11 17:46:19 +0000500 requireContainerCoordinates().localPositionOf(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800501 selection.end.selectable.getLayoutCoordinates()!!,
502 getAdjustedCoordinates(
503 selection.end.selectable.getHandlePosition(
504 selection = selection,
505 isStartHandle = false
506 )
507 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700508 )
509 } else {
510 dragBeginPosition + dragTotalDistance
511 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700512 updateSelection(
Qingqing Denga5d80952019-10-11 16:46:52 -0700513 startPosition = currentStart,
514 endPosition = currentEnd,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800515 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700516 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700517 return dragDistance
518 }
haoyue6d80a12020-12-02 16:04:52 -0800519
520 override fun onStop(velocity: Offset) {
521 showSelectionToolbar()
522 }
523
524 override fun onCancel() {
525 showSelectionToolbar()
526 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700527 }
528 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700529
530 private fun convertToContainerCoordinates(
531 layoutCoordinates: LayoutCoordinates,
532 offset: Offset
533 ): Offset? {
534 val coordinates = containerLayoutCoordinates
535 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000536 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700537 }
538
539 private fun updateSelection(
540 startPosition: Offset?,
541 endPosition: Offset?,
542 longPress: Boolean = false,
543 isStartHandle: Boolean = true
544 ) {
545 if (startPosition == null || endPosition == null) return
546 val newSelection = mergeSelections(
547 startPosition = startPosition,
548 endPosition = endPosition,
549 longPress = longPress,
550 isStartHandle = isStartHandle,
551 previousSelection = selection
552 )
553 if (newSelection != selection) onSelectionChange(newSelection)
554 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700555}
556
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200557internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700558 return lhs?.merge(rhs) ?: rhs
559}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800560
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100561internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
562
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200563internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800564 selectable: Selectable,
565 selection: Selection
566): AnnotatedString {
567 val currentText = selectable.getText()
568
569 return if (
570 selectable != selection.start.selectable &&
571 selectable != selection.end.selectable
572 ) {
573 // Select the full text content if the current selectable is between the
574 // start and the end selectables.
575 currentText
576 } else if (
577 selectable == selection.start.selectable &&
578 selectable == selection.end.selectable
579 ) {
580 // Select partial text content if the current selectable is the start and
581 // the end selectable.
582 if (selection.handlesCrossed) {
583 currentText.subSequence(selection.end.offset, selection.start.offset)
584 } else {
585 currentText.subSequence(selection.start.offset, selection.end.offset)
586 }
587 } else if (selectable == selection.start.selectable) {
588 // Select partial text content if the current selectable is the start
589 // selectable.
590 if (selection.handlesCrossed) {
591 currentText.subSequence(0, selection.start.offset)
592 } else {
593 currentText.subSequence(selection.start.offset, currentText.length)
594 }
595 } else {
596 // Selectable partial text content if the current selectable is the end
597 // selectable.
598 if (selection.handlesCrossed) {
599 currentText.subSequence(selection.end.offset, currentText.length)
600 } else {
601 currentText.subSequence(0, selection.end.offset)
602 }
603 }
604}
haoyue2678c62020-12-09 08:39:12 -0800605
606/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800607internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800608 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
609 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
610 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000611 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800612 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000613 windowToLocal(boundsInWindow.topLeft),
614 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800615 )
616}
617
haoyuac341f02021-01-22 22:01:56 -0800618internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800619 offset.x in left..right && offset.y in top..bottom