diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseKeyEventsTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseKeyEventsTestCase.kt
index 37722e020696a..3c2fd67d02035 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseKeyEventsTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseKeyEventsTestCase.kt
@@ -42,6 +42,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.testutils.PollingCheck
import androidx.testutils.withActivity
@@ -51,7 +52,6 @@ import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
-import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -144,14 +144,11 @@ abstract class BaseKeyEventsTestCase(private val activityC
}
}
+ @SdkSuppress(maxSdkVersion = 35) // b/460511639
@Test
@LargeTest
@Throws(InterruptedException::class)
fun testBackCollapsesActionView() {
- assumeFalse(
- "Test fails on cuttlefish b/460511639",
- Build.MODEL.contains("Cuttlefish", ignoreCase = true),
- )
with(ActivityScenario.launch(activityClass)) {
// Click on the Search menu item
onView(withId(R.id.action_search)).perform(click())
@@ -208,11 +205,8 @@ abstract class BaseKeyEventsTestCase(private val activityC
@Test
@MediumTest
+ @SdkSuppress(maxSdkVersion = 35) // b/460511639
fun testBackPressWithEmptyMenuHandledByActivity() {
- assumeFalse(
- "Test fails on cuttlefish b/460511639",
- Build.MODEL.contains("Cuttlefish", ignoreCase = true),
- )
with(ActivityScenario.launch(activityClass)) {
// Pressing the menu key with an empty menu does nothing.
val scenario = (this as? ActivityScenario)!!
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/KeyEventsTestCaseWithWindowDecor.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/KeyEventsTestCaseWithWindowDecor.kt
index b71cec6ec0a7c..a02303b7542c3 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/KeyEventsTestCaseWithWindowDecor.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/KeyEventsTestCaseWithWindowDecor.kt
@@ -16,7 +16,6 @@
package androidx.appcompat.app
import android.content.Context
-import android.os.Build
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
@@ -27,23 +26,20 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.pressKey
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import androidx.testutils.PollingCheck
import androidx.testutils.withActivity
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
-import org.junit.Assume.assumeFalse
import org.junit.Test
class KeyEventsTestCaseWithWindowDecor :
BaseKeyEventsTestCase(WindowDecorAppCompatActivity::class.java) {
+ @SdkSuppress(maxSdkVersion = 35) // b/460511639
@Test
@LargeTest
@Throws(Throwable::class)
fun testUnhandledKeys() {
- assumeFalse(
- "Test fails on cuttlefish b/460511639",
- Build.MODEL.contains("Cuttlefish", ignoreCase = true),
- )
with(ActivityScenario.launch(WindowDecorAppCompatActivity::class.java)) {
val listener = MockUnhandledKeyListener()
val mockView1: View = withActivity { HandlerView(this) }
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ArcSpline.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ArcSpline.kt
index 5f77d0171189d..9cc7b15e15325 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ArcSpline.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ArcSpline.kt
@@ -387,6 +387,10 @@ private const val HalfPi = (PI * 0.5).toFloat()
private val OurPercentCache: FloatArray = FloatArray(91)
-internal expect inline fun toRadians(value: Double): Double
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun toRadians(value: Double): Double {
+ // No Kotlin multiplatform function out of a box, but it's a trivial calculation
+ return value * (PI / 180.0)
+}
internal expect inline fun binarySearch(array: FloatArray, position: Float): Int
diff --git a/compose/animation/animation-core/src/jvmAndAndroidMain/kotlin/androidx/compose/animation/core/ArcSpline.jvmAndAndroid.kt b/compose/animation/animation-core/src/jvmAndAndroidMain/kotlin/androidx/compose/animation/core/ArcSpline.jvmAndAndroid.kt
index 5ade8f9e4eac4..728488d72e810 100644
--- a/compose/animation/animation-core/src/jvmAndAndroidMain/kotlin/androidx/compose/animation/core/ArcSpline.jvmAndAndroid.kt
+++ b/compose/animation/animation-core/src/jvmAndAndroidMain/kotlin/androidx/compose/animation/core/ArcSpline.jvmAndAndroid.kt
@@ -16,11 +16,6 @@
package androidx.compose.animation.core
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun toRadians(value: Double): Double {
- return Math.toRadians(value)
-}
-
@Suppress("NOTHING_TO_INLINE")
internal actual inline fun binarySearch(array: FloatArray, position: Float): Int {
return array.binarySearch(position)
diff --git a/compose/animation/animation-core/src/linuxx64StubsMain/kotlin/androidx/compose/animation/core/ArcSpline.linuxx64Stubs.kt b/compose/animation/animation-core/src/linuxx64StubsMain/kotlin/androidx/compose/animation/core/ArcSpline.linuxx64Stubs.kt
index d8d41dd9de498..9bd4c8e27e781 100644
--- a/compose/animation/animation-core/src/linuxx64StubsMain/kotlin/androidx/compose/animation/core/ArcSpline.linuxx64Stubs.kt
+++ b/compose/animation/animation-core/src/linuxx64StubsMain/kotlin/androidx/compose/animation/core/ArcSpline.linuxx64Stubs.kt
@@ -16,9 +16,6 @@
package androidx.compose.animation.core
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun toRadians(value: Double): Double = implementedInJetBrainsFork()
-
@Suppress("NOTHING_TO_INLINE")
internal actual inline fun binarySearch(array: FloatArray, position: Float): Int =
implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation-layout/api/current.txt b/compose/foundation/foundation-layout/api/current.txt
index ecfcd2e395dd3..2ec9ce764e817 100644
--- a/compose/foundation/foundation-layout/api/current.txt
+++ b/compose/foundation/foundation-layout/api/current.txt
@@ -204,6 +204,9 @@ package androidx.compose.foundation.layout {
property @Deprecated public abstract androidx.compose.ui.unit.Dp maxWidthInLine;
}
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="This foundation layout API is experimental and is likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGridApi {
+ }
+
@SuppressCompatibility @kotlin.RequiresOptIn(message="The API of this layout is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalLayoutApi {
}
@@ -275,6 +278,110 @@ package androidx.compose.foundation.layout {
method @BytecodeOnly @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalLayoutApi public static androidx.compose.ui.Modifier! fillMaxRowHeight$default(androidx.compose.foundation.layout.FlowRowScope!, androidx.compose.ui.Modifier!, float, int, Object!);
}
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @kotlin.jvm.JvmInline public final value class Fr {
+ ctor @KotlinOnly public Fr(float value);
+ method @BytecodeOnly public static androidx.compose.foundation.layout.Fr! box-impl(float);
+ method @BytecodeOnly public static float constructor-impl(float);
+ method @InaccessibleFromKotlin public float getValue();
+ method @BytecodeOnly public float unbox-impl();
+ property public float value;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.foundation.layout.LayoutScopeMarker public interface GridConfigurationScope extends androidx.compose.ui.unit.Density {
+ method @KotlinOnly public void column(androidx.compose.foundation.layout.Fr weight);
+ method @KotlinOnly public void column(androidx.compose.foundation.layout.GridTrackSize size);
+ method @KotlinOnly public void column(androidx.compose.ui.unit.Dp size);
+ method public void column(float percentage);
+ method @BytecodeOnly public void column-0680j_4(float);
+ method @BytecodeOnly public void column-118E5d0(long);
+ method @BytecodeOnly public void column-XZblgos(float);
+ method @KotlinOnly public void columnGap(androidx.compose.ui.unit.Dp gap);
+ method @BytecodeOnly public void columnGap-0680j_4(float);
+ method @KotlinOnly public void gap(androidx.compose.ui.unit.Dp all);
+ method @KotlinOnly public void gap(androidx.compose.ui.unit.Dp row, androidx.compose.ui.unit.Dp column);
+ method @BytecodeOnly public void gap-0680j_4(float);
+ method @BytecodeOnly public void gap-YgX7TsA(float, float);
+ method @BytecodeOnly public int getFlow-ITJdzs4();
+ method @BytecodeOnly public default float getFr-9P9H2UQ(double);
+ method @BytecodeOnly public default float getFr-9P9H2UQ(float);
+ method @BytecodeOnly public default float getFr-9P9H2UQ(int);
+ method @KotlinOnly public void row(androidx.compose.foundation.layout.Fr weight);
+ method @KotlinOnly public void row(androidx.compose.foundation.layout.GridTrackSize size);
+ method @KotlinOnly public void row(androidx.compose.ui.unit.Dp size);
+ method public void row(float percentage);
+ method @BytecodeOnly public void row-0680j_4(float);
+ method @BytecodeOnly public void row-118E5d0(long);
+ method @BytecodeOnly public void row-XZblgos(float);
+ method @KotlinOnly public void rowGap(androidx.compose.ui.unit.Dp gap);
+ method @BytecodeOnly public void rowGap-0680j_4(float);
+ method @BytecodeOnly public void setFlow-4t4_IgM(int);
+ property public abstract androidx.compose.foundation.layout.GridFlow flow;
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Stable public default androidx.compose.foundation.layout.Fr int.fr;
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Stable public default androidx.compose.foundation.layout.Fr float.fr;
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Stable public default androidx.compose.foundation.layout.Fr double.fr;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @kotlin.jvm.JvmInline public final value class GridFlow {
+ method @BytecodeOnly public static androidx.compose.foundation.layout.GridFlow! box-impl(int);
+ method @BytecodeOnly public int unbox-impl();
+ field public static final androidx.compose.foundation.layout.GridFlow.Companion Companion;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static final class GridFlow.Companion {
+ method @BytecodeOnly @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public int getColumn-ITJdzs4();
+ method @BytecodeOnly @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public int getRow-ITJdzs4();
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public inline androidx.compose.foundation.layout.GridFlow Column;
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public inline androidx.compose.foundation.layout.GridFlow Row;
+ }
+
+ @SuppressCompatibility public final class GridKt {
+ method @BytecodeOnly @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Composable public static void Grid(kotlin.jvm.functions.Function1 super androidx.compose.foundation.layout.GridConfigurationScope!,kotlin.Unit!>, androidx.compose.ui.Modifier?, kotlin.jvm.functions.Function3 super androidx.compose.foundation.layout.GridScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+ method @KotlinOnly @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Composable public static inline void Grid(kotlin.jvm.functions.Function1 config, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1 content);
+ method @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static void columns(androidx.compose.foundation.layout.GridConfigurationScope, androidx.compose.foundation.layout.GridTrackSpec... specs);
+ method @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static void rows(androidx.compose.foundation.layout.GridConfigurationScope, androidx.compose.foundation.layout.GridTrackSpec... specs);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface GridScope {
+ method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier gridItem(androidx.compose.ui.Modifier, optional int row, optional int column, optional int rowSpan, optional int columnSpan, optional androidx.compose.ui.Alignment alignment);
+ method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier gridItem(androidx.compose.ui.Modifier, kotlin.ranges.IntRange rows, kotlin.ranges.IntRange columns, optional androidx.compose.ui.Alignment alignment);
+ method @BytecodeOnly @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! gridItem$default(androidx.compose.foundation.layout.GridScope!, androidx.compose.ui.Modifier!, int, int, int, int, androidx.compose.ui.Alignment!, int, Object!);
+ method @BytecodeOnly @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! gridItem$default(androidx.compose.foundation.layout.GridScope!, androidx.compose.ui.Modifier!, kotlin.ranges.IntRange!, kotlin.ranges.IntRange!, androidx.compose.ui.Alignment!, int, Object!);
+ field public static final androidx.compose.foundation.layout.GridScope.Companion Companion;
+ field @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static final int GridIndexUnspecified = 0; // 0x0
+ field @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static final int MaxGridIndex = 1000; // 0x3e8
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static final class GridScope.Companion {
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static int GridIndexUnspecified;
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static int MaxGridIndex;
+ field public static final int GridIndexUnspecified = 0; // 0x0
+ field public static final int MaxGridIndex = 1000; // 0x3e8
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class GridTrackSize implements androidx.compose.foundation.layout.GridTrackSpec {
+ method @BytecodeOnly public static androidx.compose.foundation.layout.GridTrackSize! box-impl(long);
+ method @BytecodeOnly public long unbox-impl();
+ field public static final androidx.compose.foundation.layout.GridTrackSize.Companion Companion;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static final class GridTrackSize.Companion {
+ method @KotlinOnly @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize Fixed(androidx.compose.ui.unit.Dp size);
+ method @BytecodeOnly @androidx.compose.runtime.Stable public long Fixed-psSkOvk(float);
+ method @KotlinOnly @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize Flex(@FloatRange(from=0.0) androidx.compose.foundation.layout.Fr weight);
+ method @BytecodeOnly @androidx.compose.runtime.Stable public long Flex-KGB9zo8(@FloatRange(from=0.0) float);
+ method @KotlinOnly @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize Percentage(@FloatRange(from=0.0) float value);
+ method @BytecodeOnly @androidx.compose.runtime.Stable public long Percentage-9Tp3RV8(@FloatRange(from=0.0) float);
+ method @BytecodeOnly public long getAuto-eyNpfc4();
+ method @BytecodeOnly public long getMaxContent-eyNpfc4();
+ method @BytecodeOnly public long getMinContent-eyNpfc4();
+ property @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize Auto;
+ property @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize MaxContent;
+ property @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize MinContent;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public sealed exhaustive interface GridTrackSpec {
+ }
+
public final class IntrinsicKt {
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier requiredHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
diff --git a/compose/foundation/foundation-layout/api/restricted_current.txt b/compose/foundation/foundation-layout/api/restricted_current.txt
index e9d0231946d28..e72e674f85b17 100644
--- a/compose/foundation/foundation-layout/api/restricted_current.txt
+++ b/compose/foundation/foundation-layout/api/restricted_current.txt
@@ -223,6 +223,9 @@ package androidx.compose.foundation.layout {
property @Deprecated public abstract androidx.compose.ui.unit.Dp maxWidthInLine;
}
+ @SuppressCompatibility @kotlin.RequiresOptIn(message="This foundation layout API is experimental and is likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGridApi {
+ }
+
@SuppressCompatibility @kotlin.RequiresOptIn(message="The API of this layout is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalLayoutApi {
}
@@ -298,6 +301,123 @@ package androidx.compose.foundation.layout {
method @BytecodeOnly @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalLayoutApi public static androidx.compose.ui.Modifier! fillMaxRowHeight$default(androidx.compose.foundation.layout.FlowRowScope!, androidx.compose.ui.Modifier!, float, int, Object!);
}
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @kotlin.jvm.JvmInline public final value class Fr {
+ ctor @KotlinOnly public Fr(float value);
+ method @BytecodeOnly public static androidx.compose.foundation.layout.Fr! box-impl(float);
+ method @BytecodeOnly public static float constructor-impl(float);
+ method @InaccessibleFromKotlin public float getValue();
+ method @BytecodeOnly public float unbox-impl();
+ property public float value;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.foundation.layout.LayoutScopeMarker public interface GridConfigurationScope extends androidx.compose.ui.unit.Density {
+ method @KotlinOnly public void column(androidx.compose.foundation.layout.Fr weight);
+ method @KotlinOnly public void column(androidx.compose.foundation.layout.GridTrackSize size);
+ method @KotlinOnly public void column(androidx.compose.ui.unit.Dp size);
+ method public void column(float percentage);
+ method @BytecodeOnly public void column-0680j_4(float);
+ method @BytecodeOnly public void column-118E5d0(long);
+ method @BytecodeOnly public void column-XZblgos(float);
+ method @KotlinOnly public void columnGap(androidx.compose.ui.unit.Dp gap);
+ method @BytecodeOnly public void columnGap-0680j_4(float);
+ method @KotlinOnly public void gap(androidx.compose.ui.unit.Dp all);
+ method @KotlinOnly public void gap(androidx.compose.ui.unit.Dp row, androidx.compose.ui.unit.Dp column);
+ method @BytecodeOnly public void gap-0680j_4(float);
+ method @BytecodeOnly public void gap-YgX7TsA(float, float);
+ method @BytecodeOnly public int getFlow-ITJdzs4();
+ method @BytecodeOnly public default float getFr-9P9H2UQ(double);
+ method @BytecodeOnly public default float getFr-9P9H2UQ(float);
+ method @BytecodeOnly public default float getFr-9P9H2UQ(int);
+ method @KotlinOnly public void row(androidx.compose.foundation.layout.Fr weight);
+ method @KotlinOnly public void row(androidx.compose.foundation.layout.GridTrackSize size);
+ method @KotlinOnly public void row(androidx.compose.ui.unit.Dp size);
+ method public void row(float percentage);
+ method @BytecodeOnly public void row-0680j_4(float);
+ method @BytecodeOnly public void row-118E5d0(long);
+ method @BytecodeOnly public void row-XZblgos(float);
+ method @KotlinOnly public void rowGap(androidx.compose.ui.unit.Dp gap);
+ method @BytecodeOnly public void rowGap-0680j_4(float);
+ method @BytecodeOnly public void setFlow-4t4_IgM(int);
+ property public abstract androidx.compose.foundation.layout.GridFlow flow;
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Stable public default androidx.compose.foundation.layout.Fr int.fr;
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Stable public default androidx.compose.foundation.layout.Fr float.fr;
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Stable public default androidx.compose.foundation.layout.Fr double.fr;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @kotlin.jvm.JvmInline public final value class GridFlow {
+ ctor @KotlinOnly @kotlin.PublishedApi internal GridFlow(int bits);
+ method @BytecodeOnly public static androidx.compose.foundation.layout.GridFlow! box-impl(int);
+ method @BytecodeOnly @kotlin.PublishedApi internal static int constructor-impl(int);
+ method @BytecodeOnly public int unbox-impl();
+ field public static final androidx.compose.foundation.layout.GridFlow.Companion Companion;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static final class GridFlow.Companion {
+ method @BytecodeOnly @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public int getColumn-ITJdzs4();
+ method @BytecodeOnly @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public int getRow-ITJdzs4();
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public inline androidx.compose.foundation.layout.GridFlow Column;
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public inline androidx.compose.foundation.layout.GridFlow Row;
+ }
+
+ @SuppressCompatibility public final class GridKt {
+ method @BytecodeOnly @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Composable public static void Grid(kotlin.jvm.functions.Function1 super androidx.compose.foundation.layout.GridConfigurationScope!,kotlin.Unit!>, androidx.compose.ui.Modifier?, kotlin.jvm.functions.Function3 super androidx.compose.foundation.layout.GridScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+ method @KotlinOnly @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Composable public static inline void Grid(kotlin.jvm.functions.Function1 config, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1 content);
+ method @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static void columns(androidx.compose.foundation.layout.GridConfigurationScope, androidx.compose.foundation.layout.GridTrackSpec... specs);
+ method @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static void rows(androidx.compose.foundation.layout.GridConfigurationScope, androidx.compose.foundation.layout.GridTrackSpec... specs);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @kotlin.PublishedApi internal final class GridMeasurePolicy implements androidx.compose.ui.layout.MeasurePolicy {
+ ctor public GridMeasurePolicy(androidx.compose.runtime.State extends kotlin.jvm.functions.Function1 super androidx.compose.foundation.layout.GridConfigurationScope,kotlin.Unit>> configState);
+ method @KotlinOnly public androidx.compose.ui.layout.MeasureResult measure(androidx.compose.ui.layout.MeasureScope, java.util.List measurables, androidx.compose.ui.unit.Constraints constraints);
+ method @BytecodeOnly public androidx.compose.ui.layout.MeasureResult measure-3p2s80s(androidx.compose.ui.layout.MeasureScope, java.util.List extends androidx.compose.ui.layout.Measurable!>, long);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface GridScope {
+ method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier gridItem(androidx.compose.ui.Modifier, optional int row, optional int column, optional int rowSpan, optional int columnSpan, optional androidx.compose.ui.Alignment alignment);
+ method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier gridItem(androidx.compose.ui.Modifier, kotlin.ranges.IntRange rows, kotlin.ranges.IntRange columns, optional androidx.compose.ui.Alignment alignment);
+ method @BytecodeOnly @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! gridItem$default(androidx.compose.foundation.layout.GridScope!, androidx.compose.ui.Modifier!, int, int, int, int, androidx.compose.ui.Alignment!, int, Object!);
+ method @BytecodeOnly @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! gridItem$default(androidx.compose.foundation.layout.GridScope!, androidx.compose.ui.Modifier!, kotlin.ranges.IntRange!, kotlin.ranges.IntRange!, androidx.compose.ui.Alignment!, int, Object!);
+ field public static final androidx.compose.foundation.layout.GridScope.Companion Companion;
+ field @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static final int GridIndexUnspecified = 0; // 0x0
+ field @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static final int MaxGridIndex = 1000; // 0x3e8
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static final class GridScope.Companion {
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static int GridIndexUnspecified;
+ property @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static int MaxGridIndex;
+ field public static final int GridIndexUnspecified = 0; // 0x0
+ field public static final int MaxGridIndex = 1000; // 0x3e8
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @kotlin.PublishedApi internal final class GridScopeInstance implements androidx.compose.foundation.layout.GridScope {
+ method public androidx.compose.ui.Modifier gridItem(androidx.compose.ui.Modifier, int row, int column, int rowSpan, int columnSpan, androidx.compose.ui.Alignment alignment);
+ method public androidx.compose.ui.Modifier gridItem(androidx.compose.ui.Modifier, kotlin.ranges.IntRange rows, kotlin.ranges.IntRange columns, androidx.compose.ui.Alignment alignment);
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class GridTrackSize implements androidx.compose.foundation.layout.GridTrackSpec {
+ method @BytecodeOnly public static androidx.compose.foundation.layout.GridTrackSize! box-impl(long);
+ method @BytecodeOnly public long unbox-impl();
+ field public static final androidx.compose.foundation.layout.GridTrackSize.Companion Companion;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public static final class GridTrackSize.Companion {
+ method @KotlinOnly @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize Fixed(androidx.compose.ui.unit.Dp size);
+ method @BytecodeOnly @androidx.compose.runtime.Stable public long Fixed-psSkOvk(float);
+ method @KotlinOnly @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize Flex(@FloatRange(from=0.0) androidx.compose.foundation.layout.Fr weight);
+ method @BytecodeOnly @androidx.compose.runtime.Stable public long Flex-KGB9zo8(@FloatRange(from=0.0) float);
+ method @KotlinOnly @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize Percentage(@FloatRange(from=0.0) float value);
+ method @BytecodeOnly @androidx.compose.runtime.Stable public long Percentage-9Tp3RV8(@FloatRange(from=0.0) float);
+ method @BytecodeOnly public long getAuto-eyNpfc4();
+ method @BytecodeOnly public long getMaxContent-eyNpfc4();
+ method @BytecodeOnly public long getMinContent-eyNpfc4();
+ property @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize Auto;
+ property @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize MaxContent;
+ property @androidx.compose.runtime.Stable public androidx.compose.foundation.layout.GridTrackSize MinContent;
+ }
+
+ @SuppressCompatibility @androidx.compose.foundation.layout.ExperimentalGridApi public sealed exhaustive interface GridTrackSpec {
+ }
+
public final class IntrinsicKt {
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier requiredHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
diff --git a/compose/foundation/foundation-layout/bcv/native/current.txt b/compose/foundation/foundation-layout/bcv/native/current.txt
index c4aa43818895d..b804a99535070 100644
--- a/compose/foundation/foundation-layout/bcv/native/current.txt
+++ b/compose/foundation/foundation-layout/bcv/native/current.txt
@@ -6,6 +6,10 @@
// - Show declarations: true
// Library unique name:
+open annotation class androidx.compose.foundation.layout/ExperimentalGridApi : kotlin/Annotation { // androidx.compose.foundation.layout/ExperimentalGridApi|null[0]
+ constructor () // androidx.compose.foundation.layout/ExperimentalGridApi.|(){}[0]
+}
+
open annotation class androidx.compose.foundation.layout/ExperimentalLayoutApi : kotlin/Annotation { // androidx.compose.foundation.layout/ExperimentalLayoutApi|null[0]
constructor () // androidx.compose.foundation.layout/ExperimentalLayoutApi.|(){}[0]
}
@@ -54,6 +58,44 @@ abstract interface androidx.compose.foundation.layout/FlowColumnScope : androidx
abstract interface androidx.compose.foundation.layout/FlowRowScope : androidx.compose.foundation.layout/RowScope // androidx.compose.foundation.layout/FlowRowScope|null[0]
+abstract interface androidx.compose.foundation.layout/GridConfigurationScope : androidx.compose.ui.unit/Density { // androidx.compose.foundation.layout/GridConfigurationScope|null[0]
+ open val fr // androidx.compose.foundation.layout/GridConfigurationScope.fr|@kotlin.Double{}fr[0]
+ open fun (kotlin/Double).(): androidx.compose.foundation.layout/Fr // androidx.compose.foundation.layout/GridConfigurationScope.fr.|@kotlin.Double(){}[0]
+ open val fr // androidx.compose.foundation.layout/GridConfigurationScope.fr|@kotlin.Float{}fr[0]
+ open fun (kotlin/Float).(): androidx.compose.foundation.layout/Fr // androidx.compose.foundation.layout/GridConfigurationScope.fr.|@kotlin.Float(){}[0]
+ open val fr // androidx.compose.foundation.layout/GridConfigurationScope.fr|@kotlin.Int{}fr[0]
+ open fun (kotlin/Int).(): androidx.compose.foundation.layout/Fr // androidx.compose.foundation.layout/GridConfigurationScope.fr.|@kotlin.Int(){}[0]
+
+ abstract var flow // androidx.compose.foundation.layout/GridConfigurationScope.flow|{}flow[0]
+ abstract fun (): androidx.compose.foundation.layout/GridFlow // androidx.compose.foundation.layout/GridConfigurationScope.flow.|(){}[0]
+ abstract fun (androidx.compose.foundation.layout/GridFlow) // androidx.compose.foundation.layout/GridConfigurationScope.flow.|(androidx.compose.foundation.layout.GridFlow){}[0]
+
+ abstract fun column(androidx.compose.foundation.layout/Fr) // androidx.compose.foundation.layout/GridConfigurationScope.column|column(androidx.compose.foundation.layout.Fr){}[0]
+ abstract fun column(androidx.compose.foundation.layout/GridTrackSize) // androidx.compose.foundation.layout/GridConfigurationScope.column|column(androidx.compose.foundation.layout.GridTrackSize){}[0]
+ abstract fun column(androidx.compose.ui.unit/Dp) // androidx.compose.foundation.layout/GridConfigurationScope.column|column(androidx.compose.ui.unit.Dp){}[0]
+ abstract fun column(kotlin/Float) // androidx.compose.foundation.layout/GridConfigurationScope.column|column(kotlin.Float){}[0]
+ abstract fun columnGap(androidx.compose.ui.unit/Dp) // androidx.compose.foundation.layout/GridConfigurationScope.columnGap|columnGap(androidx.compose.ui.unit.Dp){}[0]
+ abstract fun gap(androidx.compose.ui.unit/Dp) // androidx.compose.foundation.layout/GridConfigurationScope.gap|gap(androidx.compose.ui.unit.Dp){}[0]
+ abstract fun gap(androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp) // androidx.compose.foundation.layout/GridConfigurationScope.gap|gap(androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp){}[0]
+ abstract fun row(androidx.compose.foundation.layout/Fr) // androidx.compose.foundation.layout/GridConfigurationScope.row|row(androidx.compose.foundation.layout.Fr){}[0]
+ abstract fun row(androidx.compose.foundation.layout/GridTrackSize) // androidx.compose.foundation.layout/GridConfigurationScope.row|row(androidx.compose.foundation.layout.GridTrackSize){}[0]
+ abstract fun row(androidx.compose.ui.unit/Dp) // androidx.compose.foundation.layout/GridConfigurationScope.row|row(androidx.compose.ui.unit.Dp){}[0]
+ abstract fun row(kotlin/Float) // androidx.compose.foundation.layout/GridConfigurationScope.row|row(kotlin.Float){}[0]
+ abstract fun rowGap(androidx.compose.ui.unit/Dp) // androidx.compose.foundation.layout/GridConfigurationScope.rowGap|rowGap(androidx.compose.ui.unit.Dp){}[0]
+}
+
+abstract interface androidx.compose.foundation.layout/GridScope { // androidx.compose.foundation.layout/GridScope|null[0]
+ abstract fun (androidx.compose.ui/Modifier).gridItem(kotlin.ranges/IntRange, kotlin.ranges/IntRange, androidx.compose.ui/Alignment = ...): androidx.compose.ui/Modifier // androidx.compose.foundation.layout/GridScope.gridItem|gridItem@androidx.compose.ui.Modifier(kotlin.ranges.IntRange;kotlin.ranges.IntRange;androidx.compose.ui.Alignment){}[0]
+ abstract fun (androidx.compose.ui/Modifier).gridItem(kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., androidx.compose.ui/Alignment = ...): androidx.compose.ui/Modifier // androidx.compose.foundation.layout/GridScope.gridItem|gridItem@androidx.compose.ui.Modifier(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;androidx.compose.ui.Alignment){}[0]
+
+ final object Companion { // androidx.compose.foundation.layout/GridScope.Companion|null[0]
+ final const val GridIndexUnspecified // androidx.compose.foundation.layout/GridScope.Companion.GridIndexUnspecified|{}GridIndexUnspecified[0]
+ final fun (): kotlin/Int // androidx.compose.foundation.layout/GridScope.Companion.GridIndexUnspecified.|(){}[0]
+ final const val MaxGridIndex // androidx.compose.foundation.layout/GridScope.Companion.MaxGridIndex|{}MaxGridIndex[0]
+ final fun (): kotlin/Int // androidx.compose.foundation.layout/GridScope.Companion.MaxGridIndex.|(){}[0]
+ }
+}
+
abstract interface androidx.compose.foundation.layout/PaddingValues { // androidx.compose.foundation.layout/PaddingValues|null[0]
abstract fun calculateBottomPadding(): androidx.compose.ui.unit/Dp // androidx.compose.foundation.layout/PaddingValues.calculateBottomPadding|calculateBottomPadding(){}[0]
abstract fun calculateLeftPadding(androidx.compose.ui.unit/LayoutDirection): androidx.compose.ui.unit/Dp // androidx.compose.foundation.layout/PaddingValues.calculateLeftPadding|calculateLeftPadding(androidx.compose.ui.unit.LayoutDirection){}[0]
@@ -95,6 +137,59 @@ abstract interface androidx.compose.foundation.layout/WindowInsets { // androidx
final object Companion // androidx.compose.foundation.layout/WindowInsets.Companion|null[0]
}
+sealed interface androidx.compose.foundation.layout/GridTrackSpec // androidx.compose.foundation.layout/GridTrackSpec|null[0]
+
+final class androidx.compose.foundation.layout/GridMeasurePolicy : androidx.compose.ui.layout/MeasurePolicy { // androidx.compose.foundation.layout/GridMeasurePolicy|null[0]
+ constructor (androidx.compose.runtime/State>) // androidx.compose.foundation.layout/GridMeasurePolicy.|(androidx.compose.runtime.State>){}[0]
+
+ final fun (androidx.compose.ui.layout/MeasureScope).measure(kotlin.collections/List, androidx.compose.ui.unit/Constraints): androidx.compose.ui.layout/MeasureResult // androidx.compose.foundation.layout/GridMeasurePolicy.measure|measure@androidx.compose.ui.layout.MeasureScope(kotlin.collections.List;androidx.compose.ui.unit.Constraints){}[0]
+}
+
+final value class androidx.compose.foundation.layout/Fr { // androidx.compose.foundation.layout/Fr|null[0]
+ constructor (kotlin/Float) // androidx.compose.foundation.layout/Fr.|(kotlin.Float){}[0]
+
+ final val value // androidx.compose.foundation.layout/Fr.value|{}value[0]
+ final fun (): kotlin/Float // androidx.compose.foundation.layout/Fr.value.|(){}[0]
+
+ final fun equals(kotlin/Any?): kotlin/Boolean // androidx.compose.foundation.layout/Fr.equals|equals(kotlin.Any?){}[0]
+ final fun hashCode(): kotlin/Int // androidx.compose.foundation.layout/Fr.hashCode|hashCode(){}[0]
+ final fun toString(): kotlin/String // androidx.compose.foundation.layout/Fr.toString|toString(){}[0]
+}
+
+final value class androidx.compose.foundation.layout/GridFlow { // androidx.compose.foundation.layout/GridFlow|null[0]
+ constructor (kotlin/Int) // androidx.compose.foundation.layout/GridFlow.|(kotlin.Int){}[0]
+
+ final fun equals(kotlin/Any?): kotlin/Boolean // androidx.compose.foundation.layout/GridFlow.equals|equals(kotlin.Any?){}[0]
+ final fun hashCode(): kotlin/Int // androidx.compose.foundation.layout/GridFlow.hashCode|hashCode(){}[0]
+ final fun toString(): kotlin/String // androidx.compose.foundation.layout/GridFlow.toString|toString(){}[0]
+
+ final object Companion { // androidx.compose.foundation.layout/GridFlow.Companion|null[0]
+ final val Column // androidx.compose.foundation.layout/GridFlow.Companion.Column|{}Column[0]
+ final inline fun (): androidx.compose.foundation.layout/GridFlow // androidx.compose.foundation.layout/GridFlow.Companion.Column.|(){}[0]
+ final val Row // androidx.compose.foundation.layout/GridFlow.Companion.Row|{}Row[0]
+ final inline fun (): androidx.compose.foundation.layout/GridFlow // androidx.compose.foundation.layout/GridFlow.Companion.Row.|(){}[0]
+ }
+}
+
+final value class androidx.compose.foundation.layout/GridTrackSize : androidx.compose.foundation.layout/GridTrackSpec { // androidx.compose.foundation.layout/GridTrackSize|null[0]
+ final fun equals(kotlin/Any?): kotlin/Boolean // androidx.compose.foundation.layout/GridTrackSize.equals|equals(kotlin.Any?){}[0]
+ final fun hashCode(): kotlin/Int // androidx.compose.foundation.layout/GridTrackSize.hashCode|hashCode(){}[0]
+ final fun toString(): kotlin/String // androidx.compose.foundation.layout/GridTrackSize.toString|toString(){}[0]
+
+ final object Companion { // androidx.compose.foundation.layout/GridTrackSize.Companion|null[0]
+ final val Auto // androidx.compose.foundation.layout/GridTrackSize.Companion.Auto|{}Auto[0]
+ final fun (): androidx.compose.foundation.layout/GridTrackSize // androidx.compose.foundation.layout/GridTrackSize.Companion.Auto.|(){}[0]
+ final val MaxContent // androidx.compose.foundation.layout/GridTrackSize.Companion.MaxContent|{}MaxContent[0]
+ final fun (): androidx.compose.foundation.layout/GridTrackSize // androidx.compose.foundation.layout/GridTrackSize.Companion.MaxContent.|(){}[0]
+ final val MinContent // androidx.compose.foundation.layout/GridTrackSize.Companion.MinContent|{}MinContent[0]
+ final fun (): androidx.compose.foundation.layout/GridTrackSize // androidx.compose.foundation.layout/GridTrackSize.Companion.MinContent.|(){}[0]
+
+ final fun Fixed(androidx.compose.ui.unit/Dp): androidx.compose.foundation.layout/GridTrackSize // androidx.compose.foundation.layout/GridTrackSize.Companion.Fixed|Fixed(androidx.compose.ui.unit.Dp){}[0]
+ final fun Flex(androidx.compose.foundation.layout/Fr): androidx.compose.foundation.layout/GridTrackSize // androidx.compose.foundation.layout/GridTrackSize.Companion.Flex|Flex(androidx.compose.foundation.layout.Fr){}[0]
+ final fun Percentage(kotlin/Float): androidx.compose.foundation.layout/GridTrackSize // androidx.compose.foundation.layout/GridTrackSize.Companion.Percentage|Percentage(kotlin.Float){}[0]
+ }
+}
+
final value class androidx.compose.foundation.layout/WindowInsetsSides { // androidx.compose.foundation.layout/WindowInsetsSides|null[0]
final fun equals(kotlin/Any?): kotlin/Boolean // androidx.compose.foundation.layout/WindowInsetsSides.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // androidx.compose.foundation.layout/WindowInsetsSides.hashCode|hashCode(){}[0]
@@ -197,6 +292,11 @@ final object androidx.compose.foundation.layout/ColumnScopeInstance : androidx.c
final fun (androidx.compose.ui/Modifier).weight(kotlin/Float, kotlin/Boolean): androidx.compose.ui/Modifier // androidx.compose.foundation.layout/ColumnScopeInstance.weight|weight@androidx.compose.ui.Modifier(kotlin.Float;kotlin.Boolean){}[0]
}
+final object androidx.compose.foundation.layout/GridScopeInstance : androidx.compose.foundation.layout/GridScope { // androidx.compose.foundation.layout/GridScopeInstance|null[0]
+ final fun (androidx.compose.ui/Modifier).gridItem(kotlin.ranges/IntRange, kotlin.ranges/IntRange, androidx.compose.ui/Alignment): androidx.compose.ui/Modifier // androidx.compose.foundation.layout/GridScopeInstance.gridItem|gridItem@androidx.compose.ui.Modifier(kotlin.ranges.IntRange;kotlin.ranges.IntRange;androidx.compose.ui.Alignment){}[0]
+ final fun (androidx.compose.ui/Modifier).gridItem(kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int, androidx.compose.ui/Alignment): androidx.compose.ui/Modifier // androidx.compose.foundation.layout/GridScopeInstance.gridItem|gridItem@androidx.compose.ui.Modifier(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;androidx.compose.ui.Alignment){}[0]
+}
+
final object androidx.compose.foundation.layout/RowScopeInstance : androidx.compose.foundation.layout/RowScope { // androidx.compose.foundation.layout/RowScopeInstance|null[0]
final fun (androidx.compose.ui/Modifier).align(androidx.compose.ui/Alignment.Vertical): androidx.compose.ui/Modifier // androidx.compose.foundation.layout/RowScopeInstance.align|align@androidx.compose.ui.Modifier(androidx.compose.ui.Alignment.Vertical){}[0]
final fun (androidx.compose.ui/Modifier).alignBy(androidx.compose.ui.layout/HorizontalAlignmentLine): androidx.compose.ui/Modifier // androidx.compose.foundation.layout/RowScopeInstance.alignBy|alignBy@androidx.compose.ui.Modifier(androidx.compose.ui.layout.HorizontalAlignmentLine){}[0]
@@ -252,6 +352,8 @@ final val androidx.compose.foundation.layout/tappableElement // androidx.compose
final val androidx.compose.foundation.layout/waterfall // androidx.compose.foundation.layout/waterfall|@androidx.compose.foundation.layout.WindowInsets.Companion{}waterfall[0]
final fun (androidx.compose.foundation.layout/WindowInsets.Companion).(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.layout/WindowInsets // androidx.compose.foundation.layout/waterfall.|@androidx.compose.foundation.layout.WindowInsets.Companion(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
+final fun (androidx.compose.foundation.layout/GridConfigurationScope).androidx.compose.foundation.layout/columns(kotlin/Array...) // androidx.compose.foundation.layout/columns|columns@androidx.compose.foundation.layout.GridConfigurationScope(kotlin.Array...){}[0]
+final fun (androidx.compose.foundation.layout/GridConfigurationScope).androidx.compose.foundation.layout/rows(kotlin/Array...) // androidx.compose.foundation.layout/rows|rows@androidx.compose.foundation.layout.GridConfigurationScope(kotlin.Array...){}[0]
final fun (androidx.compose.foundation.layout/PaddingValues).androidx.compose.foundation.layout/calculateEndPadding(androidx.compose.ui.unit/LayoutDirection): androidx.compose.ui.unit/Dp // androidx.compose.foundation.layout/calculateEndPadding|calculateEndPadding@androidx.compose.foundation.layout.PaddingValues(androidx.compose.ui.unit.LayoutDirection){}[0]
final fun (androidx.compose.foundation.layout/PaddingValues).androidx.compose.foundation.layout/calculateStartPadding(androidx.compose.ui.unit/LayoutDirection): androidx.compose.ui.unit/Dp // androidx.compose.foundation.layout/calculateStartPadding|calculateStartPadding@androidx.compose.foundation.layout.PaddingValues(androidx.compose.ui.unit.LayoutDirection){}[0]
final fun (androidx.compose.foundation.layout/PaddingValues).androidx.compose.foundation.layout/minus(androidx.compose.foundation.layout/PaddingValues): androidx.compose.foundation.layout/PaddingValues // androidx.compose.foundation.layout/minus|minus@androidx.compose.foundation.layout.PaddingValues(androidx.compose.foundation.layout.PaddingValues){}[0]
@@ -360,4 +462,5 @@ final fun androidx.compose.foundation.layout/rowMeasurePolicy(androidx.compose.f
final fun androidx.compose.foundation.layout/rowMeasurementHelper(androidx.compose.foundation.layout/Arrangement.Horizontal, androidx.compose.foundation.layout/Arrangement.Vertical, kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.layout/MeasurePolicy // androidx.compose.foundation.layout/rowMeasurementHelper|rowMeasurementHelper(androidx.compose.foundation.layout.Arrangement.Horizontal;androidx.compose.foundation.layout.Arrangement.Vertical;kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final inline fun androidx.compose.foundation.layout/Box(androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, kotlin/Boolean, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation.layout/Box|Box(androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;kotlin.Boolean;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final inline fun androidx.compose.foundation.layout/Column(androidx.compose.ui/Modifier?, androidx.compose.foundation.layout/Arrangement.Vertical?, androidx.compose.ui/Alignment.Horizontal?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation.layout/Column|Column(androidx.compose.ui.Modifier?;androidx.compose.foundation.layout.Arrangement.Vertical?;androidx.compose.ui.Alignment.Horizontal?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
+final inline fun androidx.compose.foundation.layout/Grid(noinline kotlin/Function1, androidx.compose.ui/Modifier?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation.layout/Grid|Grid(kotlin.Function1;androidx.compose.ui.Modifier?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final inline fun androidx.compose.foundation.layout/Row(androidx.compose.ui/Modifier?, androidx.compose.foundation.layout/Arrangement.Horizontal?, androidx.compose.ui/Alignment.Vertical?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation.layout/Row|Row(androidx.compose.ui.Modifier?;androidx.compose.foundation.layout.Arrangement.Horizontal?;androidx.compose.ui.Alignment.Vertical?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
diff --git a/compose/foundation/foundation-layout/integration-tests/layout-demos/src/main/java/androidx/compose/foundation/layout/demos/GridDemo.kt b/compose/foundation/foundation-layout/integration-tests/layout-demos/src/main/java/androidx/compose/foundation/layout/demos/GridDemo.kt
new file mode 100644
index 0000000000000..114ac0f029c54
--- /dev/null
+++ b/compose/foundation/foundation-layout/integration-tests/layout-demos/src/main/java/androidx/compose/foundation/layout/demos/GridDemo.kt
@@ -0,0 +1,413 @@
+/*
+ * Copyright 2026 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.
+ */
+
+@file:OptIn(ExperimentalGridApi::class)
+
+package androidx.compose.foundation.layout.demos
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalGridApi
+import androidx.compose.foundation.layout.Grid
+import androidx.compose.foundation.layout.GridFlow
+import androidx.compose.foundation.layout.GridScope
+import androidx.compose.foundation.layout.GridTrackSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.columns
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun GridDemo() {
+ Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp)) {
+ MixedSizingDemo()
+ Spacer(Modifier.height(32.dp))
+ FlexibleSizingDemo()
+ Spacer(Modifier.height(32.dp))
+ ContentBasedSizingDemo()
+ Spacer(Modifier.height(32.dp))
+ NegativeIndicesDemo()
+ Spacer(Modifier.height(32.dp))
+ GapsAndContentDemo()
+ Spacer(Modifier.height(32.dp))
+ AlignmentDemo()
+ Spacer(Modifier.height(32.dp))
+ AutoPlacementDemo()
+ Spacer(Modifier.height(32.dp))
+ InfiniteConstraintsDemo()
+ Spacer(Modifier.height(32.dp))
+ MinContentSafetyDemo()
+ }
+}
+
+@Composable
+private fun AutoPlacementDemo() {
+ DemoHeader("Auto Placement & Flow")
+
+ Text(
+ "1. Flow = Row (Fixed Cols, Implicit Rows)",
+ fontSize = 12.sp,
+ fontStyle = FontStyle.Italic,
+ modifier = Modifier.padding(bottom = 4.dp),
+ )
+ Grid(
+ config = {
+ repeat(3) { column(GridTrackSize.Fixed(80.dp)) } // 3 Explicit Columns
+ gap(4.dp)
+ flow = GridFlow.Row // Default
+ },
+ modifier = Modifier.demoContainer(borderColor = Color.Blue),
+ ) {
+ // 1. Simple auto items
+ repeat(3) { index ->
+ GridDemoItem(text = "${index + 1}", color = Color.Cyan, measureSize = false)
+ }
+
+ // 2. Auto item with Span (Takes 2 spots)
+ GridDemoItem(text = "Span 2", columnSpan = 2, color = Color.Magenta, measureSize = false)
+
+ // 3. More auto items (Wrapping to next row)
+ repeat(2) { index ->
+ GridDemoItem(text = "${index + 5}", color = Color.Cyan, measureSize = false)
+ }
+ }
+
+ Spacer(Modifier.height(24.dp))
+
+ Text(
+ "2. Flow = Column (Fixed Rows, Implicit Cols)",
+ fontSize = 12.sp,
+ fontStyle = FontStyle.Italic,
+ modifier = Modifier.padding(bottom = 4.dp),
+ )
+ // Scrollable container to allow implicit columns to grow horizontally
+ Box(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState())) {
+ Grid(
+ config = {
+ flow = GridFlow.Column
+ repeat(3) { row(50.dp) } // 3 Explicit Rows
+ gap(4.dp)
+ },
+ modifier = Modifier.border(1.dp, Color.Magenta.copy(alpha = 0.5f)).padding(8.dp),
+ ) {
+ repeat(10) { index ->
+ GridDemoItem(
+ text = "${index + 1}",
+ color = if (index % 2 == 0) Color.Yellow else Color.Green,
+ measureSize = false,
+ // Explicit size helps visualization in 'Auto' implicit tracks
+ modifier = Modifier.size(60.dp, 40.dp),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun MixedSizingDemo() {
+ DemoHeader("Mixed Sizing: Fixed vs Fraction vs Flex")
+ Grid(
+ config = {
+ columns(
+ GridTrackSize.Fixed(100.dp),
+ GridTrackSize.Percentage(0.3f),
+ GridTrackSize.Flex(1.fr),
+ )
+ row(100.dp)
+ },
+ modifier = Modifier.demoContainer(),
+ ) {
+ GridDemoItem(text = "Fixed\n100dp", color = Color.Red, row = 1, column = 1)
+ GridDemoItem(text = "Fraction\n30% of Total", color = Color.Blue, row = 1, column = 2)
+ GridDemoItem(text = "Flex\nRest of Space", color = Color.Green, row = 1, column = 3)
+ }
+}
+
+@Composable
+private fun FlexibleSizingDemo() {
+ DemoHeader("Flexible (Fr) Sizing")
+ Grid(
+ config = {
+ column(80.dp)
+ column(1.fr)
+ column(2.fr)
+ row(1.fr)
+ row(60.dp)
+ },
+ modifier = Modifier.height(200.dp).demoContainer(borderColor = Color.Magenta),
+ ) {
+ val rowLabels = listOf("Flex 1.fr", "60dp")
+ val colLabels = listOf("Fixed 80dp", "Flex 1.fr", "Flex 2.fr")
+
+ rowLabels.forEachIndexed { rowIndex, rowLabel ->
+ colLabels.forEachIndexed { colIndex, colLabel ->
+ GridDemoItem(
+ text = "H: $rowLabel\nW: $colLabel",
+ row = rowIndex + 1,
+ column = colIndex + 1,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun NegativeIndicesDemo() {
+ DemoHeader("Negative Indices")
+ Grid(
+ config = {
+ repeat(3) { column(60.dp) }
+ repeat(3) { row(60.dp) }
+ gap(4.dp)
+ },
+ modifier = Modifier.border(1.dp, Color.Gray),
+ ) {
+ GridDemoItem(text = "TL", row = 1, column = 1, color = Color.Red)
+ GridDemoItem("TR", row = 1, column = -1, color = Color.Blue)
+ GridDemoItem("BL", row = -1, column = 1, color = Color.Green)
+ GridDemoItem("BR", row = -1, column = -1, color = Color.Yellow)
+ GridDemoItem("Center", row = 2, column = 2, color = Color.Gray)
+ }
+}
+
+@Composable
+private fun ContentBasedSizingDemo() {
+ DemoHeader("Intrinsic Sizing (Min vs Max Content)")
+ Grid(
+ config = {
+ column(GridTrackSize.MinContent)
+ column(GridTrackSize.MaxContent)
+ column(GridTrackSize.Flex(1.fr))
+ column(GridTrackSize.Auto)
+ row(GridTrackSize.Auto)
+ gap(8.dp)
+ },
+ modifier = Modifier.demoContainer(borderColor = Color.Black),
+ ) {
+ GridDemoItem(text = "Min Content\nWraps", row = 1, column = 1, color = Color.Red)
+ GridDemoItem(text = "Max Content Expands", row = 1, column = 2, color = Color.Blue)
+ GridDemoItem(text = "Flex Fills\nRemainder", row = 1, column = 3, color = Color.Green)
+ GridDemoItem(text = "Auto\nContent", row = 1, column = 4, color = Color.Yellow)
+ }
+}
+
+@Composable
+private fun GapsAndContentDemo() {
+ DemoHeader("Gaps & Content Sizing")
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(100.dp))
+ column(GridTrackSize.Flex(1.fr))
+ column(GridTrackSize.Fixed(200.dp))
+ row(GridTrackSize.Fixed(60.dp))
+ row(GridTrackSize.Fixed(100.dp))
+ gap(row = 12.dp, column = 6.dp)
+ },
+ modifier = Modifier.demoContainer(borderColor = Color.Cyan),
+ ) {
+ repeat(6) {
+ val row = (it / 3) + 1
+ val col = (it % 3) + 1
+ GridDemoItem(text = "Item ${it + 1}", measureSize = false, row = row, column = col)
+ }
+ }
+}
+
+@Composable
+private fun AlignmentDemo() {
+ DemoHeader("Cell Content Alignment")
+ Grid(
+ config = {
+ repeat(3) { column(GridTrackSize.Fixed(100.dp)) }
+ repeat(3) { row(GridTrackSize.Fixed(100.dp)) }
+ gap(4.dp)
+ },
+ modifier = Modifier.demoContainer(borderColor = Color.Red),
+ ) {
+ val alignments =
+ listOf(
+ Alignment.TopStart to "TopStart",
+ Alignment.TopCenter to "TopCenter",
+ Alignment.TopEnd to "TopEnd",
+ Alignment.CenterStart to "CenterStart",
+ Alignment.Center to "Center",
+ Alignment.CenterEnd to "CenterEnd",
+ Alignment.BottomStart to "BottomStart",
+ Alignment.BottomCenter to "BottomCenter",
+ Alignment.BottomEnd to "BottomEnd",
+ )
+
+ alignments.forEachIndexed { index, (alignment, name) ->
+ val row = (index / 3) + 1
+ val col = (index % 3) + 1
+
+ Box(
+ Modifier.gridItem(row, col)
+ .fillMaxSize()
+ .background(Color.LightGray.copy(alpha = 0.2f))
+ .border(1.dp, Color.DarkGray.copy(alpha = 0.1f))
+ )
+
+ Box(
+ Modifier.gridItem(row, col, alignment = alignment)
+ .size(60.dp, 40.dp)
+ .background(Color.Yellow.copy(alpha = 0.7f))
+ .border(1.dp, Color.Red),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(name, fontSize = 10.sp)
+ }
+ }
+ }
+}
+
+@Composable
+private fun InfiniteConstraintsDemo() {
+ DemoHeader("Infinite Constraints")
+ Text(
+ "Percentage tracks fall back to Auto sizing when placed in an infinite container.",
+ fontSize = 12.sp,
+ fontStyle = FontStyle.Italic,
+ modifier = Modifier.padding(bottom = 4.dp),
+ )
+
+ Row(
+ Modifier.fillMaxWidth()
+ .border(1.dp, Color.Gray)
+ .padding(8.dp)
+ .horizontalScroll(rememberScrollState())
+ ) {
+ Grid(
+ config = {
+ column(GridTrackSize.Percentage(0.5f))
+ row(GridTrackSize.Auto)
+ gap(4.dp)
+ },
+ modifier = Modifier.border(1.dp, Color.Blue),
+ ) {
+ GridDemoItem(
+ text = "I am 150dp wide\n(Percentage -> Auto)",
+ modifier = Modifier.width(150.dp),
+ color = Color.Cyan,
+ )
+ }
+ }
+}
+
+@Composable
+private fun MinContentSafetyDemo() {
+ DemoHeader("Min-Content Flex")
+ Text(
+ "Flex tracks implement minmax(min-content, 1fr).",
+ fontSize = 12.sp,
+ fontStyle = FontStyle.Italic,
+ modifier = Modifier.padding(bottom = 4.dp),
+ )
+ Box(Modifier.width(50.dp).border(2.dp, Color.Red).padding(2.dp)) {
+ Grid(
+ config = {
+ column(GridTrackSize.Flex(1.fr))
+ row(GridTrackSize.Auto)
+ },
+ modifier = Modifier.border(1.dp, Color.Green),
+ ) {
+ GridDemoItem(
+ text = "Min 120dp",
+ modifier = Modifier.width(120.dp),
+ color = Color.Magenta,
+ )
+ }
+ }
+}
+
+@Composable
+private fun DemoHeader(text: String) =
+ Text(
+ text,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier.padding(bottom = 8.dp),
+ )
+
+@Composable
+private fun GridScope.GridDemoItem(
+ text: String,
+ modifier: Modifier = Modifier,
+ row: Int? = null,
+ column: Int? = null,
+ rowSpan: Int = 1,
+ columnSpan: Int = 1,
+ color: Color = Color.Green,
+ measureSize: Boolean = true,
+) {
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ val density = LocalDensity.current
+ var finalModifier = modifier.fillMaxSize()
+
+ if (row != null && column != null) {
+ finalModifier = finalModifier.gridItem(row, column, rowSpan, columnSpan)
+ } else if (rowSpan > 1 || columnSpan > 1) {
+ finalModifier = finalModifier.gridItem(rowSpan = rowSpan, columnSpan = columnSpan)
+ }
+
+ Box(
+ finalModifier
+ .onSizeChanged { size = it }
+ .background(color.copy(alpha = 0.1f))
+ .border(1.dp, color.copy(alpha = 0.5f))
+ .padding(2.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(text = text, fontSize = 10.sp, textAlign = TextAlign.Center)
+ if (measureSize) {
+ val w = with(density) { size.width.toDp().value.toInt() }
+ val h = with(density) { size.height.toDp().value.toInt() }
+ Text(text = "$w x $h", fontSize = 9.sp, fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+}
+
+private fun Modifier.demoContainer(borderColor: Color = Color.Black) =
+ this.fillMaxWidth().border(1.dp, borderColor.copy(alpha = 0.5f)).padding(8.dp)
diff --git a/compose/foundation/foundation-layout/integration-tests/layout-demos/src/main/java/androidx/compose/foundation/layout/demos/LayoutDemos.kt b/compose/foundation/foundation-layout/integration-tests/layout-demos/src/main/java/androidx/compose/foundation/layout/demos/LayoutDemos.kt
index 18ed55e40f433..c348dfe2d5e9c 100644
--- a/compose/foundation/foundation-layout/integration-tests/layout-demos/src/main/java/androidx/compose/foundation/layout/demos/LayoutDemos.kt
+++ b/compose/foundation/foundation-layout/integration-tests/layout-demos/src/main/java/androidx/compose/foundation/layout/demos/LayoutDemos.kt
@@ -29,5 +29,6 @@ val LayoutDemos =
ComposableDemo("Contextual Flow Row") { ContextualFlowRowDemo() },
ComposableDemo("Contextual FlowColumn") { ContextualFlowColumnDemo() },
ComposableDemo("Rtl support") { RtlDemo() },
+ ComposableDemo("Grid") { GridDemo() },
),
)
diff --git a/compose/foundation/foundation-layout/src/androidDeviceTest/kotlin/androidx/compose/foundation/layout/GridTest.kt b/compose/foundation/foundation-layout/src/androidDeviceTest/kotlin/androidx/compose/foundation/layout/GridTest.kt
new file mode 100644
index 0000000000000..1b6ef306f4c1d
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidDeviceTest/kotlin/androidx/compose/foundation/layout/GridTest.kt
@@ -0,0 +1,2304 @@
+/*
+ * Copyright 2026 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.
+ */
+
+@file:OptIn(ExperimentalGridApi::class)
+
+package androidx.compose.foundation.layout
+
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.node.Ref
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.unit.Constraints
+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 java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class GridTest : LayoutTest() {
+
+ @Before
+ fun before() {
+ isDebugInspectorInfoEnabled = true
+ }
+
+ @After
+ fun after() {
+ isDebugInspectorInfoEnabled = false
+ }
+
+ @Test
+ fun testGrid_itemModifierChange_triggersRelayout() =
+ with(density) {
+ val size = 50
+ val sizeDp = size.toDp()
+ var targetRow by mutableStateOf(1)
+
+ // Latch for initial layout (Row 1)
+ val initialLatch = CountDownLatch(1)
+ // Latch for update layout (Row 2)
+ val updateLatch = CountDownLatch(1)
+
+ val childPosition = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(sizeDp))
+ row(GridTrackSize.Fixed(sizeDp))
+ row(GridTrackSize.Fixed(sizeDp))
+ }
+ ) {
+ Box(
+ Modifier.gridItem(row = targetRow, column = 1)
+ .size(sizeDp)
+ .onGloballyPositioned { coordinates ->
+ childPosition.value = coordinates.localToRoot(Offset.Zero)
+ if (initialLatch.count > 0) {
+ initialLatch.countDown()
+ } else {
+ updateLatch.countDown()
+ }
+ }
+ )
+ }
+ }
+
+ // 1. Verify Initial Position (Row 1 -> Index 0 -> 0px)
+ assertTrue(
+ "Timed out waiting for initial layout",
+ initialLatch.await(1, TimeUnit.SECONDS),
+ )
+ assertEquals(Offset(0f, 0f), childPosition.value)
+
+ // 2. Update State
+ targetRow = 2
+
+ // 3. Verify Updated Position (Row 2 -> Index 1 -> 50px)
+ assertTrue(
+ "Timed out waiting for layout update",
+ updateLatch.await(1, TimeUnit.SECONDS),
+ )
+ assertEquals(Offset(0f, size.toFloat()), childPosition.value)
+ }
+
+ @Test
+ fun testGrid_configStateChange_updatesLayout() =
+ with(density) {
+ var trackSize by mutableStateOf(50.dp)
+
+ // Use separate latches for the initial pass and the update pass.
+ val initialLayoutLatch = CountDownLatch(1)
+ val updateLayoutLatch = CountDownLatch(1)
+
+ val childSize = Ref()
+
+ show {
+ Grid(
+ config = {
+ // Reading 'trackSize' here registers a dependency.
+ // When 'trackSize' changes, this lambda re-executes, triggering
+ // the MeasurePolicy to re-measure.
+ column(GridTrackSize.Fixed(trackSize))
+ row(GridTrackSize.Fixed(trackSize))
+ }
+ ) {
+ Box(
+ Modifier.gridItem(1, 1).fillMaxSize().onGloballyPositioned { coordinates ->
+ childSize.value = coordinates.size
+
+ // If the first latch is still open, this is the initial layout.
+ // Otherwise, we are in the update phase.
+ if (initialLayoutLatch.count > 0) {
+ initialLayoutLatch.countDown()
+ } else {
+ updateLayoutLatch.countDown()
+ }
+ }
+ )
+ }
+ }
+
+ // Verify Initial State (50.dp)
+ // Wait for the FIRST layout pass to complete
+ assertTrue(
+ "Timed out waiting for initial layout",
+ initialLayoutLatch.await(1, TimeUnit.SECONDS),
+ )
+ assertEquals(
+ "Initial size incorrect",
+ IntSize(50.dp.roundToPx(), 50.dp.roundToPx()),
+ childSize.value,
+ )
+
+ // Change State
+ trackSize = 100.dp
+
+ // Verify Update (100.dp)
+ // Wait for the SECOND layout pass (recomposition) to complete
+ assertTrue(
+ "Timed out waiting for layout update",
+ updateLayoutLatch.await(1, TimeUnit.SECONDS),
+ )
+ assertEquals(
+ "Grid did not update track size after config state changed",
+ IntSize(100.dp.roundToPx(), 100.dp.roundToPx()),
+ childSize.value,
+ )
+ }
+
+ @Test
+ fun testGrid_fixedTracks_sizeCorrectly() =
+ with(density) {
+ val size1 = 50
+ val size2 = 100
+ val size1Dp = size1.toDp()
+ val size2Dp = size2.toDp()
+
+ val positionedLatch = CountDownLatch(2)
+ val childSize = Array(2) { Ref() }
+ val childPosition = Array(2) { Ref() }
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(size1Dp))
+ column(GridTrackSize.Fixed(size2Dp))
+ row(GridTrackSize.Fixed(size1Dp))
+ }
+ ) {
+ // R1, C1
+ Box(
+ Modifier.gridItem(1, 1)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize[0], childPosition[0], positionedLatch)
+ )
+ // R1, C2
+ Box(
+ Modifier.gridItem(1, 2)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize[1], childPosition[1], positionedLatch)
+ )
+ }
+ }
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+ // Check Item 1 (50x50) at (0,0)
+ assertEquals(IntSize(size1, size1), childSize[0].value)
+ assertEquals(Offset(0f, 0f), childPosition[0].value)
+
+ // Check Item 2 (100x50) at (50,0)
+ assertEquals(IntSize(size2, size1), childSize[1].value)
+ assertEquals(Offset(size1.toFloat(), 0f), childPosition[1].value)
+ }
+
+ @Test
+ fun testGrid_fractionTracks() =
+ with(density) {
+ val totalSize = 200
+ val totalSizeDp = totalSize.toDp()
+
+ // Col 1: 25% = 50px
+ // Col 2: 75% = 150px
+ val expectedCol1 = (totalSize * 0.25f).roundToInt()
+ val expectedCol2 = (totalSize * 0.75f).roundToInt()
+
+ val positionedLatch = CountDownLatch(2)
+ val childSize = Array(2) { Ref() }
+ val childPosition = Array(2) { Ref() }
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Percentage(0.25f))
+ column(GridTrackSize.Percentage(0.75f))
+ row(GridTrackSize.Fixed(50.dp))
+ },
+ modifier = Modifier.size(totalSizeDp, 50.dp),
+ ) {
+ Box(
+ Modifier.gridItem(1, 1)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize[0], childPosition[0], positionedLatch)
+ )
+ Box(
+ Modifier.gridItem(1, 2)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize[1], childPosition[1], positionedLatch)
+ )
+ }
+ }
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(IntSize(expectedCol1, 50.dp.roundToPx()), childSize[0].value)
+ assertEquals(Offset(0f, 0f), childPosition[0].value)
+
+ assertEquals(IntSize(expectedCol2, 50.dp.roundToPx()), childSize[1].value)
+ assertEquals(Offset(expectedCol1.toFloat(), 0f), childPosition[1].value)
+ }
+
+ @Test
+ fun testGrid_percentageRows_resolvesAgainstHeight() =
+ with(density) {
+ // Scenario:
+ // Fixed Height Container (200px).
+ // Row 1: 25% (50px)
+ // Row 2: 75% (150px)
+
+ val totalHeight = 200
+ val expectedRow1 = (totalHeight * 0.25f).roundToInt()
+ val expectedRow2 = (totalHeight * 0.75f).roundToInt()
+
+ val latch = CountDownLatch(2)
+ val sizes = Array(2) { Ref() }
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(50.dp))
+ row(GridTrackSize.Percentage(0.25f))
+ row(GridTrackSize.Percentage(0.75f))
+ },
+ modifier = Modifier.height(totalHeight.toDp()),
+ ) {
+ // Item in Row 1
+ Box(
+ Modifier.gridItem(1, 1).fillMaxSize().saveLayoutInfo(sizes[0], Ref(), latch)
+ )
+ // Item in Row 2
+ Box(
+ Modifier.gridItem(2, 1).fillMaxSize().saveLayoutInfo(sizes[1], Ref(), latch)
+ )
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(expectedRow1, sizes[0].value?.height)
+ assertEquals(expectedRow2, sizes[1].value?.height)
+ }
+
+ @Test
+ fun testGrid_flexTracks() =
+ with(density) {
+ val totalWidth = 300
+ val fixedWidth = 100
+
+ // Remaining space = 200
+ // Flex 1: 1fr = 200 * (1/4) = 50
+ // Flex 2: 3fr = 200 * (3/4) = 150
+
+ val positionedLatch = CountDownLatch(3)
+ val childSize = Array(3) { Ref() }
+ val childPosition = Array(3) { Ref() }
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(fixedWidth.toDp()))
+ column(GridTrackSize.Flex(1.fr))
+ column(GridTrackSize.Flex(3.fr))
+ row(GridTrackSize.Fixed(50.dp))
+ },
+ modifier = Modifier.width(totalWidth.toDp()),
+ ) {
+ Box(
+ Modifier.gridItem(1, 1)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize[0], childPosition[0], positionedLatch)
+ )
+ Box(
+ Modifier.gridItem(1, 2)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize[1], childPosition[1], positionedLatch)
+ )
+ Box(
+ Modifier.gridItem(1, 3)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize[2], childPosition[2], positionedLatch)
+ )
+ }
+ }
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+ // Fixed Col
+ assertEquals(IntSize(fixedWidth, 50.dp.roundToPx()), childSize[0].value)
+ assertEquals(Offset(0f, 0f), childPosition[0].value)
+
+ // Flex 1 (50px)
+ assertEquals(IntSize(50, 50.dp.roundToPx()), childSize[1].value)
+ assertEquals(Offset(fixedWidth.toFloat(), 0f), childPosition[1].value)
+
+ // Flex 3 (150px)
+ assertEquals(IntSize(150, 50.dp.roundToPx()), childSize[2].value)
+ assertEquals(Offset((fixedWidth + 50).toFloat(), 0f), childPosition[2].value)
+ }
+
+ @Test
+ fun testGrid_flexRespectsMinContent() =
+ with(density) {
+ val minContentSize = 100
+ val totalSize = 150 // Only 50px left for flex
+ val latch = CountDownLatch(2)
+ val sizes = Array(2) { Ref() }
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.MinContent)
+ column(GridTrackSize.Flex(1.fr))
+ row(GridTrackSize.Fixed(50.dp))
+ },
+ modifier = Modifier.width(totalSize.toDp()),
+ ) {
+ // Item 1 (MinContent): Needs 100px
+ IntrinsicItem(
+ minWidth = minContentSize,
+ minIntrinsicWidth = minContentSize,
+ maxIntrinsicWidth = minContentSize,
+ modifier = Modifier.gridItem(1, 1).saveLayoutInfo(sizes[0], Ref(), latch),
+ )
+ // Item 2 (Flex): Gets remaining 50px
+ Box(
+ Modifier.gridItem(1, 2).fillMaxSize().saveLayoutInfo(sizes[1], Ref(), latch)
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(minContentSize, sizes[0].value?.width)
+ assertEquals(totalSize - minContentSize, sizes[1].value?.width)
+ }
+
+ @Test
+ fun testGrid_flexTrack_minContent_lowerBound() =
+ with(density) {
+ val containerWidth = 50
+ val itemMinSize = 100
+ val latch = CountDownLatch(1)
+ val childSize = Ref()
+
+ show {
+ // Container is 50px wide, but item needs 100px.
+ // Flex track should expand to 100px (min-content), ignoring the 50px constraint.
+ Box(Modifier.width(containerWidth.toDp())) {
+ Grid(
+ config = {
+ column(GridTrackSize.Flex(1.fr))
+ row(GridTrackSize.Fixed(50.dp))
+ }
+ ) {
+ IntrinsicItem(
+ minWidth = itemMinSize,
+ minIntrinsicWidth = itemMinSize,
+ maxIntrinsicWidth = itemMinSize,
+ modifier =
+ Modifier.gridItem(1, 1)
+ .fillMaxHeight()
+ .saveLayoutInfo(childSize, Ref(), latch),
+ )
+ }
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(
+ "Flex track should not shrink below min-content size",
+ itemMinSize,
+ childSize.value?.width,
+ )
+ }
+
+ @Test
+ fun testGrid_flexTracks_distributeSpace_afterMinContent() =
+ with(density) {
+ // Scenario:
+ // Container = 200px
+ // Track 1 (1fr): Has large content (150px)
+ // Track 2 (1fr): Has small content (10px)
+ //
+ // Calculation:
+ // 1. Base Sizes (Pass 1):
+ // Track 1 = 150px
+ // Track 2 = 10px
+ // Used = 160px
+ //
+ // 2. Remaining Space (Pass 2):
+ // 200px - 160px = 40px
+ //
+ // 3. Distribution (Equal weight 1fr):
+ // Track 1 adds 20px -> Final 170px
+ // Track 2 adds 20px -> Final 30px
+
+ val containerWidth = 200
+ val item1MinSize = 150
+ val item2MinSize = 10
+ val latch = CountDownLatch(2)
+ val size1 = Ref()
+ val size2 = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Flex(1.fr))
+ column(GridTrackSize.Flex(1.fr))
+ row(GridTrackSize.Fixed(50.dp))
+ },
+ modifier = Modifier.width(containerWidth.toDp()),
+ ) {
+ // Item 1: Large Min Content
+ IntrinsicItem(
+ minWidth = item1MinSize,
+ minIntrinsicWidth = item1MinSize,
+ maxIntrinsicWidth = item1MinSize,
+ modifier =
+ Modifier.gridItem(1, 1)
+ .fillMaxWidth() // <--- ADD THIS
+ .saveLayoutInfo(size1, Ref(), latch),
+ )
+
+ // Item 2: Small Min Content
+ IntrinsicItem(
+ minWidth = item2MinSize,
+ minIntrinsicWidth = item2MinSize,
+ maxIntrinsicWidth = item2MinSize,
+ modifier =
+ Modifier.gridItem(1, 2)
+ .fillMaxWidth() // <--- ADD THIS
+ .saveLayoutInfo(size2, Ref(), latch),
+ )
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals("Track 1 should be Base(150) + Share(20)", 170, size1.value?.width)
+ assertEquals("Track 2 should be Base(10) + Share(20)", 30, size2.value?.width)
+ }
+
+ @Test
+ fun testGrid_mixedTrackTypes_resolutionOrder() =
+ with(density) {
+ val fixedSize = 50
+ val contentSize = 30
+ val totalSize = 200 // 50 (Fixed) + 30 (Auto) + Flex = 200 -> Flex = 120
+
+ val latch = CountDownLatch(3)
+ val sizes = Array(3) { Ref() }
+ val positions = Array(3) { Ref() }
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(fixedSize.toDp())) // Col 1: Fixed
+ column(GridTrackSize.Auto) // Col 2: Auto (Content based)
+ column(GridTrackSize.Flex(1.fr)) // Col 3: Flex (Remaining)
+ row(GridTrackSize.Fixed(50.dp))
+ },
+ modifier = Modifier.width(totalSize.toDp()),
+ ) {
+ // Col 1: Fixed item
+ Box(
+ Modifier.gridItem(1, 1)
+ .fillMaxSize()
+ .saveLayoutInfo(sizes[0], positions[0], latch)
+ )
+
+ // Col 2: Auto item (determines track width)
+ Box(
+ Modifier.gridItem(1, 2)
+ .width(contentSize.toDp())
+ .fillMaxHeight()
+ .saveLayoutInfo(sizes[1], positions[1], latch)
+ )
+
+ // Col 3: Flex item
+ Box(
+ Modifier.gridItem(1, 3)
+ .fillMaxSize()
+ .saveLayoutInfo(sizes[2], positions[2], latch)
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Col 1: 50
+ assertEquals(fixedSize, sizes[0].value?.width)
+ // Col 2: 30 (sized by content)
+ assertEquals(contentSize, sizes[1].value?.width)
+ // Col 3: 200 - 50 - 30 = 120
+ assertEquals(120, sizes[2].value?.width)
+ }
+
+ @Test
+ fun testGrid_gaps() =
+ with(density) {
+ val size = 50
+ val gap = 10
+ val gapDp = gap.toDp()
+ val sizeDp = size.toDp()
+
+ val positionedLatch = CountDownLatch(2)
+ val childPosition = Array(2) { Ref() }
+ // Use explicit dummy refs instead of passing null to avoid overload ambiguity
+ val dummySize = Array(2) { Ref() }
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(sizeDp))
+ column(GridTrackSize.Fixed(sizeDp))
+ row(GridTrackSize.Fixed(sizeDp))
+ gap(gapDp)
+ }
+ ) {
+ // Item 1: (0, 0)
+ Box(
+ Modifier.gridItem(1, 1)
+ .fillMaxSize()
+ .saveLayoutInfo(dummySize[0], childPosition[0], positionedLatch)
+ )
+ // Item 2: (50 + 10, 0) = (60, 0)
+ Box(
+ Modifier.gridItem(1, 2)
+ .fillMaxSize()
+ .saveLayoutInfo(dummySize[1], childPosition[1], positionedLatch)
+ )
+ }
+ }
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(Offset(0f, 0f), childPosition[0].value)
+ assertEquals(Offset((size + gap).toFloat(), 0f), childPosition[1].value)
+ }
+
+ @Test
+ fun testGrid_flexWithGaps() =
+ with(density) {
+ val size = 50
+ val gap = 10
+ // Available space for tracks: 50 - 10 = 40.
+ // 1.fr + 1.fr = 2 parts. 40 / 2 = 20 per track.
+ val expectedColWidth = (size - gap) / 2
+
+ val positionedLatch = CountDownLatch(2)
+ val childSize = Array(2) { Ref() }
+ val childPosition = Array(2) { Ref() }
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Flex(1.fr))
+ column(GridTrackSize.Flex(1.fr))
+ row(GridTrackSize.Fixed(size.toDp()))
+ gap(gap.toDp())
+ },
+ modifier = Modifier.requiredSize(size.toDp()),
+ ) {
+ Box(
+ Modifier.gridItem(1, 1)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize[0], childPosition[0], positionedLatch)
+ )
+ Box(
+ Modifier.gridItem(1, 2)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize[1], childPosition[1], positionedLatch)
+ )
+ }
+ }
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+ assertEquals(IntSize(expectedColWidth, size), childSize[0].value)
+ assertEquals(IntSize(expectedColWidth, size), childSize[1].value)
+ assertEquals(Offset(0f, 0f), childPosition[0].value)
+ assertEquals(Offset((expectedColWidth + gap).toFloat(), 0f), childPosition[1].value)
+ }
+
+ @Test
+ fun testGrid_spanningWithGaps() {
+ val size = 50
+ val gap = 10
+ // Spanning 2 columns: size + gap + size = 50 + 10 + 50 = 110
+ val expectedWidth = size * 2 + gap
+
+ val positionedLatch = CountDownLatch(1)
+ val childSize = Ref()
+ val dummyPos = Ref()
+
+ show {
+ Grid(
+ config = {
+ repeat(3) { column(GridTrackSize.Fixed(size.toDp())) }
+ row(GridTrackSize.Fixed(size.toDp()))
+ gap(gap.toDp())
+ }
+ ) {
+ Box(
+ Modifier.gridItem(row = 1, column = 1, columnSpan = 2)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize, dummyPos, positionedLatch)
+ )
+ }
+ }
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+ assertEquals(IntSize(expectedWidth, size), childSize.value)
+ }
+
+ @Test
+ fun testGrid_implicitTracks_respectGaps() =
+ with(density) {
+ val size = 50
+ val gap = 10
+ val latch = CountDownLatch(2)
+ val pos = Array(2) { Ref() }
+
+ // Create a dummy ref instead of passing null
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(size.toDp())) // 1 Explicit Col
+ row(GridTrackSize.Fixed(size.toDp()))
+ gap(gap.toDp())
+ }
+ ) {
+ // Item 1: Col 1
+ Box(
+ Modifier.gridItem(1, 1)
+ .size(size.toDp())
+ .saveLayoutInfo(dummy, pos[0], latch)
+ )
+ // Item 2: Col 2 (Implicit). Should be at 50 + 10 = 60.
+ Box(
+ Modifier.gridItem(1, 2)
+ .size(size.toDp())
+ .saveLayoutInfo(dummy, pos[1], latch)
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ assertEquals(Offset((size + gap).toFloat(), 0f), pos[1].value)
+ }
+
+ @Test
+ fun testGrid_gapPrecedence_specificOverridesGeneric() =
+ with(density) {
+ // Scenario:
+ // gap(10) sets both.
+ // rowGap(20) overrides row gap.
+ // columnGap(5) overrides column gap.
+ // Result: RowGap = 20, ColumnGap = 5.
+
+ val size = 50
+ val baseGap = 10
+ val rowGapOverride = 20
+ val colGapOverride = 5
+
+ val sizeDp = size.toDp()
+ val latch = CountDownLatch(3)
+ val pos = Array(3) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ repeat(2) { column(GridTrackSize.Fixed(sizeDp)) }
+ repeat(2) { row(GridTrackSize.Fixed(sizeDp)) }
+
+ gap(baseGap.toDp()) // Sets both to 10
+ rowGap(rowGapOverride.toDp()) // Overrides row to 20
+ columnGap(colGapOverride.toDp()) // Overrides col to 5
+ }
+ ) {
+ // (0,0)
+ Box(Modifier.gridItem(1, 1).size(sizeDp).saveLayoutInfo(dummy, pos[0], latch))
+ // (0,1) -> X should be Size + ColGap(5)
+ Box(Modifier.gridItem(1, 2).size(sizeDp).saveLayoutInfo(dummy, pos[1], latch))
+ // (1,0) -> Y should be Size + RowGap(20)
+ Box(Modifier.gridItem(2, 1).size(sizeDp).saveLayoutInfo(dummy, pos[2], latch))
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ assertEquals(Offset((size + colGapOverride).toFloat(), 0f), pos[1].value)
+ assertEquals(Offset(0f, (size + rowGapOverride).toFloat()), pos[2].value)
+ }
+
+ @Test
+ fun testGrid_alignment() =
+ with(density) {
+ val cellSize = 100
+ val itemSize = 40
+ // Center: (100 - 40) / 2 = 30
+ val expectedOffset = 30f
+ val cellSizeDp = cellSize.toDp()
+ val itemSizeDp = itemSize.toDp()
+
+ val positionedLatch = CountDownLatch(1)
+ val childPosition = Ref()
+ val dummySize = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(cellSizeDp))
+ row(GridTrackSize.Fixed(cellSizeDp))
+ }
+ ) {
+ // Item smaller than cell, aligned center
+ Box(
+ Modifier.gridItem(1, 1, alignment = Alignment.Center)
+ .size(itemSizeDp)
+ .saveLayoutInfo(dummySize, childPosition, positionedLatch)
+ )
+ }
+ }
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(Offset(expectedOffset, expectedOffset), childPosition.value)
+ }
+
+ @Test
+ fun testGrid_alignment_spanning() =
+ with(density) {
+ val colSize = 50
+ val rowSize = 60
+ val gap = 10
+ val itemSize = 40
+
+ // Calculate total area dimensions including the gap
+ val spannedWidth = (colSize * 2) + gap
+ val spannedHeight = (rowSize * 2) + gap
+
+ // Alignment.BottomEnd calculation:
+ // x = ContainerWidth - ItemWidth
+ // y = ContainerHeight - ItemHeight
+ val expectedX = (spannedWidth - itemSize).toFloat()
+ val expectedY = (spannedHeight - itemSize).toFloat()
+
+ val positionedLatch = CountDownLatch(1)
+ val childPosition = Ref()
+ val dummySize = Ref()
+
+ show {
+ Grid(
+ config = {
+ repeat(2) { column(GridTrackSize.Fixed(colSize.toDp())) }
+ repeat(2) { row(GridTrackSize.Fixed(rowSize.toDp())) }
+ gap(gap.toDp())
+ }
+ ) {
+ Box(
+ Modifier.gridItem(
+ 1,
+ 1,
+ rowSpan = 2,
+ columnSpan = 2,
+ alignment = Alignment.BottomEnd,
+ )
+ .size(itemSize.toDp())
+ .saveLayoutInfo(dummySize, childPosition, positionedLatch)
+ )
+ }
+ }
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+ assertEquals(Offset(expectedX, expectedY), childPosition.value)
+ }
+
+ @Test
+ fun testGrid_alignment_rtl() =
+ with(density) {
+ val cellSize = 100
+ val itemSize = 40
+ val cellSizeDp = cellSize.toDp()
+ val itemSizeDp = itemSize.toDp()
+
+ val positionedLatch = CountDownLatch(2)
+ val startAlignedPos = Ref()
+ val absLeftAlignedPos = Ref()
+
+ show {
+ // Force RTL layout direction
+ androidx.compose.runtime.CompositionLocalProvider(
+ androidx.compose.ui.platform.LocalLayoutDirection provides
+ androidx.compose.ui.unit.LayoutDirection.Rtl
+ ) {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(cellSizeDp))
+ row(GridTrackSize.Fixed(cellSizeDp))
+ }
+ ) {
+ // 1. Alignment.TopStart (Relative 2D Alignment)
+ // In RTL, "Start" is visually on the RIGHT.
+ // Cell Width 100. Item 40.
+ // Expect visual position: 100 - 40 = 60px from visual Left.
+ Box(
+ Modifier.gridItem(1, 1, alignment = Alignment.TopStart)
+ .size(itemSizeDp)
+ .saveLayoutInfo(Ref(), startAlignedPos, positionedLatch)
+ )
+
+ // 2. AbsoluteAlignment.TopLeft (Absolute 2D Alignment)
+ // "Left" is always visually Left, regardless of layout direction.
+ // Expect visual position: 0px from visual Left.
+ Box(
+ // Use TopLeft (2D) instead of Left (1D) to satisfy the Alignment
+ // type requirement
+ Modifier.gridItem(
+ 1,
+ 1,
+ alignment = androidx.compose.ui.AbsoluteAlignment.TopLeft,
+ )
+ .size(itemSizeDp)
+ .saveLayoutInfo(Ref(), absLeftAlignedPos, positionedLatch)
+ )
+ }
+ }
+ }
+
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+ // 1. Start (Right side of container)
+ assertEquals(
+ "Alignment.TopStart in RTL should be visually on the Right (60px from left)",
+ Offset(60f, 0f),
+ startAlignedPos.value,
+ )
+
+ // 2. Absolute Left (Left side of container)
+ assertEquals(
+ "AbsoluteAlignment.TopLeft in RTL should visually remain on the Left (0px)",
+ Offset(0f, 0f),
+ absLeftAlignedPos.value,
+ )
+ }
+
+ @Test
+ fun testGrid_contentBasedSizing() {
+ val childSize = Ref()
+ val latch = CountDownLatch(1)
+ val dummyPosition = Ref()
+
+ show {
+ Grid(
+ config = {
+ // Col 1: MinContent (should match min intrinsic width)
+ column(GridTrackSize.MinContent)
+ // Col 2: MaxContent (should match max intrinsic width)
+ column(GridTrackSize.MaxContent)
+ row(GridTrackSize.Fixed(50.dp))
+ }
+ ) {
+ // Item 1: Min=50, Max=100
+ IntrinsicItem(
+ minWidth = 50,
+ minIntrinsicWidth = 50,
+ maxIntrinsicWidth = 100,
+ modifier = Modifier.gridItem(1, 1).fillMaxSize(),
+ )
+
+ // Item 2: Min=50, Max=100
+ IntrinsicItem(
+ minWidth = 50,
+ minIntrinsicWidth = 50,
+ maxIntrinsicWidth = 100,
+ modifier =
+ Modifier.gridItem(1, 2)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize, dummyPosition, latch),
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Col 1 should be 50 (min)
+ // Col 2 should be 100 (max)
+ // Item 2 in Col 2 should have width 100
+ assertEquals(100, childSize.value?.width)
+ }
+
+ @Test
+ fun testGrid_implicitTracks_shrinkToFitContent() =
+ with(density) {
+ // Scenario:
+ // Item placed in Col 10.
+ // Cols 1-9 should be implicit Auto.
+ // If they have no content, they should have width 0.
+ // Col 10 should have width 50.
+ // Total Grid Width should be 50 (0+0...+50).
+
+ val size = 50
+ val sizeDp = size.toDp()
+ val latch = CountDownLatch(1)
+ val pos = Ref()
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ // No explicit columns
+ row(GridTrackSize.Fixed(sizeDp))
+ }
+ ) {
+ // Place far away at Column 5.
+ // Columns 1, 2, 3, 4 are Implicit Auto and Empty -> Size 0.
+ Box(
+ Modifier.gridItem(column = 5).size(sizeDp).saveLayoutInfo(dummy, pos, latch)
+ )
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Position should be 0 because previous columns collapsed.
+ // Note: If gaps were added, they would add up (4 * gap).
+ assertEquals(Offset(0f, 0f), pos.value)
+ }
+
+ @Test
+ fun testGrid_rowSpan_expandsRowsToFitContent() =
+ with(density) {
+ // Scenario:
+ // 2 Columns (Fixed 50)
+ // 2 Rows (Auto)
+ // Item 1 (Col 1, Row 1): Small (10px height)
+ // Item 2 (Col 1, Row 2): Small (10px height)
+ // Item 3 (Col 2, Row 1, Span 2): Tall (100px height)
+ //
+ // Expected Behavior:
+ // Without spanning logic: Rows would be 10px each (total 20px). Item 3 would overflow.
+ // With spanning logic: Item 3 needs 100px.
+ // Deficit = 100 - (10 + 10) = 80px.
+ // Distribute 80px / 2 rows = +40px each.
+ // Final Row Heights: 10 + 40 = 50px each.
+ // Total Grid Height: 100px.
+
+ val colWidth = 50.dp
+ val smallItemHeight = 10.dp
+ val tallItemHeight = 100.dp
+ val expectedTotalHeight = 100.dp.roundToPx()
+
+ val latch = CountDownLatch(1)
+ val gridSize = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(colWidth))
+ column(GridTrackSize.Fixed(colWidth))
+ row(GridTrackSize.Auto)
+ row(GridTrackSize.Auto)
+ },
+ modifier =
+ Modifier.onGloballyPositioned {
+ gridSize.value = it.size
+ latch.countDown()
+ },
+ ) {
+ // Col 1, Row 1
+ Box(Modifier.gridItem(1, 1).size(colWidth, smallItemHeight))
+ // Col 1, Row 2
+ Box(Modifier.gridItem(2, 1).size(colWidth, smallItemHeight))
+
+ // Col 2, Row 1, Span 2 (The driver of expansion)
+ Box(
+ Modifier.gridItem(row = 1, column = 2, rowSpan = 2)
+ .size(colWidth, tallItemHeight)
+ )
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(
+ "Grid height should expand to accommodate the tall row-spanning item",
+ expectedTotalHeight,
+ gridSize.value?.height,
+ )
+ }
+
+ @Test
+ fun testGrid_rowSpan_expandsFlexRows() =
+ with(density) {
+ // Scenario:
+ // Container Height = 100px (Fixed constraint)
+ // 2 Rows (Flex 1fr)
+ // Item 1 (Row 1): Empty
+ // Item 2 (Row 2): Empty
+ // Item 3 (Span 2): Tall (200px)
+ //
+ // Expected Behavior:
+ // Flex logic initially splits 100px -> 50px each.
+ // Spanning logic sees Item 3 needs 200px.
+ // Deficit = 200 - 100 = 100px.
+ // Rows should grow to 100px each.
+ // Total Height = 200px (Grid expands beyond parent constraint if content demands it).
+
+ val containerHeight = 100.dp
+ val tallItemHeight = 200.dp
+ val expectedTotalHeight = 200.dp.roundToPx()
+
+ val latch = CountDownLatch(1)
+ val gridSize = Ref()
+
+ show {
+ // Wrap in a box with fixed height to simulate constraints,
+ // but allow Grid to be larger (unbounded internal checks)
+ Box(
+ Modifier.height(containerHeight)
+ .wrapContentHeight(align = Alignment.Top, unbounded = true)
+ ) {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(50.dp))
+ row(GridTrackSize.Flex(1.fr))
+ row(GridTrackSize.Flex(1.fr))
+ },
+ modifier =
+ Modifier.onGloballyPositioned {
+ gridSize.value = it.size
+ latch.countDown()
+ },
+ ) {
+ // Spanning item forcing expansion
+ Box(
+ Modifier.gridItem(row = 1, column = 1, rowSpan = 2)
+ .size(50.dp, tallItemHeight)
+ )
+ }
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(
+ "Flex rows should expand beyond 1fr share if spanning item requires it",
+ expectedTotalHeight,
+ gridSize.value?.height,
+ )
+ }
+
+ @Test
+ fun testGrid_autoPlacement_rowFlow() =
+ with(density) {
+ val size = 50
+ val sizeDp = size.toDp()
+ val latch = CountDownLatch(3)
+ val pos = Array(3) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(sizeDp))
+ column(GridTrackSize.Fixed(sizeDp))
+ // Rows are implicit/auto
+ flow = GridFlow.Row
+ }
+ ) {
+ // We use Modifier.size because implicit Auto tracks need content size to expand
+ // Item 1 -> (0,0)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[0], latch))
+ // Item 2 -> (50,0)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[1], latch))
+ // Item 3 -> Wraps to next row -> (0, 50)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[2], latch))
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ assertEquals(Offset(size.toFloat(), 0f), pos[1].value)
+ assertEquals(Offset(0f, size.toFloat()), pos[2].value)
+ }
+
+ @Test
+ fun testGrid_autoPlacement_columnFlow() =
+ with(density) {
+ val size = 50
+ val sizeDp = size.toDp()
+ val latch = CountDownLatch(3)
+ val pos = Array(3) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ flow = GridFlow.Column
+ row(GridTrackSize.Fixed(sizeDp))
+ row(GridTrackSize.Fixed(sizeDp))
+ // Cols are implicit/auto
+ }
+ ) {
+ // Item 1 -> (0,0)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[0], latch))
+ // Item 2 -> (0,50)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[1], latch))
+ // Item 3 -> Wraps to next col -> (50,0)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[2], latch))
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ assertEquals(Offset(0f, size.toFloat()), pos[1].value)
+ assertEquals(Offset(size.toFloat(), 0f), pos[2].value)
+ }
+
+ @Test
+ fun testGrid_autoPlacement_wrapping_respectsGaps() =
+ with(density) {
+ val size = 50
+ val gap = 10
+ val sizeDp = size.toDp()
+ val gapDp = gap.toDp()
+ val latch = CountDownLatch(2)
+ val pos = Array(2) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(sizeDp)) // Only 1 column
+ row(GridTrackSize.Fixed(sizeDp))
+ gap(gapDp)
+ flow = GridFlow.Row
+ }
+ ) {
+ // Item 1: (0,0)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[0], latch))
+ // Item 2: Wraps to (0,1). Should include vertical gap.
+ // Y Position = Row 1 Height (50) + Gap (10) = 60
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[1], latch))
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ assertEquals(Offset(0f, (size + gap).toFloat()), pos[1].value)
+ }
+
+ @Test
+ fun testGrid_columnFlow_wrapsCorrectly() =
+ with(density) {
+ val size = 50
+ val sizeDp = size.toDp()
+ val latch = CountDownLatch(3)
+ val pos = Array(3) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ flow = GridFlow.Column
+ // 2 Explicit Rows
+ row(GridTrackSize.Fixed(sizeDp))
+ row(GridTrackSize.Fixed(sizeDp))
+ }
+ ) {
+ // Item 1 -> (0,0)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[0], latch))
+ // Item 2 -> (0,50)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[1], latch))
+ // Item 3 -> Wraps to Next Column -> (50,0)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[2], latch))
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ assertEquals(Offset(0f, size.toFloat()), pos[1].value)
+ assertEquals(Offset(size.toFloat(), 0f), pos[2].value)
+ }
+
+ @Test
+ fun testGrid_columnFlow_createsImplicitColumns() =
+ with(density) {
+ val itemSize = 50
+ val latch = CountDownLatch(1)
+ val gridSize = Ref()
+
+ show {
+ Grid(
+ config = {
+ // 2 Explicit Rows. Flow = Column.
+ row(GridTrackSize.Fixed(itemSize.toDp()))
+ row(GridTrackSize.Fixed(itemSize.toDp()))
+ flow = GridFlow.Column
+ },
+ modifier =
+ Modifier.onGloballyPositioned {
+ gridSize.value = it.size
+ latch.countDown()
+ },
+ ) {
+ // 4 items.
+ // Items 1, 2 fill Col 1 (Rows 1, 2)
+ // Items 3, 4 should create implicit Col 2 (Rows 1, 2)
+ repeat(4) { Box(Modifier.size(itemSize.toDp())) }
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Expected: 2 Rows (Explicit) x 2 Columns (1 Explicit + 1 Implicit)
+ // Implicit columns default to Auto -> itemSize (50)
+ assertEquals(IntSize(itemSize * 2, itemSize * 2), gridSize.value)
+ }
+
+ @Test
+ fun testGrid_columnFlow_implicitRows() =
+ with(density) {
+ // Scenario:
+ // Flow = Column.
+ // Explicitly define 1 Column (so we know the width).
+ // Do NOT define any Rows.
+ // Item 1: (0,0)
+ // Item 2: Should stack below at (1,0) -> Creating Implicit Row 2
+ // Item 3: Should stack below at (2,0) -> Creating Implicit Row 3
+
+ val size = 50.dp
+ val sizePx = size.roundToPx().toFloat()
+ val latch = CountDownLatch(3)
+ val pos = Array(3) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ flow = GridFlow.Column
+ // Define column width so layout isn't 0 width
+ column(GridTrackSize.Fixed(size))
+ // Do NOT define rows.
+ // This allows the column to grow infinitely downwards.
+ }
+ ) {
+ // (0,0)
+ Box(Modifier.size(size).saveLayoutInfo(dummy, pos[0], latch))
+ // (1, 0) -> Implicit Row 2
+ Box(Modifier.size(size).saveLayoutInfo(dummy, pos[1], latch))
+ // (2, 0) -> Implicit Row 3
+ Box(Modifier.size(size).saveLayoutInfo(dummy, pos[2], latch))
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // All X should be 0 (Column 0)
+ // Y should increment by sizePx
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ assertEquals(Offset(0f, sizePx), pos[1].value)
+ assertEquals(Offset(0f, sizePx * 2), pos[2].value)
+ }
+
+ @Test
+ fun testGrid_mixedPlacement_skipsOccupiedCells() =
+ with(density) {
+ val size = 50
+ val sizeDp = size.toDp()
+ val latch = CountDownLatch(3)
+ val pos = Array(3) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ repeat(3) { column(GridTrackSize.Fixed(sizeDp)) }
+ row(GridTrackSize.Fixed(sizeDp))
+ }
+ ) {
+ // 1. Explicit Item at (0, 1) (Middle Column)
+ Box(Modifier.gridItem(1, 2).size(sizeDp).saveLayoutInfo(dummy, pos[0], latch))
+
+ // 2. Auto Item 1 -> Should go to (0, 0)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[1], latch))
+
+ // 3. Auto Item 2 -> Should skip (0, 1) and go to (0, 2)
+ Box(Modifier.size(sizeDp).saveLayoutInfo(dummy, pos[2], latch))
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Explicit
+ assertEquals(Offset(size.toFloat(), 0f), pos[0].value)
+ // Auto 1
+ assertEquals(Offset(0f, 0f), pos[1].value)
+ // Auto 2
+ assertEquals(Offset((size * 2).toFloat(), 0f), pos[2].value)
+ }
+
+ @Test
+ fun testGrid_explicitPlacement_allowsOverlaps() =
+ with(density) {
+ // Scenario:
+ // Two items explicitly placed in (1, 1).
+ // They should occupy the same space. The grid should not throw or shift them.
+
+ val size = 50
+ val sizeDp = size.toDp()
+ val latch = CountDownLatch(2)
+ val pos = Array(2) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(sizeDp))
+ row(GridTrackSize.Fixed(sizeDp))
+ }
+ ) {
+ // Item 1
+ Box(Modifier.gridItem(1, 1).size(sizeDp).saveLayoutInfo(dummy, pos[0], latch))
+ // Item 2 (Same Cell)
+ Box(Modifier.gridItem(1, 1).size(sizeDp).saveLayoutInfo(dummy, pos[1], latch))
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ assertEquals(Offset(0f, 0f), pos[1].value)
+ }
+
+ @Test
+ fun testGrid_explicitPlacement_doesNotMoveAutoCursor() =
+ with(density) {
+ // Scenario:
+ // 3 Columns.
+ // Item 1: Auto (0,0). Cursor moves to (0,1).
+ // Item 2: Explicit far away (0, 2). Cursor should REMAIN at (0,1).
+ // Item 3: Auto. Should fill the gap at (0,1), NOT start after Item 2.
+
+ val size = 50.dp
+ val latch = CountDownLatch(3)
+ val pos = Array(3) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ repeat(3) { column(GridTrackSize.Fixed(size)) }
+ row(GridTrackSize.Fixed(size))
+ }
+ ) {
+ // 1. Auto -> (0,0)
+ Box(Modifier.size(size).saveLayoutInfo(dummy, pos[0], latch))
+ // 2. Explicit -> (0,2). Should NOT move cursor.
+ Box(Modifier.gridItem(1, 3).size(size).saveLayoutInfo(dummy, pos[1], latch))
+ // 3. Auto -> Should fill (0,1)
+ Box(Modifier.size(size).saveLayoutInfo(dummy, pos[2], latch))
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Calculate pixel size of a single track first
+ val sizePx = size.roundToPx().toFloat()
+
+ // Auto 1 (0,0)
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ // Explicit (Col 3 -> Index 2)
+ assertEquals(Offset(sizePx * 2, 0f), pos[1].value)
+ // Auto 2 (Col 2 -> Index 1)
+ assertEquals(Offset(sizePx, 0f), pos[2].value)
+ }
+
+ @Test
+ fun testGrid_fixedRow_autoCol_skipsOccupied() =
+ with(density) {
+ // Scenario:
+ // 3 Columns.
+ // Item 1: Explicitly placed at (0, 0).
+ // Item 2: Fixed Row 0. Should skip (0,0) and go to (0,1).
+ // Item 3: Fixed Row 0. Should skip (0,0) and (0,1) and go to (0,2).
+
+ val size = 50.dp
+ val latch = CountDownLatch(3)
+ val pos = Array(3) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ repeat(3) { column(GridTrackSize.Fixed(size)) }
+ row(GridTrackSize.Fixed(size))
+ }
+ ) {
+ // 1. Occupy (0,0) explicitly
+ Box(Modifier.gridItem(1, 1).size(size).saveLayoutInfo(dummy, pos[0], latch))
+ // 2. Request Row 1 (Auto Col). Should find Col 2.
+ Box(Modifier.gridItem(row = 1).size(size).saveLayoutInfo(dummy, pos[1], latch))
+ // 3. Request Row 1 (Auto Col). Should find Col 3.
+ Box(Modifier.gridItem(row = 1).size(size).saveLayoutInfo(dummy, pos[2], latch))
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Calculate pixel size of a single track first
+ val sizePx = size.roundToPx().toFloat()
+
+ // (0,0)
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ // (50, 0)
+ assertEquals(Offset(sizePx, 0f), pos[1].value)
+ // (100, 0) -> Sum of two tracks
+ assertEquals(Offset(sizePx * 2, 0f), pos[2].value)
+ }
+
+ @Test
+ fun testGrid_fixedCol_autoRow_skipsOccupied() =
+ with(density) {
+ // Scenario:
+ // 1 Column, 3 Rows.
+ // Item 1: Explicitly placed at (0, 0).
+ // Item 2: Fixed Col 0. Should skip (0,0) and go to (1,0).
+
+ val size = 50.dp
+ val latch = CountDownLatch(2)
+ val pos = Array(2) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(size))
+ repeat(3) { row(GridTrackSize.Fixed(size)) }
+ }
+ ) {
+ // 1. Occupy (0,0) explicitly
+ Box(Modifier.gridItem(1, 1).size(size).saveLayoutInfo(dummy, pos[0], latch))
+ // 2. Request Col 1 (Auto Row). Should skip Row 1 and land in Row 2.
+ Box(
+ Modifier.gridItem(column = 1)
+ .size(size)
+ .saveLayoutInfo(dummy, pos[1], latch)
+ )
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Calculate pixel size of a single track first
+ val sizePx = size.roundToPx().toFloat()
+
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ assertEquals(Offset(0f, sizePx), pos[1].value)
+ }
+
+ @Test
+ fun testGrid_mixedFlow_fixedRowInColumnFlow() =
+ with(density) {
+ // Scenario:
+ // Flow = Column.
+ // Item 1: Fixed Row 1.
+ // Since flow is column, the logic must look for the *first available column* in that
+ // fixed row.
+
+ val size = 50.dp
+ val latch = CountDownLatch(1)
+ val pos = Ref()
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(size))
+ column(GridTrackSize.Fixed(size))
+ row(GridTrackSize.Fixed(size))
+ row(GridTrackSize.Fixed(size))
+ flow = GridFlow.Column
+ }
+ ) {
+ // Occupy (1,0) - (Row 2, Col 1)
+ Box(Modifier.gridItem(2, 1).size(size))
+
+ // Request Row 2.
+ // Since (2,1) is occupied, it should find (2,2).
+ Box(Modifier.gridItem(row = 2).size(size).saveLayoutInfo(dummy, pos, latch))
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Calculate pixel size of a single track first
+ val sizePx = size.roundToPx().toFloat()
+
+ // Should be at Row 2 (Index 1), Col 2 (Index 1) -> (50, 50)
+ assertEquals(Offset(sizePx, sizePx), pos.value)
+ }
+
+ @Test
+ fun testGrid_negativeIndices_placeCorrectly() =
+ with(density) {
+ val size = 50
+ val sizeDp = size.toDp()
+
+ val positionedLatch = CountDownLatch(4)
+ val childPosition = Array(4) { Ref() }
+ val dummySize = Array(4) { Ref() }
+
+ show {
+ Grid(
+ config = {
+ repeat(3) { column(GridTrackSize.Fixed(sizeDp)) }
+ repeat(3) { row(GridTrackSize.Fixed(sizeDp)) }
+ }
+ ) {
+ // 1. Top-Left (1, 1) -> (0, 0)
+ Box(
+ Modifier.gridItem(1, 1)
+ .fillMaxSize()
+ .saveLayoutInfo(dummySize[0], childPosition[0], positionedLatch)
+ )
+ // 2. Top-Right (1, -1) -> (100, 0) (Last Column)
+ Box(
+ Modifier.gridItem(1, -1)
+ .fillMaxSize()
+ .saveLayoutInfo(dummySize[1], childPosition[1], positionedLatch)
+ )
+ // 3. Bottom-Left (-1, 1) -> (0, 100) (Last Row)
+ Box(
+ Modifier.gridItem(-1, 1)
+ .fillMaxSize()
+ .saveLayoutInfo(dummySize[2], childPosition[2], positionedLatch)
+ )
+ // 4. Bottom-Right (-1, -1) -> (100, 100) (Last Row, Last Column)
+ Box(
+ Modifier.gridItem(-1, -1)
+ .fillMaxSize()
+ .saveLayoutInfo(dummySize[3], childPosition[3], positionedLatch)
+ )
+ }
+ }
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+ // 1. (0, 0)
+ assertEquals(Offset(0f, 0f), childPosition[0].value)
+ // 2. (100, 0) -> Col index 2 * 50
+ assertEquals(Offset((size * 2).toFloat(), 0f), childPosition[1].value)
+ // 3. (0, 100) -> Row index 2 * 50
+ assertEquals(Offset(0f, (size * 2).toFloat()), childPosition[2].value)
+ // 4. (100, 100)
+ assertEquals(Offset((size * 2).toFloat(), (size * 2).toFloat()), childPosition[3].value)
+ }
+
+ @Test
+ fun testGrid_negativeIndex_refersToExplicitBounds() =
+ with(density) {
+ val size = 50
+ val latch = CountDownLatch(1)
+ val pos = Ref()
+
+ // Create a dummy ref instead of passing null
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ repeat(2) {
+ column(GridTrackSize.Fixed(size.toDp()))
+ } // Explicit Cols: 0, 1
+ row(GridTrackSize.Fixed(size.toDp()))
+ }
+ ) {
+ // Create an implicit 3rd column (Index 2)
+ Box(Modifier.gridItem(1, 3).size(size.toDp()))
+
+ // Place item at column = -1.
+ // Should map to Explicit Col 1 (the 2nd column), NOT the implicit 3rd column.
+ Box(
+ Modifier.gridItem(column = -1)
+ .size(size.toDp())
+ .saveLayoutInfo(dummy, pos, latch)
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Expect pos at 2nd column (Index 1) -> 50px
+ assertEquals(Offset(size.toFloat(), 0f), pos.value)
+ }
+
+ @Test
+ fun testGrid_invalidNegativeIndices_fallbackToAuto() =
+ with(density) {
+ val size = 50
+ val sizeDp = size.toDp()
+ val latch = CountDownLatch(2)
+ val pos1 = Ref()
+ val pos2 = Ref()
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ // 2x2 Grid
+ repeat(2) { column(GridTrackSize.Fixed(sizeDp)) }
+ repeat(2) { row(GridTrackSize.Fixed(sizeDp)) }
+ }
+ ) {
+ // Case 1: Valid Negative (-1 -> Index 1)
+ Box(
+ Modifier.gridItem(row = -1, column = -1)
+ .size(sizeDp)
+ .saveLayoutInfo(dummy, pos1, latch)
+ )
+
+ // Case 2: Invalid Negative (-5 -> Index -3 -> Invalid)
+ // Should be treated as "Unspecified" and auto-placed.
+ // Since (1,1) is empty (Item 1 is at 1,1 0-based), it should go to (0,0).
+ Box(
+ Modifier.gridItem(row = -5, column = -5)
+ .size(sizeDp)
+ .saveLayoutInfo(dummy, pos2, latch)
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Item 1 (Valid -1,-1): Bottom-Right (50, 50)
+ assertEquals(Offset(size.toFloat(), size.toFloat()), pos1.value)
+
+ // Item 2 (Invalid -5,-5): Auto-placed to first available slot (0,0)
+ assertEquals(Offset(0f, 0f), pos2.value)
+ }
+
+ @Test
+ fun testGrid_spanning() {
+ val colSize = 50
+ val rowSize = 50
+
+ val positionedLatch = CountDownLatch(1)
+ val childSize = Ref()
+ val childPosition = Ref()
+
+ show {
+ Grid(
+ config = {
+ repeat(3) { column(GridTrackSize.Fixed(colSize.toDp())) }
+ repeat(3) { row(GridTrackSize.Fixed(rowSize.toDp())) }
+ }
+ ) {
+ // Item at R2, C2 spanning 2 rows and 2 columns
+ // Should be at (50, 50) with size (100, 100)
+ Box(
+ Modifier.gridItem(row = 2, column = 2, rowSpan = 2, columnSpan = 2)
+ .fillMaxSize()
+ .saveLayoutInfo(childSize, childPosition, positionedLatch)
+ )
+ }
+ }
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(IntSize(colSize * 2, rowSize * 2), childSize.value)
+ assertEquals(Offset(colSize.toFloat(), rowSize.toFloat()), childPosition.value)
+ }
+
+ @Test
+ fun testGrid_spanEntireGrid() =
+ with(density) {
+ val size = 50
+ val sizeDp = size.toDp()
+ val latch = CountDownLatch(1)
+ val itemSize = Ref()
+
+ show {
+ Grid(
+ config = {
+ repeat(4) { column(GridTrackSize.Fixed(sizeDp)) }
+ row(GridTrackSize.Fixed(sizeDp))
+ }
+ ) {
+ // Spans all 4 columns
+ Box(
+ Modifier.gridItem(1, 1, columnSpan = 4)
+ .fillMaxSize()
+ .saveLayoutInfo(itemSize, Ref(), latch)
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(size * 4, itemSize.value?.width)
+ }
+
+ @Test
+ fun testGrid_spanning_intoImplicitTracks() =
+ with(density) {
+ val size = 50
+ val latch = CountDownLatch(1)
+ val itemSize = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(size.toDp())) // 1 Explicit Column
+ row(GridTrackSize.Fixed(size.toDp()))
+ }
+ ) {
+ // Place at Col 1, Span 2.
+ // Should cover Explicit Col 1 + Implicit Col 2.
+ // Implicit Col 2 should size to Auto. Since this item spans,
+ // and Auto tracks ignore spanning items for intrinsic sizing (as per your
+ // design),
+ // the implicit track might collapse to 0 OR resize if logic allows.
+ // *Correction*: Your logic adds `Auto` tracks. If no other item is in Col 2,
+ // it will be size 0.
+ // Let's add a non-spanning item in Col 2 to give it size.
+
+ // Item A: Spans Col 1 and Col 2
+ Box(
+ Modifier.gridItem(1, 1, columnSpan = 2)
+ .fillMaxSize()
+ .saveLayoutInfo(itemSize, Ref(), latch)
+ )
+
+ // Item B: Sits in Implicit Col 2 to force it to have size
+ Box(Modifier.gridItem(1, 2).size(size.toDp()))
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Width = Col 1 (50) + Col 2 (50 from Item B) = 100
+ assertEquals(size * 2, itemSize.value?.width)
+ }
+
+ @Test
+ fun testGrid_itemSpanLargerThanExplicitGrid_doesNotLoop() =
+ with(density) {
+ val size = 50
+ val sizeDp = size.toDp()
+ val latch = CountDownLatch(1)
+ val pos = Ref()
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ // 2 Explicit Columns
+ column(GridTrackSize.Fixed(sizeDp))
+ column(GridTrackSize.Fixed(sizeDp))
+ flow = GridFlow.Row
+ }
+ ) {
+ // Item spans 3 columns (Exceeds explicit count of 2)
+ // Should be placed at (0,0) and create implicit tracks
+ Box(
+ Modifier.gridItem(columnSpan = 3)
+ .size(sizeDp)
+ .saveLayoutInfo(dummy, pos, latch)
+ )
+ }
+ }
+ assertTrue("Timed out - likely infinite loop", latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(Offset(0f, 0f), pos.value)
+ }
+
+ @Test
+ fun testGrid_respectsMinConstraints_expandsToFill() =
+ with(density) {
+ val smallTrackSize = 50.dp
+ val largeParentSize = 100.dp
+ val expectedSize = 100.dp.roundToPx()
+
+ val positionedLatch = CountDownLatch(1)
+ val gridSize = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(smallTrackSize))
+ row(GridTrackSize.Fixed(smallTrackSize))
+ },
+ // Force the Grid to be larger than its content
+ modifier =
+ Modifier.size(largeParentSize).onGloballyPositioned { coordinates ->
+ gridSize.value = coordinates.size
+ positionedLatch.countDown()
+ },
+ ) { /* empty */
+ }
+ }
+
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(
+ "Grid should expand to satisfy min constraints",
+ IntSize(expectedSize, expectedSize),
+ gridSize.value,
+ )
+ }
+
+ @Test
+ fun testGrid_respectsMaxConstraints_coercesSize() =
+ with(density) {
+ val largeTrackSize = 200.dp
+ val smallParentSize = 100.dp
+ val expectedSize = 100.dp.roundToPx()
+
+ val positionedLatch = CountDownLatch(1)
+ val gridSize = Ref()
+
+ show {
+ // Wrap in Box with propagateMinConstraints=false to test pure max constraints
+ Box(Modifier.size(smallParentSize)) {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(largeTrackSize))
+ row(GridTrackSize.Fixed(largeTrackSize))
+ },
+ modifier =
+ Modifier.onGloballyPositioned { coordinates ->
+ gridSize.value = coordinates.size
+ positionedLatch.countDown()
+ },
+ ) { /* empty */
+ }
+ }
+ }
+
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(
+ "Grid should respect max constraints even if tracks are larger",
+ IntSize(expectedSize, expectedSize),
+ gridSize.value,
+ )
+ }
+
+ @Test
+ fun testGrid_respectsConstraints_whenContentOverflows() =
+ with(density) {
+ val parentSize = 100
+ val contentSize = 200 // Larger than parent
+
+ val latch = CountDownLatch(1)
+ val gridSize = Ref()
+
+ show {
+ // Parent container restricts size to 100x100
+ Box(Modifier.size(parentSize.toDp())) {
+ Grid(
+ config = {
+ // Grid wants to be 200x200
+ column(GridTrackSize.Fixed(contentSize.toDp()))
+ row(GridTrackSize.Fixed(contentSize.toDp()))
+ },
+ modifier =
+ Modifier.onGloballyPositioned {
+ gridSize.value = it.size
+ latch.countDown()
+ },
+ ) {
+ Box(Modifier.gridItem(1, 1).fillMaxSize())
+ }
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Assert that Grid reported the PARENT'S size (clamped), not the content size
+ assertEquals(
+ "Grid should be clamped to parent max width/height",
+ IntSize(parentSize, parentSize),
+ gridSize.value,
+ )
+ }
+
+ @Test
+ fun testGrid_respectsConstraints_whenContentUnderflows() =
+ with(density) {
+ val minSize = 200
+ val contentSize = 50 // Smaller than parent min
+
+ val latch = CountDownLatch(1)
+ val gridSize = Ref()
+
+ show {
+ // Parent enforces minimum size of 200x200 (e.g. fillMaxSize)
+ Box(Modifier.requiredSize(minSize.toDp())) {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(contentSize.toDp()))
+ row(GridTrackSize.Fixed(contentSize.toDp()))
+ },
+ modifier =
+ Modifier.fillMaxSize() // Request to fill parent
+ .onGloballyPositioned {
+ gridSize.value = it.size
+ latch.countDown()
+ },
+ ) {
+ Box(Modifier.gridItem(1, 1).fillMaxSize())
+ }
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Assert that Grid expanded to meet the minimum constraints
+ assertEquals(
+ "Grid should expand to meet min constraints",
+ IntSize(minSize, minSize),
+ gridSize.value,
+ )
+ }
+
+ @Test
+ fun testGrid_percentageTrack_inIndefiniteContainer_fallbacksToAuto() =
+ with(density) {
+ val positionedLatch = CountDownLatch(1)
+ val gridSize = Ref()
+
+ show {
+ // Wrap in a Row to provide infinite width constraint
+ Row {
+ Grid(
+ config = {
+ // 50% of Infinity cannot be calculated.
+ // Fallback to Auto (MaxContent) and fit the item.
+ column(GridTrackSize.Percentage(0.5f))
+ row(GridTrackSize.Fixed(50.dp))
+ },
+ modifier =
+ Modifier.onGloballyPositioned { coordinates ->
+ gridSize.value = coordinates.size
+ positionedLatch.countDown()
+ },
+ ) {
+ // The item is 10.dp wide. The track should expand to fit this.
+ Box(Modifier.gridItem(1, 1).size(10.dp))
+ }
+ }
+ }
+
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+
+ // Width should be 10.dp (Size of the content), NOT 0.
+ // Height should be 50.dp (Fixed)
+ assertEquals(IntSize(10.dp.roundToPx(), 50.dp.roundToPx()), gridSize.value)
+ }
+
+ @Test
+ fun testGrid_flexColumn_inInfiniteConstraints_withGap_doesNotExpand() =
+ with(density) {
+ // Scenario: Grid is inside a Row + horizontalScroll (Infinite Width).
+ // It has a Flex column and a GAP.
+ //
+ // Bug Trigger:
+ // 1. Available Width = Infinity.
+ // 2. Gap = 10px.
+ // 3. Calculation: availableTrackSpace = Infinity - 10 = 2,147,483,637.
+ // 4. Check: (availableTrackSpace == Infinity) is FALSE.
+ // 5. Result: Logic thinks it has 2 billion pixels of space to distribute.
+ // 6. Flex track expands to ~2 billion pixels.
+
+ val gap = 10.dp
+ val itemSize = 50.dp
+ val expectedWidth = itemSize.roundToPx() // Should shrink to fit content
+
+ val latch = CountDownLatch(1)
+ val gridSize = Ref()
+
+ show {
+ // Parent provides Infinite Width constraint
+ Row(Modifier.horizontalScroll(rememberScrollState())) {
+ Grid(
+ config = {
+ column(GridTrackSize.Flex(1.fr)) // Should behave like MinContent/Auto
+ column(
+ GridTrackSize.Fixed(0.dp)
+ ) // Dummy column to ensure gap is applied
+ row(GridTrackSize.Fixed(50.dp))
+ columnGap(gap)
+ },
+ modifier =
+ Modifier.onGloballyPositioned {
+ gridSize.value = it.size
+ latch.countDown()
+ },
+ ) {
+ // Item in Flex column
+ Box(Modifier.gridItem(1, 1).size(itemSize))
+ }
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ assertEquals(
+ "Flex column in infinite constraints should fallback to min-content size",
+ expectedWidth + gap.roundToPx(), // 50 (Item) + 10 (Gap) + 0 (Col 2)
+ gridSize.value?.width,
+ )
+ }
+
+ @Test
+ fun testGrid_zeroSizeTrack_layoutCorrectly() =
+ with(density) {
+ val size = 50
+ val latch = CountDownLatch(2)
+ val pos = Array(2) { Ref() }
+ val dummy = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(size.toDp()))
+ column(GridTrackSize.Fixed(0.dp)) // Zero width column
+ column(GridTrackSize.Fixed(size.toDp()))
+ row(GridTrackSize.Fixed(size.toDp()))
+ }
+ ) {
+ // Item 1: Col 1
+ Box(
+ Modifier.gridItem(1, 1)
+ .size(size.toDp())
+ .saveLayoutInfo(dummy, pos[0], latch)
+ )
+ // Item 2: Col 3 (Skipping Col 2 which is 0 width)
+ Box(
+ Modifier.gridItem(1, 3)
+ .size(size.toDp())
+ .saveLayoutInfo(dummy, pos[1], latch)
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+
+ // Item 1 at 0
+ assertEquals(Offset(0f, 0f), pos[0].value)
+ // Item 2 at 50 + 0 = 50
+ assertEquals(Offset(size.toFloat(), 0f), pos[1].value)
+ }
+
+ @Test
+ fun testGrid_zeroSizeChildren() {
+ val trackSize = 50
+ val latch = CountDownLatch(1)
+ val gridSize = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(trackSize.toDp()))
+ row(GridTrackSize.Fixed(trackSize.toDp()))
+ },
+ modifier =
+ Modifier.onGloballyPositioned {
+ gridSize.value = it.size
+ latch.countDown()
+ },
+ ) {
+ // Place a zero-sized item.
+ // It should still occupy the logical cell (1,1), but draw nothing.
+ // The Grid should still size itself to the Fixed tracks (50x50).
+ Box(Modifier.gridItem(1, 1).size(0.dp))
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(IntSize(trackSize, trackSize), gridSize.value)
+ }
+
+ @Test
+ fun testGrid_itemFillsCell_whenRequested() {
+ val trackSize = 100
+ val latch = CountDownLatch(1)
+ val childSize = Ref()
+
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(trackSize.toDp()))
+ row(GridTrackSize.Fixed(trackSize.toDp()))
+ }
+ ) {
+ // Item has no intrinsic size, but requests fillMaxSize().
+ // It should fill the definition of the track (100x100).
+ Box(Modifier.gridItem(1, 1).fillMaxSize().saveLayoutInfo(childSize, Ref(), latch))
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(IntSize(trackSize, trackSize), childSize.value)
+ }
+
+ @Test
+ fun testGrid_nestedGrid() =
+ with(density) {
+ val outerSize = 100
+ val outerSizeDp = outerSize.toDp()
+
+ // Inner grid will be placed in a 100x100 cell.
+ // It will have 2 columns of 50 each.
+ val latch = CountDownLatch(1)
+ val innerItemSize = Ref()
+
+ show {
+ // Outer Grid
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(outerSizeDp))
+ row(GridTrackSize.Fixed(outerSizeDp))
+ }
+ ) {
+ // Inner Grid placed at (1,1) of Outer Grid
+ Grid(
+ modifier = Modifier.gridItem(1, 1).fillMaxSize(),
+ config = {
+ column(GridTrackSize.Flex(1.fr))
+ column(GridTrackSize.Flex(1.fr))
+ row(GridTrackSize.Flex(1.fr))
+ },
+ ) {
+ // Item inside Inner Grid (Col 1)
+ Box(
+ Modifier.gridItem(1, 1)
+ .fillMaxSize()
+ .saveLayoutInfo(innerItemSize, Ref(), latch)
+ )
+ // Item inside Inner Grid (Col 2) just to fill space
+ Box(Modifier.gridItem(1, 2).fillMaxSize())
+ }
+ }
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(IntSize(50, 100), innerItemSize.value)
+ }
+
+ @Test
+ fun testGrid_stressTest_manyItems() =
+ with(density) {
+ val itemSize = 10
+ val itemSizeDp = itemSize.toDp()
+ val itemCount = 100
+ val cols = 10
+
+ // 100 items / 10 cols = 10 rows.
+ // Total Height = 10 rows * 10px = 100px.
+ val expectedHeight = 100
+
+ val latch = CountDownLatch(1)
+ val gridSize = Ref()
+
+ show {
+ Grid(
+ config = {
+ // 10 Fixed columns
+ repeat(cols) { column(GridTrackSize.Fixed(itemSizeDp)) }
+ // Implicit rows
+ flow = GridFlow.Row
+ },
+ modifier =
+ Modifier.onGloballyPositioned {
+ gridSize.value = it.size
+ latch.countDown()
+ },
+ ) {
+ repeat(itemCount) { Box(Modifier.size(itemSizeDp)) }
+ }
+ }
+
+ assertTrue(latch.await(3, TimeUnit.SECONDS))
+
+ assertEquals(10 * itemSize, gridSize.value?.width)
+ assertEquals(expectedHeight, gridSize.value?.height)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun testGrid_invalidIndices_throws() {
+ show {
+ Grid(
+ config = {
+ column(GridTrackSize.Fixed(10.dp))
+ row(GridTrackSize.Fixed(10.dp))
+ }
+ ) {
+ Box(Modifier.gridItem(100000, 1)) // Out of bounds
+ }
+ }
+ }
+
+ @Composable
+ private fun IntrinsicItem(
+ minWidth: Int,
+ minIntrinsicWidth: Int,
+ maxIntrinsicWidth: Int,
+ modifier: Modifier = Modifier,
+ ) {
+ Layout(
+ modifier,
+ measurePolicy =
+ object : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List,
+ constraints: Constraints,
+ ): MeasureResult {
+ return layout(
+ constraints.minWidth.coerceAtLeast(minWidth),
+ constraints.minHeight,
+ ) {}
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurables: List,
+ height: Int,
+ ) = minIntrinsicWidth
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurables: List,
+ height: Int,
+ ) = maxIntrinsicWidth
+ },
+ )
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ExperimentalGridApi.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ExperimentalGridApi.kt
new file mode 100644
index 0000000000000..3e9e85b50c624
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ExperimentalGridApi.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2026 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.foundation.layout
+
+@RequiresOptIn(
+ "This foundation layout API is experimental and is likely to change or be removed in the future."
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class ExperimentalGridApi
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Grid.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Grid.kt
new file mode 100644
index 0000000000000..6803cf2809d90
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Grid.kt
@@ -0,0 +1,1823 @@
+/*
+ * Copyright 2026 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.
+ */
+
+@file:OptIn(ExperimentalGridApi::class)
+
+package androidx.compose.foundation.layout
+
+import androidx.annotation.FloatRange
+import androidx.collection.LongList
+import androidx.collection.MutableIntSet
+import androidx.collection.MutableObjectList
+import androidx.collection.mutableLongListOf
+import androidx.compose.foundation.layout.GridScope.Companion.GridIndexUnspecified
+import androidx.compose.foundation.layout.GridScope.Companion.MaxGridIndex
+import androidx.compose.foundation.layout.internal.JvmDefaultWithCompatibility
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ParentDataModifierNode
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastForEach
+import kotlin.jvm.JvmInline
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+/**
+ * A 2D layout composable that arranges children into a grid of rows and columns.
+ *
+ * The [Grid] allows defining explicit tracks (columns and rows) with various sizing capabilities,
+ * including fixed sizes (`dp`), flexible fractions (`fr`), percentages, and content-based sizing
+ * (`Auto`).
+ *
+ * **Key Features:**
+ * * **Explicit vs. Implicit:** You define the main structure via [config] (explicit tracks). If
+ * items are placed outside these defined bounds, or if auto-placement creates new rows/columns,
+ * the grid automatically extends using implicit sizing (defaults to `Auto`).
+ * * **Flexible Sizing:** Use [Fr] units (e.g., `1.fr`, `2.fr`) to distribute available space
+ * proportionally among tracks.
+ * * **Auto-placement:** Items without a specific [GridScope.gridItem] modifier flow automatically
+ * into the next available cell based on the configured [GridFlow]. .
+ *
+ * @param config A block that defines the columns, rows, and gaps of the grid. This block runs
+ * during the measure pass, enabling efficient updates based on state.
+ * @param modifier The modifier to be applied to the layout.
+ * @param content The content of the grid. Direct children can use [GridScope.gridItem] to configure
+ * their position and span.
+ * @see GridScope.gridItem
+ * @see GridConfigurationScope
+ */
+@Composable
+@ExperimentalGridApi
+inline fun Grid(
+ noinline config: GridConfigurationScope.() -> Unit,
+ modifier: Modifier = Modifier,
+ content: @Composable GridScope.() -> Unit,
+) {
+ // Capture the latest config lambda in a State object.
+ // This ensures we always have access to the latest lambda without recreating the policy.
+ val currentConfig = rememberUpdatedState(config)
+
+ // Create a stable MeasurePolicy instance.
+ // We use 'remember' without keys so the policy instance itself never changes.
+ // The policy reads 'currentConfig.value' inside measure(), triggering invalidation
+ // when the config changes.
+ val measurePolicy = remember { GridMeasurePolicy(currentConfig) }
+
+ Layout(
+ content = { GridScopeInstance.content() },
+ modifier = modifier,
+ measurePolicy = measurePolicy,
+ )
+}
+
+/** Scope for the children of [Grid]. */
+@LayoutScopeMarker
+@Immutable
+@JvmDefaultWithCompatibility
+@ExperimentalGridApi
+interface GridScope {
+ /**
+ * Configures the position, span, and alignment of an element within a [Grid] layout.
+ *
+ * Apply this modifier to direct children of a [Grid] composable.
+ *
+ * **Default Behavior:** If this modifier is not applied to a child, the child will be
+ * automatically placed in the next available cell (spanning 1 row and 1 column) according to
+ * the configured [GridFlow].
+ *
+ * **Indexing:** Grid row and column indices are **1-based**.
+ * * **Positive** values count from the start (1 is the first row/column).
+ * * **Negative** values count from the end (-1 is the last explicitly defined row/column).
+ *
+ * **Auto-placement:** If [row] or [column] are left to their default value
+ * ([GridIndexUnspecified]), the [Grid] layout will automatically place the item based on the
+ * configured [GridFlow].
+ *
+ * @param row The specific 1-based row index to place the item in. Positive values count from
+ * the start (1 is the first row). Negative values count from the end (-1 is the last row).
+ * Must be within the range [-[MaxGridIndex], [MaxGridIndex]]. Defaults to
+ * [GridIndexUnspecified] for auto-placement.
+ * @param column The specific 1-based column index to place the item in. Positive values count
+ * from the start (1 is the first column). Negative values count from the end (-1 is the last
+ * column). Must be within the range [-[MaxGridIndex], [MaxGridIndex]]. Defaults to
+ * [GridIndexUnspecified] for auto-placement.
+ * @param rowSpan The number of rows this item should occupy. Must be greater than 0. Defaults
+ * to 1.
+ * @param columnSpan The number of columns this item should occupy. Must be greater than 0.
+ * Defaults to 1.
+ * @param alignment Specifies how the content should be aligned within the grid cell(s) it
+ * occupies. Defaults to [Alignment.TopStart].
+ * @throws IllegalArgumentException if [row] or [column] (when specified) are outside the valid
+ * range, or if [rowSpan] or [columnSpan] are less than 1.
+ * @see GridIndexUnspecified
+ * @see MaxGridIndex
+ */
+ @Stable
+ fun Modifier.gridItem(
+ row: Int = GridIndexUnspecified,
+ column: Int = GridIndexUnspecified,
+ rowSpan: Int = 1,
+ columnSpan: Int = 1,
+ alignment: Alignment = Alignment.TopStart,
+ ): Modifier
+
+ /**
+ * Configures the position, span, and alignment of an element within a [Grid] layout using
+ * ranges.
+ *
+ * This convenience overload converts [IntRange] inputs into row/column indices and spans.
+ *
+ * **Equivalence:**
+ * - `rows = 4..5` maps to `row = 4`, `rowSpan = 2`.
+ * - `columns = 1..1` maps to `column = 1`, `columnSpan = 1`.
+ *
+ * Example: `Modifier.gridItem(rows = 2..3, columns = 1..2)` is functionally equivalent to
+ * `Modifier.gridItem(row = 2, rowSpan = 2, column = 1, columnSpan = 2)`.
+ *
+ * @param rows The range of rows to occupy (e.g., `1..2`). The start determines the row index,
+ * and the size of the range determines the span.
+ * @param columns The range of columns to occupy (e.g., `1..3`). The start determines the column
+ * index, and the size of the range determines the span.
+ * @param alignment Specifies how the content should be aligned within the grid cell(s).
+ * Defaults to [Alignment.TopStart].
+ * @see Modifier.gridItem
+ */
+ @Stable
+ fun Modifier.gridItem(
+ rows: IntRange,
+ columns: IntRange,
+ alignment: Alignment = Alignment.TopStart,
+ ): Modifier
+
+ companion object {
+ /**
+ * The maximum allowed index for a row or column (inclusive).
+ *
+ * This hard limit prevents performance degradation, layout timeouts, or memory issues
+ * potentially caused by accidental loop overflows or unreasonably large sparse grid
+ * definitions.
+ */
+ @ExperimentalGridApi const val MaxGridIndex: Int = 1000
+
+ /**
+ * Sentinel value indicating that a grid position (row or column) is not manually specified
+ * and should be determined automatically by the layout flow.
+ */
+ @ExperimentalGridApi const val GridIndexUnspecified: Int = 0
+ }
+}
+
+/** Internal implementation of [GridScope]. Stateless object to avoid allocations. */
+@PublishedApi
+@ExperimentalGridApi
+internal object GridScopeInstance : GridScope {
+
+ override fun Modifier.gridItem(
+ row: Int,
+ column: Int,
+ rowSpan: Int,
+ columnSpan: Int,
+ alignment: Alignment,
+ ): Modifier {
+ if (row != GridIndexUnspecified) {
+ require(row in -MaxGridIndex..MaxGridIndex) {
+ "row must be between -$MaxGridIndex and $MaxGridIndex"
+ }
+ }
+ if (column != GridIndexUnspecified) {
+ require(column in -MaxGridIndex..MaxGridIndex) {
+ "column must be between -$MaxGridIndex and $MaxGridIndex"
+ }
+ }
+ require(rowSpan > 0) { "rowSpan must be > 0" }
+ require(columnSpan > 0) { "columnSpan must be > 0" }
+ return this.then(GridItemElement(row, column, rowSpan, columnSpan, alignment))
+ }
+
+ override fun Modifier.gridItem(
+ rows: IntRange,
+ columns: IntRange,
+ alignment: Alignment,
+ ): Modifier {
+ require(!rows.isEmpty()) { "Row range ($rows) cannot be empty" }
+ require(!columns.isEmpty()) { "Column range ($columns) cannot be empty" }
+
+ val row = rows.first
+ val rowSpan = rows.last - rows.first + 1
+ val column = columns.first
+ val columnSpan = columns.last - columns.first + 1
+ return this.gridItem(row, column, rowSpan, columnSpan, alignment)
+ }
+}
+
+/**
+ * Scope for configuring the structure of a [Grid].
+ *
+ * This interface is implemented by the configuration block in [Grid]. It allows defining columns,
+ * rows, and gaps.
+ */
+@LayoutScopeMarker
+@ExperimentalGridApi
+interface GridConfigurationScope : Density {
+
+ /**
+ * The direction in which items that do not specify a position are placed. Defaults to
+ * [GridFlow.Row].
+ */
+ var flow: GridFlow
+
+ /** Defines a fixed-width column. Maps to [GridTrackSize.Fixed]. */
+ fun column(size: Dp)
+
+ /** Defines a flexible column. Maps to [GridTrackSize.Flex]. */
+ fun column(weight: Fr)
+
+ /** Defines a percentage-based column. Maps to [GridTrackSize.Percentage]. */
+ fun column(percentage: Float)
+
+ /** Defines a new column track with the specified [size]. */
+ fun column(size: GridTrackSize)
+
+ /** Defines a fixed-width row. Maps to [GridTrackSize.Fixed]. */
+ fun row(size: Dp)
+
+ /** Defines a flexible row. Maps to [GridTrackSize.Flex]. */
+ fun row(weight: Fr)
+
+ /** Defines a percentage-based row. Maps to [GridTrackSize.Percentage]. */
+ fun row(percentage: Float)
+
+ /** Defines a new row track with the specified [size]. */
+ fun row(size: GridTrackSize)
+
+ /**
+ * Sets both the row and column gaps (gutters) to [all].
+ *
+ * **Precedence:** If this is called multiple times, or mixed with [columnGap] or [rowGap], the
+ * **last call** takes precedence.
+ *
+ * @throws IllegalArgumentException if [all] is negative.
+ */
+ fun gap(all: Dp)
+
+ /**
+ * Sets independent gaps for rows and columns.
+ *
+ * **Precedence:** If this is called multiple times, or mixed with [columnGap] or [rowGap], the
+ * **last call** takes precedence.
+ *
+ * @throws IllegalArgumentException if [row] or [column] is negative.
+ */
+ fun gap(row: Dp, column: Dp)
+
+ /**
+ * Sets the gap (gutter) size between columns.
+ *
+ * **Precedence:** If this is called multiple times, the **last call** takes precedence. This
+ * call will overwrite the column component of any previous [gap] call.
+ *
+ * @throws IllegalArgumentException if [gap] is negative.
+ */
+ fun columnGap(gap: Dp)
+
+ /**
+ * Sets the gap (gutter) size between rows.
+ *
+ * **Precedence:** If this is called multiple times, the **last call** takes precedence. This
+ * call will overwrite the row component of any previous [gap] call.
+ *
+ * @throws IllegalArgumentException if [gap] is negative.
+ */
+ fun rowGap(gap: Dp)
+
+ /** Creates an [Fr] unit from an [Int]. */
+ @Stable
+ @ExperimentalGridApi
+ val Int.fr: Fr
+ get() = Fr(this.toFloat())
+
+ /** Creates an [Fr] unit from a [Float]. */
+ @Stable
+ @ExperimentalGridApi
+ val Float.fr: Fr
+ get() = Fr(this)
+
+ /** Creates an [Fr] unit from a [Double]. */
+ @Stable
+ @ExperimentalGridApi
+ val Double.fr: Fr
+ get() = Fr(this.toFloat())
+}
+
+/** Adds multiple columns with the specified [specs]. */
+@ExperimentalGridApi
+fun GridConfigurationScope.columns(vararg specs: GridTrackSpec) {
+ for (spec in specs) {
+ if (spec is GridTrackSize) {
+ column(spec)
+ }
+ }
+}
+
+/** Adds multiple rows with the specified [specs]. */
+@ExperimentalGridApi
+fun GridConfigurationScope.rows(vararg specs: GridTrackSpec) {
+ for (spec in specs) {
+ if (spec is GridTrackSize) {
+ row(spec)
+ }
+ }
+}
+
+/** Defines the direction in which auto-placed items flow within the grid. */
+@JvmInline
+@ExperimentalGridApi
+value class GridFlow @PublishedApi internal constructor(private val bits: Int) {
+
+ companion object {
+ /** Items are placed filling the first row, then moving to the next row. */
+ @ExperimentalGridApi
+ inline val Row
+ get() = GridFlow(0)
+
+ /** Items are placed filling the first column, then moving to the next column. */
+ @ExperimentalGridApi
+ inline val Column
+ get() = GridFlow(1)
+ }
+
+ override fun toString(): String =
+ when (this) {
+ Row -> "Row"
+ Column -> "Column"
+ else -> "GridFlow($bits)"
+ }
+}
+
+/**
+ * Represents a flexible unit used for sizing [Grid] tracks.
+ *
+ * One [Fr] unit represents a fraction of the *remaining* space in the grid container after
+ * [GridTrackSize.Fixed] and [GridTrackSize.Percentage] tracks have been allocated.
+ */
+@JvmInline
+@ExperimentalGridApi
+value class Fr(val value: Float) {
+ override fun toString(): String = "$value.fr"
+}
+
+/**
+ * Marker interface to enable vararg usage with [GridTrackSize].
+ *
+ * This allows the configuration DSL to accept [GridTrackSize] items in a vararg (e.g.,
+ * `columns(Fixed(10.dp), Flex(1.fr))`), bypassing the Kotlin limitation on value class varargs.
+ */
+@ExperimentalGridApi sealed interface GridTrackSpec
+
+/**
+ * Defines the size of a track (a row or a column) in a [Grid].
+ *
+ * Use the companion functions (e.g., [Fixed], [Flex]) to create instances.
+ */
+@Immutable
+@JvmInline
+@ExperimentalGridApi
+value class GridTrackSize internal constructor(internal val encodedValue: Long) : GridTrackSpec {
+
+ internal val type: Int
+ get() = (encodedValue ushr 32).toInt()
+
+ internal val value: Float
+ get() = Float.fromBits(encodedValue.toInt())
+
+ override fun toString(): String =
+ when (type) {
+ TypeFixed -> "Fixed(${value}dp)"
+ TypePercentage -> "Percentage($value)"
+ TypeFlex -> "Flex(${value}fr)"
+ TypeMinContent -> "MinContent"
+ TypeMaxContent -> "MaxContent"
+ TypeAuto -> "Auto"
+ else -> "Unknown"
+ }
+
+ companion object {
+ internal const val TypeFixed = 1
+ internal const val TypePercentage = 2
+ internal const val TypeFlex = 3
+ internal const val TypeMinContent = 4
+ internal const val TypeMaxContent = 5
+ internal const val TypeAuto = 6
+
+ /**
+ * A track with a fixed [Dp] size.
+ *
+ * @param size The size of the track.
+ * @throws IllegalArgumentException if [size] is negative or [Dp.Unspecified].
+ */
+ @Stable
+ fun Fixed(size: Dp): GridTrackSize {
+ require(size != Dp.Unspecified && size.value >= 0f) {
+ "Fixed size must be non-negative and specified (was $size)"
+ }
+ return pack(TypeFixed, size.value)
+ }
+
+ /**
+ * A track sized as a percentage of the **total** available size of the grid container.
+ * **Note:** In this implementation, percentages are calculated based on the **remaining
+ * available space after gaps**. This differs from the W3C CSS Grid spec, where percentages
+ * are based on the container size regardless of gaps. This behavior prevents unexpected
+ * overflows when mixing gaps and percentages (e.g., `50%` + `50%` + `gap` will fit
+ * perfectly here, but would overflow in CSS).
+ *
+ * @param value The percentage of the container size.
+ * @throws IllegalArgumentException if [value] is negative.
+ */
+ @Stable
+ fun Percentage(@FloatRange(from = 0.0) value: Float): GridTrackSize {
+ require(value >= 0f) { "Percentage cannot be negative" }
+ return pack(TypePercentage, value)
+ }
+
+ /**
+ * A flexible track that takes a share of the **remaining** space after Fixed and Percentage
+ * tracks are allocated.
+ *
+ * @param weight The flexible weight. Space is distributed proportional to this weight
+ * divided by the total flex weight. Must be non-negative.
+ * @throws IllegalArgumentException if [weight] is negative.
+ */
+ @Stable
+ fun Flex(@FloatRange(from = 0.0) weight: Fr): GridTrackSize {
+ require(weight.value >= 0f) { "Flex weight must be positive" }
+ return pack(TypeFlex, weight.value)
+ }
+
+ /** A track that sizes itself to fit the minimum intrinsic size of its contents. */
+ @Stable val MinContent = pack(TypeMinContent, 0f)
+
+ /** A track that sizes itself to fit the maximum intrinsic size of its contents. */
+ @Stable val MaxContent = pack(TypeMaxContent, 0f)
+
+ /**
+ * A track that behaves automatically, typically similar to [MinContent] or [Flex] depending
+ * on context.
+ */
+ @Stable val Auto = pack(TypeAuto, 0f)
+
+ private fun pack(type: Int, value: Float): GridTrackSize {
+ // Pack Type (High 32) and Float bits (Low 32) into one Long.
+ // Mask 0xFFFFFFFFL prevents sign extension when casting int to long.
+ val raw = (type.toLong() shl 32) or (value.toRawBits().toLong() and 0xFFFFFFFFL)
+ return GridTrackSize(raw)
+ }
+ }
+}
+
+/**
+ * The modifier element that creates and updates [GridItemNode].
+ *
+ * @property row The 1-based row index, or [GridScope.GridIndexUnspecified] for auto-placement.
+ * @property column The 1-based column index, or [GridScope.GridIndexUnspecified] for
+ * auto-placement.
+ * @property rowSpan The number of rows the item should occupy.
+ * @property columnSpan The number of columns the item should occupy.
+ * @property alignment The alignment of the content within the grid cell.
+ * @see GridItemNode
+ */
+private class GridItemElement(
+ val row: Int,
+ val column: Int,
+ val rowSpan: Int,
+ val columnSpan: Int,
+ val alignment: Alignment,
+) : ModifierNodeElement() {
+ override fun create(): GridItemNode = GridItemNode(row, column, rowSpan, columnSpan, alignment)
+
+ override fun update(node: GridItemNode) {
+ node.row = row
+ node.column = column
+ node.rowSpan = rowSpan
+ node.columnSpan = columnSpan
+ node.alignment = alignment
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "gridItem"
+ properties["row"] = row
+ properties["column"] = column
+ properties["rowSpan"] = rowSpan
+ properties["columnSpan"] = columnSpan
+ properties["alignment"] = alignment
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is GridItemElement) return false
+
+ if (row != other.row) return false
+ if (column != other.column) return false
+ if (rowSpan != other.rowSpan) return false
+ if (columnSpan != other.columnSpan) return false
+ if (alignment != other.alignment) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = row
+ result = 31 * result + column
+ result = 31 * result + rowSpan
+ result = 31 * result + columnSpan
+ result = 31 * result + alignment.hashCode()
+ return result
+ }
+}
+
+/**
+ * The modifier node that provides parent data to the [Grid] layout.
+ *
+ * This class implements [ParentDataModifierNode], allowing the parent [Grid] layout to inspect the
+ * configuration (row, column, spans) of this specific child during the measurement phase via the
+ * [modifyParentData] method.
+ *
+ * @property row The 1-based row index, or [GridScope.GridIndexUnspecified] for auto-placement.
+ * @property column The 1-based column index, or [GridScope.GridIndexUnspecified] for
+ * auto-placement.
+ * @property rowSpan The number of rows the item should occupy.
+ * @property columnSpan The number of columns the item should occupy.
+ * @property alignment The alignment of the content within the grid cell.
+ * @throws IllegalArgumentException if [rows] or [columns] ranges are empty, or if the derived
+ * row/column indices or spans do not meet the requirements of the primary [gridItem] function.
+ * @see GridScope.gridItem for the public API and input validation.
+ */
+private class GridItemNode(
+ var row: Int,
+ var column: Int,
+ var rowSpan: Int,
+ var columnSpan: Int,
+ var alignment: Alignment,
+) : Modifier.Node(), ParentDataModifierNode {
+ override fun Density.modifyParentData(parentData: Any?) = this@GridItemNode
+}
+
+/** A stable MeasurePolicy that reads configuration from a State. */
+@PublishedApi
+@ExperimentalGridApi
+internal class GridMeasurePolicy(
+ private val configState: State Unit>
+) : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List,
+ constraints: Constraints,
+ ): MeasureResult {
+ // 1. Run Configuration DSL
+ val gridConfig = GridConfigurationScopeImpl(this).apply(configState.value)
+
+ // 2. Resolve Grid Item Indices (Resolve explicit and Auto placement)
+ // This calculates the concrete index (row, col) for every item and determines total grid
+ // size.
+ val resolvedGridItemsResult =
+ resolveGridItemIndices(
+ measurables = measurables,
+ columnSpecs = gridConfig.columnSpecs,
+ rowSpecs = gridConfig.rowSpecs,
+ flow = gridConfig.flow,
+ )
+
+ // 3. Resolve Track Sizes
+ val trackSizes =
+ calculateGridTrackSizes(
+ density = this,
+ gridItems = resolvedGridItemsResult.gridItems,
+ columnSpecs = gridConfig.columnSpecs,
+ rowSpecs = gridConfig.rowSpecs,
+ totalColCount = resolvedGridItemsResult.gridSize.width,
+ totalRowCount = resolvedGridItemsResult.gridSize.height,
+ columnGap = gridConfig.columnGap,
+ rowGap = gridConfig.rowGap,
+ constraints = constraints,
+ )
+
+ // 4. Measure Children
+ // Measures content constraints based on track sizes and mutates GridItem with result.
+ measureItems(
+ gridItems = resolvedGridItemsResult.gridItems,
+ trackSizes = trackSizes,
+ layoutDirection = layoutDirection,
+ )
+
+ // 5. Layout
+ // Coerce the final size within constraints.
+ // If content is larger, it will overflow (report Max).
+ // If content is smaller than Min, it will expand (report Min).
+ val layoutWidth = constraints.constrainWidth(trackSizes.totalWidth)
+ val layoutHeight = constraints.constrainHeight(trackSizes.totalHeight)
+ return layout(layoutWidth, layoutHeight) {
+ val columnOffsets =
+ calculateTrackOffsets(trackSizes.columnWidths, trackSizes.columnGapPx)
+ val rowOffsets = calculateTrackOffsets(trackSizes.rowHeights, trackSizes.rowGapPx)
+ resolvedGridItemsResult.gridItems.forEach { gridItem ->
+ val placeable = gridItem.placeable
+ // Only place if measurement succeeded (guard against edge cases)
+ if (placeable != null) {
+ val x = columnOffsets[gridItem.column] + gridItem.offsetX
+ val y = rowOffsets[gridItem.row] + gridItem.offsetY
+ placeable.place(x, y)
+ }
+ }
+ }
+ }
+}
+
+private class GridConfigurationScopeImpl(density: Density) :
+ GridConfigurationScope, Density by density {
+ val columnSpecs = mutableLongListOf()
+ val rowSpecs = mutableLongListOf()
+ var columnGap: Dp = 0.dp
+ var rowGap: Dp = 0.dp
+
+ override var flow: GridFlow = GridFlow.Row
+
+ override fun column(size: Dp) {
+ column(GridTrackSize.Fixed(size))
+ }
+
+ override fun column(weight: Fr) {
+ column(GridTrackSize.Flex(weight))
+ }
+
+ override fun column(percentage: Float) {
+ column(GridTrackSize.Percentage(percentage))
+ }
+
+ override fun column(size: GridTrackSize) {
+ columnSpecs.add(size.encodedValue)
+ }
+
+ override fun row(size: Dp) {
+ row(GridTrackSize.Fixed(size))
+ }
+
+ override fun row(weight: Fr) {
+ row(GridTrackSize.Flex(weight))
+ }
+
+ override fun row(percentage: Float) {
+ row(GridTrackSize.Percentage(percentage))
+ }
+
+ override fun row(size: GridTrackSize) {
+ rowSpecs.add(size.encodedValue)
+ }
+
+ override fun gap(all: Dp) {
+ require(all.value >= 0f) { "Gap must be non-negative" }
+ columnGap = all
+ rowGap = all
+ }
+
+ override fun gap(row: Dp, column: Dp) {
+ require(row.value >= 0f) { "Row gap must be non-negative" }
+ require(column.value >= 0f) { "Column gap must be non-negative" }
+ rowGap = row
+ columnGap = column
+ }
+
+ override fun columnGap(gap: Dp) {
+ require(gap.value >= 0f) { "Column gap must be non-negative" }
+ columnGap = gap
+ }
+
+ override fun rowGap(gap: Dp) {
+ require(gap.value >= 0f) { "Row gap must be non-negative" }
+ rowGap = gap
+ }
+}
+
+/**
+ * A mutable state object representing a single child in the Grid throughout the layout lifecycle.
+ *
+ * This object is created once during the [resolveGridItemIndices] phase (containing only placement
+ * info) and is reused and mutated during the [measureItems] phase to store the resulting
+ * [Placeable] and calculation offsets. This significantly reduces object allocation per layout
+ * pass.
+ */
+private class GridItem(
+ val measurable: Measurable,
+ var row: Int,
+ var column: Int,
+ var rowSpan: Int,
+ var columnSpan: Int,
+ val alignment: Alignment,
+ var placeable: Placeable? = null,
+ var offsetX: Int = 0,
+ var offsetY: Int = 0,
+)
+
+/**
+ * The output of the [resolveGridItemIndices] algorithm.
+ *
+ * This container holds the complete layout plan required for subsequent measurement phases. It
+ * encapsulates both the individual item positions and the aggregate dimensions of the grid.
+ *
+ * The [gridSize] is critical because it reveals the extent of the "Implicit Grid" — tracks that
+ * were not explicitly defined by the user but were created automatically to accommodate auto-placed
+ * items or items with out-of-bounds indices.
+ *
+ * @property gridItems The list of all items with their resolved (row, column) coordinates.
+ * @property gridSize The total number of rows and columns required to house all items. (width =
+ * total columns, height = total rows).
+ */
+private class ResolvedGridItemIndicesResult(
+ val gridItems: MutableObjectList,
+ val gridSize: IntSize,
+)
+
+/**
+ * The "Master Blueprint" holding the calculated pixel dimensions for the entire grid.
+ *
+ * This class acts as a lookup table during the measurement phase. Instead of recalculating sizes
+ * for every item, we compute the track sizes once and pass this object around.
+ *
+ * @property columnWidths Array containing the exact width in pixels for each column index.
+ * @property rowHeights Array containing the exact height in pixels for each row index.
+ * @property totalWidth The sum of all column widths plus gaps.
+ * @property totalHeight The sum of all row heights plus gaps.
+ * @property columnGapPx The spacing between columns.
+ * @property rowGapPx The spacing between rows.
+ */
+private class GridTrackSizes(
+ val columnWidths: IntArray,
+ val rowHeights: IntArray,
+ val totalWidth: Int,
+ val totalHeight: Int,
+ val columnGapPx: Int,
+ val rowGapPx: Int,
+)
+
+/**
+ * Executes the "Sparse Packing" auto-placement algorithm to resolve every item's position.
+ *
+ * This function is the "Engine" of the auto-placement logic. It transforms a list of raw
+ * measurables (with potentially unspecified `row`/`column` values) into a concrete plan where every
+ * item has a specific (row, column) coordinate.
+ *
+ * **Algorithm Overview:**
+ * 1. **Explicit Placement:** Items with both `row` and `column` manually specified are placed
+ * first. They anchor the grid and do not move.
+ * 2. **Auto-Placement Cursor:** A "cursor" (current row/column pointer) tracks the next available
+ * position.
+ * 3. **Filling Gaps:** The algorithm iterates through the remaining items. For each item:
+ * - It advances the cursor to the first slot that can accommodate the item's span without
+ * overlapping existing items.
+ * - It respects the [flow] direction (Row-major vs Column-major).
+ * - It creates "Implicit Tracks" (expanding the grid bounds) if an item is placed outside the
+ * currently defined area.
+ *
+ * @param measurables The raw list of children to place.
+ * @param columnSpecs The explicit column definitions (used to determine wrapping points).
+ * @param rowSpecs The explicit row definitions (used to determine wrapping points).
+ * @param flow The direction ([GridFlow.Row] or [GridFlow.Column]) to fill the grid.
+ * @return A [ResolvedGridItemIndicesResult] containing the final positions and the *total* grid
+ * dimensions (Explicit + Implicit).
+ */
+private fun resolveGridItemIndices(
+ measurables: List,
+ columnSpecs: LongList,
+ rowSpecs: LongList,
+ flow: GridFlow,
+): ResolvedGridItemIndicesResult {
+ val gridItems = MutableObjectList(measurables.size)
+
+ // Key = (row shl 16) | (column & 0xFFFF)
+ // Supports up to 65,535 rows/cols (well within MaxGridIndex).
+ val occupiedCells = MutableIntSet()
+
+ val explicitColCount = columnSpecs.size
+ val explicitRowCount = rowSpecs.size
+
+ // Track the effective size of the grid (starts at explicit size, expands if items are placed
+ // outside)
+ var maxRow = explicitRowCount
+ var maxCol = explicitColCount
+
+ // Pack into Int (Row in high 16 bits, Col in low 16 bits)
+ // Supports up to 65,535 rows/cols.
+ fun packCoordinate(row: Int, column: Int): Int = (row shl 16) or (column and 0xFFFF)
+
+ // Checks if the target area (defined by start position and span) overlaps with any existing
+ // item.
+ fun isAreaOccupied(startRow: Int, startCol: Int, rowSpan: Int, colSpan: Int): Boolean {
+ // Fast-path: Check boundary limits first
+ if (startRow + rowSpan > MaxGridIndex || startCol + colSpan > MaxGridIndex) return true
+ for (r in startRow until startRow + rowSpan) {
+ for (c in startCol until startCol + colSpan) {
+ if (occupiedCells.contains(packCoordinate(r, c))) return true
+ }
+ }
+ return false
+ }
+
+ // Marks the cells in the area as occupied.
+ fun markAreaOccupied(startRow: Int, startCol: Int, rowSpan: Int, colSpan: Int) {
+ for (r in startRow until startRow + rowSpan) {
+ for (c in startCol until startCol + colSpan) {
+ occupiedCells.add(packCoordinate(r, c))
+ }
+ }
+ }
+
+ // The "Cursor" tracks the position of the last auto-placed item.
+ // Subsequent auto-placed items attempt to start searching from here to avoid re-scanning the
+ // whole grid.
+ var autoPlacementCursorRow = 0
+ var autoPlacementCursorCol = 0
+
+ measurables.fastForEach { measurable ->
+ val data = measurable.parentData as? GridItemNode
+ val rowSpan = data?.rowSpan ?: 1
+ val colSpan = data?.columnSpan ?: 1
+
+ // Convert 1-based user indices to 0-based internal indices.
+ // Returns null if the user index was unspecified (Auto).
+ val requestedRow =
+ resolveToZeroBasedIndex(data?.row ?: GridIndexUnspecified, explicitRowCount)
+ val requestedCol =
+ resolveToZeroBasedIndex(data?.column ?: GridIndexUnspecified, explicitColCount)
+
+ var finalRow = -1
+ var finalCol = -1
+
+ // 1. Fully Explicit (Row & Column fixed)
+ // We simply place it there. Overlaps are allowed for explicit placement.
+ if (requestedRow != -1 && requestedCol != -1) {
+ finalRow = requestedRow
+ finalCol = requestedCol
+ }
+ // 2. Fixed Row (Search for Column)
+ else if (requestedRow != -1) {
+ // Search for the first available column in the specified row.
+ finalRow = requestedRow
+ var candidateCol = 0
+
+ // If flowing by Row, and we are on the cursor's row, start searching from cursor
+ if (flow == GridFlow.Row && requestedRow == autoPlacementCursorRow) {
+ candidateCol = autoPlacementCursorCol
+ }
+ while (candidateCol < MaxGridIndex) {
+ if (!isAreaOccupied(requestedRow, candidateCol, rowSpan, colSpan)) {
+ finalCol = candidateCol
+ break
+ }
+ candidateCol++
+ }
+ }
+ // 3. Fixed Column (Search for Row)
+ else if (requestedCol != -1) {
+ // Search for the first available row in the specified column.
+ finalCol = requestedCol
+ var candidateRow = 0
+ // If flowing by Column, and we are on the cursor's col, start searching from cursor
+ if (flow == GridFlow.Column && requestedCol == autoPlacementCursorCol) {
+ candidateRow = autoPlacementCursorRow
+ }
+ while (candidateRow < MaxGridIndex) {
+ if (!isAreaOccupied(candidateRow, requestedCol, rowSpan, colSpan)) {
+ finalRow = candidateRow
+ break
+ }
+ candidateRow++
+ }
+ }
+ // 4. Fully Auto (Search for Slot)
+ else {
+ // Start searching from the current cursor position.
+ var candidateRow = autoPlacementCursorRow
+ var candidateCol = autoPlacementCursorCol
+
+ while (candidateRow < MaxGridIndex && candidateCol < MaxGridIndex) {
+ // Wrapping Logic
+ // If the item doesn't fit in the current track (explicit bounds), wrap to next.
+ if (flow == GridFlow.Row) {
+ // If we have explicit columns and exceed them...
+ if (explicitColCount > 0 && candidateCol + colSpan > explicitColCount) {
+ // If we are NOT at start, wrap.
+ // If we ARE at start (0) and still don't fit, we must overflow (create
+ // implicit track).
+ if (candidateCol > 0) {
+ candidateCol = 0
+ candidateRow++
+ continue // Re-evaluate wrapping at new position
+ }
+ }
+ } else { // GridFlow.Column
+ if (explicitRowCount > 0 && candidateRow + rowSpan > explicitRowCount) {
+ if (candidateRow > 0) {
+ candidateRow = 0
+ candidateCol++
+ continue
+ }
+ }
+ }
+
+ if (!isAreaOccupied(candidateRow, candidateCol, rowSpan, colSpan)) {
+ finalRow = candidateRow
+ finalCol = candidateCol
+ break
+ }
+
+ // Increment
+ if (flow == GridFlow.Row) {
+ candidateCol++
+ // If we drift too far right without wrapping (infinite grid), force wrap safety
+ if (candidateCol > MaxGridIndex) {
+ candidateCol = 0
+ candidateRow++
+ }
+ } else {
+ candidateRow++
+ if (candidateRow > MaxGridIndex) {
+ candidateRow = 0
+ candidateCol++
+ }
+ }
+ }
+ }
+
+ // If auto-placement failed to find a spot (e.g. MaxGridIndex reached),
+ // we default to 0,0 to avoid crashing, though visual overlap will occur.
+ val placementRow = max(0, finalRow)
+ val placementCol = max(0, finalCol)
+
+ markAreaOccupied(placementRow, placementCol, rowSpan, colSpan)
+
+ // Populate the mutable GridItem
+ gridItems.add(
+ GridItem(
+ measurable = measurable,
+ row = placementRow,
+ column = placementCol,
+ rowSpan = rowSpan,
+ columnSpan = colSpan,
+ alignment = data?.alignment ?: Alignment.TopStart,
+ )
+ )
+
+ // Expand total grid bounds if necessary
+ maxRow = max(maxRow, placementRow + rowSpan)
+ maxCol = max(maxCol, placementCol + colSpan)
+
+ // Update Cursor (Only for non-explicit placements)
+ // Only update cursor if the item was NOT fully explicit.
+ // Explicit items are "out of flow" and shouldn't drag the cursor with them.
+ if (requestedRow == -1 || requestedCol == -1) {
+ if (flow == GridFlow.Row) {
+ autoPlacementCursorRow = placementRow
+ autoPlacementCursorCol = placementCol + colSpan
+ } else {
+ autoPlacementCursorRow = placementRow + rowSpan
+ autoPlacementCursorCol = placementCol
+ }
+ }
+ }
+
+ return ResolvedGridItemIndicesResult(gridItems, IntSize(maxCol, maxRow))
+}
+
+/**
+ * Resolves a 1-based user index (positive or negative) to a 0-based concrete index.
+ *
+ * @param index The user-provided index (e.g., 1, -1, or [GridIndexUnspecified]).
+ * @param maxCount The number of explicit tracks defined (used for negative index resolution).
+ * @return The 0-based index, or -1 if the index was unspecified or invalid (e.g. negative index out
+ * of bounds).
+ */
+private fun resolveToZeroBasedIndex(index: Int, maxCount: Int): Int {
+ if (index == GridIndexUnspecified) return -1
+
+ // Positive Index (e.g., 5): Maps to 4.
+ // Always valid (allows creating implicit tracks if > maxCount).
+ if (index > 0) return index - 1
+
+ // Negative Index (e.g., -1): Maps to maxCount - 1.
+ // Must check if it points to a valid explicit track [0..maxCount-1].
+ // If it points before 0 (e.g. -5 in a 2-row grid), it is invalid.
+ val resolved = maxCount + index
+ return if (resolved >= 0) resolved else -1
+}
+
+/**
+ * Resolves the abstract [GridTrackSize] specifications for all rows and columns into concrete pixel
+ * dimensions. This function is the core of the size calculation logic for the [Grid].
+ *
+ * **Calculation Order:** The calculation is performed in two main phases:
+ * 1. **Column Widths:** Column widths are calculated first. This is crucial because the height of
+ * many UI elements (like text) depends on the available width.
+ * 2. **Row Heights:** Row heights are calculated second, utilizing the resolved column widths to
+ * accurately measure items, especially those with content that wraps.
+ *
+ * **Track Type Resolution:** Within each phase, different [GridTrackSize] types are resolved as
+ * follows:
+ * - [GridTrackSize.Fixed]: Converted directly to pixels using the [density].
+ * - [GridTrackSize.Percentage]: Calculated based on the available space for tracks (after
+ * subtracting gaps). Falls back to content-based size (MaxContent) if the available space on that
+ * axis is infinite (e.g., in a scrollable container).
+ * - [GridTrackSize.MinContent], [GridTrackSize.MaxContent], [GridTrackSize.Auto]: Determined by
+ * measuring the intrinsic sizes of the items within the track. `Auto` typically behaves like
+ * `MaxContent`.
+ * - [GridTrackSize.Flex]: Initially sized to their minimum content size. After all other types and
+ * spanning items are accounted for, any remaining space is distributed proportionally among flex
+ * tracks.
+ *
+ * **Implicit Tracks:** Tracks not explicitly defined in [columnSpecs] or [rowSpecs] (i.e., indices
+ * beyond the spec list sizes) are treated as `GridTrackSize.Auto`.
+ *
+ * **Spanning Items:** The function accounts for items spanning multiple tracks, potentially
+ * increasing the sizes of growable tracks ([Auto], [MinContent], [MaxContent], [Flex]) to
+ * accommodate them.
+ *
+ * @param density The current screen density, used for converting Dp to pixels.
+ * @param gridItems The list of all grid items, including their placement and spans.
+ * @param columnSpecs The explicit configurations for columns.
+ * @param rowSpecs The explicit configurations for rows.
+ * @param totalColCount The total number of columns in the grid (explicit + implicit).
+ * @param totalRowCount The total number of rows in the grid (explicit + implicit).
+ * @param constraints The layout constraints from the parent composable.
+ * @param columnGap The spacing in Dp between columns.
+ * @param rowGap The spacing in Dp between rows.
+ * @return A [GridTrackSizes] object containing the calculated pixel sizes for each column and row,
+ * the total grid dimensions, and the gap sizes in pixels.
+ */
+private fun calculateGridTrackSizes(
+ density: Density,
+ gridItems: MutableObjectList,
+ columnSpecs: LongList,
+ rowSpecs: LongList,
+ totalColCount: Int, // Total (Implicit + Explicit)
+ totalRowCount: Int, // Total (Implicit + Explicit)
+ constraints: Constraints,
+ columnGap: Dp,
+ rowGap: Dp,
+): GridTrackSizes {
+ val colGapPx = with(density) { columnGap.roundToPx() }
+ val rowGapPx = with(density) { rowGap.roundToPx() }
+
+ // Group items by track index to avoid O(Tracks * Items) loop
+ // Array of lists, where index corresponds to the column index
+ val itemsByColumn = arrayOfNulls>(totalColCount)
+ // Array of lists, where index corresponds to the row index
+ val itemsByRow = arrayOfNulls>(totalRowCount)
+
+ gridItems.forEach { item ->
+ // Populate Column Lookup
+ if (item.column < totalColCount) {
+ val list =
+ itemsByColumn[item.column]
+ ?: MutableObjectList().also { itemsByColumn[item.column] = it }
+ list.add(item)
+ }
+ // Populate Row Lookup
+ if (item.row < totalRowCount) {
+ val list =
+ itemsByRow[item.row]
+ ?: MutableObjectList().also { itemsByRow[item.row] = it }
+ list.add(item)
+ }
+ }
+
+ // --- Phase 1: Calculate Column Widths ---
+ // Use totalColCount for array size
+ val columnWidths = IntArray(totalColCount)
+ // If constraints are infinite (e.g. horizontal scroll), we pass Infinity.
+ // This triggers the fallback logic in calculateAxisSize (Percentage -> Auto).
+ val availableWidth =
+ if (constraints.hasFixedWidth) constraints.maxWidth else Constraints.Infinity
+
+ val totalTrackWidth =
+ calculateColumnWidths(
+ density = density,
+ explicitSpecs = columnSpecs,
+ totalCount = totalColCount,
+ availableSpace = availableWidth,
+ outSizes = columnWidths,
+ itemsByColumn = itemsByColumn,
+ constraints = constraints,
+ gridItems = gridItems,
+ columnGap = colGapPx,
+ )
+
+ // --- Phase 2: Calculate Row Heights ---
+ val rowHeights = IntArray(totalRowCount)
+ val availableHeight =
+ if (constraints.hasFixedHeight) constraints.maxHeight else Constraints.Infinity
+
+ val totalTrackHeight =
+ calculateRowHeights(
+ density = density,
+ explicitSpecs = rowSpecs,
+ totalCount = totalRowCount,
+ availableSpace = availableHeight,
+ outSizes = rowHeights,
+ itemsByRow = itemsByRow,
+ constraints = constraints,
+ columnWidths = columnWidths,
+ gridItems = gridItems,
+ rowGap = rowGapPx,
+ )
+
+ val totalColumnGap = max(0, columnSpecs.size - 1) * colGapPx
+ val totalRowGap = max(0, rowSpecs.size - 1) * rowGapPx
+
+ return GridTrackSizes(
+ columnWidths = columnWidths,
+ rowHeights = rowHeights,
+ columnGapPx = colGapPx,
+ rowGapPx = rowGapPx,
+ totalWidth = totalTrackWidth + totalColumnGap,
+ totalHeight = totalTrackHeight + totalRowGap,
+ )
+}
+
+/**
+ * Calculates the specific pixel width of every column in the grid.
+ *
+ * This function implements the horizontal axis sizing logic. It resolves column widths based on
+ * explicit configuration, available space, and content intrinsic sizes.
+ *
+ * **Algorithm Overview:**
+ * 1. **Pass 1 (Base Sizes):** Calculates the initial width of each column based on its
+ * [GridTrackSize].
+ * * **Implicit Tracks:** Indices beyond `explicitSpecs` default to [GridTrackSize.Auto].
+ * * **Fixed:** Resolves directly to pixels.
+ * * **Percentage:** Resolves against total available width. Falls back to `Auto` (MaxContent) if
+ * width is infinite (e.g., inside a container made horizontally scrollable with the
+ * `horizontalScroll` modifier).
+ * * **Flex:** Starts at `min-content` size to prevent collapse if content exists.
+ * * **Auto/Content-based:** Measured using the intrinsic width of items in that column.
+ * 2. **Pass 1.5 (Spanning Items):** Increases column widths if an item spanning multiple columns
+ * requires more width than the sum of those columns.
+ * 3. **Pass 2 (Flex Distribution):** Distributes any remaining horizontal space among
+ * [GridTrackSize.Flex] columns according to their weight.
+ *
+ * @param density Used for Dp-to-Px conversion.
+ * @param explicitSpecs The user-defined column configurations.
+ * @param totalCount The total number of columns (explicit + implicit).
+ * @param availableSpace The maximum width available (or [Constraints.Infinity]).
+ * @param outSizes Output array where calculated widths are stored. **Mutated in-place**.
+ * @param itemsByColumn Optimization lookup: List of items starting in each column index.
+ * @param constraints Parent constraints (used for fallback behavior and cross-axis limits).
+ * @param gridItems All items in the grid (used for spanning logic).
+ * @param columnGap The spacing between columns.
+ * @return The total used width in pixels (sum of all column widths).
+ */
+private fun calculateColumnWidths(
+ density: Density,
+ explicitSpecs: LongList,
+ totalCount: Int,
+ availableSpace: Int,
+ outSizes: IntArray,
+ itemsByColumn: Array?>,
+ constraints: Constraints,
+ gridItems: MutableObjectList,
+ columnGap: Int,
+): Int {
+ if (totalCount == 0) return 0
+
+ var totalFlex = 0f
+ // Calculate total space consumed by gaps.
+ // e.g., 3 columns have 2 gaps. (N-1) * gap.
+ val totalGapSpace = (columnGap * (totalCount - 1)).coerceAtLeast(0)
+
+ // Calculate space available for actual tracks (Total - Gaps).
+ // If availableSpace is Infinity, availableTrackSpace value becomes Constraints.Infinity
+ val availableTrackSpace =
+ if (availableSpace == Constraints.Infinity) {
+ Constraints.Infinity
+ } else {
+ (availableSpace - totalGapSpace).coerceAtLeast(0)
+ }
+
+ // Height constraint used when measuring intrinsic width.
+ // Usually Infinity (standard intrinsic measurement), unless parent enforces strict height.
+ val crossAxisAvailable =
+ if (constraints.hasBoundedHeight) constraints.maxHeight else Constraints.Infinity
+
+ // --- Pass 1: Base Sizes (Single-Span Items) ---
+ // Iterate through every column index (both explicit and implicit).
+ for (index in 0 until totalCount) {
+ // If index exceeds explicit specs, treat it as an Implicit Auto track.
+ val specRaw =
+ if (index < explicitSpecs.size) explicitSpecs[index]
+ else GridTrackSize.Auto.encodedValue
+ val spec = GridTrackSize(specRaw)
+
+ val size =
+ when (spec.type) {
+ GridTrackSize.TypeFixed -> with(density) { spec.value.dp.roundToPx() }
+
+ GridTrackSize.TypePercentage -> {
+ if (availableTrackSpace != Constraints.Infinity) {
+ (spec.value * availableTrackSpace).roundToInt()
+ } else {
+ // If the Grid is in a horizontally scrolling container
+ // (infinite width), we cannot calculate a percentage of "Infinity".
+ // We default to 'Auto' (MaxIntrinsic) so the content remains visible.
+ calculateMaxIntrinsicWidth(itemsByColumn[index], crossAxisAvailable)
+ }
+ }
+
+ GridTrackSize.TypeFlex -> {
+ totalFlex += spec.value
+ // Flex tracks start at their 'min-content' size.
+ // This implements `minmax(min-content, fr)`.
+ // It ensures that even if there is no remaining space to distribute,
+ // the column is at least wide enough to show its content.
+ calculateMinIntrinsicWidth(itemsByColumn[index], crossAxisAvailable)
+ }
+
+ GridTrackSize.TypeMinContent ->
+ calculateMinIntrinsicWidth(itemsByColumn[index], crossAxisAvailable)
+ GridTrackSize.TypeMaxContent ->
+ calculateMaxIntrinsicWidth(itemsByColumn[index], crossAxisAvailable)
+ // Auto typically behaves like MaxContent in most contexts (fit the content
+ // comfortably).
+ GridTrackSize.TypeAuto ->
+ calculateMaxIntrinsicWidth(itemsByColumn[index], crossAxisAvailable)
+ // Measure the max intrinsic width of all items in this column.
+ else -> calculateMaxIntrinsicWidth(itemsByColumn[index], crossAxisAvailable)
+ }
+ outSizes[index] = size
+ }
+
+ // --- Pass 1.5: Spanning Items ---
+ // If an item spans 2 columns, and those 2 columns (base sizes) sum to 100px, but the item
+ // is 150px wide, we must grow the columns by 50px.
+ distributeSpanningSpace(
+ explicitSpecs = explicitSpecs,
+ sizes = outSizes,
+ gridItems = gridItems,
+ isRowAxis = false,
+ constraints = constraints,
+ crossAxisSizes = null, // Not needed for column width calculation
+ gap = columnGap,
+ )
+
+ var usedSpace = 0
+ for (size in outSizes) {
+ usedSpace += size
+ }
+
+ // --- Pass 2: Flex Distribution ---
+ // If we have finite width and unused space, distribute it to Flex columns.
+ val remainingSpace =
+ if (availableTrackSpace == Constraints.Infinity) 0
+ else max(0, availableTrackSpace - usedSpace)
+
+ var totalAddedFromFlex = 0
+ if (totalFlex > 0 && remainingSpace > 0) {
+ var distributed = 0
+ var accumulatedFlex = 0f
+
+ for (index in 0 until totalCount) {
+ val specRaw =
+ if (index < explicitSpecs.size) explicitSpecs[index]
+ else GridTrackSize.Auto.encodedValue
+ val spec = GridTrackSize(specRaw)
+
+ if (spec.type == GridTrackSize.TypeFlex) {
+ accumulatedFlex += spec.value
+ // Distribute space proportionally based on weight.
+ // Uses an accumulation algorithm to avoid rounding errors summing to >
+ // remainingSpace.
+ val targetSpace = (accumulatedFlex / totalFlex * remainingSpace).roundToInt()
+ val share = max(0, targetSpace - distributed)
+
+ outSizes[index] += share
+ distributed += share
+ totalAddedFromFlex = distributed
+ }
+ }
+ }
+
+ return usedSpace + totalAddedFromFlex
+}
+
+/**
+ * Calculates the specific pixel height of every row in the grid.
+ *
+ * This function implements the vertical axis sizing logic. Unlike columns (which are usually fixed
+ * or determined by parent width), row heights often depend on the *width* of the content within
+ * them. Therefore, this function **must** be called after [calculateColumnWidths].
+ *
+ * **Algorithm Overview:**
+ * 1. **Pass 1 (Base Sizes):** Calculates the initial height of each row based on its
+ * [GridTrackSize].
+ * * **Implicit Tracks:** Indices beyond `explicitSpecs` default to [GridTrackSize.Auto].
+ * * **Auto/Content-based:** Measured using the pre-calculated `columnWidths`. This ensures text
+ * wraps correctly within its specific cell width.
+ * * **Percentage:** Resolves against total height. Falls back to `Auto` if height is infinite
+ * (e.g., inside a ScrollView).
+ * * **Flex:** Starts at `min-content` size to prevent collapse if content exists.
+ * 2. **Pass 1.5 (Spanning Items):** Increases row heights if an item spanning multiple rows is
+ * taller than the sum of those rows.
+ * 3. **Pass 2 (Flex Distribution):** Distributes any remaining vertical space among
+ * [GridTrackSize.Flex] rows according to their weight.
+ *
+ * @param density Used for Dp-to-Px conversion.
+ * @param explicitSpecs The user-defined row configurations.
+ * @param totalCount The total number of rows (explicit + implicit).
+ * @param availableSpace The maximum height available (or [Constraints.Infinity]).
+ * @param outSizes Output array where calculated heights are stored. **Mutated in-place**.
+ * @param itemsByRow Optimization lookup: List of items starting in each row index.
+ * @param constraints Parent constraints (used for max height limits).
+ * @param columnWidths The resolved widths of columns. **Critical** for measuring text height.
+ * @param gridItems All items in the grid (used for spanning logic).
+ * @param rowGap The spacing between rows.
+ * @return The total used height in pixels (sum of all row heights).
+ */
+private fun calculateRowHeights(
+ density: Density,
+ explicitSpecs: LongList,
+ totalCount: Int,
+ availableSpace: Int,
+ outSizes: IntArray,
+ itemsByRow: Array?>,
+ constraints: Constraints,
+ columnWidths: IntArray,
+ gridItems: MutableObjectList,
+ rowGap: Int,
+): Int {
+ if (totalCount == 0) return 0
+
+ var totalFlex = 0f
+ // Calculate total space consumed by gaps.
+ // e.g., 3 columns have 2 gaps. (N-1) * gap.
+ val totalGapSpace = (rowGap * (totalCount - 1)).coerceAtLeast(0)
+
+ // Calculate space available for actual tracks (Total - Gaps).
+ // If availableSpace is Infinity, availableTrackSpace value becomes Constraints.Infinity
+ val availableTrackSpace =
+ if (availableSpace == Constraints.Infinity) {
+ Constraints.Infinity
+ } else {
+ (availableSpace - totalGapSpace).coerceAtLeast(0)
+ }
+
+ // --- Pass 1: Base Sizes (Single-Span Items) ---
+ // We iterate through every row index (both explicit and implicit).
+ for (index in 0 until totalCount) {
+ // If index exceeds explicit specs, treat it as an Implicit Auto track.
+ val specRaw =
+ if (index < explicitSpecs.size) explicitSpecs[index]
+ else GridTrackSize.Auto.encodedValue
+ val spec = GridTrackSize(specRaw)
+
+ val size =
+ when (spec.type) {
+ GridTrackSize.TypeFixed -> with(density) { spec.value.dp.roundToPx() }
+
+ GridTrackSize.TypePercentage -> {
+ if (availableTrackSpace != Constraints.Infinity) {
+ (spec.value * availableTrackSpace).roundToInt()
+ } else {
+ // If the Grid is in a vertically scrolling container
+ // (infinite height), we cannot calculate a percentage of "Infinity".
+ // We default to 'Auto' (MaxIntrinsic) so the content remains visible.
+ calculateMaxIntrinsicHeight(
+ items = itemsByRow[index],
+ columnWidths = columnWidths,
+ fallbackWidth = constraints.maxWidth,
+ )
+ }
+ }
+
+ GridTrackSize.TypeFlex -> {
+ totalFlex += spec.value
+ // Flex tracks start at their 'min-content' size.
+ // This implements `minmax(min-content, fr)`.
+ // It ensures that even if there is no remaining space to distribute,
+ // the row is at least tall enough to show its content.
+ calculateMinIntrinsicHeight(
+ items = itemsByRow[index],
+ columnWidths = columnWidths,
+ fallbackWidth = constraints.maxWidth,
+ )
+ }
+
+ GridTrackSize.TypeMinContent ->
+ calculateMinIntrinsicHeight(
+ items = itemsByRow[index],
+ columnWidths = columnWidths,
+ fallbackWidth = constraints.maxWidth,
+ )
+ GridTrackSize.TypeMaxContent ->
+ calculateMaxIntrinsicHeight(
+ items = itemsByRow[index],
+ columnWidths = columnWidths,
+ fallbackWidth = constraints.maxWidth,
+ )
+ // Auto typically behaves like MaxContent in most contexts (fit the content
+ // comfortably).
+ GridTrackSize.TypeAuto ->
+ calculateMaxIntrinsicHeight(
+ items = itemsByRow[index],
+ columnWidths = columnWidths,
+ fallbackWidth = constraints.maxWidth,
+ )
+ else ->
+ calculateMaxIntrinsicHeight(
+ items = itemsByRow[index],
+ columnWidths = columnWidths,
+ fallbackWidth = constraints.maxWidth,
+ )
+ }
+ outSizes[index] = size
+ }
+
+ // --- Pass 1.5: Spanning Items ---
+ // If an item spans 2 rows, and those 2 rows (base sizes) sum to 100px, but the item
+ // is 150px tall, we must grow the rows by 50px.
+ distributeSpanningSpace(
+ explicitSpecs = explicitSpecs,
+ sizes = outSizes,
+ gridItems = gridItems,
+ isRowAxis = true,
+ constraints = constraints,
+ crossAxisSizes = columnWidths,
+ gap = rowGap,
+ )
+
+ var usedSpace = 0
+ for (size in outSizes) {
+ usedSpace += size
+ }
+
+ // --- Pass 2: Flex Distribution ---
+ //
+ // If we have finite height and unused space, distribute it to Flex rows.
+ val remainingSpace =
+ if (availableTrackSpace == Constraints.Infinity) 0
+ else max(0, availableTrackSpace - usedSpace)
+
+ var totalAddedFromFlex = 0
+ if (totalFlex > 0 && remainingSpace > 0) {
+ var distributed = 0
+ var accumulatedFlex = 0f
+
+ for (index in 0 until totalCount) {
+ val specRaw =
+ if (index < explicitSpecs.size) explicitSpecs[index]
+ else GridTrackSize.Auto.encodedValue
+ val spec = GridTrackSize(specRaw)
+
+ if (spec.type == GridTrackSize.TypeFlex) {
+ accumulatedFlex += spec.value
+ // Distribute space proportionally based on weight.
+ // Uses an accumulation algorithm to avoid rounding errors summing to >
+ // remainingSpace.
+ val targetSpace = (accumulatedFlex / totalFlex * remainingSpace).roundToInt()
+ val share = max(0, targetSpace - distributed)
+
+ outSizes[index] += share
+ distributed += share
+ totalAddedFromFlex = distributed
+ }
+ }
+ }
+
+ return usedSpace + totalAddedFromFlex
+}
+
+private fun calculateMaxIntrinsicWidth(
+ items: MutableObjectList?,
+ heightConstraint: Int,
+): Int {
+ if (items == null) return 0
+ var maxSize = 0
+ items.forEach { item ->
+ if (item.columnSpan == 1) {
+ val size = item.measurable.maxIntrinsicWidth(heightConstraint)
+ if (size > maxSize) maxSize = size
+ }
+ }
+ return maxSize
+}
+
+private fun calculateMinIntrinsicWidth(
+ items: MutableObjectList?,
+ heightConstraint: Int,
+): Int {
+ if (items == null) return 0
+ var maxSize = 0
+ items.forEach { item ->
+ if (item.columnSpan == 1) {
+ val size = item.measurable.minIntrinsicWidth(heightConstraint)
+ if (size > maxSize) maxSize = size
+ }
+ }
+ return maxSize
+}
+
+private fun calculateMaxIntrinsicHeight(
+ items: MutableObjectList?,
+ columnWidths: IntArray,
+ fallbackWidth: Int,
+): Int {
+ if (items == null) return 0
+ var maxSize = 0
+ items.forEach { item ->
+ if (item.rowSpan == 1) {
+ val colIndex = item.column
+ val width = if (colIndex < columnWidths.size) columnWidths[colIndex] else fallbackWidth
+ val size = item.measurable.maxIntrinsicHeight(width)
+ if (size > maxSize) maxSize = size
+ }
+ }
+ return maxSize
+}
+
+private fun calculateMinIntrinsicHeight(
+ items: MutableObjectList?,
+ columnWidths: IntArray,
+ fallbackWidth: Int,
+): Int {
+ if (items == null) return 0
+ var maxSize = 0
+ items.forEach { item ->
+ if (item.rowSpan == 1) {
+ val colIndex = item.column
+ val width = if (colIndex < columnWidths.size) columnWidths[colIndex] else fallbackWidth
+ val size = item.measurable.minIntrinsicHeight(width)
+ if (size > maxSize) maxSize = size
+ }
+ }
+ return maxSize
+}
+
+/**
+ * Increases the size of "growable" tracks (Auto, Flex, MinContent, MaxContent) to accommodate items
+ * that span across multiple tracks.
+ *
+ * This represents **Pass 1.5** of the grid sizing algorithm. It runs after base track sizes
+ * (Pass 1) are calculated but before flexible space (Pass 2) is distributed.
+ *
+ * **The Problem:** An item spanning 2 columns might have a minimum intrinsic width of 200px. If the
+ * base size of those 2 columns (plus the gap) only equals 150px, the item will be clipped or
+ * overlap.
+ *
+ * **The Solution (Deficit Distribution):**
+ * 1. Calculate the **Deficit**: `RequiredSize - (SumOfTracks + SumOfGaps)`.
+ * 2. Distribute this deficit evenly among the tracks involved in the span, *excluding* rigid tracks
+ * ([GridTrackSize.Fixed] and [GridTrackSize.Percentage]).
+ *
+ * @param explicitSpecs The user-defined track specifications. Used to determine if a track is rigid
+ * (Fixed/Percentage) or growable (Intrinsic).
+ * @param sizes The current calculated pixel sizes of the tracks.
+ * @param gridItems The list of all items to check for spanning requirements.
+ * @param isRowAxis `true` if calculating Row Heights, `false` if calculating Column Widths.
+ * @param constraints The parent layout constraints.
+ * @param crossAxisSizes The calculated sizes of the *opposite* axis (e.g., Column Widths when
+ * calculating Row Heights). This is crucial for correctly measuring the intrinsic height of items
+ * that wrap text based on specific column widths.
+ * @param gap The spacing between tracks.
+ */
+private fun distributeSpanningSpace(
+ explicitSpecs: LongList,
+ sizes: IntArray,
+ gridItems: MutableObjectList,
+ isRowAxis: Boolean,
+ constraints: Constraints,
+ crossAxisSizes: IntArray?,
+ gap: Int,
+) {
+ gridItems.forEach { item ->
+ val trackIndex = if (isRowAxis) item.row else item.column
+ val span = if (isRowAxis) item.rowSpan else item.columnSpan
+
+ // Single-span items were already handled during Base Size calculation (Pass 1).
+ if (span <= 1) return@forEach
+
+ val endIndex = (trackIndex + span).coerceAtMost(sizes.size)
+
+ // --- Step 1: Analyze current space & identifying growable tracks ---
+ // We sum the current size of all tracks this item spans to see if they are already big
+ // enough.
+ var currentSpannedSize = 0
+ var tracksToGrowCount = 0
+
+ for (i in trackIndex until endIndex) {
+ currentSpannedSize += sizes[i]
+
+ // Implicit tracks (indices >= specs.size) default to Auto.
+ val specRaw =
+ if (i < explicitSpecs.size) explicitSpecs[i] else GridTrackSize.Auto.encodedValue
+ val spec = GridTrackSize(specRaw)
+
+ // Fixed and Percentage tracks are considered "Rigid". They respect the user's explicit
+ // definition and do not expand to fit content from spanning items.
+ // Only Intrinsic tracks (Auto, Flex, Min/MaxContent) absorb the deficit.
+ if (spec.type != GridTrackSize.TypeFixed && spec.type != GridTrackSize.TypePercentage) {
+ tracksToGrowCount++
+ }
+ }
+
+ // --- Step 2: Calculate the Item's Required Size (Intrinsic Measurement) ---
+ // This differs based on the axis.
+ val requiredSize =
+ if (isRowAxis) {
+ // Case: Calculating Row Heights.
+ // To get the correct intrinsic height (e.g., for wrapping text), we need to know
+ // the exact width the item occupies. This is the sum of the columns it spans.
+ var itemWidth = 0
+ if (crossAxisSizes != null) {
+ val colStart = item.column
+ val colEnd = (colStart + item.columnSpan).coerceAtMost(crossAxisSizes.size)
+ for (i in colStart until colEnd) {
+ itemWidth += crossAxisSizes[i]
+ }
+ // Add the gaps that are included in the span.
+ val spannedGaps = max(0, item.columnSpan - 1) * gap
+ itemWidth += spannedGaps
+ } else {
+ // If we don't know column widths, constrain only by parent max.
+ itemWidth = constraints.maxWidth
+ }
+ item.measurable.maxIntrinsicHeight(itemWidth)
+ } else {
+ // Case: Calculating Column Widths.
+ // Intrinsic width is typically calculated against infinite height (maxContent),
+ // or the parent's bounded height if specified.
+ val heightConstraint =
+ if (constraints.hasBoundedHeight) constraints.maxHeight
+ else Constraints.Infinity
+ item.measurable.maxIntrinsicWidth(heightConstraint)
+ }
+
+ // --- Step 3: Distribute Deficit ---
+ val deficit = requiredSize - currentSpannedSize
+
+ // If the item needs more space than currently available, and we have eligible tracks to
+ // grow, we distribute the missing pixels evenly.
+ if (deficit > 0 && tracksToGrowCount > 0) {
+ val share = deficit / tracksToGrowCount
+ var remainder = deficit % tracksToGrowCount
+
+ for (i in trackIndex until endIndex) {
+ val specRaw =
+ if (i < explicitSpecs.size) explicitSpecs[i]
+ else GridTrackSize.Auto.encodedValue
+ val spec = GridTrackSize(specRaw)
+
+ // Only add space to the "growable" tracks identified in Step 1.
+ if (
+ spec.type != GridTrackSize.TypeFixed &&
+ spec.type != GridTrackSize.TypePercentage
+ ) {
+ // Add the base share + 1 pixel if we still have remainder to distribute.
+ // This ensures (share * count) + remainder == total deficit.
+ val add = share + if (remainder > 0) 1 else 0
+ sizes[i] += add
+ if (remainder > 0) remainder--
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Measures the content of every grid item based on its resolved position and span.
+ *
+ * This function converts abstract grid coordinates (row/column indices) into concrete pixel
+ * constraints. It determines the exact width and height of the cell(s) an item spans and measures
+ * the child content against those bounds.
+ *
+ * This method calculates the span size in O(1) time using the pre-computed offset arrays: `Size =
+ * (End_Offset + End_Size) - Start_Offset`
+ *
+ * This function mutates the provided [gridItems] list, updating each item with its measured
+ * [Placeable] and calculated (x, y) offsets.
+ *
+ * @param gridItems The list of all grid items.
+ * @param trackSizes The calculated pixel sizes for every row and column track.
+ * @param layoutDirection The current layout direction.
+ */
+private fun measureItems(
+ gridItems: MutableObjectList,
+ trackSizes: GridTrackSizes,
+ layoutDirection: LayoutDirection,
+) {
+ val rowCount = trackSizes.rowHeights.size
+ val colCount = trackSizes.columnWidths.size
+
+ gridItems.forEach { item ->
+ val row = item.row
+ val col = item.column
+
+ if (row < rowCount && col < colCount) {
+ var width = 0
+ val colLimit = (col + item.columnSpan).coerceAtMost(colCount)
+ for (i in col until colLimit) {
+ width += trackSizes.columnWidths[i]
+ }
+ // Add gaps for spanned columns
+ val colSpanActual = colLimit - col
+ if (colSpanActual > 1) {
+ width += (colSpanActual - 1) * trackSizes.columnGapPx
+ }
+
+ var height = 0
+ val rowLimit = (row + item.rowSpan).coerceAtMost(rowCount)
+ for (i in row until rowLimit) {
+ height += trackSizes.rowHeights[i]
+ }
+ // Add gaps for spanned rows
+ val rowSpanActual = rowLimit - row
+ if (rowSpanActual > 1) {
+ height += (rowSpanActual - 1) * trackSizes.rowGapPx
+ }
+
+ // Use loose constraints to allow alignment to work.
+ // If strict fixed constraints are used, child size == cell size, so alignment is
+ // ignored.
+ val constraints = Constraints(maxWidth = width, maxHeight = height)
+ val placeable = item.measurable.measure(constraints)
+
+ // Calculate Alignment Offset
+ val containerSize = IntSize(width, height)
+ val contentSize = IntSize(placeable.width, placeable.height)
+ val alignmentOffset =
+ item.alignment.align(
+ size = contentSize,
+ space = containerSize,
+ layoutDirection = layoutDirection,
+ )
+
+ item.placeable = placeable
+ // Alignment.align already accounts for RTL (Start = right side) relative to 0,0.
+ item.offsetX = alignmentOffset.x
+ item.offsetY = alignmentOffset.y
+ }
+ }
+}
+
+/**
+ * Computes the cumulative starting position (offset) for each track.
+ *
+ * This function converts a list of track sizes (e.g., column widths or row heights) into absolute
+ * coordinates by accumulating the size of previous tracks and the specified [gapPx] between them.
+ *
+ * Example logic:
+ * - Offset[0] = 0
+ * - Offset[1] = Size[0] + Gap
+ * - Offset[2] = Size[0] + Gap + Size[1] + Gap
+ *
+ * @param sizes An array containing the size of each individual track.
+ * @param gapPx The spacing in pixels to insert between consecutive tracks.
+ * @return An [IntArray] of the same length as [sizes], where index `i` contains the starting
+ * coordinate of that track.
+ */
+private fun calculateTrackOffsets(sizes: IntArray, gapPx: Int): IntArray {
+ val offsets = IntArray(sizes.size)
+ var current = 0
+ for (i in sizes.indices) {
+ offsets[i] = current
+ current += sizes[i] + gapPx
+ }
+ return offsets
+}
diff --git a/compose/material/material/src/androidDeviceTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt b/compose/material/material/src/androidDeviceTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
index 63817a0ce3d96..faf25c2a54af4 100644
--- a/compose/material/material/src/androidDeviceTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
+++ b/compose/material/material/src/androidDeviceTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
@@ -57,6 +57,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
@@ -109,6 +110,7 @@ class ExposedDropdownMenuTest {
rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
}
+ @SdkSuppress(maxSdkVersion = 35) // b/454429920
@Test
fun expandedBehaviour_dismissesOnBackPress() {
rule.setMaterialContent {
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index bb96e91e775ac..bc3a017cb8cd7 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -3074,6 +3074,8 @@ package androidx.compose.material3 {
method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SearchBar-nbWgWpA(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2 super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.runtime.Composer?, int, int);
method @KotlinOnly @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopSearchBar(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.SearchBarScrollBehavior? scrollBehavior);
method @BytecodeOnly @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopSearchBar-qKj4JfE(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2 super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.foundation.layout.WindowInsets?, androidx.compose.material3.SearchBarScrollBehavior?, androidx.compose.runtime.Composer?, int, int);
+ method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberContainedSearchBarState(optional androidx.compose.material3.SearchBarValue initialValue, optional androidx.compose.animation.core.AnimationSpec animationSpecForExpand, optional androidx.compose.animation.core.AnimationSpec animationSpecForCollapse, optional androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeIn, optional androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeOut);
+ method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberContainedSearchBarState(androidx.compose.material3.SearchBarValue?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.runtime.Composer?, int, int);
method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberSearchBarState(optional androidx.compose.material3.SearchBarValue initialValue, optional androidx.compose.animation.core.AnimationSpec animationSpecForExpand, optional androidx.compose.animation.core.AnimationSpec animationSpecForCollapse);
method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberSearchBarState(androidx.compose.material3.SearchBarValue?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.runtime.Composer?, int, int);
}
@@ -3095,6 +3097,7 @@ package androidx.compose.material3 {
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SearchBarState {
ctor public SearchBarState(androidx.compose.material3.SearchBarValue initialValue, androidx.compose.animation.core.AnimationSpec animationSpecForExpand, androidx.compose.animation.core.AnimationSpec animationSpecForCollapse);
+ ctor public SearchBarState(androidx.compose.material3.SearchBarValue initialValue, androidx.compose.animation.core.AnimationSpec animationSpecForExpand, androidx.compose.animation.core.AnimationSpec animationSpecForCollapse, androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeIn, androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeOut);
method public suspend Object? animateToCollapsed(kotlin.coroutines.Continuation super kotlin.Unit>);
method public suspend Object? animateToExpanded(kotlin.coroutines.Continuation super kotlin.Unit>);
method @InaccessibleFromKotlin public androidx.compose.ui.layout.LayoutCoordinates? getCollapsedCoords();
@@ -3114,6 +3117,7 @@ package androidx.compose.material3 {
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final class SearchBarState.Companion {
method public androidx.compose.runtime.saveable.Saver Saver(androidx.compose.animation.core.AnimationSpec animationSpecForExpand, androidx.compose.animation.core.AnimationSpec animationSpecForCollapse);
+ method public androidx.compose.runtime.saveable.Saver Saver(androidx.compose.animation.core.AnimationSpec animationSpecForExpand, androidx.compose.animation.core.AnimationSpec animationSpecForCollapse, androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeIn, androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeOut);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum SearchBarValue {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index bb96e91e775ac..bc3a017cb8cd7 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -3074,6 +3074,8 @@ package androidx.compose.material3 {
method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SearchBar-nbWgWpA(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2 super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.runtime.Composer?, int, int);
method @KotlinOnly @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopSearchBar(androidx.compose.material3.SearchBarState state, kotlin.jvm.functions.Function0 inputField, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional androidx.compose.ui.unit.Dp tonalElevation, optional androidx.compose.ui.unit.Dp shadowElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.SearchBarScrollBehavior? scrollBehavior);
method @BytecodeOnly @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopSearchBar-qKj4JfE(androidx.compose.material3.SearchBarState, kotlin.jvm.functions.Function2 super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.ui.Modifier?, androidx.compose.ui.graphics.Shape?, androidx.compose.material3.SearchBarColors?, float, float, androidx.compose.foundation.layout.WindowInsets?, androidx.compose.material3.SearchBarScrollBehavior?, androidx.compose.runtime.Composer?, int, int);
+ method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberContainedSearchBarState(optional androidx.compose.material3.SearchBarValue initialValue, optional androidx.compose.animation.core.AnimationSpec animationSpecForExpand, optional androidx.compose.animation.core.AnimationSpec animationSpecForCollapse, optional androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeIn, optional androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeOut);
+ method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberContainedSearchBarState(androidx.compose.material3.SearchBarValue?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.runtime.Composer?, int, int);
method @KotlinOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberSearchBarState(optional androidx.compose.material3.SearchBarValue initialValue, optional androidx.compose.animation.core.AnimationSpec animationSpecForExpand, optional androidx.compose.animation.core.AnimationSpec animationSpecForCollapse);
method @BytecodeOnly @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SearchBarState rememberSearchBarState(androidx.compose.material3.SearchBarValue?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.animation.core.AnimationSpec?, androidx.compose.runtime.Composer?, int, int);
}
@@ -3095,6 +3097,7 @@ package androidx.compose.material3 {
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SearchBarState {
ctor public SearchBarState(androidx.compose.material3.SearchBarValue initialValue, androidx.compose.animation.core.AnimationSpec animationSpecForExpand, androidx.compose.animation.core.AnimationSpec animationSpecForCollapse);
+ ctor public SearchBarState(androidx.compose.material3.SearchBarValue initialValue, androidx.compose.animation.core.AnimationSpec animationSpecForExpand, androidx.compose.animation.core.AnimationSpec animationSpecForCollapse, androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeIn, androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeOut);
method public suspend Object? animateToCollapsed(kotlin.coroutines.Continuation super kotlin.Unit>);
method public suspend Object? animateToExpanded(kotlin.coroutines.Continuation super kotlin.Unit>);
method @InaccessibleFromKotlin public androidx.compose.ui.layout.LayoutCoordinates? getCollapsedCoords();
@@ -3114,6 +3117,7 @@ package androidx.compose.material3 {
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final class SearchBarState.Companion {
method public androidx.compose.runtime.saveable.Saver Saver(androidx.compose.animation.core.AnimationSpec animationSpecForExpand, androidx.compose.animation.core.AnimationSpec animationSpecForCollapse);
+ method public androidx.compose.runtime.saveable.Saver Saver(androidx.compose.animation.core.AnimationSpec animationSpecForExpand, androidx.compose.animation.core.AnimationSpec animationSpecForCollapse, androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeIn, androidx.compose.animation.core.AnimationSpec animationSpecForContentFadeOut);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum SearchBarValue {
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt
index 9455a24ddd5e5..de4ad35e8429b 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt
@@ -19,6 +19,10 @@
package androidx.compose.material3.samples
import androidx.annotation.Sampled
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.slideIn
+import androidx.compose.animation.slideOut
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -46,6 +50,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
@@ -56,6 +61,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.rememberContainedSearchBarState
import androidx.compose.material3.rememberSearchBarState
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
@@ -65,6 +71,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -107,7 +114,7 @@ fun SimpleSearchBarSample() {
@Composable
fun FullScreenSearchBarScaffoldSample() {
val textFieldState = rememberTextFieldState()
- val searchBarState = rememberSearchBarState()
+ val searchBarState = rememberContainedSearchBarState()
val scope = rememberCoroutineScope()
val scrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior()
val appBarWithSearchColors =
@@ -137,8 +144,8 @@ fun FullScreenSearchBarScaffoldSample() {
state = searchBarState,
colors = appBarWithSearchColors,
inputField = inputField,
- navigationIcon = { SampleNavigationIcon() },
- actions = { SampleActions() },
+ navigationIcon = { SampleNavigationIcon(searchBarState, isAnimated = true) },
+ actions = { SampleActions(searchBarState, isAnimated = true) },
)
ExpandedFullScreenContainedSearchBar(
state = searchBarState,
@@ -199,8 +206,8 @@ fun DockedSearchBarScaffoldSample() {
state = searchBarState,
colors = appBarWithSearchColors,
inputField = inputField,
- navigationIcon = { SampleNavigationIcon() },
- actions = { SampleActions() },
+ navigationIcon = { SampleNavigationIcon(searchBarState) },
+ actions = { SampleActions(searchBarState) },
)
ExpandedDockedSearchBarWithGap(state = searchBarState, inputField = inputField) {
SampleSearchResults(
@@ -273,28 +280,58 @@ private fun SampleTrailingIcon() =
}
}
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
-private fun SampleNavigationIcon() =
- TooltipBox(
- positionProvider =
- TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
- tooltip = { PlainTooltip { Text("Menu") } },
- state = rememberTooltipState(),
+private fun SampleNavigationIcon(state: SearchBarState, isAnimated: Boolean = false) =
+ AnimatedVisibility(
+ visible = !isAnimated || state.targetValue == SearchBarValue.Collapsed,
+ enter =
+ slideIn(
+ animationSpec = motionScheme.fastSpatialSpec(),
+ initialOffset = { IntOffset(-it.width, 0) },
+ ),
+ exit =
+ slideOut(
+ animationSpec = tween(durationMillis = 150, delayMillis = 0),
+ targetOffset = { IntOffset(-it.width, 0) },
+ ),
) {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(imageVector = Icons.Default.Menu, contentDescription = "Menu")
+ TooltipBox(
+ positionProvider =
+ TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
+ tooltip = { PlainTooltip { Text("Menu") } },
+ state = rememberTooltipState(),
+ ) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Default.Menu, contentDescription = "Menu")
+ }
}
}
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
-private fun SampleActions() =
- TooltipBox(
- positionProvider =
- TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
- tooltip = { PlainTooltip { Text("Account") } },
- state = rememberTooltipState(),
+private fun SampleActions(state: SearchBarState, isAnimated: Boolean = false) =
+ AnimatedVisibility(
+ visible = !isAnimated || state.targetValue == SearchBarValue.Collapsed,
+ enter =
+ slideIn(
+ animationSpec = motionScheme.fastSpatialSpec(),
+ initialOffset = { IntOffset(it.width, 0) },
+ ),
+ exit =
+ slideOut(
+ animationSpec = tween(durationMillis = 150, delayMillis = 0),
+ targetOffset = { IntOffset(it.width, 0) },
+ ),
) {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(imageVector = Icons.Default.AccountCircle, contentDescription = "Account")
+ TooltipBox(
+ positionProvider =
+ TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
+ tooltip = { PlainTooltip { Text("Account") } },
+ state = rememberTooltipState(),
+ ) {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(imageVector = Icons.Default.AccountCircle, contentDescription = "Account")
+ }
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt
index afb1565d35e76..a887023d18dad 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SearchBar.kt
@@ -32,6 +32,7 @@ import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateDecay
import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
@@ -137,6 +138,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.focus.FocusDirection
@@ -455,7 +457,11 @@ fun AppBarWithSearch(
)
}
}
- Box(modifier = Modifier.weight(1f)) {
+ val isVisible =
+ !state.expandsToFullScreen ||
+ state.currentValue == SearchBarValue.Collapsed ||
+ state.targetValue == SearchBarValue.Expanded // prevent flickering
+ Box(modifier = Modifier.weight(1f).alpha(if (isVisible) 1f else 0f)) {
SearchBar(
state = state,
inputField = inputField,
@@ -536,6 +542,7 @@ fun ExpandedFullScreenContainedSearchBar(
properties: DialogProperties = DialogProperties(),
content: @Composable ColumnScope.() -> Unit,
) {
+ state.expandsToFullScreen = true
ExpandedFullScreenSearchBarImpl(state = state, properties = properties) {
focusRequester,
predictiveBackState ->
@@ -557,6 +564,7 @@ fun ExpandedFullScreenContainedSearchBar(
tonalElevation = tonalElevation,
shadowElevation = shadowElevation,
windowInsets = windowInsets(),
+ isContained = true,
content = content,
)
}
@@ -603,6 +611,7 @@ fun ExpandedFullScreenSearchBar(
properties: DialogProperties = DialogProperties(),
content: @Composable ColumnScope.() -> Unit,
) {
+ state.expandsToFullScreen = true
ExpandedFullScreenSearchBarImpl(state = state, properties = properties) {
focusRequester,
predictiveBackState ->
@@ -624,6 +633,7 @@ fun ExpandedFullScreenSearchBar(
tonalElevation = tonalElevation,
shadowElevation = shadowElevation,
windowInsets = windowInsets(),
+ isContained = false,
content = {
HorizontalDivider(color = colors.dividerColor)
content()
@@ -1064,9 +1074,12 @@ enum class SearchBarValue {
@Stable
class SearchBarState
private constructor(
- private val animatable: Animatable,
+ internal val animatable: Animatable,
+ private val contentAnimatable: Animatable,
private val animationSpecForExpand: AnimationSpec,
private val animationSpecForCollapse: AnimationSpec,
+ private val animationSpecForContentFadeIn: AnimationSpec