diff --git a/android/app/src/main/java/com/labfytools/trainlog/ui/BodyScreen.kt b/android/app/src/main/java/com/labfytools/trainlog/ui/BodyScreen.kt index 32e790a..5552e8e 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/ui/BodyScreen.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/ui/BodyScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.ColumnScope import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.LocalDensity import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -378,7 +379,7 @@ private fun BodyMetricField( ) } -private data class BodyMetricItem( +internal data class BodyMetricItem( val label: String, val unit: String, val value: String, @@ -386,10 +387,19 @@ private data class BodyMetricItem( ) /** Two columns retain scanability; very narrow accessibility layouts stack. */ +internal fun bodyMetricColumnCount(availableWidthDp: Float, fontScale: Float): Int { + val twoCompactFieldsDp = 2 * 140f + 10f + return if (availableWidthDp >= twoCompactFieldsDp && fontScale < 1.5f) 2 else 1 +} + @Composable -private fun ColumnScope.BodyMetricGrid(items: List) { +internal fun ColumnScope.BodyMetricGrid(items: List) { BoxWithConstraints(Modifier.fillMaxWidth()) { - val columns = if (maxWidth < 330.dp) 1 else 2 + /* WHY: this 360 dp device has 312 dp after screen padding; the old + * 330 dp threshold therefore stacked fields that need only 140 dp. + * CONTRACT: two compact fields plus their 10 dp gutter are preferred + * unless width or accessibility font scaling makes them unreadable. */ + val columns = bodyMetricColumnCount(maxWidth.value, LocalDensity.current.fontScale) Column(verticalArrangement = Arrangement.spacedBy(0.dp)) { items.chunked(columns).forEach { rowItems -> Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { diff --git a/android/app/src/test/java/com/labfytools/trainlog/ui/BodyMetricGridTest.kt b/android/app/src/test/java/com/labfytools/trainlog/ui/BodyMetricGridTest.kt new file mode 100644 index 0000000..06ac6e5 --- /dev/null +++ b/android/app/src/test/java/com/labfytools/trainlog/ui/BodyMetricGridTest.kt @@ -0,0 +1,74 @@ +package com.labfytools.trainlog.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.width +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.unit.dp +import com.labfytools.trainlog.ui.theme.TrainlogTheme +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class BodyMetricGridTest { + @get:Rule val compose = createComposeRule() + + private val items = listOf( + BodyMetricItem("POIDS", "kg", "", {}), + BodyMetricItem("COU", "cm", "", {}), + BodyMetricItem("TOUR DE TAILLE", "cm", "", {}), + BodyMetricItem("HANCHES", "cm", "", {}), + ) + + @Test + fun deployedPhoneContentWidthUsesTwoColumns() { + // 1080 px / 480 dpi = 360 dp; the screen frame leaves about 312 dp. + assertEquals(2, bodyMetricColumnCount(312f, 0.9f)) + } + + @Test + fun genuinelyNarrowWidthStacksFields() { + assertEquals(1, bodyMetricColumnCount(280f, 1f)) + } + + @Test + fun largeAccessibilityTextStacksFields() { + assertEquals(1, bodyMetricColumnCount(312f, 1.5f)) + } + + @Test + fun deployedPhoneWidthRendersPairedFieldsWithoutClippingUnits() { + compose.setContent { + TrainlogTheme { + Column(Modifier.width(312.dp)) { BodyMetricGrid(items) } + } + } + val weight = compose.onNodeWithText("POIDS").fetchSemanticsNode().boundsInRoot + val neck = compose.onNodeWithText("COU").fetchSemanticsNode().boundsInRoot + assertTrue(neck.left > weight.left) + assertTrue(kotlin.math.abs(neck.top - weight.top) < 2f) + compose.onNodeWithText("kg").fetchSemanticsNode() + assertTrue(compose.onAllNodesWithText("cm", useUnmergedTree = true).fetchSemanticsNodes().isNotEmpty()) + compose.onNodeWithText("TOUR DE TAILLE").fetchSemanticsNode() + } + + @Test + fun trulyNarrowLayoutStacksPairedFields() { + compose.setContent { + TrainlogTheme { + Column(Modifier.width(280.dp)) { BodyMetricGrid(items.take(2)) } + } + } + val weight = compose.onNodeWithText("POIDS").fetchSemanticsNode().boundsInRoot + val neck = compose.onNodeWithText("COU").fetchSemanticsNode().boundsInRoot + assertTrue(neck.top > weight.top) + } +}