Merge "Account for core/layout offset in CursorAnchorInfo" into androidx-main
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
index 5b2a4c6..9457bb7 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
@@ -43,6 +43,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.toSize
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -109,11 +110,25 @@
         }
     }
 
-    private val windowOffset = Offset(12f, 34f)
+    private val layoutOffset = Offset(98f, 47f)
+    private val coreNodeOffset = Offset(73f, 50f)
+    private val coreNodeSize = IntSize(25, 51)
+    private val decoratorNodeOffset = Offset(84f, 59f)
+    private val decoratorNodeSize = IntSize(19, 66)
     private val layoutState = TextLayoutState().apply {
-        coreNodeCoordinates = TestLayoutCoordinates(windowOffset = windowOffset, isAttached = true)
+        textLayoutNodeCoordinates =
+            TestLayoutCoordinates(windowOffset = layoutOffset, isAttached = true)
+        coreNodeCoordinates = TestLayoutCoordinates(
+            windowOffset = coreNodeOffset,
+            size = coreNodeSize,
+            isAttached = true
+        )
         decoratorNodeCoordinates =
-            TestLayoutCoordinates(windowOffset = windowOffset, isAttached = true)
+            TestLayoutCoordinates(
+                windowOffset = decoratorNodeOffset,
+                size = decoratorNodeSize,
+                isAttached = true
+            )
     }
 
     @Test
@@ -130,9 +145,12 @@
             selection = textFieldState.selection,
             composition = textFieldState.composition,
             textLayoutResult = layoutState.layoutResult!!,
-            matrix = getAndroidMatrix(windowOffset),
-            innerTextFieldBounds = Rect.Zero,
-            decorationBoxBounds = Rect.Zero,
+            matrix = getAndroidMatrix(layoutOffset),
+            innerTextFieldBounds = Rect(coreNodeOffset - layoutOffset, coreNodeSize.toSize()),
+            decorationBoxBounds = Rect(
+                decoratorNodeOffset - layoutOffset,
+                decoratorNodeSize.toSize()
+            ),
         )
         Truth.assertThat(reportedCursorAnchorInfos).containsExactly(expectedInfo)
         reportedCursorAnchorInfos.clear()
@@ -182,8 +200,9 @@
         Truth.assertThat(reportedCursorAnchorInfos).isEmpty()
 
         // Trigger new layout.
-        layoutState.coreNodeCoordinates =
-            TestLayoutCoordinates(windowOffset = Offset(67f, 89f), isAttached = true)
+        val newLayoutOffset = Offset(67f, 89f)
+        layoutState.textLayoutNodeCoordinates =
+            TestLayoutCoordinates(windowOffset = newLayoutOffset, isAttached = true)
 
         // Monitoring update.
         val expectedInfo = builder.build(
@@ -191,9 +210,12 @@
             selection = textFieldState.selection,
             composition = textFieldState.composition,
             textLayoutResult = layoutState.layoutResult!!,
-            matrix = getAndroidMatrix(Offset(67f, 89f)),
-            innerTextFieldBounds = Rect.Zero,
-            decorationBoxBounds = Rect.Zero,
+            matrix = getAndroidMatrix(newLayoutOffset),
+            innerTextFieldBounds = Rect(coreNodeOffset - newLayoutOffset, coreNodeSize.toSize()),
+            decorationBoxBounds = Rect(
+                decoratorNodeOffset - newLayoutOffset,
+                decoratorNodeSize.toSize()
+            ),
         )
         Truth.assertThat(reportedCursorAnchorInfos).containsExactly(expectedInfo)
     }
@@ -213,16 +235,20 @@
             selection = textFieldState.selection,
             composition = textFieldState.composition,
             textLayoutResult = layoutState.layoutResult!!,
-            matrix = getAndroidMatrix(windowOffset),
-            innerTextFieldBounds = Rect.Zero,
-            decorationBoxBounds = Rect.Zero,
+            matrix = getAndroidMatrix(layoutOffset),
+            innerTextFieldBounds = Rect(coreNodeOffset - layoutOffset, coreNodeSize.toSize()),
+            decorationBoxBounds = Rect(
+                decoratorNodeOffset - layoutOffset,
+                decoratorNodeSize.toSize()
+            ),
         )
         Truth.assertThat(reportedCursorAnchorInfos).containsExactly(expectedInfo)
         reportedCursorAnchorInfos.clear()
 
         // Trigger new layout.
-        layoutState.coreNodeCoordinates =
-            TestLayoutCoordinates(windowOffset = Offset(67f, 89f), isAttached = true)
+        val newLayoutOffset = Offset(67f, 89f)
+        layoutState.textLayoutNodeCoordinates =
+            TestLayoutCoordinates(windowOffset = newLayoutOffset, isAttached = true)
 
         // Monitoring update.
         val expectedInfo2 = builder.build(
@@ -230,9 +256,12 @@
             selection = textFieldState.selection,
             composition = textFieldState.composition,
             textLayoutResult = layoutState.layoutResult!!,
-            matrix = getAndroidMatrix(Offset(67f, 89f)),
-            innerTextFieldBounds = Rect.Zero,
-            decorationBoxBounds = Rect.Zero,
+            matrix = getAndroidMatrix(newLayoutOffset),
+            innerTextFieldBounds = Rect(coreNodeOffset - newLayoutOffset, coreNodeSize.toSize()),
+            decorationBoxBounds = Rect(
+                decoratorNodeOffset - newLayoutOffset,
+                decoratorNodeSize.toSize()
+            ),
         )
         Truth.assertThat(reportedCursorAnchorInfos).containsExactly(expectedInfo2)
     }
