Merge "Change MaxLinesHeightModifier to also handle minLines" into androidx-main
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
new file mode 100644
index 0000000..7c40323
--- /dev/null
+++ b/compose/foundation/foundation/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.compose.foundation.text.MaxLinesHeightModifierKt:
+    Removed class androidx.compose.foundation.text.MaxLinesHeightModifierKt
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index df13635..4fc8b33 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -909,6 +909,9 @@
   public final class CoreTextKt {
   }
 
+  public final class HeightInLinesModifierKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class InlineTextContent {
     ctor public InlineTextContent(androidx.compose.ui.text.Placeholder placeholder, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> children);
     method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit> getChildren();
@@ -982,9 +985,6 @@
   public final class LongPressTextDragObserverKt {
   }
 
-  public final class MaxLinesHeightModifierKt {
-  }
-
   public final class StringHelpersKt {
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 1bcf19f..b514e7d 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -1153,6 +1153,9 @@
   public final class CoreTextKt {
   }
 
+  public final class HeightInLinesModifierKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class InlineTextContent {
     ctor public InlineTextContent(androidx.compose.ui.text.Placeholder placeholder, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> children);
     method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit> getChildren();
@@ -1229,9 +1232,6 @@
   public final class LongPressTextDragObserverKt {
   }
 
-  public final class MaxLinesHeightModifierKt {
-  }
-
   public final class StringHelpersKt {
   }
 
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
new file mode 100644
index 0000000..7c40323
--- /dev/null
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.compose.foundation.text.MaxLinesHeightModifierKt:
+    Removed class androidx.compose.foundation.text.MaxLinesHeightModifierKt
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index df13635..4fc8b33 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -909,6 +909,9 @@
   public final class CoreTextKt {
   }
 
+  public final class HeightInLinesModifierKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class InlineTextContent {
     ctor public InlineTextContent(androidx.compose.ui.text.Placeholder placeholder, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> children);
     method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit> getChildren();
@@ -982,9 +985,6 @@
   public final class LongPressTextDragObserverKt {
   }
 
-  public final class MaxLinesHeightModifierKt {
-  }
-
   public final class StringHelpersKt {
   }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/MaxLinesHeightModifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
similarity index 69%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/MaxLinesHeightModifierTest.kt
rename to compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
index 60b4ec6..c278e7b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/MaxLinesHeightModifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
@@ -22,7 +22,7 @@
 import androidx.compose.foundation.layout.requiredWidth
 import androidx.compose.foundation.text.CoreTextField
 import androidx.compose.foundation.text.TEST_FONT
-import androidx.compose.foundation.text.maxLinesHeight
+import androidx.compose.foundation.text.heightInLines
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
@@ -63,7 +63,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-class MaxLinesHeightModifierTest {
+class HeightInLinesModifierTest {
 
     private val longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
         "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam," +
@@ -87,40 +87,7 @@
     }
 
     @Test
-    fun maxLinesHeight_shortInputText() {
-        val (textLayoutResult, height) = setTextFieldWithMaxLines(TextFieldValue("abc"), 5)
-
-        rule.runOnIdle {
-            assertThat(textLayoutResult).isNotNull()
-            assertThat(textLayoutResult!!.lineCount).isEqualTo(1)
-            assertThat(textLayoutResult.size.height).isEqualTo(height)
-        }
-    }
-
-    @Test
-    fun maxLinesHeight_notApplied_infiniteMaxLines() {
-        val (textLayoutResult, height) =
-            setTextFieldWithMaxLines(TextFieldValue(longText), Int.MAX_VALUE)
-
-        rule.runOnIdle {
-            assertThat(textLayoutResult).isNotNull()
-            assertThat(textLayoutResult!!.size.height).isEqualTo(height)
-        }
-    }
-
-    @Test(expected = IllegalArgumentException::class)
-    fun maxLinesHeight_invalidValue() {
-        rule.setContent {
-            CoreTextField(
-                value = TextFieldValue(),
-                onValueChange = {},
-                modifier = Modifier.maxLinesHeight(0, TextStyle.Default)
-            )
-        }
-    }
-
-    @Test
-    fun maxLinesHeight_longInputText() {
+    fun minLines_shortInputText() {
         var subjectLayout: TextLayoutResult? = null
         var subjectHeight: Int? = null
         var twoLineHeight: Int? = null
@@ -136,8 +103,8 @@
                 onTextLayoutResult = {
                     subjectLayout = it
                 },
-                TextFieldValue(longText),
-                2
+                textFieldValue = TextFieldValue("abc"),
+                minLines = 2
             )
             HeightObservingText(
                 onGlobalHeightPositioned = {
@@ -145,8 +112,125 @@
                     twoLinePositionedLatch.countDown()
                 },
                 onTextLayoutResult = {},
-                TextFieldValue("1\n2"),
-                2
+                textFieldValue = TextFieldValue("1\n2"),
+                minLines = 2
+            )
+        }
+        assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+        assertThat(twoLinePositionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+        rule.runOnIdle {
+            assertThat(subjectLayout).isNotNull()
+            assertThat(subjectLayout!!.lineCount).isEqualTo(1)
+            assertThat(subjectHeight!!).isEqualTo(twoLineHeight)
+        }
+    }
+
+    @Test
+    fun maxLines_shortInputText() {
+        val (textLayoutResult, height) = setTextFieldWithMaxLines(
+            TextFieldValue("abc"),
+            maxLines = 5
+        )
+
+        rule.runOnIdle {
+            assertThat(textLayoutResult).isNotNull()
+            assertThat(textLayoutResult!!.lineCount).isEqualTo(1)
+            assertThat(textLayoutResult.size.height).isEqualTo(height)
+        }
+    }
+
+    @Test
+    fun maxLines_notApplied_infiniteMaxLines() {
+        val (textLayoutResult, height) =
+            setTextFieldWithMaxLines(TextFieldValue(longText), Int.MAX_VALUE)
+
+        rule.runOnIdle {
+            assertThat(textLayoutResult).isNotNull()
+            assertThat(textLayoutResult!!.size.height).isEqualTo(height)
+        }
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun minLines_invalidValue() {
+        rule.setContent {
+            CoreTextField(
+                value = TextFieldValue(),
+                onValueChange = {},
+                modifier = Modifier.heightInLines(textStyle = TextStyle.Default, minLines = 0)
+            )
+        }
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun maxLines_invalidValue() {
+        rule.setContent {
+            CoreTextField(
+                value = TextFieldValue(),
+                onValueChange = {},
+                modifier = Modifier.heightInLines(textStyle = TextStyle.Default, maxLines = 0)
+            )
+        }
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun minLines_greaterThan_maxLines_invalidValue() {
+        rule.setContent {
+            CoreTextField(
+                value = TextFieldValue(),
+                onValueChange = {},
+                modifier = Modifier.heightInLines(
+                    textStyle = TextStyle.Default,
+                    minLines = 2,
+                    maxLines = 1
+                )
+            )
+        }
+    }
+
+    @Test
+    fun minLines_longInputText() {
+        val (textLayoutResult, height) = setTextFieldWithMaxLines(
+            TextFieldValue(longText),
+            minLines = 2
+        )
+
+        rule.runOnIdle {
+            assertThat(textLayoutResult).isNotNull()
+            // should be in the 20s, but use this to create invariant for the next assertion
+            assertThat(textLayoutResult!!.lineCount).isGreaterThan(2)
+            assertThat(textLayoutResult.size.height).isEqualTo(height)
+        }
+    }
+
+    @Test
+    fun maxLines_longInputText() {
+        var subjectLayout: TextLayoutResult? = null
+        var subjectHeight: Int? = null
+        var twoLineHeight: Int? = null
+        val positionedLatch = CountDownLatch(1)
+        val twoLinePositionedLatch = CountDownLatch(1)
+
+        rule.setContent {
+            HeightObservingText(
+                onGlobalHeightPositioned = {
+                    subjectHeight = it
+                    positionedLatch.countDown()
+                },
+                onTextLayoutResult = {
+                    subjectLayout = it
+                },
+                textFieldValue = TextFieldValue(longText),
+                maxLines = 2
+            )
+            HeightObservingText(
+                onGlobalHeightPositioned = {
+                    twoLineHeight = it
+                    twoLinePositionedLatch.countDown()
+                },
+                onTextLayoutResult = {},
+                textFieldValue = TextFieldValue("1\n2"),
+                maxLines = 2
             )
         }
         assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
@@ -216,9 +300,14 @@
 
     @Test
     fun testInspectableValue() {
-        val modifier = Modifier.maxLinesHeight(10, TextStyle.Default) as InspectableValue
-        assertThat(modifier.nameFallback).isEqualTo("maxLinesHeight")
+        val modifier = Modifier.heightInLines(
+            textStyle = TextStyle.Default,
+            minLines = 5,
+            maxLines = 10
+        ) as InspectableValue
+        assertThat(modifier.nameFallback).isEqualTo("heightInLines")
         assertThat(modifier.inspectableElements.asIterable()).containsExactly(
+            ValueElement("minLines", 5),
             ValueElement("maxLines", 10),
             ValueElement("textStyle", TextStyle.Default)
         )
@@ -226,7 +315,8 @@
 
     private fun setTextFieldWithMaxLines(
         textFieldValue: TextFieldValue,
-        maxLines: Int
+        minLines: Int = 1,
+        maxLines: Int = Int.MAX_VALUE
     ): Pair<TextLayoutResult?, Int?> {
         var textLayoutResult: TextLayoutResult? = null
         var height: Int? = null
@@ -241,8 +331,9 @@
                 onTextLayoutResult = {
                     textLayoutResult = it
                 },
-                textFieldValue,
-                maxLines
+                textFieldValue = textFieldValue,
+                minLines = minLines,
+                maxLines = maxLines
             )
         }
         assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
@@ -255,7 +346,8 @@
         onGlobalHeightPositioned: (Int) -> Unit,
         onTextLayoutResult: (TextLayoutResult) -> Unit,
         textFieldValue: TextFieldValue,
-        maxLines: Int,
+        minLines: Int = 1,
+        maxLines: Int = Int.MAX_VALUE,
         textStyle: TextStyle = TextStyle.Default
     ) {
         Box(
@@ -269,7 +361,11 @@
                 textStyle = textStyle,
                 modifier = Modifier
                     .requiredWidth(100.dp)
-                    .maxLinesHeight(maxLines, textStyle),
+                    .heightInLines(
+                        textStyle = textStyle,
+                        minLines = minLines,
+                        maxLines = maxLines
+                    ),
                 onTextLayout = onTextLayoutResult
             )
         }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
index 50b289d..99b70f41 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
@@ -33,7 +33,7 @@
 import androidx.compose.foundation.text.Handle
 import androidx.compose.foundation.text.TextFieldScrollerPosition
 import androidx.compose.foundation.text.TextLayoutResultProxy
-import androidx.compose.foundation.text.maxLinesHeight
+import androidx.compose.foundation.text.heightInLines
 import androidx.compose.foundation.text.selection.isSelectionHandle
 import androidx.compose.foundation.text.textFieldScroll
 import androidx.compose.foundation.text.textFieldScrollable
@@ -706,7 +706,7 @@
             softWrap = isVertical,
             modifier = modifier
                 .testTag(TextfieldTag)
-                .maxLinesHeight(resolvedMaxLines, TextStyle.Default)
+                .heightInLines(textStyle = TextStyle.Default, maxLines = resolvedMaxLines)
                 .textFieldScrollable(scrollerPosition)
                 .textFieldScroll(
                     remember { scrollerPosition },
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index ac6cbcf..cc57d5d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -548,7 +548,10 @@
                 // min height is set for maxLines == 1 in order to prevent text cuts for single line
                 // TextFields
                 .heightIn(min = state.minHeightForSingleLineField)
-                .maxLinesHeight(maxLines, textStyle)
+                .heightInLines(
+                    textStyle = textStyle,
+                    maxLines = maxLines
+                )
                 .textFieldScroll(
                     scrollerPosition,
                     value,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
similarity index 72%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
index 1ebda22..2092f2d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
@@ -30,26 +30,39 @@
 import androidx.compose.ui.text.font.FontSynthesis
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.resolveDefaults
+import androidx.compose.ui.unit.Dp
 
 /**
- * Constraint the height of the text field so that it vertically occupies no more than [maxLines]
- * number of lines.
+ * The default minimum height in terms of minimum number of visible lines.
  */
-internal fun Modifier.maxLinesHeight(
-    /*@IntRange(from = 1)*/
-    maxLines: Int,
-    textStyle: TextStyle
+internal const val DefaultMinLines: Int = 1
+
+/**
+ * Constraint the height of the text field so that it vertically occupies at least [minLines]
+ * number of lines and at most [maxLines] number of lines.
+ */
+internal fun Modifier.heightInLines(
+    textStyle: TextStyle,
+    minLines: Int = DefaultMinLines,
+    maxLines: Int = Int.MAX_VALUE
 ) = composed(
     inspectorInfo = debugInspectorInfo {
-        name = "maxLinesHeight"
+        name = "heightInLines"
+        properties["minLines"] = minLines
         properties["maxLines"] = maxLines
         properties["textStyle"] = textStyle
     }
 ) {
+    require(minLines > 0) {
+        "minLines must be greater than 0"
+    }
     require(maxLines > 0) {
         "maxLines must be greater than 0"
     }
-    if (maxLines == Int.MAX_VALUE) return@composed Modifier
+    require(minLines <= maxLines) {
+        "minLines $minLines must be lower than or equal to maxLines $maxLines"
+    }
+    if (minLines == DefaultMinLines && maxLines == Int.MAX_VALUE) return@composed Modifier
 
     val density = LocalDensity.current
     val fontFamilyResolver = LocalFontFamilyResolver.current
@@ -61,7 +74,7 @@
         resolveDefaults(textStyle, layoutDirection)
     }
     val typeface by remember(fontFamilyResolver, resolvedStyle) {
-         fontFamilyResolver.resolve(
+        fontFamilyResolver.resolve(
             resolvedStyle.fontFamily,
             resolvedStyle.fontWeight ?: FontWeight.Normal,
             resolvedStyle.fontStyle ?: FontStyle.Normal,
@@ -102,9 +115,15 @@
         ).height
     }
     val lineHeight = firstTwoLinesHeight - firstLineHeight
-    val precomputedMaxLinesHeight = firstLineHeight + lineHeight * (maxLines - 1)
+    val precomputedMinLinesHeight =
+        if (minLines == DefaultMinLines) null else firstLineHeight + lineHeight * (minLines - 1)
+    val precomputedMaxLinesHeight =
+        if (maxLines == Int.MAX_VALUE) null else firstLineHeight + lineHeight * (maxLines - 1)
 
-    Modifier.heightIn(
-        max = with(density) { precomputedMaxLinesHeight.toDp() }
-    )
+    with(density) {
+        Modifier.heightIn(
+            min = precomputedMinLinesHeight?.toDp() ?: Dp.Unspecified,
+            max = precomputedMaxLinesHeight?.toDp() ?: Dp.Unspecified
+        )
+    }
 }
\ No newline at end of file