├── zipline ├── src │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── dropbox │ │ │ └── componentbox │ │ │ └── zipline │ │ │ └── ComponentBoxService.kt │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── dropbox │ │ │ └── componentbox │ │ │ └── zipline │ │ │ ├── ComponentBoxService.kt │ │ │ └── ComponentBoxZipline.kt │ ├── iosMain │ │ └── kotlin │ │ │ └── com │ │ │ └── dropbox │ │ │ └── componentbox │ │ │ └── zipline │ │ │ └── ComponentBoxService.kt │ ├── jsMain │ │ └── kotlin │ │ │ └── com │ │ │ └── dropbox │ │ │ └── componentbox │ │ │ └── zipline │ │ │ ├── ComponentBoxService.kt │ │ │ └── main.kt │ └── hostMain │ │ └── kotlin │ │ └── com │ │ └── dropbox │ │ └── componentbox │ │ └── zipline │ │ └── LaunchComponentBoxZipline.kt ├── gradle.properties ├── README.md └── build.gradle.kts ├── model ├── gradle.properties ├── src │ ├── androidMain │ │ └── AndroidManifest.xml │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── dropbox │ │ └── componentbox │ │ └── model │ │ ├── EventController.kt │ │ ├── ComposableModelFactory.kt │ │ ├── StatefulComposable.kt │ │ ├── ComposableModel.kt │ │ ├── ComposableModelProvider.kt │ │ └── StatefulComponentBox.kt └── build.gradle.kts ├── componentbox ├── gradle.properties ├── src │ ├── androidMain │ │ └── AndroidManifest.xml │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── dropbox │ │ └── componentbox │ │ ├── Routes.kt │ │ ├── ComponentBoxExport.kt │ │ ├── SerializableComponentBox.kt │ │ ├── FontStyle.kt │ │ ├── TextDecoration.kt │ │ ├── Events.kt │ │ ├── Tree.kt │ │ ├── Router.kt │ │ ├── Text.kt │ │ ├── FontWeight.kt │ │ ├── Shape.kt │ │ ├── Action.kt │ │ ├── TextUnit.kt │ │ ├── Box.kt │ │ ├── AnnotatedString.kt │ │ ├── Forest.kt │ │ ├── TextStyle.kt │ │ ├── Color.kt │ │ ├── Column.kt │ │ ├── ComponentBox.kt │ │ ├── LazyColumn.kt │ │ ├── Graph.kt │ │ ├── Arrangement.kt │ │ ├── Alignment.kt │ │ ├── Trail.kt │ │ ├── SpanStyle.kt │ │ ├── AnnotatedStringElement.kt │ │ ├── Modifier.kt │ │ ├── Button.kt │ │ └── Component.kt ├── build.gradle.kts └── README.md ├── .codecov.yml ├── samples └── counter │ ├── android │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin │ │ │ └── com │ │ │ └── dropbox │ │ │ └── componentbox │ │ │ └── samples │ │ │ └── counter │ │ │ ├── RenderingEngine.kt │ │ │ ├── CounterApp.kt │ │ │ └── MainActivity.kt │ └── build.gradle.kts │ └── server │ ├── src │ ├── androidMain │ │ └── AndroidManifest.xml │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── dropbox │ │ │ └── componentbox │ │ │ └── samples │ │ │ └── counter │ │ │ └── server │ │ │ ├── model │ │ │ ├── CounterEvent.kt │ │ │ └── Counter.kt │ │ │ └── ui │ │ │ ├── dynamic │ │ │ ├── ComponentBoxId.kt │ │ │ ├── Flow.kt │ │ │ ├── Hybrid.kt │ │ │ ├── Graph.kt │ │ │ └── Screen.kt │ │ │ └── static │ │ │ ├── Hybrid.kt │ │ │ └── CounterScreen.kt │ ├── jvmMain │ │ └── kotlin │ │ │ └── com │ │ │ └── dropbox │ │ │ └── componentbox │ │ │ └── samples │ │ │ └── counter │ │ │ └── server │ │ │ └── v1.kt │ └── jsMain │ │ └── kotlin │ │ └── com │ │ └── dropbox │ │ └── componentbox │ │ └── samples │ │ └── counter │ │ └── server │ │ └── main.kt │ ├── componentbox │ └── json │ │ └── com │ │ └── dropbox │ │ └── componentbox │ │ └── samples │ │ └── counter │ │ └── server │ │ └── v1 │ │ └── CounterScreen.json │ └── build.gradle.kts ├── componentbox-gradle-plugin ├── gradle.properties ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── com │ └── dropbox │ └── componentbox │ └── plugin │ └── ComponentBoxPlugin.kt ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .gitignore ├── RELEASING.md ├── settings.gradle.kts ├── CHANGELOG.md ├── gradle.properties ├── gradlew.bat ├── CODE_OF_CONDUCT.md ├── README.md ├── gradlew └── LICENSE.txt /zipline/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /model/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=com.dropbox.componentbox 2 | POM_ARTIFACT_ID=model 3 | POM_PACKAGING=jar -------------------------------------------------------------------------------- /zipline/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=com.dropbox.componentbox 2 | POM_ARTIFACT_ID=zipline 3 | POM_PACKAGING=jar -------------------------------------------------------------------------------- /componentbox/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=com.dropbox.componentbox 2 | POM_ARTIFACT_ID=componentbox 3 | POM_PACKAGING=jar -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 50..80 3 | round: down 4 | precision: 2 5 | 6 | comment: 7 | layout: diff, files -------------------------------------------------------------------------------- /samples/counter/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /componentbox-gradle-plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=componentbox-gradle-plugin 2 | POM_NAME=Component Box Gradle plugin -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/componentbox/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/counter/server/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /componentbox/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /model/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Routes.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | typealias Routes = MutableMap 4 | 5 | -------------------------------------------------------------------------------- /model/src/commonMain/kotlin/com/dropbox/componentbox/model/EventController.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.model 2 | 3 | interface EventController { 4 | fun on(id: Id) 5 | } -------------------------------------------------------------------------------- /zipline/src/commonMain/kotlin/com/dropbox/componentbox/zipline/ComponentBoxService.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.zipline 2 | 3 | 4 | expect class ComponentBoxService { 5 | fun launch(manifestUrl: String) 6 | } -------------------------------------------------------------------------------- /zipline/src/iosMain/kotlin/com/dropbox/componentbox/zipline/ComponentBoxService.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.zipline 2 | 3 | actual class ComponentBoxService { 4 | actual fun launch(manifestUrl: String) { 5 | } 6 | } -------------------------------------------------------------------------------- /zipline/src/jsMain/kotlin/com/dropbox/componentbox/zipline/ComponentBoxService.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.zipline 2 | 3 | actual class ComponentBoxService { 4 | actual fun launch(manifestUrl: String) { 5 | } 6 | } -------------------------------------------------------------------------------- /samples/counter/server/src/commonMain/kotlin/com/dropbox/componentbox/samples/counter/server/model/CounterEvent.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter.server.model 2 | 3 | enum class CounterEvent { 4 | Increment, 5 | Decrement 6 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Mar 23 17:28:03 EDT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /zipline/src/jsMain/kotlin/com/dropbox/componentbox/zipline/main.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.zipline 2 | 3 | import app.cash.zipline.Zipline 4 | 5 | 6 | private val zipline by lazy { Zipline.get() } 7 | 8 | @OptIn(ExperimentalJsExport::class) 9 | @JsExport 10 | fun main() { 11 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/ComponentBoxExport.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | @MustBeDocumented 4 | @Retention(AnnotationRetention.RUNTIME) 5 | @Target(allowedTargets = [AnnotationTarget.FUNCTION, AnnotationTarget.CLASS]) 6 | annotation class ComponentBoxExport -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/SerializableComponentBox.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | @MustBeDocumented 4 | @Retention(AnnotationRetention.RUNTIME) 5 | @Target(allowedTargets = [AnnotationTarget.CLASS, AnnotationTarget.FUNCTION]) 6 | annotation class SerializableComponentBox -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | java_pid54109.hprof.d2bp4g.b.ij 2 | java_pid54109.hprof 3 | build 4 | android/build 5 | common/build 6 | desktop/build 7 | captures 8 | .gradle 9 | .idea 10 | bin 11 | gen 12 | out 13 | *.iml 14 | .directory 15 | node_modules 16 | local.properties 17 | android/mobile/release/* 18 | ios.xcworkspace/ 19 | .DS_Store 20 | .externalNativeBuild 21 | yarn.lock 22 | -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/FontStyle.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Represents a font style. 7 | */ 8 | @Serializable 9 | sealed class FontStyle { 10 | object Normal : FontStyle() 11 | object Italic : FontStyle() 12 | object Oblique : FontStyle() 13 | } -------------------------------------------------------------------------------- /model/src/commonMain/kotlin/com/dropbox/componentbox/model/ComposableModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.model 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | 5 | interface ComposableModelFactory> { 6 | fun create(): T 7 | } 8 | 9 | val LocalComposableModelFactory = compositionLocalOf?> { null } 10 | -------------------------------------------------------------------------------- /samples/counter/server/src/commonMain/kotlin/com/dropbox/componentbox/samples/counter/server/ui/dynamic/ComponentBoxId.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter.server.ui.dynamic 2 | 3 | enum class ComponentBoxId(val value: String) { 4 | CounterOnboardingFlow("counter_onboarding_flow"), 5 | CounterLoginScreen("counter_login_screen"), 6 | CounterScreen("counter_screen") 7 | } -------------------------------------------------------------------------------- /model/src/commonMain/kotlin/com/dropbox/componentbox/model/StatefulComposable.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.model 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.dropbox.componentbox.Component 5 | 6 | @Composable 7 | inline fun > 8 | StatefulComposable(creator: (model: Model) -> Component): Component = creator(composableModel()) 9 | -------------------------------------------------------------------------------- /samples/counter/android/src/main/kotlin/com/dropbox/componentbox/samples/counter/RenderingEngine.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.dropbox.componentbox.Component 5 | 6 | class RenderingEngine { 7 | @Composable 8 | operator fun invoke(builder: () -> Component?) { 9 | // TODO() 10 | } 11 | } -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Change the version in `gradle.properties` to a non-SNAPSHOT version 4 | 2. Update the `CHANGELOG.md` 5 | 3. `git commit -am "Prepare for release X.Y.Z"` 6 | 4. `git tag -a vX.Y.Z -m "Version X.Y.Z"` 7 | 5. `git push && git push --tags` to trigger CI to deploy the release 8 | 7. Update `gradle.properties` to next SNAPSHOT version 9 | 8. `git commit -am "Prepare for development"` 10 | 9. `git push && git push --tags` -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/TextDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Represents a text decoration. 7 | */ 8 | @Serializable 9 | sealed interface TextDecoration { 10 | @Serializable 11 | data class DrawUnderline(val color: Color) : TextDecoration 12 | @Serializable 13 | data class DrawLineThrough(val color: Color) : TextDecoration 14 | } -------------------------------------------------------------------------------- /samples/counter/server/src/jvmMain/kotlin/com/dropbox/componentbox/samples/counter/server/v1.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter.server 2 | 3 | import com.dropbox.componentbox.ComponentBoxExport 4 | import com.dropbox.componentbox.SerializableComponentBox 5 | import com.dropbox.componentbox.samples.counter.server.ui.static.static 6 | 7 | 8 | @ComponentBoxExport 9 | class v1 { 10 | @SerializableComponentBox 11 | fun CounterScreen() = static 12 | } -------------------------------------------------------------------------------- /samples/counter/server/src/commonMain/kotlin/com/dropbox/componentbox/samples/counter/server/ui/static/Hybrid.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter.server.ui.static 2 | 3 | import com.dropbox.componentbox.forest 4 | import kotlinx.serialization.Serializable 5 | import com.dropbox.componentbox.tree as static 6 | 7 | 8 | @Serializable 9 | val staticHybrid = forest { 10 | tree("increment_button", static { incrementButton }) 11 | tree("decrement_button", static { decrementButton }) 12 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Events.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | sealed class Events { 7 | @Serializable 8 | data class Semantic( 9 | val onClick: Action.Semantic, 10 | val onLongClick: Action.Semantic 11 | ) : Events() 12 | 13 | data class Lambda( 14 | val onClick: Action.Lambda, 15 | val onLongClick: Action.Lambda 16 | ) : Events() 17 | } -------------------------------------------------------------------------------- /samples/counter/server/componentbox/json/com/dropbox/componentbox/samples/counter/server/v1/CounterScreen.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": { 3 | "type": "com.dropbox.componentbox.LazyColumn.Static", 4 | "verticalArrangement": { 5 | "type": "com.dropbox.componentbox.Arrangement.SpaceEvenly", 6 | "space": { 7 | "value": 2.0 8 | } 9 | }, 10 | "horizontalAlignment": { 11 | "type": "com.dropbox.componentbox.Alignment.Start" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("VERSION_CATALOGS") 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenLocal() 6 | google() 7 | gradlePluginPortal() 8 | mavenCentral() 9 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 10 | } 11 | } 12 | rootProject.name = "componentbox" 13 | 14 | include(":componentbox") 15 | include(":zipline") 16 | include(":model") 17 | include(":componentbox-gradle-plugin") 18 | include(":samples:counter:android") 19 | include(":samples:counter:server") -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Tree.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * A container of a root component. 7 | * Represents the hierarchical structure of UI components. For example, a screen. 8 | */ 9 | @Serializable 10 | sealed interface Tree : ComponentBox { 11 | @Serializable 12 | data class Static( 13 | val root: Component 14 | ) : Tree 15 | 16 | data class Dynamic( 17 | val root: Component 18 | ) : Tree 19 | } 20 | -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Router.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.coroutines.flow.MutableStateFlow 4 | import kotlinx.coroutines.flow.StateFlow 5 | 6 | class Router( 7 | private val routes: Map, 8 | start: String 9 | ) { 10 | private val stateFlow = MutableStateFlow(requireNotNull(routes[start])) 11 | val component: StateFlow = stateFlow 12 | 13 | fun navigateTo(next: String) { 14 | stateFlow.value = requireNotNull(routes[next]) 15 | } 16 | } -------------------------------------------------------------------------------- /zipline/src/commonMain/kotlin/com/dropbox/componentbox/zipline/ComponentBoxZipline.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.zipline 2 | 3 | import app.cash.zipline.ZiplineService 4 | import com.dropbox.componentbox.Forest 5 | import com.dropbox.componentbox.Graph 6 | 7 | sealed interface ComponentBoxZipline : ZiplineService 8 | interface ComponentBoxGraph : Graph.Dynamic, ComponentBoxZipline 9 | interface ComponentBoxForest : Forest.Dynamic, ComponentBoxZipline 10 | interface ComponentBoxTrail : ComponentBoxZipline 11 | interface ComponentBoxTree : ComponentBoxZipline 12 | -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Text.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Represents a piece of text with optional color and text style information. 7 | * @property text The text content. 8 | * @property color The color of the text, or null if not specified. 9 | * @property style The style of the text, or null if not specified. 10 | */ 11 | @Serializable 12 | data class Text( 13 | val text: String?, 14 | val color: Color?, 15 | val style: TextStyle? 16 | ) : Component 17 | -------------------------------------------------------------------------------- /samples/counter/server/src/jsMain/kotlin/com/dropbox/componentbox/samples/counter/server/main.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter.server 2 | 3 | import app.cash.zipline.Zipline 4 | import app.cash.zipline.ZiplineService 5 | 6 | interface Test : ZiplineService { 7 | val value: String 8 | } 9 | 10 | data class RealTest( 11 | override val value: String 12 | ) : Test 13 | 14 | @OptIn(ExperimentalJsExport::class) 15 | @JsExport 16 | fun launchZipline() { 17 | val zipline = Zipline.get() 18 | zipline.bind("triviaService", RealTest("component box")) 19 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/FontWeight.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Represents a font weight. 7 | */ 8 | @Serializable 9 | sealed class FontWeight { 10 | object Thin : FontWeight() 11 | object ExtraLight : FontWeight() 12 | object Light : FontWeight() 13 | object Normal : FontWeight() 14 | object Medium : FontWeight() 15 | object SemiBold : FontWeight() 16 | object Bold : FontWeight() 17 | @Serializable 18 | object ExtraBold : FontWeight() 19 | object Black : FontWeight() 20 | } -------------------------------------------------------------------------------- /samples/counter/server/src/commonMain/kotlin/com/dropbox/componentbox/samples/counter/server/ui/dynamic/Flow.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter.server.ui.dynamic 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.dropbox.componentbox.ComponentBoxExport 5 | import com.dropbox.componentbox.Trail 6 | import com.dropbox.componentbox.model.statefulComponentBox 7 | 8 | @Composable 9 | @ComponentBoxExport 10 | fun statefulOnboardingFlow() = statefulComponentBox(init = null) { 11 | onboardingFlow() 12 | } 13 | 14 | 15 | @Composable 16 | fun onboardingFlow() = Trail { 17 | node(counterScreenUI()) 18 | node(counterScreenUI()) 19 | } -------------------------------------------------------------------------------- /samples/counter/android/src/main/kotlin/com/dropbox/componentbox/samples/counter/CounterApp.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter 2 | 3 | import android.app.Application 4 | import io.ktor.client.HttpClient 5 | import io.ktor.client.request.get 6 | import io.ktor.client.statement.HttpResponse 7 | import io.ktor.client.statement.readBytes 8 | 9 | 10 | class CounterApp : Application() { 11 | 12 | 13 | } 14 | 15 | 16 | class ComponentBoxClient { 17 | private val client = HttpClient() 18 | suspend operator fun invoke(binaryUrl: String): ByteArray { 19 | val response: HttpResponse = client.get(binaryUrl) 20 | return response.readBytes() 21 | } 22 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | /** 7 | * Represents the shape of a UI element. 8 | */ 9 | @Serializable 10 | sealed class Shape { 11 | /** 12 | * The circular shape. 13 | */ 14 | object Circle : Shape() 15 | 16 | /** 17 | * The cut corner shape. 18 | */ 19 | object CutCorner : Shape() 20 | 21 | /** 22 | * The rectangular shape. 23 | */ 24 | object Rectangle : Shape() 25 | 26 | /** 27 | * The rounded corner shape. 28 | * 29 | * @property size The size of the rounded corner. 30 | */ 31 | data class RoundedCorner(val size: Dp) : Shape() 32 | } -------------------------------------------------------------------------------- /samples/counter/server/src/commonMain/kotlin/com/dropbox/componentbox/samples/counter/server/ui/dynamic/Hybrid.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter.server.ui.dynamic 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.dropbox.componentbox.ComponentBoxExport 5 | import com.dropbox.componentbox.Forest 6 | import com.dropbox.componentbox.Tree 7 | import com.dropbox.componentbox.model.statefulComponentBox 8 | 9 | @Composable 10 | @ComponentBoxExport 11 | fun statefulLoginScreen() = statefulComponentBox(init = null) { 12 | loginScreen() 13 | } 14 | 15 | @Composable 16 | fun loginScreen() = Forest { 17 | tree("increment_button", Tree { IncrementButton() }) 18 | tree("decrement_button", Tree { DecrementButton() }) 19 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Action.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Represents a response to a user-triggered event. 7 | */ 8 | sealed class Action { 9 | /** 10 | * Models events with semantic identifiers. 11 | * @param Event Used on deserialization to map the event to a lambda function. 12 | */ 13 | @Serializable 14 | data class Semantic( 15 | val event: Event 16 | ) : Action() 17 | 18 | /** 19 | * Models events with lambda functions. 20 | */ 21 | data class Lambda( 22 | val run: () -> Unit 23 | ) : Action() 24 | } 25 | 26 | fun lambda(run: () -> Unit) = Action.Lambda(run) 27 | fun semantic(run: () -> Event) = Action.Semantic(run()) -------------------------------------------------------------------------------- /model/src/commonMain/kotlin/com/dropbox/componentbox/model/ComposableModel.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.model 2 | 3 | import kotlinx.coroutines.flow.MutableStateFlow 4 | import kotlinx.coroutines.flow.StateFlow 5 | 6 | /** 7 | * Represents a stateful unidirectional data flow model. 8 | * @param initialState The initial state of the model. 9 | */ 10 | abstract class ComposableModel( 11 | initialState: State 12 | ) { 13 | private val stateFlow: MutableStateFlow = MutableStateFlow(initialState) 14 | val state: StateFlow = stateFlow 15 | 16 | fun setState(next: State) { 17 | stateFlow.value = next 18 | } 19 | 20 | fun withState(block: (state: State) -> Unit) { 21 | block(stateFlow.value) 22 | } 23 | 24 | abstract fun on(event: Event) 25 | } -------------------------------------------------------------------------------- /model/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.multiplatform") 3 | id("com.android.library") 4 | id("org.jetbrains.compose") version "1.3.1" 5 | } 6 | 7 | kotlin { 8 | jvm() 9 | ios() 10 | android() 11 | js { 12 | browser() 13 | binaries.executable() 14 | } 15 | 16 | sourceSets { 17 | val commonMain by getting { 18 | dependencies { 19 | implementation(project(":componentbox")) 20 | implementation(compose.runtime) 21 | } 22 | } 23 | } 24 | } 25 | 26 | android { 27 | 28 | compileSdk = libs.versions.android.compile.sdk.get().toInt() 29 | 30 | defaultConfig { 31 | minSdk = libs.versions.android.min.sdk.get().toInt() 32 | } 33 | 34 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 35 | } -------------------------------------------------------------------------------- /samples/counter/server/src/commonMain/kotlin/com/dropbox/componentbox/samples/counter/server/model/Counter.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter.server.model 2 | 3 | import com.dropbox.componentbox.model.ComposableModel 4 | import com.dropbox.componentbox.samples.counter.server.model.CounterEvent.Decrement 5 | import com.dropbox.componentbox.samples.counter.server.model.CounterEvent.Increment 6 | 7 | 8 | class Counter : ComposableModel(0) { 9 | private fun increment() { 10 | withState { 11 | setState(state.value + 1) 12 | } 13 | } 14 | 15 | private fun decrement() { 16 | withState { 17 | setState(state.value - 1) 18 | } 19 | } 20 | 21 | override fun on(event: CounterEvent) = when (event) { 22 | Increment -> increment() 23 | Decrement -> decrement() 24 | } 25 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/TextUnit.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | /** 7 | * Represents a text unit, which can be either in scaled pixels or pixels. 8 | */ 9 | @Serializable 10 | sealed class TextUnit { 11 | 12 | /** 13 | * Represents a text unit in scaled pixels. 14 | * @property value The value of the text unit in scaled pixels. 15 | */ 16 | @Serializable 17 | data class Sp(val value: Float) : TextUnit() 18 | 19 | /** 20 | * Represents a text unit in pixels. 21 | * @property value The value of the text unit in pixels. 22 | */ 23 | @Serializable 24 | data class Px(val value: Int) : TextUnit() 25 | 26 | companion object { 27 | fun sp(value: Float): TextUnit = Sp(value) 28 | fun px(value: Int): TextUnit = Px(value) 29 | } 30 | } -------------------------------------------------------------------------------- /samples/counter/server/src/commonMain/kotlin/com/dropbox/componentbox/samples/counter/server/ui/dynamic/Graph.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter.server.ui.dynamic 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.dropbox.componentbox.ComponentBoxExport 5 | import com.dropbox.componentbox.Graph 6 | import com.dropbox.componentbox.model.statefulComponentBoxGraph 7 | import com.dropbox.componentbox.samples.counter.server.ui.dynamic.ComponentBoxId.CounterLoginScreen 8 | import com.dropbox.componentbox.samples.counter.server.ui.dynamic.ComponentBoxId.CounterOnboardingFlow 9 | import com.dropbox.componentbox.samples.counter.server.ui.dynamic.ComponentBoxId.CounterScreen 10 | 11 | @Composable 12 | @ComponentBoxExport 13 | fun graph() = statefulComponentBoxGraph(init = null) { 14 | Graph(start = CounterOnboardingFlow.value) { 15 | componentBox(CounterLoginScreen.value, loginScreen()) 16 | componentBox(CounterOnboardingFlow.value, onboardingFlow()) 17 | componentBox(CounterScreen.value, counterScreen()) 18 | } 19 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Box.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | sealed class Box : Component { 8 | 9 | abstract val modifier: Modifier 10 | abstract val children: MutableList 11 | 12 | @Serializable 13 | class Static( 14 | override val modifier: Modifier = Modifier(), 15 | val events: Events.Semantic? = null, 16 | override val children: MutableList = mutableListOf() 17 | ) : Box() { 18 | fun child(component: Component) { 19 | children.add(component) 20 | } 21 | } 22 | 23 | class Dynamic( 24 | override val modifier: Modifier = Modifier(), 25 | val events: Events.Lambda? = null, 26 | override val children: MutableList = mutableListOf() 27 | ) : Box() { 28 | @Composable 29 | fun child(component: Component) { 30 | children.add(component) 31 | } 32 | } 33 | 34 | 35 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/AnnotatedString.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | /** 7 | * Enables text with inline styling. 8 | * @param elements List of [AnnotatedStringElement] which can be plain text or styled spans. 9 | * @property text Convenience method for creating and adding [AnnotatedStringElement.Text] 10 | * @property span Convenience method for creating and adding [AnnotatedStringElement.Span] 11 | */ 12 | @Serializable 13 | data class AnnotatedString( 14 | val elements: MutableList = mutableListOf() 15 | ) : Component { 16 | fun text(text: String, style: TextStyle? = null, softBreak: Boolean = false) { 17 | elements.add(AnnotatedStringElement.Text(text, style, softBreak)) 18 | } 19 | 20 | fun span(start: Int, end: Int, style: SpanStyle) { 21 | elements.add(AnnotatedStringElement.Span(start, end, style)) 22 | } 23 | 24 | fun inlineContent(id: String, content: InlineTextContent) { 25 | elements.add(AnnotatedStringElement.InlineContent(id, content)) 26 | } 27 | } -------------------------------------------------------------------------------- /model/src/commonMain/kotlin/com/dropbox/componentbox/model/ComposableModelProvider.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.model 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.runtime.staticCompositionLocalOf 6 | 7 | class ComposableModelProvider { 8 | val modelMap = mutableMapOf>() 9 | 10 | inline fun > getOrCreateModel(crossinline creator: () -> T): T { 11 | val key = requireNotNull(T::class.simpleName) 12 | return modelMap.getOrPut(key) { creator() } as T 13 | } 14 | } 15 | 16 | val LocalComposableModelProvider = staticCompositionLocalOf { ComposableModelProvider() } 17 | 18 | 19 | @Composable 20 | inline fun > composableModel(): T { 21 | val factory = LocalComposableModelFactory.current as? ComposableModelFactory 22 | ?: error("No factory provided for creating a ComposableModel of type ${T::class.simpleName}") 23 | 24 | val provider = LocalComposableModelProvider.current 25 | return remember { provider.getOrCreateModel(factory::create) } 26 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Forest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.serialization.Serializable 5 | 6 | 7 | /** 8 | * A set of trees. 9 | * Represents a hybrid module or feature in the application. 10 | * For example, a screen with some of its UI components driven by Component Box. 11 | */ 12 | @Serializable 13 | sealed interface Forest : ComponentBox { 14 | val trees: MutableMap 15 | 16 | @Serializable 17 | class Static( 18 | override val trees: MutableMap = mutableMapOf() 19 | ) : Forest { 20 | fun tree(id: TreeId, tree: Tree.Static) { 21 | trees[id] = tree 22 | } 23 | } 24 | 25 | interface Dynamic : Forest { 26 | @Composable 27 | fun tree(id: TreeId, tree: Tree.Dynamic) 28 | } 29 | } 30 | 31 | data class DynamicForest( 32 | override val trees: MutableMap = mutableMapOf() 33 | ) : Forest.Dynamic { 34 | 35 | @Composable 36 | override fun tree(id: TreeId, tree: Tree.Dynamic) { 37 | trees[id] = tree 38 | } 39 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/TextStyle.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | /** 7 | * Represents a style to be applied to text. 8 | * @property color The text color to be applied to the text, or null if not specified. 9 | * @property fontWeight The font weight to be applied to the text, or null if not specified. 10 | * @property fontStyle The font style to be applied to the text, or null if not specified. 11 | * @property fontSize The font size to be applied to the text, or null if not specified. 12 | * @property letterSpacing The letter spacing to be applied to the text, or null if not specified. 13 | * @property lineHeight The line height to be applied to the text, or null if not specified. 14 | */ 15 | @Serializable 16 | data class TextStyle( 17 | val color: Color? = null, 18 | val fontWeight: FontWeight? = null, 19 | val fontStyle: FontStyle? = null, 20 | val fontSize: TextUnit? = null, 21 | val letterSpacing: TextUnit? = null, 22 | val lineHeight: TextUnit? = null 23 | ) { 24 | fun color(color: Color): TextStyle = copy(color = color) 25 | fun fontWeight(fontWeight: FontWeight): TextStyle = copy(fontWeight = fontWeight) 26 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.1.0] (2022-04-09) 6 | ### Added 7 | - Target iOS 8 | - Add Swift package 9 | 10 | ### Changed 11 | - Use `Color` class 12 | - Implement `Passable` and `@IsPassable` 13 | 14 | ## [0.1.0-alpha] (2022-03-13) 15 | ### Added 16 | - Default Material implementation 17 | - Common utils 18 | - Surface component 19 | - ComponentBoxPresenter class 20 | - ComponentBoxState class 21 | - ComponentBoxViewState class 22 | - ComponentBoxContext class 23 | - ComponentBoxZipline class 24 | - ComponentBoxView class 25 | - ComponentBoxDialogFragment class 26 | - ComponentBoxFragment class 27 | 28 | ### Changed 29 | - Renamed models package to "foundation" 30 | - Renamed HorizontalBanner to Banner 31 | - Rearchitected Discovery app 32 | - Refactored Zipline integration with Discovery app 33 | 34 | ### Removed 35 | - VerticalBanner type 36 | 37 | ## [0.0.1-alpha] (2022-03-04) 38 | - Initial public release 39 | 40 | [Unreleased]: https://github.com/dropbox/componentbox/compare/v0.1.0...HEAD 41 | [0.1.0]: https://github.com/dropbox/componentbox/releases/tag/v0.1.0 42 | [0.1.0-alpha]: https://github.com/dropbox/componentbox/releases/tag/v0.1.0-alpha 43 | [0.0.1-alpha]: https://github.com/dropbox/componentbox/releases/tag/v0.0.1-alpha -------------------------------------------------------------------------------- /samples/counter/server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.multiplatform") 3 | id("com.android.library") 4 | id("org.jetbrains.compose") version "1.3.1" 5 | id("com.dropbox.componentbox.plugin") 6 | kotlin("plugin.serialization") 7 | } 8 | 9 | kotlin { 10 | jvm { 11 | 12 | } 13 | ios() 14 | android() 15 | js { 16 | browser() 17 | binaries.executable() 18 | } 19 | 20 | sourceSets { 21 | 22 | val commonMain by getting { 23 | dependencies { 24 | implementation(compose.runtime) 25 | api(libs.kotlin.std.lib) 26 | api(libs.kotlinx.coroutines.core) 27 | implementation(project(":componentbox")) 28 | implementation(project(":model")) 29 | implementation("app.cash.zipline:zipline:0.9.17") 30 | } 31 | } 32 | val jvmMain by getting { 33 | dependsOn(commonMain) 34 | } 35 | 36 | } 37 | } 38 | 39 | android { 40 | 41 | compileSdk = libs.versions.android.compile.sdk.get().toInt() 42 | 43 | defaultConfig { 44 | minSdk = libs.versions.android.min.sdk.get().toInt() 45 | } 46 | 47 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 48 | } -------------------------------------------------------------------------------- /componentbox-gradle-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | 3 | plugins { 4 | id("org.jetbrains.kotlin.jvm") 5 | id("java-gradle-plugin") 6 | id("maven-publish") 7 | id("org.jetbrains.kotlin.plugin.serialization") 8 | id("app.cash.zipline") 9 | 10 | } 11 | 12 | dependencies { 13 | compileOnly(gradleApi()) 14 | 15 | implementation(libs.kotlin.gradle.plugin) 16 | compileOnly(libs.android.gradle.plugin) 17 | implementation(project(":componentbox")) 18 | implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.0") 19 | api(libs.kotlinx.serialization.core) 20 | api(libs.kotlinx.serialization.json) 21 | implementation("app.cash.zipline:zipline:0.9.17") 22 | implementation("app.cash.zipline:zipline-gradle-plugin:0.9.17") 23 | } 24 | 25 | publishing { 26 | publications { 27 | create("pluginMavenPublication") { 28 | from(components["kotlin"]) 29 | groupId = "com.dropbox.componentbox" 30 | artifactId = "componentbox-gradle-plugin" 31 | version = version.toString() 32 | } 33 | } 34 | } 35 | 36 | gradlePlugin { 37 | plugins { 38 | create("componentBoxZipline") { 39 | id = "com.dropbox.componentbox.plugin" 40 | implementationClass = "com.dropbox.componentbox.plugin.ComponentBoxPlugin" 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /samples/counter/android/src/main/kotlin/com/dropbox/componentbox/samples/counter/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.runtime.collectAsState 7 | import com.dropbox.componentbox.zipline.ComponentBoxService 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers 10 | 11 | class MainActivity : ComponentActivity() { 12 | private val scope = CoroutineScope(Dispatchers.Default) 13 | private val service = ComponentBoxService(scope) 14 | private val componentBox = service.componentBox 15 | private val render = RenderingEngine() 16 | override fun onStart() { 17 | super.onStart() 18 | service.launch(LOCAL_MANIFEST_URL) 19 | } 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | 24 | setContent { 25 | val root = componentBox.collectAsState() 26 | render { 27 | root.value 28 | } 29 | } 30 | } 31 | 32 | companion object { 33 | private const val JS_BINARY_URL = "https://api.componentbox.dropboxer.io/best/js/1" 34 | private const val LOCAL_MANIFEST_URL = "http://10.0.2.2:8080/manifest.zipline.json" 35 | } 36 | } -------------------------------------------------------------------------------- /zipline/README.md: -------------------------------------------------------------------------------- 1 | ## Zipline 2 | 3 | `ComponentBoxService` wraps `Zipline` to load and manage a Component Box. 4 | 5 | - It sets up a single-threaded executor service for Zipline and a corresponding coroutine 6 | dispatcher. 7 | - The `launch` function takes a `manifestUrl` and starts the process of downloading, caching, and 8 | loading the `Kotlin/JS` code using `ZiplineLoader`. 9 | - The loaded Component Box is stored in a mutable state flow called `stateFlow`, which allows 10 | observing and reacting to state changes of the loaded component. 11 | - The close function shuts down the executor service, effectively stopping `Zipline`. 12 | 13 | ### Usage 14 | 15 | 1. To use `ComponentBoxService` in your project, create an instance of it, passing 16 | a `CoroutineScope`: 17 | 18 | ```kotlin 19 | val componentBoxService = ComponentBoxService(scope = CoroutineScope(Dispatchers.Main)) 20 | ``` 21 | 22 | 2. Then, call the launch function with a manifest URL: 23 | 24 | ```kotlin 25 | componentBoxService.launch("http://localhost:8080/manifest.zipline.json") 26 | ``` 27 | 28 | 3. You can observe the state of the loaded Component Box using the `componentBox` property: 29 | 30 | ```kotlin 31 | val componentBox = componentBoxService.componentBox.collectAsState() 32 | ``` 33 | 34 | 4. Finally, don't forget to call the close function when you're done using the ComponentBoxService: 35 | 36 | ```kotlin 37 | componentBoxService.close() 38 | ``` -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Color.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Represents a color, which can be either in RGB format or hexadecimal format. 7 | */ 8 | @Serializable 9 | sealed class Color { 10 | 11 | /** 12 | * Represents a color in RGB format. 13 | * @property red The value of the red channel. 14 | * @property green The value of the green channel. 15 | * @property blue The value of the blue channel. 16 | * @property alpha The value of the alpha channel, which defaults to 1.0 if not specified. 17 | */ 18 | @Serializable 19 | 20 | data class Rgb(val red: Int, val green: Int, val blue: Int, val alpha: Float = 1f) : Color() 21 | 22 | /** 23 | * Represents a color in hexadecimal format. 24 | * @property value The hexadecimal value of the color. 25 | */ 26 | @Serializable 27 | data class Hex(val value: String) : Color() 28 | 29 | /** 30 | * Represents a color identified by name. 31 | * @property value The name of the color. 32 | */ 33 | @Serializable 34 | data class Name(val value: String) : Color() 35 | 36 | companion object { 37 | fun rgb(red: Int, green: Int, blue: Int, alpha: Float = 1f): Color = 38 | Rgb(red, green, blue, alpha) 39 | 40 | fun hex(value: String): Color = Hex(value) 41 | fun named(value: String): Color = Name(value) 42 | } 43 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.dropbox.componentbox 2 | VERSION_NAME=0.3.0 3 | 4 | POM_ARTIFACT_ID=componentbox 5 | POM_NAME=Component Box 6 | POM_DESCRIPTION=A Kotlin multiplatform library for building dynamic server-driven UI 7 | POM_PACKAGING=jar 8 | 9 | POM_URL=https://github.com/dropbox/componentbox 10 | POM_SCM_URL=https://github.com/dropbox/componentbox 11 | POM_SCM_CONNECTION=scm:git:git://github.com/dropbox/componentbox.git 12 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/dropbox/componentbox.git 13 | 14 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 15 | POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt 16 | POM_LICENCE_DIST=repo 17 | 18 | POM_DEVELOPER_ID=dropbox 19 | POM_DEVELOPER_NAME=Dropbox, Inc. 20 | POM_DEVELOPER_URL=https://github.com/dropbox 21 | 22 | kotlin.mpp.enableGranularSourceSetsMetadata=true 23 | kotlin.native.enableDependencyPropagation=false 24 | org.gradle.jvmargs=-Xmx8192m -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1024m 25 | android.useAndroidX=true 26 | kotlin.js.compiler=ir 27 | kotlin.incremental.js.ir=true 28 | android.enableJetifier=false 29 | systemProp.org.gradle.internal.http.connectionTimeout=480000 30 | systemProp.org.gradle.internal.http.socketTimeout=480000 31 | systemProp.org.gradle.internal.repository.max.retries=10 32 | systemProp.org.gradle.internal.repository.initial.backoff=500 33 | kotlin.js.generate.executable.default=true 34 | kotlin.mpp.androidSourceSetLayoutVersion1.nowarn=true 35 | kotlin.mpp.androidSourceSetLayoutVersion=2 -------------------------------------------------------------------------------- /model/src/commonMain/kotlin/com/dropbox/componentbox/model/StatefulComponentBox.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.model 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | import androidx.compose.runtime.remember 6 | import com.dropbox.componentbox.ComponentBox 7 | import com.dropbox.componentbox.Graph 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.StateFlow 10 | 11 | @Composable 12 | fun statefulComponentBox(init: Output, creator: @Composable () -> Output): StateFlow { 13 | 14 | val modelProvider = remember { ComposableModelProvider() } 15 | 16 | val stateFlow: MutableStateFlow = MutableStateFlow(init) 17 | val componentBox: StateFlow = stateFlow 18 | 19 | CompositionLocalProvider(LocalComposableModelProvider provides modelProvider) { 20 | stateFlow.value = creator() 21 | } 22 | 23 | return componentBox 24 | } 25 | 26 | 27 | @Composable 28 | fun statefulComponentBoxGraph(init: Output, creator: @Composable () -> Output): StateFlow { 29 | 30 | val modelProvider = remember { ComposableModelProvider() } 31 | 32 | val stateFlow: MutableStateFlow = MutableStateFlow(init) 33 | val graph: StateFlow = stateFlow 34 | 35 | CompositionLocalProvider(LocalComposableModelProvider provides modelProvider) { 36 | stateFlow.value = creator() 37 | } 38 | 39 | return graph 40 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Column.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.serialization.Serializable 5 | 6 | 7 | @Serializable 8 | sealed class Column : Component { 9 | abstract val modifier: Modifier 10 | abstract val verticalArrangement: Arrangement.Vertical? 11 | abstract val horizontalAlignment: Alignment.Horizontal? 12 | abstract val children: MutableList 13 | 14 | @Serializable 15 | class Static( 16 | override val modifier: Modifier = Modifier(), 17 | val events: Events.Semantic? = null, 18 | override val verticalArrangement: Arrangement.Vertical? = null, 19 | override val horizontalAlignment: Alignment.Horizontal? = null, 20 | override val children: MutableList = mutableListOf() 21 | ) : Column() { 22 | fun child(component: Component) { 23 | children.add(component) 24 | } 25 | } 26 | 27 | class Dynamic( 28 | override val modifier: Modifier = Modifier(), 29 | val events: Events.Lambda? = null, 30 | override val verticalArrangement: Arrangement.Vertical? = null, 31 | override val horizontalAlignment: Alignment.Horizontal? = null, 32 | override val children: MutableList = mutableListOf() 33 | ) : Column() { 34 | @Composable 35 | fun child(component: Component) { 36 | children.add(component) 37 | } 38 | } 39 | 40 | 41 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/ComponentBox.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | sealed interface ComponentBox 8 | 9 | typealias ComponentBoxId = String 10 | typealias GraphId = String 11 | typealias ForestId = String 12 | typealias TreeId = String 13 | 14 | fun graph( 15 | start: ComponentBoxId, 16 | builder: Graph.Static.() -> Unit 17 | ): Graph.Static = 18 | StaticGraph(start) 19 | 20 | fun componentBox(builder: Forest.Static.() -> Unit): Forest.Static = 21 | Forest.Static() 22 | 23 | fun tree(root: () -> Component): Tree.Static = Tree.Static(root()) 24 | 25 | @Composable 26 | fun Tree(root: @Composable () -> Component): Tree.Dynamic = Tree.Dynamic(root()) 27 | 28 | @Composable 29 | fun Forest(trees: @Composable Forest.Dynamic.() -> Unit): Forest.Dynamic = 30 | DynamicForest() 31 | 32 | fun forest(trees: Forest.Static.() -> Unit): Forest.Static = Forest.Static() 33 | 34 | @Composable 35 | fun ComponentBox(root: @Composable () -> Component): Tree.Dynamic = Tree.Dynamic(root()) 36 | 37 | @Composable 38 | fun ComponentBox(builder: @Composable Forest.Dynamic.() -> Unit) = 39 | DynamicForest() 40 | 41 | @Composable 42 | fun Graph(start: ComponentBoxId, builder: @Composable Graph.Dynamic.() -> Unit) = 43 | DynamicGraph(start) 44 | 45 | @Composable 46 | fun Trail(builder: @Composable Trail.Dynamic.() -> Unit) = DynamicTrail() 47 | 48 | fun trail(builder: Trail.Static.() -> Unit) = Trail.Static() -------------------------------------------------------------------------------- /samples/counter/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | } 5 | 6 | android { 7 | compileSdk = libs.versions.android.compile.sdk.get().toInt() 8 | 9 | defaultConfig { 10 | minSdk = libs.versions.android.min.sdk.get().toInt() 11 | } 12 | 13 | buildFeatures { 14 | compose = true 15 | } 16 | 17 | composeOptions { 18 | kotlinCompilerExtensionVersion = "1.4.4" 19 | } 20 | 21 | packagingOptions { 22 | exclude("META-INF/versions/9/previous-compilation-data.bin") 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation("androidx.core:core-ktx:1.9.0") 28 | implementation("androidx.appcompat:appcompat:1.6.1") 29 | implementation("androidx.compose.ui:ui:1.5.0-alpha01") 30 | implementation("androidx.compose.foundation:foundation:1.5.0-alpha01") 31 | implementation("androidx.compose.material:material:1.5.0-alpha01") 32 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") 33 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") 34 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 35 | implementation("io.ktor:ktor-client-core:2.2.4") 36 | implementation("io.ktor:ktor-client-json:2.2.4") 37 | implementation("io.ktor:ktor-client-serialization:2.2.4") 38 | 39 | implementation("androidx.activity:activity-compose:1.7.0") 40 | 41 | 42 | api(libs.kotlin.std.lib) 43 | api(libs.kotlinx.coroutines.core) 44 | implementation(project(":componentbox")) 45 | implementation(project(":zipline")) 46 | } -------------------------------------------------------------------------------- /zipline/src/androidMain/kotlin/com/dropbox/componentbox/zipline/ComponentBoxService.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.zipline 2 | 3 | import app.cash.zipline.loader.ManifestVerifier 4 | import app.cash.zipline.loader.ZiplineLoader 5 | import com.dropbox.componentbox.ComponentBox 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.asCoroutineDispatcher 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.StateFlow 10 | import okhttp3.OkHttpClient 11 | import java.util.concurrent.Executors 12 | 13 | 14 | actual class ComponentBoxService( 15 | private val scope: CoroutineScope 16 | ) { 17 | private val ziplineExecutorService = Executors.newSingleThreadExecutor { Thread(it, "Zipline") } 18 | private val ziplineDispatcher = ziplineExecutorService.asCoroutineDispatcher() 19 | private val okHttpClient = OkHttpClient() 20 | 21 | private val stateFlow: MutableStateFlow = MutableStateFlow(null) 22 | val componentBox: StateFlow = stateFlow 23 | 24 | 25 | actual fun launch(manifestUrl: String) { 26 | launchComponentBoxZipline( 27 | scope = scope, 28 | ziplineDispatcher = ziplineDispatcher, 29 | ziplineLoader = ZiplineLoader( 30 | dispatcher = ziplineDispatcher, 31 | manifestVerifier = ManifestVerifier.NO_SIGNATURE_CHECKS, 32 | httpClient = okHttpClient 33 | ), 34 | manifestUrl = manifestUrl, 35 | componentBox = stateFlow, 36 | ) 37 | } 38 | 39 | fun close() { 40 | ziplineExecutorService.shutdown() 41 | } 42 | } -------------------------------------------------------------------------------- /zipline/src/hostMain/kotlin/com/dropbox/componentbox/zipline/LaunchComponentBoxZipline.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.zipline 2 | 3 | import app.cash.zipline.loader.LoadResult 4 | import app.cash.zipline.loader.ZiplineLoader 5 | import com.dropbox.componentbox.ComponentBox 6 | import kotlinx.coroutines.CoroutineDispatcher 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.SupervisorJob 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.launch 11 | 12 | 13 | fun launchComponentBoxZipline( 14 | scope: CoroutineScope, 15 | ziplineDispatcher: CoroutineDispatcher, 16 | ziplineLoader: ZiplineLoader, 17 | manifestUrl: String, 18 | componentBox: MutableStateFlow 19 | ) { 20 | scope.launch(ziplineDispatcher + SupervisorJob()) { 21 | val result = ziplineLoader.loadOnce( 22 | applicationName = "counter", 23 | manifestUrl = manifestUrl 24 | ) 25 | 26 | if (result is LoadResult.Success) { 27 | val zipline = result.zipline 28 | val componentBoxZipline = zipline.take("ComponentBoxZipline") 29 | 30 | val job = launch { 31 | componentBox.value = when (componentBoxZipline) { 32 | is ComponentBoxForest -> componentBoxZipline 33 | is ComponentBoxGraph -> componentBoxZipline 34 | is ComponentBoxTrail -> TODO() 35 | is ComponentBoxTree -> TODO() 36 | } 37 | } 38 | 39 | job.invokeOnCompletion { 40 | componentBoxZipline.close() 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/LazyColumn.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | sealed class LazyColumn : Component { 8 | abstract val modifier: Modifier 9 | abstract val events: Events? 10 | abstract val verticalArrangement: Arrangement.Vertical? 11 | abstract val horizontalAlignment: Alignment.Horizontal? 12 | abstract val contentPadding: PaddingValues? 13 | abstract val children: MutableList 14 | 15 | data class Dynamic( 16 | override val modifier: Modifier = Modifier(), 17 | override val events: Events.Lambda? = null, 18 | override val verticalArrangement: Arrangement.Vertical? = null, 19 | override val horizontalAlignment: Alignment.Horizontal? = null, 20 | override val contentPadding: PaddingValues? = null, 21 | override val children: MutableList = mutableListOf() 22 | ) : LazyColumn() { 23 | @Composable 24 | fun child(component: Component) { 25 | children.add(component) 26 | } 27 | } 28 | 29 | @Serializable 30 | data class Static( 31 | override val modifier: Modifier = Modifier(), 32 | override val events: Events.Semantic? = null, 33 | override val verticalArrangement: Arrangement.Vertical? = null, 34 | override val horizontalAlignment: Alignment.Horizontal? = null, 35 | override val contentPadding: PaddingValues? = null, 36 | override val children: MutableList = mutableListOf() 37 | ) : LazyColumn() { 38 | fun child(component: Component) { 39 | children.add(component) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /samples/counter/server/src/commonMain/kotlin/com/dropbox/componentbox/samples/counter/server/ui/static/CounterScreen.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter.server.ui.static 2 | 3 | import com.dropbox.componentbox.Alignment 4 | import com.dropbox.componentbox.Arrangement 5 | import com.dropbox.componentbox.Color 6 | import com.dropbox.componentbox.FontWeight 7 | import com.dropbox.componentbox.SerializableComponentBox 8 | import com.dropbox.componentbox.TextStyle 9 | import com.dropbox.componentbox.dp 10 | import com.dropbox.componentbox.lazyColumn 11 | import com.dropbox.componentbox.samples.counter.server.model.CounterEvent 12 | import com.dropbox.componentbox.samples.counter.server.model.CounterEvent.Decrement 13 | import com.dropbox.componentbox.samples.counter.server.model.CounterEvent.Increment 14 | import com.dropbox.componentbox.semantic 15 | import com.dropbox.componentbox.text 16 | import com.dropbox.componentbox.textButton 17 | import com.dropbox.componentbox.tree 18 | 19 | 20 | val static = tree { 21 | lazyColumn( 22 | verticalArrangement = Arrangement.SpaceEvenly(2.dp), 23 | horizontalAlignment = Alignment.Start 24 | ) { 25 | child(header) 26 | child(count) 27 | child(incrementButton) 28 | child(decrementButton) 29 | } 30 | } 31 | 32 | 33 | val header = text( 34 | text = "Component Box Counter", 35 | style = TextStyle(fontWeight = FontWeight.ExtraBold) 36 | ) 37 | 38 | val count = text( 39 | text = "Count: \${COUNTER_STATE}", 40 | style = TextStyle(color = Color.Hex("#FF0000")) 41 | ) 42 | 43 | val incrementButton = textButton( 44 | text = "+1", 45 | onClick = semantic { Increment } 46 | ) 47 | 48 | val decrementButton = textButton( 49 | text = "-1", 50 | onClick = semantic { Decrement } 51 | ) -------------------------------------------------------------------------------- /zipline/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.multiplatform") 3 | id("com.android.library") 4 | kotlin("plugin.serialization") 5 | id("app.cash.zipline") 6 | 7 | } 8 | 9 | kotlin { 10 | ios() 11 | android() 12 | 13 | js { 14 | browser() 15 | binaries.executable() 16 | } 17 | 18 | sourceSets { 19 | val commonMain by getting { 20 | dependencies { 21 | implementation("app.cash.zipline:zipline:0.9.17") 22 | implementation(project(":componentbox")) 23 | implementation("io.ktor:ktor-client-core:2.2.4") 24 | implementation("io.ktor:ktor-client-json:2.2.4") 25 | implementation("io.ktor:ktor-client-serialization:2.2.4") 26 | } 27 | } 28 | 29 | val hostMain by creating { 30 | dependsOn(commonMain) 31 | dependencies { 32 | implementation("app.cash.zipline:zipline-loader:0.9.17") 33 | api(libs.okio.core) 34 | } 35 | } 36 | 37 | val androidMain by getting { 38 | dependsOn(hostMain) 39 | dependencies { 40 | implementation("io.ktor:ktor-client-okhttp:2.2.4") 41 | 42 | } 43 | } 44 | val iosMain by getting { 45 | dependsOn(hostMain) 46 | } 47 | } 48 | } 49 | 50 | android { 51 | 52 | compileSdk = libs.versions.android.compile.sdk.get().toInt() 53 | 54 | defaultConfig { 55 | minSdk = libs.versions.android.min.sdk.get().toInt() 56 | } 57 | 58 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 59 | } 60 | 61 | 62 | zipline { 63 | mainFunction.set("com.dropbox.componentbox.samples.counter.common.zipline.launch") 64 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | android-min-sdk = "27" 3 | android-compile-sdk = "33" 4 | android-target-sdk = "33" 5 | coroutines = "1.7.0-Beta" 6 | gradle = "7.3.1" 7 | gradle-maven-publish-plugin = "0.23.2" 8 | kmm-bridge = "0.3.2" 9 | kotlin = "1.8.10" 10 | kotlin-gradle-plugin = "1.7.21" 11 | kotlinx-cli = "0.3.4" 12 | kotlinx-serialization = "1.5.0" 13 | kotlinx-wasm-jsinterop = "0.2.1" 14 | kover = "0.6.0" 15 | wasm-runtime = "0.0.48" 16 | okio = "3.3.0" 17 | 18 | [libraries] 19 | gradle-maven-publish-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "gradle-maven-publish-plugin" } 20 | kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } 21 | kotlin-std-lib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } 22 | kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 23 | android-gradle-plugin = { module = "com.android.tools.build:gradle", version = "7.4.2" } 24 | 25 | kotlinx-cli = { module = "org.jetbrains.kotlinx:kotlinx-cli", version.ref = "kotlinx-cli" } 26 | kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinx-serialization" } 27 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 28 | kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } 29 | kotlinx-wasm-jsinterop = { module = "org.jetbrains.kotlinx:kotlinx-wasm-jsinterop", version.ref = "kotlinx-wasm-jsinterop" } 30 | kover = { module = "org.jetbrains.kotlinx:kover", version.ref = "kover" } 31 | okio-core = { module = "com.squareup.okio:okio", version.ref = "okio" } 32 | -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Graph.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | import kotlinx.coroutines.flow.StateFlow 6 | import kotlinx.serialization.Serializable 7 | 8 | /** 9 | * A collection of forests and trails. 10 | * Represents the navigation hierarchy for an application. 11 | */ 12 | 13 | sealed interface Graph { 14 | val start: ComponentBoxId 15 | val componentBoxes: MutableMap 16 | 17 | 18 | interface Static : Graph { 19 | fun componentBox(id: ComponentBoxId, componentBox: ComponentBox) 20 | } 21 | 22 | interface Dynamic : Graph { 23 | val componentBox: StateFlow 24 | 25 | @Composable 26 | fun componentBox(id: ComponentBoxId, componentBox: ComponentBox) 27 | 28 | fun navigateTo(componentBoxId: ComponentBoxId) 29 | } 30 | } 31 | 32 | @Serializable 33 | class StaticGraph( 34 | override val start: ComponentBoxId, 35 | override val componentBoxes: MutableMap = mutableMapOf() 36 | ) : Graph.Static { 37 | override fun componentBox(id: ComponentBoxId, componentBox: ComponentBox) { 38 | componentBoxes[id] = componentBox 39 | } 40 | } 41 | 42 | class DynamicGraph( 43 | override val start: ComponentBoxId, 44 | override val componentBoxes: MutableMap = mutableMapOf() 45 | ) : Graph.Dynamic { 46 | 47 | private val stateFlow = MutableStateFlow(requireNotNull(componentBoxes[start])) 48 | override val componentBox: StateFlow = stateFlow 49 | 50 | @Composable 51 | override fun componentBox(id: ComponentBoxId, componentBox: ComponentBox) { 52 | componentBoxes[id] = componentBox 53 | } 54 | 55 | override fun navigateTo(componentBoxId: ComponentBoxId) { 56 | val next = componentBoxes[componentBoxId] 57 | if (next != null) { 58 | stateFlow.value = next 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Arrangement.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Represents an arrangement along a horizontal or vertical axis. 7 | */ 8 | @Serializable 9 | sealed interface Arrangement { 10 | /** 11 | * Represents an arrangement along a horizontal axis. 12 | */ 13 | @Serializable 14 | sealed interface Horizontal : Arrangement 15 | 16 | /** 17 | * Represents an arrangement along a vertical axis. 18 | */ 19 | @Serializable 20 | sealed interface Vertical : Arrangement 21 | 22 | /** 23 | * The start horizontal arrangement. 24 | */ 25 | @Serializable 26 | object Start : Horizontal 27 | 28 | /** 29 | * The end horizontal arrangement. 30 | */ 31 | @Serializable 32 | object End : Horizontal 33 | 34 | /** 35 | * The top vertical arrangement. 36 | */ 37 | @Serializable 38 | object Top : Vertical 39 | 40 | /** 41 | * The bottom vertical arrangement. 42 | */ 43 | @Serializable 44 | object Bottom : Vertical 45 | 46 | /** 47 | * The center arrangement, both horizontally and vertically. 48 | */ 49 | @Serializable 50 | object Center : Horizontal, Vertical 51 | 52 | /** 53 | * The space-between horizontal arrangement. 54 | * @property space The space to distribute between the children. 55 | */ 56 | @Serializable 57 | data class SpaceBetween(val space: Dp) : Horizontal 58 | 59 | /** 60 | * The space-around arrangement, both horizontally and vertically. 61 | * @property space The space to distribute around the children. 62 | */ 63 | @Serializable 64 | data class SpaceAround(val space: Dp) : Horizontal, Vertical 65 | 66 | /** 67 | * The space-evenly arrangement, both horizontally and vertically. 68 | * @property space The space to distribute evenly between and around the children. 69 | */ 70 | @Serializable 71 | data class SpaceEvenly(val space: Dp) : Horizontal, Vertical 72 | } -------------------------------------------------------------------------------- /componentbox/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import org.jetbrains.dokka.gradle.DokkaTask 4 | 5 | plugins { 6 | kotlin("multiplatform") version "1.8.10" 7 | kotlin("plugin.serialization") 8 | id("com.android.library") 9 | id("org.jetbrains.dokka") 10 | id("maven-publish") 11 | id("org.jetbrains.compose") version "1.3.1" 12 | } 13 | 14 | 15 | kotlin { 16 | android() 17 | jvm() 18 | js { 19 | browser() 20 | binaries.executable() 21 | } 22 | ios() 23 | 24 | sourceSets { 25 | val commonMain by getting { 26 | dependencies { 27 | api(libs.kotlinx.serialization.core) 28 | api(libs.kotlinx.serialization.json) 29 | api(libs.kotlin.std.lib) 30 | api(libs.kotlinx.coroutines.core) 31 | implementation(compose.runtime) 32 | } 33 | } 34 | 35 | val jvmMain by getting { 36 | } 37 | 38 | val androidMain by getting { 39 | dependencies { 40 | } 41 | } 42 | 43 | val jsMain by getting { 44 | dependencies { 45 | } 46 | } 47 | 48 | val iosMain by getting 49 | } 50 | } 51 | 52 | android { 53 | 54 | compileSdk = libs.versions.android.compile.sdk.get().toInt() 55 | 56 | defaultConfig { 57 | minSdk = libs.versions.android.min.sdk.get().toInt() 58 | } 59 | 60 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 61 | } 62 | 63 | tasks.withType().configureEach { 64 | dokkaSourceSets.configureEach { 65 | reportUndocumented.set(false) 66 | skipDeprecated.set(true) 67 | jdkVersion.set(8) 68 | } 69 | } 70 | 71 | publishing { 72 | publications { 73 | create("componentBoxMavenPublication") { 74 | from(components["kotlin"]) 75 | groupId = group.toString() 76 | artifactId = "componentbox" 77 | version = version.toString() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Alignment.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Represents an alignment along a horizontal or vertical axis. 7 | */ 8 | @Serializable 9 | sealed interface Alignment { 10 | /** 11 | * Represents an alignment along a horizontal axis. 12 | */ 13 | @Serializable 14 | sealed interface Horizontal : Alignment 15 | 16 | /** 17 | * Represents an alignment along a vertical axis. 18 | */ 19 | @Serializable 20 | sealed interface Vertical : Alignment 21 | 22 | /** 23 | * The start horizontal alignment. 24 | */ 25 | @Serializable 26 | object Start : Horizontal 27 | 28 | /** 29 | * The end horizontal alignment. 30 | */ 31 | @Serializable 32 | object End : Horizontal 33 | 34 | /** 35 | * The top vertical alignment. 36 | */ 37 | @Serializable 38 | object Top : Vertical 39 | 40 | /** 41 | * The bottom vertical alignment. 42 | */ 43 | @Serializable 44 | object Bottom : Vertical 45 | 46 | /** 47 | * The center alignment, both horizontally and vertically. 48 | */ 49 | @Serializable 50 | object Center : Horizontal, Vertical 51 | 52 | /** 53 | * The space-between horizontal alignment. 54 | * 55 | * @property space The space to distribute between the children. 56 | */ 57 | @Serializable 58 | data class SpaceBetween(val space: Dp) : Horizontal 59 | 60 | /** 61 | * The space-around alignment, both horizontally and vertically. 62 | * 63 | * @property space The space to distribute around the children. 64 | */ 65 | @Serializable 66 | data class SpaceAround(val space: Dp) : Horizontal, Vertical 67 | 68 | /** 69 | * The space-evenly alignment, both horizontally and vertically. 70 | * 71 | * @property space The space to distribute evenly between and around the children. 72 | */ 73 | @Serializable 74 | data class SpaceEvenly(val space: Dp) : Horizontal, Vertical 75 | } 76 | 77 | 78 | -------------------------------------------------------------------------------- /componentbox/README.md: -------------------------------------------------------------------------------- 1 | # Component Box 2 | 3 | ## Components 4 | 5 | ### AnnotatedString 6 | 7 | ```kotlin 8 | val annotatedString = annotatedString() 9 | 10 | annotatedString.inlineContent( 11 | id = "componentBox", 12 | content = InlineTextContent( 13 | placeholder = Placeholder( 14 | 20.sp, 15 | 20.sp, 16 | PlaceholderVerticalAlign.TextCenter 17 | ) 18 | ) 19 | ) { 20 | Image( 21 | painter = painter, 22 | contentDescription = "componentBox" 23 | ) 24 | } 25 | annotatedString.text("Component", TextStyle(fontSize = 24.sp)) 26 | annotatedString.text(" ") 27 | annotatedString.text("Box") 28 | annotatedString.span(0, 5, SpanStyle(fontWeight = FontWeight.Bold)) 29 | annotatedString.span(6, 12, SpanStyle(textDecoration = TextDecoration.Underline)) 30 | ``` 31 | 32 | ### Box 33 | 34 | ```kotlin 35 | val box = box { 36 | child(heading) 37 | child(button) 38 | } 39 | 40 | ``` 41 | 42 | ### Column 43 | 44 | ```kotlin 45 | val column = column( 46 | verticalArrangement = Arrangement.SpaceBetween(4.dp), 47 | horizontalAlignment = Alignment.Center 48 | ) { 49 | child(heading) 50 | child(button) 51 | } 52 | 53 | ``` 54 | 55 | ### ContainedButton 56 | 57 | ```kotlin 58 | val containedButton = containedButton( 59 | onClick = {}, 60 | backgroundColor = Color.named("Primary"), 61 | contentColor = Color.named("OnPrimary"), 62 | elevation = 0.dp, 63 | shape = Shape.Rectangle 64 | ) { 65 | child(text("Learn more")) 66 | } 67 | ``` 68 | 69 | ### LazyColumn 70 | 71 | ```kotlin 72 | val lazyColumn = lazyColumn { 73 | child(heading) 74 | child(button) 75 | } 76 | 77 | ``` 78 | 79 | ### Navigation 80 | 81 | ```kotlin 82 | val navigation = navigation(start = "account_tab") { 83 | route("account_tab", accountTab) 84 | route("manage_account_screen", manageAccountScreen) 85 | } 86 | ``` 87 | 88 | ### Text 89 | 90 | ```kotlin 91 | val text = text("Component Box", Color.RED, TextStyle.BOLD) 92 | ``` 93 | 94 | ### TextButton 95 | 96 | ```kotlin 97 | val textButton = textButton(text = "Learn more") 98 | ``` -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Trail.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * A sequence of trees. 8 | * Represents the connection of root components. 9 | * For example, an onboarding flow. 10 | */ 11 | sealed interface Trail : ComponentBox { 12 | var root: Node? 13 | val nodes: MutableList> 14 | 15 | interface Node : Tree { 16 | val value: T 17 | var parent: Node? 18 | var child: Node? 19 | var prev: Node? 20 | var next: Node? 21 | } 22 | 23 | 24 | @Serializable 25 | data class Static( 26 | override var root: Node? = null, 27 | override val nodes: MutableList> = mutableListOf(root).filterNotNull().toMutableList() 28 | ) : Trail { 29 | fun node(node: Node) { 30 | if (root == null) { 31 | root = node 32 | nodes.add(node) 33 | return 34 | } 35 | 36 | val last = nodes.last() 37 | last.next = node 38 | node.prev = last 39 | nodes.add(node) 40 | } 41 | } 42 | 43 | interface Dynamic : Trail { 44 | @Composable 45 | fun node(tree: Tree.Dynamic) 46 | } 47 | 48 | } 49 | 50 | data class DynamicTrail( 51 | override var root: Trail.Node? = null, 52 | override val nodes: MutableList> = mutableListOf(root).filterNotNull().toMutableList() 53 | ) : Trail.Dynamic { 54 | @Composable 55 | override fun node(tree: Tree.Dynamic) { 56 | val node = TrailNode(tree) 57 | 58 | if (root == null) { 59 | root = node 60 | nodes.add(node) 61 | return 62 | } 63 | 64 | val last = nodes.last() 65 | last.next = node 66 | node.prev = last 67 | nodes.add(node) 68 | } 69 | } 70 | 71 | data class TrailNode( 72 | override val value: T, 73 | override var parent: Trail.Node? = null, 74 | override var child: Trail.Node? = null, 75 | override var prev: Trail.Node? = null, 76 | override var next: Trail.Node? = null 77 | ) : Trail.Node -------------------------------------------------------------------------------- /samples/counter/server/src/commonMain/kotlin/com/dropbox/componentbox/samples/counter/server/ui/dynamic/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.samples.counter.server.ui.dynamic 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.dropbox.componentbox.Alignment 5 | import com.dropbox.componentbox.Arrangement 6 | import com.dropbox.componentbox.Color 7 | import com.dropbox.componentbox.ComponentBoxExport 8 | import com.dropbox.componentbox.ContainedButton 9 | import com.dropbox.componentbox.Forest 10 | import com.dropbox.componentbox.LazyColumn 11 | import com.dropbox.componentbox.TextStyle 12 | import com.dropbox.componentbox.Tree 13 | import com.dropbox.componentbox.dp 14 | import com.dropbox.componentbox.lambda 15 | import com.dropbox.componentbox.model.StatefulComposable 16 | import com.dropbox.componentbox.model.statefulComponentBox 17 | import com.dropbox.componentbox.samples.counter.server.model.Counter 18 | import com.dropbox.componentbox.samples.counter.server.model.CounterEvent.Decrement 19 | import com.dropbox.componentbox.samples.counter.server.model.CounterEvent.Increment 20 | import com.dropbox.componentbox.samples.counter.server.ui.static.header 21 | import com.dropbox.componentbox.samples.counter.server.ui.static.static 22 | import com.dropbox.componentbox.text 23 | 24 | 25 | @Composable 26 | @ComponentBoxExport 27 | fun statefulCounterScreen() = statefulComponentBox(init = static) { 28 | counterScreen() 29 | } 30 | 31 | @Composable 32 | fun counterScreenUI() = Tree { 33 | LazyColumn( 34 | verticalArrangement = Arrangement.SpaceEvenly(2.dp), 35 | horizontalAlignment = Alignment.Start 36 | ) { 37 | child(header) 38 | child(Count()) 39 | child(IncrementButton()) 40 | child(DecrementButton()) 41 | } 42 | } 43 | 44 | @Composable 45 | fun counterScreen() = Forest { 46 | tree("screen", counterScreenUI()) 47 | } 48 | 49 | @Composable 50 | fun IncrementButton() = StatefulComposable { 51 | ContainedButton(onClick = lambda { it.on(Increment) }) { 52 | text(text = "+1") 53 | } 54 | } 55 | 56 | @Composable 57 | fun DecrementButton() = StatefulComposable { 58 | ContainedButton(onClick = lambda { it.on(Decrement) }) { 59 | text(text = "-1") 60 | } 61 | } 62 | 63 | @Composable 64 | fun Count() = StatefulComposable { 65 | text( 66 | text = "Count: ${it.state.value}", 67 | style = TextStyle(color = Color.Hex("#FF0000")) 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/SpanStyle.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Represents a style to be applied to a span of text. 7 | * @property color The text color to be applied to the span, or null if not specified. 8 | * @property backgroundColor The background color to be applied to the span, or null if not specified. 9 | * @property fontStyle The font style to be applied to the span, or null if not specified. 10 | * @property fontWeight The font weight to be applied to the span, or null if not specified. 11 | * @property fontSize The font size to be applied to the span, or null if not specified. 12 | * @property letterSpacing The letter spacing to be applied to the span, or null if not specified. 13 | * @property textDecoration The text decoration to be applied to the span, or null if not specified. 14 | */ 15 | @Serializable 16 | data class SpanStyle( 17 | val color: Color? = null, 18 | val backgroundColor: Color? = null, 19 | val fontStyle: FontStyle? = null, 20 | val fontWeight: FontWeight? = null, 21 | val fontSize: TextUnit? = null, 22 | val letterSpacing: TextUnit? = null, 23 | val textDecoration: TextDecoration? = null 24 | ) { 25 | companion object { 26 | fun from(style: SpanStyle? = null): SpanStyleBuilder = SpanStyleBuilder( 27 | style 28 | ?: SpanStyle() 29 | ) 30 | } 31 | 32 | @Serializable 33 | class SpanStyleBuilder(private var style: SpanStyle) { 34 | fun color(color: Color) { 35 | style = style.copy(color = color) 36 | } 37 | 38 | fun backgroundColor(color: Color) { 39 | style = style.copy(backgroundColor = color) 40 | } 41 | 42 | fun fontStyle(fontStyle: FontStyle) { 43 | style = style.copy(fontStyle = fontStyle) 44 | } 45 | 46 | fun fontWeight(fontWeight: FontWeight) { 47 | style = style.copy(fontWeight = fontWeight) 48 | } 49 | 50 | fun fontSize(fontSize: TextUnit) { 51 | style = style.copy(fontSize = fontSize) 52 | } 53 | 54 | fun letterSpacing(letterSpacing: TextUnit) { 55 | style = style.copy(letterSpacing = letterSpacing) 56 | } 57 | 58 | fun textDecoration(textDecoration: TextDecoration) { 59 | style = style.copy(textDecoration = textDecoration) 60 | } 61 | 62 | fun build() = style 63 | } 64 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/AnnotatedStringElement.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * Represents an element of an annotated string, which can be either a Text or a Span. 8 | */ 9 | @Serializable 10 | sealed interface AnnotatedStringElement { 11 | /** 12 | * Represents a piece of text with optional style information and a soft break flag. 13 | * @property text The text content. 14 | * @property style The style of the text, or null if not specified. 15 | * @property softBreak Whether the text should be followed by a soft break. 16 | */ 17 | @Serializable 18 | data class Text( 19 | val text: String, 20 | val style: TextStyle? = null, 21 | val softBreak: Boolean = false 22 | ) : AnnotatedStringElement 23 | 24 | 25 | /** 26 | * Represents a span of text with a style applied to it. 27 | * @property start The start index of the span in the annotated string. 28 | * @property end The end index of the span in the annotated string. 29 | * @property style The style applied to the span. 30 | */ 31 | @Serializable 32 | data class Span( 33 | val start: Int, 34 | val end: Int, 35 | val style: SpanStyle 36 | ) : AnnotatedStringElement 37 | 38 | /** 39 | * Represents the inline content added to an [AnnotatedString]. 40 | * Used to store the content ID and associated [InlineTextContent]. 41 | */ 42 | 43 | data class InlineContent( 44 | val id: String, 45 | val content: InlineTextContent 46 | ) : AnnotatedStringElement 47 | } 48 | 49 | /** 50 | * Used to define the inline content that will be displayed within a [Text] component. 51 | * @param placeholder A [Placeholder] object that defines the size and vertical alignment of the space reserved for the inline content. 52 | * @param content A composable function that represents the inline content to be displayed within the reserved space. 53 | */ 54 | 55 | data class InlineTextContent( 56 | val placeholder: Placeholder, 57 | val content: @Composable () -> Unit 58 | ) 59 | 60 | 61 | /** 62 | * Used to reserve space for inline content within a [Text] component. 63 | * @param width The width of the placeholder. 64 | * @param height The height of the placeholder. 65 | * @param verticalAlign A [PlaceholderVerticalAlign] enum value that specifies how the placeholder should be vertically aligned with respect to the surrounding text. 66 | */ 67 | @Serializable 68 | data class Placeholder( 69 | val width: TextUnit.Sp, 70 | val height: TextUnit.Sp, 71 | val verticalAlign: PlaceholderVerticalAlign 72 | ) 73 | 74 | /** 75 | * Used to define the vertical alignment of a [Placeholder] with respect to the surrounding text. 76 | */ 77 | enum class PlaceholderVerticalAlign { 78 | TextTop, 79 | TextCenter, 80 | TextBottom, 81 | AboveBaseline, 82 | BelowBaseline 83 | } 84 | 85 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 |

open source logo

3 | 4 | 5 |

Dropbox Open Source Code of Conduct

6 |

Dropbox believes that an inclusive development environment fosters greater technical achievement. To encourage a diverse group of contributors we've adopted this code of conduct.

7 | 8 | 9 | 10 | ## The Code 11 | The list of ground rules are by no means exhaustive, instead they should be used as a guide when participating in or contributing to an open source project maintained by Dropbox. 12 | 13 | - **Be friendly and patient.** 14 | - **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 15 | - **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. 16 | - **Be respectful.** We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It's important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Dropbox open source community should be respectful when dealing with one another. 17 | - **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: 18 | - Violent threats or language directed against another person. 19 | - Discriminatory jokes and language. 20 | - Posting sexually explicit or violent material. 21 | - Posting (or threatening to post) other people's personally identifying information ("doxing"). 22 | - Personal insults, especially those using racist or sexist terms. 23 | - Unwelcome sexual attention. 24 | - Advocating for, or encouraging, any of the above behavior. 25 | - Repeated harassment of others. In general, if someone asks you to stop, then stop. 26 | 27 | ## Reporting Issues 28 | If you believe someone is violating the code of conduct, we ask that you [report it to us](https://goo.gl/forms/sCD2o33YQW4NInhr2). All reports will be handled with discretion. If you believe anyone is in physical danger, please notify appropriate law enforcement first. After evaluating the report we will make a decision of how to respond, which can include asking (or requiring) anyone who violates this policy to leave the Dropbox open source community. 29 | 30 | ## Attribution 31 | This code of conduct is based on the [Open Code of Conduct](http://todogroup.org/opencodeofconduct/) and the [Django Code of Conduct](https://www.djangoproject.com/conduct/). 32 | -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Modifier.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Represents a modifier to be applied to a UI element. 7 | * @property background The background color to be applied to the UI element. 8 | * @property border The border to be applied to the UI element. 9 | * @property padding The padding values to be applied to the UI element. 10 | * @property size The size of the UI element. 11 | * @property shape The shape of the UI element. 12 | * @property alpha The alpha value to be applied to the UI element. 13 | * @property rotation The rotation angle to be applied to the UI element. 14 | * @property scaleX The horizontal scale factor to be applied to the UI element. 15 | * @property scaleY The vertical scale factor to be applied to the UI element. 16 | * @property offset The offset values to be applied to the UI element. 17 | * @property zIndex The z-index value to be applied to the UI element. 18 | * @property clip Whether clipping should be applied to the UI element. 19 | */ 20 | @Serializable 21 | data class Modifier( 22 | val background: Color? = null, 23 | val border: Border? = null, 24 | val padding: PaddingValues? = null, 25 | val size: Size? = null, 26 | val shape: Shape? = null, 27 | val alpha: Float = 1f, 28 | val rotation: Float = 0f, 29 | val scaleX: Float = 1f, 30 | val scaleY: Float = 1f, 31 | val offset: Offset? = null, 32 | val zIndex: Float = 0f, 33 | val clip: Boolean = true 34 | ) 35 | 36 | /** 37 | * Represents a border to be applied to a UI element. 38 | * @property width The width of the border. 39 | * @property color The color of the border. 40 | * @property shape The shape of the border. 41 | */ 42 | @Serializable 43 | data class Border( 44 | val width: Dp, 45 | val color: Color, 46 | val shape: Shape 47 | ) 48 | 49 | 50 | /** 51 | * Represents padding values to be applied to a UI element. 52 | * @property start The start padding value. 53 | * @property top The top padding value. 54 | * @property end The end padding value. 55 | * @property bottom The bottom padding value. 56 | */ 57 | @Serializable 58 | data class PaddingValues( 59 | val start: Dp, 60 | val top: Dp, 61 | val end: Dp, 62 | val bottom: Dp 63 | ) 64 | 65 | 66 | /** 67 | * Represents the size of a UI element. 68 | * @property width The width of the UI element. 69 | * @property height The height of the UI element. 70 | */ 71 | @Serializable 72 | data class Size( 73 | val width: Dp, 74 | val height: Dp 75 | ) 76 | 77 | /** 78 | * Represents a dimension value in density-independent pixels. 79 | * @property value The value of the dimension in density-independent pixels. 80 | */ 81 | @Serializable 82 | data class Dp(val value: Float) { 83 | operator fun plus(other: Dp): Dp = Dp(value + other.value) 84 | operator fun minus(other: Dp): Dp = Dp(value - other.value) 85 | operator fun times(scalar: Float): Dp = Dp(value * scalar) 86 | operator fun div(scalar: Float): Dp = Dp(value / scalar) 87 | } 88 | 89 | val Float.dp: Dp 90 | get() = Dp(this) 91 | 92 | val Int.dp: Dp 93 | get() = Dp(this.toFloat()) 94 | 95 | 96 | /** 97 | * Represents the offset of a UI element in density-independent pixels. 98 | * @property x The horizontal offset value. 99 | * @property y The vertical offset value. 100 | */ 101 | @Serializable 102 | data class Offset( 103 | val x: Dp = 0.dp, 104 | val y: Dp = 0.dp 105 | ) -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Button.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * The base class for a button. 8 | * @property modifier The modifier to be applied to the button. 9 | * @property enabled Whether the button should be enabled or disabled. 10 | */ 11 | @Serializable 12 | sealed class Button : Component { 13 | 14 | abstract val modifier: Modifier 15 | abstract val enabled: Boolean 16 | abstract val onClick: Action? 17 | 18 | /** 19 | * A button with a contained style. 20 | * 21 | * @property backgroundColor The background color of the button. 22 | * @property contentColor The content color of the button. 23 | * @property elevation The elevation of the button. 24 | * @property shape The shape of the button. 25 | */ 26 | @Serializable 27 | sealed class Contained : Button() { 28 | abstract val backgroundColor: Color? 29 | abstract val contentColor: Color? 30 | abstract val elevation: Dp? 31 | abstract val shape: Shape? 32 | abstract val children: MutableList 33 | 34 | /** 35 | * A button with a contained style and executable event handler. 36 | * @property onClick The callback function to be called when the button is clicked. 37 | */ 38 | data class Dynamic( 39 | override val modifier: Modifier = Modifier(), 40 | override val enabled: Boolean = true, 41 | override val onClick: Action.Lambda? = null, 42 | override val backgroundColor: Color? = null, 43 | override val contentColor: Color? = null, 44 | override val elevation: Dp? = null, 45 | override val shape: Shape? = null, 46 | override val children: MutableList = mutableListOf() 47 | ) : Contained() { 48 | fun child(component: Component) { 49 | children.add(component) 50 | } 51 | } 52 | 53 | /** 54 | * A button with a contained style and semantically identifiable event handler. 55 | * @property onClick The action to be performed when the button is clicked. 56 | */ 57 | @Serializable 58 | data class Static( 59 | override val modifier: Modifier = Modifier(), 60 | override val enabled: Boolean = true, 61 | override val onClick: Action.Semantic? = null, 62 | override val backgroundColor: Color? = null, 63 | override val contentColor: Color? = null, 64 | override val elevation: Dp? = null, 65 | override val shape: Shape? = null, 66 | override val children: MutableList = mutableListOf() 67 | ) : Contained() { 68 | fun child(component: Component) { 69 | children.add(component) 70 | } 71 | } 72 | } 73 | 74 | 75 | /** 76 | * A button with a text style. 77 | * 78 | * @property contentColor: Color 79 | */ 80 | @Serializable 81 | sealed class Text : Button() { 82 | abstract val text: String 83 | abstract val contentColor: Color? 84 | 85 | data class Dynamic( 86 | override val modifier: Modifier = Modifier(), 87 | override val text: String, 88 | override val contentColor: Color? = null, 89 | override val enabled: Boolean = true, 90 | override val onClick: Action.Lambda? = null 91 | ) : Text() 92 | 93 | @Serializable 94 | data class Static( 95 | override val modifier: Modifier = Modifier(), 96 | override val text: String, 97 | override val contentColor: Color? = null, 98 | override val enabled: Boolean = true, 99 | override val onClick: Action.Semantic? = null 100 | ) : Text() 101 | } 102 | } -------------------------------------------------------------------------------- /componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Component.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | sealed interface Component 8 | 9 | fun annotatedString( 10 | elements: MutableList = mutableListOf() 11 | ): Component = AnnotatedString(elements) 12 | 13 | fun box( 14 | modifier: Modifier = Modifier(), 15 | events: Events.Semantic? = null, 16 | children: Box.() -> Unit 17 | ): Component = Box.Static(modifier, events) 18 | 19 | @Composable 20 | fun Box( 21 | modifier: Modifier = Modifier(), 22 | events: Events.Lambda? = null, 23 | children: @Composable Box.() -> Unit 24 | ): Component = Box.Dynamic(modifier, events) 25 | 26 | fun column( 27 | modifier: Modifier = Modifier(), 28 | events: Events.Semantic? = null, 29 | verticalArrangement: Arrangement.Vertical? = null, 30 | horizontalAlignment: Alignment.Horizontal? = null, 31 | children: Column.Static.() -> Unit 32 | ): Component = Column.Static(modifier, events, verticalArrangement, horizontalAlignment) 33 | 34 | @Composable 35 | fun Column( 36 | modifier: Modifier = Modifier(), 37 | events: Events.Lambda? = null, 38 | verticalArrangement: Arrangement.Vertical? = null, 39 | horizontalAlignment: Alignment.Horizontal? = null, 40 | children: @Composable Column.Dynamic.() -> Unit 41 | ): Component = Column.Dynamic(modifier, events, verticalArrangement, horizontalAlignment) 42 | 43 | 44 | fun containedButton( 45 | modifier: Modifier = Modifier(), 46 | enabled: Boolean = true, 47 | onClick: Action.Semantic? = null, 48 | backgroundColor: Color? = null, 49 | contentColor: Color? = null, 50 | elevation: Dp? = null, 51 | shape: Shape, 52 | children: Button.Contained.Static.() -> Unit 53 | ): Component = Button.Contained.Static( 54 | modifier, 55 | enabled, 56 | onClick, 57 | backgroundColor, 58 | contentColor, 59 | elevation, 60 | shape 61 | ) 62 | 63 | 64 | @Composable 65 | fun ContainedButton( 66 | modifier: Modifier = Modifier(), 67 | enabled: Boolean = true, 68 | onClick: Action.Lambda? = null, 69 | backgroundColor: Color? = null, 70 | contentColor: Color? = null, 71 | elevation: Dp? = null, 72 | shape: Shape? = null, 73 | children: @Composable Button.Contained.Dynamic.() -> Unit 74 | ): Component = Button.Contained.Dynamic( 75 | modifier, 76 | enabled, 77 | onClick, 78 | backgroundColor, 79 | contentColor, 80 | elevation, 81 | shape 82 | ) 83 | 84 | @Composable 85 | fun LazyColumn( 86 | modifier: Modifier = Modifier(), 87 | events: Events.Lambda? = null, 88 | verticalArrangement: Arrangement.Vertical? = null, 89 | horizontalAlignment: Alignment.Horizontal? = null, 90 | contentPaddingValues: PaddingValues? = null, 91 | children: @Composable LazyColumn.Dynamic.() -> Unit 92 | ): Component = LazyColumn.Dynamic( 93 | modifier, 94 | events, 95 | verticalArrangement, 96 | horizontalAlignment, 97 | contentPaddingValues 98 | ) 99 | 100 | 101 | fun lazyColumn( 102 | modifier: Modifier = Modifier(), 103 | events: Events.Semantic? = null, 104 | verticalArrangement: Arrangement.Vertical? = null, 105 | horizontalAlignment: Alignment.Horizontal? = null, 106 | contentPaddingValues: PaddingValues? = null, 107 | children: LazyColumn.Static.() -> Unit 108 | ): Component = LazyColumn.Static( 109 | modifier, 110 | events, 111 | verticalArrangement, 112 | horizontalAlignment, 113 | contentPaddingValues 114 | ) 115 | 116 | fun text( 117 | text: String, 118 | color: Color? = null, 119 | style: TextStyle? = null 120 | ): Component = Text(text, color, style) 121 | 122 | 123 | fun textButton( 124 | modifier: Modifier = Modifier(), 125 | text: String, 126 | contentColor: Color? = null, 127 | enabled: Boolean = false, 128 | onClick: Action.Semantic? = null, 129 | ): Component = Button.Text.Static(modifier, text, contentColor, enabled, onClick) 130 | 131 | @Composable 132 | fun TextButton( 133 | modifier: Modifier = Modifier(), 134 | text: String, 135 | contentColor: Color? = null, 136 | enabled: Boolean = false, 137 | onClick: Action.Lambda? = null, 138 | ): Component = Button.Text.Dynamic(modifier, text, contentColor, enabled, onClick) 139 | -------------------------------------------------------------------------------- /componentbox-gradle-plugin/src/main/kotlin/com/dropbox/componentbox/plugin/ComponentBoxPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.componentbox.plugin 2 | 3 | import app.cash.zipline.gradle.ZiplineCompileTask 4 | import app.cash.zipline.gradle.ZiplinePlugin 5 | import com.dropbox.componentbox.SerializableComponentBox 6 | import com.dropbox.componentbox.Tree 7 | import kotlinx.serialization.encodeToString 8 | import kotlinx.serialization.json.Json 9 | import org.gradle.api.Plugin 10 | import org.gradle.api.Project 11 | import org.gradle.internal.impldep.org.objectweb.asm.Opcodes 12 | import org.jetbrains.kotlin.gradle.dsl.KotlinCompile 13 | import org.objectweb.asm.AnnotationVisitor 14 | import java.io.FileInputStream 15 | import java.net.URL 16 | import java.net.URLClassLoader 17 | 18 | class ComponentBoxPlugin : Plugin { 19 | private val ziplinePlugin = ZiplinePlugin() 20 | 21 | override fun apply(target: Project) { 22 | target.pluginManager.apply("com.dropbox.componentbox.plugin") 23 | ziplinePlugin.apply(target) 24 | createZiplineExecutionTask(target) 25 | createComponentBoxJsonTask(target) 26 | } 27 | 28 | private fun createZiplineExecutionTask(target: Project) { 29 | val ziplineExecutionTask = target.tasks.register("componentBoxZipline") { 30 | it.group = "Component Box" 31 | it.description = "Execute all Zipline tasks" 32 | } 33 | 34 | target.tasks.withType(ZiplineCompileTask::class.java) { ziplineCompileTask -> 35 | ziplineExecutionTask.configure { 36 | it.dependsOn(ziplineCompileTask) 37 | } 38 | } 39 | } 40 | 41 | private fun createComponentBoxJsonTask(project: Project) { 42 | val compileTaskName = "compileKotlinMetadata" 43 | val compileTask = project.tasks.named(compileTaskName, KotlinCompile::class.java) 44 | 45 | val json = Json { 46 | prettyPrint = true 47 | } 48 | 49 | project.tasks.register("componentBoxJson") { 50 | 51 | it.dependsOn(compileTask) 52 | it.group = "Component Box" 53 | it.description = 54 | "Generate JSON from Component Box exports" 55 | 56 | it.doLast { 57 | 58 | val jvmMainOutputDir = project.buildDir.resolve("classes/kotlin/jvm/main") 59 | 60 | val files = 61 | jvmMainOutputDir.walk().flatMap { file -> listOf(file, file.parentFile) } 62 | .filter { file -> file.isFile && file.name.endsWith(".class") } 63 | .toList() 64 | 65 | 66 | val urls = files.filter { 67 | it.isFile && it.name.endsWith(".class") 68 | }.map { 69 | it.toURI().toURL() 70 | } 71 | .toTypedArray() 72 | 73 | val classpath = arrayOf(jvmMainOutputDir.toURI().toURL()) + urls 74 | val urlClassLoader = URLClassLoader(classpath, javaClass.classLoader) 75 | 76 | val annotatedClasses = mutableListOf>() 77 | 78 | 79 | urls.forEach { url -> 80 | try { 81 | val classReader = org.objectweb.asm.ClassReader(FileInputStream(url.path)) 82 | val myAnnotationChecker = MyAnnotationChecker() 83 | 84 | classReader.accept(myAnnotationChecker, 0) 85 | 86 | val className = classReader.className 87 | 88 | if (myAnnotationChecker.hasAnnotation) { 89 | annotatedClasses.add(Pair(url, className)) 90 | } 91 | } catch (error: Throwable) { 92 | println(error) 93 | } 94 | } 95 | 96 | annotatedClasses.forEach { (url, className) -> 97 | val name = className.replace("/", ".") 98 | 99 | try { 100 | 101 | val clazz = urlClassLoader.loadClass(name) 102 | val method = clazz.methods.firstOrNull { method -> 103 | method.isAnnotationPresent(SerializableComponentBox::class.java) 104 | } 105 | 106 | if (method != null) { 107 | val instance = clazz.newInstance() 108 | val result = method.invoke(instance) as? Tree.Static 109 | 110 | if (result != null) { 111 | val serializedResult = json.encodeToString(result) 112 | val path = "/json/${className}/${method.name}.json" 113 | val outputFile = 114 | project.file(project.projectDir.path + "/componentbox" + path) 115 | 116 | if (!outputFile.parentFile.exists()) { 117 | outputFile.parentFile.mkdirs() 118 | } 119 | 120 | if (outputFile.exists()) { 121 | outputFile.writeText(serializedResult) 122 | } else { 123 | outputFile.createNewFile() 124 | outputFile.writeText(serializedResult) 125 | } 126 | 127 | } 128 | } 129 | } catch (error: Throwable) { 130 | println(error) 131 | } 132 | } 133 | } 134 | } 135 | } 136 | } 137 | 138 | 139 | class MyAnnotationChecker : org.objectweb.asm.ClassVisitor(Opcodes.ASM9) { 140 | var hasAnnotation = false 141 | override fun visitAnnotation( 142 | descriptor: String?, 143 | visible: Boolean 144 | ): AnnotationVisitor { 145 | 146 | if (descriptor?.contains("Lcom/dropbox/componentbox/ComponentBoxExport;") == true) { 147 | hasAnnotation = true 148 | } 149 | 150 | return super.visitAnnotation(descriptor, visible) ?: FallbackAnnotationVisitor() 151 | } 152 | } 153 | 154 | class FallbackAnnotationVisitor : AnnotationVisitor(Opcodes.ASM9) { 155 | var hasAnnotation = false 156 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | componentbox 2 | 3 | # Component Box 4 | 5 | > [!NOTE] 6 | > 7 | > **No longer active!** 8 | > 9 | > After internal experimentation and discussion, we've decided to move forward with other options. 10 | 11 | ### Sample 12 | 13 | #### Model (server) 14 | 15 | ```kotlin 16 | class Counter : ComposableModel(0) { 17 | private fun increment() { 18 | withState { 19 | setState(state.value + 1) 20 | } 21 | } 22 | 23 | private fun decrement() { 24 | withState { 25 | setState(state.value - 1) 26 | } 27 | } 28 | 29 | override fun on(event: CounterEvent) = when (event) { 30 | Increment -> increment() 31 | Decrement -> decrement() 32 | } 33 | } 34 | ``` 35 | 36 | #### UI Representation (server) 37 | 38 | ##### Static 39 | 40 | ###### [Tree](componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Tree.kt) 41 | 42 | ```kotlin 43 | @SerializableComponentBox 44 | fun main() = componentBox { 45 | tree { 46 | lazyColumn( 47 | verticalArrangement = Arrangement.SpaceEvenly(2.dp), 48 | horizontalAlignment = Alignment.Start 49 | ) { 50 | child(header) 51 | child(count) 52 | child(incrementButton) 53 | child(decrementButton) 54 | } 55 | } 56 | } 57 | 58 | val header = text( 59 | text = "Component Box Counter", 60 | style = TextStyle(fontWeight = FontWeight.ExtraBold) 61 | ) 62 | 63 | val count = text( 64 | text = "Count: \${COUNTER_STATE}", 65 | style = TextStyle(color = Color.Hex("#FF0000")) 66 | ) 67 | 68 | val incrementButton = textButton( 69 | text = "+1", 70 | onClick = semantic { Increment } 71 | ) 72 | 73 | val decrementButton = textButton( 74 | text = "-1", 75 | onClick = semantic { Decrement } 76 | ) 77 | ``` 78 | 79 | ##### Dynamic 80 | 81 | ###### [Graph](componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Graph.kt) 82 | 83 | ```kotlin 84 | @Composable 85 | @ComponentBoxExport 86 | fun main() = statefulComponentBoxGraph(init = null) { 87 | Graph(start = CounterOnboardingFlow.value) { 88 | componentBox(CounterLoginScreen.value, LoginScreen()) 89 | componentBox(CounterOnboardingFlow.value, OnboardingFlow()) 90 | componentBox(CounterScreen.Home.value, HomeScreen()) 91 | } 92 | } 93 | ``` 94 | 95 | ###### [Forest](componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Forest.kt) 96 | 97 | ```kotlin 98 | @Composable 99 | fun LoginScreen() = Forest { 100 | tree("heading", Tree { LoginHeading() }) 101 | tree("button", Tree { LoginButton() }) 102 | } 103 | ``` 104 | 105 | ###### [Trail](componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Trail.kt) 106 | 107 | ```kotlin 108 | @Composable 109 | fun OnboardingFlow() = Trail { 110 | node(WelcomeScreen()) 111 | node(FeatureDiscoveryScreen()) 112 | node(HomeScreen()) 113 | } 114 | ``` 115 | 116 | ###### [Tree](componentbox/src/commonMain/kotlin/com/dropbox/componentbox/Tree.kt) 117 | 118 | ```kotlin 119 | @Composable 120 | fun HomeScreen() = ComponentBox { 121 | Tree { 122 | LazyColumn( 123 | verticalArrangement = Arrangement.SpaceEvenly(2.dp), 124 | horizontalAlignment = Alignment.Start 125 | ) { 126 | child(header) 127 | child(Count()) 128 | child(IncrementButton()) 129 | child(DecrementButton()) 130 | } 131 | } 132 | } 133 | 134 | @Composable 135 | fun IncrementButton() = StatefulComposable { 136 | ContainedButton(onClick = lambda { it.on(Increment) }) { 137 | text(text = "+1") 138 | } 139 | } 140 | 141 | @Composable 142 | fun DecrementButton() = StatefulComposable { 143 | ContainedButton(onClick = lambda { it.on(Decrement) }) { 144 | text(text = "-1") 145 | } 146 | } 147 | 148 | @Composable 149 | fun Count() = StatefulComposable { 150 | text( 151 | text = "Count: ${it.state.value}", 152 | style = TextStyle(color = Color.Hex("#FF0000")) 153 | ) 154 | } 155 | 156 | 157 | ``` 158 | 159 | #### Binaries (server) 160 | 161 | ```shell 162 | ./gradlew componentBoxJs 163 | ``` 164 | 165 | ```shell 166 | ./gradlew componentBoxJson 167 | ``` 168 | 169 | #### Jetpack Compose (Android) 170 | 171 | ##### Activity 172 | 173 | ```kotlin 174 | class ComponentBoxActivity : ComponentActivity() { 175 | private val scope = CoroutineScope(Dispatchers.Default) 176 | private val service = ComponentBoxService(scope) 177 | private val componentBox = service.componentBox 178 | private val render = RenderingEngine() 179 | 180 | override fun onStart() { 181 | super.onStart() 182 | service.launch(MANIFEST_URL) 183 | } 184 | 185 | override fun onCreate(savedInstanceState: Bundle?) { 186 | super.onCreate(savedInstanceState) 187 | 188 | setContent { 189 | val root = componentBox.collectAsState() 190 | render { 191 | root.value 192 | } 193 | } 194 | } 195 | } 196 | ``` 197 | 198 | ##### Composable 199 | 200 | ```kotlin 201 | @Composable 202 | fun ComponentBoxView(componentBox: StateFlow, render: RenderingEngine) { 203 | val root = componentBox.collectAsState() 204 | render { 205 | root.value 206 | } 207 | } 208 | ``` 209 | 210 | #### React (web) 211 | 212 | ```js 213 | export default function ComponentBoxView(props: {manifestUrl: string}) { 214 | const [root, setRoot] = useState(null); 215 | const service = new ComponentBoxService(); 216 | const render = new RenderingEngine(); 217 | 218 | useEffect(() => { 219 | async function launch(manifestUrl: string): Tree { 220 | const componentBox = await service.launch(manifestUrl) 221 | setRoot(componentBox.root) 222 | } 223 | 224 | launch(props.manifestUrl) 225 | 226 | }, [props.manifestUrl]); 227 | 228 | return render(root) 229 | } 230 | ``` 231 | 232 | #### SwiftUI (iOS) 233 | 234 | ```swift 235 | struct ComponentBoxView: View { 236 | @StateObject private var service = ComponentBoxService() 237 | @State private var root: Component? 238 | 239 | var body: some View { 240 | render { 241 | root 242 | } 243 | .onAppear { 244 | service.launch(from: MANIFEST_URL) { result in 245 | switch result { 246 | case .success(let componentBox): 247 | DispatchQueue.main.async { 248 | self.root = componentBox.root 249 | } 250 | } 251 | } 252 | } 253 | } 254 | ``` 255 | 256 | ## Snapshots 257 | 258 | Snapshots are available 259 | in [Sonatype's snapshots repository](https://s01.oss.sonatype.org/content/repositories/snapshots/com/dropbox/componentbox/). 260 | 261 | ## License 262 | 263 | ```text 264 | Copyright (c) 2023 Dropbox, Inc. 265 | 266 | Licensed under the Apache License, Version 2.0 (the "License"); 267 | you may not use this file except in compliance with the License. 268 | You may obtain a copy of the License at 269 | 270 | http://www.apache.org/licenses/LICENSE-2.0 271 | 272 | Unless required by applicable law or agreed to in writing, software 273 | distributed under the License is distributed on an "AS IS" BASIS, 274 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 275 | See the License for the specific language governing permissions and 276 | limitations under the License. 277 | ``` 278 | 279 | ## Acknowledgments 280 | | [cashapp](https://github.com/cashapp) | Thanks to our friends at [Cash App](http://github.com/cashapp) for [Zipline](http://github.com/cashapp/zipline) | 281 | |:-:|---| 282 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS --------------------------------------------------------------------------------