[go: nahoru, domu]

blob: 4d9c32fe47492217ddb551c95720d141489f0d11 [file] [log] [blame]
Qingqing Deng6f56a912019-05-13 10:10:37 -07001/*
2 * Copyright 2019 The Android Open Source Project
3 *
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
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010017package androidx.compose.ui.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070018
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010019import androidx.compose.runtime.State
20import androidx.compose.runtime.getValue
21import androidx.compose.runtime.mutableStateOf
22import androidx.compose.runtime.setValue
23import androidx.compose.runtime.structuralEqualityPolicy
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010024import androidx.compose.ui.geometry.Offset
25import androidx.compose.ui.geometry.Rect
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010026import androidx.compose.ui.gesture.DragObserver
27import androidx.compose.ui.gesture.LongPressDragObserver
28import androidx.compose.ui.hapticfeedback.HapticFeedback
29import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010030import androidx.compose.ui.layout.LayoutCoordinates
31import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010032import androidx.compose.ui.platform.TextToolbar
33import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010034import androidx.compose.ui.text.AnnotatedString
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010035import androidx.compose.ui.text.InternalTextApi
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010036import androidx.compose.ui.text.length
37import androidx.compose.ui.text.subSequence
Nader Jawade6a9b332020-05-21 13:49:20 -070038import kotlin.math.max
39import kotlin.math.min
Qingqing Deng6f56a912019-05-13 10:10:37 -070040
Qingqing Deng35f97ea2019-09-18 19:24:37 -070041/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010042 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070043 */
Qingqing Deng48b32a02020-06-22 17:06:27 -070044@OptIn(InternalTextApi::class)
Siyamed Sinir700df8452019-10-22 20:23:58 -070045internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070046 /**
47 * The current selection.
48 */
49 var selection: Selection? = null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010050 set(value) {
51 field = value
52 updateHandleOffsets()
Qingqing Dengde023cc2020-04-24 14:23:41 -070053 hideSelectionToolbar()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010054 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070055
56 /**
57 * The manager will invoke this every time it comes to the conclusion that the selection should
58 * change. The expectation is that this callback will end up causing `setSelection` to get
59 * called. This is what makes this a "controlled component".
60 */
61 var onSelectionChange: (Selection?) -> Unit = {}
62
63 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080064 * [HapticFeedback] handle to perform haptic feedback.
65 */
66 var hapticFeedBack: HapticFeedback? = null
67
68 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070069 * [ClipboardManager] to perform clipboard features.
70 */
71 var clipboardManager: ClipboardManager? = null
72
73 /**
Qingqing Denga89450d2020-04-03 19:11:49 -070074 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
75 */
76 var textToolbar: TextToolbar? = null
77
78 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070079 * Layout Coordinates of the selection container.
80 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010081 var containerLayoutCoordinates: LayoutCoordinates? = null
82 set(value) {
83 field = value
84 updateHandleOffsets()
Qingqing Dengde023cc2020-04-24 14:23:41 -070085 updateSelectionToolbarPosition()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010086 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070087
88 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070089 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
90 * recalculated.
91 */
Nader Jawad6df06122020-06-03 15:27:08 -070092 private var dragBeginPosition = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -070093
94 /**
95 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
96 * it will be zeroed out.
97 */
Nader Jawad6df06122020-06-03 15:27:08 -070098 private var dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -070099
100 /**
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700101 * A flag to check if the selection start or end handle is being dragged.
102 * If this value is true, then onPress will not select any text.
103 * This value will be set to true when either handle is being dragged, and be reset to false
104 * when the dragging is stopped.
105 */
106 private var draggingHandle = false
107
108 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100109 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
110 * is null when handle shouldn't be displayed.
111 * It is a [State] so reading it during the composition will cause recomposition every time
112 * the position has been changed.
113 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700114 var startHandlePosition by mutableStateOf<Offset?>(
115 null,
116 policy = structuralEqualityPolicy()
117 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100118 private set
119
120 /**
121 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
122 * is null when handle shouldn't be displayed.
123 * It is a [State] so reading it during the composition will cause recomposition every time
124 * the position has been changed.
125 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700126 var endHandlePosition by mutableStateOf<Offset?>(
127 null,
128 policy = structuralEqualityPolicy()
129 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100130 private set
131
132 init {
133 selectionRegistrar.onPositionChangeCallback = {
134 updateHandleOffsets()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700135 hideSelectionToolbar()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100136 }
137 }
138
139 private fun updateHandleOffsets() {
140 val selection = selection
141 val containerCoordinates = containerLayoutCoordinates
142 if (selection != null && containerCoordinates != null && containerCoordinates.isAttached) {
143 val startLayoutCoordinates = selection.start.selectable.getLayoutCoordinates()
144 val endLayoutCoordinates = selection.end.selectable.getLayoutCoordinates()
145
146 if (startLayoutCoordinates != null && endLayoutCoordinates != null) {
147 startHandlePosition = containerCoordinates.childToLocal(
148 startLayoutCoordinates,
149 selection.start.selectable.getHandlePosition(
150 selection = selection,
151 isStartHandle = true
152 )
153 )
154 endHandlePosition = containerCoordinates.childToLocal(
155 endLayoutCoordinates,
156 selection.end.selectable.getHandlePosition(
157 selection = selection,
158 isStartHandle = false
159 )
160 )
161 return
162 }
163 }
164 startHandlePosition = null
165 endHandlePosition = null
166 }
167
168 /**
169 * Returns non-nullable [containerLayoutCoordinates].
170 */
171 internal fun requireContainerCoordinates(): LayoutCoordinates {
172 val coordinates = containerLayoutCoordinates
173 require(coordinates != null)
174 require(coordinates.isAttached)
175 return coordinates
176 }
177
178 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700179 * Iterates over the handlers, gets the selection for each Composable, and merges all the
180 * returned [Selection]s.
181 *
Nader Jawad6df06122020-06-03 15:27:08 -0700182 * @param startPosition [Offset] for the start of the selection
183 * @param endPosition [Offset] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800184 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800185 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700186 *
187 * @return [Selection] object which is constructed by combining all Composables that are
188 * selected.
189 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800190 // This function is internal for testing purposes.
191 internal fun mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700192 startPosition: Offset,
193 endPosition: Offset,
Siyamed Sinir0100f122019-11-16 00:23:12 -0800194 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800195 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800196 isStartHandle: Boolean = true
Siyamed Sinir472c3162019-10-21 23:41:00 -0700197 ): Selection? {
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800198
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100199 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng247f2b42019-12-12 19:48:37 -0800200 .fold(null) { mergedSelection: Selection?,
Jeff Gastona3bbb7e2020-09-18 15:11:15 -0400201 handler: Selectable ->
Qingqing Deng247f2b42019-12-12 19:48:37 -0800202 merge(
203 mergedSelection,
204 handler.getSelection(
205 startPosition = startPosition,
206 endPosition = endPosition,
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100207 containerLayoutCoordinates = requireContainerCoordinates(),
Qingqing Deng247f2b42019-12-12 19:48:37 -0800208 longPress = longPress,
209 previousSelection = previousSelection,
210 isStartHandle = isStartHandle
211 )
Siyamed Sinir700df8452019-10-22 20:23:58 -0700212 )
Qingqing Deng247f2b42019-12-12 19:48:37 -0800213 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800214 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
215 HapticFeedbackType.TextHandleMove
216 )
217 return newSelection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700218 }
219
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800220 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100221 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800222 var selectedText: AnnotatedString? = null
223
224 selection?.let {
225 for (handler in selectables) {
226 // Continue if the current selectable is before the selection starts.
227 if (handler != it.start.selectable && handler != it.end.selectable &&
228 selectedText == null
229 ) continue
230
231 val currentSelectedText = getCurrentSelectedText(
232 selectable = handler,
233 selection = it
234 )
235 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
236
237 // Break if the current selectable is the last selected selectable.
238 if (handler == it.end.selectable && !it.handlesCrossed ||
239 handler == it.start.selectable && it.handlesCrossed
240 ) break
241 }
242 }
243 return selectedText
244 }
245
Qingqing Dengde023cc2020-04-24 14:23:41 -0700246 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700247 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700248 selectedText?.let { clipboardManager?.setText(it) }
249 }
250
251 /**
252 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
253 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
254 * the copy method as a callback when "copy" is clicked.
255 */
256 internal fun showSelectionToolbar() {
257 selection?.let {
Qingqing Deng40519572020-07-10 13:25:36 -0700258 textToolbar?.showMenu(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700259 getContentRect(),
Qingqing Deng40519572020-07-10 13:25:36 -0700260 onCopyRequested = {
261 copy()
262 onRelease()
263 }
Qingqing Dengce140562020-04-24 14:46:22 -0700264 )
Qingqing Denga89450d2020-04-03 19:11:49 -0700265 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700266 }
267
Qingqing Dengde023cc2020-04-24 14:23:41 -0700268 private fun hideSelectionToolbar() {
269 if (textToolbar?.status == TextToolbarStatus.Shown) {
270 val selection = selection
271 if (selection == null) {
272 textToolbar?.hide()
273 }
274 }
275 }
276
277 private fun updateSelectionToolbarPosition() {
278 if (textToolbar?.status == TextToolbarStatus.Shown) {
279 showSelectionToolbar()
280 }
281 }
282
283 /**
284 * Calculate selected region as [Rect]. The top is the top of the first selected
285 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
286 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
287 */
288 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700289 val selection = selection ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700290 val startLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700291 selection.start.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700292 val endLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700293 selection.end.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700294
295 val localLayoutCoordinates = containerLayoutCoordinates
296 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
297 var startOffset = localLayoutCoordinates.childToLocal(
298 startLayoutCoordinates,
299 selection.start.selectable.getHandlePosition(
300 selection = selection,
301 isStartHandle = true
302 )
303 )
304 var endOffset = localLayoutCoordinates.childToLocal(
305 endLayoutCoordinates,
306 selection.end.selectable.getHandlePosition(
307 selection = selection,
308 isStartHandle = false
309 )
310 )
311
312 startOffset = localLayoutCoordinates.localToRoot(startOffset)
313 endOffset = localLayoutCoordinates.localToRoot(endOffset)
314
315 val left = min(startOffset.x, endOffset.x)
316 val right = max(startOffset.x, endOffset.x)
317
318 var startTop = localLayoutCoordinates.childToLocal(
319 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700320 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700321 0f,
322 selection.start.selectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700323 )
324 )
325
326 var endTop = localLayoutCoordinates.childToLocal(
327 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700328 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700329 0.0f,
330 selection.end.selectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700331 )
332 )
333
334 startTop = localLayoutCoordinates.localToRoot(startTop)
335 endTop = localLayoutCoordinates.localToRoot(endTop)
336
337 val top = min(startTop.y, endTop.y)
Nader Jawad16e330a2020-05-21 21:21:01 -0700338 val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700339
340 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700341 left,
342 top,
343 right,
344 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700345 )
346 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700347 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700348 }
349
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800350 // This is for PressGestureDetector to cancel the selection.
351 fun onRelease() {
352 // Call mergeSelections with an out of boundary input to inform all text widgets to
353 // cancel their individual selection.
Qingqing Denga5d80952019-10-11 16:46:52 -0700354 mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700355 startPosition = Offset(-1f, -1f),
356 endPosition = Offset(-1f, -1f),
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800357 previousSelection = selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800358 )
Siyamed Sinire810eab2019-11-22 12:36:38 -0800359 if (selection != null) onSelectionChange(null)
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800360 }
361
Siyamed Sinire810eab2019-11-22 12:36:38 -0800362 val longPressDragObserver = object : LongPressDragObserver {
Nader Jawad6df06122020-06-03 15:27:08 -0700363 override fun onLongPress(pxPosition: Offset) {
Siyamed Sinire810eab2019-11-22 12:36:38 -0800364 if (draggingHandle) return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100365 val coordinates = containerLayoutCoordinates
366 if (coordinates == null || !coordinates.isAttached) return
Siyamed Sinire810eab2019-11-22 12:36:38 -0800367 val newSelection = mergeSelections(
368 startPosition = pxPosition,
369 endPosition = pxPosition,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800370 longPress = true,
371 previousSelection = selection
Siyamed Sinire810eab2019-11-22 12:36:38 -0800372 )
373 if (newSelection != selection) onSelectionChange(newSelection)
374 dragBeginPosition = pxPosition
Siyamed Sinir700df8452019-10-22 20:23:58 -0700375 }
Siyamed Sinir700df8452019-10-22 20:23:58 -0700376
Siyamed Sinire810eab2019-11-22 12:36:38 -0800377 override fun onDragStart() {
378 super.onDragStart()
379 // selection never started
380 if (selection == null) return
381 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700382 dragTotalDistance = Offset.Zero
Siyamed Sinire810eab2019-11-22 12:36:38 -0800383 }
384
Nader Jawad6df06122020-06-03 15:27:08 -0700385 override fun onDrag(dragDistance: Offset): Offset {
Siyamed Sinire810eab2019-11-22 12:36:38 -0800386 // selection never started, did not consume any drag
Nader Jawad6df06122020-06-03 15:27:08 -0700387 if (selection == null) return Offset.Zero
Siyamed Sinire810eab2019-11-22 12:36:38 -0800388
389 dragTotalDistance += dragDistance
390 val newSelection = mergeSelections(
391 startPosition = dragBeginPosition,
392 endPosition = dragBeginPosition + dragTotalDistance,
Qingqing Deng74baaa22019-12-12 13:28:25 -0800393 longPress = true,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800394 previousSelection = selection
Siyamed Sinire810eab2019-11-22 12:36:38 -0800395 )
396
397 if (newSelection != selection) onSelectionChange(newSelection)
398 return dragDistance
399 }
400 }
401
Siyamed Sinir472c3162019-10-21 23:41:00 -0700402 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700403 return object : DragObserver {
Nader Jawad6df06122020-06-03 15:27:08 -0700404 override fun onStart(downPosition: Offset) {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700405 val selection = selection!!
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100406 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700407 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100408 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700409 val beginLayoutCoordinates = if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800410 selection.start.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700411 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800412 selection.end.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700413 }
414
Qingqing Deng6f56a912019-05-13 10:10:37 -0700415 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100416 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700417 val beginCoordinates = getAdjustedCoordinates(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800418 if (isStartHandle)
419 selection.start.selectable.getHandlePosition(
420 selection = selection, isStartHandle = true
421 ) else
422 selection.end.selectable.getHandlePosition(
423 selection = selection, isStartHandle = false
424 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700425 )
426
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100427 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700428 // selection container coordinates.
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100429 dragBeginPosition = requireContainerCoordinates().childToLocal(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700430 beginLayoutCoordinates,
431 beginCoordinates
432 )
433
434 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700435 dragTotalDistance = Offset.Zero
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700436 draggingHandle = true
Qingqing Deng6f56a912019-05-13 10:10:37 -0700437 }
438
Nader Jawad6df06122020-06-03 15:27:08 -0700439 override fun onDrag(dragDistance: Offset): Offset {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700440 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700441 dragTotalDistance += dragDistance
442
Siyamed Sinir472c3162019-10-21 23:41:00 -0700443 val currentStart = if (isStartHandle) {
444 dragBeginPosition + dragTotalDistance
445 } else {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100446 requireContainerCoordinates().childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800447 selection.start.selectable.getLayoutCoordinates()!!,
448 getAdjustedCoordinates(
449 selection.start.selectable.getHandlePosition(
450 selection = selection,
451 isStartHandle = true
452 )
453 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700454 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700455 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700456
457 val currentEnd = if (isStartHandle) {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100458 requireContainerCoordinates().childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800459 selection.end.selectable.getLayoutCoordinates()!!,
460 getAdjustedCoordinates(
461 selection.end.selectable.getHandlePosition(
462 selection = selection,
463 isStartHandle = false
464 )
465 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700466 )
467 } else {
468 dragBeginPosition + dragTotalDistance
469 }
470
Qingqing Denga5d80952019-10-11 16:46:52 -0700471 val finalSelection = mergeSelections(
472 startPosition = currentStart,
473 endPosition = currentEnd,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800474 previousSelection = selection,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800475 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700476 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700477 onSelectionChange(finalSelection)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700478 return dragDistance
479 }
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700480
Nader Jawad6df06122020-06-03 15:27:08 -0700481 override fun onStop(velocity: Offset) {
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700482 super.onStop(velocity)
483 draggingHandle = false
484 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700485 }
486 }
487}
488
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200489internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700490 return lhs?.merge(rhs) ?: rhs
491}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800492
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200493internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800494 selectable: Selectable,
495 selection: Selection
496): AnnotatedString {
497 val currentText = selectable.getText()
498
499 return if (
500 selectable != selection.start.selectable &&
501 selectable != selection.end.selectable
502 ) {
503 // Select the full text content if the current selectable is between the
504 // start and the end selectables.
505 currentText
506 } else if (
507 selectable == selection.start.selectable &&
508 selectable == selection.end.selectable
509 ) {
510 // Select partial text content if the current selectable is the start and
511 // the end selectable.
512 if (selection.handlesCrossed) {
513 currentText.subSequence(selection.end.offset, selection.start.offset)
514 } else {
515 currentText.subSequence(selection.start.offset, selection.end.offset)
516 }
517 } else if (selectable == selection.start.selectable) {
518 // Select partial text content if the current selectable is the start
519 // selectable.
520 if (selection.handlesCrossed) {
521 currentText.subSequence(0, selection.start.offset)
522 } else {
523 currentText.subSequence(selection.start.offset, currentText.length)
524 }
525 } else {
526 // Selectable partial text content if the current selectable is the end
527 // selectable.
528 if (selection.handlesCrossed) {
529 currentText.subSequence(selection.end.offset, currentText.length)
530 } else {
531 currentText.subSequence(0, selection.end.offset)
532 }
533 }
534}