Merge "Allow measurement from within LayoutModifier's placement block" into androidx-main
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureInPlacementTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureInPlacementTest.kt
new file mode 100644
index 0000000..031ba54
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureInPlacementTest.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.layout
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.TestActivity
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MeasureInPlacementTest {
+
+    @Suppress("DEPRECATION")
+    @get:Rule
+    val rule = createAndroidComposeRule<TestActivity>()
+
+    @Before
+    fun setup() {
+        rule.activity.hasFocusLatch.await(5, TimeUnit.SECONDS)
+    }
+
+    /**
+     * Make sure that measurement in the layout modifier's placement block doesn't crash.
+     */
+    @Test
+    fun measureInModifierPlacement() {
+        var childSize = IntSize.Zero
+        rule.setContent {
+            val measureInPlaceModifier = Modifier.layout { measurable, constraints ->
+                layout(100, 100) {
+                    val p = measurable.measure(constraints)
+                    childSize = IntSize(p.width, p.height)
+                    p.place(0, 0)
+                }
+            }
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .then(measureInPlaceModifier)
+            ) {
+                Box(Modifier.size(10.dp))
+            }
+        }
+
+        rule.waitForIdle()
+        assertThat(childSize.width).isGreaterThan(0)
+        assertThat(childSize.height).isGreaterThan(0)
+    }
+
+    /**
+     * Make sure that measurement in the layout's placement block doesn't crash.
+     */
+    @Test
+    fun measureInLayoutPlacement() {
+        var childSize = IntSize.Zero
+        rule.setContent {
+            Layout(modifier = Modifier.fillMaxSize(), content = @Composable {
+                Box(Modifier.size(10.dp))
+            }) { measurables, constraints ->
+                layout(100, 100) {
+                    val p = measurables[0].measure(constraints)
+                    childSize = IntSize(p.width, p.height)
+                    p.place(0, 0)
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        assertThat(childSize.width).isGreaterThan(0)
+        assertThat(childSize.height).isGreaterThan(0)
+    }
+
+    /**
+     * Make sure that measurement in the layout modifier's placement block doesn't crash when
+     * LookaheadLayout is used.
+     */
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun measureInModifierPlacementWithLookaheadLayout() {
+        var childSize = IntSize.Zero
+        rule.setContent {
+            LookaheadLayout(content = @Composable {
+                val measureInPlaceModifier = Modifier.layout { measurable, constraints ->
+                    layout(100, 100) {
+                        val p = measurable.measure(constraints)
+                        childSize = IntSize(p.width, p.height)
+                        p.place(0, 0)
+                    }
+                }
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .then(measureInPlaceModifier)
+                ) {
+                    Box(Modifier.size(10.dp))
+                }
+            }, measurePolicy = { measurables, constraints ->
+                val p = measurables[0].measure(constraints)
+                layout(p.width, p.height) {
+                    p.place(0, 0)
+                }
+            })
+        }
+
+        rule.waitForIdle()
+        assertThat(childSize.width).isGreaterThan(0)
+        assertThat(childSize.height).isGreaterThan(0)
+    }
+
+    /**
+     * Make sure that measurement in the layout's placement block doesn't crash when
+     * LookaheadLayout is used.
+     */
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun measureInLayoutPlacementWithLookaheadLayout() {
+        var childSize = IntSize.Zero
+        rule.setContent {
+            LookaheadLayout(content = @Composable {
+                Layout(modifier = Modifier.fillMaxSize(), content = @Composable {
+                    Box(Modifier.size(10.dp))
+                }) { measurables, constraints ->
+                    layout(100, 100) {
+                        val p = measurables[0].measure(constraints)
+                        childSize = IntSize(p.width, p.height)
+                        p.place(0, 0)
+                    }
+                }
+            }, measurePolicy = { measurables, constraints ->
+                val p = measurables[0].measure(constraints)
+                layout(p.width, p.height) {
+                    p.place(0, 0)
+                }
+            })
+        }
+
+        rule.waitForIdle()
+        assertThat(childSize.width).isGreaterThan(0)
+        assertThat(childSize.height).isGreaterThan(0)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index 6d91114..a35e1e0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -260,6 +260,7 @@
                 (!duringAlignmentLinesQuery && !innerCoordinator.isPlacingForAlignment &&
                     layoutPending)) {
                 layoutPending = false
+                val oldLayoutState = layoutState
                 layoutState = LayoutState.LayingOut
                 with(layoutNode) {
                     val owner = requireOwner()
@@ -280,7 +281,7 @@
                         }
                     }
                 }
-                layoutState = LayoutState.Idle
+                layoutState = oldLayoutState
 
                 if (innerCoordinator.isPlacingForAlignment &&
                     coordinatesAccessedDuringPlacement
@@ -422,7 +423,9 @@
             }
 
             // Post-lookahead (if any) placement
+            layoutState = LayoutState.LayingOut
             placeOuterCoordinator(position, zIndex, layerBlock)
+            layoutState = LayoutState.Idle
         }
 
         private fun placeOuterCoordinator(
@@ -691,6 +694,7 @@
                 lookaheadLayoutPending)
             ) {
                 lookaheadLayoutPending = false
+                val oldLayoutState = layoutState
                 layoutState = LayoutState.LookaheadLayingOut
                 val owner = layoutNode.requireOwner()
                 owner.snapshotObserver.observeLayoutSnapshotReads(layoutNode) {
@@ -722,7 +726,7 @@
                         }
                     }
                 }
-                layoutState = LayoutState.Idle
+                layoutState = oldLayoutState
                 if (coordinatesAccessedDuringPlacement &&
                     lookaheadDelegate.isPlacingForAlignment) {
                     requestLayout()
@@ -880,6 +884,7 @@
             zIndex: Float,
             layerBlock: (GraphicsLayerScope.() -> Unit)?
         ) {
+            layoutState = LayoutState.LookaheadLayingOut
             placedOnce = true
             if (position != lastPosition) {
                 notifyChildrenUsingCoordinatesWhilePlacing()
@@ -893,6 +898,7 @@
                 }
             }
             lastPosition = position
+            layoutState = LayoutState.Idle
         }
 
         // We are setting our measuredSize to match the coerced outerCoordinator size, to prevent
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
index d3349b8..2e5689a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutTreeConsistencyChecker.kt
@@ -70,8 +70,9 @@
             }
             if (layoutPending) {
                 return relayoutNodes.contains(this) ||
-                    parent?.measurePending == true ||
-                    parent?.layoutPending == true ||
+                    parent == null ||
+                    parent.measurePending ||
+                    parent.layoutPending ||
                     parentLayoutState == LayoutNode.LayoutState.Measuring ||
                     parentLayoutState == LayoutNode.LayoutState.LayingOut
             }
@@ -93,11 +94,12 @@
             }
             if (lookaheadLayoutPending) {
                 return relayoutNodes.contains(this) ||
-                    parent?.lookaheadMeasurePending == true ||
-                    parent?.lookaheadLayoutPending == true ||
+                    parent == null ||
+                    parent.lookaheadMeasurePending ||
+                    parent.lookaheadLayoutPending ||
                     parentLayoutState == LayoutNode.LayoutState.LookaheadMeasuring ||
                     parentLayoutState == LayoutNode.LayoutState.LookaheadLayingOut ||
-                    (parent?.layoutPending == true && mLookaheadScope!!.root == this)
+                    (parent.layoutPending && mLookaheadScope!!.root == this)
             }
         }
         return true