Merge "Text  doesn't crash when text is very very tall" into androidx-main
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
index 6d8eba5..2935ce6 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
@@ -89,4 +89,22 @@
         val subject = finalMaxLines(false, TextOverflow.Ellipsis, 4)
         assertThat(subject).isEqualTo(1)
     }
+
+    @Test
+    fun fixedCoercedNeverCrashes() {
+        var a = 0
+        var b = 0
+        while (1 shl a > 0) {
+            val width = 1 shl a
+            while (1 shl b > 0) {
+                val height = 1 shl b
+                /* shouldn't crash */
+                val constraints = Constraints.fixedCoerceHeightAndWidthForBits(width, height)
+                println("$width $height => $constraints")
+                b++
+            }
+            b = 0
+            a++
+        }
+    }
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeTest.kt
index c1fc291..dda2f9a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNodeTest.kt
@@ -2,7 +2,11 @@
 
 import android.content.Context
 import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.drawscope.DrawScope
@@ -10,6 +14,7 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -44,4 +49,17 @@
         }
         rule.waitForIdle()
     }
+
+    @Test
+    fun exceedsMaxConstraintSize_doesNotCrash() {
+        rule.setContent {
+            val state = rememberScrollState()
+            Column(Modifier.verticalScroll(state)) {
+                BasicText(
+                    text = AnnotatedString("text\n".repeat(10_000)),
+                    style = TextStyle(fontSize = 50.sp),
+                )
+            }
+        }
+    }
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeTest.kt
index ac0b381..7cd8687 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeTest.kt
@@ -19,7 +19,11 @@
 import android.content.Context
 import android.graphics.Typeface
 import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.geometry.Size
@@ -37,6 +41,7 @@
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.font.createFontFamilyResolver
 import androidx.compose.ui.text.font.toFontFamily
+import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -81,6 +86,19 @@
         rule.waitForIdle()
     }
 
+    @Test
+    fun exceedsMaxConstraintSize_doesNotCrash() {
+        rule.setContent {
+            val state = rememberScrollState()
+            Column(Modifier.verticalScroll(state)) {
+                BasicText(
+                    text = "text\n".repeat(10_000),
+                    style = TextStyle(fontSize = 50.sp),
+                )
+            }
+        }
+    }
+
     // TODO(b/279797016) re-enable this test, and add a path for AnnotatedString
     @Ignore("b/279797016 drawBehind is currently broken in tot")
     @OptIn(ExperimentalCoroutinesApi::class)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
index 93823de..d1ce33a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
@@ -86,3 +86,37 @@
     val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
     return if (overwriteMaxLines) 1 else maxLinesIn.coerceAtLeast(1)
 }
+
+private const val BigConstraintValue = (1 shl 18) - 1
+private const val MediumConstraintValue = (1 shl 16) - 1
+private const val SmallConstraintValue = (1 shl 15) - 1
+private const val TinyConstraintValue = (1 shl 13) - 1
+
+/**
+ * Make constraints that never throw from being too large. Prefer to keep accurate width information
+ * first, then constrain height based on the size of width.
+ *
+ * This will return a Constraint with the same or smaller dimensions than the passed (width, height)
+ *
+ * see b/312294386 for more details
+ *
+ * This particular logic is text specific, so not generalizing.
+ *
+ * @param width desired width (has priority)
+ * @param height desired height (uses the remaining bits after width)
+ *
+ * @return a safe Constraint that never throws for running out of bits
+ */
+internal fun Constraints.Companion.fixedCoerceHeightAndWidthForBits(
+    width: Int,
+    height: Int
+): Constraints {
+    val safeWidth = minOf(width, BigConstraintValue - 1)
+    val safeHeight = when {
+        safeWidth < TinyConstraintValue -> minOf(height, BigConstraintValue - 1)
+        safeWidth < SmallConstraintValue -> minOf(height, MediumConstraintValue - 1)
+        safeWidth < MediumConstraintValue -> minOf(height, SmallConstraintValue - 1)
+        else -> minOf(height, TinyConstraintValue - 1)
+    }
+    return fixed(safeWidth, safeHeight)
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
index 2852f89..7ea53ff 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
@@ -405,9 +405,9 @@
 
         // then allow children to measure _inside_ our final box, with the above placeholders
         val placeable = measurable.measure(
-            Constraints.fixed(
-                textLayoutResult.size.width,
-                textLayoutResult.size.height
+            Constraints.fixedCoerceHeightAndWidthForBits(
+                width = textLayoutResult.size.width,
+                height = textLayoutResult.size.height
             )
         )
 
@@ -416,7 +416,6 @@
             textLayoutResult.size.height,
             baselineCache!!
         ) {
-            // this is basically a graphicsLayer
             placeable.place(0, 0)
         }
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
index 4764b12..b6fbe0e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
@@ -340,7 +340,7 @@
 
         // then allow children to measure _inside_ our final box, with the above placeholders
         val placeable = measurable.measure(
-            Constraints.fixed(
+            Constraints.fixedCoerceHeightAndWidthForBits(
                 layoutSize.width,
                 layoutSize.height
             )
@@ -351,7 +351,6 @@
             layoutSize.height,
             baselineCache!!
         ) {
-            // this is basically a graphicsLayer
             placeable.place(0, 0)
         }
     }