@@ -252,9 +281,12 @@
             selection = textFieldState.selection,
             composition = textFieldState.composition,
             textLayoutResult = layoutState.layoutResult!!,
-            matrix = getAndroidMatrix(windowOffset),
-            innerTextFieldBounds = Rect.Zero,
-            decorationBoxBounds = Rect.Zero,
+            matrix = getAndroidMatrix(layoutOffset),
+            innerTextFieldBounds = Rect(coreNodeOffset - layoutOffset, coreNodeSize.toSize()),
+            decorationBoxBounds = Rect(
+                decoratorNodeOffset - layoutOffset,
+                decoratorNodeSize.toSize()
+            ),
         )
         Truth.assertThat(reportedCursorAnchorInfos).containsExactly(expectedInfo)
         reportedCursorAnchorInfos.clear()
@@ -335,12 +367,13 @@
         override fun localPositionOf(
             sourceCoordinates: LayoutCoordinates,
             relativeToSource: Offset
-        ): Offset = relativeToSource
+        ): Offset = sourceCoordinates.localToWindow(relativeToSource) - windowOffset
 
         override fun localBoundingBoxOf(
             sourceCoordinates: LayoutCoordinates,
             clipBounds: Boolean
-        ): Rect = Rect.Zero
+        ): Rect =
+            Rect(localPositionOf(sourceCoordinates, Offset.Zero), sourceCoordinates.size.toSize())
 
         override fun transformToScreen(matrix: Matrix) {
             matrix.translate(windowOffset.x, windowOffset.y, 0f)
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilder.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilder.android.kt
index 99d3944..e4168d9 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilder.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilder.android.kt
@@ -28,11 +28,11 @@
  * Helper function to build
  * [CursorAnchorInfo](https://developer.android.com/reference/android/view/inputmethod/CursorAnchorInfo).
  *
- * @param matrix matrix that transforms local coordinates into screen coordinates
- * @param innerTextFieldBounds visible bounds of the text field in local coordinates, or an empty
- *   rectangle if the text field is not visible
- * @param decorationBoxBounds visible bounds of the decoration box in local coordinates, or an empty
- *   rectangle if the decoration box is not visible
+ * @param matrix matrix that transforms text layout coordinates into screen coordinates
+ * @param innerTextFieldBounds visible bounds of the text field in text layout coordinates, or an
+ *   empty rectangle if the text field is not visible
+ * @param decorationBoxBounds visible bounds of the decoration box in text layout coordinates, or an
+ *   empty rectangle if the decoration box is not visible
  */
 internal fun CursorAnchorInfo.Builder.build(
     text: CharSequence,
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoController.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoController.android.kt
index 81b76da..a45685a 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoController.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoController.android.kt
@@ -23,10 +23,9 @@
 import android.view.inputmethod.InputConnection.CURSOR_UPDATE_FILTER_EDITOR_BOUNDS
 import android.view.inputmethod.InputConnection.CURSOR_UPDATE_FILTER_INSERTION_MARKER
 import android.view.inputmethod.InputConnection.CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.text.selection.visibleBounds
 import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.setFrom
 import kotlinx.coroutines.CoroutineScope
@@ -36,7 +35,6 @@
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.launch
 
-@OptIn(ExperimentalFoundationApi::class)
 internal class CursorAnchorInfoController(
     private val textFieldState: TransformedTextFieldState,
     private val textLayoutState: TextLayoutState,
@@ -165,6 +163,9 @@
 
     private fun calculateCursorAnchorInfo(): CursorAnchorInfo? {
         // State reads
+        val textLayoutCoordinates = textLayoutState.textLayoutNodeCoordinates
+            ?.takeIf { it.isAttached }
+            ?: return null
         val coreCoordinates = textLayoutState.coreNodeCoordinates
             ?.takeIf { it.isAttached }
             ?: return null
@@ -175,16 +176,15 @@
             ?: return null
         val text = textFieldState.visualText
 
-        // Updates matrix to transform text field local coordinates to screen coordinates.
+        // Updates matrix to transform text layout coordinates to screen coordinates.
         matrix.reset()
-        coreCoordinates.transformToScreen(matrix)
+        textLayoutCoordinates.transformToScreen(matrix)
         androidMatrix.setFrom(matrix)
 
-        val innerTextFieldBounds: Rect = coreCoordinates.visibleBounds()
-        val decorationBoxBounds: Rect = coreCoordinates.localBoundingBoxOf(
-            decorationBoxCoordinates,
-            clipBounds = false
-        )
+        val innerTextFieldBounds = coreCoordinates.visibleBounds()
+            .translate(textLayoutCoordinates.localPositionOf(coreCoordinates, Offset.Zero))
+        val decorationBoxBounds = decorationBoxCoordinates.visibleBounds()
+            .translate(textLayoutCoordinates.localPositionOf(decorationBoxCoordinates, Offset.Zero))
         return builder.build(
             text,
             text.selection,