[go: nahoru, domu]

blob: 3acc691929e29c2c23c9719cb20d56aaa0817fd0 [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
Siyamed Sinirfd8bc422019-11-21 18:23:58 -080017package androidx.ui.core.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070018
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010019import androidx.compose.State
20import androidx.compose.StructurallyEqual
21import androidx.compose.getValue
22import androidx.compose.mutableStateOf
23import androidx.compose.setValue
Qingqing Deng6f56a912019-05-13 10:10:37 -070024import androidx.ui.core.LayoutCoordinates
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070025import androidx.ui.core.clipboard.ClipboardManager
Qingqing Deng6f56a912019-05-13 10:10:37 -070026import androidx.ui.core.gesture.DragObserver
Qingqing Dengbf370e72019-10-17 11:14:39 -070027import androidx.ui.core.gesture.LongPressDragObserver
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080028import androidx.ui.core.hapticfeedback.HapticFeedback
29import androidx.ui.core.hapticfeedback.HapticFeedbackType
Qingqing Denga89450d2020-04-03 19:11:49 -070030import androidx.ui.core.texttoolbar.TextToolbar
Qingqing Dengde023cc2020-04-24 14:23:41 -070031import androidx.ui.core.texttoolbar.TextToolbarStatus
Qingqing Denga89450d2020-04-03 19:11:49 -070032import androidx.ui.geometry.Rect
Qingqing Deng648e1bf2019-12-30 11:49:48 -080033import androidx.ui.text.AnnotatedString
34import androidx.ui.text.length
35import androidx.ui.text.subSequence
Qingqing Dengde023cc2020-04-24 14:23:41 -070036import androidx.ui.unit.Px
George Mount842c8c12020-01-08 16:03:42 -080037import androidx.ui.unit.PxPosition
Qingqing Dengde023cc2020-04-24 14:23:41 -070038import androidx.ui.unit.max
39import androidx.ui.unit.min
George Mount842c8c12020-01-08 16:03:42 -080040import androidx.ui.unit.px
Qingqing Deng6f56a912019-05-13 10:10:37 -070041
Qingqing Deng35f97ea2019-09-18 19:24:37 -070042/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010043 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070044 */
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 */
92 private var dragBeginPosition = PxPosition.Origin
93
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 */
98 private var dragTotalDistance = PxPosition.Origin
99
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 */
114 var startHandlePosition by mutableStateOf<PxPosition?>(null, areEquivalent = StructurallyEqual)
115 private set
116
117 /**
118 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
119 * is null when handle shouldn't be displayed.
120 * It is a [State] so reading it during the composition will cause recomposition every time
121 * the position has been changed.
122 */
123 var endHandlePosition by mutableStateOf<PxPosition?>(null, areEquivalent = StructurallyEqual)
124 private set
125
126 init {
127 selectionRegistrar.onPositionChangeCallback = {
128 updateHandleOffsets()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700129 hideSelectionToolbar()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100130 }
131 }
132
133 private fun updateHandleOffsets() {
134 val selection = selection
135 val containerCoordinates = containerLayoutCoordinates
136 if (selection != null && containerCoordinates != null && containerCoordinates.isAttached) {
137 val startLayoutCoordinates = selection.start.selectable.getLayoutCoordinates()
138 val endLayoutCoordinates = selection.end.selectable.getLayoutCoordinates()
139
140 if (startLayoutCoordinates != null && endLayoutCoordinates != null) {
141 startHandlePosition = containerCoordinates.childToLocal(
142 startLayoutCoordinates,
143 selection.start.selectable.getHandlePosition(
144 selection = selection,
145 isStartHandle = true
146 )
147 )
148 endHandlePosition = containerCoordinates.childToLocal(
149 endLayoutCoordinates,
150 selection.end.selectable.getHandlePosition(
151 selection = selection,
152 isStartHandle = false
153 )
154 )
155 return
156 }
157 }
158 startHandlePosition = null
159 endHandlePosition = null
160 }
161
162 /**
163 * Returns non-nullable [containerLayoutCoordinates].
164 */
165 internal fun requireContainerCoordinates(): LayoutCoordinates {
166 val coordinates = containerLayoutCoordinates
167 require(coordinates != null)
168 require(coordinates.isAttached)
169 return coordinates
170 }
171
172 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700173 * Iterates over the handlers, gets the selection for each Composable, and merges all the
174 * returned [Selection]s.
175 *
176 * @param startPosition [PxPosition] for the start of the selection
177 * @param endPosition [PxPosition] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800178 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800179 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700180 *
181 * @return [Selection] object which is constructed by combining all Composables that are
182 * selected.
183 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800184 // This function is internal for testing purposes.
185 internal fun mergeSelections(
Siyamed Sinir472c3162019-10-21 23:41:00 -0700186 startPosition: PxPosition,
187 endPosition: PxPosition,
Siyamed Sinir0100f122019-11-16 00:23:12 -0800188 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800189 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800190 isStartHandle: Boolean = true
Siyamed Sinir472c3162019-10-21 23:41:00 -0700191 ): Selection? {
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800192
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100193 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng247f2b42019-12-12 19:48:37 -0800194 .fold(null) { mergedSelection: Selection?,
195 handler: Selectable ->
196 merge(
197 mergedSelection,
198 handler.getSelection(
199 startPosition = startPosition,
200 endPosition = endPosition,
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100201 containerLayoutCoordinates = requireContainerCoordinates(),
Qingqing Deng247f2b42019-12-12 19:48:37 -0800202 longPress = longPress,
203 previousSelection = previousSelection,
204 isStartHandle = isStartHandle
205 )
Siyamed Sinir700df8452019-10-22 20:23:58 -0700206 )
Qingqing Deng247f2b42019-12-12 19:48:37 -0800207 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800208 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
209 HapticFeedbackType.TextHandleMove
210 )
211 return newSelection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700212 }
213
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800214 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100215 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800216 var selectedText: AnnotatedString? = null
217
218 selection?.let {
219 for (handler in selectables) {
220 // Continue if the current selectable is before the selection starts.
221 if (handler != it.start.selectable && handler != it.end.selectable &&
222 selectedText == null
223 ) continue
224
225 val currentSelectedText = getCurrentSelectedText(
226 selectable = handler,
227 selection = it
228 )
229 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
230
231 // Break if the current selectable is the last selected selectable.
232 if (handler == it.end.selectable && !it.handlesCrossed ||
233 handler == it.start.selectable && it.handlesCrossed
234 ) break
235 }
236 }
237 return selectedText
238 }
239
Qingqing Dengde023cc2020-04-24 14:23:41 -0700240 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700241 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700242 selectedText?.let { clipboardManager?.setText(it) }
243 }
244
245 /**
246 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
247 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
248 * the copy method as a callback when "copy" is clicked.
249 */
250 internal fun showSelectionToolbar() {
251 selection?.let {
Qingqing Dengce140562020-04-24 14:46:22 -0700252 textToolbar?.showCopyMenu(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700253 getContentRect(),
254 onCopyRequested = { copy() },
Qingqing Dengce140562020-04-24 14:46:22 -0700255 onDeselectRequested = { onRelease() }
256 )
Qingqing Denga89450d2020-04-03 19:11:49 -0700257 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700258 }
259
Qingqing Dengde023cc2020-04-24 14:23:41 -0700260 private fun hideSelectionToolbar() {
261 if (textToolbar?.status == TextToolbarStatus.Shown) {
262 val selection = selection
263 if (selection == null) {
264 textToolbar?.hide()
265 }
266 }
267 }
268
269 private fun updateSelectionToolbarPosition() {
270 if (textToolbar?.status == TextToolbarStatus.Shown) {
271 showSelectionToolbar()
272 }
273 }
274
275 /**
276 * Calculate selected region as [Rect]. The top is the top of the first selected
277 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
278 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
279 */
280 private fun getContentRect(): Rect {
281 val selection = selection ?: return Rect.zero
282 val startLayoutCoordinates =
283 selection.start.selectable.getLayoutCoordinates() ?: return Rect.zero
284 val endLayoutCoordinates =
285 selection.end.selectable.getLayoutCoordinates() ?: return Rect.zero
286
287 val localLayoutCoordinates = containerLayoutCoordinates
288 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
289 var startOffset = localLayoutCoordinates.childToLocal(
290 startLayoutCoordinates,
291 selection.start.selectable.getHandlePosition(
292 selection = selection,
293 isStartHandle = true
294 )
295 )
296 var endOffset = localLayoutCoordinates.childToLocal(
297 endLayoutCoordinates,
298 selection.end.selectable.getHandlePosition(
299 selection = selection,
300 isStartHandle = false
301 )
302 )
303
304 startOffset = localLayoutCoordinates.localToRoot(startOffset)
305 endOffset = localLayoutCoordinates.localToRoot(endOffset)
306
307 val left = min(startOffset.x, endOffset.x)
308 val right = max(startOffset.x, endOffset.x)
309
310 var startTop = localLayoutCoordinates.childToLocal(
311 startLayoutCoordinates,
312 PxPosition(
313 Px.Zero,
314 selection.start.selectable.getBoundingBox(selection.start.offset).top.px
315 )
316 )
317
318 var endTop = localLayoutCoordinates.childToLocal(
319 endLayoutCoordinates,
320 PxPosition(
321 Px.Zero,
322 selection.end.selectable.getBoundingBox(selection.end.offset).top.px
323 )
324 )
325
326 startTop = localLayoutCoordinates.localToRoot(startTop)
327 endTop = localLayoutCoordinates.localToRoot(endTop)
328
329 val top = min(startTop.y, endTop.y)
330 val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).px
331
332 return Rect(
333 left.value,
334 top.value,
335 right.value,
336 bottom.value
337 )
338 }
339 return Rect.zero
340 }
341
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800342 // This is for PressGestureDetector to cancel the selection.
343 fun onRelease() {
344 // Call mergeSelections with an out of boundary input to inform all text widgets to
345 // cancel their individual selection.
Qingqing Denga5d80952019-10-11 16:46:52 -0700346 mergeSelections(
Siyamed Sinir0100f122019-11-16 00:23:12 -0800347 startPosition = PxPosition((-1).px, (-1).px),
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800348 endPosition = PxPosition((-1).px, (-1).px),
349 previousSelection = selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800350 )
Siyamed Sinire810eab2019-11-22 12:36:38 -0800351 if (selection != null) onSelectionChange(null)
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800352 }
353
Siyamed Sinire810eab2019-11-22 12:36:38 -0800354 val longPressDragObserver = object : LongPressDragObserver {
355 override fun onLongPress(pxPosition: PxPosition) {
356 if (draggingHandle) return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100357 val coordinates = containerLayoutCoordinates
358 if (coordinates == null || !coordinates.isAttached) return
Siyamed Sinire810eab2019-11-22 12:36:38 -0800359 val newSelection = mergeSelections(
360 startPosition = pxPosition,
361 endPosition = pxPosition,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800362 longPress = true,
363 previousSelection = selection
Siyamed Sinire810eab2019-11-22 12:36:38 -0800364 )
365 if (newSelection != selection) onSelectionChange(newSelection)
366 dragBeginPosition = pxPosition
Siyamed Sinir700df8452019-10-22 20:23:58 -0700367 }
Siyamed Sinir700df8452019-10-22 20:23:58 -0700368
Siyamed Sinire810eab2019-11-22 12:36:38 -0800369 override fun onDragStart() {
370 super.onDragStart()
371 // selection never started
372 if (selection == null) return
373 // Zero out the total distance that being dragged.
374 dragTotalDistance = PxPosition.Origin
375 }
376
377 override fun onDrag(dragDistance: PxPosition): PxPosition {
378 // selection never started, did not consume any drag
379 if (selection == null) return PxPosition.Origin
380
381 dragTotalDistance += dragDistance
382 val newSelection = mergeSelections(
383 startPosition = dragBeginPosition,
384 endPosition = dragBeginPosition + dragTotalDistance,
Qingqing Deng74baaa22019-12-12 13:28:25 -0800385 longPress = true,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800386 previousSelection = selection
Siyamed Sinire810eab2019-11-22 12:36:38 -0800387 )
388
389 if (newSelection != selection) onSelectionChange(newSelection)
390 return dragDistance
391 }
392 }
393
Qingqing Deng13743f72019-07-15 15:00:45 -0700394 /**
395 * Adjust coordinates for given text offset.
396 *
397 * Currently [android.text.Layout.getLineBottom] returns y coordinates of the next
398 * line's top offset, which is not included in current line's hit area. To be able to
399 * hit current line, move up this y coordinates by 1 pixel.
400 */
Siyamed Sinir472c3162019-10-21 23:41:00 -0700401 private fun getAdjustedCoordinates(position: PxPosition): PxPosition {
402 return PxPosition(position.x, position.y - 1.px)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700403 }
404
Siyamed Sinir472c3162019-10-21 23:41:00 -0700405 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700406 return object : DragObserver {
Andrey Kulikov0e6c40f2019-09-06 18:33:14 +0100407 override fun onStart(downPosition: PxPosition) {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700408 val selection = selection!!
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100409 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700410 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100411 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700412 val beginLayoutCoordinates = if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800413 selection.start.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700414 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800415 selection.end.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700416 }
417
Qingqing Deng6f56a912019-05-13 10:10:37 -0700418 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100419 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700420 val beginCoordinates = getAdjustedCoordinates(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800421 if (isStartHandle)
422 selection.start.selectable.getHandlePosition(
423 selection = selection, isStartHandle = true
424 ) else
425 selection.end.selectable.getHandlePosition(
426 selection = selection, isStartHandle = false
427 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700428 )
429
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100430 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700431 // selection container coordinates.
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100432 dragBeginPosition = requireContainerCoordinates().childToLocal(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700433 beginLayoutCoordinates,
434 beginCoordinates
435 )
436
437 // Zero out the total distance that being dragged.
438 dragTotalDistance = PxPosition.Origin
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700439 draggingHandle = true
Qingqing Deng6f56a912019-05-13 10:10:37 -0700440 }
441
442 override fun onDrag(dragDistance: PxPosition): PxPosition {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700443 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700444 dragTotalDistance += dragDistance
445
Siyamed Sinir472c3162019-10-21 23:41:00 -0700446 val currentStart = if (isStartHandle) {
447 dragBeginPosition + dragTotalDistance
448 } else {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100449 requireContainerCoordinates().childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800450 selection.start.selectable.getLayoutCoordinates()!!,
451 getAdjustedCoordinates(
452 selection.start.selectable.getHandlePosition(
453 selection = selection,
454 isStartHandle = true
455 )
456 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700457 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700458 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700459
460 val currentEnd = if (isStartHandle) {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100461 requireContainerCoordinates().childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800462 selection.end.selectable.getLayoutCoordinates()!!,
463 getAdjustedCoordinates(
464 selection.end.selectable.getHandlePosition(
465 selection = selection,
466 isStartHandle = false
467 )
468 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700469 )
470 } else {
471 dragBeginPosition + dragTotalDistance
472 }
473
Qingqing Denga5d80952019-10-11 16:46:52 -0700474 val finalSelection = mergeSelections(
475 startPosition = currentStart,
476 endPosition = currentEnd,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800477 previousSelection = selection,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800478 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700479 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700480 onSelectionChange(finalSelection)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700481 return dragDistance
482 }
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700483
484 override fun onStop(velocity: PxPosition) {
485 super.onStop(velocity)
486 draggingHandle = false
487 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700488 }
489 }
490}
491
Siyamed Sinir700df8452019-10-22 20:23:58 -0700492private fun merge(lhs: Selection?, rhs: Selection?): Selection? {
493 return lhs?.merge(rhs) ?: rhs
494}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800495
496private fun getCurrentSelectedText(
497 selectable: Selectable,
498 selection: Selection
499): AnnotatedString {
500 val currentText = selectable.getText()
501
502 return if (
503 selectable != selection.start.selectable &&
504 selectable != selection.end.selectable
505 ) {
506 // Select the full text content if the current selectable is between the
507 // start and the end selectables.
508 currentText
509 } else if (
510 selectable == selection.start.selectable &&
511 selectable == selection.end.selectable
512 ) {
513 // Select partial text content if the current selectable is the start and
514 // the end selectable.
515 if (selection.handlesCrossed) {
516 currentText.subSequence(selection.end.offset, selection.start.offset)
517 } else {
518 currentText.subSequence(selection.start.offset, selection.end.offset)
519 }
520 } else if (selectable == selection.start.selectable) {
521 // Select partial text content if the current selectable is the start
522 // selectable.
523 if (selection.handlesCrossed) {
524 currentText.subSequence(0, selection.start.offset)
525 } else {
526 currentText.subSequence(selection.start.offset, currentText.length)
527 }
528 } else {
529 // Selectable partial text content if the current selectable is the end
530 // selectable.
531 if (selection.handlesCrossed) {
532 currentText.subSequence(selection.end.offset, currentText.length)
533 } else {
534 currentText.subSequence(0, selection.end.offset)
535 }
536 }
537}