diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt index b94bfb81..0221f1f9 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt @@ -19,6 +19,8 @@ import com.jakewharton.mosaic.ui.unit.Constraints import com.jakewharton.mosaic.ui.unit.IntOffset import com.jakewharton.mosaic.ui.unit.IntSize import com.jakewharton.mosaic.ui.unit.constrain +import com.jakewharton.mosaic.ui.unit.constrainHeight +import com.jakewharton.mosaic.ui.unit.constrainWidth import kotlin.math.max const val s = " " @@ -156,3 +158,65 @@ fun testIntrinsics( } } } + +@Composable +internal fun ConstrainedBox( + constraints: Constraints, + modifier: Modifier = Modifier, + content: @Composable () -> Unit = {}, +) { + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + val measurePolicy = object : MeasurePolicy { + override fun MeasureScope.measure( + measurables: List, + incomingConstraints: Constraints, + ): MeasureResult { + val measurable = measurables.firstOrNull() + val childConstraints = incomingConstraints.constrain(constraints) + val placeable = measurable?.measure(childConstraints) + + val layoutWidth = placeable?.width ?: childConstraints.minWidth + val layoutHeight = placeable?.height ?: childConstraints.minHeight + return layout(layoutWidth, layoutHeight) { + placeable?.place(0, 0) + } + } + + override fun minIntrinsicWidth( + measurables: List, + height: Int, + ): Int { + val width = measurables.firstOrNull()?.minIntrinsicWidth(height) ?: 0 + return constraints.constrainWidth(width) + } + + override fun minIntrinsicHeight( + measurables: List, + width: Int, + ): Int { + val height = measurables.firstOrNull()?.minIntrinsicHeight(width) ?: 0 + return constraints.constrainHeight(height) + } + + override fun maxIntrinsicWidth( + measurables: List, + height: Int, + ): Int { + val width = measurables.firstOrNull()?.maxIntrinsicWidth(height) ?: 0 + return constraints.constrainWidth(width) + } + + override fun maxIntrinsicHeight( + measurables: List, + width: Int, + ): Int { + val height = measurables.firstOrNull()?.maxIntrinsicHeight(width) ?: 0 + return constraints.constrainHeight(height) + } + } + Layout( + content = content, + modifier = modifier, + measurePolicy = measurePolicy, + ) +} diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/BoxTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/BoxTest.kt new file mode 100644 index 00000000..478d3edc --- /dev/null +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/BoxTest.kt @@ -0,0 +1,544 @@ +package com.jakewharton.mosaic.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.jakewharton.mosaic.AnsiRendering +import com.jakewharton.mosaic.ConstrainedBox +import com.jakewharton.mosaic.Container +import com.jakewharton.mosaic.TestChar +import com.jakewharton.mosaic.TestFiller +import com.jakewharton.mosaic.layout.MeasurePolicy +import com.jakewharton.mosaic.layout.aspectRatio +import com.jakewharton.mosaic.layout.fillMaxSize +import com.jakewharton.mosaic.layout.padding +import com.jakewharton.mosaic.layout.requiredSize +import com.jakewharton.mosaic.layout.requiredWidthIn +import com.jakewharton.mosaic.layout.size +import com.jakewharton.mosaic.layout.width +import com.jakewharton.mosaic.modifier.Modifier +import com.jakewharton.mosaic.mosaicNodes +import com.jakewharton.mosaic.mosaicNodesWithMeasureAndPlace +import com.jakewharton.mosaic.position +import com.jakewharton.mosaic.renderMosaic +import com.jakewharton.mosaic.replaceLineEndingsWithCRLF +import com.jakewharton.mosaic.s +import com.jakewharton.mosaic.size +import com.jakewharton.mosaic.testIntrinsics +import com.jakewharton.mosaic.ui.unit.Constraints +import com.jakewharton.mosaic.ui.unit.IntOffset +import com.jakewharton.mosaic.ui.unit.IntSize +import kotlin.test.Test + +class BoxTest { + @Test fun boxWithAlignedAndPositionedChildren() { + val size = 6 + + val content = @Composable { + Container(alignment = Alignment.TopStart) { + Box { + Container( + modifier = Modifier.align(Alignment.BottomEnd), + width = size, + height = size, + ) + TestFiller(modifier = Modifier.matchParentSize().padding(2)) + } + } + } + + val rootNode = mosaicNodesWithMeasureAndPlace(content) + val actual = renderMosaic(content) + + val boxNode = rootNode.children[0].children[0] + + val alignedChildContainerNode = boxNode.children[0] + val positionedChildContainerNode = boxNode.children[1] + + assertThat(alignedChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(alignedChildContainerNode.position).isEqualTo(IntOffset.Zero) + assertThat(positionedChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(positionedChildContainerNode.position).isEqualTo(IntOffset.Zero) + + assertThat(actual).isEqualTo( + """ + | $s + | $s + | $TestChar$TestChar $s + | $TestChar$TestChar $s + | $s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) + } + + @Test fun boxWithMultipleAlignedChildren() { + val size = 200 + val doubleSize = size * 2 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Container(alignment = Alignment.TopStart) { + Box { + Container( + modifier = Modifier.align(Alignment.BottomEnd), + width = size, + height = size, + ) + Container( + modifier = Modifier.align(Alignment.BottomEnd), + width = doubleSize, + height = doubleSize, + ) + } + } + } + + val boxNode = rootNode.children[0].children[0] + + val firstChildContainerNode = boxNode.children[0] + val secondChildContainerNode = boxNode.children[1] + + assertThat(boxNode.size).isEqualTo(IntSize(doubleSize, doubleSize)) + assertThat(firstChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(firstChildContainerNode.position).isEqualTo(IntOffset(size, size)) + assertThat(secondChildContainerNode.size).isEqualTo(IntSize(doubleSize, doubleSize)) + assertThat(secondChildContainerNode.position).isEqualTo(IntOffset.Zero) + } + + @Test fun boxWithStretchChildrenPaddingLeftTop() { + val size = 6 + val halfSize = size / 2 + val inset = 1 + + val content = @Composable { + Container(alignment = Alignment.TopStart) { + Box { + Container( + modifier = Modifier.align(Alignment.Center), + width = size, + height = size, + ) + TestFiller( + modifier = Modifier.matchParentSize().padding(left = inset, top = inset).size(halfSize), + ) + } + } + } + + val rootNode = mosaicNodesWithMeasureAndPlace(content) + val actual = renderMosaic(content) + + val boxNode = rootNode.children[0].children[0] + + val firstChildContainerNode = boxNode.children[0] + val secondChildContainerNode = boxNode.children[1] + + assertThat(boxNode.size).isEqualTo(IntSize(size, size)) + assertThat(firstChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(firstChildContainerNode.position).isEqualTo(IntOffset.Zero) + assertThat(secondChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(secondChildContainerNode.position).isEqualTo(IntOffset.Zero) + + assertThat(actual).isEqualTo( + """ + | $s + | $TestChar$TestChar$TestChar$TestChar$TestChar + | $TestChar$TestChar$TestChar$TestChar$TestChar + | $TestChar$TestChar$TestChar$TestChar$TestChar + | $TestChar$TestChar$TestChar$TestChar$TestChar + | $TestChar$TestChar$TestChar$TestChar$TestChar + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) + } + + @Test fun boxWithStretchChildrenPaddingRightBottom() { + val size = 6 + val halfSize = size / 2 + val inset = 1 + + val content = @Composable { + Container(alignment = Alignment.TopStart) { + Box { + Container( + modifier = Modifier.align(Alignment.Center), + width = size, + height = size, + ) + TestFiller( + modifier = Modifier.matchParentSize().padding(right = inset, bottom = inset) + .size(halfSize), + ) + } + } + } + + val rootNode = mosaicNodesWithMeasureAndPlace(content) + val actual = renderMosaic(content) + + val boxNode = rootNode.children[0].children[0] + + val firstChildContainerNode = boxNode.children[0] + val secondChildContainerNode = boxNode.children[1] + + assertThat(boxNode.size).isEqualTo(IntSize(size, size)) + assertThat(firstChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(firstChildContainerNode.position).isEqualTo(IntOffset.Zero) + assertThat(secondChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(secondChildContainerNode.position).isEqualTo(IntOffset.Zero) + + assertThat(actual).isEqualTo( + """ + |$TestChar$TestChar$TestChar$TestChar$TestChar$s + |$TestChar$TestChar$TestChar$TestChar$TestChar$s + |$TestChar$TestChar$TestChar$TestChar$TestChar$s + |$TestChar$TestChar$TestChar$TestChar$TestChar$s + |$TestChar$TestChar$TestChar$TestChar$TestChar$s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) + } + + @Test fun boxWithStretchChildrenPaddingLeftRight() { + val size = 6 + val halfSize = size / 2 + val inset = 1 + + val content = @Composable { + Container(alignment = Alignment.TopStart) { + Box { + Container( + modifier = Modifier.align(Alignment.Center), + width = size, + height = size, + ) + TestFiller( + modifier = Modifier.matchParentSize().padding(left = inset, right = inset) + .size(halfSize), + ) + } + } + } + + val rootNode = mosaicNodesWithMeasureAndPlace(content) + val actual = renderMosaic(content) + + val boxNode = rootNode.children[0].children[0] + + val firstChildContainerNode = boxNode.children[0] + val secondChildContainerNode = boxNode.children[1] + + assertThat(boxNode.size).isEqualTo(IntSize(size, size)) + assertThat(firstChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(firstChildContainerNode.position).isEqualTo(IntOffset.Zero) + assertThat(secondChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(secondChildContainerNode.position).isEqualTo(IntOffset.Zero) + + assertThat(actual).isEqualTo( + """ + | $TestChar$TestChar$TestChar$TestChar$s + | $TestChar$TestChar$TestChar$TestChar$s + | $TestChar$TestChar$TestChar$TestChar$s + | $TestChar$TestChar$TestChar$TestChar$s + | $TestChar$TestChar$TestChar$TestChar$s + | $TestChar$TestChar$TestChar$TestChar$s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) + } + + @Test fun boxWithStretchChildrenPaddingTopBottom() { + val size = 6 + val halfSize = size / 2 + val inset = 1 + + val content = @Composable { + Container(alignment = Alignment.TopStart) { + Box { + Container( + modifier = Modifier.align(Alignment.Center), + width = size, + height = size, + ) + TestFiller( + modifier = Modifier.matchParentSize().padding(top = inset, bottom = inset) + .size(halfSize), + ) + } + } + } + + val rootNode = mosaicNodesWithMeasureAndPlace(content) + val actual = renderMosaic(content) + + val boxNode = rootNode.children[0].children[0] + + val firstChildContainerNode = boxNode.children[0] + val secondChildContainerNode = boxNode.children[1] + + assertThat(boxNode.size).isEqualTo(IntSize(size, size)) + assertThat(firstChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(firstChildContainerNode.position).isEqualTo(IntOffset.Zero) + assertThat(secondChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(secondChildContainerNode.position).isEqualTo(IntOffset.Zero) + + assertThat(actual).isEqualTo( + """ + | $s + |$TestChar$TestChar$TestChar$TestChar$TestChar$TestChar + |$TestChar$TestChar$TestChar$TestChar$TestChar$TestChar + |$TestChar$TestChar$TestChar$TestChar$TestChar$TestChar + |$TestChar$TestChar$TestChar$TestChar$TestChar$TestChar + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) + } + + @Test fun boxExpanded() { + val size = 250 + val halfSize = 125 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Container(alignment = Alignment.TopStart) { + Container(modifier = Modifier.size(size)) { + Box { + Container(modifier = Modifier.fillMaxSize()) + Container( + modifier = Modifier.align(Alignment.BottomEnd), + width = halfSize, + height = halfSize, + ) + } + } + } + } + + val outerContainerNode = rootNode.children[0].children[0] + val boxNode = outerContainerNode.children[0] + + val firstChildContainerNode = boxNode.children[0] + val secondChildContainerNode = boxNode.children[1] + + assertThat(outerContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(firstChildContainerNode.size).isEqualTo(IntSize(size, size)) + assertThat(firstChildContainerNode.position).isEqualTo(IntOffset.Zero) + assertThat(secondChildContainerNode.size).isEqualTo(IntSize(halfSize, halfSize)) + assertThat(secondChildContainerNode.position).isEqualTo( + IntOffset( + size - halfSize, + size - halfSize, + ), + ) + } + + @Test fun boxAlignmentParameter() { + val outerSize = 50 + val innerSize = 10 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Box( + contentAlignment = Alignment.BottomEnd, + modifier = Modifier.requiredSize(outerSize), + ) { + Box(Modifier.requiredSize(innerSize)) + } + } + + val innerBoxNode = rootNode.children[0].children[0] + + assertThat(innerBoxNode.position) + .isEqualTo(IntOffset(outerSize - innerSize, outerSize - innerSize)) + } + + @Test fun boxOutermostGravityWins() { + val size = 10 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Box(Modifier.requiredSize(size)) { + Box(Modifier.align(Alignment.BottomEnd).align(Alignment.TopStart)) + } + } + + val innerBoxNode = rootNode.children[0].children[0] + + assertThat(innerBoxNode.position).isEqualTo(IntOffset(size, size)) + } + + @Test fun boxChildAffectsBoxSize() { + val size = mutableIntStateOf(10) + var measure = 0 + var layout = 0 + + val rootNode = mosaicNodes { + Box { + Layout( + content = { + Box { + Box( + Modifier.requiredSize(size.value, 10), + ) + } + }, + measurePolicy = remember { + MeasurePolicy { measurables, constraints -> + val placeable = measurables.first().measure(constraints) + ++measure + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + ++layout + } + } + }, + ) + } + } + + val rendering = AnsiRendering() + rendering.render(rootNode) + + assertThat(measure).isEqualTo(1) + assertThat(layout).isEqualTo(1) + + size.value = 20 + rendering.render(rootNode) + + assertThat(measure).isEqualTo(2) + assertThat(layout).isEqualTo(2) + } + + @Test fun boxCanPropagateMinConstraints() { + val rootNode = mosaicNodesWithMeasureAndPlace { + Box( + modifier = Modifier.requiredWidthIn(20, 40), + propagateMinConstraints = true, + ) { + Box(modifier = Modifier.width(10)) + } + } + + val innerBoxNode = rootNode.children[0].children[0] + + assertThat(innerBoxNode.width).isEqualTo(20) + } + + @Test fun boxTracksPropagateMinConstraintsChanges() { + val pmc = mutableStateOf(true) + + val content = @Composable { + Box( + modifier = Modifier.requiredWidthIn(20, 40), + propagateMinConstraints = pmc.value, + contentAlignment = Alignment.Center, + ) { + Box(modifier = Modifier.width(10)) + } + } + + val firstRootNode = mosaicNodesWithMeasureAndPlace(content) + assertThat(firstRootNode.children[0].children[0].width).isEqualTo(20) + + pmc.value = false + val secondRootNode = mosaicNodesWithMeasureAndPlace(content) + assertThat(secondRootNode.children[0].children[0].width).isEqualTo(10) + } + + @Test fun boxHasCorrectIntrinsicMeasurements() { + val testWidth = 90 + val testHeight = 80 + + val testDimension = 200 + // When measuring the height with testDimension, width should be double + val expectedWidth = testDimension * 2 + // When measuring the width with testDimension, height should be half + val expectedHeight = testDimension / 2 + + testIntrinsics( + @Composable { + Box { + Container(modifier = Modifier.align(Alignment.TopStart).aspectRatio(2f)) + ConstrainedBox( + constraints = Constraints.fixed(testWidth, testHeight), + modifier = Modifier.align(Alignment.BottomCenter), + ) + ConstrainedBox( + constraints = Constraints.fixed(200, 200), + modifier = Modifier.matchParentSize().padding(10), + ) + } + }, + ) { minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight -> + // Min width + assertThat(minIntrinsicWidth(0)).isEqualTo(testWidth) + assertThat(minIntrinsicWidth(testDimension)).isEqualTo(expectedWidth) + assertThat(minIntrinsicWidth(Constraints.Infinity)).isEqualTo(testWidth) + // Min height + assertThat(minIntrinsicHeight(0)).isEqualTo(testHeight) + assertThat(minIntrinsicHeight(testDimension)).isEqualTo(expectedHeight) + assertThat(minIntrinsicHeight(Constraints.Infinity)).isEqualTo(testHeight) + // Max width + assertThat(maxIntrinsicWidth(0)).isEqualTo(testWidth) + assertThat(maxIntrinsicWidth(testDimension)).isEqualTo(expectedWidth) + assertThat(maxIntrinsicWidth(Constraints.Infinity)).isEqualTo(testWidth) + // Max height + assertThat(maxIntrinsicHeight(0)).isEqualTo(testHeight) + assertThat(maxIntrinsicHeight(testDimension)).isEqualTo(expectedHeight) + assertThat(maxIntrinsicHeight(Constraints.Infinity)).isEqualTo(testHeight) + } + } + + @Test fun boxHasCorrectIntrinsicMeasurementsWithNoAlignedChildren() { + testIntrinsics( + @Composable { + Box { + ConstrainedBox( + modifier = Modifier.matchParentSize().padding(10), + constraints = Constraints.fixed(200, 200), + ) + } + }, + ) { minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight -> + // Min width + assertThat(minIntrinsicWidth(50)).isEqualTo(0) + assertThat(minIntrinsicWidth(Constraints.Infinity)).isEqualTo(0) + // Min height + assertThat(minIntrinsicHeight(50)).isEqualTo(0) + assertThat(minIntrinsicHeight(Constraints.Infinity)).isEqualTo(0) + // Max width + assertThat(maxIntrinsicWidth(50)).isEqualTo(0) + assertThat(maxIntrinsicWidth(Constraints.Infinity)).isEqualTo(0) + // Max height + assertThat(maxIntrinsicHeight(50)).isEqualTo(0) + assertThat(maxIntrinsicHeight(Constraints.Infinity)).isEqualTo(0) + } + } + + @Test fun boxSimpleDebug() { + val actual = mosaicNodes { + Box() + } + + assertThat(actual.toString()).isEqualTo( + """ + |Box() x=0 y=0 w=0 h=0 + """.trimMargin(), + ) + } + + @Test fun boxDebug() { + val actual = mosaicNodes { + Box(contentAlignment = Alignment.BottomCenter, propagateMinConstraints = true) {} + } + + assertThat(actual.toString()).isEqualTo( + """ + |Box(alignment=Alignment(horizontalBias=0, verticalBias=1), propagateMinConstraints=true) x=0 y=0 w=0 h=0 + """.trimMargin(), + ) + } +} diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/FillerTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/FillerTest.kt new file mode 100644 index 00000000..e6437bd0 --- /dev/null +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/FillerTest.kt @@ -0,0 +1,189 @@ +package com.jakewharton.mosaic.ui + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.jakewharton.mosaic.Container +import com.jakewharton.mosaic.TestChar +import com.jakewharton.mosaic.TestFiller +import com.jakewharton.mosaic.layout.height +import com.jakewharton.mosaic.layout.padding +import com.jakewharton.mosaic.layout.size +import com.jakewharton.mosaic.layout.width +import com.jakewharton.mosaic.modifier.Modifier +import com.jakewharton.mosaic.mosaicNodes +import com.jakewharton.mosaic.mosaicNodesWithMeasureAndPlace +import com.jakewharton.mosaic.renderMosaic +import com.jakewharton.mosaic.replaceLineEndingsWithCRLF +import com.jakewharton.mosaic.s +import com.jakewharton.mosaic.size +import com.jakewharton.mosaic.ui.unit.Constraints +import com.jakewharton.mosaic.ui.unit.IntSize +import kotlin.test.Test + +class FillerTest { + private val bigConstraints = Constraints(maxWidth = 5000, maxHeight = 5000) + + @Test fun fillerFixed() { + val width = 4 + val height = 6 + + val actual = renderMosaic { + TestFiller(Modifier.size(width = width, height = height)) + } + + assertThat(actual).isEqualTo( + """ + |$TestChar$TestChar$TestChar$TestChar + |$TestChar$TestChar$TestChar$TestChar + |$TestChar$TestChar$TestChar$TestChar + |$TestChar$TestChar$TestChar$TestChar + |$TestChar$TestChar$TestChar$TestChar + |$TestChar$TestChar$TestChar$TestChar + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) + } + + @Test fun fillerFixedWithPadding() { + val width = 4 + val height = 6 + + val actual = renderMosaic { + TestFiller(Modifier.size(width = width, height = height).padding(1)) + } + + assertThat(actual).isEqualTo( + """ + | $s + | $TestChar$TestChar$s + | $TestChar$TestChar$s + | $TestChar$TestChar$s + | $TestChar$TestChar$s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) + } + + @Test fun fillerFixedSize() { + val width = 40 + val height = 71 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Container(constraints = bigConstraints) { + TestFiller(Modifier.size(width = width, height = height)) + } + } + + val fillerNode = rootNode.children[0].children[0] + assertThat(fillerNode.size).isEqualTo(IntSize(width, height)) + } + + @Test fun fillerFixedWithSmallerContainer() { + val width = 40 + val height = 71 + + val containerWidth = 5 + val containerHeight = 7 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Box { + Container( + constraints = Constraints( + maxWidth = containerWidth, + maxHeight = containerHeight, + ), + ) { + TestFiller(Modifier.size(width = width, height = height)) + } + } + } + + val fillerNode = rootNode.children[0].children[0].children[0] + assertThat(fillerNode.size).isEqualTo(IntSize(containerWidth, containerHeight)) + } + + @Test fun fillerWidth() { + val width = 71 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Container(constraints = bigConstraints) { + TestFiller(Modifier.width(width)) + } + } + + val fillerNode = rootNode.children[0].children[0] + assertThat(fillerNode.size).isEqualTo(IntSize(width, 0)) + } + + @Test fun fillerWidthWithSmallerContainer() { + val width = 40 + + val containerWidth = 5 + val containerHeight = 7 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Box { + Container( + constraints = Constraints( + maxWidth = containerWidth, + maxHeight = containerHeight, + ), + ) { + TestFiller(Modifier.width(width)) + } + } + } + + val fillerNode = rootNode.children[0].children[0].children[0] + assertThat(fillerNode.size).isEqualTo(IntSize(containerWidth, 0)) + } + + @Test fun fillerHeight() { + val height = 7 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Container(constraints = bigConstraints) { + TestFiller(Modifier.height(height)) + } + } + + val fillerNode = rootNode.children[0].children[0] + assertThat(fillerNode.size).isEqualTo(IntSize(0, height)) + } + + @Test fun fillerHeightWithSmallerContainer() { + val height = 23 + + val containerWidth = 5 + val containerHeight = 7 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Box { + Container( + constraints = Constraints( + maxWidth = containerWidth, + maxHeight = containerHeight, + ), + ) { + TestFiller(Modifier.height(height)) + } + } + } + + val fillerNode = rootNode.children[0].children[0].children[0] + assertThat(fillerNode.size).isEqualTo(IntSize(0, containerHeight)) + } + + @Test fun fillerDebug() { + val actual = mosaicNodes { + TestFiller() + } + + assertThat(actual.toString()).isEqualTo( + """ + |Filler('$TestChar') x=0 y=0 w=0 h=0 DrawBehind + """.trimMargin(), + ) + } +} diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/RowColumnModifierTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/RowColumnModifierTest.kt new file mode 100644 index 00000000..96ee2074 --- /dev/null +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/RowColumnModifierTest.kt @@ -0,0 +1,215 @@ +package com.jakewharton.mosaic.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.jakewharton.mosaic.layout.height +import com.jakewharton.mosaic.layout.size +import com.jakewharton.mosaic.layout.width +import com.jakewharton.mosaic.layout.wrapContentHeight +import com.jakewharton.mosaic.layout.wrapContentWidth +import com.jakewharton.mosaic.modifier.Modifier +import com.jakewharton.mosaic.mosaicNodesWithMeasureAndPlace +import kotlin.test.Test + +class RowColumnModifierTest { + @Test fun rowUpdatesOnAlignmentChange() { + val count = 5 + var alignment by mutableStateOf(Alignment.Top) + + val content = @Composable { + Box(Modifier.size(100)) { + Row(Modifier.wrapContentHeight()) { + repeat(count) { index -> + Box(Modifier.width(20).height(if (index == count - 1) 10 else 20).align(alignment)) + } + } + } + } + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + val fifthChildBoxNode = rootNode.children[0].children[0].children[count - 1] + assertThat(fifthChildBoxNode.y).isEqualTo(0) + } + + alignment = Alignment.CenterVertically + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + val fifthChildBoxNode = rootNode.children[0].children[0].children[count - 1] + assertThat(fifthChildBoxNode.y).isEqualTo(5) + } + } + + @Test fun rowUpdatesOnWeightChange() { + val count = 5 + var fill by mutableStateOf(false) + + val content = @Composable { + Box(Modifier.size(200)) { + Row(Modifier.wrapContentHeight()) { + repeat(count) { + Box(Modifier.size(20).weight(1f, fill)) + } + } + } + } + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + repeat(count) { index -> + val childBoxNode = rootNode.children[0].children[0].children[index] + assertThat(childBoxNode.width).isEqualTo(20) + } + } + + fill = true + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + repeat(count) { index -> + val childBoxNode = rootNode.children[0].children[0].children[index] + assertThat(childBoxNode.width).isEqualTo(40) + } + } + } + + @Test fun rowUpdatesOnWeightAndAlignmentChange() { + val count = 5 + var fill by mutableStateOf(false) + var alignment by mutableStateOf(Alignment.Top) + + val content = @Composable { + Box(Modifier.size(200)) { + Row(Modifier.wrapContentHeight()) { + repeat(count) { index -> + Box( + Modifier.width(20).height(if (index == count - 1) 10 else 20) + .weight(1f, fill) + .align(alignment), + ) + } + } + } + } + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + repeat(count) { index -> + val childBoxNode = rootNode.children[0].children[0].children[index] + assertThat(childBoxNode.width).isEqualTo(20) + } + val childBoxNode = rootNode.children[0].children[0].children[count - 1] + assertThat(childBoxNode.y).isEqualTo(0) + } + + alignment = Alignment.CenterVertically + fill = true + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + repeat(count) { index -> + val childBoxNode = rootNode.children[0].children[0].children[index] + assertThat(childBoxNode.width).isEqualTo(40) + } + val childBoxNode = rootNode.children[0].children[0].children[count - 1] + assertThat(childBoxNode.y).isEqualTo(5) + } + } + + @Test fun columnUpdatesOnAlignmentChange() { + val count = 5 + var alignment by mutableStateOf(Alignment.Start) + + val content = @Composable { + Box(Modifier.size(100)) { + Column(Modifier.wrapContentWidth().wrapContentHeight()) { + repeat(count) { index -> + Box(Modifier.height(20).width(if (index == count - 1) 10 else 20).align(alignment)) + } + } + } + } + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + val fifthChildBoxNode = rootNode.children[0].children[0].children[count - 1] + assertThat(fifthChildBoxNode.x).isEqualTo(0) + } + + alignment = Alignment.CenterHorizontally + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + val fifthChildBoxNode = rootNode.children[0].children[0].children[count - 1] + assertThat(fifthChildBoxNode.x).isEqualTo(5) + } + } + + @Test fun columnUpdatesOnWeightChange() { + val count = 5 + var fill by mutableStateOf(false) + + val content = @Composable { + Box(Modifier.size(200)) { + Column(Modifier.wrapContentHeight()) { + repeat(count) { + Box(Modifier.size(20).weight(1f, fill)) + } + } + } + } + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + repeat(count) { index -> + val childBoxNode = rootNode.children[0].children[0].children[index] + assertThat(childBoxNode.height).isEqualTo(20) + } + } + + fill = true + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + repeat(count) { index -> + val childBoxNode = rootNode.children[0].children[0].children[index] + assertThat(childBoxNode.height).isEqualTo(40) + } + } + } + + @Test fun columnUpdatesOnWeightAndAlignmentChange() { + val count = 5 + var fill by mutableStateOf(false) + var alignment by mutableStateOf(Alignment.Start) + + val content = @Composable { + Box(Modifier.size(200)) { + Column(Modifier.wrapContentHeight()) { + repeat(count) { index -> + Box( + Modifier.height(20).width(if (index == count - 1) 10 else 20).weight(1f, fill) + .align(alignment), + ) + } + } + } + } + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + repeat(count) { index -> + val childBoxNode = rootNode.children[0].children[0].children[index] + assertThat(childBoxNode.height).isEqualTo(20) + } + val childBoxNode = rootNode.children[0].children[0].children[count - 1] + assertThat(childBoxNode.x).isEqualTo(0) + } + + alignment = Alignment.CenterHorizontally + fill = true + + mosaicNodesWithMeasureAndPlace(content).let { rootNode -> + repeat(count) { index -> + val childBoxNode = rootNode.children[0].children[0].children[index] + assertThat(childBoxNode.height).isEqualTo(40) + } + val childBoxNode = rootNode.children[0].children[0].children[count - 1] + assertThat(childBoxNode.x).isEqualTo(5) + } + } +} diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/SpacerTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/SpacerTest.kt new file mode 100644 index 00000000..1bc459fc --- /dev/null +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/SpacerTest.kt @@ -0,0 +1,165 @@ +package com.jakewharton.mosaic.ui + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.jakewharton.mosaic.Container +import com.jakewharton.mosaic.layout.height +import com.jakewharton.mosaic.layout.size +import com.jakewharton.mosaic.layout.width +import com.jakewharton.mosaic.modifier.Modifier +import com.jakewharton.mosaic.mosaicNodes +import com.jakewharton.mosaic.mosaicNodesWithMeasureAndPlace +import com.jakewharton.mosaic.renderMosaic +import com.jakewharton.mosaic.replaceLineEndingsWithCRLF +import com.jakewharton.mosaic.s +import com.jakewharton.mosaic.size +import com.jakewharton.mosaic.ui.unit.Constraints +import com.jakewharton.mosaic.ui.unit.IntSize +import kotlin.test.Test + +class SpacerTest { + private val bigConstraints = Constraints(maxWidth = 5000, maxHeight = 5000) + + @Test fun spacerFixed() { + val width = 4 + val height = 6 + + val actual = renderMosaic { + Spacer(Modifier.size(width = width, height = height)) + } + + assertThat(actual).isEqualTo( + """ + | $s + | $s + | $s + | $s + | $s + | $s + | + """.trimMargin().replaceLineEndingsWithCRLF(), + ) + } + + @Test fun spacerFixedSize() { + val width = 40 + val height = 71 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Container(constraints = bigConstraints) { + Spacer(Modifier.size(width = width, height = height)) + } + } + + val spacerNode = rootNode.children[0].children[0] + assertThat(spacerNode.size).isEqualTo(IntSize(width, height)) + } + + @Test fun spacerFixedWithSmallerContainer() { + val width = 40 + val height = 71 + + val containerWidth = 5 + val containerHeight = 7 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Box { + Container( + constraints = Constraints( + maxWidth = containerWidth, + maxHeight = containerHeight, + ), + ) { + Spacer(Modifier.size(width = width, height = height)) + } + } + } + + val spacerNode = rootNode.children[0].children[0].children[0] + assertThat(spacerNode.size).isEqualTo(IntSize(containerWidth, containerHeight)) + } + + @Test fun spacerWidth() { + val width = 71 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Container(constraints = bigConstraints) { + Spacer(Modifier.width(width)) + } + } + + val spacerNode = rootNode.children[0].children[0] + assertThat(spacerNode.size).isEqualTo(IntSize(width, 0)) + } + + @Test fun spacerWidthWithSmallerContainer() { + val width = 40 + + val containerWidth = 5 + val containerHeight = 7 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Box { + Container( + constraints = Constraints( + maxWidth = containerWidth, + maxHeight = containerHeight, + ), + ) { + Spacer(Modifier.width(width)) + } + } + } + + val spacerNode = rootNode.children[0].children[0].children[0] + assertThat(spacerNode.size).isEqualTo(IntSize(containerWidth, 0)) + } + + @Test fun spacerHeight() { + val height = 7 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Container(constraints = bigConstraints) { + Spacer(Modifier.height(height)) + } + } + + val spacerNode = rootNode.children[0].children[0] + assertThat(spacerNode.size).isEqualTo(IntSize(0, height)) + } + + @Test fun spacerHeightWithSmallerContainer() { + val height = 23 + + val containerWidth = 5 + val containerHeight = 7 + + val rootNode = mosaicNodesWithMeasureAndPlace { + Box { + Container( + constraints = Constraints( + maxWidth = containerWidth, + maxHeight = containerHeight, + ), + ) { + Spacer(Modifier.height(height)) + } + } + } + + val spacerNode = rootNode.children[0].children[0].children[0] + assertThat(spacerNode.size).isEqualTo(IntSize(0, containerHeight)) + } + + @Test fun spacerDebug() { + val actual = mosaicNodes { + Spacer() + } + + assertThat(actual.toString()).isEqualTo( + """ + |Spacer() x=0 y=0 w=0 h=0 + """.trimMargin(), + ) + } +}