diff --git a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/request/ViewerStatus.kt b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/request/ViewerStatus.kt index ee39adcdcf..003b5d4311 100644 --- a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/request/ViewerStatus.kt +++ b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/request/ViewerStatus.kt @@ -12,7 +12,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver -import androidx.compose.ui.layout.SubcomposeLayout +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.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle @@ -20,6 +26,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp @@ -82,28 +89,47 @@ fun ViewerStatusLine( FirstThatFits(full, short, style, color, modifier) } -/** [full] when it fits on one line, otherwise [short], ellipsized. */ +/** + * [full] when it fits on one line, otherwise [short], ellipsized. A plain layout rather than a + * subcompose one, because the deck's pill sizes itself to this line's intrinsic width. + */ @Composable -private fun FirstThatFits( +internal fun FirstThatFits( full: AnnotatedString, short: AnnotatedString, style: TextStyle, color: Color, modifier: Modifier, ) { - SubcomposeLayout(modifier) { constraints -> - val whole = subcompose("full") { + Layout( + modifier = modifier, + content = { Text(text = full, style = style, color = color, maxLines = 1, softWrap = false) - }.single().measure(constraints.copy(minWidth = 0, maxWidth = Int.MAX_VALUE)) - val placeable = if (whole.width <= constraints.maxWidth) { - whole - } else { - subcompose("short") { - Text(text = short, style = style, color = color, maxLines = 1, overflow = TextOverflow.Ellipsis) - }.single().measure(constraints.copy(minWidth = 0)) - } - layout(placeable.width, placeable.height) { placeable.place(0, 0) } + Text(text = short, style = style, color = color, maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + measurePolicy = FirstThatFitsPolicy, + ) +} + +private object FirstThatFitsPolicy : MeasurePolicy { + override fun MeasureScope.measure(measurables: List, constraints: Constraints): MeasureResult { + val (full, short) = measurables + val whole = full.measure(constraints.copy(minWidth = 0, maxWidth = Constraints.Infinity)) + val placeable = if (whole.width <= constraints.maxWidth) whole else short.measure(constraints.copy(minWidth = 0)) + return layout(placeable.width, placeable.height) { placeable.place(0, 0) } } + + override fun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List, height: Int): Int = + measurables[0].maxIntrinsicWidth(height) + + override fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List, height: Int): Int = + measurables[1].minIntrinsicWidth(height) + + override fun IntrinsicMeasureScope.maxIntrinsicHeight(measurables: List, width: Int): Int = + measurables.maxOf { it.maxIntrinsicHeight(width) } + + override fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List, width: Int): Int = + measurables.maxOf { it.minIntrinsicHeight(width) } } /** diff --git a/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/request/FirstThatFitsTest.kt b/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/request/FirstThatFitsTest.kt new file mode 100644 index 0000000000..a38f50c6fc --- /dev/null +++ b/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/request/FirstThatFitsTest.kt @@ -0,0 +1,59 @@ +package com.flipcash.shared.chat.ui.request + +import androidx.compose.foundation.layout.width +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.layout +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertTrue + +/** The deck's pill reads the status line's intrinsic width, so the line must be able to give one. */ +@RunWith(RobolectricTestRunner::class) +class FirstThatFitsTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private val full = AnnotatedString("You owe $5.00 ยท $5.00 of $15.00") + private val short = AnnotatedString("You owe $5.00") + + @Test + fun `reports an intrinsic width when asked for one`() { + var intrinsic = -1 + composeTestRule.setContent { + FirstThatFits( + full = full, + short = short, + style = TextStyle.Default, + color = Color.White, + modifier = Modifier.layout { measurable, constraints -> + intrinsic = measurable.maxIntrinsicWidth(constraints.maxHeight) + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { placeable.place(0, 0) } + }, + ) + } + composeTestRule.waitForIdle() + assertTrue(intrinsic > 0) + composeTestRule.onNodeWithText(full.text).assertIsDisplayed() + } + + @Test + fun `falls back to the short line when the full one does not fit`() { + composeTestRule.setContent { + FirstThatFits(full, short, TextStyle.Default, Color.White, Modifier.width(1.dp)) + } + composeTestRule.onNodeWithText(full.text).assertIsNotDisplayed() + composeTestRule.onNodeWithText(short.text).assertIsDisplayed() + } +}