From 02dd0f2ff1665cb9afada95dbb1b1d24b62d1db1 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 | 185 ++++++++++++++++++ .../UIViewRedwoodLayoutWidgetFactory.kt | 4 +- .../redwood/testing/presenter/BoxSandbox.kt | 2 +- 3 files changed, 187 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..b58a6828b1 --- /dev/null +++ b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewBox.kt @@ -0,0 +1,185 @@ +/* + * 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.Margin +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.UIView +import platform.darwin.NSInteger + +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 height(height: Constraint) { + value.heightConstraint = height + } + + 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 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 } + height = size.useContents { this.height } + } + Constraint.Wrap -> { + height = size.useContents { this.height } + + width = typedSubviews + .map { it.sizeThatFits(size).useContents { this.width } } + .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 } } + .max() + } + Constraint.Wrap -> { + val unconstrainedSizes = typedSubviews + .map { it.sizeThatFits(size) } + + 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 fe23cec09f..5435600d03 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 @@ -30,7 +30,7 @@ import com.example.redwood.testing.compose.Button import com.example.redwood.testing.compose.Text @Composable -fun BoxSandbox(modifier: Modifier = Modifier) { +fun BoxSandbox() { Column( width = Constraint.Fill, height = Constraint.Fill,