@@ -18,6 +18,7 @@ package androidx.compose.foundation.text
1818
1919import androidx.compose.foundation.ComposeFoundationFlags
2020import androidx.compose.foundation.ExperimentalFoundationApi
21+ import androidx.compose.foundation.contextmenu.contextMenuGestures
2122import androidx.compose.foundation.layout.Box
2223import androidx.compose.foundation.text.contextmenu.data.TextContextMenuKeys
2324import androidx.compose.foundation.text.contextmenu.data.TextContextMenuSession
@@ -37,6 +38,7 @@ import androidx.compose.runtime.MutableState
3738import androidx.compose.runtime.mutableStateOf
3839import androidx.compose.runtime.neverEqualPolicy
3940import androidx.compose.runtime.remember
41+ import androidx.compose.runtime.rememberCoroutineScope
4042import androidx.compose.runtime.snapshotFlow
4143import androidx.compose.ui.ExperimentalComposeUiApi
4244import androidx.compose.ui.Modifier
@@ -54,12 +56,14 @@ import kotlin.time.Duration
5456import kotlin.time.Duration.Companion.milliseconds
5557import kotlin.time.Duration.Companion.seconds
5658import kotlinx.coroutines.CancellableContinuation
59+ import kotlinx.coroutines.CoroutineScope
5760import kotlinx.coroutines.FlowPreview
5861import kotlinx.coroutines.coroutineScope
5962import kotlinx.coroutines.delay
6063import kotlinx.coroutines.flow.filterNotNull
6164import kotlinx.coroutines.launch
6265import kotlinx.coroutines.suspendCancellableCoroutine
66+ import platform.CoreGraphics.CGRectMake
6367
6468/* *
6569 * Context menu area for [BasicTextField] (with [TextFieldValue] argument).
@@ -73,7 +77,7 @@ internal actual fun ContextMenuArea(
7377 // The first time the menu is called up, the menu item provider contains a non-final set of
7478 // menu items, which causes the context menu callout to blink.
7579 // Adding a small delay resolves this issue.
76- ProvideDefaultPlatformTextContextMenuProviders (
80+ ProvideNewContextMenuDefaultProviders (
7781 menuDelay = 100 .milliseconds,
7882 modifier = manager.contextMenuAreaModifier,
7983 content = content
@@ -100,12 +104,17 @@ internal actual fun ContextMenuArea(
100104 } else {
101105 Modifier
102106 }
103- ProvideDefaultPlatformTextContextMenuProviders (
107+ ProvideNewContextMenuDefaultProviders (
104108 modifier = modifier,
105109 content = content
106110 )
107111 } else {
108- CommonContextMenuArea (selectionState, enabled, content)
112+ val scope = rememberCoroutineScope()
113+ PlatformContextMenu (
114+ getState = { selectionState.contextMenuItemsState(scope) },
115+ enabled = enabled,
116+ content = content
117+ )
109118 }
110119}
111120
@@ -119,18 +128,21 @@ internal actual fun ContextMenuArea(
119128 content : @Composable () -> Unit
120129) {
121130 if (ComposeFoundationFlags .isNewContextMenuEnabled) {
122- ProvideDefaultPlatformTextContextMenuProviders (
131+ ProvideNewContextMenuDefaultProviders (
123132 modifier = manager.contextMenuAreaModifier,
124133 content = content
125134 )
126135 } else {
127- CommonContextMenuArea (manager, content)
136+ PlatformContextMenu (
137+ getState = { manager.contextMenuItemsState() },
138+ content = content
139+ )
128140 }
129141}
130142
131143@OptIn(ExperimentalComposeUiApi ::class )
132144@Composable
133- private fun ProvideDefaultPlatformTextContextMenuProviders (
145+ private fun ProvideNewContextMenuDefaultProviders (
134146 menuDelay : Duration = 0.seconds,
135147 modifier : Modifier = Modifier ,
136148 content : @Composable () -> Unit
@@ -155,9 +167,10 @@ private fun ProvideDefaultPlatformTextContextMenuProviders(
155167 coordinates = { layoutCoordinates.value }
156168 )
157169 }
170+
158171 CompositionLocalProvider (
159- LocalTextContextMenuToolbarProvider provides (toolbarProvider ? : provider) ,
160- LocalTextContextMenuDropdownProvider provides (dropdownProvider ? : provider) ,
172+ LocalTextContextMenuToolbarProvider providesDefault provider,
173+ LocalTextContextMenuDropdownProvider providesDefault provider,
161174 content = {
162175 Box (
163176 modifier = modifier.onGloballyPositioned { layoutCoordinates.value = it }
@@ -168,6 +181,10 @@ private fun ProvideDefaultPlatformTextContextMenuProviders(
168181 }
169182 }
170183 )
184+ } else {
185+ Box (modifier = modifier, propagateMinConstraints = true ) {
186+ content()
187+ }
171188 }
172189}
173190
@@ -188,7 +205,7 @@ private class ContextMenuToolbarProvider(
188205 @OptIn(FlowPreview ::class )
189206 override suspend fun showTextContextMenu (dataProvider : TextContextMenuDataProvider ) {
190207 var session: TextContextMenuSession ? = null
191- val result = coroutineScope {
208+ coroutineScope {
192209 val job = launch {
193210 delay(menuDelay)
194211 snapshotFlow {
@@ -240,10 +257,107 @@ private class ContextMenuToolbarProvider(
240257 }
241258 job.cancel()
242259 }
243- return result
244260 }
245261}
246262
263+ @Composable
264+ private fun PlatformContextMenu (
265+ getState : () -> ContextMenuItemsState ,
266+ enabled : Boolean = true,
267+ content : @Composable () -> Unit
268+ ) {
269+ val layoutCoordinates: MutableState <LayoutCoordinates ?> = remember {
270+ mutableStateOf(value = null , policy = neverEqualPolicy())
271+ }
272+ val editMenuView = remember {
273+ CMPEditMenuView ().also {
274+ it.userInteractionEnabled = false
275+ }
276+ }
277+
278+ val modifier = if (enabled) {
279+ val density = LocalDensity .current
280+ Modifier .onGloballyPositioned {
281+ layoutCoordinates.value = it
282+ }.contextMenuGestures { offset ->
283+ val coordinates = layoutCoordinates.value ? : return @contextMenuGestures
284+ val layoutPosition = coordinates.positionInWindow()
285+ val layoutBounds = coordinates.boundsInWindow()
286+
287+ val state = getState()
288+ val rect = CGRectMake (
289+ x = with (density) {
290+ (offset.x + layoutPosition.x - layoutBounds.left).toDp().value.toDouble()
291+ },
292+ y = with (density) {
293+ (offset.y + layoutPosition.y - layoutBounds.top).toDp().value.toDouble()
294+ },
295+ width = 1.0 ,
296+ height = 1.0
297+ )
298+ editMenuView.showEditMenuAtRect(
299+ rect,
300+ state.copy,
301+ state.cut,
302+ state.paste,
303+ state.selectAll
304+ )
305+ }
306+ } else {
307+ Modifier
308+ }
309+ Box (
310+ modifier = modifier.then(ContextMenuLayoutElement (editMenuView)),
311+ propagateMinConstraints = true ,
312+ ) {
313+ content()
314+ }
315+ }
316+
317+ private fun TextFieldSelectionState.contextMenuItemsState (scope : CoroutineScope ): ContextMenuItemsState {
318+ return ContextMenuItemsState (
319+ copy = if (canCopy()) {
320+ { scope.launch { copy(cancelSelection = true ) } }
321+ } else {
322+ null
323+ },
324+ paste = if (canPaste()) {
325+ { scope.launch { paste() } }
326+ } else {
327+ null
328+ },
329+ cut = if (canCut()) {
330+ { scope.launch { cut() } }
331+ } else {
332+ null
333+ },
334+ selectAll = if (canSelectAll()) {
335+ { selectAll() }
336+ } else {
337+ null
338+ },
339+ rect = Rect .Zero
340+ )
341+ }
342+
343+ private fun SelectionManager.contextMenuItemsState (): ContextMenuItemsState {
344+ return ContextMenuItemsState (
345+ copy = if (isNonEmptySelection()) {
346+ { copy() }
347+ } else {
348+ null
349+ },
350+ paste = null ,
351+ cut = null ,
352+ selectAll = if (! isEntireContainerSelected()) {
353+ { selectAll() }
354+ } else {
355+ null
356+ },
357+ rect = Rect .Zero
358+ )
359+ }
360+
247361private class TextContextMenuSessionImpl (
248362 val editMenuView : CMPEditMenuView ,
249363 val continuation : CancellableContinuation <Unit >
0 commit comments