Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

@file:Suppress("DEPRECATION")

package androidx.compose.foundation.text.input.internal

import androidx.compose.runtime.getValue
Expand All @@ -23,6 +25,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.platform.TextEditorOffsetMapping
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.CommitTextCommand
Expand All @@ -48,6 +51,7 @@ internal actual fun createLegacyPlatformTextInputServiceAdapter():

private var textFieldValue by mutableStateOf(TextFieldValue())
private var textLayoutResult by mutableStateOf<TextLayoutResult?>(null)
private var offsetMapping by mutableStateOf(OffsetMapping.Identity)
private var focusedRectInRoot by mutableStateOf(Rect.Zero)
private var textFieldRectInRoot by mutableStateOf(Rect.Zero)
private var textClippingRectInRoot by mutableStateOf(Rect.Zero)
Expand Down Expand Up @@ -94,6 +98,7 @@ internal actual fun createLegacyPlatformTextInputServiceAdapter():
) {
this.textFieldValue = textFieldValue
this.textLayoutResult = textLayoutResult
this.offsetMapping = offsetMapping

val matrix = Matrix().also { textFieldToRootTransform(it) }
textFieldRectInRoot = matrix.map(decorationBoxBounds)
Expand All @@ -105,6 +110,26 @@ internal actual fun createLegacyPlatformTextInputServiceAdapter():

override fun startStylusHandwriting() {}

private val transformedOffsetMapping = object : TextEditorOffsetMapping {
override fun mapToTransformed(range: TextRange): TextRange = if (range.collapsed) {
offsetMapping.originalToTransformed(range.start).let(::TextRange)
Comment thread
ASalavei marked this conversation as resolved.
Resolved
} else {
TextRange(
start = offsetMapping.originalToTransformed(range.start),
end = offsetMapping.originalToTransformed(range.end)
)
}

override fun mapFromTransformed(range: TextRange): TextRange = if (range.collapsed) {
offsetMapping.transformedToOriginal(range.start).let(::TextRange)
Comment thread
ASalavei marked this conversation as resolved.
Resolved
} else {
TextRange(
start = offsetMapping.transformedToOriginal(range.start),
end = offsetMapping.transformedToOriginal(range.end)
)
}
}

private fun makeRequest(
imeOptions: ImeOptions,
onEditCommand: (List<EditCommand>) -> Unit,
Expand Down Expand Up @@ -136,6 +161,7 @@ internal actual fun createLegacyPlatformTextInputServiceAdapter():
onEditCommand = onEditCommand,
onImeAction = onImeActionPerformed,
textLayoutResult = { textLayoutResult },
offsetMapping = transformedOffsetMapping,
focusedRectInRoot = { focusedRectInRoot },
textFieldRectInRoot = { textFieldRectInRoot },
textClippingRectInRoot = { textClippingRectInRoot },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.PlatformTextInputMethodRequest
import androidx.compose.ui.platform.PlatformTextInputSession
import androidx.compose.ui.platform.TextEditorOffsetMapping
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
Expand Down Expand Up @@ -111,6 +112,7 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe
onEditCommand = ::onEditCommand,
onImeAction = onImeAction,
textLayoutResult = layoutState::layoutResult,
offsetMapping = VisualTextOffsetMapping(state),
focusedRectInRoot = ::focusedRectInRoot,
textFieldRectInRoot = ::textFieldRectInRoot,
textClippingRectInRoot = ::textClippingRectInRoot,
Expand All @@ -125,6 +127,23 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe
private fun TextFieldCharSequence.toTextFieldValue() =
TextFieldValue(toString(), selection, composition)

@OptIn(ExperimentalComposeUiApi::class)
private class VisualTextOffsetMapping(
private val state: TransformedTextFieldState,
) : TextEditorOffsetMapping {
override fun mapToTransformed(range: TextRange): TextRange =
TextRange(
start = state.mapToTransformed(range.start).start,
end = state.mapToTransformed(range.end).start
)

override fun mapFromTransformed(range: TextRange): TextRange =
TextRange(
start = state.mapFromTransformed(range.start).start,
end = state.mapFromTransformed(range.end).start
)
}

@Suppress("NOTHING_TO_INLINE")
@OptIn(ExperimentalComposeUiApi::class)
private inline fun (() -> TextFieldCharSequence).asTextEditorState() = object : TextEditorState {
Expand Down Expand Up @@ -245,6 +264,7 @@ internal data class SkikoPlatformTextInputMethodRequest(
override val onEditCommand: (List<EditCommand>) -> Unit,
override val onImeAction: ((ImeAction) -> Unit)?,
override val textLayoutResult: () -> TextLayoutResult?,
override val offsetMapping: TextEditorOffsetMapping,
override val focusedRectInRoot: () -> Rect?,
override val textFieldRectInRoot: () -> Rect?,
override val textClippingRectInRoot: () -> Rect?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package androidx.compose.ui.platform

import androidx.compose.ui.ExperimentalComposeUiApi
Comment thread
ASalavei marked this conversation as resolved.
Resolved
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.uikit.utils.CMPTextInputStringTokenizer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

@file:Suppress("DEPRECATION")

package androidx.compose.ui.scene

import androidx.compose.runtime.getValue
Expand All @@ -23,6 +25,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.platform.PlatformTextInputMethodRequest
import androidx.compose.ui.platform.TextEditorOffsetMapping
import androidx.compose.ui.platform.TextInputService
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
Expand All @@ -47,7 +50,6 @@ import kotlinx.coroutines.launch
* Mimics the functionality of the LegacyPlatformTextInputServiceAdapter. Used to provide backward
* compatibility for the [androidx.compose.ui.text.input.PlatformTextInputService].
*/
@Suppress("DEPRECATION")
internal class TextInputServiceAdapter(
private val textInputService: TextInputService,
private val coroutineScope: CoroutineScope,
Expand All @@ -61,6 +63,7 @@ internal class TextInputServiceAdapter(

private var textFieldValue by mutableStateOf(TextFieldValue())
private var textLayoutResult by mutableStateOf<TextLayoutResult?>(null)
private var offsetMapping by mutableStateOf(OffsetMapping.Identity)
private var focusedRectInRoot by mutableStateOf(Rect.Zero)
private var textFieldRectInRoot by mutableStateOf(Rect.Zero)
private var textClippingRectInRoot by mutableStateOf(Rect.Zero)
Expand Down Expand Up @@ -104,6 +107,7 @@ internal class TextInputServiceAdapter(
) {
this.textFieldValue = textFieldValue
this.textLayoutResult = textLayoutResult
this.offsetMapping = offsetMapping

val matrix = Matrix().also { textFieldToRootTransform(it) }
textFieldRectInRoot = matrix.map(decorationBoxBounds)
Expand All @@ -113,6 +117,26 @@ internal class TextInputServiceAdapter(
unclippedTextOffsetInRoot = textClippingRectInRoot.topLeft - innerTextFieldBounds.topLeft
}

private val transformedOffsetMapping = object : TextEditorOffsetMapping {
override fun mapToTransformed(range: TextRange): TextRange = if (range.collapsed) {
TextRange(offsetMapping.originalToTransformed(range.start))
} else {
TextRange(
start = offsetMapping.originalToTransformed(range.start),
end = offsetMapping.originalToTransformed(range.end)
)
}

override fun mapFromTransformed(range: TextRange): TextRange = if (range.collapsed) {
TextRange(offsetMapping.transformedToOriginal(range.start))
} else {
TextRange(
start = offsetMapping.transformedToOriginal(range.start),
end = offsetMapping.transformedToOriginal(range.end)
)
}
}

private fun makeRequest(
imeOptions: ImeOptions,
onEditCommand: (List<EditCommand>) -> Unit,
Expand All @@ -136,6 +160,8 @@ internal class TextInputServiceAdapter(
override val onImeAction: (ImeAction) -> Unit get() = onImeActionPerformed
override val textLayoutResult: () -> TextLayoutResult?
get() = { this@TextInputServiceAdapter.textLayoutResult }
override val offsetMapping: TextEditorOffsetMapping
get() = transformedOffsetMapping
override val focusedRectInRoot: () -> Rect?
get() = { this@TextInputServiceAdapter.focusedRectInRoot }
override val textFieldRectInRoot: () -> Rect?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.compose.ui.platform.TextToolbarStatus
import androidx.compose.ui.platform.NativeTextInputContextMenuCustomAction
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.detachedCopy
import androidx.compose.ui.platform.mapToTransformed
import androidx.compose.ui.scene.ComposeSceneFocusManager
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.style.ResolvedTextDirection
Expand Down Expand Up @@ -138,10 +139,11 @@ internal open class ComposeTextInputConnection(
if (position < 0 || position > text.length) {
return null
}
if (position > currentTextLayoutResult.multiParagraph.intrinsics.annotatedString.length) {
val transformedPosition = offsetMapping.mapToTransformed(position)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need transformed range here as well?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - position in the original text coordinates.
currentTextLayoutResult uses transformed text coordinates, so we have to convert it.

if (transformedPosition > currentTextLayoutResult.multiParagraph.intrinsics.annotatedString.length) {
return null
}
val rect = currentTextLayoutResult.getCursorRect(position).translate(offset)
val rect = currentTextLayoutResult.getCursorRect(transformedPosition).translate(offset)
return rect.toDpRect(rootView.density).let {
val halfWidth = CURSOR_THICKNESS / 2
val center = (it.left + it.right) / 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import androidx.compose.ui.platform.PlatformTextInputMethodRequest
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.platform.toUIColor
import androidx.compose.ui.platform.detachedCopy
import androidx.compose.ui.platform.mapFromTransformed
import androidx.compose.ui.platform.mapToTransformed
import androidx.compose.ui.scene.ComposeSceneFocusManager
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
Expand Down Expand Up @@ -175,10 +177,11 @@ internal class NativeTextInputConnection(
return null
}
val currentTextLayoutResult = textLayoutResult ?: return null
if (position > currentTextLayoutResult.multiParagraph.intrinsics.annotatedString.length) {
val transformedPosition = offsetMapping.mapToTransformed(position)
if (transformedPosition > currentTextLayoutResult.multiParagraph.intrinsics.annotatedString.length) {
return null
}
val rect = currentTextLayoutResult.getCursorRect(position)
val rect = currentTextLayoutResult.getCursorRect(transformedPosition)
return rect.toDpRect(rootView.density).let {
val halfWidth = CURSOR_THICKNESS / 2
val center = (it.left + it.right) / 2
Expand All @@ -192,17 +195,21 @@ internal class NativeTextInputConnection(
if (range.collapsed || isIncorrect(range)) {
return emptyList()
}
val transformedRange = offsetMapping.mapToTransformed(range)
if (transformedRange.collapsed) {
return emptyList()
}
val currentTextLayoutResult = textLayoutResult ?: return emptyList()

// Layout in native text input mode may be outdated, so not checking this may cause OOB error
// This workaround should be deleted after https://youtrack.jetbrains.com/issue/CMP-9767/
if (range.end > currentTextLayoutResult.multiParagraph.intrinsics.annotatedString.length) return emptyList()
if (transformedRange.end > currentTextLayoutResult.multiParagraph.intrinsics.annotatedString.length) return emptyList()

val startSelectionHandleRect = currentTextLayoutResult.getCursorRect(range.start)
val endSelectionHandleRect = currentTextLayoutResult.getCursorRect(range.end)
val startSelectionHandleRect = currentTextLayoutResult.getCursorRect(transformedRange.start)
val endSelectionHandleRect = currentTextLayoutResult.getCursorRect(transformedRange.end)

val firstLineNumber = currentTextLayoutResult.getLineForOffset(range.start)
val lastLineNumber = currentTextLayoutResult.getLineForOffset(range.end)
val firstLineNumber = currentTextLayoutResult.getLineForOffset(transformedRange.start)
val lastLineNumber = currentTextLayoutResult.getLineForOffset(transformedRange.end)

return if (firstLineNumber == lastLineNumber) {
listOf(
Expand Down Expand Up @@ -284,24 +291,28 @@ internal class NativeTextInputConnection(
if (range.collapsed || isIncorrect(range)) {
return null
}
val transformedRange = offsetMapping.mapToTransformed(range)
if (transformedRange.collapsed) {
return null
}
val currentTextLayoutResult = textLayoutResult ?: return null

// Layout in native text input mode may be outdated, so not checking this may cause OOB error
// This workaround should be deleted after https://youtrack.jetbrains.com/issue/CMP-9767/
if (range.end > currentTextLayoutResult.multiParagraph.intrinsics.annotatedString.length) return null
if (transformedRange.end > currentTextLayoutResult.multiParagraph.intrinsics.annotatedString.length) return null

val startHandleLineNumber = currentTextLayoutResult.getLineForOffset(range.start)
val endHandleLineNumber = currentTextLayoutResult.getLineForOffset(range.end)
val startHandleLineNumber = currentTextLayoutResult.getLineForOffset(transformedRange.start)
val endHandleLineNumber = currentTextLayoutResult.getLineForOffset(transformedRange.end)

val startHandleRect = currentTextLayoutResult.getCursorRect(range.start)
val startHandleRect = currentTextLayoutResult.getCursorRect(transformedRange.start)

return if (startHandleLineNumber == endHandleLineNumber) {
Rect(
topLeft = startHandleRect.topLeft,
bottomRight = currentTextLayoutResult.getCursorRect(range.end).bottomRight
bottomRight = currentTextLayoutResult.getCursorRect(transformedRange.end).bottomRight
).toDpRect(rootView.density)
} else {
val startLineNumber = currentTextLayoutResult.getLineForOffset(range.start)
val startLineNumber = currentTextLayoutResult.getLineForOffset(transformedRange.start)
val startLineRight = currentTextLayoutResult.getLineRight(startLineNumber)
Rect(
startHandleRect.left,
Expand All @@ -314,20 +325,23 @@ internal class NativeTextInputConnection(

override fun closestPositionToPoint(point: DpOffset): Int? {
return textLayoutResult?.getOffsetForPosition(point.toOffset(rootView.density))
?.let { offsetMapping.mapFromTransformed(it) }
Comment thread
ASalavei marked this conversation as resolved.
Resolved
}

override fun closestPositionToPoint(point: DpOffset, withinRange: TextRange): Int? {
val pointOffset =
textLayoutResult?.getOffsetForPosition(point.toOffset(rootView.density))
?: return null
return pointOffset.coerceIn(withinRange.start, withinRange.end)
return offsetMapping.mapFromTransformed(pointOffset)
.coerceIn(withinRange.start, withinRange.end)
}

override fun characterRangeAtPoint(point: DpOffset): TextRange? {
val pointOffset =
textLayoutResult?.getOffsetForPosition(point.toOffset(rootView.density))
?: return null
return textLayoutResult?.getWordBoundary(pointOffset)
val wordBoundary = textLayoutResult?.getWordBoundary(pointOffset) ?: return null
return offsetMapping.mapFromTransformed(wordBoundary)
}

override fun positionWithinRange(
Expand All @@ -340,16 +354,18 @@ internal class NativeTextInputConnection(
TextLayoutDirection.Down -> range.end
else -> {
val layout = textLayoutResult ?: return null
val startLine = layout.getLineForOffset(range.start)
val endLine = layout.getLineForOffset(range.end)
val mapping = offsetMapping
val transformedRange = mapping.mapToTransformed(range)
val startLine = layout.getLineForOffset(transformedRange.start)
val endLine = layout.getLineForOffset(transformedRange.end)

val candidateOffsets = buildSet {
add(range.start)
add(range.end)
add(transformedRange.start)
add(transformedRange.end)

for (line in startLine..endLine) {
add(max(range.start, layout.getLineStart(line)))
add(min(range.end, layout.getLineEnd(line)))
add(max(transformedRange.start, layout.getLineStart(line)))
add(min(transformedRange.end, layout.getLineEnd(line)))
}
}

Expand All @@ -359,7 +375,7 @@ internal class NativeTextInputConnection(
TextLayoutDirection.Right ->
candidateOffsets.maxByOrNull { layout.getHorizontalPosition(it, true) }
else -> null
}
}?.let { mapping.mapFromTransformed(it) }
}
}
}
Expand Down
Loading
Loading