├── .github └── workflows │ └── tests.yml ├── .idea └── copyright │ ├── Apache.xml │ └── profiles_settings.xml ├── LICENSE.txt ├── build-logic ├── convention │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ ├── android-app-library-convention.kt │ │ ├── android-application-convention.gradle.kts │ │ ├── android-library-convention.gradle.kts │ │ ├── kotlin-jvm-convention.gradle.kts │ │ ├── kotlin-jvm-convention.kt │ │ ├── kotlin-library-convention.gradle.kts │ │ └── publishing-library-convention.gradle.kts ├── settings.gradle.kts └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── library ├── compose-threepane │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── tunjid │ │ └── treenav │ │ └── compose │ │ └── threepane │ │ ├── ThreePane.kt │ │ ├── ThreePaneMovableElementSharedTransitionScope.kt │ │ ├── ThreePaneSharedTransitionScope.kt │ │ └── transforms │ │ ├── BackPreviewTransform.kt │ │ ├── MovableSharedElementTransform.kt │ │ └── ThreePaneAdaptiveTransform.kt ├── compose │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── com │ │ │ └── tunjid │ │ │ └── treenav │ │ │ └── compose │ │ │ └── navigation3 │ │ │ └── decorators │ │ │ └── ViewModelStoreNavEntryDecorator.android.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── tunjid │ │ │ └── treenav │ │ │ └── compose │ │ │ ├── DecoratedNavEntryMultiPaneDisplayScope.kt │ │ │ ├── Defaults.kt │ │ │ ├── MultiPaneDisplay.kt │ │ │ ├── MultiPaneDisplayState.kt │ │ │ ├── PaneEntry.kt │ │ │ ├── PaneMovableElementSharedTransitionScope.kt │ │ │ ├── PaneScope.kt │ │ │ ├── PaneSharedTransitionScope.kt │ │ │ ├── PanedNavigationState.kt │ │ │ ├── SlotBasedPanedNavigationState.kt │ │ │ ├── StackNavExt.kt │ │ │ ├── lifecycle │ │ │ ├── DestinationLifecycleOwner.kt │ │ │ └── DestinationViewModelStoreCreator.kt │ │ │ ├── moveablesharedelement │ │ │ ├── MovableSharedElementState.kt │ │ │ └── MovableSharedElements.kt │ │ │ ├── navigation3 │ │ │ ├── DecoratedNavEntryProvider.kt │ │ │ ├── NavEntry.kt │ │ │ ├── NavEntryDecorator.kt │ │ │ ├── NavEntryWrapper.kt │ │ │ └── decorators │ │ │ │ ├── MovableContentNavEntryDecorator.kt │ │ │ │ ├── SavedStateNavEntryDecorator.kt │ │ │ │ ├── TransitionAwareLifecycleNavEntryDecorator.kt │ │ │ │ ├── Utilities.kt │ │ │ │ └── ViewModelStoreNavEntryDecorator.kt │ │ │ └── transforms │ │ │ ├── PaneModifierTransform.kt │ │ │ └── Transforms.kt │ │ ├── commonTest │ │ └── kotlin │ │ │ └── com │ │ │ └── tunjid │ │ │ └── treenav │ │ │ └── compose │ │ │ ├── SlotBasedAdaptiveNavigationStateTest.kt │ │ │ └── StackNavExtTest.kt │ │ ├── jvmMain │ │ └── kotlin │ │ │ └── com │ │ │ └── tunjid │ │ │ └── treenav │ │ │ └── compose │ │ │ └── navigation3 │ │ │ └── decorators │ │ │ └── ViewModelStoreNavEntryDecorator.jvm.kt │ │ ├── main │ │ └── res │ │ │ └── values │ │ │ └── AndroidManifest.xml │ │ └── nativeMain │ │ └── kotlin │ │ └── com │ │ └── tunjid │ │ └── treenav │ │ └── compose │ │ └── navigation3 │ │ └── decorators │ │ └── ViewModelStoreNavEntryDecorator.native.kt ├── strings │ ├── build.gradle.kts │ └── src │ │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── tunjid │ │ │ └── treenav │ │ │ └── strings │ │ │ ├── Paths.kt │ │ │ ├── Route.kt │ │ │ ├── RouteDelegate.kt │ │ │ ├── RouteMatcher.kt │ │ │ ├── RouteTrie.kt │ │ │ └── TrieNode.kt │ │ └── commonTest │ │ └── kotlin │ │ ├── RouteDelegateTest.kt │ │ ├── RouteMatcherTest.kt │ │ ├── RouteTrieTest.kt │ │ └── TrieNodeTest.kt └── treenav │ ├── build.gradle.kts │ └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── tunjid │ │ └── treenav │ │ ├── MultiStackNav.kt │ │ ├── Node.kt │ │ └── StackNav.kt │ └── commonTest │ └── kotlin │ ├── MultiStackNavTest.kt │ └── StackNavTest.kt ├── libraryVersion.properties ├── readme.md ├── sample ├── android │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── tunjid │ │ │ └── tyler │ │ │ └── MainActivity.kt │ │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml ├── common │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ └── strings.xml │ │ ├── commonMain │ │ ├── composeResources │ │ │ └── drawable │ │ │ │ ├── aisha-1.jpg │ │ │ │ ├── aisha-2.jpg │ │ │ │ ├── aisha-3.jpg │ │ │ │ ├── aisha-4.jpg │ │ │ │ ├── bjorn-1.jpg │ │ │ │ ├── bjorn-2.jpg │ │ │ │ ├── bjorn-3.jpg │ │ │ │ ├── bjorn-4.jpg │ │ │ │ ├── diego-1.jpg │ │ │ │ ├── diego-2.jpg │ │ │ │ ├── diego-3.jpg │ │ │ │ ├── diego-4.jpg │ │ │ │ ├── kenji-1.jpg │ │ │ │ ├── kenji-2.jpg │ │ │ │ ├── kenji-3.jpg │ │ │ │ ├── kenji-4.jpg │ │ │ │ ├── lin-1.jpg │ │ │ │ ├── lin-2.jpg │ │ │ │ ├── lin-3.jpg │ │ │ │ └── lin-4.jpg │ │ └── kotlin │ │ │ └── com │ │ │ └── tunjid │ │ │ └── demo │ │ │ └── common │ │ │ └── ui │ │ │ ├── AppBars.kt │ │ │ ├── DemoApp.kt │ │ │ ├── DragToPop.kt │ │ │ ├── PaneNavigation.kt │ │ │ ├── PaneScaffold.kt │ │ │ ├── PredictiveBack.kt │ │ │ ├── ProfilePhoto.kt │ │ │ ├── Theme.kt │ │ │ ├── avatar │ │ │ ├── AvatarScreen.kt │ │ │ ├── AvatarViewModel.kt │ │ │ └── PaneEntry.kt │ │ │ ├── chat │ │ │ ├── ChatScreen.kt │ │ │ ├── ChatViewModel.kt │ │ │ └── PaneEntry.kt │ │ │ ├── chatrooms │ │ │ ├── ChatRoomsScreen.kt │ │ │ ├── ChatRoomsViewModel.kt │ │ │ └── PaneEntry.kt │ │ │ ├── data │ │ │ ├── AppData.kt │ │ │ └── NavigationRepository.kt │ │ │ ├── me │ │ │ └── PaneEntry.kt │ │ │ └── profile │ │ │ ├── PaneEntry.kt │ │ │ ├── ProfileScreen.kt │ │ │ └── ProfileViewModel.kt │ │ └── iosMain │ │ └── kotlin │ │ └── main.ios.kt └── desktop │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ └── jvmMain │ └── kotlin │ └── com │ └── tunjid │ └── demo │ └── Main.kt └── 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 1.8 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '17' 21 | distribution: 'adopt' 22 | - name: Validate Gradle wrapper 23 | uses: gradle/actions/wrapper-validation@v3 24 | - name: JVM tests 25 | run: ./gradlew jvmTest 26 | -------------------------------------------------------------------------------- /.idea/copyright/Apache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021 Google LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /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 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 18 | 19 | plugins { 20 | `kotlin-dsl` 21 | } 22 | 23 | group = "com.tunjid.treenav.buildlogic" 24 | 25 | java { 26 | sourceCompatibility = JavaVersion.VERSION_17 27 | targetCompatibility = JavaVersion.VERSION_17 28 | } 29 | 30 | kotlin { 31 | compilerOptions { 32 | jvmTarget.set(JvmTarget.JVM_17) 33 | } 34 | } 35 | 36 | dependencies { 37 | implementation(libs.jetbrains.compose.gradlePlugin) 38 | implementation(libs.kotlin.gradlePlugin) 39 | implementation(libs.android.gradlePlugin) 40 | implementation(libs.compose.compiler.plugin) 41 | implementation(libs.dokka.gradlePlugin) 42 | } 43 | -------------------------------------------------------------------------------- /build-logic/convention/src/main/kotlin/android-app-library-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 com.android.build.api.dsl.CommonExtension 18 | import org.gradle.api.JavaVersion 19 | import org.gradle.api.artifacts.VersionCatalogsExtension 20 | 21 | /* 22 | * Copyright 2021 Google LLC 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * https://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | /** 38 | * Sets common values for Android Applications and Libraries 39 | */ 40 | fun org.gradle.api.Project.androidConfiguration( 41 | extension: CommonExtension<*, *, *, *, *, *> 42 | ) = extension.apply { 43 | namespace = "com.tunjid.treenav.${project.name.replace("-", ".")}" 44 | compileSdk = 36 45 | 46 | defaultConfig { 47 | minSdk = 23 48 | } 49 | 50 | compileOptions { 51 | sourceCompatibility = JavaVersion.VERSION_11 52 | targetCompatibility = JavaVersion.VERSION_11 53 | } 54 | configureKotlinJvm() 55 | } 56 | 57 | val org.gradle.api.Project.versionCatalog 58 | get() = extensions.getByType(VersionCatalogsExtension::class.java) 59 | .named("libs") -------------------------------------------------------------------------------- /build-logic/convention/src/main/kotlin/android-application-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 | plugins { 18 | id("com.android.application") 19 | } 20 | 21 | android { 22 | androidConfiguration(this) 23 | 24 | defaultConfig { 25 | targetSdk = 33 26 | } 27 | } -------------------------------------------------------------------------------- /build-logic/convention/src/main/kotlin/android-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 | plugins { 18 | id("com.android.library") 19 | } 20 | 21 | android { 22 | androidConfiguration(this) 23 | 24 | sourceSets { 25 | named("main") { 26 | // Pull Android manifest from src/androidMain in multiplatform dirs 27 | if (file("src/androidMain").exists()) { 28 | manifest.srcFile("src/androidMain/AndroidManifest.xml") 29 | res.srcDirs("src/androidMain/res") 30 | } 31 | } 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 | /* 18 | * Copyright 2021 Google LLC 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * https://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | 33 | plugins { 34 | kotlin("multiplatform") 35 | } 36 | 37 | kotlin { 38 | configureKotlinJvm() 39 | } -------------------------------------------------------------------------------- /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.JavaVersion 18 | import org.gradle.api.plugins.JavaPluginExtension 19 | import org.gradle.kotlin.dsl.configure 20 | import org.gradle.kotlin.dsl.withType 21 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 22 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 23 | 24 | /* 25 | * Copyright 2021 Google LLC 26 | * 27 | * Licensed under the Apache License, Version 2.0 (the "License"); 28 | * you may not use this file except in compliance with the License. 29 | * You may obtain a copy of the License at 30 | * 31 | * https://www.apache.org/licenses/LICENSE-2.0 32 | * 33 | * Unless required by applicable law or agreed to in writing, software 34 | * distributed under the License is distributed on an "AS IS" BASIS, 35 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 36 | * See the License for the specific language governing permissions and 37 | * limitations under the License. 38 | */ 39 | 40 | /** 41 | * Configure base Kotlin options for JVM (non-Android) 42 | */ 43 | internal fun org.gradle.api.Project.configureKotlinJvm() { 44 | extensions.configure { 45 | // Up to Java 11 APIs are available through desugaring 46 | // https://developer.android.com/studio/write/java11-minimal-support-table 47 | sourceCompatibility = JavaVersion.VERSION_11 48 | targetCompatibility = JavaVersion.VERSION_11 49 | } 50 | configureKotlin() 51 | } 52 | 53 | /** 54 | * Configure base Kotlin options 55 | */ 56 | private fun org.gradle.api.Project.configureKotlin() { 57 | // Use withType to workaround https://youtrack.jetbrains.com/issue/KT-55947 58 | tasks.withType().configureEach { 59 | compilerOptions { 60 | // Set JVM target to 11 61 | jvmTarget.set(JvmTarget.JVM_11) 62 | freeCompilerArgs.set( 63 | freeCompilerArgs.get() + listOf( 64 | "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", 65 | "-opt-in=androidx.compose.material.ExperimentalMaterialApi", 66 | "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", 67 | "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", 68 | "-opt-in=kotlinx.coroutines.FlowPreview" 69 | ) 70 | ) 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /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 | /* 18 | * Copyright 2021 Google LLC 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * https://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | 33 | plugins { 34 | kotlin("multiplatform") 35 | } 36 | 37 | kotlin { 38 | applyDefaultHierarchyTemplate() 39 | androidTarget { 40 | publishLibraryVariants("release") 41 | } 42 | jvm { 43 | testRuns["test"].executionTask.configure { 44 | useJUnit() 45 | } 46 | } 47 | listOf( 48 | iosX64(), 49 | iosArm64(), 50 | iosSimulatorArm64(), 51 | ).forEach { iosTarget -> 52 | iosTarget.binaries.framework { 53 | baseName = project.name 54 | isStatic = true 55 | } 56 | } 57 | sourceSets { 58 | all { 59 | languageSettings.apply { 60 | optIn("androidx.compose.animation.ExperimentalAnimationApi") 61 | optIn("androidx.compose.foundation.ExperimentalFoundationApi") 62 | optIn("androidx.compose.material.ExperimentalMaterialApi") 63 | optIn("androidx.compose.ui.ExperimentalComposeUiApi") 64 | optIn("kotlinx.serialization.ExperimentalSerializationApi") 65 | optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") 66 | optIn("kotlinx.coroutines.FlowPreview") 67 | } 68 | } 69 | } 70 | configureKotlinJvm() 71 | } 72 | 73 | -------------------------------------------------------------------------------- /build-logic/convention/src/main/kotlin/publishing-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 | plugins { 18 | `maven-publish` 19 | signing 20 | id("org.jetbrains.dokka") 21 | } 22 | 23 | allprojects { 24 | val versionKey = project.name + "_version" 25 | val libProps = rootProject.ext.get("libProps") as? java.util.Properties 26 | ?: return@allprojects 27 | group = libProps["groupId"] as String 28 | version = libProps[versionKey] as String 29 | 30 | task("printProjectVersion") { 31 | doLast { 32 | println(">> " + project.name + " version is " + version) 33 | } 34 | } 35 | } 36 | 37 | val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) { 38 | dokkaSourceSets { 39 | try { 40 | named("iosTest") { 41 | suppress.set(true) 42 | } 43 | } catch (e: Exception) { 44 | } 45 | } 46 | } 47 | 48 | val javadocJar: TaskProvider by tasks.registering(Jar::class) { 49 | dependsOn(dokkaHtml) 50 | archiveClassifier.set("javadoc") 51 | from(dokkaHtml.outputDirectory) 52 | } 53 | 54 | publishing { 55 | publications { 56 | withType { 57 | artifact(javadocJar) 58 | pom { 59 | name.set(project.name) 60 | description.set("A kotlin multiplatform experiment for representing app navigation with tree like data structures") 61 | url.set("https://github.com/tunjid/treenav") 62 | licenses { 63 | license { 64 | name.set("Apache License 2.0") 65 | url.set("https://github.com/tunjid/treenav/blob/main/LICENSE.txt") 66 | } 67 | } 68 | developers { 69 | developer { 70 | id.set("tunjid") 71 | name.set("Adetunji Dahunsi") 72 | email.set("tjdah100@gmail.com") 73 | } 74 | } 75 | scm { 76 | connection.set("scm:git:github.com/tunjid/treenav.git") 77 | developerConnection.set("scm:git:ssh://github.com/tunjid/treenav.git") 78 | url.set("https://github.com/tunjid/treenav/tree/main") 79 | } 80 | } 81 | } 82 | } 83 | repositories { 84 | val localProperties = rootProject.ext.get("localProps") as? java.util.Properties 85 | ?: return@repositories 86 | 87 | val publishUrl = localProperties.getProperty("publishUrl") 88 | if (publishUrl != null) { 89 | maven { 90 | name = localProperties.getProperty("repoName") 91 | url = uri(localProperties.getProperty("publishUrl")) 92 | credentials { 93 | username = localProperties.getProperty("username") 94 | password = localProperties.getProperty("password") 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | 102 | signing { 103 | val localProperties = rootProject.ext.get("localProps") as? java.util.Properties 104 | ?: return@signing 105 | 106 | val signingKey = localProperties.getProperty("signingKey") 107 | val signingPassword = localProperties.getProperty("signingPassword") 108 | 109 | if (signingKey != null && signingPassword != null) { 110 | useInMemoryPgpKeys(signingKey, signingPassword) 111 | sign(publishing.publications) 112 | } 113 | } 114 | 115 | val signingTasks = tasks.withType() 116 | tasks.withType().configureEach { 117 | dependsOn(signingTasks) 118 | } -------------------------------------------------------------------------------- /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/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/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 | #Mon Jul 05 07:23:39 EDT 2021 17 | distributionBase=GRADLE_USER_HOME 18 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 19 | distributionPath=wrapper/dists 20 | zipStorePath=wrapper/dists 21 | zipStoreBase=GRADLE_USER_HOME 22 | -------------------------------------------------------------------------------- /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 | * http://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 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 18 | buildscript { 19 | extra.apply { 20 | set("localProps", java.util.Properties().apply { 21 | file("local.properties").let { file -> 22 | if (file.exists()) load(java.io.FileInputStream(file)) 23 | } 24 | }) 25 | set("libProps", java.util.Properties().apply { 26 | file("libraryVersion.properties").let { file -> 27 | if (file.exists()) load(java.io.FileInputStream(file)) 28 | } 29 | }) 30 | } 31 | 32 | repositories { 33 | google() 34 | mavenCentral() 35 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 36 | maven("https://plugins.gradle.org/m2/") 37 | } 38 | } 39 | 40 | plugins { 41 | alias(libs.plugins.android.application) apply false 42 | alias(libs.plugins.android.library) apply false 43 | alias(libs.plugins.compose.compiler) apply false 44 | alias(libs.plugins.jetbrains.compose) apply false 45 | alias(libs.plugins.jetbrains.dokka) apply false 46 | alias(libs.plugins.kotlin.android) apply false 47 | alias(libs.plugins.kotlin.multiplatform) apply false 48 | } 49 | -------------------------------------------------------------------------------- /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 | # http://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 | android.useAndroidX=true 17 | kotlin.code.style=official 18 | kotlin.js.generate.executable.default=false 19 | org.jetbrains.compose.experimental.jscanvas.enabled=true 20 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/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 | #Mon Jul 05 07:23:39 EDT 2021 17 | distributionBase=GRADLE_USER_HOME 18 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 19 | distributionPath=wrapper/dists 20 | zipStorePath=wrapper/dists 21 | zipStoreBase=GRADLE_USER_HOME 22 | -------------------------------------------------------------------------------- /library/compose-threepane/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /library/compose-threepane/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("publishing-library-convention") 4 | id("android-library-convention") 5 | id("kotlin-jvm-convention") 6 | id("kotlin-library-convention") 7 | id("maven-publish") 8 | signing 9 | id("org.jetbrains.dokka") 10 | id("org.jetbrains.compose") 11 | alias(libs.plugins.compose.compiler) 12 | } 13 | 14 | android { 15 | buildFeatures { 16 | compose = true 17 | } 18 | } 19 | 20 | kotlin { 21 | sourceSets { 22 | commonMain { 23 | dependencies { 24 | implementation(project(":library:treenav")) 25 | implementation(project(":library:compose")) 26 | 27 | implementation(libs.jetbrains.compose.runtime) 28 | implementation(libs.jetbrains.compose.foundation) 29 | implementation(libs.jetbrains.compose.foundation.layout) 30 | } 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneMovableElementSharedTransitionScope.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 | * http://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.treenav.compose.threepane 18 | 19 | import androidx.compose.animation.ExperimentalSharedTransitionApi 20 | import androidx.compose.runtime.Composable 21 | import com.tunjid.treenav.Node 22 | import com.tunjid.treenav.compose.PaneMovableElementSharedTransitionScope 23 | import com.tunjid.treenav.compose.PaneScope 24 | import com.tunjid.treenav.compose.PaneSharedTransitionScope 25 | import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope 26 | import com.tunjid.treenav.compose.rememberPaneMovableElementSharedTransitionScope 27 | import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope 28 | 29 | /** 30 | * An interface providing both [MovableSharedElementScope] and [PaneSharedTransitionScope] for 31 | * a [ThreePane] layout. 32 | */ 33 | typealias ThreePaneMovableElementSharedTransitionScope = 34 | PaneMovableElementSharedTransitionScope 35 | 36 | /** 37 | * Remembers a [ThreePaneMovableElementSharedTransitionScope] in the composition. 38 | * 39 | * @param movableSharedElementScope The [MovableSharedElementScope] used create a 40 | * [PaneSharedTransitionScope] for this [PaneScope]. 41 | * 42 | * If one is not provided, one is retrieved from this [PaneScope] using 43 | * [requireThreePaneMovableSharedElementScope]. 44 | */ 45 | @OptIn(ExperimentalSharedTransitionApi::class) 46 | @Composable 47 | fun PaneScope< 48 | ThreePane, 49 | Destination 50 | >.rememberThreePaneMovableElementSharedTransitionScope( 51 | movableSharedElementScope: MovableSharedElementScope = requireThreePaneMovableSharedElementScope() 52 | ): ThreePaneMovableElementSharedTransitionScope { 53 | val paneSharedTransitionScope = rememberPaneSharedTransitionScope( 54 | movableSharedElementScope.sharedTransitionScope 55 | ) 56 | return rememberPaneMovableElementSharedTransitionScope( 57 | paneSharedTransitionScope = paneSharedTransitionScope, 58 | movableSharedElementScope = movableSharedElementScope, 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.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 | * http://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.treenav.compose.threepane.transforms 18 | 19 | import androidx.compose.runtime.State 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.setValue 23 | import com.tunjid.treenav.Node 24 | import com.tunjid.treenav.compose.threepane.ThreePane 25 | import com.tunjid.treenav.compose.transforms.Transform 26 | import com.tunjid.treenav.compose.transforms.compoundTransform 27 | 28 | /** 29 | * An [Transform] that moves the current [Destination] in a [ThreePane.Primary] pane, to 30 | * to the [ThreePane.TransientPrimary] pane when "back" is being previewed. 31 | * 32 | * @param isPreviewingBack provides the state of the predictive back gesture. 33 | * True if the gesture is ongoing. 34 | * @param navigationStateBackTransform provides the [NavigationState] if the app were to 35 | * go "back". 36 | */ 37 | inline fun 38 | backPreviewTransform( 39 | isPreviewingBack: State, 40 | crossinline navigationStateBackTransform: NavigationState.() -> NavigationState, 41 | ): Transform { 42 | var lastPrimaryDestination by mutableStateOf(null) 43 | 44 | return compoundTransform( 45 | destinationTransform = { navigationState, previousTransform -> 46 | val previousDestination = previousTransform(navigationState) 47 | lastPrimaryDestination = previousDestination 48 | if (isPreviewingBack.value) previousTransform(navigationState.navigationStateBackTransform()) 49 | else previousDestination 50 | }, 51 | paneTransform = paneTransform@{ destination, previousTransform -> 52 | val previousMapping = previousTransform(destination) 53 | val isPreviewing by isPreviewingBack 54 | if (!isPreviewing) return@paneTransform previousMapping 55 | // Back is being previewed, therefore the original mapping is already for back. 56 | // Pass the previous primary value into transient. 57 | val transientDestination = checkNotNull(lastPrimaryDestination) { 58 | "Attempted to show last destination without calling destination transform" 59 | } 60 | val paneMapping = previousTransform(transientDestination) 61 | val transient = paneMapping[ThreePane.Primary] 62 | previousMapping + (ThreePane.TransientPrimary to transient) 63 | } 64 | ) 65 | } 66 | 67 | -------------------------------------------------------------------------------- /library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.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 | * http://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.treenav.compose.threepane.transforms 18 | 19 | import androidx.compose.runtime.State 20 | import androidx.compose.runtime.derivedStateOf 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.ui.unit.Dp 25 | import androidx.compose.ui.unit.dp 26 | import com.tunjid.treenav.Node 27 | import com.tunjid.treenav.compose.threepane.ThreePane 28 | import com.tunjid.treenav.compose.transforms.PaneTransform 29 | import com.tunjid.treenav.compose.transforms.Transform 30 | 31 | /** 32 | * An [Transform] that selectively displays panes for a [ThreePane] layout 33 | * based on the space available determined by the [windowWidthState]. 34 | * 35 | * @param windowWidthState provides the current width of the display in Dp. 36 | */ 37 | fun 38 | threePanedAdaptiveTransform( 39 | windowWidthState: State, 40 | secondaryPaneBreakPoint: State = mutableStateOf(SECONDARY_PANE_MIN_WIDTH_BREAKPOINT_DP), 41 | tertiaryPaneBreakPoint: State = mutableStateOf(TERTIARY_PANE_MIN_WIDTH_BREAKPOINT_DP), 42 | ): Transform = 43 | PaneTransform { destination, previousTransform -> 44 | val showSecondary by remember { 45 | derivedStateOf { windowWidthState.value >= secondaryPaneBreakPoint.value } 46 | } 47 | val showTertiary by remember { 48 | derivedStateOf { windowWidthState.value >= tertiaryPaneBreakPoint.value } 49 | } 50 | 51 | val originalMapping = previousTransform(destination) 52 | val primaryNode = originalMapping[ThreePane.Primary] 53 | mapOf( 54 | ThreePane.Primary to primaryNode, 55 | ThreePane.Secondary to originalMapping[ThreePane.Secondary].takeIf { secondaryDestination -> 56 | secondaryDestination?.id != primaryNode?.id && showSecondary 57 | }, 58 | ThreePane.Tertiary to originalMapping[ThreePane.Tertiary].takeIf { tertiaryDestination -> 59 | tertiaryDestination?.id != primaryNode?.id && showTertiary 60 | }, 61 | ) 62 | } 63 | 64 | private val SECONDARY_PANE_MIN_WIDTH_BREAKPOINT_DP = 600.dp 65 | private val TERTIARY_PANE_MIN_WIDTH_BREAKPOINT_DP = 1200.dp -------------------------------------------------------------------------------- /library/compose/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /library/compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("publishing-library-convention") 4 | id("android-library-convention") 5 | id("kotlin-jvm-convention") 6 | id("kotlin-library-convention") 7 | id("maven-publish") 8 | signing 9 | id("org.jetbrains.dokka") 10 | id("org.jetbrains.compose") 11 | alias(libs.plugins.compose.compiler) 12 | } 13 | 14 | android { 15 | buildFeatures { 16 | compose = true 17 | } 18 | } 19 | 20 | kotlin { 21 | sourceSets { 22 | commonMain { 23 | dependencies { 24 | implementation(project(":library:treenav")) 25 | 26 | implementation(libs.androidx.collection) 27 | 28 | implementation(libs.jetbrains.compose.runtime) 29 | implementation(libs.jetbrains.compose.foundation) 30 | implementation(libs.jetbrains.compose.foundation.layout) 31 | 32 | implementation(libs.jetbrains.lifecycle.runtime) 33 | implementation(libs.jetbrains.lifecycle.runtime.compose) 34 | implementation(libs.jetbrains.lifecycle.viewmodel) 35 | implementation(libs.jetbrains.lifecycle.viewmodel.compose) 36 | 37 | // implementation(libs.androidx.navigation3) 38 | implementation(libs.jetbrains.savedstate.compose) 39 | 40 | } 41 | } 42 | androidMain { 43 | dependencies { 44 | implementation(libs.androidx.activity.compose) 45 | // implementation(libs.androidx.viewmodel.navigation3) 46 | } 47 | } 48 | 49 | commonTest { 50 | dependencies { 51 | implementation(kotlin("test")) 52 | } 53 | } 54 | 55 | all { 56 | languageSettings.apply { 57 | optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") 58 | optIn("kotlinx.coroutines.FlowPreview") 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /library/compose/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /library/compose/src/androidMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.android.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 | * http://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.treenav.compose.navigation3.decorators 18 | 19 | import androidx.activity.compose.LocalActivity 20 | import androidx.compose.runtime.Composable 21 | import androidx.lifecycle.SavedStateViewModelFactory 22 | import androidx.savedstate.SavedStateRegistryOwner 23 | 24 | @Composable 25 | internal actual fun shouldRemoveViewModelStoreCallback(): () -> Boolean { 26 | val activity = LocalActivity.current 27 | return { activity?.isChangingConfigurations != true } 28 | } 29 | 30 | internal actual fun SavedStateViewModelFactory( 31 | savedStateRegistryOwner: SavedStateRegistryOwner 32 | ): SavedStateViewModelFactory = SavedStateViewModelFactory( 33 | null, 34 | savedStateRegistryOwner 35 | ) -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/Defaults.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 | * http://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.treenav.compose 18 | 19 | import androidx.compose.animation.BoundsTransform 20 | import androidx.compose.animation.ExperimentalSharedTransitionApi 21 | import androidx.compose.animation.SharedTransitionScope.OverlayClip 22 | import androidx.compose.animation.SharedTransitionScope.SharedContentState 23 | import androidx.compose.animation.core.Spring.StiffnessMediumLow 24 | import androidx.compose.animation.core.VisibilityThreshold 25 | import androidx.compose.animation.core.spring 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.Stable 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.geometry.Rect 30 | import androidx.compose.ui.graphics.Path 31 | import androidx.compose.ui.unit.Density 32 | import androidx.compose.ui.unit.LayoutDirection 33 | 34 | @Stable 35 | internal object Defaults { 36 | 37 | val EmptyElement: @Composable (Any?, Modifier) -> Unit = { _, _ -> } 38 | 39 | @ExperimentalSharedTransitionApi 40 | val DefaultBoundsTransform = BoundsTransform { _, _ -> DefaultSpring } 41 | 42 | @ExperimentalSharedTransitionApi 43 | val ParentClip: OverlayClip = 44 | object : OverlayClip { 45 | override fun getClipPath( 46 | sharedContentState: SharedContentState, 47 | bounds: Rect, 48 | layoutDirection: LayoutDirection, 49 | density: Density, 50 | ): Path? { 51 | return sharedContentState.parentSharedContentState?.clipPathInOverlay 52 | } 53 | } 54 | 55 | } 56 | 57 | private val DefaultSpring = spring( 58 | stiffness = StiffnessMediumLow, 59 | visibilityThreshold = Rect.VisibilityThreshold 60 | ) 61 | -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.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 | * http://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.treenav.compose 18 | 19 | import androidx.compose.animation.AnimatedContent 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.Stable 23 | import androidx.compose.runtime.saveable.SaveableStateHolder 24 | import androidx.compose.ui.Modifier 25 | import androidx.lifecycle.Lifecycle 26 | import androidx.lifecycle.LifecycleOwner 27 | import androidx.lifecycle.ViewModelStoreOwner 28 | import androidx.lifecycle.compose.LocalLifecycleOwner 29 | import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner 30 | import com.tunjid.treenav.Node 31 | 32 | /** 33 | * Scope that provides context about individual panes [Pane] in an [MultiPaneDisplay]. 34 | */ 35 | @Stable 36 | interface MultiPaneDisplayScope { 37 | 38 | @Composable 39 | fun Destination( 40 | pane: Pane, 41 | ) 42 | 43 | fun adaptationsIn( 44 | pane: Pane, 45 | ): Set 46 | 47 | fun destinationIn( 48 | pane: Pane, 49 | ): Destination? 50 | } 51 | 52 | /** 53 | * A Display that provides the following for each 54 | * navigation [Destination] that shows up in its panes: 55 | * 56 | * - A single [SaveableStateHolder] for each navigation [Destination] that shows up in its panes. 57 | * [SaveableStateHolder.SaveableStateProvider] is keyed on the [Destination]s [Node.id]. 58 | * 59 | * - A [ViewModelStoreOwner] for each [Destination] via [LocalViewModelStoreOwner]. 60 | * Once present in the navigation tree, a [Destination] will always use the same 61 | * [ViewModelStoreOwner], regardless of where in the tree it is, until its is removed from the tree. 62 | * [Destination]s are unique based on their [Node.id]. 63 | * 64 | * - A [LifecycleOwner] for each [Destination] via [LocalLifecycleOwner]. This [LifecycleOwner] 65 | * follows the [Lifecycle] of its immediate parent, unless it is animating out or placed in the 66 | * backstack. This is defined by [PaneScope.isActive], which is a function of the backing 67 | * [AnimatedContent] for each [Pane] displayed and if the current [Destination] 68 | * matches [MultiPaneDisplayScope.destinationIn] in the visible [Pane]. 69 | * 70 | * @param state the driving [MultiPaneDisplayState] that applies adaptive semantics and 71 | * transforms for each navigation destination shown in the [MultiPaneDisplay]. 72 | */ 73 | @Composable 74 | fun MultiPaneDisplay( 75 | state: MultiPaneDisplayState, 76 | modifier: Modifier = Modifier, 77 | content: @Composable MultiPaneDisplayScope.() -> Unit, 78 | ) { 79 | Box( 80 | modifier = modifier 81 | ) { 82 | DecoratedNavEntryMultiPaneDisplayScope( 83 | state = state, 84 | content = content 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneEntry.kt: -------------------------------------------------------------------------------- 1 | package com.tunjid.treenav.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import com.tunjid.treenav.Node 6 | import com.tunjid.treenav.compose.transforms.RenderTransform 7 | 8 | /** 9 | * Provides the logic used to select, configure and place a navigation [Destination] for each 10 | * pane [Pane] for the current active navigation [Destination]. 11 | */ 12 | @Stable 13 | class PaneEntry( 14 | internal val renderTransform: RenderTransform, 15 | internal val paneTransform: @Composable (Destination) -> Map, 16 | internal val content: @Composable PaneScope.(Destination) -> Unit, 17 | ) -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneMovableElementSharedTransitionScope.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 | * http://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 | @file:Suppress("unused") 19 | 20 | package com.tunjid.treenav.compose 21 | 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.Stable 24 | import androidx.compose.runtime.remember 25 | import com.tunjid.treenav.Node 26 | import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope 27 | 28 | /** 29 | * A type alias for [PaneMovableElementSharedTransitionScope] for usages where the generic types 30 | * are not required. 31 | */ 32 | typealias MovableElementSharedTransitionScope = PaneMovableElementSharedTransitionScope<*, *> 33 | 34 | /** 35 | * An interface providing both [MovableSharedElementScope] and [PaneSharedTransitionScope] 36 | * semantics. 37 | */ 38 | @Stable 39 | interface PaneMovableElementSharedTransitionScope : 40 | PaneSharedTransitionScope, MovableSharedElementScope 41 | 42 | /** 43 | * Remembers a [PaneMovableElementSharedTransitionScope] in the composition. 44 | * 45 | * @param paneSharedTransitionScope the backing [PaneSharedTransitionScope] for this [PaneScope]. 46 | * @param movableSharedElementScope the backing [MovableSharedElementScope] for this [PaneScope]. 47 | */ 48 | @Composable 49 | fun rememberPaneMovableElementSharedTransitionScope( 50 | paneSharedTransitionScope: PaneSharedTransitionScope, 51 | movableSharedElementScope: MovableSharedElementScope, 52 | ): PaneMovableElementSharedTransitionScope { 53 | return remember { 54 | DelegatingPaneMovableElementSharedTransitionScope( 55 | paneSharedTransitionScope = paneSharedTransitionScope, 56 | movableSharedElementScope = movableSharedElementScope, 57 | ) 58 | } 59 | } 60 | 61 | @Stable 62 | private class DelegatingPaneMovableElementSharedTransitionScope( 63 | val paneSharedTransitionScope: PaneSharedTransitionScope, 64 | val movableSharedElementScope: MovableSharedElementScope, 65 | ) : PaneMovableElementSharedTransitionScope, 66 | PaneSharedTransitionScope by paneSharedTransitionScope, 67 | MovableSharedElementScope by movableSharedElementScope 68 | -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneScope.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 | * http://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.treenav.compose 18 | 19 | import androidx.compose.animation.AnimatedContentScope 20 | import androidx.compose.animation.AnimatedVisibilityScope 21 | import androidx.compose.runtime.Stable 22 | import androidx.compose.runtime.State 23 | import androidx.compose.runtime.getValue 24 | import androidx.compose.runtime.mutableStateOf 25 | import androidx.compose.runtime.setValue 26 | import com.tunjid.treenav.Node 27 | import kotlin.jvm.JvmInline 28 | 29 | /** 30 | * Scope for navigation destinations that can show up in an arbitrary pane. 31 | */ 32 | @Stable 33 | interface PaneScope : AnimatedVisibilityScope { 34 | 35 | /** 36 | * Provides information about the adaptive context that created this [PaneScope]. 37 | */ 38 | val paneState: PaneState 39 | 40 | /** 41 | * Whether or not this [PaneScope] is active in its current pane. It is inactive when 42 | * it is animating out of its [AnimatedVisibilityScope]. 43 | */ 44 | val isActive: Boolean 45 | 46 | } 47 | 48 | /** 49 | * An implementation of [PaneScope] that supports animations and shared elements 50 | */ 51 | @Stable 52 | internal class AnimatedPaneScope( 53 | paneState: PaneState, 54 | activeState: State, 55 | val animatedContentScope: AnimatedContentScope 56 | ) : PaneScope, AnimatedVisibilityScope by animatedContentScope { 57 | 58 | override var paneState by mutableStateOf(paneState) 59 | 60 | override val isActive: Boolean by activeState 61 | } 62 | 63 | /** 64 | * Information about content in a pane 65 | */ 66 | @Stable 67 | sealed interface PaneState { 68 | val currentDestination: Destination? 69 | val pane: Pane? 70 | val adaptations: Set 71 | } 72 | 73 | /** 74 | * [Slot] based implementation of [PaneState] 75 | */ 76 | internal data class SlotPaneState( 77 | val slot: Slot?, 78 | val previousDestination: Destination?, 79 | override val currentDestination: Destination?, 80 | override val pane: Pane?, 81 | override val adaptations: Set, 82 | ) : PaneState 83 | 84 | /** 85 | * A spot taken by an [PaneEntry] that may be moved in from pane to pane. 86 | */ 87 | @JvmInline 88 | internal value class Slot internal constructor(val index: Int) 89 | -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.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 | * http://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.treenav.compose 18 | 19 | import androidx.compose.animation.BoundsTransform 20 | import androidx.compose.animation.ExperimentalSharedTransitionApi 21 | import androidx.compose.animation.SharedTransitionScope 22 | import androidx.compose.animation.SharedTransitionScope.OverlayClip 23 | import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize 24 | import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion.contentSize 25 | import androidx.compose.runtime.Stable 26 | import androidx.compose.ui.Modifier 27 | import com.tunjid.treenav.Node 28 | 29 | /** 30 | * A [SharedTransitionScope] that is aware of the relationship between the [Pane]s in 31 | * its [MultiPaneDisplay]. This allows for defining the semantics of 32 | * shared element behavior when shared elements move in between [Pane]s during the 33 | * transition. 34 | */ 35 | @OptIn(ExperimentalSharedTransitionApi::class) 36 | @Stable 37 | interface PaneSharedTransitionScope : 38 | PaneScope, SharedTransitionScope { 39 | 40 | /** 41 | * Conceptual equivalent of [SharedTransitionScope.sharedElement], with the exception 42 | * of a key being passed instead of [SharedTransitionScope.SharedContentState]. This is because 43 | * each [PaneState.pane] may need its own [SharedTransitionScope.SharedContentState] and 44 | * will need to be managed by the implementation of this method. 45 | * 46 | * @see [SharedTransitionScope.sharedElement]. 47 | */ 48 | fun Modifier.paneSharedElement( 49 | key: Any, 50 | boundsTransform: BoundsTransform = Defaults.DefaultBoundsTransform, 51 | placeHolderSize: PlaceHolderSize = contentSize, 52 | renderInOverlayDuringTransition: Boolean = true, 53 | visible: Boolean? = null, 54 | zIndexInOverlay: Float = 0f, 55 | clipInOverlayDuringTransition: OverlayClip = Defaults.ParentClip, 56 | ): Modifier 57 | } 58 | -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavigationState.kt: -------------------------------------------------------------------------------- 1 | package com.tunjid.treenav.compose 2 | 3 | /** 4 | * A description of the process that the layout undertook to adapt to the present 5 | * pane in its new configuration. 6 | */ 7 | sealed class Adaptation { 8 | 9 | /** 10 | * Destinations remained the same in the pane 11 | */ 12 | data object Same : Adaptation() 13 | 14 | /** 15 | * Destinations were changed in the pane 16 | */ 17 | data object Change : Adaptation() 18 | 19 | /** 20 | * The current back stack is a sublist of a previously displayed back stack. 21 | */ 22 | data object Pop : Adaptation() 23 | 24 | /** 25 | * Destinations were swapped in between panes 26 | */ 27 | data class Swap( 28 | val from: Pane, 29 | val to: Pane?, 30 | ) : Adaptation() 31 | 32 | /** 33 | * Checks if a [Swap] [Adaptation] involved [pane]. 34 | */ 35 | operator fun Swap.contains(pane: Pane?): Boolean = pane == from || pane == to 36 | 37 | } 38 | -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/StackNavExt.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 | * http://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.treenav.compose 18 | 19 | import com.tunjid.treenav.MultiStackNav 20 | import com.tunjid.treenav.Node 21 | import com.tunjid.treenav.StackNav 22 | import com.tunjid.treenav.backStack 23 | 24 | /** 25 | * A convenience method for reading the back stack for this [MultiStackNav] 26 | * optimized for consumption for a [MultiPaneDisplay]. 27 | */ 28 | inline fun MultiStackNav.multiPaneDisplayBackstack(): List = 29 | backStack( 30 | includeCurrentDestinationChildren = true, 31 | placeChildrenBeforeParent = true, 32 | distinctDestinations = true, 33 | ) 34 | .filterIsInstance() 35 | 36 | /** 37 | * A convenience method for reading the back stack for this [MultiStackNav] 38 | * optimized for consumption for a [MultiPaneDisplay]. 39 | */ 40 | inline fun StackNav.multiPaneDisplayBackstack(): List = 41 | backStack( 42 | includeCurrentDestinationChildren = true, 43 | placeChildrenBeforeParent = true, 44 | distinctDestinations = true, 45 | ) 46 | .filterIsInstance() -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/lifecycle/DestinationLifecycleOwner.kt: -------------------------------------------------------------------------------- 1 | package com.tunjid.treenav.compose.lifecycle 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.remember 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.Lifecycle.State 8 | import androidx.lifecycle.LifecycleOwner 9 | import androidx.lifecycle.LifecycleRegistry 10 | import androidx.lifecycle.compose.LocalLifecycleOwner 11 | import com.tunjid.treenav.Node 12 | import com.tunjid.treenav.compose.PaneScope 13 | import com.tunjid.treenav.compose.SlotBasedPanedNavigationState 14 | 15 | @Composable 16 | internal fun rememberDestinationLifecycleOwner( 17 | destination: Node, 18 | ): DestinationLifecycleOwner { 19 | val hostLifecycleOwner = LocalLifecycleOwner.current 20 | val destinationLifecycleOwner = remember(hostLifecycleOwner) { 21 | DestinationLifecycleOwner( 22 | destination = destination, 23 | host = hostLifecycleOwner 24 | ) 25 | } 26 | return destinationLifecycleOwner 27 | } 28 | 29 | @Stable 30 | internal class DestinationLifecycleOwner( 31 | private val destination: Node, 32 | private val host: LifecycleOwner 33 | ) : LifecycleOwner { 34 | 35 | private val lifecycleRegistry = LifecycleRegistry(this) 36 | 37 | override val lifecycle: Lifecycle 38 | get() = lifecycleRegistry 39 | 40 | val hostLifecycleState = host.lifecycle 41 | 42 | fun update( 43 | hostLifecycleState: State, 44 | paneScope: PaneScope<*, *>, 45 | panedNavigationState: SlotBasedPanedNavigationState<*, *>, 46 | ) { 47 | val active = paneScope.isActive 48 | val exists = panedNavigationState.backStackIds.contains( 49 | destination.id 50 | ) 51 | val derivedLifecycleState = when { 52 | !exists -> State.DESTROYED 53 | !active -> State.STARTED 54 | else -> hostLifecycleState 55 | } 56 | lifecycleRegistry.currentState = 57 | if (host.lifecycle.currentState.ordinal < derivedLifecycleState.ordinal) hostLifecycleState 58 | else derivedLifecycleState 59 | } 60 | } -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/lifecycle/DestinationViewModelStoreCreator.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 | * http://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.treenav.compose.lifecycle 18 | 19 | import androidx.compose.runtime.Stable 20 | import androidx.compose.runtime.mutableStateMapOf 21 | import androidx.lifecycle.ViewModelStore 22 | import androidx.lifecycle.ViewModelStoreOwner 23 | import com.tunjid.treenav.Node 24 | import com.tunjid.treenav.Order 25 | import com.tunjid.treenav.traverse 26 | 27 | /** 28 | * A class that lazily loads a [ViewModelStoreOwner] for each destination in the navigation graph. 29 | * Each unique destination can only have a single [ViewModelStore], regardless of how many times 30 | * it appears in the navigation graph, or its depth at any point. 31 | */ 32 | @Stable 33 | internal class DestinationViewModelStoreCreator( 34 | private val validNodeIdsReader: () -> Set 35 | ) { 36 | private val nodeIdsToViewModelStoreOwner = mutableStateMapOf() 37 | 38 | /** 39 | * Creates a [ViewModelStoreOwner] for a given [Node] 40 | */ 41 | fun viewModelStoreOwnerFor( 42 | node: Node 43 | ): ViewModelStoreOwner { 44 | val existingIds = validNodeIdsReader() 45 | check(existingIds.contains(node.id)) { 46 | """ 47 | Attempted to create a ViewModelStoreOwner for Node $node, but the Node is not 48 | present in the navigation tree 49 | """.trimIndent() 50 | } 51 | return nodeIdsToViewModelStoreOwner.getOrPut( 52 | node.id 53 | ) { 54 | object : ViewModelStoreOwner { 55 | override val viewModelStore: ViewModelStore = ViewModelStore() 56 | } 57 | } 58 | } 59 | 60 | fun clearStoreFor(childNode: Node) { 61 | val existingIds = validNodeIdsReader() 62 | childNode.traverse(Order.BreadthFirst) { 63 | if (!existingIds.contains(it.id)) { 64 | nodeIdsToViewModelStoreOwner.remove(it.id) 65 | ?.viewModelStore 66 | ?.clear() 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElementState.kt: -------------------------------------------------------------------------------- 1 | package com.tunjid.treenav.compose.moveablesharedelement 2 | 3 | import androidx.compose.animation.ExperimentalSharedTransitionApi 4 | import androidx.compose.animation.SharedTransitionScope 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.DisposableEffect 7 | import androidx.compose.runtime.Stable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.movableContentOf 10 | import androidx.compose.runtime.mutableIntStateOf 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Modifier 14 | 15 | @OptIn(ExperimentalSharedTransitionApi::class) 16 | @Stable 17 | internal class MovableSharedElementState( 18 | sharedContentState: SharedTransitionScope.SharedContentState, 19 | sharedElement: @Composable (State, Modifier) -> Unit, 20 | onRemoved: () -> Unit 21 | ) { 22 | 23 | var sharedContentState by mutableStateOf(sharedContentState) 24 | private var composedRefCount by mutableIntStateOf(0) 25 | 26 | val moveableSharedElement: @Composable (Any?, Modifier) -> Unit = 27 | movableContentOf { state, modifier -> 28 | @Suppress("UNCHECKED_CAST") 29 | sharedElement( 30 | // The shared element composable will be created by the first screen and reused by 31 | // subsequent screens. This updates the state from other screens so changes are seen. 32 | state as State, 33 | modifier 34 | ) 35 | 36 | DisposableEffect(Unit) { 37 | ++composedRefCount 38 | onDispose { 39 | if (--composedRefCount <= 0) onRemoved() 40 | } 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntry.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 | * http://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.treenav.compose.navigation3 18 | 19 | import androidx.compose.runtime.Composable 20 | 21 | /** 22 | * Entry maintains and stores the key and the content represented by that key. Entries should be 23 | * created as part of a [NavDisplay.entryProvider](reference/androidx/navigation/NavDisplay). 24 | * 25 | * @param key key for this entry 26 | * @param metadata provides information to the display 27 | * @param content content for this entry to be displayed when this entry is active 28 | */ 29 | internal open class NavEntry( 30 | open val key: T, 31 | open val metadata: Map = emptyMap(), 32 | open val content: @Composable (T) -> Unit 33 | ) 34 | -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntryDecorator.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 | * http://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.treenav.compose.navigation3 18 | 19 | import androidx.compose.runtime.Composable 20 | import kotlin.jvm.JvmSuppressWildcards 21 | 22 | /** Marker class to hold the onPop and decorator functions that will be invoked at runtime. */ 23 | internal class NavEntryDecorator 24 | internal constructor( 25 | internal val onPop: (key: Any) -> Unit, 26 | internal val navEntryDecorator: @Composable (entry: NavEntry) -> Unit 27 | ) 28 | 29 | /** 30 | * Function to provide information to all the [NavEntry] that are integrated with a 31 | * [DecoratedNavEntryProvider]. 32 | * 33 | * @param onPop a callback that provides the key of a [NavEntry] that has been popped from the 34 | * backStack and is leaving composition. This optional callback should to be used to clean up 35 | * states that were used to decorate the NavEntry3 36 | * @param decorator the composable function to provide information to a [NavEntry] [decorator]. Note 37 | * that this function only gets invoked for NavEntries that are actually getting rendered (i.e. by 38 | * invoking the [NavEntry.content].) 39 | */ 40 | internal fun navEntryDecorator( 41 | onPop: (key: Any) -> Unit = {}, 42 | decorator: @Composable (entry: NavEntry) -> Unit 43 | ): NavEntryDecorator = NavEntryDecorator(onPop, decorator) 44 | 45 | /** 46 | * Wraps a [NavEntry] with the list of [NavEntryDecorator] in the order that the decorators were 47 | * added to the list and invokes the content of the wrapped entry. 48 | */ 49 | @Composable 50 | internal fun DecorateNavEntry( 51 | entry: NavEntry, 52 | entryDecorators: List<@JvmSuppressWildcards NavEntryDecorator<*>> 53 | ) { 54 | @Suppress("UNCHECKED_CAST") 55 | (entryDecorators as List<@JvmSuppressWildcards NavEntryDecorator>) 56 | .distinct() 57 | .foldRight(initial = entry) { decorator, wrappedEntry -> 58 | object : NavEntryWrapper(wrappedEntry) { 59 | override val content: @Composable ((T) -> Unit) = { 60 | decorator.navEntryDecorator(wrappedEntry) 61 | } 62 | } 63 | } 64 | .content 65 | .invoke(entry.key) 66 | } -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/NavEntryWrapper.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 | * http://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.treenav.compose.navigation3 18 | 19 | import androidx.compose.runtime.Composable 20 | 21 | /** 22 | * Class that wraps a [NavEntry] within another [NavEntry]. 23 | * 24 | * This provides a nesting mechanism for [NavEntry]s that allows properly nested content. 25 | * 26 | * @param navEntry the [NavEntry] to wrap 27 | */ 28 | internal open class NavEntryWrapper(val navEntry: NavEntry) : 29 | NavEntry(navEntry.key, navEntry.metadata, navEntry.content) { 30 | override val key: T 31 | get() = navEntry.key 32 | 33 | override val metadata: Map 34 | get() = navEntry.metadata 35 | 36 | override val content: @Composable (T) -> Unit 37 | get() = navEntry.content 38 | } 39 | -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/TransitionAwareLifecycleNavEntryDecorator.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 | * http://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.treenav.compose.navigation3.decorators 18 | 19 | 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.CompositionLocalProvider 22 | import androidx.compose.runtime.DisposableEffect 23 | import androidx.compose.runtime.LaunchedEffect 24 | import androidx.compose.runtime.remember 25 | import androidx.lifecycle.Lifecycle 26 | import androidx.lifecycle.LifecycleEventObserver 27 | import androidx.lifecycle.LifecycleOwner 28 | import androidx.lifecycle.LifecycleRegistry 29 | import androidx.lifecycle.compose.LocalLifecycleOwner 30 | import com.tunjid.treenav.compose.navigation3.navEntryDecorator 31 | 32 | @Composable 33 | internal fun transitionAwareLifecycleNavEntryDecorator( 34 | backStack: List, 35 | isSettled: @Composable () -> Boolean 36 | ) = navEntryDecorator { entry -> 37 | val isInBackStack = entry.key in backStack 38 | val settled = isSettled() 39 | val maxLifecycle = 40 | when { 41 | isInBackStack && settled -> Lifecycle.State.RESUMED 42 | isInBackStack && !settled -> Lifecycle.State.STARTED 43 | else /* !isInBackStack */ -> Lifecycle.State.CREATED 44 | } 45 | LifecycleOwner(maxLifecycle = maxLifecycle) { entry.content.invoke(entry.key) } 46 | } 47 | 48 | @Composable 49 | private fun LifecycleOwner( 50 | maxLifecycle: Lifecycle.State = Lifecycle.State.RESUMED, 51 | parentLifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, 52 | content: @Composable () -> Unit 53 | ) { 54 | val childLifecycleOwner = remember(parentLifecycleOwner) { ChildLifecycleOwner() } 55 | // Pass LifecycleEvents from the parent down to the child 56 | DisposableEffect(childLifecycleOwner, parentLifecycleOwner) { 57 | val observer = LifecycleEventObserver { _, event -> 58 | childLifecycleOwner.handleLifecycleEvent(event) 59 | } 60 | 61 | parentLifecycleOwner.lifecycle.addObserver(observer) 62 | 63 | onDispose { parentLifecycleOwner.lifecycle.removeObserver(observer) } 64 | } 65 | // Ensure that the child lifecycle is capped at the maxLifecycle 66 | LaunchedEffect(childLifecycleOwner, maxLifecycle) { 67 | childLifecycleOwner.maxLifecycle = maxLifecycle 68 | } 69 | // Now install the LifecycleOwner as a composition local 70 | CompositionLocalProvider(LocalLifecycleOwner provides childLifecycleOwner) { 71 | content.invoke() 72 | } 73 | } 74 | 75 | private class ChildLifecycleOwner : LifecycleOwner { 76 | private val lifecycleRegistry = LifecycleRegistry(this) 77 | 78 | override val lifecycle: Lifecycle 79 | get() = lifecycleRegistry 80 | 81 | var maxLifecycle: Lifecycle.State = Lifecycle.State.INITIALIZED 82 | set(maxState) { 83 | field = maxState 84 | updateState() 85 | } 86 | 87 | private var parentLifecycleState: Lifecycle.State = Lifecycle.State.CREATED 88 | 89 | fun handleLifecycleEvent(event: Lifecycle.Event) { 90 | parentLifecycleState = event.targetState 91 | updateState() 92 | } 93 | 94 | fun updateState() { 95 | if (parentLifecycleState.ordinal < maxLifecycle.ordinal) { 96 | lifecycleRegistry.currentState = parentLifecycleState 97 | } else { 98 | lifecycleRegistry.currentState = maxLifecycle 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/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 | * http://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.treenav.compose.navigation3.decorators 18 | 19 | internal fun getIdForKey(key: Any, count: Int): Int = 31 * key.hashCode() + count 20 | -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/PaneModifierTransform.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 | * http://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.treenav.compose.transforms 18 | 19 | import androidx.compose.foundation.layout.Box 20 | import androidx.compose.ui.Modifier 21 | import com.tunjid.treenav.Node 22 | import com.tunjid.treenav.compose.MultiPaneDisplayState 23 | import com.tunjid.treenav.compose.PaneScope 24 | 25 | /** 26 | * A [MultiPaneDisplayState] that allows for centrally defining the [Modifier] for 27 | * each [Pane] displayed within it. 28 | * 29 | * @param paneModifier a lambda for specifying the [Modifier] for each [Pane] in a [PaneScope]. 30 | */ 31 | fun paneModifierTransform( 32 | paneModifier: PaneScope.() -> Modifier = { Modifier }, 33 | ): Transform = 34 | RenderTransform { destination, previousTransform -> 35 | Box( 36 | modifier = paneModifier() 37 | ) { 38 | previousTransform(destination) 39 | } 40 | } -------------------------------------------------------------------------------- /library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/Transforms.kt: -------------------------------------------------------------------------------- 1 | package com.tunjid.treenav.compose.transforms 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.tunjid.treenav.Node 5 | import com.tunjid.treenav.compose.MultiPaneDisplay 6 | import com.tunjid.treenav.compose.MultiPaneDisplayState 7 | import com.tunjid.treenav.compose.PaneScope 8 | 9 | /** 10 | * Provides APIs for adjusting what is presented in a [MultiPaneDisplay]. 11 | */ 12 | sealed interface Transform 13 | 14 | /** 15 | * A [Transform] that allows for changing the current [Destination] in the [MultiPaneDisplay] 16 | * sees without actually modifying the backing [NavigationState]. 17 | */ 18 | fun interface DestinationTransform 19 | : Transform { 20 | 21 | /** 22 | * Given a [NavigationState], provide the current [Destination] to show. The [Destination] 23 | * returned must already exist in the back stack of the [MultiPaneDisplayState.navigationState]. 24 | * 25 | * @param navigationState the current navigation state. 26 | * @param previousTransform a [Transform] that when invoked, returns the [Destination] that 27 | * would have been shown pre-transform that can then be composed with new logic. 28 | */ 29 | fun toDestination( 30 | navigationState: NavigationState, 31 | previousTransform: (NavigationState) -> Destination, 32 | ): Destination 33 | } 34 | 35 | /** 36 | * A [Transform] that allows for changing which [Destination] shows in which [Pane]. 37 | */ 38 | fun interface PaneTransform 39 | : Transform { 40 | 41 | /** 42 | * Given the current [Destination], provide what [Destination] to show in a [Pane]. 43 | * Each [Destination] in the returned mapping must already exist in the 44 | * back stack of the [MultiPaneDisplayState.navigationState]. 45 | * 46 | * @param destination the current [Destination] to display. 47 | * @param previousTransform a [Transform] that when invoked, returns the pane to destination 48 | * mapping for the current [Destination] pre-transform that can then be composed with new logic. 49 | */ 50 | @Composable 51 | fun toPanesAndDestinations( 52 | destination: Destination, 53 | previousTransform: @Composable (Destination) -> Map, 54 | ): Map 55 | } 56 | 57 | /** 58 | * A [Transform] that allows for the rendering semantics of a [Destination] in a given 59 | * [PaneScope]. 60 | */ 61 | fun interface RenderTransform 62 | : Transform { 63 | 64 | /** 65 | * Given the current [Destination], and its [PaneScope], compose additional presentation 66 | * logic. 67 | * 68 | * @param destination the current [Destination] to display in the provided [PaneScope]. 69 | * @param previousTransform a [Transform] that when invoked, renders the [Destination] 70 | * for the [PaneScope ]pre-transform that can then be composed with new logic. 71 | */ 72 | @Composable 73 | fun PaneScope.Render( 74 | destination: Destination, 75 | previousTransform: @Composable PaneScope.(Destination) -> Unit, 76 | ) 77 | } 78 | 79 | internal class CompoundTransform( 80 | destinationTransform: DestinationTransform?, 81 | paneTransform: PaneTransform?, 82 | renderTransform: RenderTransform?, 83 | ) : Transform { 84 | val transforms = listOfNotNull( 85 | destinationTransform, 86 | paneTransform, 87 | renderTransform, 88 | ) 89 | } 90 | 91 | /** 92 | * Creates a transform that an aggregation of the transforms provided to it. 93 | * 94 | * @see DestinationTransform 95 | * @see PaneTransform 96 | * @see RenderTransform 97 | */ 98 | fun compoundTransform( 99 | destinationTransform: DestinationTransform? = null, 100 | paneTransform: PaneTransform? = null, 101 | renderTransform: RenderTransform? = null, 102 | ): Transform = CompoundTransform( 103 | destinationTransform = destinationTransform, 104 | paneTransform = paneTransform, 105 | renderTransform = renderTransform, 106 | ) -------------------------------------------------------------------------------- /library/compose/src/commonTest/kotlin/com/tunjid/treenav/compose/StackNavExtTest.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 | * http://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.treenav.compose 18 | 19 | import com.tunjid.treenav.MultiStackNav 20 | import com.tunjid.treenav.StackNav 21 | import com.tunjid.treenav.backStack 22 | import com.tunjid.treenav.push 23 | import com.tunjid.treenav.switch 24 | import kotlin.test.Test 25 | import kotlin.test.assertEquals 26 | 27 | 28 | class StackNavExtTest { 29 | 30 | @Test 31 | fun testStackNavMultiPaneDisplayBackstack() { 32 | 33 | val subject = StackNav(name = "subject") 34 | 35 | val pushed = subject 36 | .push(TestNode("A", children = listOf(TestNode("1")))) 37 | .push(TestNode("B")) 38 | .push(TestNode("C")) 39 | .push(TestNode("D")) 40 | .push(TestNode("E", children = listOf(TestNode("1"), TestNode("2")))) 41 | .push(TestNode("F")) 42 | 43 | println( 44 | pushed.backStack( 45 | includeCurrentDestinationChildren = true, 46 | placeChildrenBeforeParent = true, 47 | distinctDestinations = true, 48 | ) 49 | ) 50 | assertEquals( 51 | expected = pushed.backStack( 52 | includeCurrentDestinationChildren = true, 53 | placeChildrenBeforeParent = true, 54 | distinctDestinations = true, 55 | ), 56 | actual = pushed.multiPaneDisplayBackstack() 57 | ) 58 | } 59 | 60 | @Test 61 | fun testMultiStackNavMultiPaneDisplayBackstack() { 62 | 63 | val subject = MultiStackNav( 64 | name = "subject", 65 | stacks = listOf("0", "1", "2").map(::StackNav) 66 | ) 67 | 68 | val pushed = subject 69 | .push(TestNode("A")) 70 | .push(TestNode("B")) 71 | .push(TestNode("C")) 72 | .switch(toIndex = 2) 73 | .push(TestNode("D")) 74 | .push(TestNode("E")) 75 | .switch(toIndex = 1) 76 | .push(TestNode("F")) 77 | 78 | assertEquals( 79 | expected = pushed.backStack( 80 | includeCurrentDestinationChildren = true, 81 | placeChildrenBeforeParent = true, 82 | distinctDestinations = true, 83 | ), 84 | actual = pushed.multiPaneDisplayBackstack() 85 | ) 86 | } 87 | } -------------------------------------------------------------------------------- /library/compose/src/jvmMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.jvm.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 | * http://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.treenav.compose.navigation3.decorators 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.lifecycle.SavedStateViewModelFactory 21 | import androidx.savedstate.SavedStateRegistryOwner 22 | 23 | @Composable 24 | internal actual fun shouldRemoveViewModelStoreCallback(): () -> Boolean { 25 | return { true } 26 | } 27 | 28 | internal actual fun SavedStateViewModelFactory( 29 | savedStateRegistryOwner: SavedStateRegistryOwner 30 | ): SavedStateViewModelFactory = SavedStateViewModelFactory() -------------------------------------------------------------------------------- /library/compose/src/main/res/values/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /library/compose/src/nativeMain/kotlin/com/tunjid/treenav/compose/navigation3/decorators/ViewModelStoreNavEntryDecorator.native.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 | * http://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.treenav.compose.navigation3.decorators 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.lifecycle.SavedStateViewModelFactory 21 | import androidx.savedstate.SavedStateRegistryOwner 22 | 23 | @Composable 24 | internal actual fun shouldRemoveViewModelStoreCallback(): () -> Boolean { 25 | return { true } 26 | } 27 | 28 | internal actual fun SavedStateViewModelFactory( 29 | savedStateRegistryOwner: SavedStateRegistryOwner 30 | ): SavedStateViewModelFactory = SavedStateViewModelFactory() -------------------------------------------------------------------------------- /library/strings/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 | * http://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("publishing-library-convention") 20 | id("android-library-convention") 21 | id("kotlin-jvm-convention") 22 | id("kotlin-library-convention") 23 | id("maven-publish") 24 | signing 25 | id("org.jetbrains.dokka") 26 | } 27 | 28 | kotlin { 29 | js(IR) { 30 | nodejs() 31 | browser() 32 | } 33 | linuxX64() 34 | macosX64() 35 | macosArm64() 36 | mingwX64() 37 | tvosSimulatorArm64() 38 | watchosSimulatorArm64() 39 | 40 | sourceSets { 41 | val commonMain by getting { 42 | dependencies { 43 | implementation(project(":library:treenav")) 44 | } 45 | } 46 | commonTest { 47 | dependencies { 48 | implementation(kotlin("test")) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /library/strings/src/commonMain/kotlin/com/tunjid/treenav/strings/Paths.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 | * http://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.treenav.strings 18 | 19 | import kotlin.jvm.JvmInline 20 | 21 | /** 22 | * Defines the pattern for a URL path that may have path arguments within. 23 | * 24 | * Patterns are in the format of /users/{id}. 25 | * 26 | * Patterns support multiple path arguments, and path arguments must have unique names. 27 | */ 28 | @JvmInline 29 | value class PathPattern( 30 | val template: String 31 | ) { 32 | init { 33 | checkNotNull(pathPatternRegex.matchEntire(template)) { 34 | "The pattern provided \"$template\" must be in the format of a URL path" 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * Converts a properly formatted string of a url path and its queries to an instance of 41 | * [RouteParams] 42 | */ 43 | internal fun String.routeParams( 44 | pattern: PathPattern 45 | ) = RouteParams( 46 | pathAndQueries = this, 47 | pathArgs = pathArgs(pattern.template), 48 | queryParams = queryParams(), 49 | ) 50 | 51 | /** 52 | * Optimistically parses url query parameters from a string. 53 | * Output is nonsensical for malformed urls. 54 | */ 55 | private fun String.queryParams(): Map> { 56 | val queryString = split("?").last() 57 | val result = mutableMapOf>() 58 | val pairs = queryString.split("&") 59 | pairs.forEach { keyValueString -> 60 | val keyValueArray = keyValueString.split("=") 61 | if (keyValueArray.size != 2) return@forEach 62 | val (key, value) = keyValueArray 63 | result[key] = result.getOrPut(key, ::listOf) + value 64 | } 65 | return result 66 | } 67 | 68 | 69 | private fun String.pathArgs(pattern: String): Map { 70 | val path = split("?").first() 71 | val pathKeys = mutableListOf() 72 | 73 | val basicPattern = pattern.replace(regex = pathRegex) { 74 | pathKeys.add( 75 | it.value.replace( 76 | regex = pathArgRegex, 77 | replacement = "" 78 | ) 79 | ) 80 | "(.*?)" 81 | } 82 | 83 | val result = basicPattern 84 | .toRegex() 85 | .matchEntire(input = path) ?: return emptyMap() 86 | 87 | return pathKeys 88 | .zip(result.groupValues.drop(1)) 89 | .toMap() 90 | } 91 | 92 | // Android's icu regex implementation needs the escape 93 | @Suppress("RegExpRedundantEscape") 94 | private val pathRegex = "\\{(.*?)\\}".toRegex() 95 | private val pathArgRegex = "[{}]".toRegex() 96 | 97 | @Suppress("RegExpRedundantEscape") 98 | private val pathPatternRegex = "^\\/[/.a-zA-Z0-9-{}*]+\$".toRegex() -------------------------------------------------------------------------------- /library/strings/src/commonMain/kotlin/com/tunjid/treenav/strings/Route.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 | * http://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.treenav.strings 18 | 19 | import com.tunjid.treenav.Node 20 | 21 | /** 22 | * A navigation node that represents a navigation destination on the screen. 23 | */ 24 | interface Route : Node { 25 | val routeParams: RouteParams 26 | 27 | override val id: String 28 | get() = routeParams.pathAndQueries.split("?").first() 29 | } 30 | 31 | /** 32 | * Class holding pertinent information for a [String] representation of a [Route] 33 | */ 34 | class RouteParams( 35 | /** 36 | * The path and query strings of a url concatenated 37 | */ 38 | val pathAndQueries: String, 39 | /** 40 | * Arguments for path variables in the string 41 | */ 42 | val pathArgs: Map, 43 | /** 44 | * Arguments for query parameters in the string 45 | */ 46 | val queryParams: Map>, 47 | ) 48 | 49 | fun routeString( 50 | path: String, 51 | queryParams: Map> 52 | ) = when { 53 | queryParams.isEmpty() -> path 54 | else -> { 55 | val queryParameters = queryParams.entries.joinToString(separator = "&") { (key, values) -> 56 | values.joinToString(separator = "&") { value -> 57 | "$key=$value" 58 | } 59 | } 60 | "$path?$queryParameters" 61 | } 62 | } 63 | 64 | /** 65 | * Creates a [Route] containing the specified [params] and [children]. 66 | */ 67 | fun routeOf( 68 | params: RouteParams, 69 | children: List = emptyList(), 70 | ): Route = BasicRoute( 71 | routeParams = params, 72 | children = children 73 | ) 74 | 75 | /** 76 | * Creates a [Route] by combining the [path], [pathArgs] and [queryParams] 77 | * into [RouteParams] with the specified [children]. 78 | */ 79 | @Suppress("unused") 80 | fun routeOf( 81 | path: String, 82 | pathArgs: Map = emptyMap(), 83 | queryParams: Map> = emptyMap(), 84 | children: List = listOf(), 85 | ): Route = BasicRoute( 86 | path = path, 87 | pathArgs = pathArgs, 88 | queryParams = queryParams, 89 | children = children 90 | ) 91 | 92 | /** 93 | * Basic route definition 94 | */ 95 | private class BasicRoute( 96 | override val routeParams: RouteParams, 97 | override val children: List = emptyList(), 98 | ) : Route { 99 | 100 | constructor( 101 | path: String, 102 | pathArgs: Map = emptyMap(), 103 | queryParams: Map> = emptyMap(), 104 | children: List = emptyList(), 105 | ) : this( 106 | routeParams = RouteParams( 107 | pathAndQueries = path, 108 | pathArgs = pathArgs, 109 | queryParams = queryParams, 110 | ), 111 | children = children 112 | ) 113 | 114 | override fun toString(): String = id 115 | override fun equals(other: Any?): Boolean { 116 | if (this === other) return true 117 | if (other !is Route) return false 118 | 119 | return id == other.id 120 | } 121 | 122 | override fun hashCode(): Int = id.hashCode() 123 | } 124 | 125 | -------------------------------------------------------------------------------- /library/strings/src/commonMain/kotlin/com/tunjid/treenav/strings/RouteDelegate.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 | * http://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.treenav.strings 18 | 19 | import kotlin.properties.ReadOnlyProperty 20 | import kotlin.reflect.KProperty 21 | 22 | private class RouteDelegate( 23 | private val isOptional: Boolean, 24 | private val isPath: Boolean, 25 | private val default: T? = null, 26 | private val mapper: (String) -> T, 27 | ) : ReadOnlyProperty { 28 | @Suppress("UNCHECKED_CAST") 29 | override operator fun getValue( 30 | thisRef: Route, 31 | property: KProperty<*> 32 | ): T = when (val value = when { 33 | isPath -> thisRef.routeParams.pathArgs[property.name] 34 | else -> thisRef.routeParams.queryParams[property.name]?.firstOrNull() 35 | }) { 36 | null -> when { 37 | isOptional -> default 38 | else -> default ?: throw NoSuchElementException( 39 | "There is no value for the property '${property.name}' in the Route's ${if (isPath) "path arguments" else "query parameters"}." 40 | ) 41 | } 42 | 43 | else -> mapper(value) 44 | } as T 45 | } 46 | 47 | fun routePath( 48 | default: String? = null 49 | ): ReadOnlyProperty = RouteDelegate( 50 | isOptional = false, 51 | isPath = true, 52 | default = default, 53 | mapper = { it } 54 | ) 55 | 56 | fun mappedRoutePath( 57 | default: T? = null, 58 | mapper: (String) -> T, 59 | ): ReadOnlyProperty = RouteDelegate( 60 | isOptional = false, 61 | isPath = true, 62 | default = default, 63 | mapper = mapper 64 | ) 65 | 66 | fun routeQuery( 67 | default: String? = null 68 | ): ReadOnlyProperty = RouteDelegate( 69 | isOptional = false, 70 | isPath = false, 71 | default = default, 72 | mapper = { it } 73 | ) 74 | 75 | fun mappedRouteQuery( 76 | default: T? = null, 77 | mapper: (String) -> T, 78 | ): ReadOnlyProperty = RouteDelegate( 79 | isOptional = false, 80 | isPath = false, 81 | default = default, 82 | mapper = mapper 83 | ) 84 | 85 | fun optionalRouteQuery( 86 | default: String? = null 87 | ): ReadOnlyProperty = RouteDelegate( 88 | isOptional = true, 89 | isPath = false, 90 | default = default, 91 | mapper = { it } 92 | ) 93 | 94 | fun optionalMappedRouteQuery( 95 | default: T? = null, 96 | mapper: (String) -> T, 97 | ): ReadOnlyProperty = RouteDelegate( 98 | isOptional = true, 99 | isPath = false, 100 | default = default, 101 | mapper = mapper 102 | ) -------------------------------------------------------------------------------- /library/strings/src/commonMain/kotlin/com/tunjid/treenav/strings/RouteMatcher.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 | * http://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.treenav.strings 18 | 19 | /** 20 | * Matches a specific [pattern] to a specific [Route]. 21 | */ 22 | interface RouteMatcher { 23 | 24 | /** 25 | * The [pattern] describing the general form of the [Route] to match. 26 | */ 27 | val pattern: PathPattern 28 | 29 | /** 30 | * Creates a [Route] from the parsed parameter parameters. 31 | */ 32 | fun route(params: RouteParams): Route 33 | } 34 | 35 | /** 36 | * Parses urls to create [Route] instances. 37 | */ 38 | fun interface RouteParser { 39 | 40 | /** 41 | * Given a url [pathAndQueries] defining a navigation [Route], this method attempts to create 42 | * the matching [Route] for it, otherwise it returns null. 43 | */ 44 | fun parse(pathAndQueries: String): Route? 45 | } 46 | 47 | /** 48 | * Creates a [RouteParser] from a list of [RouteMatcher]s. 49 | * Note that the order of [RouteMatcher]s matter; place more specific matchers first. 50 | */ 51 | fun routeParserFrom( 52 | vararg matchers: RouteMatcher 53 | ): RouteParser = object : RouteParser { 54 | 55 | private val rootTrieNode = matchers.fold( 56 | TrieNode() 57 | ) { routeNode, matcher -> 58 | routeNode.insert( 59 | pattern = matcher.pattern, 60 | item = matcher, 61 | ) 62 | routeNode 63 | } 64 | 65 | override fun parse(pathAndQueries: String): Route? { 66 | val matcher = rootTrieNode.find( 67 | path = pathAndQueries, 68 | ) ?: return null 69 | 70 | return matcher.route( 71 | pathAndQueries.routeParams(matcher.pattern) 72 | ) 73 | } 74 | } 75 | 76 | /** 77 | * Convenience function for creating a [RouteMatcher] for parsing 78 | * URL-like [String] representations of [Route]s 79 | */ 80 | fun urlRouteMatcher( 81 | routePattern: String, 82 | routeMapper: (RouteParams) -> Route 83 | ) = object : RouteMatcher { 84 | 85 | override val pattern = PathPattern(routePattern) 86 | 87 | override fun route(params: RouteParams): Route = routeMapper(params) 88 | } 89 | -------------------------------------------------------------------------------- /library/strings/src/commonMain/kotlin/com/tunjid/treenav/strings/RouteTrie.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 | * http://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.treenav.strings 18 | 19 | /** 20 | * A class for looking up items that match a certain path pattern by route 21 | */ 22 | class RouteTrie { 23 | 24 | private val rootTrieNode = TrieNode() 25 | 26 | operator fun set(pattern: PathPattern, item: T) = rootTrieNode.insert( 27 | pattern = pattern, 28 | item = item, 29 | ) 30 | 31 | operator fun get(route: Route): T? = rootTrieNode.find( 32 | path = route.id, 33 | ) 34 | } 35 | 36 | /** 37 | * Converts this [Map] to a [RouteTrie] capable of looking up any type [T] by matching it 38 | * to it's [PathPattern]. 39 | */ 40 | fun Map.toRouteTrie() = RouteTrie().apply { 41 | forEach { (pattern, value) -> set(pattern, value) } 42 | } -------------------------------------------------------------------------------- /library/strings/src/commonMain/kotlin/com/tunjid/treenav/strings/TrieNode.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 | * http://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.treenav.strings 18 | 19 | /** 20 | * A trie for looking up paths by segments 21 | */ 22 | internal class TrieNode { 23 | val children: MutableMap> = HashMap() 24 | var item: T? = null 25 | } 26 | 27 | internal fun TrieNode.insert(pattern: PathPattern, item: T) = insert( 28 | segments = pattern.segments, 29 | item = item, 30 | index = 0, 31 | ) 32 | 33 | internal fun TrieNode.find(path: String): T? = find( 34 | segments = path.pathSegments(), 35 | index = 0, 36 | ) 37 | 38 | private fun TrieNode.insert(segments: List, item: T, index: Int) { 39 | if (index == segments.size) { 40 | this.item = item 41 | return 42 | } 43 | val segment = segments[index].asPathSegment 44 | if (segment !in children) children[segment] = TrieNode() 45 | 46 | children.getValue(segment).insert( 47 | segments = segments, 48 | item = item, 49 | index = index + 1 50 | ) 51 | } 52 | 53 | private fun TrieNode.find(segments: List, index: Int): T? { 54 | if (index == segments.size) return item 55 | val segment = segments[index] 56 | 57 | val child = children[segment] 58 | // Try matching against a parameter 59 | ?: children[PARAMETER_MATCHER] 60 | ?: return null 61 | 62 | return child.find( 63 | segments = segments, 64 | index = index + 1 65 | ) 66 | } 67 | 68 | private fun String.pathSegments(): List = split("?") 69 | .first() 70 | .split("/") 71 | .filter(String::isNotBlank) 72 | 73 | private val String.asPathSegment 74 | get() = when { 75 | isPathParameter -> PARAMETER_MATCHER 76 | else -> this 77 | } 78 | 79 | private val String.isPathParameter get() = first() == '{' && last() == '}' 80 | 81 | private val PathPattern.segments get() = template.pathSegments() 82 | 83 | private const val PARAMETER_MATCHER = "**PARAMETER**" 84 | -------------------------------------------------------------------------------- /library/strings/src/commonTest/kotlin/RouteDelegateTest.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 | * http://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.treenav.strings.Route 18 | import com.tunjid.treenav.strings.mappedRoutePath 19 | import com.tunjid.treenav.strings.mappedRouteQuery 20 | import com.tunjid.treenav.strings.optionalMappedRouteQuery 21 | import com.tunjid.treenav.strings.optionalRouteQuery 22 | import com.tunjid.treenav.strings.routeOf 23 | import com.tunjid.treenav.strings.routePath 24 | import com.tunjid.treenav.strings.routeQuery 25 | import kotlin.test.Test 26 | import kotlin.test.assertEquals 27 | import kotlin.test.assertFailsWith 28 | 29 | 30 | class RouteDelegateTest { 31 | 32 | @Test 33 | fun testDelegateReading() { 34 | 35 | val route = routeOf( 36 | path = "users", 37 | pathArgs = mapOf( 38 | "userId" to "1" 39 | ), 40 | queryParams = mapOf( 41 | "page" to listOf("0"), 42 | "offset" to listOf("10"), 43 | ), 44 | ) 45 | 46 | assertEquals( 47 | expected = 1, 48 | actual = route.userId, 49 | ) 50 | 51 | assertEquals( 52 | expected = 0, 53 | actual = route.page, 54 | ) 55 | 56 | assertEquals( 57 | expected = "10", 58 | actual = route.offset, 59 | ) 60 | 61 | assertEquals( 62 | expected = 100, 63 | actual = route.count, 64 | ) 65 | 66 | assertEquals( 67 | expected = "profilePictures", 68 | actual = route.category, 69 | ) 70 | } 71 | 72 | @Test 73 | fun testDelegateDefaults() { 74 | val route = routeOf( 75 | path = "users", 76 | ) 77 | 78 | assertEquals( 79 | expected = "2", 80 | actual = route.defaultProfileId, 81 | ) 82 | assertEquals( 83 | expected = 20, 84 | actual = route.defaultPage, 85 | ) 86 | 87 | assertEquals( 88 | expected = "200", 89 | actual = route.defaultOffset, 90 | ) 91 | } 92 | 93 | @Test 94 | fun testDelegateExceptions() { 95 | val route = routeOf( 96 | path = "users", 97 | ) 98 | 99 | assertFailsWith { 100 | route.userId 101 | } 102 | assertFailsWith { 103 | route.profileId 104 | } 105 | assertFailsWith { 106 | route.page 107 | } 108 | assertFailsWith { 109 | route.offset 110 | } 111 | 112 | assertEquals( 113 | expected = 100, 114 | actual = route.count, 115 | ) 116 | 117 | assertEquals( 118 | expected = "profilePictures", 119 | actual = route.category, 120 | ) 121 | } 122 | } 123 | 124 | private val Route.userId by mappedRoutePath( 125 | mapper = String::toInt, 126 | ) 127 | 128 | private val Route.profileId by routePath() 129 | 130 | private val Route.page by mappedRouteQuery( 131 | mapper = String::toInt, 132 | ) 133 | 134 | private val Route.offset by routeQuery() 135 | 136 | private val Route.count by optionalMappedRouteQuery( 137 | mapper = String::toInt, 138 | default = 100, 139 | ) 140 | 141 | private val Route.category by optionalRouteQuery( 142 | default = "profilePictures" 143 | ) 144 | 145 | private val Route.defaultProfileId by routePath("2") 146 | 147 | private val Route.defaultPage by mappedRouteQuery( 148 | default = 20, 149 | mapper = String::toInt, 150 | ) 151 | 152 | private val Route.defaultOffset by routeQuery( 153 | default = "200", 154 | ) 155 | 156 | -------------------------------------------------------------------------------- /library/strings/src/commonTest/kotlin/RouteMatcherTest.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 | * http://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.treenav.strings.routeOf 18 | import com.tunjid.treenav.strings.routeParserFrom 19 | import com.tunjid.treenav.strings.routeString 20 | import com.tunjid.treenav.strings.urlRouteMatcher 21 | import kotlin.test.Test 22 | import kotlin.test.assertEquals 23 | 24 | 25 | class UrlRouteMatcherTest { 26 | 27 | @Test 28 | fun testSimpleUrlRouteMatching() { 29 | val routeParser = routeParserFrom( 30 | urlRouteMatcher( 31 | routePattern = "/users/{id}", 32 | routeMapper = ::routeOf 33 | ) 34 | ) 35 | val route = routeParser.parse("/users/jeff") 36 | 37 | assertEquals( 38 | expected = "/users/jeff", 39 | actual = route?.id 40 | ) 41 | assertEquals( 42 | expected = "jeff", 43 | actual = route?.routeParams?.pathArgs?.get("id") 44 | ) 45 | } 46 | 47 | @Test 48 | fun testSimpleUrlRouteMatchingWithQueryParams() { 49 | val routeParser = routeParserFrom( 50 | urlRouteMatcher( 51 | routePattern = "/users/{id}", 52 | routeMapper = ::routeOf 53 | ) 54 | ) 55 | val routeString = routeString( 56 | path = "/users/jeff", 57 | queryParams = mapOf( 58 | "age" to listOf("27"), 59 | "job" to listOf("dev"), 60 | "hobby" to listOf("running", "reading"), 61 | ) 62 | ) 63 | 64 | assertEquals( 65 | expected = "/users/jeff?age=27&job=dev&hobby=running&hobby=reading", 66 | actual = routeString, 67 | ) 68 | 69 | val route = routeParser.parse(routeString) 70 | 71 | assertEquals( 72 | expected = "/users/jeff", 73 | actual = route?.id 74 | ) 75 | assertEquals( 76 | expected = "jeff", 77 | actual = route?.routeParams?.pathArgs?.get("id") 78 | ) 79 | assertEquals( 80 | expected = "27", 81 | actual = route?.routeParams?.queryParams?.get("age")?.first() 82 | ) 83 | assertEquals( 84 | expected = "dev", 85 | actual = route?.routeParams?.queryParams?.get("job")?.first() 86 | ) 87 | assertEquals( 88 | expected = listOf("running", "reading"), 89 | actual = route?.routeParams?.queryParams?.get("hobby") 90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /library/strings/src/commonTest/kotlin/RouteTrieTest.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 | * http://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.treenav.strings.PathPattern 18 | import com.tunjid.treenav.strings.routeOf 19 | import com.tunjid.treenav.strings.toRouteTrie 20 | import kotlin.test.Test 21 | import kotlin.test.assertEquals 22 | 23 | 24 | class RouteTrieTest { 25 | 26 | @Test 27 | fun testReTrieVal() { 28 | val patterns = listOf( 29 | "/users", 30 | "/users/{id}", 31 | "/users/{id}/photos/grid", 32 | "/users/{id}/photos/pager", 33 | "/home", 34 | "/settings", 35 | "/sign-in", 36 | ).map(::PathPattern) 37 | 38 | val routeTrie = patterns 39 | .mapIndexed { index, pattern -> 40 | pattern to index 41 | } 42 | .toMap() 43 | .toRouteTrie() 44 | 45 | assertEquals( 46 | expected = 0, 47 | actual = routeTrie[routeOf("users")], 48 | message = "users path should be found at index 0", 49 | ) 50 | assertEquals( 51 | expected = 1, 52 | actual = routeTrie[routeOf("users/1")], 53 | message = "users path with parameter should be found at index 1", 54 | ) 55 | assertEquals( 56 | expected = null, 57 | actual = routeTrie[routeOf("users/1/photos")], 58 | message = "users path with photos suffix parameter should not be found", 59 | ) 60 | assertEquals( 61 | expected = 2, 62 | actual = routeTrie[routeOf("users/1/photos/grid")], 63 | message = "users path with parameter and photos and grid segments should be found at index 2", 64 | ) 65 | assertEquals( 66 | expected = 3, 67 | actual = routeTrie[routeOf("users/1/photos/pager")], 68 | message = "users path with parameter and photos and pager segments should be found at index 3", 69 | 70 | ) 71 | assertEquals( 72 | expected = null, 73 | actual = routeTrie[routeOf("users/1/photos/pager/oops")], 74 | message = "users path with 'o' parameter suffix should not be found", 75 | ) 76 | assertEquals( 77 | expected = 3, 78 | actual = routeTrie[routeOf("users/1/photos/pager?page=0")], 79 | message = "users path with parameter and photos and pager segments with queries should be found at index 3", 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /library/strings/src/commonTest/kotlin/TrieNodeTest.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 | * http://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.treenav.strings.PathPattern 18 | import com.tunjid.treenav.strings.TrieNode 19 | import com.tunjid.treenav.strings.find 20 | import com.tunjid.treenav.strings.insert 21 | import kotlin.test.Test 22 | import kotlin.test.assertEquals 23 | 24 | 25 | class TrieNodeTest { 26 | 27 | @Test 28 | fun testReTrieVal() { 29 | val trieNode = TrieNode() 30 | 31 | val patterns = listOf( 32 | "/users", 33 | "/users/{id}", 34 | "/users/{id}/photos/grid", 35 | "/users/{id}/photos/pager", 36 | "/home", 37 | "/settings", 38 | "/sign-in", 39 | ).map(::PathPattern) 40 | 41 | patterns.forEachIndexed { index, pattern -> 42 | trieNode.insert( 43 | pattern = pattern, 44 | item = index, 45 | ) 46 | } 47 | 48 | assertEquals( 49 | expected = 0, 50 | actual = trieNode.find("users"), 51 | message = "users path should be found at index 0", 52 | ) 53 | assertEquals( 54 | expected = 1, 55 | actual = trieNode.find("users/1"), 56 | message = "users path with parameter should be found at index 1", 57 | ) 58 | assertEquals( 59 | expected = null, 60 | actual = trieNode.find("users/1/photos"), 61 | message = "users path with photos suffix parameter should not be found", 62 | ) 63 | assertEquals( 64 | expected = 2, 65 | actual = trieNode.find("users/1/photos/grid"), 66 | message = "users path with parameter and photos and grid segments should be found at index 2", 67 | ) 68 | assertEquals( 69 | expected = 3, 70 | actual = trieNode.find("users/1/photos/pager"), 71 | message = "users path with parameter and photos and pager segments should be found at index 3", 72 | 73 | ) 74 | assertEquals( 75 | expected = null, 76 | actual = trieNode.find("users/1/photos/pager/oops"), 77 | message = "users path with 'o' parameter suffix should not be found", 78 | ) 79 | assertEquals( 80 | expected = 3, 81 | actual = trieNode.find("users/1/photos/pager?page=0"), 82 | message = "users path with parameter and photos and pager segments with queries should be found at index 3", 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /library/treenav/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 | * http://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("publishing-library-convention") 20 | id("android-library-convention") 21 | id("kotlin-jvm-convention") 22 | id("kotlin-library-convention") 23 | id("maven-publish") 24 | signing 25 | id("org.jetbrains.dokka") 26 | } 27 | 28 | kotlin { 29 | js(IR) { 30 | nodejs() 31 | browser() 32 | } 33 | linuxX64() 34 | macosX64() 35 | macosArm64() 36 | mingwX64() 37 | tvosSimulatorArm64() 38 | watchosSimulatorArm64() 39 | 40 | sourceSets { 41 | commonTest { 42 | dependencies { 43 | implementation(kotlin("test")) 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /library/treenav/src/commonMain/kotlin/com/tunjid/treenav/Node.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 | * http://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.treenav 18 | 19 | /** 20 | * A representation of a navigation node in a navigation tree. 21 | * 22 | * [Node] instances are identified by their [Node.id]. The same [Node] can appear multiple times 23 | * within the tree. To traverse the navigation tree use [Node.traverse], specifying the [Order]. 24 | */ 25 | interface Node { 26 | val id: String 27 | 28 | val children: List get() = listOf() 29 | } 30 | 31 | sealed class Order { 32 | data object BreadthFirst : Order() 33 | data object DepthFirst : Order() 34 | } 35 | 36 | /** 37 | * Traverses each node in the tree with [this] as a parent 38 | */ 39 | 40 | inline fun Node.traverse(order: Order, crossinline onNodeVisited: (Node) -> Unit) = when (order) { 41 | Order.BreadthFirst -> { 42 | val queue = mutableListOf(this) 43 | while (queue.isNotEmpty()) { 44 | // deque. 45 | val node = queue.removeAt(0) 46 | // Visit node. 47 | onNodeVisited(node) 48 | // Push child children on the queue. 49 | for (element in node.children) queue.add(element) 50 | } 51 | } 52 | 53 | Order.DepthFirst -> { 54 | val stack = mutableListOf(this) 55 | while (stack.isNotEmpty()) { 56 | // Pop off end of stack. 57 | val node = stack.removeAt(stack.lastIndex) 58 | // Visit node. 59 | onNodeVisited(node) 60 | // Push child children on the stack. 61 | for (i in node.children.lastIndex downTo 0) stack.add(node.children[i]) 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * Returns a [List] of all [Node]s in the tree with [this] as a parent 68 | */ 69 | fun Node.flatten(order: Order): List { 70 | val result = mutableListOf() 71 | traverse(order, result::add) 72 | return result 73 | } 74 | 75 | /** 76 | * Returns the set of children in [this] that are not present in [node] 77 | */ 78 | operator fun Node.minus(node: Node): Set { 79 | val left = mutableSetOf().let { set -> 80 | this.traverse(Order.BreadthFirst, set::add) 81 | set - this 82 | } 83 | val right = mutableSetOf().let { set -> 84 | node.traverse(Order.BreadthFirst, set::add) 85 | set - node 86 | } 87 | return left - right 88 | } 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /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.treenav 17 | treenav_version=0.0.27 18 | strings_version=0.0.27 19 | compose_version=0.0.27 20 | compose-threepane_version=0.0.27 -------------------------------------------------------------------------------- /sample/android/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/android/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("android-application-convention") 19 | id("kotlin-android") 20 | alias(libs.plugins.compose.compiler) 21 | } 22 | 23 | android { 24 | defaultConfig { 25 | applicationId = "com.tunjid.treenav" 26 | targetSdk = 35 27 | versionCode = 1 28 | versionName = "1.0" 29 | 30 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 31 | } 32 | 33 | buildTypes { 34 | release { 35 | isMinifyEnabled = false 36 | proguardFiles( 37 | getDefaultProguardFile("proguard-android-optimize.txt"), 38 | "proguard-rules.pro" 39 | ) 40 | signingConfig = signingConfigs.getByName("debug") 41 | } 42 | } 43 | } 44 | 45 | dependencies { 46 | implementation(project(":sample:common")) 47 | 48 | implementation(libs.androidx.core.ktx) 49 | implementation(libs.androidx.appcompat) 50 | 51 | implementation(libs.androidx.activity.compose) 52 | implementation(libs.jetbrains.compose.material3) 53 | implementation(libs.jetbrains.compose.animation) 54 | 55 | implementation(libs.google.material) 56 | 57 | implementation(libs.tunjid.composables) 58 | } 59 | -------------------------------------------------------------------------------- /sample/android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /sample/android/src/main/java/com/tunjid/tyler/MainActivity.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.tyler 18 | 19 | import android.os.Bundle 20 | import androidx.activity.BackEventCompat 21 | import androidx.activity.compose.PredictiveBackHandler 22 | import androidx.activity.compose.setContent 23 | import androidx.activity.enableEdgeToEdge 24 | import androidx.appcompat.app.AppCompatActivity 25 | import androidx.compose.runtime.remember 26 | import androidx.compose.ui.geometry.Offset 27 | import androidx.compose.ui.unit.IntOffset 28 | import androidx.compose.ui.unit.round 29 | import com.tunjid.demo.common.ui.App 30 | import com.tunjid.demo.common.ui.AppState 31 | import com.tunjid.demo.common.ui.AppTheme 32 | import kotlinx.coroutines.flow.Flow 33 | import kotlin.coroutines.cancellation.CancellationException 34 | 35 | class MainActivity : AppCompatActivity() { 36 | override fun onCreate(savedInstanceState: Bundle?) { 37 | super.onCreate(savedInstanceState) 38 | enableEdgeToEdge() 39 | setContent { 40 | AppTheme { 41 | val appState = remember { AppState() } 42 | App(appState) 43 | 44 | PredictiveBackHandler { backEvents: Flow -> 45 | try { 46 | backEvents.collect { backEvent -> 47 | appState.backPreviewState.apply { 48 | atStart = backEvent.swipeEdge == BackEventCompat.EDGE_LEFT 49 | progress = backEvent.progress 50 | pointerOffset = Offset( 51 | x = backEvent.touchX, 52 | y = backEvent.touchY 53 | ).round() 54 | } 55 | } 56 | // Dismiss back preview 57 | appState.backPreviewState.apply { 58 | progress = Float.NaN 59 | pointerOffset = IntOffset.Zero 60 | } 61 | // Pop navigation 62 | appState.goBack() 63 | } catch (e: CancellationException) { 64 | appState.backPreviewState.apply { 65 | progress = Float.NaN 66 | pointerOffset = IntOffset.Zero 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /sample/android/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | 25 | 31 | 34 | 37 | 38 | 39 | 40 | 46 | 47 | -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/android/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/android/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/android/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/android/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/android/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/android/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/android/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 32 | 33 | -------------------------------------------------------------------------------- /sample/android/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | #FFBB86FC 19 | #FF6200EE 20 | #FF3700B3 21 | #FF03DAC5 22 | #FF018786 23 | #FF000000 24 | #FFFFFFFF 25 | 26 | -------------------------------------------------------------------------------- /sample/android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Tyler 19 | 20 | -------------------------------------------------------------------------------- /sample/android/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 32 | 33 | -------------------------------------------------------------------------------- /sample/common/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("android-library-convention") 19 | id("kotlin-library-convention") 20 | id("org.jetbrains.compose") 21 | alias(libs.plugins.compose.compiler) 22 | } 23 | 24 | kotlin { 25 | sourceSets { 26 | named("commonMain") { 27 | dependencies { 28 | implementation(project(":library:treenav")) 29 | implementation(project(":library:strings")) 30 | implementation(project(":library:compose")) 31 | implementation(project(":library:compose-threepane")) 32 | 33 | implementation(compose.components.resources) 34 | 35 | implementation(libs.jetbrains.compose.runtime) 36 | implementation(libs.jetbrains.compose.animation) 37 | implementation(libs.jetbrains.compose.material3) 38 | implementation(libs.jetbrains.compose.foundation.layout) 39 | implementation(libs.jetbrains.compose.ui.backhandler) 40 | 41 | implementation(libs.jetbrains.lifecycle.runtime) 42 | implementation(libs.jetbrains.lifecycle.runtime.compose) 43 | implementation(libs.jetbrains.lifecycle.viewmodel) 44 | implementation(libs.jetbrains.lifecycle.viewmodel.compose) 45 | 46 | implementation(libs.jetbrains.compose.material3.adaptive) 47 | implementation(libs.jetbrains.compose.material3.adaptive.layout) 48 | implementation(libs.jetbrains.compose.material3.adaptive.navigation.suite) 49 | 50 | implementation(libs.jetbrains.compose.material.icons.core) 51 | implementation(libs.jetbrains.compose.material.icons.extended) 52 | 53 | implementation(libs.kotlinx.coroutines.core) 54 | implementation(libs.kotlinx.datetime) 55 | 56 | implementation(libs.tunjid.mutator.core.common) 57 | implementation(libs.tunjid.mutator.coroutines.common) 58 | implementation(libs.tunjid.composables) 59 | } 60 | } 61 | named("androidMain") { 62 | dependencies { 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /sample/common/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 28 | 29 | 35 | 38 | 41 | 42 | 43 | 44 | 50 | 51 | -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/common/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Tiler Demo 19 | 20 | -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/aisha-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/aisha-1.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/aisha-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/aisha-2.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/aisha-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/aisha-3.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/aisha-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/aisha-4.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/bjorn-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/bjorn-1.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/bjorn-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/bjorn-2.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/bjorn-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/bjorn-3.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/bjorn-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/bjorn-4.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/diego-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/diego-1.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/diego-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/diego-2.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/diego-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/diego-3.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/diego-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/diego-4.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/kenji-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/kenji-1.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/kenji-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/kenji-2.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/kenji-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/kenji-3.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/kenji-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/kenji-4.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/lin-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/lin-1.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/lin-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/lin-2.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/lin-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/lin-3.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/composeResources/drawable/lin-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunjid/treeNav/80733c7036514e3c093d5aa169d76e7e8ed89f1c/sample/common/src/commonMain/composeResources/drawable/lin-4.jpg -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/AppBars.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 | * http://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.demo.common.ui 18 | 19 | 20 | import androidx.compose.foundation.layout.WindowInsets 21 | import androidx.compose.foundation.layout.statusBars 22 | import androidx.compose.material.icons.Icons 23 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 24 | import androidx.compose.material3.ExperimentalMaterial3Api 25 | import androidx.compose.material3.Icon 26 | import androidx.compose.material3.IconButton 27 | import androidx.compose.material3.Text 28 | import androidx.compose.material3.TopAppBar 29 | import androidx.compose.material3.TopAppBarDefaults 30 | import androidx.compose.runtime.Composable 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.graphics.Color 33 | import androidx.compose.ui.unit.Dp 34 | import androidx.compose.ui.unit.dp 35 | import com.tunjid.composables.collapsingheader.CollapsingHeaderState 36 | import com.tunjid.composables.collapsingheader.rememberCollapsingHeaderState 37 | 38 | @Composable 39 | fun rememberAppBarCollapsingHeaderState( 40 | expandedHeight: Dp 41 | ): CollapsingHeaderState { 42 | val statusBars = WindowInsets.statusBars 43 | return rememberCollapsingHeaderState( 44 | collapsedHeight = { 45 | 56.dp.toPx() + 46 | statusBars.getTop(this).toFloat() + 47 | statusBars.getBottom(this).toFloat() 48 | }, 49 | initialExpandedHeight = { expandedHeight.toPx() } 50 | ) 51 | } 52 | 53 | @OptIn(ExperimentalMaterial3Api::class) 54 | @Composable 55 | fun SampleTopAppBar( 56 | title: String, 57 | onBackPressed: (() -> Unit)? = null, 58 | modifier: Modifier = Modifier, 59 | ) { 60 | TopAppBar( 61 | title = { 62 | Text(text = title) 63 | }, 64 | navigationIcon = { 65 | if (onBackPressed != null) IconButton( 66 | onClick = onBackPressed, 67 | content = { 68 | Icon( 69 | imageVector = Icons.AutoMirrored.Filled.ArrowBack, 70 | contentDescription = null, 71 | ) 72 | } 73 | ) 74 | }, 75 | colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), 76 | modifier = modifier, 77 | ) 78 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DragToPop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Adetunji Dahunsi 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 | * http://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.demo.common.ui 18 | 19 | import androidx.compose.foundation.layout.Box 20 | import androidx.compose.foundation.layout.offset 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.DisposableEffect 23 | import androidx.compose.runtime.Stable 24 | import androidx.compose.runtime.State 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.runtime.setValue 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.platform.LocalDensity 31 | import androidx.compose.ui.unit.IntOffset 32 | import androidx.compose.ui.unit.dp 33 | import androidx.compose.ui.unit.round 34 | import com.tunjid.composables.dragtodismiss.DragToDismissState 35 | import com.tunjid.composables.dragtodismiss.dragToDismiss 36 | import com.tunjid.demo.common.ui.data.SampleDestination 37 | import com.tunjid.treenav.compose.MultiPaneDisplayScope 38 | import com.tunjid.treenav.compose.threepane.ThreePane 39 | 40 | @Stable 41 | internal class DragToPopState { 42 | var isDraggingToPop by mutableStateOf(false) 43 | internal val dragToDismissState = DragToDismissState( 44 | enabled = false, 45 | ) 46 | } 47 | 48 | @Composable 49 | fun Modifier.dragToPop(): Modifier { 50 | val state = LocalAppState.current.dragToPopState 51 | DisposableEffect(state) { 52 | state.dragToDismissState.enabled = true 53 | onDispose { state.dragToDismissState.enabled = false } 54 | } 55 | // TODO: This should not be necessary. Figure out why a frame renders with 56 | // an offset of zero while the content in the transient primary container 57 | // is still visible. 58 | val dragToDismissOffset by rememberUpdatedStateIf( 59 | value = state.dragToDismissState.offset.round(), 60 | predicate = { 61 | it != IntOffset.Zero 62 | } 63 | ) 64 | return offset { dragToDismissOffset } 65 | } 66 | 67 | @Composable 68 | internal fun MultiPaneDisplayScope.DragToPopLayout( 69 | state: AppState, 70 | pane: ThreePane, 71 | ) { 72 | // Only place the DragToDismiss Modifier on the Primary pane 73 | if (pane == ThreePane.Primary) { 74 | Box( 75 | modifier = Modifier.dragToPopInternal(state) 76 | ) { 77 | Destination(pane) 78 | } 79 | // Place the transient primary screen above the primary 80 | Destination(ThreePane.TransientPrimary) 81 | } else { 82 | Destination(pane) 83 | } 84 | } 85 | 86 | @Composable 87 | private fun Modifier.dragToPopInternal(state: AppState): Modifier { 88 | val density = LocalDensity.current 89 | val dismissThreshold = remember { with(density) { 200.dp.toPx().let { it * it } } } 90 | 91 | return dragToDismiss( 92 | state = state.dragToPopState.dragToDismissState, 93 | dragThresholdCheck = { offset, _ -> 94 | offset.getDistanceSquared() > dismissThreshold 95 | }, 96 | // Enable back preview 97 | onStart = { 98 | state.dragToPopState.isDraggingToPop = true 99 | }, 100 | onCancelled = { 101 | // Dismiss back preview 102 | state.dragToPopState.isDraggingToPop = false 103 | }, 104 | onDismissed = { 105 | // Dismiss back preview 106 | state.dragToPopState.isDraggingToPop = false 107 | 108 | // Pop navigation 109 | state.goBack() 110 | } 111 | ) 112 | } 113 | 114 | @Composable 115 | private inline fun rememberUpdatedStateIf( 116 | value: T, 117 | predicate: (T) -> Boolean, 118 | ): State = remember { 119 | mutableStateOf(value) 120 | }.also { if (predicate(value)) it.value = value } 121 | 122 | -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PredictiveBack.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Adetunji Dahunsi 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 | * http://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.demo.common.ui 18 | 19 | import androidx.compose.animation.core.Spring 20 | import androidx.compose.animation.core.animate 21 | import androidx.compose.animation.core.spring 22 | import androidx.compose.foundation.background 23 | import androidx.compose.foundation.shape.RoundedCornerShape 24 | import androidx.compose.material3.MaterialTheme 25 | import androidx.compose.material3.surfaceColorAtElevation 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.LaunchedEffect 28 | import androidx.compose.runtime.getValue 29 | import androidx.compose.runtime.mutableStateOf 30 | import androidx.compose.runtime.remember 31 | import androidx.compose.runtime.setValue 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.draw.clip 34 | import androidx.compose.ui.unit.dp 35 | import com.tunjid.treenav.compose.PaneScope 36 | import com.tunjid.treenav.compose.threepane.ThreePane 37 | 38 | @Composable 39 | fun Modifier.predictiveBackBackgroundModifier( 40 | paneScope: PaneScope, 41 | ): Modifier { 42 | if (paneScope.paneState.pane != ThreePane.TransientPrimary) 43 | return this 44 | 45 | var elevation by remember { mutableStateOf(0.dp) } 46 | LaunchedEffect(Unit) { 47 | animate( 48 | initialValue = 0f, 49 | targetValue = 4f, 50 | animationSpec = spring(stiffness = Spring.StiffnessVeryLow) 51 | ) { value, _ -> elevation = value.dp } 52 | } 53 | return background( 54 | color = MaterialTheme.colorScheme.surfaceColorAtElevation(elevation), 55 | shape = RoundedCornerShape(16.dp) 56 | ) 57 | .clip(RoundedCornerShape(16.dp)) 58 | 59 | } 60 | -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/ProfilePhoto.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 | * http://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.demo.common.ui 18 | 19 | import androidx.compose.animation.core.animateDpAsState 20 | import androidx.compose.foundation.Image 21 | import androidx.compose.foundation.shape.RoundedCornerShape 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.clip 25 | import androidx.compose.ui.layout.ContentScale 26 | import androidx.compose.ui.unit.Dp 27 | import androidx.compose.ui.unit.dp 28 | import com.tunjid.composables.ui.animate 29 | import org.jetbrains.compose.resources.ExperimentalResourceApi 30 | import org.jetbrains.compose.resources.painterResource 31 | import treenavigation.sample.common.generated.resources.Res 32 | import treenavigation.sample.common.generated.resources.allDrawableResources 33 | 34 | @Composable 35 | fun ProfilePhoto( 36 | args: ProfilePhotoArgs, 37 | modifier: Modifier = Modifier, 38 | ) { 39 | Image( 40 | modifier = modifier 41 | .clip(RoundedCornerShape(animateDpAsState(args.cornerRadius).value)), 42 | contentScale = args.contentScale.animate(), 43 | contentDescription = args.contentDescription, 44 | painter = painterResource(args.profilePhotoResource()) 45 | ) 46 | } 47 | 48 | data class ProfilePhotoArgs( 49 | val profileName: String, 50 | val contentScale: ContentScale, 51 | val cornerRadius: Dp = 0.dp, 52 | val contentDescription: String? = null, 53 | ) 54 | 55 | @OptIn(ExperimentalResourceApi::class) 56 | private fun ProfilePhotoArgs.profilePhotoResource() = 57 | Res.allDrawableResources.getValue("${profileName.lowercase()}_1") 58 | -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/Theme.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.demo.common.ui 18 | 19 | import androidx.compose.foundation.isSystemInDarkTheme 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.darkColorScheme 22 | import androidx.compose.material3.lightColorScheme 23 | 24 | import androidx.compose.runtime.Composable 25 | 26 | private val DarkColorPalette = darkColorScheme() 27 | 28 | private val LightColorPalette = lightColorScheme() 29 | 30 | @Composable 31 | fun AppTheme( 32 | darkTheme: Boolean = isSystemInDarkTheme(), 33 | content: @Composable () -> Unit 34 | ) { 35 | val colors = if (darkTheme) { 36 | DarkColorPalette 37 | } else { 38 | LightColorPalette 39 | } 40 | 41 | MaterialTheme( 42 | colorScheme = colors, 43 | content = content 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/avatar/AvatarScreen.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 | * http://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.demo.common.ui.avatar 18 | 19 | import androidx.compose.animation.ExperimentalSharedTransitionApi 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.aspectRatio 22 | import androidx.compose.foundation.layout.fillMaxSize 23 | import androidx.compose.foundation.layout.fillMaxWidth 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.layout.ContentScale 28 | import com.tunjid.demo.common.ui.ProfilePhoto 29 | import com.tunjid.demo.common.ui.ProfilePhotoArgs 30 | import com.tunjid.demo.common.ui.dragToPop 31 | import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope 32 | import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf 33 | 34 | @OptIn(ExperimentalSharedTransitionApi::class) 35 | @Composable 36 | fun AvatarScreen( 37 | movableSharedElementScope: MovableSharedElementScope, 38 | state: State, 39 | onAction: (Action) -> Unit, 40 | modifier: Modifier = Modifier, 41 | ) { 42 | 43 | Box( 44 | modifier = modifier 45 | .dragToPop() 46 | .fillMaxSize() 47 | ) { 48 | val profileName = state.profileName ?: state.profile?.name ?: "" 49 | movableSharedElementScope.updatedMovableSharedElementOf( 50 | key = "${state.roomName}-$profileName", 51 | state = ProfilePhotoArgs( 52 | profileName = profileName, 53 | contentScale = ContentScale.Crop, 54 | contentDescription = null, 55 | ), 56 | modifier = Modifier 57 | .align(Alignment.Center) 58 | .fillMaxWidth() 59 | .aspectRatio(1f), 60 | sharedElement = { args: ProfilePhotoArgs, innerModifier: Modifier -> 61 | ProfilePhoto(args, innerModifier) 62 | } 63 | ) 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/avatar/AvatarViewModel.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 | * http://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.demo.common.ui.avatar 18 | 19 | import androidx.lifecycle.LifecycleCoroutineScope 20 | import androidx.lifecycle.ViewModel 21 | import com.tunjid.demo.common.ui.data.NavigationAction 22 | import com.tunjid.demo.common.ui.data.NavigationRepository 23 | import com.tunjid.demo.common.ui.data.Profile 24 | import com.tunjid.demo.common.ui.data.ProfileRepository 25 | import com.tunjid.demo.common.ui.data.navigationAction 26 | import com.tunjid.demo.common.ui.data.navigationMutations 27 | import com.tunjid.mutator.ActionStateMutator 28 | import com.tunjid.mutator.Mutation 29 | import com.tunjid.mutator.coroutines.actionStateFlowMutator 30 | import com.tunjid.mutator.coroutines.mapToMutation 31 | import com.tunjid.mutator.coroutines.toMutationStream 32 | import com.tunjid.treenav.MultiStackNav 33 | import com.tunjid.treenav.pop 34 | import kotlinx.coroutines.flow.Flow 35 | import kotlinx.coroutines.flow.StateFlow 36 | 37 | class AvatarViewModel( 38 | coroutineScope: LifecycleCoroutineScope, 39 | profileRepository: ProfileRepository = ProfileRepository, 40 | navigationRepository: NavigationRepository = NavigationRepository, 41 | profileName: String?, 42 | roomName: String?, 43 | ) : ViewModel(coroutineScope), 44 | ActionStateMutator> by coroutineScope.actionStateFlowMutator( 45 | initialState = State( 46 | roomName = roomName, 47 | profileName = profileName, 48 | ), 49 | inputs = listOf( 50 | profileRepository.profileMutations(profileName) 51 | ), 52 | actionTransform = { actions -> 53 | actions.toMutationStream( 54 | keySelector = Action::key 55 | ) { 56 | when (val type = type()) { 57 | is Action.Navigation -> navigationRepository.navigationMutations( 58 | type.flow 59 | ) 60 | } 61 | } 62 | } 63 | ) 64 | 65 | private fun ProfileRepository.profileMutations( 66 | profileName: String?, 67 | ): Flow> = 68 | (profileName?.let(::profileFor) ?: me) 69 | .mapToMutation { copy(profile = it) } 70 | 71 | data class State( 72 | val roomName: String? = null, 73 | val profileName: String? = null, 74 | val profile: Profile? = null, 75 | ) 76 | 77 | sealed class Action( 78 | val key: String, 79 | ) { 80 | sealed class Navigation : Action("Navigation"), NavigationAction { 81 | data object Pop : Navigation(), NavigationAction by navigationAction( 82 | MultiStackNav::pop 83 | ) 84 | } 85 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/avatar/PaneEntry.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 | * http://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.demo.common.ui.avatar 18 | 19 | import androidx.compose.foundation.layout.fillMaxSize 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.lifecycle.compose.LocalLifecycleOwner 23 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 24 | import androidx.lifecycle.coroutineScope 25 | import androidx.lifecycle.viewmodel.compose.viewModel 26 | import com.tunjid.demo.common.ui.PaneScaffold 27 | import com.tunjid.demo.common.ui.data.SampleDestination 28 | import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs 29 | import com.tunjid.demo.common.ui.rememberPaneScaffoldState 30 | import com.tunjid.treenav.compose.threepane.ThreePane 31 | import com.tunjid.treenav.compose.threepane.threePaneEntry 32 | 33 | fun avatarPaneEntry() = threePaneEntry( 34 | paneMapping = { destination -> 35 | check(destination is SampleDestination.Avatar) 36 | mapOf( 37 | ThreePane.Primary to destination, 38 | ThreePane.Secondary to destination.roomName?.let(SampleDestination::Chat), 39 | ThreePane.Tertiary to destination.roomName?.let { NavTabs.ChatRooms }, 40 | ) 41 | }, 42 | render = { destination -> 43 | check(destination is SampleDestination.Avatar) 44 | val scope = LocalLifecycleOwner.current.lifecycle.coroutineScope 45 | val viewModel = viewModel { 46 | AvatarViewModel( 47 | coroutineScope = scope, 48 | profileName = destination.profileName, 49 | roomName = destination.roomName, 50 | ) 51 | } 52 | rememberPaneScaffoldState().PaneScaffold( 53 | modifier = Modifier 54 | .fillMaxSize(), 55 | containerColor = Color.Transparent, 56 | content = { 57 | AvatarScreen( 58 | movableSharedElementScope = this, 59 | state = viewModel.state.collectAsStateWithLifecycle().value, 60 | onAction = viewModel.accept, 61 | modifier = Modifier.fillMaxSize() 62 | ) 63 | }, 64 | ) 65 | }, 66 | ) -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/PaneEntry.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 | * http://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.demo.common.ui.chat 18 | 19 | import androidx.compose.foundation.layout.fillMaxSize 20 | import androidx.compose.runtime.LaunchedEffect 21 | import androidx.compose.ui.Modifier 22 | import androidx.lifecycle.compose.LocalLifecycleOwner 23 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 24 | import androidx.lifecycle.coroutineScope 25 | import androidx.lifecycle.viewmodel.compose.viewModel 26 | import com.tunjid.demo.common.ui.PaneNavigationBar 27 | import com.tunjid.demo.common.ui.PaneNavigationRail 28 | import com.tunjid.demo.common.ui.PaneScaffold 29 | import com.tunjid.demo.common.ui.data.SampleDestination 30 | import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs 31 | import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier 32 | import com.tunjid.demo.common.ui.rememberPaneScaffoldState 33 | import com.tunjid.treenav.compose.threepane.ThreePane 34 | import com.tunjid.treenav.compose.threepane.threePaneEntry 35 | 36 | fun chatPaneEntry() = threePaneEntry( 37 | paneMapping = { destination -> 38 | mapOf( 39 | ThreePane.Primary to destination, 40 | ThreePane.Secondary to NavTabs.ChatRooms, 41 | ) 42 | }, 43 | render = { destination -> 44 | check(destination is SampleDestination.Chat) 45 | val scope = LocalLifecycleOwner.current.lifecycle.coroutineScope 46 | val viewModel = viewModel { 47 | ChatViewModel( 48 | coroutineScope = scope, 49 | chat = destination, 50 | ) 51 | } 52 | rememberPaneScaffoldState().PaneScaffold( 53 | modifier = Modifier 54 | .predictiveBackBackgroundModifier(this) 55 | .fillMaxSize(), 56 | content = { 57 | ChatScreen( 58 | movableSharedElementScope = this, 59 | state = viewModel.state.collectAsStateWithLifecycle().value, 60 | onAction = viewModel.accept, 61 | ) 62 | LaunchedEffect(paneState.pane) { 63 | viewModel.accept( 64 | Action.UpdateInPrimaryPane( 65 | isInPrimaryPane = paneState.pane == ThreePane.Primary 66 | ) 67 | ) 68 | } 69 | }, 70 | navigationBar = { 71 | PaneNavigationBar() 72 | }, 73 | navigationRail = { 74 | PaneNavigationRail() 75 | }, 76 | ) 77 | }, 78 | ) 79 | -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/ChatRoomsViewModel.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 | * http://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.demo.common.ui.chatrooms 18 | 19 | import androidx.lifecycle.LifecycleCoroutineScope 20 | import androidx.lifecycle.ViewModel 21 | import com.tunjid.demo.common.ui.data.ChatRoom 22 | import com.tunjid.demo.common.ui.data.ChatsRepository 23 | import com.tunjid.demo.common.ui.data.NavigationAction 24 | import com.tunjid.demo.common.ui.data.NavigationRepository 25 | import com.tunjid.demo.common.ui.data.SampleDestination 26 | import com.tunjid.demo.common.ui.data.navigationAction 27 | import com.tunjid.demo.common.ui.data.navigationMutations 28 | import com.tunjid.mutator.ActionStateMutator 29 | import com.tunjid.mutator.Mutation 30 | import com.tunjid.mutator.coroutines.actionStateFlowMutator 31 | import com.tunjid.mutator.coroutines.mapToMutation 32 | import com.tunjid.mutator.coroutines.toMutationStream 33 | import com.tunjid.treenav.push 34 | import kotlinx.coroutines.flow.Flow 35 | import kotlinx.coroutines.flow.StateFlow 36 | 37 | class ChatRoomsViewModel( 38 | coroutineScope: LifecycleCoroutineScope, 39 | chatsRepository: ChatsRepository = ChatsRepository, 40 | navigationRepository: NavigationRepository = NavigationRepository, 41 | ) : ViewModel(coroutineScope), 42 | ActionStateMutator> by coroutineScope.actionStateFlowMutator( 43 | initialState = State(), 44 | inputs = listOf( 45 | chatsRepository.loadMutations() 46 | ), 47 | actionTransform = { actions -> 48 | actions.toMutationStream( 49 | keySelector = Action::key 50 | ) { 51 | when (val type = type()) { 52 | is Action.Navigation -> navigationRepository.navigationMutations( 53 | type.flow 54 | ) 55 | } 56 | } 57 | } 58 | ) 59 | 60 | private fun ChatsRepository.loadMutations(): Flow> = rooms.mapToMutation { 61 | copy(chatRooms = it) 62 | } 63 | 64 | 65 | data class State( 66 | val chatRooms: List = emptyList(), 67 | ) 68 | 69 | sealed class Action( 70 | val key: String, 71 | ) { 72 | sealed class Navigation : Action("Navigation"), NavigationAction { 73 | data class ToRoom( 74 | val roomName: String, 75 | ) : Navigation(), NavigationAction by navigationAction( 76 | { push(SampleDestination.Chat(roomName)) } 77 | ) 78 | } 79 | } -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.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 | * http://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.demo.common.ui.chatrooms 18 | 19 | import androidx.compose.foundation.layout.fillMaxSize 20 | import androidx.compose.ui.Modifier 21 | import androidx.lifecycle.compose.LocalLifecycleOwner 22 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 23 | import androidx.lifecycle.coroutineScope 24 | import androidx.lifecycle.viewmodel.compose.viewModel 25 | import com.tunjid.demo.common.ui.PaneNavigationBar 26 | import com.tunjid.demo.common.ui.PaneNavigationRail 27 | import com.tunjid.demo.common.ui.PaneScaffold 28 | import com.tunjid.demo.common.ui.data.ChatsRepository 29 | import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier 30 | import com.tunjid.demo.common.ui.rememberPaneScaffoldState 31 | import com.tunjid.treenav.compose.threepane.threePaneEntry 32 | 33 | fun chatRoomPaneEntry( 34 | ) = threePaneEntry( 35 | render = { 36 | val scope = LocalLifecycleOwner.current.lifecycle.coroutineScope 37 | val viewModel = viewModel { 38 | ChatRoomsViewModel( 39 | coroutineScope = scope, 40 | chatsRepository = ChatsRepository 41 | ) 42 | } 43 | rememberPaneScaffoldState().PaneScaffold( 44 | modifier = Modifier 45 | .predictiveBackBackgroundModifier(this) 46 | .fillMaxSize(), 47 | content = { 48 | ChatRoomsScreen( 49 | movableSharedElementScope = this, 50 | state = viewModel.state.collectAsStateWithLifecycle().value, 51 | onAction = viewModel.accept, 52 | ) 53 | }, 54 | navigationBar = { 55 | PaneNavigationBar() 56 | }, 57 | navigationRail = { 58 | PaneNavigationRail() 59 | }, 60 | ) 61 | } 62 | ) -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/data/NavigationRepository.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 | * http://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.demo.common.ui.data 18 | 19 | import androidx.compose.material.icons.Icons 20 | import androidx.compose.material.icons.automirrored.filled.List 21 | import androidx.compose.material.icons.filled.Person 22 | import com.tunjid.mutator.Mutation 23 | import com.tunjid.mutator.coroutines.mapToManyMutations 24 | import com.tunjid.treenav.MultiStackNav 25 | import com.tunjid.treenav.Node 26 | import com.tunjid.treenav.StackNav 27 | import kotlinx.coroutines.flow.Flow 28 | import kotlinx.coroutines.flow.MutableStateFlow 29 | import kotlinx.coroutines.flow.StateFlow 30 | import kotlinx.coroutines.flow.asStateFlow 31 | import kotlinx.coroutines.flow.update 32 | 33 | sealed interface SampleDestination : Node { 34 | 35 | enum class NavTabs( 36 | val title: String, 37 | ) : SampleDestination { 38 | ChatRooms("Chat Rooms"), 39 | Me("Me"); 40 | 41 | override val id: String get() = title 42 | 43 | val icon 44 | get() = when (this) { 45 | ChatRooms -> Icons.AutoMirrored.Filled.List 46 | Me -> Icons.Default.Person 47 | } 48 | } 49 | 50 | data class Chat( 51 | val roomName: String, 52 | ) : SampleDestination { 53 | 54 | override val id: String 55 | get() = roomName 56 | 57 | override val children: List 58 | get() = listOf(NavTabs.ChatRooms) 59 | } 60 | 61 | data class Profile( 62 | val profileName: String, 63 | val roomName: String?, 64 | ) : SampleDestination { 65 | 66 | override val id: String 67 | get() = "$profileName-$roomName" 68 | 69 | override val children: List 70 | get() = listOfNotNull( 71 | roomName?.let(::Chat), 72 | roomName?.let { NavTabs.ChatRooms } 73 | ) 74 | } 75 | 76 | data class Avatar( 77 | val profileName: String, 78 | val roomName: String?, 79 | ) : SampleDestination { 80 | 81 | override val id: String 82 | get() = "avatar-$profileName-$roomName" 83 | 84 | override val children: List 85 | get() = listOfNotNull( 86 | roomName?.let(::Chat), 87 | roomName?.let { NavTabs.ChatRooms } 88 | ) 89 | } 90 | } 91 | 92 | fun interface NavigationAction { 93 | fun navigate(multiStackNav: MultiStackNav): MultiStackNav 94 | } 95 | 96 | fun navigationAction( 97 | block: MultiStackNav.() -> MultiStackNav 98 | ) = NavigationAction(block) 99 | 100 | object NavigationRepository { 101 | private val mutableNavigationStateFlow = MutableStateFlow(InitialNavState) 102 | 103 | val navigationStateFlow: StateFlow = mutableNavigationStateFlow.asStateFlow() 104 | 105 | fun navigate(action: NavigationAction) { 106 | mutableNavigationStateFlow.update(action::navigate) 107 | } 108 | } 109 | 110 | fun NavigationRepository.navigationMutations( 111 | navigationActions: Flow 112 | ): Flow> = 113 | navigationActions.mapToManyMutations { 114 | navigate(it) 115 | } 116 | 117 | private val InitialNavState = MultiStackNav( 118 | name = "Sample", 119 | stacks = listOf( 120 | StackNav( 121 | name = "chatrooms", 122 | children = listOf( 123 | SampleDestination.NavTabs.ChatRooms, 124 | ) 125 | ), 126 | StackNav( 127 | name = "me", 128 | children = listOf( 129 | SampleDestination.NavTabs.Me, 130 | ) 131 | ), 132 | ) 133 | ) -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/PaneEntry.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 | * http://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.demo.common.ui.me 18 | 19 | import androidx.compose.foundation.layout.fillMaxSize 20 | import androidx.compose.ui.Modifier 21 | import androidx.lifecycle.compose.LocalLifecycleOwner 22 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 23 | import androidx.lifecycle.coroutineScope 24 | import androidx.lifecycle.viewmodel.compose.viewModel 25 | import com.tunjid.demo.common.ui.PaneNavigationBar 26 | import com.tunjid.demo.common.ui.PaneNavigationRail 27 | import com.tunjid.demo.common.ui.PaneScaffold 28 | import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier 29 | import com.tunjid.demo.common.ui.profile.ProfileScreen 30 | import com.tunjid.demo.common.ui.profile.ProfileViewModel 31 | import com.tunjid.demo.common.ui.rememberPaneScaffoldState 32 | import com.tunjid.treenav.compose.threepane.threePaneEntry 33 | 34 | fun mePaneEntry( 35 | ) = threePaneEntry( 36 | render = { 37 | val scope = LocalLifecycleOwner.current.lifecycle.coroutineScope 38 | val viewModel = viewModel { 39 | ProfileViewModel( 40 | coroutineScope = scope, 41 | profileName = null, 42 | roomName = null, 43 | ) 44 | } 45 | rememberPaneScaffoldState().PaneScaffold( 46 | modifier = Modifier 47 | .predictiveBackBackgroundModifier(this) 48 | .fillMaxSize(), 49 | content = { 50 | ProfileScreen( 51 | movableSharedElementScope = this, 52 | state = viewModel.state.collectAsStateWithLifecycle().value, 53 | onAction = viewModel.accept, 54 | ) 55 | }, 56 | navigationBar = { 57 | PaneNavigationBar() 58 | }, 59 | navigationRail = { 60 | PaneNavigationRail() 61 | }, 62 | ) 63 | } 64 | ) -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/PaneEntry.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 | * http://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.demo.common.ui.profile 18 | 19 | import androidx.compose.foundation.layout.fillMaxSize 20 | import androidx.compose.ui.Modifier 21 | import androidx.lifecycle.compose.LocalLifecycleOwner 22 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 23 | import androidx.lifecycle.coroutineScope 24 | import androidx.lifecycle.viewmodel.compose.viewModel 25 | import com.tunjid.demo.common.ui.PaneNavigationBar 26 | import com.tunjid.demo.common.ui.PaneNavigationRail 27 | import com.tunjid.demo.common.ui.PaneScaffold 28 | import com.tunjid.demo.common.ui.data.SampleDestination 29 | import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs 30 | import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier 31 | import com.tunjid.demo.common.ui.rememberPaneScaffoldState 32 | import com.tunjid.treenav.compose.threepane.ThreePane 33 | import com.tunjid.treenav.compose.threepane.threePaneEntry 34 | 35 | fun profilePaneEntry() = threePaneEntry( 36 | paneMapping = { destination -> 37 | check(destination is SampleDestination.Profile) 38 | mapOf( 39 | ThreePane.Primary to destination, 40 | ThreePane.Secondary to destination.roomName?.let(SampleDestination::Chat), 41 | ThreePane.Tertiary to destination.roomName?.let { NavTabs.ChatRooms }, 42 | ) 43 | }, 44 | render = { destination -> 45 | check(destination is SampleDestination.Profile) 46 | val scope = LocalLifecycleOwner.current.lifecycle.coroutineScope 47 | val viewModel = viewModel { 48 | ProfileViewModel( 49 | coroutineScope = scope, 50 | profileName = destination.profileName, 51 | roomName = destination.roomName, 52 | ) 53 | } 54 | rememberPaneScaffoldState().PaneScaffold( 55 | modifier = Modifier 56 | .predictiveBackBackgroundModifier(this) 57 | .fillMaxSize(), 58 | content = { 59 | ProfileScreen( 60 | movableSharedElementScope = this, 61 | state = viewModel.state.collectAsStateWithLifecycle().value, 62 | onAction = viewModel.accept, 63 | modifier = Modifier.fillMaxSize() 64 | ) 65 | }, 66 | navigationBar = { 67 | PaneNavigationBar() 68 | }, 69 | navigationRail = { 70 | PaneNavigationRail() 71 | }, 72 | ) 73 | }, 74 | ) -------------------------------------------------------------------------------- /sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileViewModel.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 | * http://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.demo.common.ui.profile 18 | 19 | import androidx.lifecycle.LifecycleCoroutineScope 20 | import androidx.lifecycle.ViewModel 21 | import com.tunjid.demo.common.ui.chatrooms.Action.Navigation 22 | import com.tunjid.demo.common.ui.data.NavigationAction 23 | import com.tunjid.demo.common.ui.data.NavigationRepository 24 | import com.tunjid.demo.common.ui.data.Profile 25 | import com.tunjid.demo.common.ui.data.ProfileRepository 26 | import com.tunjid.demo.common.ui.data.SampleDestination 27 | import com.tunjid.demo.common.ui.data.navigationAction 28 | import com.tunjid.demo.common.ui.data.navigationMutations 29 | import com.tunjid.mutator.ActionStateMutator 30 | import com.tunjid.mutator.Mutation 31 | import com.tunjid.mutator.coroutines.actionStateFlowMutator 32 | import com.tunjid.mutator.coroutines.mapToMutation 33 | import com.tunjid.mutator.coroutines.toMutationStream 34 | import com.tunjid.treenav.MultiStackNav 35 | import com.tunjid.treenav.pop 36 | import com.tunjid.treenav.push 37 | import kotlinx.coroutines.flow.Flow 38 | import kotlinx.coroutines.flow.StateFlow 39 | 40 | class ProfileViewModel( 41 | coroutineScope: LifecycleCoroutineScope, 42 | profileRepository: ProfileRepository = ProfileRepository, 43 | navigationRepository: NavigationRepository = NavigationRepository, 44 | profileName: String?, 45 | roomName: String?, 46 | ) : ViewModel(coroutineScope), 47 | ActionStateMutator> by coroutineScope.actionStateFlowMutator( 48 | initialState = State( 49 | roomName = roomName, 50 | profileName = profileName, 51 | ), 52 | inputs = listOf( 53 | profileRepository.profileMutations(profileName) 54 | ), 55 | actionTransform = { actions -> 56 | actions.toMutationStream( 57 | keySelector = Action::key 58 | ) { 59 | when (val type = type()) { 60 | is Action.Navigation -> navigationRepository.navigationMutations( 61 | type.flow 62 | ) 63 | } 64 | } 65 | } 66 | ) 67 | 68 | private fun ProfileRepository.profileMutations( 69 | profileName: String?, 70 | ): Flow> = 71 | (profileName?.let(::profileFor) ?: me) 72 | .mapToMutation { copy(profile = it) } 73 | 74 | data class State( 75 | val roomName: String? = null, 76 | val profileName: String? = null, 77 | val profile: Profile? = null, 78 | ) 79 | 80 | sealed class Action( 81 | val key: String, 82 | ) { 83 | sealed class Navigation : Action("Navigation"), NavigationAction { 84 | data object Pop : Navigation(), NavigationAction by navigationAction( 85 | MultiStackNav::pop 86 | ) 87 | 88 | data class ToAvatar( 89 | val profileName: String, 90 | val roomName: String, 91 | ) : Navigation(), NavigationAction by navigationAction( 92 | { push(SampleDestination.Avatar(profileName = profileName, roomName = roomName)) } 93 | ) 94 | } 95 | } -------------------------------------------------------------------------------- /sample/common/src/iosMain/kotlin/main.ios.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | import com.tunjid.demo.common.ui.App 3 | import com.tunjid.demo.common.ui.AppTheme 4 | 5 | fun MainViewController() = ComposeUIViewController { 6 | AppTheme { 7 | App() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sample/desktop/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/desktop/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 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 18 | 19 | plugins { 20 | kotlin("multiplatform") // kotlin("jvm") doesn't work well in IDEA/AndroidStudio (https://github.com/JetBrains/compose-jb/issues/22) 21 | id("org.jetbrains.compose") 22 | id("kotlin-jvm-convention") 23 | alias(libs.plugins.compose.compiler) 24 | } 25 | 26 | kotlin { 27 | jvm { 28 | withJava() 29 | } 30 | 31 | sourceSets { 32 | named("jvmMain") { 33 | dependencies { 34 | implementation(project(":sample:common")) 35 | 36 | implementation(compose.desktop.currentOs) 37 | implementation(libs.jetbrains.compose.material3) 38 | implementation(libs.kotlinx.coroutines.swing) 39 | } 40 | } 41 | } 42 | } 43 | 44 | compose.desktop { 45 | application { 46 | mainClass = "com.tunjid.demo.MainKt" 47 | 48 | nativeDistributions { 49 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 50 | packageName = "Tiling" 51 | packageVersion = "1.0.0" 52 | 53 | windows { 54 | menuGroup = "Compose Examples" 55 | // see https://wixtoolset.org/documentation/manual/v3/howtos/general/generate_guids.html 56 | upgradeUuid = "C2F20D8A-F643-4BB8-9ADD-28797B7514AF" 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /sample/desktop/src/jvmMain/kotlin/com/tunjid/demo/Main.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.demo 18 | 19 | import androidx.compose.ui.unit.DpSize 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.window.Window 22 | import androidx.compose.ui.window.application 23 | import androidx.compose.ui.window.rememberWindowState 24 | import com.tunjid.demo.common.ui.App 25 | import com.tunjid.demo.common.ui.AppTheme 26 | 27 | fun main() { 28 | application { 29 | val windowState = rememberWindowState( 30 | size = DpSize(400.dp, 800.dp) 31 | ) 32 | Window( 33 | onCloseRequest = ::exitApplication, 34 | state = windowState, 35 | title = "Tiling Demo" 36 | ) { 37 | AppTheme { 38 | App() 39 | } 40 | } 41 | } 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /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 | * http://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 | pluginManagement { 18 | includeBuild("build-logic") 19 | repositories { 20 | google() 21 | mavenCentral() 22 | gradlePluginPortal() 23 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 24 | maven { 25 | url = uri("https://androidx.dev/snapshots/builds/13407944/artifacts/repository") 26 | } 27 | } 28 | } 29 | 30 | dependencyResolutionManagement { 31 | // Workaround for KT-51379 32 | repositoriesMode.set(RepositoriesMode.PREFER_PROJECT) 33 | repositories { 34 | google() 35 | mavenCentral() 36 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 37 | maven { 38 | url = uri("https://androidx.dev/snapshots/builds/13407944/artifacts/repository") 39 | } 40 | } 41 | } 42 | 43 | rootProject.name = "TreeNavigation" 44 | 45 | include( 46 | ":library:treenav", 47 | ":library:strings", 48 | ":library:compose", 49 | ":library:compose-threepane", 50 | ":sample:android", 51 | ":sample:common", 52 | ":sample:desktop", 53 | ) 54 | --------------------------------------------------------------------------------