Use offset mapping when building CursorAnchorInfo
CursorAnchorInfoBuilder was not accounting for offset mapping, which
could cause an offset out of bounds exception.
Relnote: N/A
Test: CursorAnchorInfoBuilderTest
Test: TextFieldVisualTransformationMagnifierTest
Bug: 143556460
Bug: 287523357
Change-Id: Id538760c08cd6e8efaad23c699734fa6b1ada776
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
index 3d3b62e..6e5cdb7 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
@@ -45,6 +45,7 @@
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.TextInputService
@@ -464,6 +465,7 @@
rule.runOnIdle {
assertThat(platformTextInputService.lastInputValue).isNull()
+ assertThat(platformTextInputService.offsetMapping).isNull()
assertThat(platformTextInputService.textLayoutResult).isNull()
assertThat(platformTextInputService.textLayoutPositionInWindow).isNull()
assertThat(platformTextInputService.innerTextFieldBounds).isNull()
@@ -476,6 +478,7 @@
rule.runOnIdle {
assertThat(platformTextInputService.lastInputValue).isEqualTo(value)
+ assertThat(platformTextInputService.offsetMapping).isNotNull()
assertThat(platformTextInputService.textLayoutResult).isNotNull()
assertThat(platformTextInputService.textLayoutResult).isEqualTo(textLayoutResult)
assertThat(platformTextInputService.textLayoutPositionInWindow)
@@ -512,6 +515,7 @@
var lastInputValue: TextFieldValue? = null
var lastInputImeOptions: ImeOptions? = null
+ var offsetMapping: OffsetMapping? = null
var textLayoutResult: TextLayoutResult? = null
var textLayoutPositionInWindow: Offset? = null
var innerTextFieldBounds: Rect? = null
@@ -554,12 +558,14 @@
override fun updateTextLayoutResult(
textFieldValue: TextFieldValue,
+ offsetMapping: OffsetMapping,
textLayoutResult: TextLayoutResult,
textLayoutPositionInWindow: Offset,
innerTextFieldBounds: Rect,
decorationBoxBounds: Rect
) {
lastInputValue = textFieldValue
+ this.offsetMapping = offsetMapping
this.textLayoutResult = textLayoutResult
this.textLayoutPositionInWindow = textLayoutPositionInWindow
this.innerTextFieldBounds = innerTextFieldBounds
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 06f02e7..2967ba6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -405,7 +405,12 @@
state.layoutResult?.let { layoutResult ->
state.inputSession?.let { inputSession ->
if (state.hasFocus) {
- TextFieldDelegate.updateTextLayoutResult(inputSession, value, layoutResult)
+ TextFieldDelegate.updateTextLayoutResult(
+ inputSession,
+ value,
+ offsetMapping,
+ layoutResult
+ )
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
index 8f8a6d6..2df1762 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
@@ -184,12 +184,14 @@
*
* @param textInputSession the current input session
* @param textFieldValue the editor state
+ * @param offsetMapping the offset mapping for the visual transformation
* @param textLayoutResult the layout result
*/
@JvmStatic
internal fun updateTextLayoutResult(
textInputSession: TextInputSession,
textFieldValue: TextFieldValue,
+ offsetMapping: OffsetMapping,
textLayoutResult: TextLayoutResultProxy
) {
textLayoutResult.innerTextFieldCoordinates?.let { innerTextFieldCoordinates ->
@@ -197,6 +199,7 @@
textLayoutResult.decorationBoxCoordinates?.let { decorationBoxCoordinates ->
textInputSession.updateTextLayoutResult(
textFieldValue,
+ offsetMapping,
textLayoutResult.value,
innerTextFieldCoordinates.positionInWindow(),
innerTextFieldCoordinates.visibleBounds(),
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index ba3232f..72ecd80b 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -1125,7 +1125,7 @@
method public void startInput(androidx.compose.ui.text.input.TextFieldValue value, androidx.compose.ui.text.input.ImeOptions imeOptions, kotlin.jvm.functions.Function1<? super java.util.List<? extends androidx.compose.ui.text.input.EditCommand>,kotlin.Unit> onEditCommand, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed);
method public void stopInput();
method public void updateState(androidx.compose.ui.text.input.TextFieldValue? oldValue, androidx.compose.ui.text.input.TextFieldValue newValue);
- method public default void updateTextLayoutResult(androidx.compose.ui.text.input.TextFieldValue textFieldValue, androidx.compose.ui.text.TextLayoutResult textLayoutResult, long textLayoutPositionInWindow, androidx.compose.ui.geometry.Rect innerTextFieldBounds, androidx.compose.ui.geometry.Rect decorationBoxBounds);
+ method public default void updateTextLayoutResult(androidx.compose.ui.text.input.TextFieldValue textFieldValue, androidx.compose.ui.text.input.OffsetMapping offsetMapping, androidx.compose.ui.text.TextLayoutResult textLayoutResult, long textLayoutPositionInWindow, androidx.compose.ui.geometry.Rect innerTextFieldBounds, androidx.compose.ui.geometry.Rect decorationBoxBounds);
}
public final class SetComposingRegionCommand implements androidx.compose.ui.text.input.EditCommand {
@@ -1201,7 +1201,7 @@
method public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
method public boolean showSoftwareKeyboard();
method public boolean updateState(androidx.compose.ui.text.input.TextFieldValue? oldValue, androidx.compose.ui.text.input.TextFieldValue newValue);
- method public boolean updateTextLayoutResult(androidx.compose.ui.text.input.TextFieldValue textFieldValue, androidx.compose.ui.text.TextLayoutResult textLayoutResult, long textLayoutPositionInWindow, androidx.compose.ui.geometry.Rect innerTextFieldBounds, androidx.compose.ui.geometry.Rect decorationBoxBounds);
+ method public boolean updateTextLayoutResult(androidx.compose.ui.text.input.TextFieldValue textFieldValue, androidx.compose.ui.text.input.OffsetMapping offsetMapping, androidx.compose.ui.text.TextLayoutResult textLayoutResult, long textLayoutPositionInWindow, androidx.compose.ui.geometry.Rect innerTextFieldBounds, androidx.compose.ui.geometry.Rect decorationBoxBounds);
property public final boolean isOpen;
}
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index ba3232f..72ecd80b 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -1125,7 +1125,7 @@
method public void startInput(androidx.compose.ui.text.input.TextFieldValue value, androidx.compose.ui.text.input.ImeOptions imeOptions, kotlin.jvm.functions.Function1<? super java.util.List<? extends androidx.compose.ui.text.input.EditCommand>,kotlin.Unit> onEditCommand, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed);
method public void stopInput();
method public void updateState(androidx.compose.ui.text.input.TextFieldValue? oldValue, androidx.compose.ui.text.input.TextFieldValue newValue);
- method public default void updateTextLayoutResult(androidx.compose.ui.text.input.TextFieldValue textFieldValue, androidx.compose.ui.text.TextLayoutResult textLayoutResult, long textLayoutPositionInWindow, androidx.compose.ui.geometry.Rect innerTextFieldBounds, androidx.compose.ui.geometry.Rect decorationBoxBounds);
+ method public default void updateTextLayoutResult(androidx.compose.ui.text.input.TextFieldValue textFieldValue, androidx.compose.ui.text.input.OffsetMapping offsetMapping, androidx.compose.ui.text.TextLayoutResult textLayoutResult, long textLayoutPositionInWindow, androidx.compose.ui.geometry.Rect innerTextFieldBounds, androidx.compose.ui.geometry.Rect decorationBoxBounds);
}
public final class SetComposingRegionCommand implements androidx.compose.ui.text.input.EditCommand {
@@ -1201,7 +1201,7 @@
method public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
method public boolean showSoftwareKeyboard();
method public boolean updateState(androidx.compose.ui.text.input.TextFieldValue? oldValue, androidx.compose.ui.text.input.TextFieldValue newValue);
- method public boolean updateTextLayoutResult(androidx.compose.ui.text.input.TextFieldValue textFieldValue, androidx.compose.ui.text.TextLayoutResult textLayoutResult, long textLayoutPositionInWindow, androidx.compose.ui.geometry.Rect innerTextFieldBounds, androidx.compose.ui.geometry.Rect decorationBoxBounds);
+ method public boolean updateTextLayoutResult(androidx.compose.ui.text.input.TextFieldValue textFieldValue, androidx.compose.ui.text.input.OffsetMapping offsetMapping, androidx.compose.ui.text.TextLayoutResult textLayoutResult, long textLayoutPositionInWindow, androidx.compose.ui.geometry.Rect innerTextFieldBounds, androidx.compose.ui.geometry.Rect decorationBoxBounds);
property public final boolean isOpen;
}
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
index f7d277d..9d6af43 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
@@ -180,6 +180,7 @@
* Notify the input service of layout and position changes.
*
* @param textFieldValue the text field's [TextFieldValue]
+ * @param offsetMapping the offset mapping for the visual transformation
* @param textLayoutResult the text field's [TextLayoutResult]
* @param textLayoutPositionInWindow position of the text field relative to the window
* @param innerTextFieldBounds visible bounds of the text field in local coordinates, or an
@@ -189,6 +190,7 @@
*/
fun updateTextLayoutResult(
textFieldValue: TextFieldValue,
+ offsetMapping: OffsetMapping,
textLayoutResult: TextLayoutResult,
textLayoutPositionInWindow: Offset,
innerTextFieldBounds: Rect,
@@ -196,6 +198,7 @@
) = ensureOpenSession {
platformTextInputService.updateTextLayoutResult(
textFieldValue,
+ offsetMapping,
textLayoutResult,
textLayoutPositionInWindow,
innerTextFieldBounds,
@@ -323,6 +326,7 @@
*/
fun updateTextLayoutResult(
textFieldValue: TextFieldValue,
+ offsetMapping: OffsetMapping,
textLayoutResult: TextLayoutResult,
textLayoutPositionInWindow: Offset,
innerTextFieldBounds: Rect,
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/CursorAnchorInfoBuilderTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/CursorAnchorInfoBuilderTest.kt
index b7dc4cc..af70cf4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/CursorAnchorInfoBuilderTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/CursorAnchorInfoBuilderTest.kt
@@ -35,6 +35,7 @@
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.font.toFontFamily
+import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.build
import androidx.compose.ui.text.style.TextOverflow
@@ -252,6 +253,28 @@
}
@Test
+ fun testInsertionMarkerWithVisualTransformation() {
+ val fontSize = 10.sp
+ val textFieldValue = TextFieldValue("abcde", selection = TextRange(2))
+ val offsetMapping = object : OffsetMapping {
+ override fun originalToTransformed(offset: Int) = if (offset < 2) offset else offset + 3
+ override fun transformedToOriginal(offset: Int) = throw NotImplementedError()
+ }
+ val textLayoutResult = getTextLayoutResult("ab---cde", fontSize = fontSize)
+
+ val cursorAnchorInfo =
+ CursorAnchorInfo.Builder()
+ .build(textFieldValue, textLayoutResult, matrix, offsetMapping = offsetMapping)
+
+ val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
+ assertThat(cursorAnchorInfo.insertionMarkerHorizontal).isEqualTo(5 * fontSizeInPx)
+ assertThat(cursorAnchorInfo.insertionMarkerTop).isEqualTo(0f)
+ assertThat(cursorAnchorInfo.insertionMarkerBottom).isEqualTo(fontSizeInPx)
+ assertThat(cursorAnchorInfo.insertionMarkerBaseline).isEqualTo(fontSizeInPx)
+ assertThat(cursorAnchorInfo.insertionMarkerFlags).isEqualTo(FLAG_HAS_VISIBLE_REGION)
+ }
+
+ @Test
fun testInsertionMarkerNotVisible() {
val fontSize = 10.sp
val textFieldValue = TextFieldValue("abc", selection = TextRange(1))
@@ -266,6 +289,7 @@
CursorAnchorInfo.Builder()
.build(
textFieldValue,
+ OffsetMapping.Identity,
textLayoutResult,
matrix,
innerTextFieldBounds = innerTextFieldBounds,
@@ -293,6 +317,7 @@
CursorAnchorInfo.Builder()
.build(
textFieldValue,
+ OffsetMapping.Identity,
textLayoutResult,
matrix,
innerTextFieldBounds = innerTextFieldBounds,
@@ -390,6 +415,47 @@
}
@Test
+ fun testCharacterBoundsWithVisualTransformation() {
+ val fontSize = 10.sp
+ val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
+ val text = "abcd"
+ // Composition is on "bc"
+ val composition = TextRange(2, 4)
+ val offsetMapping = object : OffsetMapping {
+ override fun originalToTransformed(offset: Int) = 2 * offset
+ override fun transformedToOriginal(offset: Int) = throw NotImplementedError()
+ }
+ val transformedText = "a-b-c-d-"
+ val textFieldValue = TextFieldValue(text, composition = composition)
+ val width = transformedText.length * fontSizeInPx
+ val textLayoutResult =
+ getTextLayoutResult(transformedText, fontSize = fontSize, width = width)
+
+ val cursorAnchorInfo =
+ CursorAnchorInfo.Builder()
+ .build(textFieldValue, textLayoutResult, matrix, offsetMapping = offsetMapping)
+
+ for (index in text.indices) {
+ if (index in composition) {
+ assertThat(cursorAnchorInfo.getCharacterBounds(index))
+ .isEqualTo(
+ RectF(
+ 2 * index * fontSizeInPx,
+ 0f,
+ (2 * index + 1) * fontSizeInPx,
+ fontSizeInPx
+ )
+ )
+ assertThat(cursorAnchorInfo.getCharacterBoundsFlags(index))
+ .isEqualTo(FLAG_HAS_VISIBLE_REGION)
+ } else {
+ assertThat(cursorAnchorInfo.getCharacterBounds(index)).isNull()
+ assertThat(cursorAnchorInfo.getCharacterBoundsFlags(index)).isEqualTo(0)
+ }
+ }
+ }
+
+ @Test
fun testCharacterBoundsVisibility() {
val fontSize = 10.sp
val fontSizeInPx = with(defaultDensity) { fontSize.toPx() }
@@ -406,6 +472,7 @@
CursorAnchorInfo.Builder()
.build(
textFieldValue,
+ OffsetMapping.Identity,
textLayoutResult,
matrix,
innerTextFieldBounds = innerTextFieldBounds,
@@ -452,6 +519,7 @@
CursorAnchorInfo.Builder()
.build(
textFieldValue,
+ OffsetMapping.Identity,
getTextLayoutResult(textFieldValue.text),
matrix,
innerTextFieldBounds = Rect(1f, 2f, 3f, 4f),
@@ -472,6 +540,7 @@
CursorAnchorInfo.Builder()
.build(
textFieldValue,
+ OffsetMapping.Identity,
getTextLayoutResult(textFieldValue.text),
matrix,
innerTextFieldBounds = Rect(1f, 2f, 3f, 4f),
@@ -503,6 +572,7 @@
CursorAnchorInfo.Builder()
.build(
textFieldValue,
+ OffsetMapping.Identity,
textLayoutResult,
matrix,
innerTextFieldBounds = innerTextFieldBounds,
@@ -563,6 +633,7 @@
CursorAnchorInfo.Builder()
.build(
textFieldValue,
+ OffsetMapping.Identity,
textLayoutResult,
matrix,
innerTextFieldBounds = innerTextFieldBounds,
@@ -613,6 +684,7 @@
textFieldValue: TextFieldValue,
textLayoutResult: TextLayoutResult,
matrix: Matrix,
+ offsetMapping: OffsetMapping = OffsetMapping.Identity,
includeInsertionMarker: Boolean = true,
includeCharacterBounds: Boolean = true,
includeEditorBounds: Boolean = true,
@@ -622,6 +694,7 @@
Rect(0f, 0f, textLayoutResult.size.width.toFloat(), textLayoutResult.size.height.toFloat())
return build(
textFieldValue,
+ offsetMapping,
textLayoutResult,
matrix,
innerTextFieldBounds = innerTextFieldBounds,
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidCursorAnchorInfoTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidCursorAnchorInfoTest.kt
index d4a44fb..f205503 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidCursorAnchorInfoTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidCursorAnchorInfoTest.kt
@@ -37,6 +37,7 @@
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.InputMethodManager
+import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.RecordingInputConnection
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.TextInputServiceAndroid
@@ -108,12 +109,14 @@
TextFieldValue("abc", selection = TextRange(2), composition = TextRange(1, 2))
textInputService.updateState(oldValue = textFieldValue, newValue = textFieldValue)
+ val offsetMapping = OffsetMapping.Identity
val textLayoutResult = getTextLayoutResult(textFieldValue.text)
var textLayoutPositionInWindow = Offset(1f, 1f)
val innerTextFieldBounds = Rect.Zero
val decorationBoxBounds = Rect.Zero
textInputService.updateTextLayoutResult(
textFieldValue = textFieldValue,
+ offsetMapping = offsetMapping,
textLayoutResult = textLayoutResult,
textLayoutPositionInWindow = textLayoutPositionInWindow,
innerTextFieldBounds = innerTextFieldBounds,
@@ -128,6 +131,7 @@
val expected =
builder.build(
textFieldValue,
+ offsetMapping,
textLayoutResult,
matrix,
innerTextFieldBounds,
@@ -139,6 +143,7 @@
textLayoutPositionInWindow = Offset(2f, 2f)
textInputService.updateTextLayoutResult(
textFieldValue = textFieldValue,
+ offsetMapping = offsetMapping,
textLayoutResult = textLayoutResult,
textLayoutPositionInWindow = textLayoutPositionInWindow,
innerTextFieldBounds = innerTextFieldBounds,
@@ -160,12 +165,14 @@
// No immediate update until updateTextLayoutResult call
verify(inputMethodManager, never()).updateCursorAnchorInfo(any())
+ val offsetMapping = OffsetMapping.Identity
val textLayoutResult = getTextLayoutResult(textFieldValue.text)
var textLayoutPositionInWindow = Offset(1f, 1f)
val innerTextFieldBounds = Rect.Zero
val decorationBoxBounds = Rect.Zero
textInputService.updateTextLayoutResult(
textFieldValue = textFieldValue,
+ offsetMapping = offsetMapping,
textLayoutResult = textLayoutResult,
textLayoutPositionInWindow = textLayoutPositionInWindow,
innerTextFieldBounds = innerTextFieldBounds,
@@ -178,6 +185,7 @@
val expected =
builder.build(
textFieldValue,
+ offsetMapping,
textLayoutResult,
matrix,
innerTextFieldBounds,
@@ -189,6 +197,7 @@
textLayoutPositionInWindow = Offset(2f, 2f)
textInputService.updateTextLayoutResult(
textFieldValue = textFieldValue,
+ offsetMapping = offsetMapping,
textLayoutResult = textLayoutResult,
textLayoutPositionInWindow = textLayoutPositionInWindow,
innerTextFieldBounds = innerTextFieldBounds,
@@ -205,12 +214,14 @@
TextFieldValue("abc", selection = TextRange(2), composition = TextRange(1, 2))
textInputService.updateState(oldValue = textFieldValue, newValue = textFieldValue)
+ val offsetMapping = OffsetMapping.Identity
val textLayoutResult = getTextLayoutResult(textFieldValue.text)
var textLayoutPositionInWindow = Offset(1f, 1f)
val innerTextFieldBounds = Rect.Zero
val decorationBoxBounds = Rect.Zero
textInputService.updateTextLayoutResult(
textFieldValue = textFieldValue,
+ offsetMapping = offsetMapping,
textLayoutResult = textLayoutResult,
textLayoutPositionInWindow = textLayoutPositionInWindow,
innerTextFieldBounds = innerTextFieldBounds,
@@ -226,6 +237,7 @@
textLayoutPositionInWindow = Offset(2f, 2f)
textInputService.updateTextLayoutResult(
textFieldValue = textFieldValue,
+ offsetMapping = offsetMapping,
textLayoutResult = textLayoutResult,
textLayoutPositionInWindow = textLayoutPositionInWindow,
innerTextFieldBounds = innerTextFieldBounds,
@@ -238,6 +250,7 @@
val expected =
builder.build(
textFieldValue,
+ offsetMapping,
textLayoutResult,
matrix,
innerTextFieldBounds,
@@ -256,6 +269,7 @@
textLayoutPositionInWindow = Offset(3f, 3f)
textInputService.updateTextLayoutResult(
textFieldValue = textFieldValue,
+ offsetMapping = offsetMapping,
textLayoutResult = textLayoutResult,
textLayoutPositionInWindow = textLayoutPositionInWindow,
innerTextFieldBounds = innerTextFieldBounds,
@@ -268,6 +282,7 @@
val expected2 =
builder.build(
textFieldValue,
+ offsetMapping,
textLayoutResult,
matrix,
innerTextFieldBounds,
@@ -282,12 +297,14 @@
TextFieldValue("abc", selection = TextRange(2), composition = TextRange(1, 2))
textInputService.updateState(oldValue = textFieldValue, newValue = textFieldValue)
+ val offsetMapping = OffsetMapping.Identity
val textLayoutResult = getTextLayoutResult(textFieldValue.text)
var textLayoutPositionInWindow = Offset(1f, 1f)
val innerTextFieldBounds = Rect.Zero
val decorationBoxBounds = Rect.Zero
textInputService.updateTextLayoutResult(
textFieldValue = textFieldValue,
+ offsetMapping = offsetMapping,
textLayoutResult = textLayoutResult,
textLayoutPositionInWindow = textLayoutPositionInWindow,
innerTextFieldBounds = innerTextFieldBounds,
@@ -304,6 +321,7 @@
val expected =
builder.build(
textFieldValue,
+ offsetMapping,
textLayoutResult,
matrix,
innerTextFieldBounds,
@@ -315,6 +333,7 @@
textLayoutPositionInWindow = Offset(2f, 2f)
textInputService.updateTextLayoutResult(
textFieldValue = textFieldValue,
+ offsetMapping = offsetMapping,
textLayoutResult = textLayoutResult,
textLayoutPositionInWindow = textLayoutPositionInWindow,
innerTextFieldBounds = innerTextFieldBounds,
@@ -327,6 +346,7 @@
val expected2 =
builder.build(
textFieldValue,
+ offsetMapping,
textLayoutResult,
matrix,
innerTextFieldBounds,
@@ -341,12 +361,14 @@
TextFieldValue("abc", selection = TextRange(2), composition = TextRange(1, 2))
textInputService.updateState(oldValue = textFieldValue, newValue = textFieldValue)
+ val offsetMapping = OffsetMapping.Identity
val textLayoutResult = getTextLayoutResult(textFieldValue.text)
var textLayoutPositionInWindow = Offset(1f, 1f)
val innerTextFieldBounds = Rect.Zero
val decorationBoxBounds = Rect.Zero
textInputService.updateTextLayoutResult(
textFieldValue = textFieldValue,
+ offsetMapping = offsetMapping,
textLayoutResult = textLayoutResult,
textLayoutPositionInWindow = textLayoutPositionInWindow,
innerTextFieldBounds = innerTextFieldBounds,
@@ -369,6 +391,7 @@
textLayoutPositionInWindow = Offset(2f, 2f)
textInputService.updateTextLayoutResult(
textFieldValue = textFieldValue,
+ offsetMapping = offsetMapping,
textLayoutResult = textLayoutResult,
textLayoutPositionInWindow = textLayoutPositionInWindow,
innerTextFieldBounds = innerTextFieldBounds,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/CursorAnchorInfoBuilder.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/CursorAnchorInfoBuilder.kt
index bb53826..707e9a6 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/CursorAnchorInfoBuilder.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/CursorAnchorInfoBuilder.kt
@@ -34,6 +34,7 @@
* [CursorAnchorInfo](https://developer.android.com/reference/android/view/inputmethod/CursorAnchorInfo).
*
* @param textFieldValue the text field's [TextFieldValue]
+ * @param offsetMapping the offset mapping for the text field's visual transformation
* @param textLayoutResult the text field's [TextLayoutResult]
* @param matrix matrix that transforms local coordinates into screen coordinates
* @param innerTextFieldBounds visible bounds of the text field in local coordinates, or an empty
@@ -47,6 +48,7 @@
*/
internal fun CursorAnchorInfo.Builder.build(
textFieldValue: TextFieldValue,
+ offsetMapping: OffsetMapping,
textLayoutResult: TextLayoutResult,
matrix: Matrix,
innerTextFieldBounds: Rect,
@@ -65,7 +67,7 @@
setSelectionRange(selectionStart, selectionEnd)
if (includeInsertionMarker) {
- setInsertionMarker(selectionStart, textLayoutResult, innerTextFieldBounds)
+ setInsertionMarker(selectionStart, offsetMapping, textLayoutResult, innerTextFieldBounds)
}
if (includeCharacterBounds) {
@@ -80,6 +82,7 @@
addCharacterBounds(
compositionStart,
compositionEnd,
+ offsetMapping,
textLayoutResult,
innerTextFieldBounds
)
@@ -103,15 +106,18 @@
private fun CursorAnchorInfo.Builder.setInsertionMarker(
selectionStart: Int,
+ offsetMapping: OffsetMapping,
textLayoutResult: TextLayoutResult,
innerTextFieldBounds: Rect
): CursorAnchorInfo.Builder {
if (selectionStart < 0) return this
- val cursorRect = textLayoutResult.getCursorRect(selectionStart)
+ val selectionStartTransformed = offsetMapping.originalToTransformed(selectionStart)
+ val cursorRect = textLayoutResult.getCursorRect(selectionStartTransformed)
val isTopVisible = innerTextFieldBounds.containsInclusive(cursorRect.topLeft)
val isBottomVisible = innerTextFieldBounds.containsInclusive(cursorRect.bottomLeft)
- val isRtl = textLayoutResult.getBidiRunDirection(selectionStart) == ResolvedTextDirection.Rtl
+ val isRtl =
+ textLayoutResult.getBidiRunDirection(selectionStartTransformed) == ResolvedTextDirection.Rtl
var flags = 0
if (isTopVisible || isBottomVisible) flags = flags or CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION
@@ -135,14 +141,28 @@
private fun CursorAnchorInfo.Builder.addCharacterBounds(
startOffset: Int,
endOffset: Int,
+ offsetMapping: OffsetMapping,
textLayoutResult: TextLayoutResult,
innerTextFieldBounds: Rect
): CursorAnchorInfo.Builder {
- val array = FloatArray((endOffset - startOffset) * 4)
- textLayoutResult.multiParagraph.fillBoundingBoxes(TextRange(startOffset, endOffset), array, 0)
+ val startOffsetTransformed = offsetMapping.originalToTransformed(startOffset)
+ val endOffsetTransformed = offsetMapping.originalToTransformed(endOffset)
+ val array = FloatArray((endOffsetTransformed - startOffsetTransformed) * 4)
+ textLayoutResult.multiParagraph.fillBoundingBoxes(
+ TextRange(
+ startOffsetTransformed,
+ endOffsetTransformed
+ ), array, 0
+ )
for (offset in startOffset until endOffset) {
- val arrayIndex = 4 * (offset - startOffset)
+ // It's possible for a visual transformation to hide some characters. If the character at
+ // the offset is hidden, then offsetTransformed points to the last preceding character that
+ // is not hidden. Since the CursorAnchorInfo API doesn't define what to return in this case,
+ // and visual transformations hiding characters should be rare, returning the bounds for the
+ // last preceding character is the simplest behavior.
+ val offsetTransformed = offsetMapping.originalToTransformed(offset)
+ val arrayIndex = 4 * (offsetTransformed - startOffsetTransformed)
val rect =
Rect(
array[arrayIndex] /* left */,
@@ -157,11 +177,11 @@
}
if (
!innerTextFieldBounds.containsInclusive(rect.topLeft) ||
- !innerTextFieldBounds.containsInclusive(rect.bottomRight)
+ !innerTextFieldBounds.containsInclusive(rect.bottomRight)
) {
flags = flags or CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION
}
- if (textLayoutResult.getBidiRunDirection(offset) == ResolvedTextDirection.Rtl) {
+ if (textLayoutResult.getBidiRunDirection(offsetTransformed) == ResolvedTextDirection.Rtl) {
flags = flags or CursorAnchorInfo.FLAG_IS_RTL
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/CursorAnchorInfoController.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/CursorAnchorInfoController.kt
index e386aa98..1271afa 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/CursorAnchorInfoController.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/CursorAnchorInfoController.kt
@@ -33,6 +33,7 @@
private var textFieldValue: TextFieldValue? = null
private var textLayoutResult: TextLayoutResult? = null
+ private var offsetMapping: OffsetMapping? = null
private var textLayoutPositionInWindow: Offset? = null
private var innerTextFieldBounds: Rect? = null
private var decorationBoxBounds: Rect? = null
@@ -83,6 +84,7 @@
* Notify the controller of layout and position changes.
*
* @param textFieldValue the text field's [TextFieldValue]
+ * @param offsetMapping the offset mapping for the visual transformation
* @param textLayoutResult the text field's [TextLayoutResult]
* @param textLayoutPositionInWindow position of the text field relative to the window
* @param innerTextFieldBounds visible bounds of the text field in local coordinates, or an
@@ -92,12 +94,14 @@
*/
fun updateTextLayoutResult(
textFieldValue: TextFieldValue,
+ offsetMapping: OffsetMapping,
textLayoutResult: TextLayoutResult,
textLayoutPositionInWindow: Offset,
innerTextFieldBounds: Rect,
decorationBoxBounds: Rect
) {
this.textFieldValue = textFieldValue
+ this.offsetMapping = offsetMapping
this.textLayoutResult = textLayoutResult
this.textLayoutPositionInWindow = textLayoutPositionInWindow
this.innerTextFieldBounds = innerTextFieldBounds
@@ -117,6 +121,7 @@
*/
fun invalidate() {
textFieldValue = null
+ offsetMapping = null
textLayoutResult = null
textLayoutPositionInWindow = null
innerTextFieldBounds = null
@@ -132,6 +137,7 @@
inputMethodManager.updateCursorAnchorInfo(
builder.build(
textFieldValue!!,
+ offsetMapping!!,
textLayoutResult!!,
matrix,
innerTextFieldBounds!!,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index 448968d..a7b9119 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -407,6 +407,7 @@
override fun updateTextLayoutResult(
textFieldValue: TextFieldValue,
+ offsetMapping: OffsetMapping,
textLayoutResult: TextLayoutResult,
textLayoutPositionInWindow: Offset,
innerTextFieldBounds: Rect,
@@ -414,6 +415,7 @@
) {
cursorAnchorInfoController.updateTextLayoutResult(
textFieldValue,
+ offsetMapping,
textLayoutResult,
textLayoutPositionInWindow,
innerTextFieldBounds,