1 /*
<lambda>null2  * Copyright 2021 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 
17 package androidx.compose.foundation.text.selection
18 
19 import androidx.compose.foundation.internal.toClipEntry
20 import androidx.compose.foundation.text.ContextMenuArea
21 import androidx.compose.foundation.text.detectDownAndDragGesturesWithObserver
22 import androidx.compose.runtime.Composable
23 import androidx.compose.runtime.CompositionLocalProvider
24 import androidx.compose.runtime.DisposableEffect
25 import androidx.compose.runtime.getValue
26 import androidx.compose.runtime.mutableStateOf
27 import androidx.compose.runtime.remember
28 import androidx.compose.runtime.rememberCoroutineScope
29 import androidx.compose.runtime.saveable.rememberSaveable
30 import androidx.compose.runtime.setValue
31 import androidx.compose.ui.Modifier
32 import androidx.compose.ui.geometry.Offset
33 import androidx.compose.ui.input.pointer.pointerInput
34 import androidx.compose.ui.platform.LocalClipboard
35 import androidx.compose.ui.platform.LocalHapticFeedback
36 import androidx.compose.ui.platform.LocalTextToolbar
37 import androidx.compose.ui.util.fastForEach
38 import kotlinx.coroutines.CoroutineStart
39 import kotlinx.coroutines.launch
40 
41 /**
42  * Enables text selection for its direct or indirect children.
43  *
44  * Use of a lazy layout, such as [LazyRow][androidx.compose.foundation.lazy.LazyRow] or
45  * [LazyColumn][androidx.compose.foundation.lazy.LazyColumn], within a [SelectionContainer] has
46  * undefined behavior on text items that aren't composed. For example, texts that aren't composed
47  * will not be included in copy operations and select all will not expand the selection to include
48  * them.
49  *
50  * @sample androidx.compose.foundation.samples.SelectionSample
51  */
52 @Composable
53 fun SelectionContainer(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
54     var selection by remember { mutableStateOf<Selection?>(null) }
55     SelectionContainer(
56         modifier = modifier,
57         selection = selection,
58         onSelectionChange = { selection = it },
59         children = content
60     )
61 }
62 
63 /**
64  * Disables text selection for its direct or indirect children. To use this, simply add this to wrap
65  * one or more text composables.
66  *
67  * @sample androidx.compose.foundation.samples.DisableSelectionSample
68  */
69 @Composable
DisableSelectionnull70 fun DisableSelection(content: @Composable () -> Unit) {
71     CompositionLocalProvider(LocalSelectionRegistrar provides null, content = content)
72 }
73 
74 /**
75  * Selection Composable.
76  *
77  * The selection composable wraps composables and let them to be selectable. It paints the selection
78  * area with start and end handles.
79  */
80 @Suppress("ComposableLambdaParameterNaming")
81 @Composable
82 internal fun SelectionContainer(
83     /** A [Modifier] for SelectionContainer. */
84     modifier: Modifier = Modifier,
85     /** Current Selection status. */
86     selection: Selection?,
87     /** A function containing customized behaviour when selection changes. */
88     onSelectionChange: (Selection?) -> Unit,
89     children: @Composable () -> Unit
90 ) {
91     val registrarImpl =
<lambda>null92         rememberSaveable(saver = SelectionRegistrarImpl.Saver) { SelectionRegistrarImpl() }
93 
<lambda>null94     val manager = remember { SelectionManager(registrarImpl) }
95 
96     val clipboard = LocalClipboard.current
97     val coroutineScope = rememberCoroutineScope()
98     manager.hapticFeedBack = LocalHapticFeedback.current
99     manager.onCopyHandler =
<lambda>null100         remember(coroutineScope, clipboard) {
101             { textToCopy ->
102                 coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
103                     clipboard.setClipEntry(textToCopy.toClipEntry())
104                 }
105             }
106         }
107     manager.textToolbar = LocalTextToolbar.current
108     manager.onSelectionChange = onSelectionChange
109     manager.selection = selection
110 
111     /*
112      * Need a layout for selection gestures that span multiple text children.
113      *
114      * b/372053402: SimpleLayout must be the top layout in this composable because
115      *     the modifier argument must be applied to the top layout in case it contains
116      *     something like `Modifier.weight`.
117      */
<lambda>null118     SimpleLayout(modifier = modifier.then(manager.modifier)) {
119         ContextMenuArea(manager) {
120             CompositionLocalProvider(LocalSelectionRegistrar provides registrarImpl) {
121                 children()
122                 if (
123                     manager.isInTouchMode &&
124                         manager.hasFocus &&
125                         !manager.isTriviallyCollapsedSelection()
126                 ) {
127                     manager.selection?.let {
128                         listOf(true, false).fastForEach { isStartHandle ->
129                             val observer =
130                                 remember(isStartHandle) {
131                                     manager.handleDragObserver(isStartHandle)
132                                 }
133 
134                             val positionProvider: () -> Offset =
135                                 remember(isStartHandle) {
136                                     if (isStartHandle) {
137                                         { manager.startHandlePosition ?: Offset.Unspecified }
138                                     } else {
139                                         { manager.endHandlePosition ?: Offset.Unspecified }
140                                     }
141                                 }
142 
143                             val direction =
144                                 if (isStartHandle) {
145                                     it.start.direction
146                                 } else {
147                                     it.end.direction
148                                 }
149 
150                             val lineHeight =
151                                 if (isStartHandle) {
152                                     manager.startHandleLineHeight
153                                 } else {
154                                     manager.endHandleLineHeight
155                                 }
156                             SelectionHandle(
157                                 offsetProvider = positionProvider,
158                                 isStartHandle = isStartHandle,
159                                 direction = direction,
160                                 handlesCrossed = it.handlesCrossed,
161                                 lineHeight = lineHeight,
162                                 modifier =
163                                     Modifier.pointerInput(observer) {
164                                         detectDownAndDragGesturesWithObserver(observer)
165                                     },
166                             )
167                         }
168                     }
169                 }
170             }
171         }
172     }
173 
<lambda>null174     DisposableEffect(manager) {
175         onDispose {
176             manager.onRelease()
177             manager.hasFocus = false
178         }
179     }
180 }
181