Merge "Take into account layers transformations from outer coordinator" into androidx-main
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt
index 1164119..3738cf2 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnGlobalRectChangedTest.kt
@@ -1502,4 +1502,38 @@
             rule.runOnIdle { assertThat(actualPosition).isEqualTo(IntOffset(30, 30)) }
         }
     }
+
+    @Test
+    fun testLayoutModifierPlacingWithOffsetAndScale() {
+        var actualPosition: IntOffset = IntOffset.Max
+        var actualPositionChild: IntOffset = IntOffset.Max
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                Box {
+                    Box(
+                        Modifier.layout { measurable, constraints ->
+                                val placeable = measurable.measure(constraints)
+                                layout(constraints.maxWidth, constraints.maxHeight) {
+                                    placeable.placeWithLayer(10, 10) {
+                                        scaleX = 2f
+                                        scaleY = 2f
+                                    }
+                                }
+                            }
+                            .onLayoutRectChanged(0, 0) { actualPosition = it.positionInRoot }
+                    ) {
+                        Box(
+                            Modifier.onLayoutRectChanged(0, 0) {
+                                    actualPositionChild = it.positionInRoot
+                                }
+                                .size(10.dp)
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle { assertThat(actualPosition).isEqualTo(IntOffset(5, 5)) }
+        rule.runOnIdle { assertThat(actualPositionChild).isEqualTo(IntOffset(5, 5)) }
+    }
 }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt
index c0a0395..095b038 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt
@@ -645,7 +645,7 @@
 
     @Test
     @SmallTest
-    fun testScaledBox() {
+    fun testTranslatedAndRotatedBox() {
         var toggle by mutableStateOf(true)
         rule.setContent {
             Box(
@@ -666,7 +666,7 @@
 
     @Test
     @SmallTest
-    fun testScaledBoxUpdate() {
+    fun testScaledBox() {
         rule.setContent {
             Box(Modifier.testTag("outer").padding(10.dp).scale(2f)) {
                 Box(Modifier.testTag("inner").size(10.dp))
@@ -844,6 +844,48 @@
         }
     }
 
+    @Test
+    @SmallTest
+    fun testLayoutPlacingWithOffsetAndScale() {
+        rule.setContent {
+            Layout(content = { Box(Modifier.testTag("inner").size(10.dp)) }) {
+                measurables,
+                constraints ->
+                val placeable = measurables.first().measure(constraints)
+                layout(constraints.maxWidth, constraints.maxHeight) {
+                    val offset = 10.dp.roundToPx()
+                    placeable.placeWithLayer(offset, offset) {
+                        scaleX = 2f
+                        scaleY = 2f
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("inner").assertRectDp(5.dp, 5.dp, 25.dp, 25.dp)
+    }
+
+    @Test
+    @SmallTest
+    fun testLayoutPlacingWithTranslateOnLayer() {
+        rule.setContent {
+            Layout(content = { Box(Modifier.testTag("inner").size(10.dp)) }) {
+                measurables,
+                constraints ->
+                val placeable = measurables.first().measure(constraints)
+                layout(constraints.maxWidth, constraints.maxHeight) {
+                    val offset = 5.dp.roundToPx()
+                    placeable.placeWithLayer(offset, offset) {
+                        translationX = 10.dp.toPx()
+                        translationY = 20.dp.toPx()
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("inner").assertRectDp(15.dp, 25.dp, 25.dp, 35.dp)
+    }
+
     /**
      * Lazy Column example that can be used to reproduce issues related to re-using LayoutNodes
      * where each item has the same Layout.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
index 36055d9..ed3fb5e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
@@ -24,7 +24,6 @@
 import androidx.compose.ui.currentTimeMillis
 import androidx.compose.ui.focus.FocusTargetModifierNode
 import androidx.compose.ui.geometry.MutableRect
-import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.isIdentity
 import androidx.compose.ui.node.DelegatableNode
@@ -275,7 +274,13 @@
     }
 
     private fun recalculateOffsetFromRoot(layoutNode: LayoutNode) {
-        val position = layoutNode.outerCoordinator.position
+        val outer = layoutNode.outerCoordinator
+        var position = outer.applyLayerTransformation(IntOffset.Zero)
+        if (!position.isSet) {
+            layoutNode.offsetFromRoot = IntOffset.Max
+            return
+        }
+        position += outer.position
         val parent = layoutNode.parent
         layoutNode.offsetFromRoot =
             if (parent != null) {
@@ -384,37 +389,51 @@
         var coordinator: NodeCoordinator? = this
         while (coordinator != null) {
             val layer = coordinator.layer
-            rect.translate(coordinator.position.toOffset())
-            coordinator = coordinator.wrappedBy
             if (layer != null) {
                 val matrix = layer.underlyingMatrix
                 if (!matrix.isIdentity()) {
                     matrix.map(rect)
                 }
             }
+            rect.translate(coordinator.position.toOffset())
+            coordinator = coordinator.wrappedBy
         }
     }
 
-    private fun LayoutNode.outerToInnerOffset(): IntOffset {
-        val terminator = outerCoordinator
-        var position = Offset.Zero
-        var coordinator: NodeCoordinator? = innerCoordinator
-        while (coordinator != null) {
-            if (coordinator === terminator) break
-            val layer = coordinator.layer
-            position += coordinator.position
-            coordinator = coordinator.wrappedBy
-            if (layer != null) {
-                val matrix = layer.underlyingMatrix
-                val analysis = matrix.analyzeComponents()
-                if (analysis.isIdentity) continue
+    private fun NodeCoordinator.applyLayerTransformation(position: IntOffset): IntOffset {
+        val layer = layer
+        if (layer != null) {
+            val matrix = layer.underlyingMatrix
+            val analysis = matrix.analyzeComponents()
+            if (!analysis.isIdentity) {
                 if (analysis.hasNonTranslationComponents) {
                     return IntOffset.Max
                 }
-                position = matrix.map(position)
+                return matrix.map(position.toOffset()).round()
             }
         }
-        return position.round()
+        return position
+    }
+
+    /**
+     * @return combined offset for all coordinators not including the outer one, the outer offset is
+     *   added into [LayoutNode.offsetFromRoot] instead. it can also return [IntOffset.Max], if the
+     *   layer transformation is too complex.
+     */
+    private fun LayoutNode.outerToInnerOffset(): IntOffset {
+        val terminator = outerCoordinator
+        var position = IntOffset.Zero
+        var coordinator: NodeCoordinator? = innerCoordinator
+        while (coordinator != null) {
+            if (coordinator === terminator) break
+            position = coordinator.applyLayerTransformation(position)
+            if (position == IntOffset.Max) {
+                return IntOffset.Max
+            }
+            position += coordinator.position
+            coordinator = coordinator.wrappedBy
+        }
+        return position
     }
 
     fun remove(layoutNode: LayoutNode) {