From f4fcc6509ede5d8da3cbc7458d8c7a72535d5c79 Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Fri, 29 Sep 2023 16:19:58 -0700 Subject: [PATCH] Add UIViewBox Implementation --- .../cash/redwood/layout/uiview/UIViewBox.kt | 216 ++++++++++++++++++ .../UIViewRedwoodLayoutWidgetFactory.kt | 4 +- .../redwood/testing/presenter/BoxSandbox.kt | 2 +- 3 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewBox.kt 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 new file mode 100644 index 0000000000..56d986ec6e --- /dev/null +++ b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewBox.kt @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2022 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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.HorizontalAlignment +import app.cash.redwood.layout.modifier.VerticalAlignment +import app.cash.redwood.layout.widget.Box +import app.cash.redwood.ui.Dp +import app.cash.redwood.ui.Margin +import app.cash.redwood.ui.dp +import app.cash.redwood.widget.UIViewChildren +import kotlinx.cinterop.CValue +import kotlinx.cinterop.convert +import kotlinx.cinterop.readValue +import kotlinx.cinterop.useContents +import platform.CoreGraphics.CGFloat +import platform.CoreGraphics.CGRectMake +import platform.CoreGraphics.CGRectZero +import platform.CoreGraphics.CGSize +import platform.CoreGraphics.CGSizeMake +import platform.UIKit.NSLayoutConstraint +import platform.UIKit.UIColor +import platform.UIKit.UIView +import platform.darwin.NSInteger +import platform.posix.INT_MAX + +internal class UIViewBox : Box { + override val value: View = View() + + override var modifier: Modifier = Modifier + + override val children = value.children + + override fun width(width: Constraint) { + value.widthConstraint = width + } + + override fun minWidth(minWidth: Dp) { + value.minWidth = minWidth + } + + override fun maxWidth(maxWidth: Dp) { + value.maxWidth = maxWidth + } + + override fun height(height: Constraint) { + value.heightConstraint = height + } + + override fun minHeight(minHeight: Dp) { + value.minHeight = minHeight + } + + override fun maxHeight(maxHeight: Dp) { + value.maxHeight = maxHeight + } + + override fun margin(margin: Margin) { + } + + override fun horizontalAlignment(horizontalAlignment: CrossAxisAlignment) { + value.horizontalAlignment = horizontalAlignment + } + + override fun verticalAlignment(verticalAlignment: CrossAxisAlignment) { + value.verticalAlignment = verticalAlignment + } + + internal class View() : UIView(CGRectZero.readValue()) { + var widthConstraint = Constraint.Wrap + var heightConstraint = Constraint.Wrap + + var minWidth = 0.dp + var maxWidth = INT_MAX.dp + + var minHeight = 0.dp + var maxHeight = INT_MAX.dp + + var horizontalAlignment = CrossAxisAlignment.Start + var verticalAlignment = CrossAxisAlignment.Start + + val children = UIViewChildren( + this, + insert = { view, index -> + insertSubview(view, index.convert()) + view.setNeedsLayout() + }, + remove = { index, count -> + val views = Array(count) { + typedSubviews[index].also(UIView::removeFromSuperview) + } + setNeedsLayout() + return@UIViewChildren views + }, + ) + + override fun layoutSubviews() { + super.layoutSubviews() + + 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 + var itemHorizontalAlignment = horizontalAlignment + var itemVerticalAlignment = verticalAlignment + it.modifier.forEach { childModifier -> + when (childModifier) { + is HorizontalAlignment -> { + itemHorizontalAlignment = childModifier.alignment + } + is VerticalAlignment -> { + itemVerticalAlignment = childModifier.alignment + } + } + } + + // Compute origin and stretch if needed + var x: CGFloat = 0.0 + var y: CGFloat = 0.0 + when (itemHorizontalAlignment) { + CrossAxisAlignment.Stretch -> { + x = 0.0 + childWidth = frame.useContents { this.size.width } + } + CrossAxisAlignment.Start -> x = 0.0 + CrossAxisAlignment.Center -> x = (frame.useContents { this.size.width } - childWidth) / 2.0 + CrossAxisAlignment.End -> x = frame.useContents { this.size.width } - childWidth + } + when (itemVerticalAlignment) { + CrossAxisAlignment.Stretch -> { + y = 0.0 + childHeight = frame.useContents { this.size.height } + } + CrossAxisAlignment.Start -> y = 0.0 + CrossAxisAlignment.Center -> y = (frame.useContents { this.size.height } - childHeight) / 2.0 + CrossAxisAlignment.End -> y = frame.useContents { this.size.height } - childHeight + } + + // 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 + + // Calculate the size based on Constraint Values + when (widthConstraint) { + Constraint.Fill -> { + when (heightConstraint) { + Constraint.Fill -> { + width = size.useContents { this.width.coerceIn(minWidth.value, maxWidth.value) } + height = size.useContents { this.height.coerceIn(minHeight.value, maxHeight.value) } + } + Constraint.Wrap -> { + height = size.useContents { this.height.coerceIn(minWidth.value, maxWidth.value) } + + // calculate the width of the biggest item + val sizeToFit = CGSizeMake(maxWidth.value, height) + width = typedSubviews + .map { it.sizeThatFits(sizeToFit).useContents { this.width } } + .max() + } + } + } + Constraint.Wrap -> { + when (heightConstraint) { + Constraint.Fill -> { + width = size.useContents { this.width.coerceIn(minWidth.value, maxWidth.value) } + + // calculate the height of the biggest item + val sizeToFit = CGSizeMake(width, maxHeight.value) + height = typedSubviews + .map { it.sizeThatFits(sizeToFit).useContents { this.height } } + .max() + } + Constraint.Wrap -> { + val unconstrainedSizes = typedSubviews + .map { it.sizeThatFits(CGSizeMake(maxWidth.value, maxHeight.value)) } + + width = unconstrainedSizes + .map { it.useContents { this.width } } + .max() + + height = unconstrainedSizes + .map { it.useContents { this.height } } + .max() + } + } + } + } + return CGSizeMake(width, height) + } + } +} + diff --git a/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewRedwoodLayoutWidgetFactory.kt b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewRedwoodLayoutWidgetFactory.kt index e77ee111eb..1cbb7651ff 100644 --- a/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewRedwoodLayoutWidgetFactory.kt +++ b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewRedwoodLayoutWidgetFactory.kt @@ -25,9 +25,7 @@ import platform.UIKit.UIView @ObjCName("UIViewRedwoodLayoutWidgetFactory", exact = true) public class UIViewRedwoodLayoutWidgetFactory : RedwoodLayoutWidgetFactory { - override fun Box(): Box { - TODO("Not yet implemented") - } + override fun Box(): Box = UIViewBox() override fun Column(): Column = UIViewFlexContainer(FlexDirection.Column) override fun Row(): Row = UIViewFlexContainer(FlexDirection.Row) override fun Spacer(): Spacer = UIViewSpacer() 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 b8f3e07a4d..6bbe54bf57 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 @@ -34,7 +34,7 @@ import com.example.redwood.testing.compose.Button2 import com.example.redwood.testing.compose.Text @Composable -fun BoxSandbox(modifier: Modifier = Modifier) { +fun BoxSandbox() { Column( width = Constraint.Fill, height = Constraint.Fill,