[go: nahoru, domu]

blob: 444c464d5ba9a60fd41759886bf4e55671fd4ee6 [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
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010027import androidx.compose.ui.hapticfeedback.HapticFeedback
28import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010029import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000030import androidx.compose.ui.layout.boundsInWindow
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010031import 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
haoyue6d80a12020-12-02 16:04:52 -080035import androidx.compose.ui.text.ExperimentalTextApi
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010036import androidx.compose.ui.text.InternalTextApi
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010037import androidx.compose.ui.text.length
38import androidx.compose.ui.text.subSequence
Nader Jawade6a9b332020-05-21 13:49:20 -070039import kotlin.math.max
40import kotlin.math.min
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 */
haoyue6d80a12020-12-02 16:04:52 -080045@OptIn(
46 InternalTextApi::class,
47 ExperimentalTextApi::class
48)
Siyamed Sinir700df8452019-10-22 20:23:58 -070049internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070050 /**
51 * The current selection.
52 */
53 var selection: Selection? = null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010054 set(value) {
55 field = value
haoyu9085c882020-12-08 12:01:06 -080056 if (value != null) {
57 updateHandleOffsets()
58 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010059 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070060
61 /**
62 * The manager will invoke this every time it comes to the conclusion that the selection should
63 * change. The expectation is that this callback will end up causing `setSelection` to get
64 * called. This is what makes this a "controlled component".
65 */
66 var onSelectionChange: (Selection?) -> Unit = {}
67
68 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080069 * [HapticFeedback] handle to perform haptic feedback.
70 */
71 var hapticFeedBack: HapticFeedback? = null
72
73 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070074 * [ClipboardManager] to perform clipboard features.
75 */
76 var clipboardManager: ClipboardManager? = null
77
78 /**
Qingqing Denga89450d2020-04-03 19:11:49 -070079 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
80 */
81 var textToolbar: TextToolbar? = null
82
83 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070084 * Layout Coordinates of the selection container.
85 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010086 var containerLayoutCoordinates: LayoutCoordinates? = null
87 set(value) {
88 field = value
89 updateHandleOffsets()
Qingqing Dengde023cc2020-04-24 14:23:41 -070090 updateSelectionToolbarPosition()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010091 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070092
93 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070094 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
95 * recalculated.
96 */
Nader Jawad6df06122020-06-03 15:27:08 -070097 private var dragBeginPosition = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -070098
99 /**
100 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
101 * it will be zeroed out.
102 */
Nader Jawad6df06122020-06-03 15:27:08 -0700103 private var dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700104
105 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100106 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
107 * is null when handle shouldn't be displayed.
108 * It is a [State] so reading it during the composition will cause recomposition every time
109 * the position has been changed.
110 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700111 var startHandlePosition by mutableStateOf<Offset?>(
112 null,
113 policy = structuralEqualityPolicy()
114 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100115 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 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700123 var endHandlePosition by mutableStateOf<Offset?>(
124 null,
125 policy = structuralEqualityPolicy()
126 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100127 private set
128
129 init {
130 selectionRegistrar.onPositionChangeCallback = {
131 updateHandleOffsets()
haoyue6d80a12020-12-02 16:04:52 -0800132 updateSelectionToolbarPosition()
133 }
134
135 selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
136 updateSelection(
137 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
138 endPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
139 isStartHandle = true,
140 longPress = true
141 )
Qingqing Dengde023cc2020-04-24 14:23:41 -0700142 hideSelectionToolbar()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100143 }
haoyue6d80a12020-12-02 16:04:52 -0800144
145 selectionRegistrar.onSelectionUpdateCallback =
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700146 { layoutCoordinates, startPosition, endPosition ->
147 updateSelection(
148 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
149 endPosition = convertToContainerCoordinates(layoutCoordinates, endPosition),
haoyu70aed732020-12-15 07:10:56 -0800150 isStartHandle = false,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700151 longPress = true
152 )
153 }
haoyue6d80a12020-12-02 16:04:52 -0800154
155 selectionRegistrar.onSelectionUpdateEndCallback = {
156 showSelectionToolbar()
157 }
haoyu9085c882020-12-08 12:01:06 -0800158
159 selectionRegistrar.onSelectableChangeCallback = { selectable ->
160 if (selectable in selectionRegistrar.selectables) {
161 // clear the selection range of each Selectable.
162 onRelease()
163 selection = null
164 }
165 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100166 }
167
168 private fun updateHandleOffsets() {
169 val selection = selection
170 val containerCoordinates = containerLayoutCoordinates
haoyue2678c62020-12-09 08:39:12 -0800171 val startLayoutCoordinates = selection?.start?.selectable?.getLayoutCoordinates()
172 val endLayoutCoordinates = selection?.end?.selectable?.getLayoutCoordinates()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100173
haoyue2678c62020-12-09 08:39:12 -0800174 if (
175 selection == null ||
176 containerCoordinates == null ||
177 !containerCoordinates.isAttached ||
178 startLayoutCoordinates == null ||
179 endLayoutCoordinates == null
180 ) {
181 this.startHandlePosition = null
182 this.endHandlePosition = null
183 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100184 }
haoyue2678c62020-12-09 08:39:12 -0800185
George Mount77ca2a22020-12-11 17:46:19 +0000186 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800187 startLayoutCoordinates,
188 selection.start.selectable.getHandlePosition(
189 selection = selection,
190 isStartHandle = true
191 )
192 )
George Mount77ca2a22020-12-11 17:46:19 +0000193 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800194 endLayoutCoordinates,
195 selection.end.selectable.getHandlePosition(
196 selection = selection,
197 isStartHandle = false
198 )
199 )
200
201 val visibleBounds = containerCoordinates.visibleBounds()
202 this.startHandlePosition =
203 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
204 this.endHandlePosition =
205 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100206 }
207
208 /**
209 * Returns non-nullable [containerLayoutCoordinates].
210 */
211 internal fun requireContainerCoordinates(): LayoutCoordinates {
212 val coordinates = containerLayoutCoordinates
213 require(coordinates != null)
214 require(coordinates.isAttached)
215 return coordinates
216 }
217
218 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700219 * Iterates over the handlers, gets the selection for each Composable, and merges all the
220 * returned [Selection]s.
221 *
Nader Jawad6df06122020-06-03 15:27:08 -0700222 * @param startPosition [Offset] for the start of the selection
223 * @param endPosition [Offset] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800224 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800225 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700226 *
227 * @return [Selection] object which is constructed by combining all Composables that are
228 * selected.
229 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800230 // This function is internal for testing purposes.
231 internal fun mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700232 startPosition: Offset,
233 endPosition: Offset,
Siyamed Sinir0100f122019-11-16 00:23:12 -0800234 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800235 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800236 isStartHandle: Boolean = true
Siyamed Sinir472c3162019-10-21 23:41:00 -0700237 ): Selection? {
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800238
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100239 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng247f2b42019-12-12 19:48:37 -0800240 .fold(null) { mergedSelection: Selection?,
Jeff Gastona3bbb7e2020-09-18 15:11:15 -0400241 handler: Selectable ->
Qingqing Deng247f2b42019-12-12 19:48:37 -0800242 merge(
243 mergedSelection,
244 handler.getSelection(
245 startPosition = startPosition,
246 endPosition = endPosition,
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100247 containerLayoutCoordinates = requireContainerCoordinates(),
Qingqing Deng247f2b42019-12-12 19:48:37 -0800248 longPress = longPress,
249 previousSelection = previousSelection,
250 isStartHandle = isStartHandle
251 )
Siyamed Sinir700df8452019-10-22 20:23:58 -0700252 )
Qingqing Deng247f2b42019-12-12 19:48:37 -0800253 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800254 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
255 HapticFeedbackType.TextHandleMove
256 )
257 return newSelection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700258 }
259
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800260 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100261 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800262 var selectedText: AnnotatedString? = null
263
264 selection?.let {
265 for (handler in selectables) {
266 // Continue if the current selectable is before the selection starts.
267 if (handler != it.start.selectable && handler != it.end.selectable &&
268 selectedText == null
269 ) continue
270
271 val currentSelectedText = getCurrentSelectedText(
272 selectable = handler,
273 selection = it
274 )
275 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
276
277 // Break if the current selectable is the last selected selectable.
278 if (handler == it.end.selectable && !it.handlesCrossed ||
279 handler == it.start.selectable && it.handlesCrossed
280 ) break
281 }
282 }
283 return selectedText
284 }
285
Qingqing Dengde023cc2020-04-24 14:23:41 -0700286 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700287 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700288 selectedText?.let { clipboardManager?.setText(it) }
289 }
290
291 /**
292 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
293 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
294 * the copy method as a callback when "copy" is clicked.
295 */
296 internal fun showSelectionToolbar() {
297 selection?.let {
Qingqing Deng40519572020-07-10 13:25:36 -0700298 textToolbar?.showMenu(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700299 getContentRect(),
Qingqing Deng40519572020-07-10 13:25:36 -0700300 onCopyRequested = {
301 copy()
302 onRelease()
303 }
Qingqing Dengce140562020-04-24 14:46:22 -0700304 )
Qingqing Denga89450d2020-04-03 19:11:49 -0700305 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700306 }
307
haoyue6d80a12020-12-02 16:04:52 -0800308 internal fun hideSelectionToolbar() {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700309 if (textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800310 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700311 }
312 }
313
314 private fun updateSelectionToolbarPosition() {
315 if (textToolbar?.status == TextToolbarStatus.Shown) {
316 showSelectionToolbar()
317 }
318 }
319
320 /**
321 * Calculate selected region as [Rect]. The top is the top of the first selected
322 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
323 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
324 */
325 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700326 val selection = selection ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700327 val startLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700328 selection.start.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700329 val endLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700330 selection.end.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700331
332 val localLayoutCoordinates = containerLayoutCoordinates
333 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000334 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700335 startLayoutCoordinates,
336 selection.start.selectable.getHandlePosition(
337 selection = selection,
338 isStartHandle = true
339 )
340 )
George Mount77ca2a22020-12-11 17:46:19 +0000341 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700342 endLayoutCoordinates,
343 selection.end.selectable.getHandlePosition(
344 selection = selection,
345 isStartHandle = false
346 )
347 )
348
349 startOffset = localLayoutCoordinates.localToRoot(startOffset)
350 endOffset = localLayoutCoordinates.localToRoot(endOffset)
351
352 val left = min(startOffset.x, endOffset.x)
353 val right = max(startOffset.x, endOffset.x)
354
George Mount77ca2a22020-12-11 17:46:19 +0000355 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700356 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700357 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700358 0f,
359 selection.start.selectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700360 )
361 )
362
George Mount77ca2a22020-12-11 17:46:19 +0000363 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700364 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700365 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700366 0.0f,
367 selection.end.selectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700368 )
369 )
370
371 startTop = localLayoutCoordinates.localToRoot(startTop)
372 endTop = localLayoutCoordinates.localToRoot(endTop)
373
374 val top = min(startTop.y, endTop.y)
Nader Jawad16e330a2020-05-21 21:21:01 -0700375 val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700376
377 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700378 left,
379 top,
380 right,
381 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700382 )
383 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700384 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700385 }
386
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800387 // This is for PressGestureDetector to cancel the selection.
388 fun onRelease() {
389 // Call mergeSelections with an out of boundary input to inform all text widgets to
390 // cancel their individual selection.
Qingqing Denga5d80952019-10-11 16:46:52 -0700391 mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700392 startPosition = Offset(-1f, -1f),
393 endPosition = Offset(-1f, -1f),
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800394 previousSelection = selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800395 )
haoyue6d80a12020-12-02 16:04:52 -0800396 hideSelectionToolbar()
Siyamed Sinire810eab2019-11-22 12:36:38 -0800397 if (selection != null) onSelectionChange(null)
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800398 }
399
Siyamed Sinir472c3162019-10-21 23:41:00 -0700400 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700401 return object : DragObserver {
Nader Jawad6df06122020-06-03 15:27:08 -0700402 override fun onStart(downPosition: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800403 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700404 val selection = selection!!
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100405 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700406 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100407 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700408 val beginLayoutCoordinates = if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800409 selection.start.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700410 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800411 selection.end.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700412 }
413
Qingqing Deng6f56a912019-05-13 10:10:37 -0700414 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100415 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700416 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800417 if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800418 selection.start.selectable.getHandlePosition(
419 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800420 )
421 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800422 selection.end.selectable.getHandlePosition(
423 selection = selection, isStartHandle = false
424 )
haoyue6d80a12020-12-02 16:04:52 -0800425 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700426 )
427
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100428 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700429 // selection container coordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000430 dragBeginPosition = requireContainerCoordinates().localPositionOf(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700431 beginLayoutCoordinates,
432 beginCoordinates
433 )
434
435 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700436 dragTotalDistance = Offset.Zero
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 {
George Mount77ca2a22020-12-11 17:46:19 +0000446 requireContainerCoordinates().localPositionOf(
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) {
George Mount77ca2a22020-12-11 17:46:19 +0000458 requireContainerCoordinates().localPositionOf(
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 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700470 updateSelection(
Qingqing Denga5d80952019-10-11 16:46:52 -0700471 startPosition = currentStart,
472 endPosition = currentEnd,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800473 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700474 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700475 return dragDistance
476 }
haoyue6d80a12020-12-02 16:04:52 -0800477
478 override fun onStop(velocity: Offset) {
479 showSelectionToolbar()
480 }
481
482 override fun onCancel() {
483 showSelectionToolbar()
484 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700485 }
486 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700487
488 private fun convertToContainerCoordinates(
489 layoutCoordinates: LayoutCoordinates,
490 offset: Offset
491 ): Offset? {
492 val coordinates = containerLayoutCoordinates
493 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000494 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700495 }
496
497 private fun updateSelection(
498 startPosition: Offset?,
499 endPosition: Offset?,
500 longPress: Boolean = false,
501 isStartHandle: Boolean = true
502 ) {
503 if (startPosition == null || endPosition == null) return
504 val newSelection = mergeSelections(
505 startPosition = startPosition,
506 endPosition = endPosition,
507 longPress = longPress,
508 isStartHandle = isStartHandle,
509 previousSelection = selection
510 )
511 if (newSelection != selection) onSelectionChange(newSelection)
512 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700513}
514
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200515internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700516 return lhs?.merge(rhs) ?: rhs
517}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800518
haoyue6d80a12020-12-02 16:04:52 -0800519@OptIn(ExperimentalTextApi::class)
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200520internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800521 selectable: Selectable,
522 selection: Selection
523): AnnotatedString {
524 val currentText = selectable.getText()
525
526 return if (
527 selectable != selection.start.selectable &&
528 selectable != selection.end.selectable
529 ) {
530 // Select the full text content if the current selectable is between the
531 // start and the end selectables.
532 currentText
533 } else if (
534 selectable == selection.start.selectable &&
535 selectable == selection.end.selectable
536 ) {
537 // Select partial text content if the current selectable is the start and
538 // the end selectable.
539 if (selection.handlesCrossed) {
540 currentText.subSequence(selection.end.offset, selection.start.offset)
541 } else {
542 currentText.subSequence(selection.start.offset, selection.end.offset)
543 }
544 } else if (selectable == selection.start.selectable) {
545 // Select partial text content if the current selectable is the start
546 // selectable.
547 if (selection.handlesCrossed) {
548 currentText.subSequence(0, selection.start.offset)
549 } else {
550 currentText.subSequence(selection.start.offset, currentText.length)
551 }
552 } else {
553 // Selectable partial text content if the current selectable is the end
554 // selectable.
555 if (selection.handlesCrossed) {
556 currentText.subSequence(selection.end.offset, currentText.length)
557 } else {
558 currentText.subSequence(0, selection.end.offset)
559 }
560 }
561}
haoyue2678c62020-12-09 08:39:12 -0800562
563/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
564private fun LayoutCoordinates.visibleBounds(): Rect {
565 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
566 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
567 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000568 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800569 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000570 windowToLocal(boundsInWindow.topLeft),
571 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800572 )
573}
574
575private fun Rect.containsInclusive(offset: Offset): Boolean =
576 offset.x in left..right && offset.y in top..bottom