├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .idea └── copyright │ ├── Apache.xml │ └── profiles_settings.xml ├── LICENSE.txt ├── build-logic ├── convention │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ ├── kotlin-jvm-convention.gradle.kts │ │ ├── kotlin-jvm-convention.kt │ │ └── kotlin-library-convention.gradle.kts ├── settings.gradle.kts └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── build.gradle.kts ├── contributing.md ├── core ├── .gitignore ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── com │ └── tunjid │ └── mutator │ └── Mutator.kt ├── coroutines ├── .gitignore ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── tunjid │ │ └── mutator │ │ └── coroutines │ │ ├── ActionStateFlowMutator.kt │ │ ├── FlowMutationExt.kt │ │ ├── FlowMutationStream.kt │ │ ├── StateFlowMutator.kt │ │ └── StateMutation.kt │ └── commonTest │ └── kotlin │ └── com │ └── tunjid │ └── mutator │ └── coroutines │ ├── ActionStateMutatorKtTest.kt │ ├── FlowMutationStreamKtTest.kt │ ├── State.kt │ └── StateProductionKtTest.kt ├── demo ├── .gitignore ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── tunjid │ │ └── mutator │ │ └── demo │ │ ├── App.kt │ │ ├── Colors.kt │ │ ├── Utilities.kt │ │ ├── editor │ │ ├── Layouts.kt │ │ └── Text.kt │ │ ├── sections │ │ ├── 1_State_Production_With_Flows.kt │ │ ├── 2_User_Actions.kt │ │ ├── 3_Combining.kt │ │ ├── 4_Merging.kt │ │ ├── 5_Formalizing.kt │ │ ├── 6_Conflicts.kt │ │ ├── 7_Conflict_Resolution.kt │ │ └── Template.kt │ │ ├── snails │ │ ├── Snail.kt │ │ ├── Snail0.kt │ │ ├── Snail1.kt │ │ ├── Snail10.kt │ │ ├── Snail11.kt │ │ ├── Snail2.kt │ │ ├── Snail3.kt │ │ ├── Snail4.kt │ │ ├── Snail5.kt │ │ ├── Snail6.kt │ │ ├── Snail7.kt │ │ ├── Snail8.kt │ │ └── Snail9.kt │ │ └── udfvisualizer │ │ └── UDFVisualizerStateHolder.kt │ ├── desktopMain │ ├── kotlin │ │ ├── com │ │ │ └── tunjid │ │ │ │ └── mutator │ │ │ │ └── demo │ │ │ │ ├── Color.kt │ │ │ │ ├── editor │ │ │ │ ├── Editor.kt │ │ │ │ ├── EditorView.kt │ │ │ │ ├── Fonts.kt │ │ │ │ ├── Layouts.kt │ │ │ │ └── Text.kt │ │ │ │ ├── platform │ │ │ │ └── Resources.kt │ │ │ │ ├── snails │ │ │ │ └── Snail.kt │ │ │ │ └── udfvisualizer │ │ │ │ └── UDFVisualizer.kt │ │ └── main.desktop.kt │ └── resources │ │ └── font │ │ ├── jetbrainsmono_bold.ttf │ │ ├── jetbrainsmono_bold_italic.ttf │ │ ├── jetbrainsmono_extrabold.ttf │ │ ├── jetbrainsmono_extrabold_italic.ttf │ │ ├── jetbrainsmono_italic.ttf │ │ ├── jetbrainsmono_medium.ttf │ │ ├── jetbrainsmono_medium_italic.ttf │ │ └── jetbrainsmono_regular.ttf │ └── jsMain │ ├── kotlin │ ├── com │ │ └── tunjid │ │ │ └── mutator │ │ │ └── demo │ │ │ ├── Color.kt │ │ │ ├── editor │ │ │ ├── Layouts.kt │ │ │ ├── Markdown.kt │ │ │ └── Text.kt │ │ │ ├── snails │ │ │ └── Snail.kt │ │ │ └── udfvisualizer │ │ │ └── UDFVisualizer.kt │ └── main.js.kt │ └── resources │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── hljs.css │ ├── index.html │ ├── manifest.json │ ├── snail.svg │ └── styles.css ├── docs ├── META-INF │ └── MANIFEST.MF ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── demo.js ├── demo.js.LICENSE.txt ├── demo.js.map ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── hljs.css ├── index.html ├── manifest.json ├── snail.svg └── styles.css ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── libraryVersion.properties ├── readme.md └── settings.gradle.kts /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: JVM Tests 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: checkout 16 | uses: actions/checkout@v2 17 | - name: Set up JDK 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '17' 21 | distribution: 'adopt' 22 | - name: Validate Gradle wrapper 23 | uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b 24 | - name: JVM tests 25 | run: ./gradlew jvmTest 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/copyright/Apache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /build-logic/convention/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | `kotlin-dsl` 19 | } 20 | 21 | group = "com.tunjid.tiler.buildlogic" 22 | 23 | java { 24 | sourceCompatibility = JavaVersion.VERSION_17 25 | targetCompatibility = JavaVersion.VERSION_17 26 | } 27 | 28 | dependencies { 29 | implementation(libs.jetbrains.compose.gradlePlugin) 30 | implementation(libs.kotlin.gradlePlugin) 31 | implementation(libs.dokka.gradlePlugin) 32 | } 33 | -------------------------------------------------------------------------------- /build-logic/convention/src/main/kotlin/kotlin-jvm-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | configureKotlinJvm() 18 | -------------------------------------------------------------------------------- /build-logic/convention/src/main/kotlin/kotlin-jvm-convention.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import org.gradle.api.Action 18 | import org.gradle.api.JavaVersion 19 | import org.gradle.api.plugins.JavaPluginExtension 20 | import org.gradle.kotlin.dsl.configure 21 | import org.gradle.kotlin.dsl.withType 22 | import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions 23 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 24 | 25 | /* 26 | * Copyright 2021 Google LLC 27 | * 28 | * Licensed under the Apache License, Version 2.0 (the "License"); 29 | * you may not use this file except in compliance with the License. 30 | * You may obtain a copy of the License at 31 | * 32 | * https://www.apache.org/licenses/LICENSE-2.0 33 | * 34 | * Unless required by applicable law or agreed to in writing, software 35 | * distributed under the License is distributed on an "AS IS" BASIS, 36 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | * See the License for the specific language governing permissions and 38 | * limitations under the License. 39 | */ 40 | 41 | /** 42 | * Configure base Kotlin options for JVM (non-Android) 43 | */ 44 | internal fun org.gradle.api.Project.configureKotlinJvm() { 45 | extensions.configure { 46 | // Up to Java 11 APIs are available through desugaring 47 | // https://developer.android.com/studio/write/java11-minimal-support-table 48 | sourceCompatibility = JavaVersion.VERSION_11 49 | targetCompatibility = JavaVersion.VERSION_11 50 | } 51 | configureKotlin() 52 | } 53 | 54 | /** 55 | * Configure base Kotlin options 56 | */ 57 | private fun org.gradle.api.Project.configureKotlin() { 58 | // Use withType to workaround https://youtrack.jetbrains.com/issue/KT-55947 59 | tasks.withType().configureEach { 60 | kotlinOptions { 61 | // Set JVM target to 11 62 | jvmTarget = JavaVersion.VERSION_11.toString() 63 | freeCompilerArgs = freeCompilerArgs + listOf( 64 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", 65 | "-Xuse-experimental=kotlinx.coroutines.FlowPreview" 66 | ) 67 | } 68 | } 69 | val kotlinOptions = "kotlinOptions" 70 | if (extensions.findByName(kotlinOptions) != null) { 71 | extensions.configure(kotlinOptions, Action { 72 | jvmTarget = JavaVersion.VERSION_11.toString() 73 | freeCompilerArgs = freeCompilerArgs + listOf( 74 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", 75 | "-Xuse-experimental=kotlinx.coroutines.FlowPreview" 76 | ) 77 | }) 78 | } 79 | } -------------------------------------------------------------------------------- /build-logic/convention/src/main/kotlin/kotlin-library-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 18 | 19 | /* 20 | * Copyright 2021 Google LLC 21 | * 22 | * Licensed under the Apache License, Version 2.0 (the "License"); 23 | * you may not use this file except in compliance with the License. 24 | * You may obtain a copy of the License at 25 | * 26 | * https://www.apache.org/licenses/LICENSE-2.0 27 | * 28 | * Unless required by applicable law or agreed to in writing, software 29 | * distributed under the License is distributed on an "AS IS" BASIS, 30 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31 | * See the License for the specific language governing permissions and 32 | * limitations under the License. 33 | */ 34 | 35 | plugins { 36 | kotlin("multiplatform") 37 | `maven-publish` 38 | signing 39 | id("org.jetbrains.dokka") 40 | } 41 | 42 | kotlin { 43 | js(IR) { 44 | nodejs() 45 | browser() 46 | } 47 | 48 | jvm() 49 | 50 | ios() 51 | iosSimulatorArm64() 52 | linuxX64() 53 | macosX64() 54 | macosArm64() 55 | mingwX64() 56 | tvos() 57 | tvosSimulatorArm64() 58 | watchos() 59 | watchosSimulatorArm64() 60 | 61 | sourceSets { 62 | @Suppress("UnusedPrivateMember") 63 | val commonMain by getting 64 | val commonTest by getting { 65 | dependencies { 66 | implementation(kotlin("test")) 67 | } 68 | } 69 | all { 70 | languageSettings.apply { 71 | optIn("androidx.compose.animation.ExperimentalAnimationApi") 72 | optIn("androidx.compose.foundation.ExperimentalFoundationApi") 73 | optIn("androidx.compose.ui.ExperimentalComposeUiApi") 74 | optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") 75 | optIn("kotlinx.coroutines.FlowPreview") 76 | } 77 | } 78 | } 79 | targets.withType(KotlinNativeTarget::class.java) { 80 | binaries.all { 81 | binaryOptions["memoryModel"] = "experimental" 82 | } 83 | } 84 | configureKotlinJvm() 85 | } 86 | 87 | allprojects { 88 | val versionKey = project.name + "_version" 89 | val libProps = parent?.ext?.get("libProps") as? java.util.Properties 90 | ?: return@allprojects 91 | group = libProps["groupId"] as String 92 | version = libProps[versionKey] as String 93 | 94 | task("printProjectVersion") { 95 | doLast { 96 | println(">> " + project.name + " version is " + version) 97 | } 98 | } 99 | } 100 | 101 | val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) { 102 | dokkaSourceSets { 103 | named("iosTest") { 104 | suppress.set(true) 105 | } 106 | } 107 | } 108 | 109 | val javadocJar: TaskProvider by tasks.registering(Jar::class) { 110 | dependsOn(dokkaHtml) 111 | archiveClassifier.set("javadoc") 112 | from(dokkaHtml.outputDirectory) 113 | } 114 | 115 | publishing { 116 | publications { 117 | withType { 118 | artifact(javadocJar) 119 | pom { 120 | name.set(project.name) 121 | description.set("A tiny library for representing mutable states and the types that drive said mutations") 122 | url.set("https://github.com/tunjid/Mutator") 123 | licenses { 124 | license { 125 | name.set("Apache License 2.0") 126 | url.set("https://github.com/tunjid/Mutator/blob/main/LICENSE") 127 | } 128 | } 129 | developers { 130 | developer { 131 | id.set("tunjid") 132 | name.set("Adetunji Dahunsi") 133 | email.set("tjdah100@gmail.com") 134 | } 135 | } 136 | scm { 137 | connection.set("scm:git:github.com/tunjid/Mutator.git") 138 | developerConnection.set("scm:git:ssh://github.com/tunjid/Mutator.git") 139 | url.set("https://github.com/tunjid/Mutator/tree/main") 140 | } 141 | } 142 | 143 | } 144 | } 145 | repositories { 146 | val localProperties = parent?.ext?.get("localProps") as? java.util.Properties 147 | ?: return@repositories 148 | 149 | val publishUrl = localProperties.getProperty("publishUrl") 150 | if (publishUrl != null) { 151 | maven { 152 | name = localProperties.getProperty("repoName") 153 | url = uri(localProperties.getProperty("publishUrl")) 154 | credentials { 155 | username = localProperties.getProperty("username") 156 | password = localProperties.getProperty("password") 157 | } 158 | } 159 | } 160 | } 161 | } 162 | 163 | 164 | signing { 165 | val localProperties = parent?.ext?.get("localProps") as? java.util.Properties 166 | ?: return@signing 167 | 168 | val signingKey = localProperties.getProperty("signingKey") 169 | val signingPassword = localProperties.getProperty("signingPassword") 170 | 171 | if (signingKey != null && signingPassword != null) { 172 | useInMemoryPgpKeys(signingKey, signingPassword) 173 | sign(publishing.publications) 174 | } 175 | } 176 | 177 | val signingTasks = tasks.withType() 178 | tasks.withType().configureEach { 179 | dependsOn(signingTasks) 180 | } -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 18 | 19 | dependencyResolutionManagement { 20 | repositories { 21 | gradlePluginPortal() 22 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 23 | mavenCentral() 24 | google() 25 | } 26 | versionCatalogs { 27 | create("libs") { 28 | from(files("../gradle/libs.versions.toml")) 29 | } 30 | } 31 | } 32 | 33 | rootProject.name = "build-logic" 34 | include(":convention") 35 | -------------------------------------------------------------------------------- /build-logic/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/build-logic/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /build-logic/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | #Mon Jul 05 07:23:39 EDT 2021 18 | distributionBase=GRADLE_USER_HOME 19 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip 20 | distributionPath=wrapper/dists 21 | zipStorePath=wrapper/dists 22 | zipStoreBase=GRADLE_USER_HOME 23 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | buildscript { 18 | extra.apply { 19 | set("localProps", java.util.Properties().apply { 20 | file("local.properties").let { file -> 21 | if (file.exists()) load(java.io.FileInputStream(file)) 22 | } 23 | }) 24 | set("libProps", java.util.Properties().apply { 25 | file("libraryVersion.properties").let { file -> 26 | if (file.exists()) load(java.io.FileInputStream(file)) 27 | } 28 | }) 29 | } 30 | repositories { 31 | google() 32 | mavenCentral() 33 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 34 | maven("https://plugins.gradle.org/m2/") 35 | } 36 | } 37 | 38 | plugins { 39 | alias(libs.plugins.android.application) apply false 40 | alias(libs.plugins.android.library) apply false 41 | alias(libs.plugins.jetbrains.compose) apply false 42 | alias(libs.plugins.jetbrains.dokka) apply false 43 | alias(libs.plugins.kotlin.android) apply false 44 | alias(libs.plugins.kotlin.multiplatform) apply false 45 | } 46 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("kotlin-library-convention") 19 | } 20 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/tunjid/mutator/Mutator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator 18 | 19 | /** 20 | * Type definition for a unit of change for a type [State]. 21 | */ 22 | typealias Mutation = State.() -> State 23 | 24 | typealias StateHolder = StateMutator 25 | 26 | interface StateMutator { 27 | val state: State 28 | } 29 | 30 | interface ActionStateMutator : StateMutator { 31 | val accept: (Action) -> Unit 32 | } 33 | 34 | /** 35 | * Syntactic sugar for creating a [Mutation] 36 | */ 37 | inline fun mutationOf(noinline mutation: State.() -> State): Mutation = mutation 38 | 39 | /** 40 | * Identity [Mutation] function; semantically a no op [Mutation] 41 | */ 42 | fun identity(): Mutation = mutationOf { this } 43 | 44 | /** 45 | * Combines two [Mutation] instances into a single [Mutation] 46 | */ 47 | operator fun Mutation.plus(other: Mutation) = mutationOf inner@{ 48 | val result = this@plus(this@inner) 49 | other.invoke(result) 50 | } 51 | -------------------------------------------------------------------------------- /coroutines/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /coroutines/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("kotlin-library-convention") 19 | } 20 | 21 | kotlin { 22 | sourceSets { 23 | val commonMain by getting { 24 | dependencies { 25 | implementation(project(":core")) 26 | implementation(libs.kotlinx.coroutines.core) 27 | } 28 | } 29 | val commonTest by getting { 30 | dependencies { 31 | implementation(libs.kotlinx.coroutines.test) 32 | implementation(libs.cashapp.turbine) 33 | } 34 | } 35 | all { 36 | languageSettings.apply { 37 | optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") 38 | optIn("kotlinx.coroutines.FlowPreview") 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /coroutines/src/commonMain/kotlin/com/tunjid/mutator/coroutines/ActionStateFlowMutator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.coroutines 18 | 19 | import com.tunjid.mutator.ActionStateMutator 20 | import com.tunjid.mutator.Mutation 21 | import com.tunjid.mutator.StateHolder 22 | import kotlinx.coroutines.CoroutineScope 23 | import kotlinx.coroutines.channels.Channel 24 | import kotlinx.coroutines.flow.Flow 25 | import kotlinx.coroutines.flow.MutableStateFlow 26 | import kotlinx.coroutines.flow.SharingStarted 27 | import kotlinx.coroutines.flow.StateFlow 28 | import kotlinx.coroutines.flow.emptyFlow 29 | import kotlinx.coroutines.flow.receiveAsFlow 30 | import kotlinx.coroutines.flow.stateIn 31 | import kotlinx.coroutines.launch 32 | 33 | typealias SuspendingStateHolder = StateHolder State> 34 | 35 | /** 36 | * Defines a [ActionStateMutator] to convert a [Flow] of [Action] into a [StateFlow] of [State]. 37 | * 38 | * [this@actionStateFlowMutator]: The [CoroutineScope] for the resulting [StateFlow]. Any [Action]s sent if there are no 39 | * subscribers to the output [StateFlow] will suspend until there is as least one subscriber. 40 | * 41 | * [initialState]: The seed state for the resulting [StateFlow]. 42 | * 43 | * [started]: Semantics for the "hotness" of the output [StateFlow] @see [Flow.stateIn] 44 | * 45 | * [stateTransform]: Further transformations o be applied to the output [StateFlow] 46 | * 47 | * [actionTransform]: Defines the transformations to the [Action] [Flow] to create [Mutation]s 48 | * of state that will be reduced into the [initialState]. This is often achieved through the 49 | * [toMutationStream] [Flow] extension function. 50 | */ 51 | fun CoroutineScope.actionStateFlowMutator( 52 | initialState: State, 53 | started: SharingStarted = SharingStarted.WhileSubscribed(DEFAULT_STOP_TIMEOUT_MILLIS), 54 | inputs: List>> = emptyList(), 55 | stateTransform: (Flow) -> Flow = { it }, 56 | actionTransform: SuspendingStateHolder.(Flow) -> Flow> = { emptyFlow() } 57 | ): ActionStateMutator> = ActionStateFlowMutator( 58 | coroutineScope = this, 59 | initialState = initialState, 60 | started = started, 61 | inputs = inputs, 62 | stateTransform = stateTransform, 63 | actionTransform = actionTransform 64 | ) 65 | 66 | private class ActionStateFlowMutator( 67 | coroutineScope: CoroutineScope, 68 | initialState: State, 69 | started: SharingStarted, 70 | inputs: List>>, 71 | stateTransform: (Flow) -> Flow = { it }, 72 | actionTransform: SuspendingStateHolder.(Flow) -> Flow> 73 | ) : ActionStateMutator>, 74 | suspend () -> State { 75 | 76 | private val actions = Channel() 77 | 78 | // Allows for reading the current state in concurrent contexts. 79 | // Note that it suspends to prevent reading state before this class is fully constructed 80 | private val stateReader = object : SuspendingStateHolder { 81 | override val state: suspend () -> State = this@ActionStateFlowMutator 82 | } 83 | 84 | override val state: StateFlow = 85 | coroutineScope.mutateState( 86 | initialState = initialState, 87 | started = started, 88 | stateTransform = stateTransform, 89 | inputs = inputs + actionTransform(stateReader, actions.receiveAsFlow()) 90 | ) 91 | 92 | override val accept: (Action) -> Unit = { action -> 93 | coroutineScope.launch { 94 | actions.send(action) 95 | } 96 | } 97 | 98 | // There's no need to suspend when reading the value, however the caller must suspend to 99 | // invoke it 100 | override suspend fun invoke(): State = state.value 101 | } 102 | 103 | /** 104 | * Represents a type as a StateFlowMutator of itself with no op [Action]s. 105 | * 106 | * This is typically useful for testing or previews 107 | */ 108 | fun State.asNoOpStateFlowMutator(): ActionStateMutator> = 109 | object : ActionStateMutator> { 110 | override val accept: (Action) -> Unit = {} 111 | override val state: StateFlow = MutableStateFlow(this@asNoOpStateFlowMutator) 112 | } 113 | 114 | internal const val DEFAULT_STOP_TIMEOUT_MILLIS = 5000L 115 | -------------------------------------------------------------------------------- /coroutines/src/commonMain/kotlin/com/tunjid/mutator/coroutines/FlowMutationExt.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.coroutines 18 | 19 | import com.tunjid.mutator.Mutation 20 | import com.tunjid.mutator.mutationOf 21 | import kotlinx.coroutines.flow.Flow 22 | import kotlinx.coroutines.flow.FlowCollector 23 | import kotlinx.coroutines.flow.flatMapConcat 24 | import kotlinx.coroutines.flow.flatMapLatest 25 | import kotlinx.coroutines.flow.flow 26 | import kotlinx.coroutines.flow.map 27 | import kotlinx.coroutines.flow.mapLatest 28 | 29 | /** 30 | * Maps each emission of [T] into a single mutation of [State] 31 | * @see [Flow.map] 32 | */ 33 | inline fun Flow.mapToMutation( 34 | crossinline mapper: State.(T) -> State 35 | ): Flow> = 36 | map { mutationOf { mapper(it) } } 37 | 38 | /** 39 | * Maps the latest emission of [T] into a single mutation of [State] 40 | * @see [Flow.mapLatest] 41 | */ 42 | inline fun Flow.mapLatestToMutation( 43 | crossinline mapper: State.(T) -> State 44 | ): Flow> = 45 | mapLatest { mutationOf { mapper(it) } } 46 | 47 | /** 48 | * Maps each emission of [T] into multiple mutations of [State] 49 | * @see [Flow.flatMapConcat] 50 | */ 51 | inline fun Flow.mapToManyMutations( 52 | crossinline block: suspend FlowCollector>.(T) -> Unit 53 | ): Flow> = 54 | flatMapConcat { flow { block(it) } } 55 | 56 | /** 57 | * Maps the latest emission of [T] into multiple mutations of [State] 58 | * @see [Flow.flatMapLatest] 59 | */ 60 | inline fun Flow.mapLatestToManyMutations( 61 | crossinline block: suspend FlowCollector>.(T) -> Unit 62 | ): Flow> = 63 | flatMapLatest { flow { block(it) } } 64 | -------------------------------------------------------------------------------- /coroutines/src/commonMain/kotlin/com/tunjid/mutator/coroutines/StateFlowMutator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.coroutines 18 | 19 | import com.tunjid.mutator.Mutation 20 | import com.tunjid.mutator.StateMutator 21 | import kotlinx.coroutines.CoroutineScope 22 | import kotlinx.coroutines.Job 23 | import kotlinx.coroutines.channels.Channel 24 | import kotlinx.coroutines.flow.Flow 25 | import kotlinx.coroutines.flow.FlowCollector 26 | import kotlinx.coroutines.flow.SharingStarted 27 | import kotlinx.coroutines.flow.StateFlow 28 | import kotlinx.coroutines.flow.receiveAsFlow 29 | import kotlinx.coroutines.launch 30 | 31 | fun CoroutineScope.stateFlowMutator( 32 | initialState: State, 33 | started: SharingStarted = SharingStarted.WhileSubscribed(), 34 | inputs: List>> 35 | ) = StateFlowMutator( 36 | scope = this, 37 | initialState = initialState, 38 | started = started, 39 | inputs = inputs 40 | ) 41 | 42 | /** 43 | * Manges state production for [State] 44 | */ 45 | class StateFlowMutator internal constructor( 46 | private val scope: CoroutineScope, 47 | initialState: State, 48 | started: SharingStarted = SharingStarted.WhileSubscribed(DEFAULT_STOP_TIMEOUT_MILLIS), 49 | inputs: List>> = emptyList(), 50 | stateTransform: (Flow) -> Flow = { it }, 51 | ) : StateMutator> { 52 | private val mutationChannel = Channel>() 53 | private val mutationSender = FlowCollector(mutationChannel::send) 54 | 55 | override val state = scope.mutateState( 56 | initialState = initialState, 57 | started = started, 58 | stateTransform = stateTransform, 59 | inputs = inputs + mutationChannel.receiveAsFlow() 60 | ) 61 | 62 | /** 63 | * Runs [block] in parallel with any other tasks submitted to [launch]. [block] is only ever run if there is an 64 | * active collector of [state], and is managed under the [SharingStarted] policy passed to this [StateMutator]. 65 | * 66 | * If there are no observers of [state] at the invocation of [launch], the Coroutine launched will suspend till 67 | * a collector begins to collect from [state]. 68 | */ 69 | fun launch( 70 | block: suspend FlowCollector>.() -> Unit 71 | ): Job = scope.launch { 72 | block(mutationSender) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /coroutines/src/commonMain/kotlin/com/tunjid/mutator/coroutines/StateMutation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.coroutines 18 | 19 | import com.tunjid.mutator.Mutation 20 | import kotlinx.coroutines.CoroutineScope 21 | import kotlinx.coroutines.flow.Flow 22 | import kotlinx.coroutines.flow.SharingStarted 23 | import kotlinx.coroutines.flow.StateFlow 24 | import kotlinx.coroutines.flow.emitAll 25 | import kotlinx.coroutines.flow.flow 26 | import kotlinx.coroutines.flow.merge 27 | import kotlinx.coroutines.flow.onEach 28 | import kotlinx.coroutines.flow.scan 29 | import kotlinx.coroutines.flow.stateIn 30 | 31 | /** 32 | * Produces a [StateFlow] by merging [inputs] and reducing them into an 33 | * [initialState] state within [this] [CoroutineScope] 34 | */ 35 | fun CoroutineScope.mutateState( 36 | initialState: State, 37 | started: SharingStarted, 38 | inputs: List>>, 39 | stateTransform: (Flow) -> Flow = { it } 40 | ): StateFlow { 41 | // Set the seed for the state 42 | var seed = initialState 43 | 44 | // Use the flow factory function to capture the seed variable 45 | return stateTransform( 46 | flow { 47 | emitAll( 48 | merge(*inputs.toTypedArray()) 49 | // Reduce into the seed so if resubscribed, the last value of state is persisted 50 | // when the flow pipeline is started again 51 | .reduceInto(seed) 52 | // Set seed after each emission 53 | .onEach { seed = it } 54 | ) 55 | } 56 | ) 57 | .stateIn( 58 | scope = this, 59 | started = started, 60 | initialValue = seed 61 | ) 62 | } 63 | 64 | fun Flow>.reduceInto(initialState: State): Flow = 65 | scan(initialState) { state, mutation -> mutation(state) } 66 | -------------------------------------------------------------------------------- /coroutines/src/commonTest/kotlin/com/tunjid/mutator/coroutines/ActionStateMutatorKtTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.coroutines 18 | 19 | import app.cash.turbine.test 20 | import com.tunjid.mutator.ActionStateMutator 21 | import kotlinx.coroutines.CoroutineScope 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.SupervisorJob 24 | import kotlinx.coroutines.cancel 25 | import kotlinx.coroutines.delay 26 | import kotlinx.coroutines.flow.SharingStarted 27 | import kotlinx.coroutines.flow.StateFlow 28 | import kotlinx.coroutines.flow.map 29 | import kotlinx.coroutines.flow.onEach 30 | import kotlinx.coroutines.test.StandardTestDispatcher 31 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 32 | import kotlinx.coroutines.test.resetMain 33 | import kotlinx.coroutines.test.runTest 34 | import kotlinx.coroutines.test.setMain 35 | import kotlin.test.AfterTest 36 | import kotlin.test.BeforeTest 37 | import kotlin.test.Test 38 | import kotlin.test.assertEquals 39 | 40 | class ActionStateMutatorKtTest { 41 | 42 | private val testDispatcher = UnconfinedTestDispatcher() 43 | 44 | @BeforeTest 45 | fun setUp() { 46 | Dispatchers.setMain(testDispatcher) 47 | } 48 | 49 | @AfterTest 50 | fun tearDown() { 51 | Dispatchers.resetMain() 52 | } 53 | 54 | @Test 55 | fun actionStateFlowMutatorRemembersLastValue() = runTest { 56 | val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) 57 | 58 | val mutator = scope.actionStateFlowMutator( 59 | initialState = State(), 60 | started = SharingStarted.WhileSubscribed(), 61 | actionTransform = { actions -> 62 | actions.toMutationStream { 63 | when (val action = type()) { 64 | is IntAction.Add -> action.flow 65 | .map { it.mutation } 66 | 67 | is IntAction.Subtract -> action.flow 68 | .map { it.mutation } 69 | } 70 | } 71 | } 72 | ) 73 | mutator.state.test { 74 | // Read first emission, should be the initial value 75 | assertEquals(State(), awaitItem()) 76 | 77 | // Add 2, then wait till its processed 78 | mutator.accept(IntAction.Add(value = 2)) 79 | assertEquals(expected = State(count = 2.0), actual = awaitItem()) 80 | 81 | cancelAndIgnoreRemainingEvents() 82 | } 83 | 84 | // At this point, there are no subscribers and the upstream is cancelled 85 | assertEquals( 86 | expected = State(count = 2.0), 87 | actual = mutator.state.value 88 | ) 89 | 90 | // Subscribe again 91 | mutator.state.test { 92 | // Read first emission, should be the last value seen 93 | assertEquals(expected = State(2.0), actual = awaitItem()) 94 | 95 | // Subtract 5, then wait till its processed 96 | mutator.accept(IntAction.Subtract(value = 5)) 97 | assertEquals(State(count = -3.0), awaitItem()) 98 | 99 | cancelAndIgnoreRemainingEvents() 100 | } 101 | 102 | scope.cancel() 103 | } 104 | 105 | @Test 106 | fun actionStateFlowMutatorSuspendsWithNoSubscribers() = runTest { 107 | val dispatcher = StandardTestDispatcher() 108 | val scope = CoroutineScope(SupervisorJob() + dispatcher) 109 | 110 | val mutator = scope.actionStateFlowMutator( 111 | initialState = State(), 112 | started = SharingStarted.WhileSubscribed(), 113 | actionTransform = { actions -> 114 | actions 115 | .onEach { delay(1000) } 116 | .toMutationStream { 117 | when (val action = type()) { 118 | is IntAction.Add -> action.flow 119 | .map { it.mutation } 120 | 121 | is IntAction.Subtract -> action.flow 122 | .map { it.mutation } 123 | } 124 | } 125 | } 126 | ) 127 | 128 | mutator.accept(IntAction.Add(value = 1)) 129 | mutator.accept(IntAction.Add(value = 1)) 130 | mutator.accept(IntAction.Add(value = 1)) 131 | mutator.accept(IntAction.Add(value = 1)) 132 | 133 | mutator.state.test { 134 | // Read first emission, should be the initial value 135 | assertEquals(State(), awaitItem()) 136 | 137 | // Queue should be drained 138 | dispatcher.scheduler.advanceTimeBy(1000) 139 | assertEquals(State(1.0), awaitItem()) 140 | 141 | dispatcher.scheduler.advanceTimeBy(1000) 142 | assertEquals(State(2.0), awaitItem()) 143 | 144 | dispatcher.scheduler.advanceTimeBy(1000) 145 | assertEquals(State(3.0), awaitItem()) 146 | 147 | dispatcher.scheduler.advanceTimeBy(1000) 148 | assertEquals(State(4.0), awaitItem()) 149 | 150 | cancelAndIgnoreRemainingEvents() 151 | } 152 | 153 | scope.cancel() 154 | } 155 | 156 | @Test 157 | fun noOpOperatorCompiles() { 158 | val noOpActionStateMutator: ActionStateMutator> = State().asNoOpStateFlowMutator() 159 | noOpActionStateMutator.accept(IntAction.Add(value = 1)) 160 | 161 | assertEquals( 162 | expected = State(), 163 | actual = noOpActionStateMutator.state.value 164 | ) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /coroutines/src/commonTest/kotlin/com/tunjid/mutator/coroutines/State.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.coroutines 18 | 19 | import com.tunjid.mutator.Mutation 20 | import com.tunjid.mutator.mutationOf 21 | 22 | data class State( 23 | val count: Double = 0.0 24 | ) 25 | 26 | sealed class Action 27 | 28 | sealed class IntAction : Action() { 29 | abstract val value: Int 30 | 31 | data class Add(override val value: Int) : IntAction() 32 | data class Subtract(override val value: Int) : IntAction() 33 | } 34 | 35 | sealed class DoubleAction : Action() { 36 | abstract val value: Double 37 | 38 | data class Divide(override val value: Double) : DoubleAction() 39 | data class Multiply(override val value: Double) : DoubleAction() 40 | } 41 | 42 | val Action.mutation: Mutation 43 | get() = when (this) { 44 | is IntAction.Add -> mutationOf { copy(count = count + value) } 45 | is IntAction.Subtract -> mutationOf { copy(count = count - value) } 46 | is DoubleAction.Divide -> mutationOf { copy(count = count / value) } 47 | is DoubleAction.Multiply -> mutationOf { copy(count = count * value) } 48 | } 49 | -------------------------------------------------------------------------------- /coroutines/src/commonTest/kotlin/com/tunjid/mutator/coroutines/StateProductionKtTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.coroutines 18 | 19 | import app.cash.turbine.test 20 | import com.tunjid.mutator.Mutation 21 | import com.tunjid.mutator.mutationOf 22 | import com.tunjid.mutator.plus 23 | import kotlinx.coroutines.CoroutineScope 24 | import kotlinx.coroutines.Dispatchers 25 | import kotlinx.coroutines.delay 26 | import kotlinx.coroutines.flow.MutableSharedFlow 27 | import kotlinx.coroutines.flow.SharingStarted 28 | import kotlinx.coroutines.flow.flow 29 | import kotlinx.coroutines.test.TestScope 30 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 31 | import kotlinx.coroutines.test.advanceTimeBy 32 | import kotlinx.coroutines.test.resetMain 33 | import kotlinx.coroutines.test.runTest 34 | import kotlinx.coroutines.test.setMain 35 | import kotlin.test.AfterTest 36 | import kotlin.test.BeforeTest 37 | import kotlin.test.Test 38 | import kotlin.test.assertEquals 39 | 40 | class StateProductionKtTest { 41 | 42 | private val testDispatcher = UnconfinedTestDispatcher() 43 | private lateinit var scope: CoroutineScope 44 | private lateinit var eventMutations: MutableSharedFlow> 45 | 46 | @BeforeTest 47 | fun setUp() { 48 | Dispatchers.setMain(testDispatcher) 49 | scope = TestScope(testDispatcher) 50 | eventMutations = MutableSharedFlow() 51 | } 52 | 53 | @AfterTest 54 | fun tearDown() { 55 | Dispatchers.resetMain() 56 | } 57 | 58 | @Test 59 | fun test_simple_state_production() = runTest { 60 | val state = scope.mutateState( 61 | initialState = State(), 62 | started = SharingStarted.WhileSubscribed(), 63 | inputs = listOf( 64 | eventMutations 65 | ) 66 | ) 67 | 68 | state.test { 69 | assertEquals(State(0.0), awaitItem()) 70 | eventMutations.emit { copy(count = count + 1) } 71 | assertEquals(State(1.0), awaitItem()) 72 | 73 | cancelAndIgnoreRemainingEvents() 74 | } 75 | } 76 | 77 | @Test 78 | fun test_state_production_persists_after_unsubscribing() = runTest { 79 | val state = scope.mutateState( 80 | initialState = State(), 81 | started = SharingStarted.WhileSubscribed(), 82 | inputs = listOf( 83 | eventMutations 84 | ) 85 | ) 86 | 87 | // Subscribe the first time 88 | state.test { 89 | assertEquals(State(0.0), awaitItem()) 90 | eventMutations.emit { copy(count = count + 1) } 91 | assertEquals(State(1.0), awaitItem()) 92 | 93 | cancelAndIgnoreRemainingEvents() 94 | } 95 | 96 | // Subscribe again. The state flow count should not be reset by the pipeline restarting 97 | state.test { 98 | assertEquals(State(1.0), awaitItem()) 99 | cancelAndIgnoreRemainingEvents() 100 | } 101 | } 102 | 103 | @Test 104 | fun test_state_production_with_merged_flows() = runTest { 105 | val state = scope.mutateState( 106 | initialState = State(), 107 | started = SharingStarted.WhileSubscribed(), 108 | inputs = listOf( 109 | eventMutations, 110 | flow { 111 | delay(1000) 112 | emit(mutationOf { copy(count = 3.0) }) 113 | 114 | delay(1000) 115 | emit(mutationOf { copy(count = 7.0) }) 116 | } 117 | ) 118 | ) 119 | 120 | state.test { 121 | assertEquals(State(0.0), awaitItem()) 122 | 123 | advanceTimeBy(1200) 124 | assertEquals(State(3.0), awaitItem()) 125 | 126 | advanceTimeBy(1200) 127 | assertEquals(State(7.0), awaitItem()) 128 | 129 | eventMutations.emit { copy(count = 0.0) } 130 | assertEquals(State(0.0), awaitItem()) 131 | 132 | cancelAndIgnoreRemainingEvents() 133 | } 134 | } 135 | 136 | @Test 137 | fun test_state_change_addition() { 138 | val additionMutation = mutationOf { 139 | copy(count = count + 1) 140 | } + 141 | mutationOf { 142 | copy(count = count + 1) 143 | } + 144 | mutationOf { 145 | copy(count = count + 1) 146 | } 147 | 148 | val state = additionMutation(State()) 149 | 150 | assertEquals(State(3.0), state) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /demo/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | kotlin("multiplatform") 19 | id("org.jetbrains.compose") 20 | id("kotlin-jvm-convention") 21 | } 22 | 23 | kotlin { 24 | jvm("desktop") 25 | js(IR) { 26 | browser() 27 | binaries.executable() 28 | } 29 | sourceSets { 30 | val commonMain by getting { 31 | dependencies { 32 | implementation(project(":core")) 33 | implementation(project(":coroutines")) 34 | 35 | implementation(libs.jetbrains.compose.runtime) 36 | 37 | implementation(libs.kotlinx.coroutines.core) 38 | } 39 | } 40 | val commonTest by getting { 41 | dependencies { 42 | implementation(libs.kotlinx.coroutines.test) 43 | implementation(libs.cashapp.turbine) 44 | } 45 | } 46 | val desktopMain by getting { 47 | dependencies { 48 | implementation(compose.desktop.currentOs) 49 | 50 | implementation(libs.jetbrains.compose.animation) 51 | implementation(libs.jetbrains.compose.material) 52 | implementation(libs.jetbrains.compose.foundation.layout) 53 | 54 | implementation(libs.richtext.commonmark) 55 | } 56 | } 57 | val jsMain by getting { 58 | dependencies { 59 | implementation(compose.html.core) 60 | 61 | implementation(libs.kotlin.wrappers.react) 62 | implementation(libs.kotlin.wrappers.reactDom) 63 | 64 | implementation(npm("react", "18.2.0")) 65 | implementation(npm("react-dom", "18.2.0")) 66 | implementation(npm("highlight.js", "10.7.2")) 67 | implementation(npm("react-markdown", "6.0.3")) 68 | 69 | } 70 | } 71 | all { 72 | languageSettings.apply { 73 | optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") 74 | optIn("kotlinx.coroutines.FlowPreview") 75 | } 76 | } 77 | } 78 | 79 | } 80 | 81 | compose.desktop { 82 | application { 83 | mainClass = "Main_desktopKt" 84 | 85 | nativeDistributions { 86 | targetFormats( 87 | org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg, 88 | org.jetbrains.compose.desktop.application.dsl.TargetFormat.Msi, 89 | org.jetbrains.compose.desktop.application.dsl.TargetFormat.Deb 90 | ) 91 | packageName = "Falling Balls" 92 | packageVersion = "1.0.0" 93 | 94 | windows { 95 | menuGroup = "Compose Examples" 96 | // see https://wixtoolset.org/documentation/manual/v3/howtos/general/generate_guids.html 97 | upgradeUuid = "18159995-d967-4CD2-8885-77BFA97CFA9F" 98 | } 99 | } 100 | } 101 | } 102 | 103 | compose.experimental { 104 | web.application {} 105 | } 106 | 107 | tasks.withType { 108 | kotlinOptions.jvmTarget = "11" 109 | } 110 | 111 | kotlin { 112 | targets.withType { 113 | binaries.all { 114 | // TODO: the current compose binary surprises LLVM, so disable checks for now. 115 | freeCompilerArgs += "-Xdisable-phases=VerifyBitcode" 116 | } 117 | } 118 | } 119 | 120 | // a temporary workaround for a bug in jsRun invocation - see https://youtrack.jetbrains.com/issue/KT-48273 121 | afterEvaluate { 122 | rootProject.extensions.configure { 123 | versions.webpackDevServer.version = "4.0.0" 124 | versions.webpackCli.version = "4.10.0" 125 | nodeVersion = "16.0.0" 126 | } 127 | } 128 | 129 | 130 | // TODO: remove when https://youtrack.jetbrains.com/issue/KT-50778 fixed 131 | project.tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile::class.java).configureEach { 132 | kotlinOptions.freeCompilerArgs += listOf( 133 | "-Xir-dce-runtime-diagnostic=log" 134 | ) 135 | } 136 | -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/App.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo 18 | 19 | import androidx.compose.runtime.Composable 20 | import com.tunjid.mutator.demo.editor.ContainerLayout 21 | import com.tunjid.mutator.demo.sections.Section1 22 | import com.tunjid.mutator.demo.sections.Section2 23 | import com.tunjid.mutator.demo.sections.Section3 24 | import com.tunjid.mutator.demo.sections.Section4 25 | import com.tunjid.mutator.demo.sections.Section5 26 | import com.tunjid.mutator.demo.sections.Section6 27 | import com.tunjid.mutator.demo.sections.Section7 28 | 29 | @Composable 30 | fun App( 31 | ) { 32 | ContainerLayout { 33 | Section1() 34 | Section2() 35 | Section3() 36 | Section4() 37 | Section5() 38 | Section6() 39 | Section7() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/Colors.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo 18 | 19 | import kotlin.math.pow 20 | import kotlin.math.roundToInt 21 | 22 | expect class Color(color: Long) { 23 | val argb: Int 24 | val r: Int 25 | val g: Int 26 | val b: Int 27 | 28 | fun isBright(): Boolean 29 | 30 | companion object { 31 | val Black: Color 32 | val LightGray: Color 33 | } 34 | } 35 | 36 | 37 | object MutedColors { 38 | private val mutedColors = listOf( 39 | Color(0xFF2980b9), // Belize Hole 40 | Color(0xFF2c3e50), // Midnight Blue 41 | Color(0xFFc0392b), // Pomegranate 42 | Color(0xFF16a085), // Green Sea 43 | Color(0xFF7f8c8d), // Concrete 44 | Color(0xFFFFFF), 45 | ) 46 | 47 | private val darkerMutedColors = listOf( 48 | Color(0xFF304233), 49 | Color(0xFF353b45), 50 | Color(0xFF392e3a), 51 | Color(0xFF3e2a2a), 52 | Color(0xFF474747), 53 | Color(0x292929) 54 | ) 55 | 56 | fun colors(isDark: Boolean) = if (isDark) darkerMutedColors else mutedColors 57 | } 58 | 59 | fun interpolateColors(fraction: Float, startValue: Int, endValue: Int): Int { 60 | val startA = (startValue shr 24 and 0xff) / 255.0f 61 | var startR = (startValue shr 16 and 0xff) / 255.0f 62 | var startG = (startValue shr 8 and 0xff) / 255.0f 63 | var startB = (startValue and 0xff) / 255.0f 64 | val endA = (endValue shr 24 and 0xff) / 255.0f 65 | var endR = (endValue shr 16 and 0xff) / 255.0f 66 | var endG = (endValue shr 8 and 0xff) / 255.0f 67 | var endB = (endValue and 0xff) / 255.0f 68 | 69 | // convert from sRGB to linear 70 | startR = startR.toDouble().pow(2.2).toFloat() 71 | startG = startG.toDouble().pow(2.2).toFloat() 72 | startB = startB.toDouble().pow(2.2).toFloat() 73 | endR = endR.toDouble().pow(2.2).toFloat() 74 | endG = endG.toDouble().pow(2.2).toFloat() 75 | endB = endB.toDouble().pow(2.2).toFloat() 76 | 77 | // compute the interpolated color in linear space 78 | var a = startA + fraction * (endA - startA) 79 | var r = startR + fraction * (endR - startR) 80 | var g = startG + fraction * (endG - startG) 81 | var b = startB + fraction * (endB - startB) 82 | 83 | // convert back to sRGB in the [0..255] range 84 | a *= 255.0f 85 | r = r.toDouble().pow(1.0 / 2.2).toFloat() * 255.0f 86 | g = g.toDouble().pow(1.0 / 2.2).toFloat() * 255.0f 87 | b = b.toDouble().pow(1.0 / 2.2).toFloat() * 255.0f 88 | 89 | return a.roundToInt() shl 24 or (r.roundToInt() shl 16) or (g.roundToInt() shl 8) or b.roundToInt() 90 | } -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/Utilities.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo 18 | 19 | import kotlinx.coroutines.CoroutineScope 20 | import kotlinx.coroutines.delay 21 | import kotlinx.coroutines.flow.Flow 22 | import kotlinx.coroutines.flow.SharingStarted 23 | import kotlinx.coroutines.flow.asFlow 24 | import kotlinx.coroutines.flow.flatMapLatest 25 | import kotlinx.coroutines.flow.flow 26 | import kotlinx.coroutines.flow.map 27 | import kotlinx.coroutines.flow.onEach 28 | import kotlinx.coroutines.flow.scan 29 | import kotlinx.coroutines.flow.shareIn 30 | 31 | const val SPEED = 1600L 32 | 33 | enum class Speed(val multiplier: Int) { 34 | One(1), Two(2), Three(3), Four(4) 35 | } 36 | 37 | val Speed.text get() = when(this) { 38 | Speed.One -> "1x" 39 | Speed.Two -> "2x" 40 | Speed.Three -> "3x" 41 | Speed.Four -> "4x" 42 | } 43 | 44 | fun intervalFlow(intervalMillis: Long) = flow { 45 | var value = 0L 46 | while (true) { 47 | emit(++value) 48 | delay(intervalMillis) 49 | } 50 | } 51 | 52 | fun CoroutineScope.speedFlow() = 53 | intervalFlow(4000) 54 | .map { Speed.values().random() } 55 | .shareIn(this, SharingStarted.WhileSubscribed()) 56 | 57 | fun Flow.toInterval(): Flow = 58 | flatMapLatest { 59 | intervalFlow(SPEED / it.multiplier) 60 | } 61 | 62 | fun Flow.toProgress(): Flow = 63 | scan(0f) { progress, _ -> (progress + 1) % 100 } 64 | 65 | data class ColorInterpolationProgress( 66 | val progress: Float, 67 | val colors: List 68 | ) 69 | 70 | fun interpolateColors( 71 | startColors: IntArray, 72 | endColors: IntArray 73 | ): Flow = (0..100).asFlow() 74 | .onEach { delay(50) } 75 | .map { percentage -> 76 | val fraction = percentage.toFloat() / 100F 77 | val colors = startColors 78 | .zip(endColors) 79 | .map { (start, end) -> 80 | interpolateColors( 81 | fraction = fraction, 82 | startValue = start, 83 | endValue = end 84 | ) 85 | } 86 | .map(Int::toLong) 87 | .map(::Color) 88 | 89 | ColorInterpolationProgress( 90 | progress = fraction, 91 | colors = colors, 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/editor/Layouts.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.editor 18 | 19 | import androidx.compose.runtime.Composable 20 | 21 | @Composable 22 | expect fun ContainerLayout(content: @Composable () -> Unit) 23 | 24 | @Composable 25 | expect fun SectionLayout(content: @Composable () -> Unit) 26 | 27 | @Composable 28 | expect fun VerticalLayout(content: @Composable () -> Unit) 29 | 30 | @Composable 31 | expect fun HorizontalLayout( 32 | centerOnMainAxis: Boolean = false, 33 | content: @Composable () -> Unit 34 | ) 35 | -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/editor/Text.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.editor 18 | 19 | import androidx.compose.runtime.Composable 20 | 21 | @Composable 22 | expect fun Paragraph(text: String) 23 | 24 | @Composable 25 | expect fun Markdown(content: String) 26 | 27 | @Composable 28 | expect fun CallToAction( 29 | text: String, 30 | centerText: Boolean = false, 31 | ) 32 | 33 | @Composable 34 | expect fun CodeBlock(content: String) -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/sections/2_User_Actions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.sections 18 | 19 | import androidx.compose.runtime.Composable 20 | import com.tunjid.mutator.demo.editor.CallToAction 21 | import com.tunjid.mutator.demo.editor.CodeBlock 22 | import com.tunjid.mutator.demo.editor.Markdown 23 | import com.tunjid.mutator.demo.editor.SectionLayout 24 | import com.tunjid.mutator.demo.snails.Snail4 25 | 26 | @Composable 27 | fun Section2() { 28 | SectionLayout { 29 | Markdown(oneMarkdown) 30 | CodeBlock(twoCode) 31 | Snail4() 32 | CallToAction(snail3Cta) 33 | Markdown(threeMarkdown) 34 | } 35 | } 36 | 37 | private val oneMarkdown = """ 38 | # User actions as `Flows` 39 | 40 | These sources of change mentioned above can be from anything that can be modeled as a flow, be it reading data from a database or user actions. Imagine if we want to be able to change the snail's color on a whim. We can add a [`MutableSharedFlow`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-mutable-shared-flow/) for the color of the snail to our state holder class, and then `combine` it with the rest of the flows to produce the state. 41 | """.trimIndent() 42 | 43 | private val twoCode = """ 44 | data class Snail4State( 45 | val progress: Float = 0f, 46 | val speed: Speed = Speed.One, 47 | val color: Color = Color.Blue, 48 | val colors: List = listOf() 49 | ) 50 | 51 | class Snail4StateHolder( 52 | scope: CoroutineScope 53 | ) { 54 | 55 | private val speed: Flow = … 56 | 57 | private val progress: Flow = … 58 | 59 | private val color: MutableStateFlow = MutableStateFlow(Color.Cyan) 60 | 61 | val state: StateFlow = combine( 62 | progress, 63 | speed, 64 | color, 65 | ::Snail4State 66 | ) 67 | .stateIn(...) 68 | 69 | fun setSnailColor(color: Color) { 70 | this.color.value = color 71 | } 72 | } 73 | """.trimIndent() 74 | 75 | private val snail3Cta = """ 76 | Tap on a color to change the snail's color. 77 | """.trimIndent() 78 | 79 | private val threeMarkdown = """ 80 | When the user wants to change the color, the `setColor` method is called, the `color` `MutableStateFlow` has its value updated, and it is emitted into the stream produced by the `combine` function to produce the new state. 81 | """.trimIndent() -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/sections/3_Combining.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.sections 18 | 19 | import androidx.compose.runtime.Composable 20 | import com.tunjid.mutator.demo.editor.CodeBlock 21 | import com.tunjid.mutator.demo.editor.Markdown 22 | import com.tunjid.mutator.demo.editor.SectionLayout 23 | 24 | @Composable 25 | fun Section3() { 26 | SectionLayout { 27 | Markdown(oneMarkdown) 28 | CodeBlock(twoCode) 29 | Markdown(threeMarkdown) 30 | } 31 | } 32 | 33 | private val oneMarkdown = """ 34 | # Combining changes in state 35 | 36 | Combining sources of state as a general means of state production is rather robust, lends itself to a wide range of cases, and works well for simple to moderate state production pipelines. It also scales linearly, that is each source of state change will need to be added to the `combine` function. This poses a problem for states with more than 5 sources of change as the `combine` function allows for at most 5 flows. This is called the [arity](https://en.wikipedia.org/wiki/Arity) of the `combine` function. 37 | 38 | One way around this is to combine the sources of state change into intermediate states, before combining them again into the final state. 39 | """.trimIndent() 40 | 41 | private val twoCode = """ 42 | data class LargeState( 43 | val property1: Int, 44 | val property2: Int, 45 | val property3: Int, 46 | val property4: Int, 47 | val property5: Int, 48 | val property6: Int, 49 | val property7: Int, 50 | val property8: Int, 51 | ) 52 | 53 | data class IntermediateState1( 54 | val property1: Int, 55 | val property2: Int, 56 | val property3: Int, 57 | val property4: Int, 58 | ) 59 | 60 | data class IntermediateState2( 61 | val property5: Int, 62 | val property6: Int, 63 | val property7: Int, 64 | val property8: Int, 65 | ) 66 | 67 | fun intFlow() = flowOf(1) 68 | 69 | class LargeStateHolder { 70 | private val intermediateState1 = combine( 71 | intFlow(), 72 | intFlow(), 73 | intFlow(), 74 | intFlow(), 75 | ::IntermediateState1 76 | ) 77 | 78 | private val intermediateState2 = combine( 79 | intFlow(), 80 | intFlow(), 81 | intFlow(), 82 | intFlow(), 83 | ::IntermediateState2 84 | ) 85 | 86 | val state = combine( 87 | intermediateState1, 88 | intermediateState2 89 | ) { state1, state2 -> 90 | LargeState( 91 | state1.property1, 92 | state1.property2, 93 | state1.property3, 94 | state1.property4, 95 | state2.property5, 96 | state2.property5, 97 | state2.property6, 98 | state2.property7, 99 | ) 100 | } 101 | } 102 | """.trimIndent() 103 | 104 | private val threeMarkdown = """ 105 | The above works, but can be difficult to maintain. As new sources of state are added or removed over time, especially those from user events, the signatures of the `IntermediateState` instances will need to change to accommodate the arity of the `combine` function causing cascading changes. This brings us to another state production approach, merging sources of change. 106 | """.trimIndent() -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/sections/5_Formalizing.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.sections 18 | 19 | import androidx.compose.runtime.Composable 20 | import com.tunjid.mutator.demo.editor.CallToAction 21 | import com.tunjid.mutator.demo.editor.CodeBlock 22 | import com.tunjid.mutator.demo.editor.Markdown 23 | import com.tunjid.mutator.demo.editor.SectionLayout 24 | import com.tunjid.mutator.demo.snails.Snail7 25 | 26 | @Composable 27 | fun Section5() = SectionLayout { 28 | Markdown(oneMarkdown) 29 | CodeBlock(twoCode) 30 | Markdown(threeMarkdown) 31 | CodeBlock(fourCode) 32 | Snail7() 33 | CallToAction(Snail7Cta) 34 | } 35 | 36 | private val oneMarkdown = """ 37 | # Formalizing state production 38 | 39 | The merge approach can be formalized into an extension function on the `CoroutineScope` the state is produced in: 40 | """.trimIndent() 41 | 42 | private val twoCode = """ 43 | fun CoroutineScope.stateFlowMutator( 44 | initialState: State, 45 | started: SharingStarted = SharingStarted.WhileSubscribed(), 46 | inputs: List>> 47 | ) : StateFlowMutator 48 | """.trimIndent() 49 | 50 | private val threeMarkdown = """ 51 | Where the use of it in the snail example becomes: 52 | """.trimIndent() 53 | 54 | private val fourCode = """ 55 | class Snail7StateHolder( 56 | private val scope: CoroutineScope 57 | ) { 58 | 59 | private val speedChanges: Flow> = … 60 | 61 | private val progressChanges: Flow> = … 62 | 63 | private val stateMutator = scope.stateFlowMutator( 64 | initialState = Snail7State(), 65 | started = SharingStarted.WhileSubscribed(), 66 | inputs = listOf( 67 | speedChanges, 68 | progressChanges, 69 | ) 70 | ) 71 | 72 | val state: StateFlow = stateMutator.state 73 | 74 | fun setSnailColor(index: Int) = stateMutator.launch { 75 | emit { copy(color = colors[index]) } 76 | } 77 | 78 | fun setProgress(progress: Float) = stateMutator.launch { 79 | emit { copy(progress = progress) } 80 | } 81 | } 82 | """.trimIndent() 83 | 84 | private val Snail7Cta = """ 85 | Snail7 is identical to Snail6; just with a formalized state production approach. 86 | """.trimIndent() 87 | -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/sections/6_Conflicts.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.sections 18 | 19 | import androidx.compose.runtime.Composable 20 | import com.tunjid.mutator.demo.editor.CallToAction 21 | import com.tunjid.mutator.demo.editor.CodeBlock 22 | import com.tunjid.mutator.demo.editor.Markdown 23 | import com.tunjid.mutator.demo.editor.SectionLayout 24 | import com.tunjid.mutator.demo.snails.Snail8 25 | 26 | @Composable 27 | fun Section6() = SectionLayout { 28 | Markdown(oneMarkdown) 29 | CodeBlock(twoCode) 30 | CallToAction(composeAnimationApiCta) 31 | Snail8() 32 | CallToAction(Snail8Cta) 33 | Markdown(threeMarkdown) 34 | } 35 | 36 | private val oneMarkdown = """ 37 | # Conflicts in state production 38 | 39 | So far, all sources of state change have been relatively harmonious. That is, they don't conflict or compete with each other. Sometimes however, especially with asynchronous data sources, state changes can clash. This most often occurs when user events trigger a set of cascading state changes. 40 | 41 | This is best illustrated with an example. Say we wanted to expose our snail to the experience of day and night. Not only that, we want the experience to animate smoothly. We can do this by adding a new method: 42 | """.trimIndent() 43 | 44 | private val twoCode = """ 45 | class Snail8StateHolder( 46 | private val scope: CoroutineScope 47 | ) { 48 | 49 | private val stateMutator = scope.stateFlowMutator( 50 | ... 51 | ) 52 | ... 53 | 54 | fun setMode(isDark: Boolean) = stateMutator.launch { 55 | emit { copy(isDark = isDark) } 56 | /* Collect from a flow that animates color changes */ 57 | interpolateColors( 58 | startColors = state.value.colors.map(Color::argb).toIntArray(), 59 | endColors = MutedColors.colors(isDark).map(Color::argb).toIntArray() 60 | ).collect { (progress, colors) -> 61 | emit { 62 | copy( 63 | colorInterpolationProgress = progress, 64 | colors = colors 65 | ) 66 | } 67 | } 68 | } 69 | } 70 | """.trimIndent() 71 | 72 | private val composeAnimationApiCta = """ 73 | In Jetpack Compose apps, animating color changes is best done with the [animateColorAsState](https://developer.android.com/reference/kotlin/androidx/compose/animation/package-summary#animateColorAsState(androidx.compose.ui.graphics.Color,androidx.compose.animation.core.AnimationSpec,kotlin.Function1)) APIs instead of manually as shown in the example above. The example is merely used to demonstrate long running operations that cause state changes, like uploading a file with a progress bar. 74 | """.trimIndent() 75 | 76 | private val Snail8Cta = """ 77 | Tap the toggle button to switch between light and dark modes for the snail. Notice that tapping in quick succession will cause the UI to flicker as state changes conflict. 78 | """.trimIndent() 79 | 80 | private val threeMarkdown = """ 81 | In the above, each time setMode is called, we first update the state to the new mode, and then crucially, begin to collect from a finite flow that updates the colors available to choose from. 82 | 83 | The source of conflict here is the `interpolateColors` `Flow`. If `setMode` is called twice in quick succession, there will be two instances of the `interpolateColors` `Flow` running which may cause the UI to flicker. 84 | """.trimIndent() 85 | -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/sections/Template.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.sections 18 | 19 | import androidx.compose.runtime.Composable 20 | import com.tunjid.mutator.demo.editor.Markdown 21 | import com.tunjid.mutator.demo.editor.SectionLayout 22 | 23 | @Composable 24 | fun Section() = SectionLayout { 25 | Markdown(oneMarkdown) 26 | } 27 | 28 | private val oneMarkdown = """ 29 | 30 | """.trimIndent() 31 | 32 | private val twoCode = """ 33 | 34 | """.trimIndent() 35 | 36 | private val threeMarkdown = """ 37 | 38 | """.trimIndent() 39 | 40 | private val fourCode = """ 41 | 42 | """.trimIndent() 43 | 44 | private val fiveMarkdown = """ 45 | 46 | """.trimIndent() 47 | 48 | private val sixCode = """ 49 | 50 | """.trimIndent() 51 | -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/snails/Snail.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.runtime.Composable 20 | import com.tunjid.mutator.demo.Color 21 | import com.tunjid.mutator.demo.MutedColors 22 | import com.tunjid.mutator.demo.Speed 23 | 24 | 25 | @Composable 26 | expect fun Illustration( 27 | content: @Composable () -> Unit 28 | ) 29 | 30 | @Composable 31 | expect fun SnailCard( 32 | color: Color = Color(0xFFFFFF), 33 | content: @Composable () -> Unit 34 | ) 35 | 36 | @Composable 37 | expect fun SnailText( 38 | color: Color, 39 | text: String 40 | ) 41 | 42 | @Composable 43 | expect fun Snail( 44 | progress: Float, 45 | speed: Speed = Speed.One, 46 | color: Color = MutedColors.colors(isDark = false).first(), 47 | onValueChange: (Float) -> Unit = {} 48 | ) 49 | 50 | @Composable 51 | expect fun ColorSwatch( 52 | colors: List = listOf(), 53 | onColorClicked: (Int) -> Unit = {} 54 | ) 55 | 56 | @Composable 57 | expect fun ToggleButton( 58 | progress: Float, 59 | onClicked: () -> Unit = {} 60 | ) -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/snails/Snail0.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.collectAsState 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.rememberCoroutineScope 24 | import com.tunjid.mutator.demo.intervalFlow 25 | import com.tunjid.mutator.demo.toProgress 26 | import kotlinx.coroutines.CoroutineScope 27 | import kotlinx.coroutines.flow.SharingStarted 28 | import kotlinx.coroutines.flow.StateFlow 29 | import kotlinx.coroutines.flow.stateIn 30 | 31 | class Snail0StateHolder( 32 | scope: CoroutineScope 33 | ) { 34 | val progress: StateFlow = intervalFlow(500) 35 | .toProgress() 36 | .stateIn( 37 | scope = scope, 38 | started = SharingStarted.WhileSubscribed(), 39 | initialValue = 0f 40 | ) 41 | } 42 | 43 | @Composable 44 | fun Snail0() { 45 | val scope = rememberCoroutineScope() 46 | val stateHolder = remember { Snail0StateHolder(scope) } 47 | val state by stateHolder.progress.collectAsState() 48 | 49 | Snail( 50 | progress = state, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/snails/Snail1.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.LaunchedEffect 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.rememberCoroutineScope 25 | import com.tunjid.mutator.demo.Color 26 | import com.tunjid.mutator.demo.MutedColors 27 | import com.tunjid.mutator.demo.editor.Paragraph 28 | import com.tunjid.mutator.demo.editor.VerticalLayout 29 | import com.tunjid.mutator.demo.udfvisualizer.Event 30 | import com.tunjid.mutator.demo.udfvisualizer.Marble 31 | import com.tunjid.mutator.demo.udfvisualizer.UDFVisualizer 32 | import com.tunjid.mutator.demo.udfvisualizer.udfVisualizerStateHolder 33 | import kotlinx.coroutines.flow.MutableStateFlow 34 | import kotlinx.coroutines.flow.StateFlow 35 | import kotlinx.coroutines.flow.asStateFlow 36 | import kotlinx.coroutines.flow.update 37 | 38 | data class Snail1State( 39 | val progress: Float = 0f, 40 | val color: Color = MutedColors.colors(false).first(), 41 | val colors: List = MutedColors.colors(false) 42 | ) 43 | 44 | class Snail1StateHolder { 45 | 46 | private val _state = MutableStateFlow(Snail1State()) 47 | val state: StateFlow = _state.asStateFlow() 48 | 49 | fun setSnailColor(index: Int) { 50 | _state.update { 51 | it.copy(color = it.colors[index]) 52 | } 53 | } 54 | 55 | fun setProgress(progress: Float) { 56 | _state.update { 57 | it.copy(progress = progress) 58 | } 59 | } 60 | } 61 | 62 | @Composable 63 | fun Snail1() { 64 | val scope = rememberCoroutineScope() 65 | val stateHolder = remember { Snail1StateHolder() } 66 | val udfStateHolder = remember { udfVisualizerStateHolder(scope) } 67 | val state by stateHolder.state.collectAsState() 68 | 69 | LaunchedEffect(state) { 70 | udfStateHolder.accept( 71 | Event.StateChange( 72 | color = state.color, 73 | metadata = Marble.Metadata.Text(state.progress.toString()) 74 | ) 75 | ) 76 | } 77 | 78 | Illustration { 79 | SnailCard { 80 | VerticalLayout { 81 | Paragraph( 82 | text = "Snail1" 83 | ) 84 | Snail( 85 | progress = state.progress, 86 | color = state.color, 87 | onValueChange = { 88 | stateHolder.setProgress(it) 89 | udfStateHolder.accept(Event.UserTriggered(metadata = Marble.Metadata.Text(it.toString()))) 90 | } 91 | ) 92 | ColorSwatch( 93 | colors = state.colors, 94 | onColorClicked = { 95 | stateHolder.setSnailColor(it) 96 | udfStateHolder.accept(Event.UserTriggered(metadata = Marble.Metadata.Tint(state.colors[it]))) 97 | } 98 | ) 99 | Paragraph( 100 | text = "Progress: ${state.progress}" 101 | ) 102 | } 103 | } 104 | UDFVisualizer(udfStateHolder) 105 | } 106 | } -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/snails/Snail2.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.LaunchedEffect 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.rememberCoroutineScope 25 | import com.tunjid.mutator.demo.editor.Paragraph 26 | import com.tunjid.mutator.demo.editor.VerticalLayout 27 | import com.tunjid.mutator.demo.intervalFlow 28 | import com.tunjid.mutator.demo.toProgress 29 | import com.tunjid.mutator.demo.udfvisualizer.Marble 30 | import com.tunjid.mutator.demo.udfvisualizer.Event 31 | import com.tunjid.mutator.demo.udfvisualizer.UDFVisualizer 32 | import com.tunjid.mutator.demo.udfvisualizer.udfVisualizerStateHolder 33 | import kotlinx.coroutines.CoroutineScope 34 | import kotlinx.coroutines.flow.SharingStarted 35 | import kotlinx.coroutines.flow.StateFlow 36 | import kotlinx.coroutines.flow.stateIn 37 | 38 | class Snail2StateHolder( 39 | scope: CoroutineScope 40 | ) { 41 | val progress: StateFlow = intervalFlow(500) 42 | .toProgress() 43 | .stateIn( 44 | scope = scope, 45 | started = SharingStarted.WhileSubscribed(), 46 | initialValue = 0f 47 | ) 48 | } 49 | 50 | @Composable 51 | fun Snail2() { 52 | val scope = rememberCoroutineScope() 53 | val stateHolder = remember { Snail2StateHolder(scope) } 54 | val udf = remember { udfVisualizerStateHolder(scope) } 55 | val state by stateHolder.progress.collectAsState() 56 | 57 | LaunchedEffect(state) { 58 | udf.accept(Event.StateChange(metadata = Marble.Metadata.Text(state.toString()))) 59 | } 60 | 61 | Illustration { 62 | SnailCard { 63 | VerticalLayout { 64 | Paragraph( 65 | text = "Snail2" 66 | ) 67 | Snail( 68 | progress = state, 69 | ) 70 | Paragraph( 71 | text = "Progress: $state" 72 | ) 73 | } 74 | } 75 | UDFVisualizer(udf) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/snails/Snail3.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.LaunchedEffect 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.rememberCoroutineScope 25 | import com.tunjid.mutator.demo.Speed 26 | import com.tunjid.mutator.demo.editor.Paragraph 27 | import com.tunjid.mutator.demo.editor.VerticalLayout 28 | import com.tunjid.mutator.demo.speedFlow 29 | import com.tunjid.mutator.demo.text 30 | import com.tunjid.mutator.demo.toInterval 31 | import com.tunjid.mutator.demo.toProgress 32 | import com.tunjid.mutator.demo.udfvisualizer.Marble 33 | import com.tunjid.mutator.demo.udfvisualizer.Event 34 | import com.tunjid.mutator.demo.udfvisualizer.UDFVisualizer 35 | import com.tunjid.mutator.demo.udfvisualizer.udfVisualizerStateHolder 36 | import kotlinx.coroutines.CoroutineScope 37 | import kotlinx.coroutines.flow.Flow 38 | import kotlinx.coroutines.flow.SharingStarted 39 | import kotlinx.coroutines.flow.StateFlow 40 | import kotlinx.coroutines.flow.combine 41 | import kotlinx.coroutines.flow.stateIn 42 | 43 | 44 | data class Snail3State( 45 | val progress: Float = 0f, 46 | val speed: Speed = Speed.One, 47 | ) 48 | 49 | class Snail3StateHolder( 50 | scope: CoroutineScope 51 | ) { 52 | private val speed: Flow = scope.speedFlow() 53 | 54 | private val progress: Flow = speed 55 | .toInterval() 56 | .toProgress() 57 | 58 | val state: StateFlow = combine( 59 | progress, 60 | speed, 61 | ::Snail3State 62 | ) 63 | .stateIn( 64 | scope = scope, 65 | started = SharingStarted.WhileSubscribed(), 66 | initialValue = Snail3State() 67 | ) 68 | } 69 | 70 | @Composable 71 | fun Snail3() { 72 | val scope = rememberCoroutineScope() 73 | val stateHolder = remember { Snail3StateHolder(scope) } 74 | val udfStateHolder = remember { udfVisualizerStateHolder(scope) } 75 | val state by stateHolder.state.collectAsState() 76 | 77 | LaunchedEffect(state) { 78 | udfStateHolder.accept( 79 | Event.StateChange( 80 | metadata = Marble.Metadata.Text(state.progress.toString()) 81 | ) 82 | ) 83 | } 84 | 85 | Illustration { 86 | SnailCard { 87 | VerticalLayout { 88 | Paragraph( 89 | text = "Snail3" 90 | ) 91 | Snail( 92 | progress = state.progress, 93 | ) 94 | Paragraph( 95 | text = "Progress: ${state.progress}; Speed: ${state.speed.text}" 96 | ) 97 | } 98 | } 99 | UDFVisualizer(udfStateHolder) 100 | } 101 | } -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/snails/Snail4.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.LaunchedEffect 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.rememberCoroutineScope 25 | import com.tunjid.mutator.demo.Color 26 | import com.tunjid.mutator.demo.MutedColors 27 | import com.tunjid.mutator.demo.Speed 28 | import com.tunjid.mutator.demo.editor.Paragraph 29 | import com.tunjid.mutator.demo.editor.VerticalLayout 30 | import com.tunjid.mutator.demo.speedFlow 31 | import com.tunjid.mutator.demo.text 32 | import com.tunjid.mutator.demo.toInterval 33 | import com.tunjid.mutator.demo.toProgress 34 | import com.tunjid.mutator.demo.udfvisualizer.Marble 35 | import com.tunjid.mutator.demo.udfvisualizer.Event 36 | import com.tunjid.mutator.demo.udfvisualizer.UDFVisualizer 37 | import com.tunjid.mutator.demo.udfvisualizer.udfVisualizerStateHolder 38 | import kotlinx.coroutines.CoroutineScope 39 | import kotlinx.coroutines.flow.Flow 40 | import kotlinx.coroutines.flow.MutableStateFlow 41 | import kotlinx.coroutines.flow.SharingStarted 42 | import kotlinx.coroutines.flow.StateFlow 43 | import kotlinx.coroutines.flow.combine 44 | import kotlinx.coroutines.flow.stateIn 45 | 46 | 47 | data class Snail4State( 48 | val progress: Float = 0f, 49 | val speed: Speed = Speed.One, 50 | val color: Color = MutedColors.colors(false).first(), 51 | val colors: List = MutedColors.colors(false) 52 | ) 53 | 54 | class Snail4StateHolder( 55 | scope: CoroutineScope 56 | ) { 57 | 58 | private val speed: Flow = scope.speedFlow() 59 | 60 | private val progress: Flow = speed 61 | .toInterval() 62 | .toProgress() 63 | 64 | private val color: MutableStateFlow = MutableStateFlow( 65 | MutedColors.colors(isDark = false).first() 66 | ) 67 | 68 | val state: StateFlow = combine( 69 | progress, 70 | speed, 71 | color, 72 | ::Snail4State 73 | ) 74 | .stateIn( 75 | scope = scope, 76 | started = SharingStarted.WhileSubscribed(), 77 | initialValue = Snail4State() 78 | ) 79 | 80 | fun setSnailColor(index: Int) { 81 | this.color.value = state.value.colors[index] 82 | } 83 | } 84 | 85 | @Composable 86 | fun Snail4() { 87 | val scope = rememberCoroutineScope() 88 | val stateHolder = remember { Snail4StateHolder(scope) } 89 | val udfStateHolder = remember { udfVisualizerStateHolder(scope) } 90 | val state by stateHolder.state.collectAsState() 91 | 92 | LaunchedEffect(state) { 93 | udfStateHolder.accept( 94 | Event.StateChange( 95 | color = state.color, 96 | metadata = Marble.Metadata.Text(state.progress.toString()) 97 | ) 98 | ) 99 | } 100 | 101 | Illustration { 102 | SnailCard { 103 | VerticalLayout { 104 | Paragraph( 105 | text = "Snail4" 106 | ) 107 | Snail( 108 | progress = state.progress, 109 | color = state.color, 110 | ) 111 | ColorSwatch( 112 | colors = state.colors, 113 | onColorClicked = { 114 | stateHolder.setSnailColor(it) 115 | udfStateHolder.accept(Event.UserTriggered(metadata = Marble.Metadata.Tint(state.colors[it]))) 116 | } 117 | ) 118 | Paragraph( 119 | text = "Progress: ${state.progress}; Speed: ${state.speed.text}" 120 | ) 121 | } 122 | } 123 | UDFVisualizer(udfStateHolder) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/snails/Snail5.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.LaunchedEffect 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.rememberCoroutineScope 25 | import com.tunjid.mutator.Mutation 26 | import com.tunjid.mutator.demo.Color 27 | import com.tunjid.mutator.demo.MutedColors 28 | import com.tunjid.mutator.demo.Speed 29 | import com.tunjid.mutator.demo.editor.Paragraph 30 | import com.tunjid.mutator.demo.editor.VerticalLayout 31 | import com.tunjid.mutator.demo.speedFlow 32 | import com.tunjid.mutator.demo.text 33 | import com.tunjid.mutator.demo.toInterval 34 | import com.tunjid.mutator.demo.udfvisualizer.Marble 35 | import com.tunjid.mutator.demo.udfvisualizer.Event 36 | import com.tunjid.mutator.demo.udfvisualizer.UDFVisualizer 37 | import com.tunjid.mutator.demo.udfvisualizer.udfVisualizerStateHolder 38 | import com.tunjid.mutator.mutationOf 39 | import kotlinx.coroutines.CoroutineScope 40 | import kotlinx.coroutines.flow.Flow 41 | import kotlinx.coroutines.flow.MutableSharedFlow 42 | import kotlinx.coroutines.flow.SharingStarted 43 | import kotlinx.coroutines.flow.StateFlow 44 | import kotlinx.coroutines.flow.map 45 | import kotlinx.coroutines.flow.merge 46 | import kotlinx.coroutines.flow.scan 47 | import kotlinx.coroutines.flow.stateIn 48 | import kotlinx.coroutines.launch 49 | 50 | 51 | data class Snail5State( 52 | val progress: Float = 0f, 53 | val speed: Speed = Speed.One, 54 | val color: Color = MutedColors.colors(false).first(), 55 | val colors: List = MutedColors.colors(false) 56 | ) 57 | 58 | class Snail5StateHolder( 59 | private val scope: CoroutineScope 60 | ) { 61 | 62 | private val speed: Flow = scope.speedFlow() 63 | 64 | private val speedChanges: Flow> = speed 65 | .map { mutationOf { copy(speed = it) } } 66 | 67 | private val progressChanges: Flow> = speed 68 | .toInterval() 69 | .map { mutationOf { copy(progress = (progress + 1) % 100) } } 70 | 71 | private val changeEvents = MutableSharedFlow>() 72 | 73 | val state: StateFlow = merge( 74 | progressChanges, 75 | speedChanges, 76 | changeEvents, 77 | ) 78 | .scan(Snail5State()) { state, mutation -> mutation(state) } 79 | .stateIn( 80 | scope = scope, 81 | started = SharingStarted.WhileSubscribed(), 82 | initialValue = Snail5State() 83 | ) 84 | 85 | fun setSnailColor(index: Int) { 86 | scope.launch { 87 | changeEvents.emit { copy(color = colors[index]) } 88 | } 89 | } 90 | } 91 | 92 | @Composable 93 | fun Snail5() { 94 | val scope = rememberCoroutineScope() 95 | val stateHolder = remember { Snail5StateHolder(scope) } 96 | val udfStateHolder = remember { udfVisualizerStateHolder(scope) } 97 | val state by stateHolder.state.collectAsState() 98 | 99 | LaunchedEffect(state) { 100 | udfStateHolder.accept( 101 | Event.StateChange( 102 | color = state.color, 103 | metadata = Marble.Metadata.Text(state.progress.toString()) 104 | ) 105 | ) 106 | } 107 | 108 | Illustration { 109 | SnailCard { 110 | VerticalLayout { 111 | Paragraph( 112 | text = "Snail5" 113 | ) 114 | Snail( 115 | progress = state.progress, 116 | color = state.color, 117 | ) 118 | ColorSwatch( 119 | colors = state.colors, 120 | onColorClicked = { 121 | stateHolder.setSnailColor(it) 122 | udfStateHolder.accept(Event.UserTriggered(metadata = Marble.Metadata.Tint(state.colors[it]))) 123 | } 124 | ) 125 | Paragraph( 126 | text = "Progress: ${state.progress}; Speed: ${state.speed.text}" 127 | ) 128 | } 129 | } 130 | UDFVisualizer(udfStateHolder) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/snails/Snail6.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.LaunchedEffect 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.rememberCoroutineScope 25 | import com.tunjid.mutator.Mutation 26 | import com.tunjid.mutator.demo.Color 27 | import com.tunjid.mutator.demo.MutedColors 28 | import com.tunjid.mutator.demo.Speed 29 | import com.tunjid.mutator.demo.editor.Paragraph 30 | import com.tunjid.mutator.demo.editor.VerticalLayout 31 | import com.tunjid.mutator.demo.speedFlow 32 | import com.tunjid.mutator.demo.text 33 | import com.tunjid.mutator.demo.toInterval 34 | import com.tunjid.mutator.demo.udfvisualizer.Marble 35 | import com.tunjid.mutator.demo.udfvisualizer.Event 36 | import com.tunjid.mutator.demo.udfvisualizer.UDFVisualizer 37 | import com.tunjid.mutator.demo.udfvisualizer.udfVisualizerStateHolder 38 | import com.tunjid.mutator.mutationOf 39 | import kotlinx.coroutines.CoroutineScope 40 | import kotlinx.coroutines.flow.Flow 41 | import kotlinx.coroutines.flow.MutableSharedFlow 42 | import kotlinx.coroutines.flow.SharingStarted 43 | import kotlinx.coroutines.flow.StateFlow 44 | import kotlinx.coroutines.flow.map 45 | import kotlinx.coroutines.flow.merge 46 | import kotlinx.coroutines.flow.scan 47 | import kotlinx.coroutines.flow.stateIn 48 | import kotlinx.coroutines.launch 49 | 50 | data class Snail6State( 51 | val progress: Float = 0f, 52 | val speed: Speed = Speed.One, 53 | val color: Color = MutedColors.colors(false).first(), 54 | val colors: List = MutedColors.colors(false) 55 | ) 56 | 57 | class Snail6StateHolder( 58 | private val scope: CoroutineScope 59 | ) { 60 | 61 | private val speed: Flow = scope.speedFlow() 62 | 63 | private val speedChanges: Flow> = speed 64 | .map { mutationOf { copy(speed = it) } } 65 | 66 | private val progressChanges: Flow> = speed 67 | .toInterval() 68 | .map { mutationOf { copy(progress = (progress + 1) % 100) } } 69 | 70 | private val changeEvents = MutableSharedFlow>() 71 | 72 | val state: StateFlow = merge( 73 | progressChanges, 74 | speedChanges, 75 | changeEvents, 76 | ) 77 | .scan(Snail6State()) { state, mutation -> mutation(state) } 78 | .stateIn( 79 | scope = scope, 80 | started = SharingStarted.WhileSubscribed(), 81 | initialValue = Snail6State() 82 | ) 83 | 84 | fun setSnailColor(index: Int) { 85 | scope.launch { 86 | changeEvents.emit { copy(color = colors[index]) } 87 | } 88 | } 89 | 90 | fun setProgress(progress: Float) { 91 | scope.launch { 92 | changeEvents.emit { copy(progress = progress) } 93 | } 94 | } 95 | } 96 | 97 | @Composable 98 | fun Snail6() { 99 | val scope = rememberCoroutineScope() 100 | val stateHolder = remember { Snail6StateHolder(scope) } 101 | val udfStateHolder = remember { udfVisualizerStateHolder(scope) } 102 | val state by stateHolder.state.collectAsState() 103 | 104 | LaunchedEffect(state) { 105 | udfStateHolder.accept( 106 | Event.StateChange( 107 | color = state.color, 108 | metadata = Marble.Metadata.Text(state.progress.toString()) 109 | ) 110 | ) 111 | } 112 | 113 | Illustration { 114 | SnailCard { 115 | VerticalLayout { 116 | Paragraph( 117 | text = "Snail6" 118 | ) 119 | Snail( 120 | progress = state.progress, 121 | color = state.color, 122 | onValueChange = { 123 | stateHolder.setProgress(it) 124 | udfStateHolder.accept(Event.UserTriggered(metadata = Marble.Metadata.Text(it.toString()))) 125 | } 126 | ) 127 | ColorSwatch( 128 | colors = state.colors, 129 | onColorClicked = { 130 | stateHolder.setSnailColor(it) 131 | udfStateHolder.accept(Event.UserTriggered(metadata = Marble.Metadata.Tint(state.colors[it]))) 132 | } 133 | ) 134 | Paragraph( 135 | text = "Progress: ${state.progress}; Speed: ${state.speed.text}" 136 | ) 137 | } 138 | } 139 | UDFVisualizer(udfStateHolder) 140 | } 141 | } -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/snails/Snail7.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.LaunchedEffect 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.rememberCoroutineScope 25 | import com.tunjid.mutator.Mutation 26 | import com.tunjid.mutator.coroutines.stateFlowMutator 27 | import com.tunjid.mutator.demo.Color 28 | import com.tunjid.mutator.demo.MutedColors 29 | import com.tunjid.mutator.demo.Speed 30 | import com.tunjid.mutator.demo.editor.Paragraph 31 | import com.tunjid.mutator.demo.editor.VerticalLayout 32 | import com.tunjid.mutator.demo.speedFlow 33 | import com.tunjid.mutator.demo.text 34 | import com.tunjid.mutator.demo.toInterval 35 | import com.tunjid.mutator.demo.udfvisualizer.Event 36 | import com.tunjid.mutator.demo.udfvisualizer.Marble 37 | import com.tunjid.mutator.demo.udfvisualizer.UDFVisualizer 38 | import com.tunjid.mutator.demo.udfvisualizer.udfVisualizerStateHolder 39 | import com.tunjid.mutator.mutationOf 40 | import kotlinx.coroutines.CoroutineScope 41 | import kotlinx.coroutines.flow.Flow 42 | import kotlinx.coroutines.flow.SharingStarted 43 | import kotlinx.coroutines.flow.StateFlow 44 | import kotlinx.coroutines.flow.map 45 | 46 | data class Snail7State( 47 | val progress: Float = 0f, 48 | val speed: Speed = Speed.One, 49 | val color: Color = MutedColors.colors(false).first(), 50 | val colors: List = MutedColors.colors(false) 51 | ) 52 | 53 | class Snail7StateHolder( 54 | scope: CoroutineScope 55 | ) { 56 | 57 | private val speed: Flow = scope.speedFlow() 58 | 59 | private val speedChanges: Flow> = speed 60 | .map { mutationOf { copy(speed = it) } } 61 | 62 | private val progressChanges: Flow> = speed 63 | .toInterval() 64 | .map { mutationOf { copy(progress = (progress + 1) % 100) } } 65 | 66 | private val stateMutator = scope.stateFlowMutator( 67 | initialState = Snail7State(), 68 | started = SharingStarted.WhileSubscribed(), 69 | inputs = listOf( 70 | speedChanges, 71 | progressChanges, 72 | ) 73 | ) 74 | 75 | val state: StateFlow = stateMutator.state 76 | 77 | fun setSnailColor(index: Int) = stateMutator.launch { 78 | emit { copy(color = colors[index]) } 79 | } 80 | 81 | fun setProgress(progress: Float) = stateMutator.launch { 82 | emit { copy(progress = progress) } 83 | } 84 | } 85 | 86 | 87 | @Composable 88 | fun Snail7() { 89 | val scope = rememberCoroutineScope() 90 | val stateHolder = remember { Snail7StateHolder(scope) } 91 | val udfStateHolder = remember { udfVisualizerStateHolder(scope) } 92 | val state by stateHolder.state.collectAsState() 93 | 94 | LaunchedEffect(state) { 95 | udfStateHolder.accept( 96 | Event.StateChange( 97 | color = state.color, 98 | metadata = Marble.Metadata.Text(state.progress.toString()) 99 | ) 100 | ) 101 | } 102 | 103 | Illustration { 104 | SnailCard { 105 | VerticalLayout { 106 | Paragraph( 107 | text = "Snail7" 108 | ) 109 | Snail( 110 | progress = state.progress, 111 | color = state.color, 112 | onValueChange = { 113 | stateHolder.setProgress(it) 114 | udfStateHolder.accept(Event.UserTriggered(metadata = Marble.Metadata.Text(it.toString()))) 115 | } 116 | ) 117 | ColorSwatch( 118 | colors = state.colors, 119 | onColorClicked = { 120 | stateHolder.setSnailColor(it) 121 | udfStateHolder.accept(Event.UserTriggered(metadata = Marble.Metadata.Tint(state.colors[it]))) 122 | } 123 | ) 124 | Paragraph( 125 | text = "Progress: ${state.progress}; Speed: ${state.speed.text}" 126 | ) 127 | } 128 | } 129 | UDFVisualizer(udfStateHolder) 130 | } 131 | } -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/snails/Snail8.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.LaunchedEffect 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.rememberCoroutineScope 25 | import com.tunjid.mutator.Mutation 26 | import com.tunjid.mutator.coroutines.stateFlowMutator 27 | import com.tunjid.mutator.demo.Color 28 | import com.tunjid.mutator.demo.MutedColors 29 | import com.tunjid.mutator.demo.Speed 30 | import com.tunjid.mutator.demo.editor.VerticalLayout 31 | import com.tunjid.mutator.demo.interpolateColors 32 | import com.tunjid.mutator.demo.speedFlow 33 | import com.tunjid.mutator.demo.text 34 | import com.tunjid.mutator.demo.toInterval 35 | import com.tunjid.mutator.demo.udfvisualizer.Event 36 | import com.tunjid.mutator.demo.udfvisualizer.Marble 37 | import com.tunjid.mutator.demo.udfvisualizer.UDFVisualizer 38 | import com.tunjid.mutator.demo.udfvisualizer.udfVisualizerStateHolder 39 | import com.tunjid.mutator.mutationOf 40 | import kotlinx.coroutines.CoroutineScope 41 | import kotlinx.coroutines.flow.Flow 42 | import kotlinx.coroutines.flow.SharingStarted 43 | import kotlinx.coroutines.flow.StateFlow 44 | import kotlinx.coroutines.flow.map 45 | 46 | 47 | data class Snail8State( 48 | val progress: Float = 0f, 49 | val speed: Speed = Speed.One, 50 | val isDark: Boolean = false, 51 | val colorIndex: Int = 0, 52 | val colorInterpolationProgress: Float = 0F, 53 | val colors: List = MutedColors.colors(false) 54 | ) 55 | 56 | val Snail8State.color get() = colors[colorIndex] 57 | 58 | val Snail8State.cardColor: Color get() = colors.last() 59 | 60 | val Snail8State.textColor: Color get() = if (cardColor.isBright()) Color.Black else Color.LightGray 61 | 62 | class Snail8StateHolder( 63 | scope: CoroutineScope 64 | ) { 65 | 66 | private val speed: Flow = scope.speedFlow() 67 | 68 | private val speedChanges: Flow> = speed 69 | .map { mutationOf { copy(speed = it) } } 70 | 71 | private val progressChanges: Flow> = speed 72 | .toInterval() 73 | .map { mutationOf { copy(progress = (progress + 1) % 100) } } 74 | 75 | private val stateMutator = scope.stateFlowMutator( 76 | initialState = Snail8State(), 77 | started = SharingStarted.WhileSubscribed(), 78 | inputs = listOf( 79 | speedChanges, 80 | progressChanges, 81 | ) 82 | ) 83 | 84 | val state: StateFlow = stateMutator.state 85 | 86 | fun setSnailColor(index: Int) = stateMutator.launch { 87 | emit { copy(colorIndex = index) } 88 | } 89 | 90 | fun setProgress(progress: Float) = stateMutator.launch { 91 | emit { copy(progress = progress) } 92 | } 93 | 94 | fun setMode(isDark: Boolean) = stateMutator.launch { 95 | emit { copy(isDark = isDark) } 96 | // Collect from a flow that animates color changes 97 | interpolateColors( 98 | startColors = state.value.colors.map(Color::argb).toIntArray(), 99 | endColors = MutedColors.colors(isDark).map(Color::argb).toIntArray() 100 | ).collect { (progress, colors) -> 101 | emit { 102 | copy( 103 | colorInterpolationProgress = progress, 104 | colors = colors 105 | ) 106 | } 107 | } 108 | } 109 | } 110 | 111 | @Composable 112 | fun Snail8() { 113 | val scope = rememberCoroutineScope() 114 | val stateHolder = remember { Snail8StateHolder(scope) } 115 | val udfStateHolder = remember { udfVisualizerStateHolder(scope) } 116 | val state by stateHolder.state.collectAsState() 117 | 118 | LaunchedEffect(state) { 119 | udfStateHolder.accept( 120 | Event.StateChange( 121 | color = state.color, 122 | metadata = Marble.Metadata.Text(state.progress.toString()) 123 | ) 124 | ) 125 | } 126 | 127 | Illustration { 128 | SnailCard(state.cardColor) { 129 | VerticalLayout { 130 | SnailText( 131 | color = state.textColor, 132 | text = "Snail8" 133 | ) 134 | Snail( 135 | progress = state.progress, 136 | color = state.color, 137 | onValueChange = { 138 | stateHolder.setProgress(it) 139 | udfStateHolder.accept(Event.UserTriggered(metadata = Marble.Metadata.Text(it.toString()))) 140 | } 141 | ) 142 | ColorSwatch( 143 | colors = state.colors, 144 | onColorClicked = { 145 | stateHolder.setSnailColor(it) 146 | udfStateHolder.accept(Event.UserTriggered(metadata = Marble.Metadata.Tint(state.colors[it]))) 147 | } 148 | ) 149 | SnailText( 150 | color = state.textColor, 151 | text = "Progress: ${state.progress}; Speed: ${state.speed.text}" 152 | ) 153 | ToggleButton( 154 | progress = state.colorInterpolationProgress, 155 | onClicked = { 156 | stateHolder.setMode(!state.isDark) 157 | udfStateHolder.accept( 158 | Event.UserTriggered( 159 | Marble.Metadata.Text( 160 | if (state.isDark) "☀️" 161 | else "\uD83C\uDF18" 162 | ) 163 | ) 164 | ) 165 | } 166 | ) 167 | } 168 | } 169 | UDFVisualizer(udfStateHolder) 170 | } 171 | } -------------------------------------------------------------------------------- /demo/src/commonMain/kotlin/com/tunjid/mutator/demo/udfvisualizer/UDFVisualizerStateHolder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.udfvisualizer 18 | 19 | import androidx.compose.runtime.Composable 20 | import com.tunjid.mutator.Mutation 21 | import com.tunjid.mutator.ActionStateMutator 22 | import com.tunjid.mutator.coroutines.actionStateFlowMutator 23 | import com.tunjid.mutator.demo.Color 24 | import com.tunjid.mutator.demo.intervalFlow 25 | import com.tunjid.mutator.mutationOf 26 | import kotlinx.coroutines.CoroutineScope 27 | import kotlinx.coroutines.flow.Flow 28 | import kotlinx.coroutines.flow.StateFlow 29 | import kotlinx.coroutines.flow.map 30 | 31 | val eventColor = Color(0xEF685C) 32 | val stateColor = Color(0x3080E9) 33 | 34 | typealias UDFVisualizerStateHolder = ActionStateMutator> 35 | 36 | sealed class Marble { 37 | 38 | sealed class Metadata { 39 | object Nothing : Metadata() 40 | data class Text(val text: String) : Metadata() 41 | data class Tint(val color: Color) : Metadata() 42 | } 43 | 44 | abstract val percentage: Int 45 | abstract val metadata: Metadata 46 | 47 | data class Event( 48 | override val percentage: Int = 0, 49 | override val metadata: Metadata = Metadata.Nothing, 50 | ) : Marble() 51 | 52 | data class State( 53 | val index: Int = -1, 54 | val color: Color = stateColor, 55 | override val percentage: Int = 0, 56 | override val metadata: Metadata = Metadata.Nothing, 57 | ) : Marble() 58 | } 59 | 60 | sealed class Event { 61 | data class StateChange( 62 | val color: Color = stateColor, 63 | val metadata: Marble.Metadata = Marble.Metadata.Nothing 64 | ) : Event() 65 | 66 | data class UserTriggered( 67 | val metadata: Marble.Metadata = Marble.Metadata.Nothing 68 | ) : Event() 69 | } 70 | 71 | data class State( 72 | val index: Int = -1, 73 | val marbles: List = listOf() 74 | ) 75 | 76 | fun udfVisualizerStateHolder( 77 | scope: CoroutineScope 78 | ): ActionStateMutator> = scope.actionStateFlowMutator( 79 | initialState = State(), 80 | inputs = listOf(frames()), 81 | actionTransform = { it.stateMutations() }, 82 | ) 83 | 84 | private fun frames(): Flow> = intervalFlow(10) 85 | .map { mutationOf { advanceFrame() } } 86 | 87 | private fun Flow.stateMutations(): Flow> = 88 | map { mutationOf { this + it } } 89 | 90 | /** 91 | * Reduces the [action] into the [State] 92 | */ 93 | private operator fun State.plus(action: Event) = copy( 94 | index = index + 1, 95 | marbles = marbles + when (action) { 96 | is Event.UserTriggered -> Marble.Event(metadata = action.metadata) 97 | is Event.StateChange -> Marble.State( 98 | index = index + 1, 99 | metadata = action.metadata, 100 | color = action.color 101 | ) 102 | } 103 | ) 104 | 105 | /** 106 | * Advances the frame for the UDF visualizer 107 | */ 108 | private fun State.advanceFrame() = copy(marbles = marbles.mapNotNull { 109 | if (it.percentage > 100) null 110 | else when (it) { 111 | is Marble.Event -> it.copy(percentage = it.percentage + 1) 112 | is Marble.State -> it.copy(percentage = it.percentage + 1) 113 | } 114 | }) 115 | 116 | @Composable 117 | expect fun UDFVisualizer(stateHolder: UDFVisualizerStateHolder) -------------------------------------------------------------------------------- /demo/src/desktopMain/kotlin/com/tunjid/mutator/demo/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo 18 | 19 | import androidx.compose.ui.graphics.luminance 20 | import androidx.compose.ui.graphics.toArgb 21 | import androidx.compose.ui.graphics.Color as ComposeColor 22 | 23 | actual class Color actual constructor(color: Long) { 24 | internal val composeColor = ComposeColor(color) 25 | 26 | actual val argb: Int = composeColor.toArgb() 27 | actual val r: Int = composeColor.red.toRgbInt() 28 | actual val g: Int = composeColor.green.toRgbInt() 29 | actual val b: Int = composeColor.blue.toRgbInt() 30 | 31 | actual fun isBright(): Boolean = composeColor.luminance() >= 0.5 32 | 33 | private fun Float.toRgbInt() = (this * 255).toInt() 34 | 35 | actual companion object { 36 | actual val Black: Color = Color(ComposeColor.Black.toArgb().toLong()) 37 | actual val LightGray: Color = Color(ComposeColor.LightGray.toArgb().toLong()) 38 | } 39 | } 40 | 41 | val Color.toComposeColor get() = composeColor -------------------------------------------------------------------------------- /demo/src/desktopMain/kotlin/com/tunjid/mutator/demo/editor/Editor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.editor 18 | 19 | import androidx.compose.ui.unit.TextUnit 20 | import androidx.compose.ui.unit.sp 21 | 22 | data class Settings( 23 | val fontSize: TextUnit = 13.sp, 24 | val maxLineSymbols: Int = 120 25 | ) 26 | 27 | data class Line(val number: Int, val content: Content) 28 | 29 | data class Lines( 30 | val data: List 31 | ) { 32 | val size: Int = data.size 33 | val lineNumberDigitCount: Int = size.toString().length 34 | 35 | operator fun get(index: Int): Line = Line( 36 | number = index, 37 | content = Content(data[index], true) 38 | ) 39 | } 40 | 41 | 42 | data class Content(val value: String, val isCode: Boolean) 43 | 44 | 45 | fun String.toCodeLines() = Lines(split("\n")) -------------------------------------------------------------------------------- /demo/src/desktopMain/kotlin/com/tunjid/mutator/demo/editor/Fonts.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.editor 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.text.font.FontFamily 21 | import androidx.compose.ui.text.font.FontStyle 22 | import androidx.compose.ui.text.font.FontWeight 23 | import com.tunjid.mutator.demo.platform.Font 24 | 25 | object Fonts { 26 | @Composable 27 | fun jetbrainsMono() = FontFamily( 28 | Font( 29 | "JetBrains Mono", 30 | "jetbrainsmono_regular", 31 | FontWeight.Normal, 32 | FontStyle.Normal 33 | ), 34 | Font( 35 | "JetBrains Mono", 36 | "jetbrainsmono_italic", 37 | FontWeight.Normal, 38 | FontStyle.Italic 39 | ), 40 | 41 | Font( 42 | "JetBrains Mono", 43 | "jetbrainsmono_bold", 44 | FontWeight.Bold, 45 | FontStyle.Normal 46 | ), 47 | Font( 48 | "JetBrains Mono", 49 | "jetbrainsmono_bold_italic", 50 | FontWeight.Bold, 51 | FontStyle.Italic 52 | ), 53 | 54 | Font( 55 | "JetBrains Mono", 56 | "jetbrainsmono_extrabold", 57 | FontWeight.ExtraBold, 58 | FontStyle.Normal 59 | ), 60 | Font( 61 | "JetBrains Mono", 62 | "jetbrainsmono_extrabold_italic", 63 | FontWeight.ExtraBold, 64 | FontStyle.Italic 65 | ), 66 | 67 | Font( 68 | "JetBrains Mono", 69 | "jetbrainsmono_medium", 70 | FontWeight.Medium, 71 | FontStyle.Normal 72 | ), 73 | Font( 74 | "JetBrains Mono", 75 | "jetbrainsmono_medium_italic", 76 | FontWeight.Medium, 77 | FontStyle.Italic 78 | ) 79 | ) 80 | } -------------------------------------------------------------------------------- /demo/src/desktopMain/kotlin/com/tunjid/mutator/demo/editor/Layouts.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.editor 18 | 19 | import androidx.compose.foundation.layout.Arrangement 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.Column 22 | import androidx.compose.foundation.layout.Row 23 | import androidx.compose.foundation.layout.fillMaxSize 24 | import androidx.compose.foundation.layout.padding 25 | import androidx.compose.foundation.layout.widthIn 26 | import androidx.compose.foundation.rememberScrollState 27 | import androidx.compose.foundation.verticalScroll 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.unit.dp 32 | 33 | @Composable 34 | actual fun ContainerLayout(content: @Composable () -> Unit) { 35 | val scrollState = rememberScrollState() 36 | Box( 37 | modifier = Modifier 38 | .fillMaxSize() 39 | ) { 40 | Column( 41 | modifier = Modifier 42 | .fillMaxSize() 43 | .verticalScroll(scrollState) 44 | .widthIn(0.dp, 600.dp) 45 | .padding(horizontal = 16.dp) 46 | .align(Alignment.Center), 47 | horizontalAlignment = Alignment.CenterHorizontally 48 | ) { 49 | content() 50 | } 51 | } 52 | } 53 | 54 | @Composable 55 | actual fun SectionLayout(content: @Composable () -> Unit) { 56 | Column( 57 | modifier = Modifier.padding(horizontal = 16.dp), 58 | verticalArrangement = Arrangement.spacedBy(16.dp), 59 | horizontalAlignment = Alignment.CenterHorizontally, 60 | content = { content() } 61 | ) 62 | } 63 | 64 | @Composable 65 | actual fun VerticalLayout(content: @Composable () -> Unit) { 66 | Column( 67 | modifier = Modifier.padding(horizontal = 16.dp), 68 | verticalArrangement = Arrangement.spacedBy(16.dp), 69 | content = { content() } 70 | ) 71 | } 72 | 73 | @Composable 74 | actual fun HorizontalLayout( 75 | centerOnMainAxis: Boolean, 76 | content: @Composable () -> Unit 77 | ) { 78 | Row( 79 | modifier = Modifier.padding(horizontal = 8.dp), 80 | horizontalArrangement = if (centerOnMainAxis) Arrangement.Center else Arrangement.Start, 81 | content = { content() } 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /demo/src/desktopMain/kotlin/com/tunjid/mutator/demo/editor/Text.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.editor 18 | 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.fillMaxWidth 22 | import androidx.compose.foundation.layout.padding 23 | import androidx.compose.foundation.shape.RoundedCornerShape 24 | import androidx.compose.material.MaterialTheme 25 | import androidx.compose.material.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.text.TextStyle 31 | import androidx.compose.ui.unit.dp 32 | import com.halilibo.richtext.markdown.Markdown 33 | import com.halilibo.richtext.ui.RichText 34 | 35 | @Composable 36 | actual fun Paragraph(text: String) { 37 | StyledText( 38 | modifier = Modifier.padding(vertical = 8.dp), 39 | text = text, 40 | style = MaterialTheme.typography.body1.copy( 41 | MaterialTheme.colors.onSurface 42 | ) 43 | ) 44 | } 45 | 46 | @Composable 47 | actual fun Markdown(content: String) { 48 | RichText( 49 | modifier = Modifier.padding(horizontal = 16.dp) 50 | ) { 51 | Markdown( 52 | content = content 53 | ) 54 | } 55 | } 56 | 57 | @Composable 58 | actual fun CallToAction( 59 | text: String, 60 | centerText: Boolean, 61 | ) { 62 | Box( 63 | modifier = Modifier.fillMaxWidth(0.8f) 64 | .background( 65 | color = Color(0xE1F4FE), 66 | shape = RoundedCornerShape(size = 16.dp) 67 | ), 68 | content = { 69 | Text( 70 | modifier = Modifier 71 | .fillMaxWidth() 72 | .padding(16.dp) 73 | .align(Alignment.Center) 74 | .let { 75 | if (centerText) it.align(Alignment.Center) 76 | else it 77 | }, 78 | color = Color(0x00589B), 79 | text = text 80 | ) 81 | }, 82 | ) 83 | } 84 | 85 | @Composable 86 | private fun StyledText( 87 | text: String, 88 | style: TextStyle, 89 | modifier: Modifier = Modifier 90 | ) { 91 | Text( 92 | modifier = modifier, 93 | style = style, 94 | text = text 95 | ) 96 | } 97 | 98 | -------------------------------------------------------------------------------- /demo/src/desktopMain/kotlin/com/tunjid/mutator/demo/platform/Resources.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.platform 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.text.font.Font 21 | import androidx.compose.ui.text.font.FontStyle 22 | import androidx.compose.ui.text.font.FontWeight 23 | 24 | @Composable 25 | internal fun Font(name: String, res: String, weight: FontWeight, style: FontStyle): Font = 26 | androidx.compose.ui.text.platform.Font("font/$res.ttf", weight, style) -------------------------------------------------------------------------------- /demo/src/desktopMain/kotlin/com/tunjid/mutator/demo/snails/Snail.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.layout.Arrangement 21 | import androidx.compose.foundation.layout.Box 22 | import androidx.compose.foundation.layout.Column 23 | import androidx.compose.foundation.layout.Row 24 | import androidx.compose.foundation.layout.fillMaxWidth 25 | import androidx.compose.foundation.layout.padding 26 | import androidx.compose.material.Button 27 | import androidx.compose.material.ButtonDefaults 28 | import androidx.compose.material.MaterialTheme 29 | import androidx.compose.material.Slider 30 | import androidx.compose.material.SliderDefaults 31 | import androidx.compose.material.Surface 32 | import androidx.compose.material.Text 33 | import androidx.compose.runtime.Composable 34 | import androidx.compose.ui.Alignment 35 | import androidx.compose.ui.Modifier 36 | import androidx.compose.ui.unit.dp 37 | import com.tunjid.mutator.demo.Color 38 | import com.tunjid.mutator.demo.Speed 39 | import com.tunjid.mutator.demo.toComposeColor 40 | 41 | @Composable 42 | actual fun Illustration( 43 | content: @Composable () -> Unit 44 | ) { 45 | Column( 46 | modifier = Modifier.fillMaxWidth(0.8f), 47 | content = { content() }, 48 | ) 49 | } 50 | 51 | @Composable 52 | actual fun SnailCard( 53 | color: Color, 54 | content: @Composable () -> Unit 55 | ) { 56 | Surface( 57 | color = color.toComposeColor, 58 | content = { content() }, 59 | ) 60 | } 61 | 62 | @Composable 63 | actual fun SnailText( 64 | color: Color, 65 | text: String 66 | ) { 67 | Text( 68 | modifier = Modifier.padding(vertical = 8.dp), 69 | text = text, 70 | color = color.toComposeColor, 71 | style = MaterialTheme.typography.body1.copy( 72 | MaterialTheme.colors.onSurface 73 | ) 74 | ) 75 | } 76 | 77 | @Composable 78 | actual fun Snail( 79 | progress: Float, 80 | speed: Speed, 81 | color: Color, 82 | onValueChange: (Float) -> Unit 83 | ) { 84 | Slider( 85 | valueRange = 0f..100f, 86 | value = progress, 87 | colors = SliderDefaults.colors( 88 | thumbColor = color.toComposeColor, 89 | activeTrackColor = color.toComposeColor 90 | ), 91 | onValueChange = onValueChange 92 | ) 93 | } 94 | 95 | @Composable 96 | actual fun ColorSwatch( 97 | colors: List, 98 | onColorClicked: (index: Int) -> Unit 99 | ) { 100 | Row( 101 | modifier = Modifier.fillMaxWidth(), 102 | horizontalArrangement = Arrangement.Center 103 | ) { 104 | colors.forEachIndexed { index, color -> 105 | Button( 106 | modifier = Modifier.padding(horizontal = 8.dp), 107 | colors = ButtonDefaults.buttonColors(backgroundColor = color.toComposeColor), 108 | onClick = { 109 | onColorClicked(index) 110 | }, 111 | content = { 112 | }, 113 | ) 114 | } 115 | } 116 | } 117 | 118 | @Composable 119 | actual fun ToggleButton( 120 | progress: Float, 121 | onClicked: () -> Unit 122 | ) { 123 | Box( 124 | modifier = Modifier.fillMaxWidth() 125 | ) { 126 | Box( 127 | modifier = Modifier 128 | .fillMaxWidth(0.25F) 129 | .align(Alignment.Center) 130 | ) { 131 | Button( 132 | modifier = Modifier 133 | .fillMaxWidth() 134 | .align(Alignment.Center) 135 | .background(Color(0xA7C7E7).composeColor), 136 | onClick = { 137 | onClicked() 138 | }, 139 | content = { 140 | Text(text = "Toggle mode") 141 | }, 142 | ) 143 | Button( 144 | modifier = Modifier 145 | .fillMaxWidth(progress) 146 | .align(Alignment.CenterStart) 147 | .background(Color(0xA7C7E7).toComposeColor), 148 | onClick = { 149 | onClicked() 150 | }, 151 | content = {}, 152 | ) 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /demo/src/desktopMain/kotlin/com/tunjid/mutator/demo/udfvisualizer/UDFVisualizer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.udfvisualizer 18 | 19 | import androidx.compose.runtime.Composable 20 | 21 | @Composable 22 | actual fun UDFVisualizer(stateHolder: UDFVisualizerStateHolder) { 23 | } -------------------------------------------------------------------------------- /demo/src/desktopMain/kotlin/main.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import androidx.compose.ui.unit.DpSize 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.window.WindowState 20 | import androidx.compose.ui.window.singleWindowApplication 21 | import com.tunjid.mutator.demo.App 22 | 23 | 24 | fun main() { 25 | singleWindowApplication( 26 | title = "State", 27 | state = WindowState(size = DpSize(800.dp, 800.dp)) 28 | ) { 29 | App() 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /demo/src/desktopMain/resources/font/jetbrainsmono_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/desktopMain/resources/font/jetbrainsmono_bold.ttf -------------------------------------------------------------------------------- /demo/src/desktopMain/resources/font/jetbrainsmono_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/desktopMain/resources/font/jetbrainsmono_bold_italic.ttf -------------------------------------------------------------------------------- /demo/src/desktopMain/resources/font/jetbrainsmono_extrabold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/desktopMain/resources/font/jetbrainsmono_extrabold.ttf -------------------------------------------------------------------------------- /demo/src/desktopMain/resources/font/jetbrainsmono_extrabold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/desktopMain/resources/font/jetbrainsmono_extrabold_italic.ttf -------------------------------------------------------------------------------- /demo/src/desktopMain/resources/font/jetbrainsmono_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/desktopMain/resources/font/jetbrainsmono_italic.ttf -------------------------------------------------------------------------------- /demo/src/desktopMain/resources/font/jetbrainsmono_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/desktopMain/resources/font/jetbrainsmono_medium.ttf -------------------------------------------------------------------------------- /demo/src/desktopMain/resources/font/jetbrainsmono_medium_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/desktopMain/resources/font/jetbrainsmono_medium_italic.ttf -------------------------------------------------------------------------------- /demo/src/desktopMain/resources/font/jetbrainsmono_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/desktopMain/resources/font/jetbrainsmono_regular.ttf -------------------------------------------------------------------------------- /demo/src/jsMain/kotlin/com/tunjid/mutator/demo/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo 18 | 19 | import kotlin.math.sqrt 20 | 21 | actual class Color actual constructor(color: Long) { 22 | 23 | actual val argb: Int = color.toInt() 24 | 25 | actual val r: Int = argb and 0x00FF0000 shr 16 26 | actual val g: Int = argb and 0x0000FF00 shr 8 27 | actual val b: Int = argb and 0x000000FF 28 | 29 | actual companion object { 30 | actual val Black: Color = Color(0x000000) 31 | actual val LightGray: Color = Color(0xFFCCCCCC) 32 | } 33 | 34 | actual fun isBright(): Boolean { 35 | val hsp = sqrt( 36 | 0.299 * (r * r) + 37 | 0.587 * (g * g) + 38 | 0.114 * (b * b) 39 | ) 40 | return hsp > 127.5 41 | } 42 | } 43 | 44 | fun Color.rgb() = org.jetbrains.compose.web.css.rgb( 45 | r = r, 46 | g = g, 47 | b = b, 48 | ) 49 | 50 | fun Color.hex(): String { 51 | val r = r.toHexString() 52 | val g = g.toHexString() 53 | val b = b.toHexString() 54 | return "#$r$g$b" 55 | } 56 | 57 | fun Int.toHexString(): String { 58 | val hex = this.toString(16) 59 | return if (hex.length == 1) "0$hex" else hex 60 | } -------------------------------------------------------------------------------- /demo/src/jsMain/kotlin/com/tunjid/mutator/demo/editor/Layouts.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.editor 18 | 19 | import androidx.compose.runtime.Composable 20 | import org.jetbrains.compose.web.css.StyleScope 21 | import org.jetbrains.compose.web.dom.Div 22 | 23 | @Composable 24 | actual fun ContainerLayout(content: @Composable () -> Unit) { 25 | StyledDiv(content = content, classNames = listOf("container")) 26 | } 27 | 28 | @Composable 29 | actual fun SectionLayout(content: @Composable () -> Unit) { 30 | StyledDiv(content = content, classNames = listOf("sectionLayout")) 31 | } 32 | 33 | @Composable 34 | actual fun VerticalLayout(content: @Composable () -> Unit) { 35 | StyledDiv(content = content, classNames = listOf("verticalLayout")) 36 | } 37 | 38 | @Composable 39 | actual fun HorizontalLayout( 40 | centerOnMainAxis: Boolean, 41 | content: @Composable () -> Unit 42 | ) { 43 | StyledDiv( 44 | content = content, 45 | classNames = listOfNotNull( 46 | "horizontalLayout", 47 | "horizontallyCentered".takeIf { centerOnMainAxis } 48 | ) 49 | ) 50 | } 51 | 52 | @Composable 53 | fun StyledDiv( 54 | styles: StyleScope.() -> Unit = {}, 55 | content: @Composable () -> Unit, 56 | classNames: List 57 | ) { 58 | Div( 59 | attrs = { 60 | style(styles) 61 | classes(*classNames.toTypedArray()) 62 | }, 63 | content = { 64 | content() 65 | } 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /demo/src/jsMain/kotlin/com/tunjid/mutator/demo/editor/Markdown.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | //@file:JsModule("react-markdown") 18 | //@file:JsNonModule 19 | // 20 | //import react.FC 21 | //import react.Props 22 | // 23 | //@JsName("ReactMarkdown") 24 | //external val reactMarkdown: FC 25 | // 26 | //external interface ReactMarkdownProps : Props { 27 | // var children: String 28 | //} -------------------------------------------------------------------------------- /demo/src/jsMain/kotlin/com/tunjid/mutator/demo/editor/Text.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.editor 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.DisposableEffect 21 | import androidx.compose.runtime.key 22 | import org.jetbrains.compose.web.css.Color 23 | import org.jetbrains.compose.web.css.backgroundColor 24 | import org.jetbrains.compose.web.css.fontSize 25 | import org.jetbrains.compose.web.css.pt 26 | import org.jetbrains.compose.web.dom.Code 27 | import org.jetbrains.compose.web.dom.Div 28 | import org.jetbrains.compose.web.dom.ElementScope 29 | import org.jetbrains.compose.web.dom.P 30 | import org.jetbrains.compose.web.dom.Pre 31 | import org.jetbrains.compose.web.dom.Text 32 | import org.w3c.dom.HTMLElement 33 | import react.FC 34 | import react.Props 35 | import react.ReactElement 36 | import react.create 37 | import react.dom.client.createRoot 38 | import web.dom.Element 39 | 40 | 41 | @Composable 42 | actual fun Paragraph(text: String) = P( 43 | content = { 44 | Text(value = text) 45 | } 46 | ) 47 | 48 | @Composable 49 | actual fun Markdown(content: String) = Div( 50 | attrs = { 51 | classes("markdown") 52 | }, 53 | content = { 54 | UseReactEffect( 55 | content, 56 | reactMarkdown.create { 57 | children = content 58 | } 59 | ) 60 | } 61 | ) 62 | 63 | @Composable 64 | actual fun CallToAction( 65 | text: String, 66 | centerText: Boolean, 67 | ) = Div( 68 | attrs = { 69 | classes( 70 | listOfNotNull( 71 | "cta", 72 | "horizontallyCentered", 73 | "centerText".takeIf { centerText }, 74 | ) 75 | ) 76 | } 77 | ) { 78 | UseReactEffect( 79 | text, 80 | reactMarkdown.create { 81 | children = text 82 | } 83 | ) 84 | } 85 | 86 | @Composable 87 | actual fun CodeBlock(content: String) = key(content) { 88 | Pre { 89 | Code({ 90 | classes("language-kotlin", "hljs") 91 | style { 92 | property("font-family", "'JetBrains Mono', monospace") 93 | property("tab-size", 4) 94 | fontSize(10.pt) 95 | backgroundColor(Color("transparent")) 96 | } 97 | }) { 98 | @Suppress("DEPRECATION") 99 | DomSideEffect(content) { 100 | it.setHighlightedCode(content) 101 | } 102 | } 103 | } 104 | } 105 | 106 | @Composable 107 | private fun ElementScope.UseReactEffect( 108 | key: Any?, 109 | content: ReactElement<*> 110 | ) { 111 | DisposableEffect(key1 = key) { 112 | val root = createRoot(scopeElement as Element) 113 | root.render(content) 114 | onDispose { root.unmount() } 115 | } 116 | } 117 | 118 | @JsName("ReactMarkdown") 119 | @JsModule("react-markdown") 120 | @JsNonModule 121 | external val reactMarkdown: FC 122 | 123 | external interface ReactMarkdownProps : Props { 124 | var children: String 125 | } 126 | 127 | @JsName("hljs") 128 | @JsModule("highlight.js") 129 | @JsNonModule 130 | external class HighlightJs { 131 | companion object { 132 | fun highlightElement(block: HTMLElement) 133 | } 134 | } 135 | 136 | private fun HTMLElement.setHighlightedCode(code: String) { 137 | innerText = code 138 | HighlightJs.highlightElement(this) 139 | } 140 | -------------------------------------------------------------------------------- /demo/src/jsMain/kotlin/com/tunjid/mutator/demo/snails/Snail.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.snails 18 | 19 | import androidx.compose.runtime.Composable 20 | import com.tunjid.mutator.demo.Color 21 | import com.tunjid.mutator.demo.Speed 22 | import com.tunjid.mutator.demo.editor.HorizontalLayout 23 | import com.tunjid.mutator.demo.editor.StyledDiv 24 | import com.tunjid.mutator.demo.hex 25 | import com.tunjid.mutator.demo.rgb 26 | import org.jetbrains.compose.web.attributes.InputType 27 | import org.jetbrains.compose.web.attributes.max 28 | import org.jetbrains.compose.web.attributes.min 29 | import org.jetbrains.compose.web.css.backgroundColor 30 | import org.jetbrains.compose.web.css.color 31 | import org.jetbrains.compose.web.css.percent 32 | import org.jetbrains.compose.web.css.width 33 | import org.jetbrains.compose.web.dom.Div 34 | import org.jetbrains.compose.web.dom.Input 35 | import org.jetbrains.compose.web.dom.P 36 | import org.jetbrains.compose.web.dom.Text 37 | 38 | @Composable 39 | actual fun Illustration( 40 | content: @Composable () -> Unit 41 | ) { 42 | StyledDiv( 43 | content = content, 44 | classNames = listOf("illustration", "sectionLayout"), 45 | ) 46 | } 47 | 48 | @Composable 49 | actual fun SnailCard( 50 | color: Color, 51 | content: @Composable () -> Unit 52 | ) { 53 | StyledDiv( 54 | content = content, 55 | classNames = listOf("card"), 56 | styles = { 57 | backgroundColor(color.rgb()) 58 | } 59 | ) 60 | } 61 | 62 | @Composable 63 | actual fun SnailText( 64 | color: Color, 65 | text: String 66 | ) = P( 67 | attrs = { 68 | style { color(color.rgb()) } 69 | }, 70 | content = { 71 | Text(value = text) 72 | } 73 | ) 74 | 75 | @Composable 76 | actual fun Snail( 77 | progress: Float, 78 | speed: Speed, 79 | color: Color, 80 | onValueChange: (Float) -> Unit 81 | ) { 82 | Input( 83 | type = InputType.Range, 84 | attrs = { 85 | classes("snail", "horizontallyCentered") 86 | style { 87 | property("--snailColor", color.hex()) 88 | } 89 | min("0") 90 | max("100") 91 | value(progress) 92 | onInput { onValueChange(it.value?.toFloat() ?: 0f) } 93 | } 94 | ) 95 | } 96 | 97 | @Composable 98 | actual fun ColorSwatch( 99 | colors: List, 100 | onColorClicked: (Int) -> Unit 101 | ) { 102 | HorizontalLayout(centerOnMainAxis = true) { 103 | colors.forEachIndexed { index, color -> 104 | Div( 105 | attrs = { 106 | classes("colorSwatch") 107 | style { 108 | backgroundColor( 109 | color.rgb() 110 | ) 111 | } 112 | onClick { onColorClicked(index) } 113 | }, 114 | content = { 115 | } 116 | ) 117 | } 118 | } 119 | } 120 | 121 | @Composable 122 | actual fun ToggleButton( 123 | progress: Float, 124 | onClicked: () -> Unit 125 | ) = Div( 126 | attrs = { 127 | classes("progress-btn", "horizontallyCentered") 128 | onClick { onClicked() } 129 | }, 130 | content = { 131 | Div( 132 | attrs = { 133 | classes("btn") 134 | }, 135 | content = { 136 | Text("Toggle mode") 137 | } 138 | ) 139 | Div( 140 | attrs = { 141 | classes("progress") 142 | style { width(((progress * 100).toInt() % 100).percent) } 143 | } 144 | ) 145 | } 146 | ) 147 | -------------------------------------------------------------------------------- /demo/src/jsMain/kotlin/com/tunjid/mutator/demo/udfvisualizer/UDFVisualizer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tunjid.mutator.demo.udfvisualizer 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.collectAsState 21 | import androidx.compose.runtime.getValue 22 | import com.tunjid.mutator.demo.hex 23 | import org.jetbrains.compose.web.css.AlignItems 24 | import org.jetbrains.compose.web.css.DisplayStyle 25 | import org.jetbrains.compose.web.css.FlexDirection 26 | import org.jetbrains.compose.web.css.JustifyContent 27 | import org.jetbrains.compose.web.css.Position 28 | import org.jetbrains.compose.web.css.alignItems 29 | import org.jetbrains.compose.web.css.background 30 | import org.jetbrains.compose.web.css.borderRadius 31 | import org.jetbrains.compose.web.css.bottom 32 | import org.jetbrains.compose.web.css.color 33 | import org.jetbrains.compose.web.css.display 34 | import org.jetbrains.compose.web.css.flexDirection 35 | import org.jetbrains.compose.web.css.fontSize 36 | import org.jetbrains.compose.web.css.height 37 | import org.jetbrains.compose.web.css.justifyContent 38 | import org.jetbrains.compose.web.css.left 39 | import org.jetbrains.compose.web.css.margin 40 | import org.jetbrains.compose.web.css.minWidth 41 | import org.jetbrains.compose.web.css.padding 42 | import org.jetbrains.compose.web.css.paddingBottom 43 | import org.jetbrains.compose.web.css.paddingLeft 44 | import org.jetbrains.compose.web.css.paddingRight 45 | import org.jetbrains.compose.web.css.paddingTop 46 | import org.jetbrains.compose.web.css.percent 47 | import org.jetbrains.compose.web.css.position 48 | import org.jetbrains.compose.web.css.px 49 | import org.jetbrains.compose.web.css.textAlign 50 | import org.jetbrains.compose.web.css.top 51 | import org.jetbrains.compose.web.css.width 52 | import org.jetbrains.compose.web.dom.Div 53 | import org.jetbrains.compose.web.dom.P 54 | import org.jetbrains.compose.web.dom.Text 55 | import org.jetbrains.compose.web.css.Color as CSSColor 56 | 57 | private val marbleSize = 22.px 58 | 59 | @Composable 60 | actual fun UDFVisualizer(stateHolder: UDFVisualizerStateHolder) { 61 | val state by stateHolder.state.collectAsState() 62 | 63 | Div( 64 | { 65 | style { 66 | display(DisplayStyle.Flex) 67 | flexDirection(FlexDirection.Column) 68 | alignItems("center") 69 | width(100.percent) 70 | padding(0.px) 71 | margin(0.px) 72 | } 73 | }, 74 | { 75 | Label("State Holder") 76 | Marbles(state.marbles) 77 | Label("UI") 78 | P { Text("UDF Visualizer") } 79 | } 80 | ) 81 | } 82 | 83 | @Composable 84 | private fun Label(text: String) = P( 85 | attrs = { 86 | style { 87 | width(200.px) 88 | borderRadius(10.px) 89 | paddingLeft(16.px) 90 | paddingTop(8.px) 91 | paddingRight(16.px) 92 | paddingBottom(8.px) 93 | textAlign("center") 94 | background("#052F41") 95 | color(CSSColor("#FFFFFF")) 96 | margin(0.px) 97 | property("z-index", 1) 98 | } 99 | }, 100 | content = { 101 | Text(value = text) 102 | } 103 | ) 104 | 105 | @Composable 106 | private fun Marbles(marbles: List) = Div( 107 | attrs = { 108 | style { 109 | width(200.px) 110 | height(120.px) 111 | position(Position.Relative) 112 | padding(0.px) 113 | margin(0.px) 114 | } 115 | }, 116 | content = { 117 | marbles.forEach { marble -> 118 | Marble(marble) 119 | } 120 | } 121 | ) 122 | 123 | @Composable 124 | private fun Marble(marble: Marble) = Div( 125 | attrs = { 126 | style { 127 | property("z-index", 0) 128 | minWidth(marbleSize) 129 | height(marbleSize) 130 | borderRadius(marbleSize) 131 | position(Position.Absolute) 132 | color(CSSColor("#FFFFFF")) 133 | textAlign("center") 134 | fontSize(13.px) 135 | padding(0.px) 136 | margin(0.px) 137 | display(DisplayStyle.Flex) 138 | alignItems(AlignItems.Center) 139 | justifyContent(JustifyContent.Center) 140 | background( 141 | when (marble) { 142 | is Marble.Event -> when (val core = marble.metadata) { 143 | Marble.Metadata.Nothing -> marble.hexColor() 144 | is Marble.Metadata.Text -> marble.hexColor() 145 | is Marble.Metadata.Tint -> core.color.hex() 146 | } 147 | 148 | is Marble.State -> marble.color.hex() 149 | } 150 | ) 151 | left( 152 | when (marble) { 153 | is Marble.Event -> 80.percent 154 | is Marble.State -> 20.percent 155 | } 156 | ) 157 | when (marble) { 158 | is Marble.Event -> bottom(marble.percentage.percent) 159 | is Marble.State -> top(marble.percentage.percent) 160 | } 161 | } 162 | }, 163 | content = { 164 | when (val core = marble.metadata) { 165 | Marble.Metadata.Nothing, 166 | is Marble.Metadata.Tint -> Unit 167 | 168 | is Marble.Metadata.Text -> Text(value = core.text) 169 | } 170 | } 171 | ) 172 | 173 | private fun Marble.color() = when (this) { 174 | is Marble.Event -> eventColor 175 | is Marble.State -> stateColor 176 | } 177 | 178 | private fun Marble.hexColor() = color().hex() 179 | -------------------------------------------------------------------------------- /demo/src/jsMain/kotlin/main.js.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import com.tunjid.mutator.demo.App 18 | import org.jetbrains.compose.web.renderComposable 19 | 20 | fun main() { 21 | renderComposable(rootElementId = "container") { 22 | App() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/src/jsMain/resources/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/jsMain/resources/android-chrome-192x192.png -------------------------------------------------------------------------------- /demo/src/jsMain/resources/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/jsMain/resources/android-chrome-512x512.png -------------------------------------------------------------------------------- /demo/src/jsMain/resources/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/jsMain/resources/apple-touch-icon.png -------------------------------------------------------------------------------- /demo/src/jsMain/resources/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/jsMain/resources/favicon-16x16.png -------------------------------------------------------------------------------- /demo/src/jsMain/resources/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/jsMain/resources/favicon-32x32.png -------------------------------------------------------------------------------- /demo/src/jsMain/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/demo/src/jsMain/resources/favicon.ico -------------------------------------------------------------------------------- /demo/src/jsMain/resources/hljs.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /*! 18 | * StackOverflow.com light style 19 | * 20 | * @stackoverflow/stacks v0.56.0 21 | * https://github.com/StackExchange/Stacks 22 | */ 23 | .hljs { 24 | display: block; 25 | overflow-x: auto; 26 | padding: .5em; 27 | color: #2f3337; 28 | background: #f6f6f6 29 | } 30 | 31 | .hljs-comment { 32 | color: #656e77 33 | } 34 | 35 | .hljs-attr, .hljs-doctag, .hljs-keyword, .hljs-meta, .hljs-meta-keyword, .hljs-section, .hljs-selector-class, .hljs-selector-pseudo, .hljs-selector-tag { 36 | color: #015692; 37 | font-weight: bold; 38 | } 39 | 40 | .hljs-attribute { 41 | color: #803378 42 | } 43 | 44 | .hljs-built_in, .hljs-literal, .hljs-name, .hljs-number, .hljs-quote, .hljs-selector-id, .hljs-template-tag, .hljs-title, .hljs-type { 45 | color: #b75501 46 | } 47 | 48 | .hljs-title { 49 | font-style: italic; 50 | } 51 | 52 | .hljs-link, .hljs-meta-string, .hljs-regexp, .hljs-selector-attr, .hljs-string, .hljs-symbol, .hljs-template-variable, .hljs-variable { 53 | color: #54790d 54 | } 55 | 56 | .hljs-bullet, .hljs-code { 57 | color: #535a60 58 | } 59 | 60 | .hljs-deletion { 61 | color: #c02d2e 62 | } 63 | 64 | .hljs-addition { 65 | color: #2f6f44 66 | } 67 | 68 | .hljs-emphasis { 69 | font-style: italic 70 | } 71 | 72 | .hljs-strong { 73 | font-weight: 700 74 | } -------------------------------------------------------------------------------- /demo/src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | State production with unidirectional data flow and Kotlin Flows 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 45 | 46 | 47 |
48 |
49 |
50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/src/jsMain/resources/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "State production with flows": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } -------------------------------------------------------------------------------- /demo/src/jsMain/resources/snail.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 59 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /demo/src/jsMain/resources/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #root { 18 | height: 100%; 19 | width: 100%; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | } 24 | 25 | #container { 26 | max-width: 720px; 27 | } 28 | 29 | pre { 30 | width: 100%; 31 | overflow: auto; 32 | height: auto; 33 | white-space: pre-wrap; 34 | word-wrap: break-word; 35 | text-align: start; 36 | } 37 | 38 | .markdown { 39 | width: 100%; 40 | } 41 | 42 | .markdown h1 { 43 | font-family: "Muli", sans-serif; 44 | font-size: 48px; 45 | letter-spacing: 0px; 46 | font-weight: normal; 47 | line-height: 90%; 48 | } 49 | 50 | .markdown h2 { 51 | font-family: "Muli", sans-serif; 52 | font-size: 34px; 53 | letter-spacing: 0px; 54 | font-weight: lighter; 55 | line-height: 90%; 56 | } 57 | 58 | .markdown h3 { 59 | font-family: "Muli", sans-serif; 60 | font-size: 24px; 61 | letter-spacing: 0px; 62 | font-weight: lighter; 63 | line-height: 80%; 64 | } 65 | 66 | .markdown p, li, ul { 67 | font-family: "Muli", sans-serif; 68 | font-size: 18px; 69 | letter-spacing: 0px; 70 | font-weight: normal; 71 | line-height: 125%; 72 | } 73 | 74 | 75 | .sectionLayout { 76 | display: flex; 77 | flex-direction: column; 78 | align-items: center; 79 | } 80 | 81 | .verticalLayout { 82 | display: flex; 83 | flex-direction: column; 84 | align-items: start; 85 | } 86 | 87 | .horizontalLayout { 88 | display: flex; 89 | flex-direction: row; 90 | align-items: center; 91 | } 92 | 93 | .centerText { 94 | text-align: center; 95 | } 96 | 97 | .colorSwatch { 98 | width: 25px; 99 | height: 25px; 100 | border-radius: 50%; 101 | margin: 16px; 102 | } 103 | 104 | .cta { 105 | width: 80%; 106 | margin: 16px; 107 | padding: 8px; 108 | border-radius: 8px; 109 | color: #00579B; 110 | background-color: #E1F4FE; 111 | } 112 | 113 | .illustration { 114 | width: 80%; 115 | padding: 16px; 116 | margin: 20px; 117 | font-family: "RobotoDraft", "Roboto", sans-serif; 118 | } 119 | 120 | .card { 121 | box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.2); 122 | transition: 0.3s; 123 | width: 100%; 124 | padding: 16px; 125 | margin: 20px; 126 | } 127 | 128 | .card:hover { 129 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); 130 | } 131 | 132 | .snail { 133 | --snailColor: rgba(0, 0, 0, 0.2); 134 | -webkit-appearance: none; 135 | width: 80%; 136 | height: 15px; 137 | border-radius: 5px; 138 | background: #d3d3d3; 139 | outline: none; 140 | -webkit-transition: .2s; 141 | transition: opacity .2s; 142 | } 143 | 144 | .snail:hover { 145 | opacity: 1; 146 | } 147 | 148 | .snail::-webkit-slider-thumb { 149 | -webkit-appearance: none; 150 | appearance: none; 151 | width: 28px; 152 | height: 28px; 153 | border-radius: 50%; 154 | background: var(--snailColor); 155 | background-image: url('snail.svg'); 156 | background-size: contain; 157 | background-position: center center; 158 | background-repeat: no-repeat; 159 | background-size: 20px; 160 | cursor: pointer; 161 | } 162 | 163 | .snail::-moz-range-thumb { 164 | width: 28px; 165 | height: 28px; 166 | border-radius: 50%; 167 | background: var(--snailColor); 168 | background-image: url('snail.svg'); 169 | background-size: contain; 170 | background-position: center center; 171 | background-repeat: no-repeat; 172 | background-size: 20px; 173 | cursor: pointer; 174 | } 175 | 176 | .horizontallyCentered { 177 | margin: 0 auto; 178 | } 179 | 180 | .progress-btn { 181 | position: relative; 182 | width: 150px; 183 | height: 50px; 184 | display: inline-block; 185 | background: #A7C7E7; 186 | font-family: "RobotoDraft", "Roboto", sans-serif; 187 | color: #fff; 188 | font-weight: normal; 189 | font-size: 20px; 190 | } 191 | 192 | .progress-btn:not(.active) { 193 | cursor: pointer; 194 | } 195 | 196 | .progress-btn .btn { 197 | position: absolute; 198 | left: 0; 199 | top: 0; 200 | right: 0; 201 | bottom: 0; 202 | line-height: 50px; 203 | text-align: center; 204 | z-index: 5; 205 | } 206 | 207 | .progress-btn .progress { 208 | position: absolute; 209 | left: 0; 210 | top: 0; 211 | right: 0; 212 | bottom: 0; 213 | width: 50%; 214 | z-index: 10; 215 | background: #2a639ccc; 216 | } 217 | 218 | 219 | -------------------------------------------------------------------------------- /docs/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | 3 | -------------------------------------------------------------------------------- /docs/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/docs/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/docs/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/docs/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/demo.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Determine if an object is a Buffer 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */ 7 | 8 | /** 9 | * @license React 10 | * react-dom.production.min.js 11 | * 12 | * Copyright (c) Facebook, Inc. and its affiliates. 13 | * 14 | * This source code is licensed under the MIT license found in the 15 | * LICENSE file in the root directory of this source tree. 16 | */ 17 | 18 | /** 19 | * @license React 20 | * react.production.min.js 21 | * 22 | * Copyright (c) Facebook, Inc. and its affiliates. 23 | * 24 | * This source code is licensed under the MIT license found in the 25 | * LICENSE file in the root directory of this source tree. 26 | */ 27 | 28 | /** 29 | * @license React 30 | * scheduler.production.min.js 31 | * 32 | * Copyright (c) Facebook, Inc. and its affiliates. 33 | * 34 | * This source code is licensed under the MIT license found in the 35 | * LICENSE file in the root directory of this source tree. 36 | */ 37 | 38 | /** @license React v17.0.2 39 | * react-is.production.min.js 40 | * 41 | * Copyright (c) Facebook, Inc. and its affiliates. 42 | * 43 | * This source code is licensed under the MIT license found in the 44 | * LICENSE file in the root directory of this source tree. 45 | */ 46 | -------------------------------------------------------------------------------- /docs/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/docs/favicon-16x16.png -------------------------------------------------------------------------------- /docs/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/docs/favicon-32x32.png -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/docs/favicon.ico -------------------------------------------------------------------------------- /docs/hljs.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /*! 18 | * StackOverflow.com light style 19 | * 20 | * @stackoverflow/stacks v0.56.0 21 | * https://github.com/StackExchange/Stacks 22 | */ 23 | .hljs { 24 | display: block; 25 | overflow-x: auto; 26 | padding: .5em; 27 | color: #2f3337; 28 | background: #f6f6f6 29 | } 30 | 31 | .hljs-comment { 32 | color: #656e77 33 | } 34 | 35 | .hljs-attr, .hljs-doctag, .hljs-keyword, .hljs-meta, .hljs-meta-keyword, .hljs-section, .hljs-selector-class, .hljs-selector-pseudo, .hljs-selector-tag { 36 | color: #015692; 37 | font-weight: bold; 38 | } 39 | 40 | .hljs-attribute { 41 | color: #803378 42 | } 43 | 44 | .hljs-built_in, .hljs-literal, .hljs-name, .hljs-number, .hljs-quote, .hljs-selector-id, .hljs-template-tag, .hljs-title, .hljs-type { 45 | color: #b75501 46 | } 47 | 48 | .hljs-title { 49 | font-style: italic; 50 | } 51 | 52 | .hljs-link, .hljs-meta-string, .hljs-regexp, .hljs-selector-attr, .hljs-string, .hljs-symbol, .hljs-template-variable, .hljs-variable { 53 | color: #54790d 54 | } 55 | 56 | .hljs-bullet, .hljs-code { 57 | color: #535a60 58 | } 59 | 60 | .hljs-deletion { 61 | color: #c02d2e 62 | } 63 | 64 | .hljs-addition { 65 | color: #2f6f44 66 | } 67 | 68 | .hljs-emphasis { 69 | font-style: italic 70 | } 71 | 72 | .hljs-strong { 73 | font-weight: 700 74 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | State production with unidirectional data flow and Kotlin Flows 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 45 | 46 | 47 |
48 |
49 |
50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "State production with flows": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } -------------------------------------------------------------------------------- /docs/snail.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 59 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #root { 18 | height: 100%; 19 | width: 100%; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | } 24 | 25 | #container { 26 | max-width: 720px; 27 | } 28 | 29 | pre { 30 | width: 100%; 31 | overflow: auto; 32 | height: auto; 33 | white-space: pre-wrap; 34 | word-wrap: break-word; 35 | text-align: start; 36 | } 37 | 38 | .markdown { 39 | width: 100%; 40 | } 41 | 42 | .markdown h1 { 43 | font-family: "Muli", sans-serif; 44 | font-size: 48px; 45 | letter-spacing: 0px; 46 | font-weight: normal; 47 | line-height: 90%; 48 | } 49 | 50 | .markdown h2 { 51 | font-family: "Muli", sans-serif; 52 | font-size: 34px; 53 | letter-spacing: 0px; 54 | font-weight: lighter; 55 | line-height: 90%; 56 | } 57 | 58 | .markdown h3 { 59 | font-family: "Muli", sans-serif; 60 | font-size: 24px; 61 | letter-spacing: 0px; 62 | font-weight: lighter; 63 | line-height: 80%; 64 | } 65 | 66 | .markdown p, li, ul { 67 | font-family: "Muli", sans-serif; 68 | font-size: 18px; 69 | letter-spacing: 0px; 70 | font-weight: normal; 71 | line-height: 125%; 72 | } 73 | 74 | 75 | .sectionLayout { 76 | display: flex; 77 | flex-direction: column; 78 | align-items: center; 79 | } 80 | 81 | .verticalLayout { 82 | display: flex; 83 | flex-direction: column; 84 | align-items: start; 85 | } 86 | 87 | .horizontalLayout { 88 | display: flex; 89 | flex-direction: row; 90 | align-items: center; 91 | } 92 | 93 | .centerText { 94 | text-align: center; 95 | } 96 | 97 | .colorSwatch { 98 | width: 25px; 99 | height: 25px; 100 | border-radius: 50%; 101 | margin: 16px; 102 | } 103 | 104 | .cta { 105 | width: 80%; 106 | margin: 16px; 107 | padding: 8px; 108 | border-radius: 8px; 109 | color: #00579B; 110 | background-color: #E1F4FE; 111 | } 112 | 113 | .illustration { 114 | width: 80%; 115 | padding: 16px; 116 | margin: 20px; 117 | font-family: "RobotoDraft", "Roboto", sans-serif; 118 | } 119 | 120 | .card { 121 | box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.2); 122 | transition: 0.3s; 123 | width: 100%; 124 | padding: 16px; 125 | margin: 20px; 126 | } 127 | 128 | .card:hover { 129 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); 130 | } 131 | 132 | .snail { 133 | --snailColor: rgba(0, 0, 0, 0.2); 134 | -webkit-appearance: none; 135 | width: 80%; 136 | height: 15px; 137 | border-radius: 5px; 138 | background: #d3d3d3; 139 | outline: none; 140 | -webkit-transition: .2s; 141 | transition: opacity .2s; 142 | } 143 | 144 | .snail:hover { 145 | opacity: 1; 146 | } 147 | 148 | .snail::-webkit-slider-thumb { 149 | -webkit-appearance: none; 150 | appearance: none; 151 | width: 28px; 152 | height: 28px; 153 | border-radius: 50%; 154 | background: var(--snailColor); 155 | background-image: url('snail.svg'); 156 | background-size: contain; 157 | background-position: center center; 158 | background-repeat: no-repeat; 159 | background-size: 20px; 160 | cursor: pointer; 161 | } 162 | 163 | .snail::-moz-range-thumb { 164 | width: 28px; 165 | height: 28px; 166 | border-radius: 50%; 167 | background: var(--snailColor); 168 | background-image: url('snail.svg'); 169 | background-size: contain; 170 | background-position: center center; 171 | background-repeat: no-repeat; 172 | background-size: 20px; 173 | cursor: pointer; 174 | } 175 | 176 | .horizontallyCentered { 177 | margin: 0 auto; 178 | } 179 | 180 | .progress-btn { 181 | position: relative; 182 | width: 150px; 183 | height: 50px; 184 | display: inline-block; 185 | background: #A7C7E7; 186 | font-family: "RobotoDraft", "Roboto", sans-serif; 187 | color: #fff; 188 | font-weight: normal; 189 | font-size: 20px; 190 | } 191 | 192 | .progress-btn:not(.active) { 193 | cursor: pointer; 194 | } 195 | 196 | .progress-btn .btn { 197 | position: absolute; 198 | left: 0; 199 | top: 0; 200 | right: 0; 201 | bottom: 0; 202 | line-height: 50px; 203 | text-align: center; 204 | z-index: 5; 205 | } 206 | 207 | .progress-btn .progress { 208 | position: absolute; 209 | left: 0; 210 | top: 0; 211 | right: 0; 212 | bottom: 0; 213 | width: 50%; 214 | z-index: 10; 215 | background: #2a639ccc; 216 | } 217 | 218 | 219 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 17 | # When configured, Gradle will run in incubating parallel mode. 18 | # This option should only be used with decoupled projects. More details, visit 19 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 20 | # org.gradle.parallel=true 21 | # AndroidX package structure to make it clearer which packages are bundled with the 22 | # Android operating system, and which are packaged with your app"s APK 23 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 24 | android.useAndroidX=true 25 | # Automatically convert third-party libraries to use AndroidX 26 | android.enableJetifier=true 27 | # Kotlin code style for this project: "official" or "obsolete": 28 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | androidGradlePlugin = "8.0.2" 3 | androidxCompose = "1.4.3" 4 | androidxComposeCompiler = "1.4.6" 5 | dokka = "1.8.10" 6 | jetbrainsCompose = "1.4.1" 7 | junit4 = "4.13.2" 8 | kotlin = "1.8.20" 9 | kotlinWrappers = "18.2.0-pre.584" 10 | kotlinxCoroutines = "1.7.3" 11 | richtext = "0.13.0" 12 | turbine = "0.7.0" 13 | 14 | [libraries] 15 | android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } 16 | junit4 = { group = "junit", name = "junit", version.ref = "junit4" } 17 | androidx-compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "androidxCompose" } 18 | androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidxCompose" } 19 | androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "androidxCompose" } 20 | androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidxCompose" } 21 | androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "androidxCompose" } 22 | androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "androidxCompose" } 23 | androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "androidxCompose" } 24 | #androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidxCompose" } 25 | androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling-preview-desktop", version.ref = "androidxCompose" } 26 | androidx-compose-ui-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidxCompose" } 27 | androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util", version.ref = "androidxCompose" } 28 | cashapp-turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" } 29 | jetbrains-compose-animation = { group = "org.jetbrains.compose.animation", name = "animation", version.ref = "jetbrainsCompose" } 30 | jetbrains-compose-foundation = { group = "org.jetbrains.compose.foundation", name = "foundation", version.ref = "jetbrainsCompose" } 31 | jetbrains-compose-foundation-layout = { group = "org.jetbrains.compose.foundation", name = "foundation-layout", version.ref = "jetbrainsCompose" } 32 | jetbrains-compose-gradlePlugin = { group = "org.jetbrains.compose", name = "compose-gradle-plugin", version.ref = "jetbrainsCompose" } 33 | jetbrains-compose-material = { group = "org.jetbrains.compose.material", name = "material", version.ref = "jetbrainsCompose" } 34 | jetbrains-compose-runtime = { group = "org.jetbrains.compose.runtime", name = "runtime", version.ref = "jetbrainsCompose" } 35 | jetbrains-compose-ui-test = { group = "org.jetbrains.compose.ui", name = "ui-test-junit4", version.ref = "jetbrainsCompose" } 36 | jetbrains-compose-ui-testManifest = { group = "org.jetbrains.compose.ui", name = "ui-test-manifest", version.ref = "jetbrainsCompose" } 37 | #jetbrains-compose-ui-tooling = { group = "org.jetbrains.compose.ui", name = "ui-tooling", version.ref = "jetbrainsCompose" } 38 | jetbrains-compose-ui-tooling = { group = "org.jetbrains.compose.ui", name = "ui-tooling-preview-desktop", version.ref = "jetbrainsCompose" } 39 | jetbrains-compose-ui-ui = { group = "org.jetbrains.compose.ui", name = "ui", version.ref = "jetbrainsCompose" } 40 | jetbrains-compose-ui-util = { group = "org.jetbrains.compose.ui", name = "ui-util", version.ref = "jetbrainsCompose" } 41 | dokka-gradlePlugin = { group = "org.jetbrains.dokka", name = "dokka-gradle-plugin", version.ref = "dokka" } 42 | kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } 43 | kotlin-wrappers-react = { group = "org.jetbrains.kotlin-wrappers", name = "kotlin-react", version.ref = "kotlinWrappers" } 44 | kotlin-wrappers-reactDom = { group = "org.jetbrains.kotlin-wrappers", name = "kotlin-react-dom", version.ref = "kotlinWrappers" } 45 | kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } 46 | kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } 47 | kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } 48 | richtext-commonmark= { group = "com.halilibo.compose-richtext", name = "richtext-commonmark", version.ref = "richtext" } 49 | richtext-material= { group = "com.halilibo.compose-richtext", name = "richtext-ui-material", version.ref = "richtext" } 50 | 51 | [plugins] 52 | android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } 53 | android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } 54 | android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } 55 | androidx-compose = { id = "com.android.test", version.ref = "androidGradlePlugin" } 56 | jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "jetbrainsCompose" } 57 | jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } 58 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 59 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 60 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 61 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 62 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/Mutator/0c94b699a3324f69488caa3847c59c26e314b421/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | distributionBase=GRADLE_USER_HOME 17 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip 18 | distributionPath=wrapper/dists 19 | zipStorePath=wrapper/dists 20 | zipStoreBase=GRADLE_USER_HOME 21 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2021 Google LLC 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 UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /libraryVersion.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | groupId=com.tunjid.mutator 17 | core_version=1.1.0 18 | coroutines_version=1.1.0 -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | pluginManagement { 17 | includeBuild("build-logic") 18 | repositories { 19 | gradlePluginPortal() 20 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 21 | google() 22 | } 23 | } 24 | dependencyResolutionManagement { 25 | // Workaround for KT-51379 26 | repositoriesMode.set(RepositoriesMode.PREFER_PROJECT) 27 | repositories { 28 | google() 29 | mavenCentral() 30 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 31 | } 32 | } 33 | rootProject.name = "Mutator" 34 | include( 35 | ":core", 36 | ":coroutines", 37 | ":demo", 38 | ) 39 | 40 | --------------------------------------------------------------------------------