From bd83ca7a6b3d0a133e9281b3ca9761f19a647e6c Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Tue, 10 Oct 2023 15:41:44 -0600 Subject: [PATCH] Update UIView Box Implementation - Replaces Button with Rectangle in the Sandbox screen - Fix for certain combinations of Wrap + Stretch not rendering correctly - Introduce handling for explicit width and height modifiers --- .../cash/redwood/layout/uiview/UIViewBox.kt | 86 +++++--- .../redwood/testing/presenter/BoxSandbox.kt | 188 ++++++++++++++---- 2 files changed, 211 insertions(+), 63 deletions(-) diff --git a/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewBox.kt b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewBox.kt index b58a6828b1..43e5938139 100644 --- a/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewBox.kt +++ b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewBox.kt @@ -18,11 +18,15 @@ package app.cash.redwood.layout.uiview import app.cash.redwood.Modifier import app.cash.redwood.layout.api.Constraint import app.cash.redwood.layout.api.CrossAxisAlignment +import app.cash.redwood.layout.modifier.Height import app.cash.redwood.layout.modifier.HorizontalAlignment import app.cash.redwood.layout.modifier.VerticalAlignment +import app.cash.redwood.layout.modifier.Width import app.cash.redwood.layout.widget.Box import app.cash.redwood.ui.Margin +import app.cash.redwood.ui.toPlatformDp import app.cash.redwood.widget.UIViewChildren +import kotlin.math.max import kotlinx.cinterop.CValue import kotlinx.cinterop.convert import kotlinx.cinterop.readValue @@ -89,12 +93,14 @@ internal class UIViewBox : Box { children.widgets.forEach { val view = it.value view.sizeToFit() - var childWidth: CGFloat = view.frame.useContents { this.size.width } - var childHeight: CGFloat = view.frame.useContents { this.size.height } - // Check for modifier overrides in the children, otherwise default to the Box's alignment values + // Check for modifier overrides in the children, otherwise default to the Box's alignment values. var itemHorizontalAlignment = horizontalAlignment var itemVerticalAlignment = verticalAlignment + + var requestedWidth: CGFloat? = null + var requestedHeight: CGFloat? = null + it.modifier.forEach { childModifier -> when (childModifier) { is HorizontalAlignment -> { @@ -103,10 +109,20 @@ internal class UIViewBox : Box { is VerticalAlignment -> { itemVerticalAlignment = childModifier.alignment } + is Width -> { + requestedWidth = childModifier.width.toPlatformDp() + } + is Height -> { + requestedHeight = childModifier.height.toPlatformDp() + } } } - // Compute origin and stretch if needed + // Use requested modifiers, otherwise use the size established from sizeToFit(). + var childWidth: CGFloat = requestedWidth ?: view.frame.useContents { this.size.width } + var childHeight: CGFloat = requestedHeight ?: view.frame.useContents { this.size.height } + + // Compute origin and stretch if needed. var x: CGFloat = 0.0 var y: CGFloat = 0.0 when (itemHorizontalAlignment) { @@ -128,58 +144,76 @@ internal class UIViewBox : Box { CrossAxisAlignment.End -> y = frame.useContents { this.size.height } - childHeight } - // Position the view + // Position the view. view.setFrame(CGRectMake(x, y, childWidth, childHeight)) } } override fun sizeThatFits(size: CValue): CValue { - var width: CGFloat = 0.0 - var height: CGFloat = 0.0 + var maxItemWidth: CGFloat = 0.0 + var maxItemHeight: CGFloat = 0.0 + + var maxRequestedWidth: CGFloat = 0.0 + var maxRequestedHeight: CGFloat = 0.0 - // Calculate the size based on Constraint Values + // Get the largest sizes based on explicit widget modifiers. + children.widgets.forEach { + it.modifier.forEach { childModifier -> + when (childModifier) { + is Width -> { + if (childModifier.width.value > maxRequestedWidth) { + maxRequestedWidth = childModifier.width.value + } + } + is Height -> { + if (childModifier.height.value > maxRequestedHeight) { + maxRequestedHeight = childModifier.height.value + } + } + } + } + } + + // Calculate the size based on Constraint values. when (widthConstraint) { Constraint.Fill -> { when (heightConstraint) { - Constraint.Fill -> { - width = size.useContents { this.width } - height = size.useContents { this.height } + Constraint.Fill -> { // Fill Fill + maxItemWidth = size.useContents { this.width } + maxItemHeight = size.useContents { this.height } } - Constraint.Wrap -> { - height = size.useContents { this.height } - - width = typedSubviews - .map { it.sizeThatFits(size).useContents { this.width } } + Constraint.Wrap -> { // Fill Wrap + maxItemWidth = size.useContents { this.width } + maxItemHeight = typedSubviews + .map { it.sizeThatFits(size).useContents { this.height } } .max() } } } Constraint.Wrap -> { when (heightConstraint) { - Constraint.Fill -> { - width = size.useContents { this.width } - - // calculate the height of the biggest item - height = typedSubviews - .map { it.sizeThatFits(size).useContents { this.height } } + Constraint.Fill -> { // Wrap Fill + maxItemWidth = typedSubviews + .map { it.sizeThatFits(size).useContents { this.width } } .max() + maxItemHeight = size.useContents { this.height } } - Constraint.Wrap -> { + Constraint.Wrap -> { // Wrap Wrap val unconstrainedSizes = typedSubviews .map { it.sizeThatFits(size) } - width = unconstrainedSizes + maxItemWidth = unconstrainedSizes .map { it.useContents { this.width } } .max() - height = unconstrainedSizes + maxItemHeight = unconstrainedSizes .map { it.useContents { this.height } } .max() } } } } - return CGSizeMake(width, height) + return CGSizeMake(max(maxRequestedWidth, maxItemWidth), max(maxRequestedHeight, maxItemHeight)) } } } diff --git a/test-app/presenter/src/commonMain/kotlin/com/example/redwood/testing/presenter/BoxSandbox.kt b/test-app/presenter/src/commonMain/kotlin/com/example/redwood/testing/presenter/BoxSandbox.kt index 5435600d03..ae496177a2 100644 --- a/test-app/presenter/src/commonMain/kotlin/com/example/redwood/testing/presenter/BoxSandbox.kt +++ b/test-app/presenter/src/commonMain/kotlin/com/example/redwood/testing/presenter/BoxSandbox.kt @@ -18,54 +18,78 @@ package com.example.redwood.testing.presenter import androidx.compose.runtime.Composable import app.cash.redwood.Modifier import app.cash.redwood.layout.api.Constraint +import app.cash.redwood.layout.api.Constraint.Companion.Fill +import app.cash.redwood.layout.api.Constraint.Companion.Wrap import app.cash.redwood.layout.api.CrossAxisAlignment +import app.cash.redwood.layout.api.CrossAxisAlignment.Companion.Center +import app.cash.redwood.layout.api.CrossAxisAlignment.Companion.End +import app.cash.redwood.layout.api.CrossAxisAlignment.Companion.Start +import app.cash.redwood.layout.api.CrossAxisAlignment.Companion.Stretch import app.cash.redwood.layout.api.MainAxisAlignment +import app.cash.redwood.layout.api.MainAxisAlignment.Companion.SpaceBetween import app.cash.redwood.layout.api.Overflow import app.cash.redwood.layout.compose.Box import app.cash.redwood.layout.compose.Column +import app.cash.redwood.layout.compose.ColumnScope import app.cash.redwood.layout.compose.Row +import app.cash.redwood.layout.compose.Spacer import app.cash.redwood.ui.Margin import app.cash.redwood.ui.dp -import com.example.redwood.testing.compose.Button +import com.example.redwood.testing.compose.Rectangle import com.example.redwood.testing.compose.Text +private val accentColor = 0xFFDDDDDDu +private val rowColor = 0xFFDDDDDDu +private val boxColor = 0xFFFFFF66u +private val backColor = 0x88FF0000u +private val middleColor = 0x8800FF00u +private val frontColor = 0x880000FFu + +private val rowHeight = 80.dp + @Composable fun BoxSandbox() { Column( - width = Constraint.Fill, - height = Constraint.Fill, + width = Fill, + height = Fill, overflow = Overflow.Scroll, - horizontalAlignment = CrossAxisAlignment.Stretch, + horizontalAlignment = Stretch, verticalAlignment = MainAxisAlignment.Start, ) { - val crossAxisAlignments = listOf( - CrossAxisAlignment.Start, - CrossAxisAlignment.Center, - CrossAxisAlignment.Stretch, - CrossAxisAlignment.End, + val crossAxisAlignments = listOf( + Start, + Center, + Stretch, + End, ) - val constraints = listOf( - Constraint.Fill, - Constraint.Wrap, + val constraints = listOf( + Fill, + Wrap, ) + Legend() + +// Uncomment to debug a specific permutation. +// BoxRow( +// width = Wrap, +// height = Wrap, +// horizontalAlignment = Start, +// verticalAlignment = Stretch, +// modifier = Modifier.height(140.dp), +// ) + // Iterate over all permutations - constraints.forEach { - val widthConstraint = it - constraints.forEach { - val heightConstraint = it - crossAxisAlignments.forEach { - val horizontalAlignment = it - crossAxisAlignments.forEach { - val verticalAlignment = it - Text("$widthConstraint $heightConstraint $horizontalAlignment $verticalAlignment") + constraints.forEach { widthConstraint -> + constraints.forEach { heightConstraint -> + crossAxisAlignments.forEach { horizontalAlignment -> + crossAxisAlignments.forEach { verticalAlignment -> BoxRow( width = widthConstraint, height = heightConstraint, horizontalAlignment = horizontalAlignment, verticalAlignment = verticalAlignment, - modifier = Modifier.height(120.dp), + modifier = Modifier.height(rowHeight), ) } } @@ -75,33 +99,123 @@ fun BoxSandbox() { } @Composable -private fun BoxRow( +private fun ColumnScope.Legend() { + Text( + "Legend", + modifier = Modifier.horizontalAlignment(Center).height(40.dp), + ) + Box( + width = Fill, + height = Wrap, + horizontalAlignment = Stretch, + verticalAlignment = Start, + modifier = Modifier.height(80.dp).margin(Margin(horizontal = 20.dp)), + ) { + Rectangle( + backgroundColor = accentColor, + cornerRadius = 12f, + modifier = Modifier.horizontalAlignment(Stretch).verticalAlignment(Stretch), + ) + Column( + width = Fill, + horizontalAlignment = Center, + ) { + Text( + "Constraints [x y] | Alignments [x y]", + modifier = Modifier.horizontalAlignment(Center).height(40.dp), + ) + Row( + horizontalAlignment = SpaceBetween, + verticalAlignment = Center, + ) { + Rectangle( + backgroundColor = boxColor, + cornerRadius = 4f, + modifier = Modifier.width(20.dp).height(20.dp), + ) + Text(" Box") + Spacer(width = 12.dp) + Rectangle( + backgroundColor = backColor, + cornerRadius = 4f, + modifier = Modifier.width(20.dp).height(20.dp), + ) + Text(" Back") + Spacer(width = 12.dp) + Rectangle( + backgroundColor = middleColor, + cornerRadius = 4f, + modifier = Modifier.width(20.dp).height(20.dp), + ) + Text(" Middle") + Spacer(width = 12.dp) + Rectangle( + backgroundColor = frontColor, + cornerRadius = 4f, + modifier = Modifier.width(20.dp).height(20.dp), + ) + Text(" Front") + } + } + } +} + +@Composable +private fun ColumnScope.BoxRow( width: Constraint, height: Constraint, horizontalAlignment: CrossAxisAlignment, verticalAlignment: CrossAxisAlignment, modifier: Modifier = Modifier, ) { + // Divider + Rectangle(accentColor, modifier = Modifier.height(1.dp).horizontalAlignment(Stretch).margin(Margin(top = 20.dp))) + Text( + "$width $height | $horizontalAlignment $verticalAlignment", + modifier = Modifier.horizontalAlignment(Center).height(40.dp), + ) Column( - width = Constraint.Fill, - height = Constraint.Fill, - horizontalAlignment = CrossAxisAlignment.Start, + width = Fill, + height = Fill, + horizontalAlignment = Start, margin = Margin(horizontal = 20.dp, vertical = 0.dp), modifier = modifier, ) { - Row( - width = Constraint.Fill, - height = Constraint.Fill, + Box( + width = Fill, + height = Fill, + horizontalAlignment = Stretch, + verticalAlignment = Stretch, ) { - Box( - width = width, - height = height, - horizontalAlignment = horizontalAlignment, - verticalAlignment = verticalAlignment, + Rectangle(backgroundColor = rowColor) + Row( + width = Fill, + height = Fill, ) { - Button("BACK - BACK - BACK\nBACK - BACK - BACK\nBACK - BACK - BACK", onClick = null) - Button("MIDDLE - MIDDLE\nMIDDLE - MIDDLE", onClick = null) - Button("FRONT", onClick = null) + // This is the box we're measuring. + Box( + width = width, + height = height, + horizontalAlignment = horizontalAlignment, + verticalAlignment = verticalAlignment, + // TODO: Add backgroundColor = boxColor once the modifiers are available. + ) { + Rectangle( + backgroundColor = backColor, + cornerRadius = 12f, + modifier = Modifier.width((rowHeight.value * 1.4).dp).height((rowHeight.value * 0.8).dp), + ) + Rectangle( + backgroundColor = middleColor, + cornerRadius = 12f, + modifier = Modifier.width((rowHeight.value * 1.2).dp).height((rowHeight.value * 0.6).dp), + ) + Rectangle( + backgroundColor = frontColor, + cornerRadius = 12f, + modifier = Modifier.width((rowHeight.value).dp).height((rowHeight.value * 0.4).dp), + ) + } } } }