diff --git a/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/components/Selection.kt b/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/components/Selection.kt index e7ad50cf2e107..24da416ebb6ad 100644 --- a/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/components/Selection.kt +++ b/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/components/Selection.kt @@ -26,8 +26,10 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll +import androidx.compose.material.TextField import androidx.compose.material3.Button import androidx.compose.material3.Text +import androidx.compose.mpp.demo.textfield.ClearFocusBox import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -40,46 +42,60 @@ import androidx.compose.ui.unit.dp @Composable fun SelectionExample() { var count by remember { mutableStateOf(0) } - Column { - Button(onClick = { count++ }) { - Text("Outside Count: $count") - } - SelectionContainer( - Modifier.padding(24.dp).fillMaxWidth() - ) { - Column { - Text( - "I'm a selection container. Double tap on word to select a word." + - " Triple tap on content to select whole paragraph.\nAnother paragraph for testing.\n" + - "And another one." - ) - Row { - DisableSelection { + val textState = remember { + mutableStateOf( + buildString { + repeat(3) { + appendLine("Text line $it") + } + } + ) + } + ClearFocusBox { + Column { + Button(onClick = { count++ }) { + Text("Outside Count: $count") + } + SelectionContainer( + Modifier.padding(24.dp).fillMaxWidth() + ) { + Column { + TextField( + textState.value, { textState.value = it }, + ) + Text( + "I'm a selection container. Double tap on word to select a word." + + " Triple tap on content to select whole paragraph.\nAnother paragraph for testing.\n" + + "And another one." + ) + Row { + DisableSelection { + Button(onClick = { count++ }) { + Text("DisableSelection Count: $count") + } + } Button(onClick = { count++ }) { - Text("DisableSelection Count: $count") + Text("SelectionContainer Count: $count") } } - Button(onClick = { count++ }) { - Text("SelectionContainer Count: $count") - } + Text("I'm another Text() block. Let's try to select me!") + Text("I'm yet another Text() with multiparagraph structure block.\nLet's try to select me!") } - Text("I'm another Text() block. Let's try to select me!") - Text("I'm yet another Text() with multiparagraph structure block.\nLet's try to select me!") } - } - Column( - Modifier - .height(100.dp) - .padding(2.dp) - .border(1.dp, Color.Blue) - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - ) { - SelectionContainer { - Text( - text = "Select text and scroll\n".repeat(100), - modifier = Modifier.fillMaxWidth(), - ) + Column( + Modifier + .height(100.dp) + .padding(2.dp) + .border(1.dp, Color.Blue) + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + SelectionContainer { + Text( + text = "Select text and scroll\n".repeat(100), + modifier = Modifier.fillMaxWidth(), + ) + } } } } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt index 2832691a34720..134d4a573a0b9 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt @@ -22,18 +22,32 @@ import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type -import androidx.compose.ui.text.input.* +import androidx.compose.ui.scene.getConstraintsToFillParent +import androidx.compose.ui.text.input.CommitTextCommand +import androidx.compose.ui.text.input.EditCommand +import androidx.compose.ui.text.input.EditProcessor +import androidx.compose.ui.text.input.FinishComposingTextCommand +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.ImeOptions +import androidx.compose.ui.text.input.PlatformTextInputService +import androidx.compose.ui.text.input.SetComposingRegionCommand +import androidx.compose.ui.text.input.SetComposingTextCommand +import androidx.compose.ui.text.input.SetSelectionCommand +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.asCGRect +import androidx.compose.ui.unit.toDpRect import androidx.compose.ui.window.FocusStack import androidx.compose.ui.window.IntermediateTextInputUIView import androidx.compose.ui.window.KeyboardEventHandler -import androidx.compose.ui.scene.getConstraintsToFillParent -import androidx.compose.ui.unit.Density import kotlin.math.absoluteValue import kotlin.math.min import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import org.jetbrains.skia.BreakIterator -import platform.UIKit.* +import platform.UIKit.NSLayoutConstraint +import platform.UIKit.UIView +import platform.UIKit.reloadInputViews internal class UIKitTextInputService( private val updateView: () -> Unit, @@ -111,17 +125,7 @@ internal class UIKitTextInputService( currentImeOptions = imeOptions currentImeActionHandler = onImeActionPerformed - textUIView?.removeFromSuperview() - textUIView = IntermediateTextInputUIView( - viewConfiguration = viewConfiguration - ).also { - it.keyboardEventHandler = keyboardEventHandler - rootView.addSubview(it) - it.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activateConstraints( - getConstraintsToFillParent(it, rootView) - ) - } + attachIntermediateTextInputView() textUIView?.input = createSkikoInput(value) textUIView?.inputTraits = getUITextInputTraits(imeOptions) @@ -137,14 +141,7 @@ internal class UIKitTextInputService( textUIView?.inputTraits = EmptyInputTraits textUIView?.input = null - textUIView?.keyboardEventHandler = null - textUIView?.let { view -> - mainScope.launch { - view.resignFirstResponder() - view.removeFromSuperview() - } - } - textUIView = null + detachIntermediateTextInputView() } override fun showSoftwareKeyboard() { @@ -274,25 +271,23 @@ internal class UIKitTextInputService( onCutRequested: (() -> Unit)?, onSelectAllRequested: (() -> Unit)? ) { - textUIView?.let { - val skiaRect = with(densityProvider()) { - org.jetbrains.skia.Rect.makeLTRB( - l = rect.left / density, - t = rect.top / density, - r = rect.right / density, - b = rect.bottom / density, - ) - } - it.showTextMenu( - targetRect = skiaRect, - textActions = object : TextActions { - override val copy: (() -> Unit)? = onCopyRequested - override val cut: (() -> Unit)? = onCutRequested - override val paste: (() -> Unit)? = onPasteRequested - override val selectAll: (() -> Unit)? = onSelectAllRequested - } - ) + if (textUIView == null) { + // If showMenu() is called and textUIView is not created, + // then it means that showMenu() called in SelectionContainer without any textfields, + // and IntermediateTextInputView must be created to show an editing menu + attachIntermediateTextInputView() + textUIView?.becomeFirstResponder() + updateView() } + textUIView?.showTextMenu( + targetRect = rect.toDpRect(densityProvider()).asCGRect(), + textActions = object : TextActions { + override val copy: (() -> Unit)? = onCopyRequested + override val cut: (() -> Unit)? = onCutRequested + override val paste: (() -> Unit)? = onPasteRequested + override val selectAll: (() -> Unit)? = onSelectAllRequested + } + ) } /** @@ -300,6 +295,10 @@ internal class UIKitTextInputService( */ override fun hide() { textUIView?.hideTextMenu() + if ((textUIView != null) && (currentInput == null)) { // means that editing context menu shown in selection container + textUIView?.resignFirstResponder() + detachIntermediateTextInputView() + } } override val status: TextToolbarStatus @@ -308,6 +307,29 @@ internal class UIKitTextInputService( else TextToolbarStatus.Hidden + private fun attachIntermediateTextInputView() { + textUIView?.removeFromSuperview() + textUIView = IntermediateTextInputUIView( + viewConfiguration = viewConfiguration + ).also { + it.keyboardEventHandler = keyboardEventHandler + rootView.addSubview(it) + it.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activateConstraints( + getConstraintsToFillParent(it, rootView) + ) + } + } + + private fun detachIntermediateTextInputView() { + textUIView?.let { view -> + view.keyboardEventHandler = null + mainScope.launch { + view.removeFromSuperview() + } + } + textUIView = null + } private fun createSkikoInput(value: TextFieldValue) = object : IOSSkikoInput { /** diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/IntermediateTextInputUIView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/IntermediateTextInputUIView.uikit.kt index 81d767d731fa1..b68d4a774edc4 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/IntermediateTextInputUIView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/IntermediateTextInputUIView.uikit.kt @@ -501,14 +501,8 @@ internal class IntermediateTextInputUIView( * @param targetRect - rectangle of selected text area * @param textActions - available (not null) actions in text menu */ - fun showTextMenu(targetRect: org.jetbrains.skia.Rect, textActions: TextActions) { - val cgRect = CGRectMake( - x = targetRect.left.toDouble(), - y = targetRect.top.toDouble(), - width = targetRect.width.toDouble(), - height = targetRect.height.toDouble() - ) - val isTargetVisible = CGRectIntersectsRect(bounds, cgRect) + fun showTextMenu(targetRect: CValue, textActions: TextActions) { + val isTargetVisible = CGRectIntersectsRect(bounds, targetRect) if (isTargetVisible) { // TODO: UIMenuController is deprecated since iOS 17 and not available on iOS 12 @@ -519,7 +513,7 @@ internal class IntermediateTextInputUIView( cancelContextMenuUpdate() CoroutineScope(Dispatchers.Main + menuMonitoringJob).launch { delay(viewConfiguration.doubleTapTimeoutMillis) - menu.showMenuFromView(targetView = this@IntermediateTextInputUIView, cgRect) + menu.showMenuFromView(targetView = this@IntermediateTextInputUIView, targetRect) } _currentTextMenuActions = textActions } else {