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