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 @@ -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 @@ -133,15 +134,16 @@ internal open class ComposeTextInputConnection(
override fun caretDpRectForPosition(position: Int): DpRect? {
val offset = textOffsetInView ?: return null
val text = currentTextFieldValue?.text ?: return null
val currentTextLayoutResult = textLayoutResult ?: return null
val currentTextLayoutResult = transformedTextLayoutResult ?: return null

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 Expand Up @@ -174,23 +176,27 @@ internal open class ComposeTextInputConnection(
if (range.collapsed || isIncorrect(range)) {
return emptySequence()
}
val transformedRange = offsetMapping.mapToTransformed(range)
if (transformedRange.collapsed) {
return emptySequence()
}
val offset = textOffsetInView ?: return emptySequence()
val currentTextLayoutResult = textLayoutResult ?: return emptySequence()
if (range.end > currentTextLayoutResult.multiParagraph.intrinsics.annotatedString.length) {
val transformedTextLayoutResult = transformedTextLayoutResult ?: return emptySequence()
if (transformedRange.end > transformedTextLayoutResult.multiParagraph.intrinsics.annotatedString.length) {
return emptySequence()
}

val firstLine = currentTextLayoutResult.getLineForOffset(range.start)
val lastLine = currentTextLayoutResult.getLineForOffset(range.end)
val startX = currentTextLayoutResult.getCursorRect(range.start).left
val endX = currentTextLayoutResult.getCursorRect(range.end).left
val firstLine = transformedTextLayoutResult.getLineForOffset(transformedRange.start)
val lastLine = transformedTextLayoutResult.getLineForOffset(transformedRange.end)
val startX = transformedTextLayoutResult.getCursorRect(transformedRange.start).left
val endX = transformedTextLayoutResult.getCursorRect(transformedRange.end).left
val density = rootView.density

return (firstLine..lastLine).asSequence().mapNotNull { line ->
val lineLeft = currentTextLayoutResult.getLineLeft(line)
val lineRight = currentTextLayoutResult.getLineRight(line)
val isRtl = currentTextLayoutResult.getParagraphDirection(
currentTextLayoutResult.getLineStart(line)
val lineLeft = transformedTextLayoutResult.getLineLeft(line)
val lineRight = transformedTextLayoutResult.getLineRight(line)
val isRtl = transformedTextLayoutResult.getParagraphDirection(
transformedTextLayoutResult.getLineStart(line)
) == ResolvedTextDirection.Rtl

val lineStartX = if (isRtl) lineRight else lineLeft
Expand All @@ -203,9 +209,9 @@ internal open class ComposeTextInputConnection(
}
Rect(
left = min(from, to),
top = currentTextLayoutResult.getLineTop(line),
top = transformedTextLayoutResult.getLineTop(line),
right = max(from, to),
bottom = currentTextLayoutResult.getLineBottom(line)
bottom = transformedTextLayoutResult.getLineBottom(line)
).translate(offset).toDpRect(density)
}
}
Expand Down
Loading
Loading