[go: nahoru, domu]

blob: 87053fb899bf38552f52456f05a31176295cc6a9 [file] [log] [blame]
Qingqing Deng6f56a912019-05-13 10:10:37 -07001/*
haoyuac341f02021-01-22 22:01:56 -08002 * Copyright 2021 The Android Open Source Project
Qingqing Deng6f56a912019-05-13 10:10:37 -07003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Matvei Malkovc287d1b2021-01-26 15:04:34 +000017@file:Suppress("DEPRECATION")
18
haoyuac341f02021-01-22 22:01:56 -080019package androidx.compose.foundation.text.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070020
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +000021import androidx.compose.foundation.fastFold
haoyuc40d02752021-01-25 17:32:47 -080022import androidx.compose.foundation.focusable
George Mount32de9dd2022-10-05 14:51:06 -070023import androidx.compose.foundation.gestures.awaitEachGesture
Ralston Da Silvade62bc62021-06-02 17:46:44 -070024import androidx.compose.foundation.gestures.waitForUpOrCancellation
Zach Klippensteinadabe342021-11-11 16:38:13 -080025import androidx.compose.foundation.text.Handle
26import androidx.compose.foundation.text.TextDragObserver
27import androidx.compose.foundation.text.selection.Selection.AnchorInfo
28import androidx.compose.runtime.MutableState
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010029import androidx.compose.runtime.State
30import androidx.compose.runtime.getValue
31import androidx.compose.runtime.mutableStateOf
32import androidx.compose.runtime.setValue
haoyuc40d02752021-01-25 17:32:47 -080033import androidx.compose.ui.Modifier
34import androidx.compose.ui.focus.FocusRequester
35import androidx.compose.ui.focus.focusRequester
haoyuc40d02752021-01-25 17:32:47 -080036import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010037import androidx.compose.ui.geometry.Offset
38import androidx.compose.ui.geometry.Rect
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010039import androidx.compose.ui.hapticfeedback.HapticFeedback
40import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010041import androidx.compose.ui.input.key.KeyEvent
42import androidx.compose.ui.input.key.onKeyEvent
Ralston Da Silvade62bc62021-06-02 17:46:44 -070043import androidx.compose.ui.input.pointer.PointerInputScope
44import androidx.compose.ui.input.pointer.pointerInput
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010045import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000046import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080047import androidx.compose.ui.layout.onGloballyPositioned
haoyu7ad5ea32021-03-22 10:36:35 -070048import androidx.compose.ui.layout.positionInWindow
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010049import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010050import androidx.compose.ui.platform.TextToolbar
51import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010052import androidx.compose.ui.text.AnnotatedString
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -080053import androidx.compose.ui.unit.IntSize
54import kotlin.math.absoluteValue
Nader Jawade6a9b332020-05-21 13:49:20 -070055import kotlin.math.max
56import kotlin.math.min
Qingqing Deng6f56a912019-05-13 10:10:37 -070057
Qingqing Deng35f97ea2019-09-18 19:24:37 -070058/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010059 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070060 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070061internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080062
63 private val _selection: MutableState<Selection?> = mutableStateOf(null)
64
Qingqing Deng6f56a912019-05-13 10:10:37 -070065 /**
66 * The current selection.
67 */
Zach Klippensteinadabe342021-11-11 16:38:13 -080068 var selection: Selection?
69 get() = _selection.value
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010070 set(value) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080071 _selection.value = value
haoyu9085c882020-12-08 12:01:06 -080072 if (value != null) {
73 updateHandleOffsets()
74 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010075 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070076
77 /**
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010078 * Is touch mode active
79 */
80 var touchMode: Boolean = true
81
82 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070083 * The manager will invoke this every time it comes to the conclusion that the selection should
84 * change. The expectation is that this callback will end up causing `setSelection` to get
85 * called. This is what makes this a "controlled component".
86 */
87 var onSelectionChange: (Selection?) -> Unit = {}
88
89 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080090 * [HapticFeedback] handle to perform haptic feedback.
91 */
92 var hapticFeedBack: HapticFeedback? = null
93
94 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070095 * [ClipboardManager] to perform clipboard features.
96 */
97 var clipboardManager: ClipboardManager? = null
98
99 /**
Qingqing Denga89450d2020-04-03 19:11:49 -0700100 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
101 */
102 var textToolbar: TextToolbar? = null
103
104 /**
haoyuc40d02752021-01-25 17:32:47 -0800105 * Focus requester used to request focus when selection becomes active.
106 */
107 var focusRequester: FocusRequester = FocusRequester()
108
109 /**
haoyu3c3fb452021-02-18 01:01:14 -0800110 * Return true if the corresponding SelectionContainer is focused.
haoyuc40d02752021-01-25 17:32:47 -0800111 */
haoyu3c3fb452021-02-18 01:01:14 -0800112 var hasFocus: Boolean by mutableStateOf(false)
haoyuc40d02752021-01-25 17:32:47 -0800113
114 /**
115 * Modifier for selection container.
116 */
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800117 val modifier
118 get() = Modifier
119 .onClearSelectionRequested { onRelease() }
120 .onGloballyPositioned { containerLayoutCoordinates = it }
121 .focusRequester(focusRequester)
122 .onFocusChanged { focusState ->
123 if (!focusState.isFocused && hasFocus) {
124 onRelease()
125 }
126 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800127 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800128 .focusable()
129 .onKeyEvent {
130 if (isCopyKeyEvent(it)) {
131 copy()
132 true
133 } else {
134 false
135 }
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100136 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800137 .then(if (shouldShowMagnifier) Modifier.selectionMagnifier(this) else Modifier)
haoyuc40d02752021-01-25 17:32:47 -0800138
haoyu7ad5ea32021-03-22 10:36:35 -0700139 private var previousPosition: Offset? = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800140
haoyuc40d02752021-01-25 17:32:47 -0800141 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700142 * Layout Coordinates of the selection container.
143 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100144 var containerLayoutCoordinates: LayoutCoordinates? = null
145 set(value) {
146 field = value
haoyu2c6e9842021-03-30 17:39:04 -0700147 if (hasFocus && selection != null) {
148 val positionInWindow = value?.positionInWindow()
149 if (previousPosition != positionInWindow) {
150 previousPosition = positionInWindow
151 updateHandleOffsets()
152 updateSelectionToolbarPosition()
153 }
haoyu3c3fb452021-02-18 01:01:14 -0800154 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100155 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700156
157 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700158 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
159 * recalculated.
160 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800161 internal var dragBeginPosition by mutableStateOf(Offset.Zero)
162 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700163
164 /**
165 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
166 * it will be zeroed out.
167 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800168 internal var dragTotalDistance by mutableStateOf(Offset.Zero)
169 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700170
171 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100172 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
173 * is null when handle shouldn't be displayed.
174 * It is a [State] so reading it during the composition will cause recomposition every time
175 * the position has been changed.
176 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800177 var startHandlePosition: Offset? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100178 private set
179
180 /**
181 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
182 * is null when handle shouldn't be displayed.
183 * It is a [State] so reading it during the composition will cause recomposition every time
184 * the position has been changed.
185 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800186 var endHandlePosition: Offset? by mutableStateOf(null)
187 private set
188
189 /**
Zach Klippenstein63870892022-01-14 12:45:18 -0800190 * The handle that is currently being dragged, or null when no handle is being dragged. To get
191 * the position of the last drag event, use [currentDragPosition].
Zach Klippensteinadabe342021-11-11 16:38:13 -0800192 */
193 var draggingHandle: Handle? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100194 private set
195
Zach Klippenstein63870892022-01-14 12:45:18 -0800196 /**
197 * When a handle is being dragged (i.e. [draggingHandle] is non-null), this is the last position
198 * of the actual drag event. It is not clamped to handle positions. Null when not being dragged.
199 */
200 var currentDragPosition: Offset? by mutableStateOf(null)
201 private set
202
Zach Klippenstein4688a462021-12-08 08:28:07 -0800203 private val shouldShowMagnifier get() = draggingHandle != null
204
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100205 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700206 selectionRegistrar.onPositionChangeCallback = { selectableId ->
207 if (
208 selectableId == selection?.start?.selectableId ||
209 selectableId == selection?.end?.selectableId
210 ) {
211 updateHandleOffsets()
212 updateSelectionToolbarPosition()
213 }
haoyue6d80a12020-12-02 16:04:52 -0800214 }
215
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100216 selectionRegistrar.onSelectionUpdateStartCallback =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700217 { layoutCoordinates, position, selectionMode ->
218 val positionInContainer = convertToContainerCoordinates(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100219 layoutCoordinates,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700220 position
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100221 )
222
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700223 if (positionInContainer != null) {
224 startSelection(
225 position = positionInContainer,
226 isStartHandle = false,
227 adjustment = selectionMode
228 )
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100229
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700230 focusRequester.requestFocus()
231 hideSelectionToolbar()
232 }
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100233 }
haoyue6d80a12020-12-02 16:04:52 -0800234
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700235 selectionRegistrar.onSelectionUpdateSelectAll =
236 { selectableId ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700237 val (newSelection, newSubselection) = selectAll(
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700238 selectableId = selectableId,
239 previousSelection = selection,
240 )
241 if (newSelection != selection) {
242 selectionRegistrar.subselections = newSubselection
243 onSelectionChange(newSelection)
244 }
245
246 focusRequester.requestFocus()
247 hideSelectionToolbar()
248 }
249
haoyue6d80a12020-12-02 16:04:52 -0800250 selectionRegistrar.onSelectionUpdateCallback =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700251 { layoutCoordinates, newPosition, previousPosition, isStartHandle, selectionMode ->
252 val newPositionInContainer =
253 convertToContainerCoordinates(layoutCoordinates, newPosition)
254 val previousPositionInContainer =
255 convertToContainerCoordinates(layoutCoordinates, previousPosition)
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100256
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700257 updateSelection(
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700258 newPosition = newPositionInContainer,
259 previousPosition = previousPositionInContainer,
260 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700261 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700262 )
263 }
haoyue6d80a12020-12-02 16:04:52 -0800264
265 selectionRegistrar.onSelectionUpdateEndCallback = {
266 showSelectionToolbar()
Zach Klippensteinadabe342021-11-11 16:38:13 -0800267 // This property is set by updateSelection while dragging, so we need to clear it after
268 // the original selection drag.
269 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800270 currentDragPosition = null
haoyue6d80a12020-12-02 16:04:52 -0800271 }
haoyu9085c882020-12-08 12:01:06 -0800272
haoyue04245e2021-03-08 14:52:56 -0800273 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
274 if (selectableKey in selectionRegistrar.subselections) {
haoyu9085c882020-12-08 12:01:06 -0800275 // clear the selection range of each Selectable.
276 onRelease()
277 selection = null
278 }
279 }
haoyue04245e2021-03-08 14:52:56 -0800280
281 selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
282 if (
283 selectableKey == selection?.start?.selectableId ||
284 selectableKey == selection?.end?.selectableId
285 ) {
286 // The selectable that contains a selection handle just unsubscribed.
287 // Hide selection handles for now
288 startHandlePosition = null
289 endHandlePosition = null
290 }
291 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100292 }
293
Zach Klippensteinadabe342021-11-11 16:38:13 -0800294 /**
295 * Returns the [Selectable] responsible for managing the given [AnchorInfo], or null if the
296 * anchor is not from a currently-registered [Selectable].
297 */
298 internal fun getAnchorSelectable(anchor: AnchorInfo): Selectable? {
299 return selectionRegistrar.selectableMap[anchor.selectableId]
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100300 }
301
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100302 private fun updateHandleOffsets() {
303 val selection = selection
304 val containerCoordinates = containerLayoutCoordinates
Zach Klippensteinadabe342021-11-11 16:38:13 -0800305 val startSelectable = selection?.start?.let(::getAnchorSelectable)
306 val endSelectable = selection?.end?.let(::getAnchorSelectable)
haoyue04245e2021-03-08 14:52:56 -0800307 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
308 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
haoyue2678c62020-12-09 08:39:12 -0800309 if (
310 selection == null ||
311 containerCoordinates == null ||
312 !containerCoordinates.isAttached ||
313 startLayoutCoordinates == null ||
314 endLayoutCoordinates == null
315 ) {
316 this.startHandlePosition = null
317 this.endHandlePosition = null
318 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100319 }
haoyue2678c62020-12-09 08:39:12 -0800320
George Mount77ca2a22020-12-11 17:46:19 +0000321 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800322 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800323 startSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800324 selection = selection,
325 isStartHandle = true
326 )
327 )
George Mount77ca2a22020-12-11 17:46:19 +0000328 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800329 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800330 endSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800331 selection = selection,
332 isStartHandle = false
333 )
334 )
335
336 val visibleBounds = containerCoordinates.visibleBounds()
337 this.startHandlePosition =
338 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
339 this.endHandlePosition =
340 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100341 }
342
343 /**
344 * Returns non-nullable [containerLayoutCoordinates].
345 */
346 internal fun requireContainerCoordinates(): LayoutCoordinates {
347 val coordinates = containerLayoutCoordinates
348 require(coordinates != null)
349 require(coordinates.isAttached)
350 return coordinates
351 }
352
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700353 internal fun selectAll(
354 selectableId: Long,
355 previousSelection: Selection?
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700356 ): Pair<Selection?, Map<Long, Selection>> {
357 val subselections = mutableMapOf<Long, Selection>()
358 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
359 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
360 val selection = if (selectable.selectableId == selectableId)
361 selectable.getSelectAllSelection() else null
362 selection?.let { subselections[selectable.selectableId] = it }
363 merge(mergedSelection, selection)
364 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700365 if (newSelection != previousSelection) {
366 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
367 }
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700368 return Pair(newSelection, subselections)
369 }
370
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800371 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100372 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800373 var selectedText: AnnotatedString? = null
374
375 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000376 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800377 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800378 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800379 if (selectable.selectableId != it.start.selectableId &&
380 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800381 selectedText == null
382 ) continue
383
384 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800385 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800386 selection = it
387 )
388 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
389
390 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800391 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
392 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800393 ) break
394 }
395 }
396 return selectedText
397 }
398
Qingqing Dengde023cc2020-04-24 14:23:41 -0700399 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700400 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700401 selectedText?.let { clipboardManager?.setText(it) }
402 }
403
404 /**
405 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
406 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
407 * the copy method as a callback when "copy" is clicked.
408 */
409 internal fun showSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800410 if (hasFocus) {
411 selection?.let {
412 textToolbar?.showMenu(
413 getContentRect(),
414 onCopyRequested = {
415 copy()
416 onRelease()
417 }
418 )
419 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700420 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700421 }
422
haoyue6d80a12020-12-02 16:04:52 -0800423 internal fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800424 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800425 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700426 }
427 }
428
429 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800430 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700431 showSelectionToolbar()
432 }
433 }
434
435 /**
436 * Calculate selected region as [Rect]. The top is the top of the first selected
437 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
438 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
439 */
440 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700441 val selection = selection ?: return Rect.Zero
Zach Klippensteinadabe342021-11-11 16:38:13 -0800442 val startSelectable = getAnchorSelectable(selection.start)
443 val endSelectable = getAnchorSelectable(selection.end)
haoyue04245e2021-03-08 14:52:56 -0800444 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
445 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700446
447 val localLayoutCoordinates = containerLayoutCoordinates
448 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000449 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700450 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800451 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700452 selection = selection,
453 isStartHandle = true
454 )
455 )
George Mount77ca2a22020-12-11 17:46:19 +0000456 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700457 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800458 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700459 selection = selection,
460 isStartHandle = false
461 )
462 )
463
464 startOffset = localLayoutCoordinates.localToRoot(startOffset)
465 endOffset = localLayoutCoordinates.localToRoot(endOffset)
466
467 val left = min(startOffset.x, endOffset.x)
468 val right = max(startOffset.x, endOffset.x)
469
George Mount77ca2a22020-12-11 17:46:19 +0000470 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700471 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700472 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700473 0f,
haoyue04245e2021-03-08 14:52:56 -0800474 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700475 )
476 )
477
George Mount77ca2a22020-12-11 17:46:19 +0000478 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700479 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700480 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700481 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800482 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700483 )
484 )
485
486 startTop = localLayoutCoordinates.localToRoot(startTop)
487 endTop = localLayoutCoordinates.localToRoot(endTop)
488
489 val top = min(startTop.y, endTop.y)
Haoyu Zhangd47f1fe2021-07-13 03:10:55 -0700490 val bottom = max(startOffset.y, endOffset.y) + (HandleHeight.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700491
492 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700493 left,
494 top,
495 right,
496 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700497 )
498 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700499 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700500 }
501
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800502 // This is for PressGestureDetector to cancel the selection.
503 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800504 selectionRegistrar.subselections = emptyMap()
haoyue6d80a12020-12-02 16:04:52 -0800505 hideSelectionToolbar()
haoyue04245e2021-03-08 14:52:56 -0800506 if (selection != null) {
507 onSelectionChange(null)
508 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
509 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800510 }
511
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800512 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver = object : TextDragObserver {
513 override fun onDown(point: Offset) {
Zach Klippenstein63870892022-01-14 12:45:18 -0800514 val selection = selection ?: return
515 val anchor = if (isStartHandle) selection.start else selection.end
516 val selectable = getAnchorSelectable(anchor) ?: return
517 // The LayoutCoordinates of the composable where the drag gesture should begin. This
518 // is used to convert the position of the beginning of the drag gesture from the
519 // composable coordinates to selection container coordinates.
520 val beginLayoutCoordinates = selectable.getLayoutCoordinates() ?: return
521
522 // The position of the character where the drag gesture should begin. This is in
523 // the composable coordinates.
524 val beginCoordinates = getAdjustedCoordinates(
525 selectable.getHandlePosition(
526 selection = selection, isStartHandle = isStartHandle
527 )
528 )
529
530 // Convert the position where drag gesture begins from composable coordinates to
531 // selection container coordinates.
532 currentDragPosition = requireContainerCoordinates().localPositionOf(
533 beginLayoutCoordinates,
534 beginCoordinates
535 )
536 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800537 }
538
539 override fun onUp() {
Zach Klippenstein63870892022-01-14 12:45:18 -0800540 draggingHandle = null
541 currentDragPosition = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800542 }
543
544 override fun onStart(startPoint: Offset) {
545 hideSelectionToolbar()
546 val selection = selection!!
547 val startSelectable =
548 selectionRegistrar.selectableMap[selection.start.selectableId]
549 val endSelectable =
550 selectionRegistrar.selectableMap[selection.end.selectableId]
551 // The LayoutCoordinates of the composable where the drag gesture should begin. This
552 // is used to convert the position of the beginning of the drag gesture from the
553 // composable coordinates to selection container coordinates.
554 val beginLayoutCoordinates = if (isStartHandle) {
555 startSelectable?.getLayoutCoordinates()!!
556 } else {
557 endSelectable?.getLayoutCoordinates()!!
558 }
559
560 // The position of the character where the drag gesture should begin. This is in
561 // the composable coordinates.
562 val beginCoordinates = getAdjustedCoordinates(
563 if (isStartHandle) {
564 startSelectable!!.getHandlePosition(
565 selection = selection, isStartHandle = true
566 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700567 } else {
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800568 endSelectable!!.getHandlePosition(
569 selection = selection, isStartHandle = false
570 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700571 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800572 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700573
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800574 // Convert the position where drag gesture begins from composable coordinates to
575 // selection container coordinates.
576 dragBeginPosition = requireContainerCoordinates().localPositionOf(
577 beginLayoutCoordinates,
578 beginCoordinates
579 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700580
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800581 // Zero out the total distance that being dragged.
582 dragTotalDistance = Offset.Zero
583 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700584
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800585 override fun onDrag(delta: Offset) {
586 dragTotalDistance += delta
587 val endPosition = dragBeginPosition + dragTotalDistance
588 val consumed = updateSelection(
589 newPosition = endPosition,
590 previousPosition = dragBeginPosition,
591 isStartHandle = isStartHandle,
592 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
593 )
594 if (consumed) {
595 dragBeginPosition = endPosition
Nader Jawad6df06122020-06-03 15:27:08 -0700596 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700597 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800598 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700599
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800600 override fun onStop() {
601 showSelectionToolbar()
602 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800603 currentDragPosition = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800604 }
haoyue6d80a12020-12-02 16:04:52 -0800605
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800606 override fun onCancel() {
607 showSelectionToolbar()
608 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800609 currentDragPosition = null
Qingqing Deng6f56a912019-05-13 10:10:37 -0700610 }
611 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700612
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700613 /**
614 * Detect tap without consuming the up event.
615 */
616 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
George Mount32de9dd2022-10-05 14:51:06 -0700617 awaitEachGesture {
618 waitForUpOrCancellation()?.let {
619 onTap(it.position)
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700620 }
621 }
622 }
623
624 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
625 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
626 }
627
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700628 private fun convertToContainerCoordinates(
629 layoutCoordinates: LayoutCoordinates,
630 offset: Offset
631 ): Offset? {
632 val coordinates = containerLayoutCoordinates
633 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000634 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700635 }
636
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700637 /**
638 * Cancel the previous selection and start a new selection at the given [position].
639 * It's used for long-press, double-click, triple-click and so on to start selection.
640 *
641 * @param position initial position of the selection. Both start and end handle is considered
642 * at this position.
643 * @param isStartHandle whether it's considered as the start handle moving. This parameter
644 * will influence the [SelectionAdjustment]'s behavior. For example,
645 * [SelectionAdjustment.Character] only adjust the moving handle.
646 * @param adjustment the selection adjustment.
647 */
648 private fun startSelection(
649 position: Offset,
650 isStartHandle: Boolean,
651 adjustment: SelectionAdjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700652 ) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700653 updateSelection(
654 startHandlePosition = position,
655 endHandlePosition = position,
656 previousHandlePosition = null,
657 isStartHandle = isStartHandle,
658 adjustment = adjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700659 )
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700660 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700661
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700662 /**
663 * Updates the selection after one of the selection handle moved.
664 *
665 * @param newPosition the new position of the moving selection handle.
666 * @param previousPosition the previous position of the moving selection handle.
667 * @param isStartHandle whether the moving selection handle is the start handle.
668 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
669 * produce the final selection range.
670 *
671 * @return a boolean representing whether the movement is consumed.
672 *
673 * @see SelectionAdjustment
674 */
675 internal fun updateSelection(
676 newPosition: Offset?,
677 previousPosition: Offset?,
678 isStartHandle: Boolean,
679 adjustment: SelectionAdjustment,
680 ): Boolean {
681 if (newPosition == null) return false
682 val otherHandlePosition = selection?.let { selection ->
683 val otherSelectableId = if (isStartHandle) {
684 selection.end.selectableId
685 } else {
686 selection.start.selectableId
687 }
688 val otherSelectable =
689 selectionRegistrar.selectableMap[otherSelectableId] ?: return@let null
690 convertToContainerCoordinates(
691 otherSelectable.getLayoutCoordinates()!!,
692 getAdjustedCoordinates(
693 otherSelectable.getHandlePosition(selection, !isStartHandle)
694 )
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700695 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700696 } ?: return false
697
698 val startHandlePosition = if (isStartHandle) newPosition else otherHandlePosition
699 val endHandlePosition = if (isStartHandle) otherHandlePosition else newPosition
700
701 return updateSelection(
702 startHandlePosition = startHandlePosition,
703 endHandlePosition = endHandlePosition,
704 previousHandlePosition = previousPosition,
705 isStartHandle = isStartHandle,
706 adjustment = adjustment
707 )
708 }
709
710 /**
711 * Updates the selection after one of the selection handle moved.
712 *
713 * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
714 * selection handle is updated each time. The only exception is that when a new selection is
715 * started. In this case, [previousHandlePosition] is always null.
716 *
717 * @param startHandlePosition the position of the start selection handle.
718 * @param endHandlePosition the position of the end selection handle.
719 * @param previousHandlePosition the position of the moving handle before the update.
720 * @param isStartHandle whether the moving selection handle is the start handle.
721 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
722 * produce the final selection range.
723 *
724 * @return a boolean representing whether the movement is consumed. It's useful for the case
725 * where a selection handle is updating consecutively. When the return value is true, it's
726 * expected that the caller will update the [startHandlePosition] to be the given
727 * [endHandlePosition] in following calls.
728 *
729 * @see SelectionAdjustment
730 */
731 internal fun updateSelection(
732 startHandlePosition: Offset,
733 endHandlePosition: Offset,
734 previousHandlePosition: Offset?,
735 isStartHandle: Boolean,
736 adjustment: SelectionAdjustment,
737 ): Boolean {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800738 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Zach Klippenstein63870892022-01-14 12:45:18 -0800739 currentDragPosition = if (isStartHandle) startHandlePosition else endHandlePosition
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700740 val newSubselections = mutableMapOf<Long, Selection>()
741 var moveConsumed = false
742 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
743 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
744 val previousSubselection =
745 selectionRegistrar.subselections[selectable.selectableId]
746 val (selection, consumed) = selectable.updateSelection(
747 startHandlePosition = startHandlePosition,
748 endHandlePosition = endHandlePosition,
749 previousHandlePosition = previousHandlePosition,
750 isStartHandle = isStartHandle,
751 containerLayoutCoordinates = requireContainerCoordinates(),
752 adjustment = adjustment,
753 previousSelection = previousSubselection,
754 )
755
756 moveConsumed = moveConsumed || consumed
757 selection?.let { newSubselections[selectable.selectableId] = it }
758 merge(mergedSelection, selection)
759 }
760 if (newSelection != selection) {
761 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
762 selectionRegistrar.subselections = newSubselections
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700763 onSelectionChange(newSelection)
764 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700765 return moveConsumed
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700766 }
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200767
768 fun contextMenuOpenAdjustment(position: Offset) {
769 val isEmptySelection = selection?.toTextRange()?.collapsed ?: true
770 // TODO(b/209483184) the logic should be more complex here, it should check that current
771 // selection doesn't include click position
772 if (isEmptySelection) {
773 startSelection(
774 position = position,
775 isStartHandle = true,
776 adjustment = SelectionAdjustment.Word
777 )
778 }
779 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700780}
781
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200782internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700783 return lhs?.merge(rhs) ?: rhs
784}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800785
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100786internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
787
Zach Klippensteinadabe342021-11-11 16:38:13 -0800788internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
789
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800790internal fun calculateSelectionMagnifierCenterAndroid(
791 manager: SelectionManager,
792 magnifierSize: IntSize
793): Offset {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800794 fun getMagnifierCenter(anchor: AnchorInfo, isStartHandle: Boolean): Offset {
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800795 val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
796 val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
797 val selectableCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified
798 // The end offset is exclusive.
799 val offset = if (isStartHandle) anchor.offset else anchor.offset - 1
800
801 // The horizontal position doesn't snap to cursor positions but should directly track the
802 // actual drag.
803 val localDragPosition = selectableCoordinates.localPositionOf(
804 containerCoordinates,
805 manager.currentDragPosition!!
806 )
807 val dragX = localDragPosition.x
808 // But it is constrained by the horizontal bounds of the current line.
809 val centerX = selectable.getRangeOfLineContaining(offset).let { line ->
810 val lineMin = selectable.getBoundingBox(line.min)
811 // line.end is exclusive, but we want the bounding box of the actual last character in
812 // the line.
813 val lineMax = selectable.getBoundingBox((line.max - 1).coerceAtLeast(line.min))
814 val minX = minOf(lineMin.left, lineMax.left)
815 val maxX = maxOf(lineMin.right, lineMax.right)
816 dragX.coerceIn(minX, maxX)
817 }
818
819 // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
820 // magnifier actually is). See
821 // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
822 if ((dragX - centerX).absoluteValue > magnifierSize.width / 2) {
823 return Offset.Unspecified
824 }
Zach Klippensteinadabe342021-11-11 16:38:13 -0800825
826 // Let the selectable determine the vertical position of the magnifier, since it should be
827 // clamped to the center of text lines.
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800828 val anchorBounds = selectable.getBoundingBox(offset)
829 val centerY = anchorBounds.center.y
830
Zach Klippensteinadabe342021-11-11 16:38:13 -0800831 return containerCoordinates.localPositionOf(
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800832 sourceCoordinates = selectableCoordinates,
833 relativeToSource = Offset(centerX, centerY)
Zach Klippensteinadabe342021-11-11 16:38:13 -0800834 )
835 }
836
837 val selection = manager.selection ?: return Offset.Unspecified
838 return when (manager.draggingHandle) {
839 null -> return Offset.Unspecified
840 Handle.SelectionStart -> getMagnifierCenter(selection.start, isStartHandle = true)
841 Handle.SelectionEnd -> getMagnifierCenter(selection.end, isStartHandle = false)
842 Handle.Cursor -> error("SelectionContainer does not support cursor")
843 }
844}
845
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200846internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800847 selectable: Selectable,
848 selection: Selection
849): AnnotatedString {
850 val currentText = selectable.getText()
851
852 return if (
haoyue04245e2021-03-08 14:52:56 -0800853 selectable.selectableId != selection.start.selectableId &&
854 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800855 ) {
856 // Select the full text content if the current selectable is between the
857 // start and the end selectables.
858 currentText
859 } else if (
haoyue04245e2021-03-08 14:52:56 -0800860 selectable.selectableId == selection.start.selectableId &&
861 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800862 ) {
863 // Select partial text content if the current selectable is the start and
864 // the end selectable.
865 if (selection.handlesCrossed) {
866 currentText.subSequence(selection.end.offset, selection.start.offset)
867 } else {
868 currentText.subSequence(selection.start.offset, selection.end.offset)
869 }
haoyue04245e2021-03-08 14:52:56 -0800870 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800871 // Select partial text content if the current selectable is the start
872 // selectable.
873 if (selection.handlesCrossed) {
874 currentText.subSequence(0, selection.start.offset)
875 } else {
876 currentText.subSequence(selection.start.offset, currentText.length)
877 }
878 } else {
879 // Selectable partial text content if the current selectable is the end
880 // selectable.
881 if (selection.handlesCrossed) {
882 currentText.subSequence(selection.end.offset, currentText.length)
883 } else {
884 currentText.subSequence(0, selection.end.offset)
885 }
886 }
887}
haoyue2678c62020-12-09 08:39:12 -0800888
889/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800890internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800891 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
892 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
893 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000894 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800895 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000896 windowToLocal(boundsInWindow.topLeft),
897 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800898 )
899}
900
haoyuac341f02021-01-22 22:01:56 -0800901internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800902 offset.x in left..right && offset.y in top..bottom