├── .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 |
4 |
5 |
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 |
--------------------------------------------------------------------------------