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 c2ba77dfd9628..a1ea6562d6cc7 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 @@ -38,6 +38,7 @@ internal class UIKitTextInputService( private val updateView: () -> Unit, private val rootViewProvider: () -> UIView, private val densityProvider: () -> Density, + private val viewConfiguration: ViewConfiguration, private val focusStack: FocusStack?, private val keyboardEventHandler: KeyboardEventHandler, ) : PlatformTextInputService, TextToolbar { @@ -112,6 +113,7 @@ internal class UIKitTextInputService( textUIView?.removeFromSuperview() textUIView = IntermediateTextInputUIView( keyboardEventHandler = keyboardEventHandler, + viewConfiguration = viewConfiguration ).also { rootView.addSubview(it) it.translatesAutoresizingMaskIntoConstraints = false diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index f73296107b9a3..18add9d96f590 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -33,8 +33,10 @@ import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.interop.LocalUIKitInteropContainer import androidx.compose.ui.interop.LocalUIKitInteropContext +import androidx.compose.ui.interop.UIKitInteropContainer import androidx.compose.ui.interop.UIKitInteropContext import androidx.compose.ui.interop.UIKitInteropTransaction +import androidx.compose.ui.node.TrackInteropContainer import androidx.compose.ui.platform.AccessibilityMediator import androidx.compose.ui.platform.AccessibilitySyncOptions import androidx.compose.ui.platform.DefaultInputModeManager @@ -51,23 +53,21 @@ import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight import androidx.compose.ui.uikit.systemDensity +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.asCGRect import androidx.compose.ui.unit.asDpOffset import androidx.compose.ui.unit.asDpRect import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import androidx.compose.ui.unit.roundToIntRect +import androidx.compose.ui.unit.toDpRect import androidx.compose.ui.unit.toOffset import androidx.compose.ui.window.FocusStack import androidx.compose.ui.window.InteractionUIView -import androidx.compose.ui.interop.UIKitInteropContainer -import androidx.compose.ui.node.TrackInteropContainer -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.asCGRect -import androidx.compose.ui.unit.toDpRect import androidx.compose.ui.window.KeyboardEventHandler import androidx.compose.ui.window.KeyboardVisibilityListenerImpl import androidx.compose.ui.window.RenderingUIView @@ -245,6 +245,15 @@ internal class ComposeSceneMediator( NSLayoutConstraint.activateConstraints(value) } + private val viewConfiguration: ViewConfiguration = + object : ViewConfiguration by EmptyViewConfiguration { + override val touchSlop: Float + get() = with(density) { + // this value is originating from iOS 16 drag behavior reverse engineering + 10.dp.toPx() + } + } + private val scene: ComposeScene by lazy { composeSceneFactory( ::onComposeSceneInvalidate, @@ -360,6 +369,7 @@ internal class ComposeSceneMediator( }, rootViewProvider = { container }, densityProvider = { container.systemDensity }, + viewConfiguration = viewConfiguration, focusStack = focusStack, keyboardEventHandler = keyboardEventHandler ) @@ -698,13 +708,7 @@ internal class ComposeSceneMediator( override fun calculateLocalPosition(positionInWindow: Offset): Offset = windowContext.calculateLocalPosition(container, positionInWindow) - override val viewConfiguration = object : ViewConfiguration by EmptyViewConfiguration { - override val touchSlop: Float - get() = with(density) { - // this value is originating from iOS 16 drag behavior reverse engineering - 10.dp.toPx() - } - } + override val viewConfiguration = this@ComposeSceneMediator.viewConfiguration override val inputModeManager = DefaultInputModeManager(InputMode.Touch) override val textInputService = this@ComposeSceneMediator.uiKitTextInputService override val textToolbar = this@ComposeSceneMediator.uiKitTextInputService 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 af08e1f3b8f55..6be295290652d 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 @@ -20,10 +20,16 @@ import androidx.compose.ui.platform.EmptyInputTraits import androidx.compose.ui.platform.IOSSkikoInput import androidx.compose.ui.platform.SkikoUITextInputTraits import androidx.compose.ui.platform.TextActions +import androidx.compose.ui.platform.ViewConfiguration import kotlinx.cinterop.COpaquePointer import kotlinx.cinterop.CValue import kotlinx.cinterop.readValue import kotlinx.cinterop.useContents +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import platform.CoreGraphics.CGPoint import platform.CoreGraphics.CGRect import platform.CoreGraphics.CGRectIntersectsRect @@ -74,10 +80,19 @@ import platform.darwin.NSInteger @Suppress("CONFLICTING_OVERLOADS") internal class IntermediateTextInputUIView( private val keyboardEventHandler: KeyboardEventHandler, + private val viewConfiguration: ViewConfiguration ) : UIView(frame = CGRectZero.readValue()), UIKeyInputProtocol, UITextInputProtocol { + private var menuMonitoringJob = Job() private var _inputDelegate: UITextInputDelegateProtocol? = null var input: IOSSkikoInput? = null + set(value) { + field = value + if (value == null) { + cancelContextMenuUpdate() + } + } + private var _currentTextMenuActions: TextActions? = null var inputTraits: SkikoUITextInputTraits = EmptyInputTraits @@ -403,7 +418,7 @@ internal class IntermediateTextInputUIView( override fun keyboardType(): UIKeyboardType = inputTraits.keyboardType() override fun keyboardAppearance(): UIKeyboardAppearance = inputTraits.keyboardAppearance() override fun returnKeyType(): UIReturnKeyType = inputTraits.returnKeyType() - override fun textContentType(): UITextContentType? = inputTraits.textContentType() + override fun textContentType(): UITextContentType = inputTraits.textContentType() override fun isSecureTextEntry(): Boolean = inputTraits.isSecureTextEntry() override fun enablesReturnKeyAutomatically(): Boolean = inputTraits.enablesReturnKeyAutomatically() @@ -469,14 +484,24 @@ internal class IntermediateTextInputUIView( } } + private fun shouldReloadContextMenuItems(actions: TextActions): Boolean { + return (_currentTextMenuActions?.copy == null) != (actions.copy == null) || + (_currentTextMenuActions?.paste == null) != (actions.paste == null) || + (_currentTextMenuActions?.cut == null) != (actions.cut == null) || + (_currentTextMenuActions?.selectAll == null) != (actions.selectAll == null) + } + + private fun cancelContextMenuUpdate() { + menuMonitoringJob.cancel() + menuMonitoringJob = Job() + } + /** * Show copy/paste text menu * @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) { - _currentTextMenuActions = textActions - val menu: UIMenuController = UIMenuController.sharedMenuController() val cgRect = CGRectMake( x = targetRect.left.toDouble(), y = targetRect.top.toDouble(), @@ -484,24 +509,27 @@ internal class IntermediateTextInputUIView( height = targetRect.height.toDouble() ) val isTargetVisible = CGRectIntersectsRect(bounds, cgRect) + if (isTargetVisible) { - if (menu.isMenuVisible()) { - menu.setTargetRect(cgRect, this) - } else { - //TODO: UIMenuController.showMenuFromView is Deprecated since iOS 17 - // and not available on iOS 12 - menu.showMenuFromView(this, cgRect) - } - } else { - if (menu.isMenuVisible()) { - //TODO: UIMenuController.hideMenu is Deprecated since iOS 17 - // and not available on iOS 12 + // TODO: UIMenuController is deprecated since iOS 17 and not available on iOS 12 + val menu: UIMenuController = UIMenuController.sharedMenuController() + if (shouldReloadContextMenuItems(textActions)) { menu.hideMenu() } + cancelContextMenuUpdate() + CoroutineScope(Dispatchers.Main + menuMonitoringJob).launch { + delay(viewConfiguration.doubleTapTimeoutMillis) + menu.showMenuFromView(targetView = this@IntermediateTextInputUIView, cgRect) + } + _currentTextMenuActions = textActions + } else { + hideTextMenu() } } fun hideTextMenu() { + cancelContextMenuUpdate() + _currentTextMenuActions = null val menu: UIMenuController = UIMenuController.sharedMenuController() menu.hideMenu()