diff --git a/test-app/android-views/src/main/kotlin/com/example/redwood/testing/android/views/AndroidTestSchemaWidgetFactory.kt b/test-app/android-views/src/main/kotlin/com/example/redwood/testing/android/views/AndroidTestSchemaWidgetFactory.kt index 978b3a5f97..b425304709 100644 --- a/test-app/android-views/src/main/kotlin/com/example/redwood/testing/android/views/AndroidTestSchemaWidgetFactory.kt +++ b/test-app/android-views/src/main/kotlin/com/example/redwood/testing/android/views/AndroidTestSchemaWidgetFactory.kt @@ -17,7 +17,9 @@ package com.example.redwood.testing.android.views import android.content.Context import android.view.View +import android.widget.Button as ButtonWidget import android.widget.TextView +import com.example.redwood.testing.widget.Button import com.example.redwood.testing.widget.TestSchemaWidgetFactory import com.example.redwood.testing.widget.Text @@ -27,7 +29,7 @@ class AndroidTestSchemaWidgetFactory( override fun Text(): Text = ViewText(TextView(context)) override fun TestRow() = throw UnsupportedOperationException() override fun ScopedTestRow() = throw UnsupportedOperationException() - override fun Button() = TODO() + override fun Button(): Button = ViewButton(ButtonWidget(context)) override fun Button2() = TODO() override fun TextInput() = TODO() } diff --git a/test-app/android-views/src/main/kotlin/com/example/redwood/testing/android/views/ViewButton.kt b/test-app/android-views/src/main/kotlin/com/example/redwood/testing/android/views/ViewButton.kt new file mode 100644 index 0000000000..d1cdee2994 --- /dev/null +++ b/test-app/android-views/src/main/kotlin/com/example/redwood/testing/android/views/ViewButton.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 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 com.example.redwood.testing.android.views + +import android.view.View +import android.widget.Button as WidgetButton +import app.cash.redwood.Modifier +import com.example.redwood.testing.widget.Button + +internal class ViewButton( + override val value: WidgetButton, +) : Button { + override var modifier: Modifier = Modifier + + override fun text(text: String?) { + value.text = text + } + + override fun onClick(onClick: (() -> Unit)?) { + value.setOnClickListener( + if (onClick != null) { + { onClick.invoke() } + } else { + null + }, + ) + } +} diff --git a/test-app/browser/src/commonMain/kotlin/com/example/redwood/testing/browser/main.kt b/test-app/browser/src/commonMain/kotlin/com/example/redwood/testing/browser/main.kt index d959c7ba95..4b936f07b9 100644 --- a/test-app/browser/src/commonMain/kotlin/com/example/redwood/testing/browser/main.kt +++ b/test-app/browser/src/commonMain/kotlin/com/example/redwood/testing/browser/main.kt @@ -21,7 +21,7 @@ import app.cash.redwood.layout.dom.HTMLElementRedwoodLayoutWidgetFactory import app.cash.redwood.lazylayout.dom.HTMLElementRedwoodLazyLayoutWidgetFactory import app.cash.redwood.widget.asRedwoodView import com.example.redwood.testing.presenter.HttpClient -import com.example.redwood.testing.presenter.RepoSearch +import com.example.redwood.testing.presenter.TestApp import com.example.redwood.testing.widget.TestSchemaWidgetFactories import kotlin.js.json import kotlinx.browser.document @@ -54,6 +54,6 @@ fun main() { } composition.setContent { - RepoSearch(client) + TestApp(client) } } diff --git a/test-app/ios-uikit/TestApp.xcodeproj/project.pbxproj b/test-app/ios-uikit/TestApp.xcodeproj/project.pbxproj index 2f87db8079..47135d3742 100644 --- a/test-app/ios-uikit/TestApp.xcodeproj/project.pbxproj +++ b/test-app/ios-uikit/TestApp.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 635661DC21F12B8000DD7240 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 635661DB21F12B8000DD7240 /* Assets.xcassets */; }; 635661DF21F12B8000DD7240 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 635661DD21F12B8000DD7240 /* LaunchScreen.storyboard */; }; CB85C0B725AFE61A007A2CC7 /* TestAppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB85C0B625AFE61A007A2CC7 /* TestAppViewController.swift */; }; + CB9E3E822AB379C4007A87CD /* ButtonBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9E3E812AB379C4007A87CD /* ButtonBinding.swift */; }; CB9F76562810A8A8008CF457 /* IosHostApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9F76552810A8A8008CF457 /* IosHostApi.swift */; }; /* End PBXBuildFile section */ @@ -28,6 +29,7 @@ 635661E021F12B8000DD7240 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 63E90CF521FEBBB700449E04 /* main.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = main.framework; path = "../ios-shared/build/xcode-frameworks/main.framework"; sourceTree = ""; }; CB85C0B625AFE61A007A2CC7 /* TestAppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppViewController.swift; sourceTree = ""; }; + CB9E3E812AB379C4007A87CD /* ButtonBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonBinding.swift; sourceTree = ""; }; CB9F76552810A8A8008CF457 /* IosHostApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosHostApi.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -71,6 +73,7 @@ CB9F76552810A8A8008CF457 /* IosHostApi.swift */, 10AA3D4B28C03D32006F125E /* IosTestSchemaWidgetFactory.swift */, 10AA3D4D28C0EA40006F125E /* TextBinding.swift */, + CB9E3E812AB379C4007A87CD /* ButtonBinding.swift */, ); path = TestApp; sourceTree = ""; @@ -169,6 +172,7 @@ buildActionMask = 2147483647; files = ( 10AA3D4C28C03D32006F125E /* IosTestSchemaWidgetFactory.swift in Sources */, + CB9E3E822AB379C4007A87CD /* ButtonBinding.swift in Sources */, CB85C0B725AFE61A007A2CC7 /* TestAppViewController.swift in Sources */, 10AA3D4E28C0EA40006F125E /* TextBinding.swift in Sources */, 635661D521F12B7E00DD7240 /* AppDelegate.swift in Sources */, diff --git a/test-app/ios-uikit/TestApp/ButtonBinding.swift b/test-app/ios-uikit/TestApp/ButtonBinding.swift new file mode 100644 index 0000000000..e4ea479304 --- /dev/null +++ b/test-app/ios-uikit/TestApp/ButtonBinding.swift @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 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. + */ + +import Foundation +import TestAppKt +import UIKit + +class ButtonBinding: Button { + private let root: UIButton = { + let view = UIButton() + view.backgroundColor = UIColor.gray + return view + }() + + var modifier: Modifier = ExposedKt.modifier() + var value: Any { root } + var onClick: (() -> Void)? = nil + + func text(text: String?) { + root.setTitle(text, for: .normal) + + // This very simple integration wraps the size of whatever text is entered. Calling + // this function will update the bounds and trigger relayout in the parent. + root.sizeToFit() + } + + func onClick(onClick: (() -> Void)? = nil) { + self.onClick = onClick + if (onClick != nil) { + root.addTarget(self, action: #selector(clicked), for: .touchUpInside) + } else { + root.removeTarget(self, action: #selector(clicked), for: .touchUpInside) + } + } + + @objc func clicked() { + if (self.onClick != nil) { + self.onClick() + } + } +} diff --git a/test-app/ios-uikit/TestApp/IosTestSchemaWidgetFactory.swift b/test-app/ios-uikit/TestApp/IosTestSchemaWidgetFactory.swift index 62fdc626b8..851e118f5b 100644 --- a/test-app/ios-uikit/TestApp/IosTestSchemaWidgetFactory.swift +++ b/test-app/ios-uikit/TestApp/IosTestSchemaWidgetFactory.swift @@ -19,15 +19,7 @@ import Foundation import UIKit import TestAppKt -class IosTestSchemaWidgetFactory: TestSchemaWidgetFactory { - let treehouseApp: TreehouseApp - let widgetSystem: TreehouseViewWidgetSystem - - init(treehouseApp: TreehouseApp, widgetSystem: TreehouseViewWidgetSystem) { - self.treehouseApp = treehouseApp - self.widgetSystem = widgetSystem - } - +class IosTestSchemaWidgetFactory: TestSchemaWidgetFactory { func TextInput() -> TextInput { fatalError() } @@ -37,7 +29,7 @@ class IosTestSchemaWidgetFactory: TestSchemaWidgetFactory { } func Button() -> Button { - fatalError() + return ButtonBinding() } func Button2() -> Button2 { diff --git a/test-app/ios-uikit/TestApp/TestAppViewController.swift b/test-app/ios-uikit/TestApp/TestAppViewController.swift index d23b08430b..e89c48808a 100644 --- a/test-app/ios-uikit/TestApp/TestAppViewController.swift +++ b/test-app/ios-uikit/TestApp/TestAppViewController.swift @@ -35,7 +35,7 @@ class TestAppViewController : UIViewController { override func loadView() { let testAppLauncher = TestAppLauncher(nsurlSession: urlSession, hostApi: IosHostApi()) let treehouseApp = testAppLauncher.createTreehouseApp() - let widgetSystem = TestSchemaWidgetSystem(treehouseApp: treehouseApp) + let widgetSystem = TestSchemaWidgetSystem() let treehouseView = TreehouseUIView(widgetSystem: widgetSystem) let content = treehouseApp.createContent( source: TestAppContent(), @@ -54,19 +54,13 @@ class TestAppContent : TreehouseContentSource { } class TestSchemaWidgetSystem : TreehouseViewWidgetSystem { - let treehouseApp: TreehouseApp - - init(treehouseApp: TreehouseApp) { - self.treehouseApp = treehouseApp - } - func widgetFactory( json: Kotlinx_serialization_jsonJson, protocolMismatchHandler: ProtocolMismatchHandler ) -> ProtocolNodeFactory { return TestSchemaProtocolNodeFactory( provider: TestSchemaWidgetFactories( - TestSchema: IosTestSchemaWidgetFactory(treehouseApp: treehouseApp, widgetSystem: self), + TestSchema: IosTestSchemaWidgetFactory(), RedwoodLayout: UIViewRedwoodLayoutWidgetFactory(), RedwoodLazyLayout: UIViewRedwoodLazyLayoutWidgetFactory() ), diff --git a/test-app/presenter-treehouse/src/jsMain/kotlin/com/example/redwood/testing/treehouse/TestAppTreehouseUi.kt b/test-app/presenter-treehouse/src/jsMain/kotlin/com/example/redwood/testing/treehouse/TestAppTreehouseUi.kt index 400acdddf3..b1407f0e59 100644 --- a/test-app/presenter-treehouse/src/jsMain/kotlin/com/example/redwood/testing/treehouse/TestAppTreehouseUi.kt +++ b/test-app/presenter-treehouse/src/jsMain/kotlin/com/example/redwood/testing/treehouse/TestAppTreehouseUi.kt @@ -18,13 +18,13 @@ package com.example.redwood.testing.treehouse import androidx.compose.runtime.Composable import app.cash.redwood.treehouse.TreehouseUi import com.example.redwood.testing.presenter.HttpClient -import com.example.redwood.testing.presenter.RepoSearch +import com.example.redwood.testing.presenter.TestApp class TestAppTreehouseUi( private val httpClient: HttpClient, ) : TreehouseUi { @Composable override fun Show() { - RepoSearch(httpClient) + TestApp(httpClient) } } diff --git a/test-app/presenter/src/commonMain/kotlin/com/example/redwood/testing/presenter/RepoSearch.kt b/test-app/presenter/src/commonMain/kotlin/com/example/redwood/testing/presenter/RepoSearch.kt index 9511a6b083..34ef94b252 100644 --- a/test-app/presenter/src/commonMain/kotlin/com/example/redwood/testing/presenter/RepoSearch.kt +++ b/test-app/presenter/src/commonMain/kotlin/com/example/redwood/testing/presenter/RepoSearch.kt @@ -27,6 +27,7 @@ import app.cash.paging.PagingSourceLoadResult import app.cash.paging.PagingSourceLoadResultPage import app.cash.paging.PagingState import app.cash.paging.compose.collectAsLazyPagingItems +import app.cash.redwood.Modifier import app.cash.redwood.layout.api.Constraint import app.cash.redwood.lazylayout.compose.LazyColumn import kotlinx.serialization.json.Json @@ -38,7 +39,7 @@ private val pagingConfig = PagingConfig(pageSize = 20, initialLoadSize = 20).app } @Composable -fun RepoSearch(httpClient: HttpClient) { +internal fun RepoSearch(httpClient: HttpClient, modifier: Modifier = Modifier) { // TODO Make term interactive with TextInput. val latestSearchTerm by remember { mutableStateOf("android") } @@ -51,6 +52,8 @@ fun RepoSearch(httpClient: HttpClient) { val lazyPagingItems = pager.flow.collectAsLazyPagingItems() LazyColumn( width = Constraint.Fill, + height = Constraint.Fill, + modifier = modifier, placeholder = { RepositoryItem(Repository(fullName = "Placeholder…", 0)) }, ) { items(lazyPagingItems.itemCount) { index -> diff --git a/test-app/presenter/src/commonMain/kotlin/com/example/redwood/testing/presenter/TestApp.kt b/test-app/presenter/src/commonMain/kotlin/com/example/redwood/testing/presenter/TestApp.kt new file mode 100644 index 0000000000..28a2eded37 --- /dev/null +++ b/test-app/presenter/src/commonMain/kotlin/com/example/redwood/testing/presenter/TestApp.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 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 com.example.redwood.testing.presenter + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import app.cash.redwood.Modifier +import app.cash.redwood.layout.api.Constraint +import app.cash.redwood.layout.api.Constraint.Companion +import app.cash.redwood.layout.api.CrossAxisAlignment +import app.cash.redwood.layout.api.Overflow +import app.cash.redwood.layout.compose.Column +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.Text + +@Composable +fun TestApp(httpClient: HttpClient) { + val screen = remember { mutableStateOf(null) } + val activeScreen = screen.value + if (activeScreen == null) { + HomeScreen(screen) + } else { + Column( + width = Constraint.Fill, + height = Constraint.Fill, + ) { + Button("Back", onClick = { screen.value = null }) + activeScreen.Show(httpClient, modifier = Modifier.grow(1.0)) + } + } +} + +@Suppress("unused") // Used via reflection. +enum class Screen { + RepoSearch { + @Composable + override fun Show(httpClient: HttpClient, modifier: Modifier) { + RepoSearch(httpClient, modifier) + } + }, + ; + + @Composable + abstract fun Show(httpClient: HttpClient, modifier: Modifier) +} + +@Composable +private fun HomeScreen(screen: MutableState) { + Column( + width = Constraint.Fill, + height = Companion.Fill, + overflow = Overflow.Scroll, + horizontalAlignment = CrossAxisAlignment.Stretch, + ) { + Text("Test App Screens:", modifier = Modifier.margin(Margin(8.dp))) + Screen.entries.forEach { + Button(it.name, onClick = { + screen.value = it + }) + } + } +}