Skip to content

Commit

Permalink
Initial iOS floating cursor support (#1312)
Browse files Browse the repository at this point in the history
## Proposed Changes

Initial iOS floating cursor support.


https://github.com/JetBrains/compose-multiplatform-core/assets/63979218/1c6681dc-5295-4712-80e0-f08f58f4687e


## Testing

Test: hold space bar and move the cursor

## Issues Fixed

Fixes: JetBrains/compose-multiplatform#4593

Co-authored-by: Andrei Salavei <Andrei.Salavei@jetbrains.com>
  • Loading branch information
alexzhirkevich and ASalavei committed Jun 3, 2024
1 parent 1694668 commit 0ff7bcc
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,17 @@

package androidx.compose.ui.platform

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.DpOffset

internal interface IOSSkikoInput {

fun beginFloatingCursor(offset: DpOffset) {}

fun updateFloatingCursor(offset: DpOffset) {}

fun endFloatingCursor() {}

/**
* A Boolean value that indicates whether the text-entry object has any text.
* https://developer.apple.com/documentation/uikit/uikeyinput/1614457-hastext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,33 @@

package androidx.compose.ui.platform

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.input.key.Key
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.scene.getConstraintsToFillParent
import androidx.compose.ui.text.TextLayoutResult
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.OffsetMapping
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.DpOffset
import androidx.compose.ui.unit.asCGRect
import androidx.compose.ui.unit.toDpRect
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.window.FocusStack
import androidx.compose.ui.window.IntermediateTextInputUIView
import androidx.compose.ui.window.KeyboardEventHandler
Expand All @@ -63,6 +69,7 @@ internal class UIKitTextInputService(
private var currentImeOptions: ImeOptions? = null
private var currentImeActionHandler: ((ImeAction) -> Unit)? = null
private var textUIView: IntermediateTextInputUIView? = null
private var textLayoutResult : TextLayoutResult? = null

/**
* Workaround to prevent calling textWillChange, textDidChange, selectionWillChange, and
Expand Down Expand Up @@ -192,6 +199,25 @@ internal class UIKitTextInputService(
}
}

override fun updateTextLayoutResult(
textFieldValue: TextFieldValue,
offsetMapping: OffsetMapping,
textLayoutResult: TextLayoutResult,
textFieldToRootTransform: (Matrix) -> Unit,
innerTextFieldBounds: Rect,
decorationBoxBounds: Rect
) {
super.updateTextLayoutResult(
textFieldValue,
offsetMapping,
textLayoutResult,
textFieldToRootTransform,
innerTextFieldBounds,
decorationBoxBounds
)
this.textLayoutResult = textLayoutResult
}

private fun handleEnterKey(event: KeyEvent): Boolean {
_tempImeActionIsCalledWithHardwareReturnKey = false
return when (event.type) {
Expand Down Expand Up @@ -332,6 +358,28 @@ internal class UIKitTextInputService(
}

private fun createSkikoInput(value: TextFieldValue) = object : IOSSkikoInput {

private var floatingCursorTranslation : Offset? = null

override fun beginFloatingCursor(offset: DpOffset) {
val cursorPos = getCursorPos() ?: getState()?.selection?.start ?: return
val cursorRect = textLayoutResult?.getCursorRect(cursorPos) ?: return
floatingCursorTranslation = cursorRect.center - offset.toOffset(densityProvider())
}

override fun updateFloatingCursor(offset: DpOffset) {
val translation = floatingCursorTranslation ?: return
val offsetPx = offset.toOffset(densityProvider())
val pos = textLayoutResult
?.getOffsetForPosition(offsetPx + translation) ?: return

sendEditCommand(SetSelectionCommand(pos, pos))
}

override fun endFloatingCursor() {
floatingCursorTranslation = null
}

/**
* A Boolean value that indicates whether the text-entry object has any text.
* https://developer.apple.com/documentation/uikit/uikeyinput/1614457-hastext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@

package androidx.compose.ui.window

import androidx.compose.ui.geometry.Offset
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 androidx.compose.ui.uikit.utils.CMPEditMenuView
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import kotlinx.cinterop.COpaquePointer
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
import kotlinx.cinterop.CValue
Expand Down Expand Up @@ -89,6 +93,18 @@ internal class IntermediateTextInputUIView(

override fun canBecomeFirstResponder() = true

override fun beginFloatingCursorAtPoint(point: CValue<CGPoint>) {
input?.beginFloatingCursor(point.useContents { DpOffset(x.dp, y.dp) })
}

override fun updateFloatingCursorAtPoint(point: CValue<CGPoint>) {
input?.updateFloatingCursor(point.useContents { DpOffset(x.dp, y.dp) })
}

override fun endFloatingCursor() {
input?.endFloatingCursor()
}

override fun pressesBegan(presses: Set<*>, withEvent: UIPressesEvent?) {
keyboardEventHandler?.pressesBegan(presses, withEvent)
super.pressesBegan(presses, withEvent)
Expand Down

0 comments on commit 0ff7bcc

Please sign in to comment.