From c81fcf1ff35eb83b269c99d8c2089c562cdca7eb Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Fri, 2 May 2025 10:47:52 -0400 Subject: [PATCH 01/11] Add PaneSharedTransitionScope --- .../com/tunjid/treenav/compose/Defaults.kt | 60 ++++++++ .../compose/PaneSharedTransitionScope.kt | 56 ++++++++ .../MovableSharedElements.kt | 38 +----- .../ThreePaneSharedTransitionScope.kt | 128 ++++++++++++++++++ .../MovableSharedElementTransform.kt | 7 +- 5 files changed, 251 insertions(+), 38 deletions(-) create mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/Defaults.kt create mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt create mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/Defaults.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/Defaults.kt new file mode 100644 index 0000000..2f8c5f2 --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/Defaults.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tunjid.treenav.compose + +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope.OverlayClip +import androidx.compose.animation.SharedTransitionScope.SharedContentState +import androidx.compose.animation.core.Spring.StiffnessMediumLow +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +@Stable +internal object Defaults { + + val EmptyElement: @Composable (Any?, Modifier) -> Unit = { _, _ -> } + + @ExperimentalSharedTransitionApi + val DefaultBoundsTransform = BoundsTransform { _, _ -> DefaultSpring } + + @ExperimentalSharedTransitionApi + val ParentClip: OverlayClip = + object : OverlayClip { + override fun getClipPath( + sharedContentState: SharedContentState, + bounds: Rect, + layoutDirection: LayoutDirection, + density: Density, + ): Path? { + return sharedContentState.parentSharedContentState?.clipPathInOverlay + } + } + +} + +private val DefaultSpring = spring( + stiffness = StiffnessMediumLow, + visibilityThreshold = Rect.VisibilityThreshold +) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt new file mode 100644 index 0000000..7a708e5 --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tunjid.treenav.compose + +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.SharedTransitionScope.OverlayClip +import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize +import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion.contentSize +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import com.tunjid.treenav.Node + +/** + * A [SharedTransitionScope] that is aware of the relationship between the [Pane]s in + * its [MultiPaneDisplay]. This allows for defining the semantics of + * shared element behavior when shared elements move in between [Pane]s during the + * transition. + * + * The API is largely analogous to [SharedTransitionScope.sharedElement], with the exception + * of a key being passed instead of [SharedTransitionScope.SharedContentState]. This is because + * each [PaneState.pane] may need its own [SharedTransitionScope.SharedContentState] and + * will need to be managed by the implementation of this method. + * + * @see [SharedTransitionScope.sharedElement]. + */ +@OptIn(ExperimentalSharedTransitionApi::class) +@Stable +interface PaneSharedTransitionScope : + SharedTransitionScope, PaneScope { + + fun Modifier.panedSharedElement( + key: Any, + boundsTransform: BoundsTransform = Defaults.DefaultBoundsTransform, + placeHolderSize: PlaceHolderSize = contentSize, + renderInOverlayDuringTransition: Boolean = true, + visible: Boolean? = null, + zIndexInOverlay: Float = 0f, + clipInOverlayDuringTransition: OverlayClip = Defaults.ParentClip, + ): Modifier +} diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt index acec47b..0b4d8b3 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt @@ -7,9 +7,6 @@ import androidx.compose.animation.SharedTransitionScope.OverlayClip import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion.contentSize import androidx.compose.animation.SharedTransitionScope.SharedContentState -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.VisibilityThreshold -import androidx.compose.animation.core.spring import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -18,11 +15,8 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.LayoutDirection import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.Defaults import com.tunjid.treenav.compose.MultiPaneDisplay import com.tunjid.treenav.compose.PaneScope @@ -109,11 +103,11 @@ fun MovableSharedElementScope.updatedMovableSharedElementOf( key: Any, state: T, modifier: Modifier = Modifier, - boundsTransform: BoundsTransform = DefaultBoundsTransform, + boundsTransform: BoundsTransform = Defaults.DefaultBoundsTransform, placeHolderSize: PlaceHolderSize = contentSize, renderInOverlayDuringTransition: Boolean = true, zIndexInOverlay: Float = 0f, - clipInOverlayDuringTransition: OverlayClip = ParentClip, + clipInOverlayDuringTransition: OverlayClip = Defaults.ParentClip, alternateOutgoingSharedElement: (@Composable (T, Modifier) -> Unit)? = null, sharedElement: @Composable (T, Modifier) -> Unit ) = movableSharedElementOf( @@ -235,7 +229,7 @@ class PanedMovableSharedElementScope( // The element is being shared in its new destination, stop showing it // in the in active one movableSharedElementHostState.isCurrentlyShared(key) - && movableSharedElementHostState.isMatchFound(key) -> EmptyElement( + && movableSharedElementHostState.isMatchFound(key) -> Defaults.EmptyElement( state, Modifier.matchParentSize() ) @@ -251,27 +245,3 @@ class PanedMovableSharedElementScope( } } } - -private val EmptyElement: @Composable (Any?, Modifier) -> Unit = { _, _ -> } - -@ExperimentalSharedTransitionApi -private val ParentClip: OverlayClip = - object : OverlayClip { - override fun getClipPath( - state: SharedContentState, - bounds: Rect, - layoutDirection: LayoutDirection, - density: Density - ): Path? { - return state.parentSharedContentState?.clipPathInOverlay - } - } - -@OptIn(ExperimentalSharedTransitionApi::class) -private val DefaultBoundsTransform = BoundsTransform { _, _ -> - spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMediumLow, - visibilityThreshold = Rect.VisibilityThreshold - ) -} \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt new file mode 100644 index 0000000..69eed6e --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2024 Adetunji Dahunsi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tunjid.treenav.compose.threepane + +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.SharedTransitionScope.OverlayClip +import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.PaneScope +import com.tunjid.treenav.compose.PaneSharedTransitionScope + +/** + * Creates and remembers a [PaneSharedTransitionScope] for [ThreePane] layouts with + * opinionated semantics for how shared elements move between panes, especially with back previews. + * + * @param sharedTransitionScope the [SharedTransitionScope] to be delegated to for core + * shared transition APIs. + */ +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun PaneScope< + ThreePane, + Destination + >.rememberPanedSharedTransitionScope( + sharedTransitionScope: SharedTransitionScope, +): PaneSharedTransitionScope = + remember { + ThreePaneSharedTransitionScope( + paneScope = this, + sharedTransitionScope = sharedTransitionScope + ) + } + +@OptIn(ExperimentalSharedTransitionApi::class) +@Stable +private class ThreePaneSharedTransitionScope @OptIn( + ExperimentalSharedTransitionApi::class +) constructor( + val paneScope: PaneScope, + val sharedTransitionScope: SharedTransitionScope, +) : PaneSharedTransitionScope, + PaneScope by paneScope, + SharedTransitionScope by sharedTransitionScope { + + @OptIn(ExperimentalSharedTransitionApi::class) + override fun Modifier.panedSharedElement( + key: Any, + boundsTransform: BoundsTransform, + placeHolderSize: PlaceHolderSize, + renderInOverlayDuringTransition: Boolean, + visible: Boolean?, + zIndexInOverlay: Float, + clipInOverlayDuringTransition: OverlayClip, + ): Modifier = composed { + + when (paneScope.paneState.pane) { + null -> throw IllegalArgumentException( + "Shared elements may only be used in non null panes" + ) + // Allow shared elements in the primary or transient primary content only + ThreePane.Primary -> when { + paneScope.isPreviewingBack -> sharedElementWithCallerManagedVisibility( + sharedContentState = rememberSharedContentState(key), + visible = false, + boundsTransform = boundsTransform, + placeHolderSize = placeHolderSize, + renderInOverlayDuringTransition = renderInOverlayDuringTransition, + zIndexInOverlay = zIndexInOverlay, + clipInOverlayDuringTransition = clipInOverlayDuringTransition, + ) + // Share the element + else -> sharedElementWithCallerManagedVisibility( + sharedContentState = rememberSharedContentState(key), + visible = when (visible) { + null -> paneScope.isActive + else -> paneScope.isActive && visible + }, + boundsTransform = boundsTransform, + placeHolderSize = placeHolderSize, + renderInOverlayDuringTransition = renderInOverlayDuringTransition, + zIndexInOverlay = zIndexInOverlay, + clipInOverlayDuringTransition = clipInOverlayDuringTransition, + ) + } + // Share the element when in the transient pane + ThreePane.TransientPrimary -> sharedElementWithCallerManagedVisibility( + sharedContentState = rememberSharedContentState(key), + visible = paneScope.isActive, + boundsTransform = boundsTransform, + placeHolderSize = placeHolderSize, + renderInOverlayDuringTransition = renderInOverlayDuringTransition, + zIndexInOverlay = zIndexInOverlay, + clipInOverlayDuringTransition = clipInOverlayDuringTransition, + ) + + // In the other panes use the element as is + ThreePane.Secondary, + ThreePane.Tertiary, + ThreePane.Overlay, + -> this + } + } +} + +private val PaneScope.isPreviewingBack: Boolean + get() = paneState.pane == ThreePane.Primary + && paneState.adaptations.contains(ThreePane.PrimaryToTransient) \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt index 6345087..5a7f5ce 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt @@ -56,10 +56,9 @@ fun PaneScope< >.requireThreePaneMovableSharedElementScope(): MovableSharedElementScope { check(this is ThreePaneMovableSharedElementScope) { """ - The current AdaptivePaneScope (${this::class.qualifiedName}) is not an instance of - a ThreePaneMovableSharedElementScope. You must configure your ThreePane AdaptiveNavHost with - AdaptiveNavHostConfiguration.movableSharedElementConfiguration(movableSharedElementHostState). - + The current PaneScope (${this::class.qualifiedName}) is not an instance of + a ThreePaneMovableSharedElementScope. You must configure your ThreePane MultiPaneDisplay with + threePanedMovableSharedElementTransform(). """.trimIndent() } return this From 1aa27f4d0815d2b5b99ca4c758b98419c5c04f4d Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sat, 3 May 2025 14:59:58 -0400 Subject: [PATCH 02/11] Add PaneSharedTransitionScope --- .../compose/PaneSharedTransitionScope.kt | 17 +- .../MovableSharedElements.kt | 14 +- .../MovableSharedElementTransform.kt | 6 +- .../com/tunjid/demo/common/ui/DemoApp.kt | 75 ++--- .../tunjid/demo/common/ui/PaneNavigation.kt | 87 ++++++ .../com/tunjid/demo/common/ui/PaneScaffold.kt | 290 ++++++++++++++++++ .../tunjid/demo/common/ui/PredictiveBack.kt | 59 ++++ .../tunjid/demo/common/ui/chat/Strategy.kt | 41 ++- .../demo/common/ui/chatrooms/Strategy.kt | 27 +- .../com/tunjid/demo/common/ui/me/Strategy.kt | 31 +- .../tunjid/demo/common/ui/profile/Strategy.kt | 28 +- 11 files changed, 582 insertions(+), 93 deletions(-) create mode 100644 sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt create mode 100644 sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt create mode 100644 sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PredictiveBack.kt diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt index 7a708e5..a42696c 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt @@ -31,19 +31,20 @@ import com.tunjid.treenav.Node * its [MultiPaneDisplay]. This allows for defining the semantics of * shared element behavior when shared elements move in between [Pane]s during the * transition. - * - * The API is largely analogous to [SharedTransitionScope.sharedElement], with the exception - * of a key being passed instead of [SharedTransitionScope.SharedContentState]. This is because - * each [PaneState.pane] may need its own [SharedTransitionScope.SharedContentState] and - * will need to be managed by the implementation of this method. - * - * @see [SharedTransitionScope.sharedElement]. */ @OptIn(ExperimentalSharedTransitionApi::class) @Stable interface PaneSharedTransitionScope : - SharedTransitionScope, PaneScope { + PaneScope, SharedTransitionScope { + /** + * Conceptual equivalent of [SharedTransitionScope.sharedElement], with the exception + * of a key being passed instead of [SharedTransitionScope.SharedContentState]. This is because + * each [PaneState.pane] may need its own [SharedTransitionScope.SharedContentState] and + * will need to be managed by the implementation of this method. + * + * @see [SharedTransitionScope.sharedElement]. + */ fun Modifier.panedSharedElement( key: Any, boundsTransform: BoundsTransform = Defaults.DefaultBoundsTransform, diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt index 0b4d8b3..6b95ef2 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt @@ -26,7 +26,12 @@ import com.tunjid.treenav.compose.PaneScope */ @OptIn(ExperimentalSharedTransitionApi::class) @Stable -interface MovableSharedElementScope : SharedTransitionScope { +interface MovableSharedElementScope { + + /** + * The backing [SharedTransitionScope] for movable shared elements. + */ + val sharedTransitionScope: SharedTransitionScope /** * Creates a movable shared element that accepts a single argument [T] and a [Modifier]. @@ -130,7 +135,7 @@ fun MovableSharedElementScope.updatedMovableSharedElementOf( @OptIn(ExperimentalSharedTransitionApi::class) @Stable class MovableSharedElementHostState( - private val sharedTransitionScope: SharedTransitionScope, + val sharedTransitionScope: SharedTransitionScope, ) : SharedTransitionScope by sharedTransitionScope { private val keysToMovableSharedElements = @@ -186,7 +191,10 @@ class MovableSharedElementHostState( class PanedMovableSharedElementScope( paneScope: PaneScope, private val movableSharedElementHostState: MovableSharedElementHostState, -) : MovableSharedElementScope, SharedTransitionScope by movableSharedElementHostState { +) : MovableSharedElementScope { + + override val sharedTransitionScope: SharedTransitionScope + get() = movableSharedElementHostState var paneScope by mutableStateOf(paneScope) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt index 5a7f5ce..3a74e02 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt @@ -69,8 +69,12 @@ fun PaneScope< private class ThreePaneMovableSharedElementScope( private val hostState: MovableSharedElementHostState, private val delegate: PanedMovableSharedElementScope, -) : MovableSharedElementScope, SharedTransitionScope by delegate, +) : MovableSharedElementScope, PaneScope by delegate.paneScope { + + override val sharedTransitionScope: SharedTransitionScope + get() = delegate.sharedTransitionScope + @OptIn(ExperimentalSharedTransitionApi::class) override fun movableSharedElementOf( key: Any, diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt index 9bf3e53..9a54869 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt @@ -17,13 +17,10 @@ package com.tunjid.demo.common.ui import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.SharedTransitionScope -import androidx.compose.animation.animateBounds +import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animate import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.snap -import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation @@ -42,12 +39,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.Scaffold import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -58,6 +54,7 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -99,20 +96,11 @@ import kotlinx.coroutines.launch @Composable fun App( appState: AppState = remember { AppState() }, -) { - NavigationSuiteScaffold( - navigationSuiteItems = { - SampleDestination.NavTabs.entries.forEach { - item( - icon = { Icon(it.icon, contentDescription = it.title) }, - label = { Text(it.title) }, - selected = it == appState.currentNavigation.current, - onClick = { appState.setTab(it) } - ) - } - } +) = Scaffold { + CompositionLocalProvider( + LocalAppState provides appState, ) { - SharedTransitionScope { sharedTransitionModifier -> + SharedTransitionLayout(Modifier.fillMaxSize()) { val backPreviewSurfaceColor = MaterialTheme.colorScheme.surfaceColorAtElevation( animateDpAsState(if (appState.isPreviewingBack) 16.dp else 0.dp).value ) @@ -122,13 +110,6 @@ fun App( sharedTransitionScope = this ) } - - var canAnimatePanes by remember { mutableStateOf(true) } - val interactingWithPanes = appState.isInteractingWithPanes() - LaunchedEffect(interactingWithPanes) { - canAnimatePanes = !interactingWithPanes - } - MultiPaneDisplay( modifier = Modifier .fillMaxSize(), @@ -136,6 +117,12 @@ fun App( remember { listOf( threePanedAdaptiveTransform( + secondaryPaneBreakPoint = mutableStateOf( + SecondaryPaneMinWidthBreakpointDp + ), + tertiaryPaneBreakPoint = mutableStateOf( + TertiaryPaneMinWidthBreakpointDp + ), windowWidthState = derivedStateOf { appState.splitLayoutState.size } @@ -150,27 +137,10 @@ fun App( movableSharedElementHostState = movableSharedElementHostState ), paneModifierTransform { - val modifier = Modifier.animateBounds( - lookaheadScope = this@SharedTransitionScope, - boundsTransform = { _, _ -> - when (paneState.pane) { - ThreePane.Primary, - ThreePane.TransientPrimary, - ThreePane.Secondary, - ThreePane.Tertiary, - -> if (canAnimatePanes) spring() else snap() - - null, - ThreePane.Overlay, - -> snap() - } - } - ) - if (paneState.pane == ThreePane.TransientPrimary) modifier + if (paneState.pane == ThreePane.TransientPrimary) Modifier .fillMaxSize() .backPreview(appState.backPreviewState) - .background(backPreviewSurfaceColor, RoundedCornerShape(16.dp)) - else modifier + else Modifier .fillMaxSize() } ) @@ -182,8 +152,7 @@ fun App( SplitLayout( state = appState.splitLayoutState, modifier = Modifier - .fillMaxSize() - then sharedTransitionModifier, + .fillMaxSize(), itemSeparators = { paneIndex, offset -> PaneSeparator( splitLayoutState = appState.splitLayoutState, @@ -297,6 +266,8 @@ class AppState( internal val isPreviewingBack get() = !backPreviewState.progress.isNaN() + internal val isMediumScreenWidthOrWider get() = splitLayoutState.size >= SecondaryPaneMinWidthBreakpointDp + internal var displayScope by mutableStateOf?>( null ) @@ -375,5 +346,11 @@ class AppState( } } +internal val LocalAppState = staticCompositionLocalOf { + TODO() +} + private val PaneSeparatorActiveWidthDp = 56.dp -private val PaneSeparatorTouchTargetWidthDp = 16.dp \ No newline at end of file +private val PaneSeparatorTouchTargetWidthDp = 16.dp +internal val SecondaryPaneMinWidthBreakpointDp = 600.dp +internal val TertiaryPaneMinWidthBreakpointDp = 1200.dp \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt new file mode 100644 index 0000000..2be659f --- /dev/null +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneNavigation.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2024 Adetunji Dahunsi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tunjid.demo.common.ui + +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tunjid.demo.common.ui.data.SampleDestination +import com.tunjid.treenav.current + + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun PaneScaffoldState.PaneBottomAppBar( + modifier: Modifier = Modifier, +) { + val appState = LocalAppState.current + val sharedContentState = rememberSharedContentState(BottomNavSharedElementKey) + NavigationBar( + modifier = modifier + .sharedElement( + sharedContentState = sharedContentState, + animatedVisibilityScope = this, + zIndexInOverlay = BottomNavSharedElementZIndex, + ), + ) { + SampleDestination.NavTabs.entries.forEach { item -> + NavigationBarItem( + icon = { + Icon( + imageVector = item.icon, + contentDescription = item.title, + ) + }, + selected = item == appState.currentNavigation.current, + onClick = { appState.setTab(item) } + ) + } + } +} + +@Suppress("UnusedReceiverParameter") +@Composable +fun PaneScaffoldState.PaneNavigationRail( + modifier: Modifier = Modifier, +) { + val appState = LocalAppState.current + NavigationRail( + modifier = modifier, + ) { + SampleDestination.NavTabs.entries.forEach { item -> + NavigationRailItem( + selected = item == appState.currentNavigation.current, + icon = { + Icon( + imageVector = item.icon, + contentDescription = item.title, + ) + }, + onClick = { appState.setTab(item) } + ) + } + } +} + +private data object BottomNavSharedElementKey + +private const val BottomNavSharedElementZIndex = 2f diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt new file mode 100644 index 0000000..018598d --- /dev/null +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt @@ -0,0 +1,290 @@ +/* + * Copyright 2024 Adetunji Dahunsi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tunjid.demo.common.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.animateBounds +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.spring +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.roundToIntSize +import androidx.compose.ui.zIndex +import com.tunjid.composables.ui.skipIf +import com.tunjid.demo.common.ui.data.SampleDestination +import com.tunjid.treenav.compose.PaneScope +import com.tunjid.treenav.compose.PaneSharedTransitionScope +import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope +import com.tunjid.treenav.compose.threepane.ThreePane +import com.tunjid.treenav.compose.threepane.rememberPanedSharedTransitionScope +import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.filterNotNull +import kotlin.math.abs + +@Stable +class PaneScaffoldState internal constructor( + private val appState: AppState, + private val movableSharedElementScope: MovableSharedElementScope, + paneSharedTransitionScope: PaneSharedTransitionScope, +) : + MovableSharedElementScope by movableSharedElementScope, + PaneSharedTransitionScope by paneSharedTransitionScope { + + internal val canShowBottomNavigation get() = !appState.isMediumScreenWidthOrWider + + internal val canShowNavRail + get() = appState.filteredPaneOrder.firstOrNull() == paneState.pane + && appState.isMediumScreenWidthOrWider + + internal val canShowFab + get() = when (paneState.pane) { + ThreePane.Primary -> true + ThreePane.TransientPrimary -> true + ThreePane.Secondary -> false + ThreePane.Tertiary -> false + ThreePane.Overlay -> false + null -> false + } + + internal var scaffoldTargetSize by mutableStateOf(IntSize.Zero) + internal var scaffoldCurrentSize by mutableStateOf(IntSize.Zero) + + internal fun hasMatchedSize(): Boolean = + abs(scaffoldCurrentSize.width - scaffoldTargetSize.width) <= 2 + && abs(scaffoldCurrentSize.height - scaffoldTargetSize.height) <= 2 + + @OptIn(ExperimentalSharedTransitionApi::class) + override fun movableSharedElementOf( + key: Any, + boundsTransform: BoundsTransform, + placeHolderSize: SharedTransitionScope.PlaceHolderSize, + renderInOverlayDuringTransition: Boolean, + zIndexInOverlay: Float, + clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip, + alternateOutgoingSharedElement:@Composable ((T, Modifier) -> Unit)?, + sharedElement:@Composable (T, Modifier) -> Unit, + ):@Composable (T, Modifier) -> Unit = movableSharedElementScope.movableSharedElementOf( + key = key, + boundsTransform = boundsTransform, + placeHolderSize = placeHolderSize, + renderInOverlayDuringTransition = renderInOverlayDuringTransition, + zIndexInOverlay = zIndexInOverlay, + clipInOverlayDuringTransition = clipInOverlayDuringTransition, + alternateOutgoingSharedElement = alternateOutgoingSharedElement, + sharedElement = sharedElement, + ) +} + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun PaneScope.PaneScaffold( + modifier: Modifier = Modifier, + containerColor: Color = MaterialTheme.colorScheme.background, + snackBarMessages: List = emptyList(), + onSnackBarMessageConsumed: (String) -> Unit = {}, + topBar: @Composable PaneScaffoldState.() -> Unit = {}, + floatingActionButton: @Composable PaneScaffoldState.() -> Unit = {}, + navigationBar: @Composable PaneScaffoldState.() -> Unit = {}, + navigationRail: @Composable PaneScaffoldState.() -> Unit = {}, + content: @Composable PaneScaffoldState.(PaddingValues) -> Unit, +) { + val appState = LocalAppState.current + val snackbarHostState = remember { SnackbarHostState() } + val movableSharedElementScope = requireThreePaneMovableSharedElementScope() + val paneSharedTransitionScope = rememberPanedSharedTransitionScope( + sharedTransitionScope = movableSharedElementScope.sharedTransitionScope, + ) + val paneScaffoldState = + remember(appState, movableSharedElementScope, paneSharedTransitionScope) { + PaneScaffoldState( + appState = appState, + movableSharedElementScope = movableSharedElementScope, + paneSharedTransitionScope = paneSharedTransitionScope, + ) + } + + val canAnimatePane = remember { mutableStateOf(true) }.also { + it.value = !appState.isInteractingWithPanes() + } + + RowPaneScaffold( + modifier = modifier, + navigationRail = { + if (paneScaffoldState.canShowNavRail) Box( + modifier = Modifier + .zIndex(2f), + ) { + paneScaffoldState.navigationRail() + } + }, + content = { + Scaffold( + modifier = Modifier + .animateBounds( + lookaheadScope = movableSharedElementScope.sharedTransitionScope, + boundsTransform = remember { + scaffoldBoundsTransform( + paneScaffoldState = paneScaffoldState, + canAnimatePane = canAnimatePane::value + ) + } + ) + .padding( + horizontal = if (appState.filteredPaneOrder.size > 1) 8.dp else 0.dp + ) + .onSizeChanged { + paneScaffoldState.scaffoldCurrentSize = it + }, + containerColor = containerColor, + topBar = { + paneScaffoldState.topBar() + }, + floatingActionButton = { + AnimatedVisibility( + visible = paneScaffoldState.canShowFab, + enter = slideInVertically(initialOffsetY = { it }), + exit = slideOutVertically(targetOffsetY = { it }), + content = { + paneScaffoldState.floatingActionButton() + }, + ) + }, + bottomBar = { + AnimatedVisibility( + visible = paneScaffoldState.canShowBottomNavigation, + enter = slideInVertically(initialOffsetY = { it }), + exit = slideOutVertically(targetOffsetY = { it }), + content = { + paneScaffoldState.navigationBar() + }, + ) + }, + snackbarHost = { + SnackbarHost(snackbarHostState) + }, + content = { paddingValues -> + paneScaffoldState.content(paddingValues) + }, + ) + } + ) + val updatedMessages = rememberUpdatedState(snackBarMessages.firstOrNull()) + LaunchedEffect(Unit) { + snapshotFlow { updatedMessages.value } + .filterNotNull() + .filterNot(String::isNullOrBlank) + .collect { message -> + snackbarHostState.showSnackbar( + message = message + ) + onSnackBarMessageConsumed(message) + } + } +} + +@Composable +private inline fun RowPaneScaffold( + modifier: Modifier = Modifier, + navigationRail: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + Row( + modifier = modifier, + content = { + Box( + modifier = Modifier + .widthIn(max = 80.dp) + ) { + navigationRail() + } + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(1f), + content = { + content() + } + ) + }, + ) +} + +@OptIn(ExperimentalSharedTransitionApi::class) +private fun scaffoldBoundsTransform( + paneScaffoldState: PaneScaffoldState, + canAnimatePane: () -> Boolean, +): BoundsTransform = BoundsTransform { _, targetBounds -> + paneScaffoldState.scaffoldTargetSize = + targetBounds.size.roundToIntSize() + + when (paneScaffoldState.paneState.pane) { + ThreePane.Primary, + ThreePane.Secondary, + ThreePane.Tertiary, + -> if (canAnimatePane()) spring() + else snap() + + ThreePane.TransientPrimary, + -> spring().skipIf(paneScaffoldState::hasMatchedSize) + + ThreePane.Overlay, + null, + -> snap() + } +} + +fun Modifier.paneClip() = + then(PaneClipModifier) + +private val PaneClipModifier = Modifier.clip( + shape = RoundedCornerShape( + topStart = 16.dp, + topEnd = 16.dp, + ) +) \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PredictiveBack.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PredictiveBack.kt new file mode 100644 index 0000000..539046a --- /dev/null +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PredictiveBack.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024 Adetunji Dahunsi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tunjid.demo.common.ui + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tunjid.treenav.compose.PaneScope +import com.tunjid.treenav.compose.threepane.ThreePane + +@Composable +fun Modifier.predictiveBackBackgroundModifier( + paneScope: PaneScope, +): Modifier { + if (paneScope.paneState.pane != ThreePane.TransientPrimary) + return this + + var elevation by remember { mutableStateOf(0.dp) } + LaunchedEffect(Unit) { + animate( + initialValue = 0f, + targetValue = 4f, + animationSpec = spring(stiffness = Spring.StiffnessVeryLow) + ) { value, _ -> elevation = value.dp } + } + return background( + color = MaterialTheme.colorScheme.surfaceColorAtElevation(elevation), + shape = RoundedCornerShape(16.dp) + ) + .clip(RoundedCornerShape(16.dp)) + +} diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt index 2c31897..ec3861a 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt @@ -23,10 +23,13 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope import androidx.lifecycle.viewmodel.compose.viewModel +import com.tunjid.demo.common.ui.PaneBottomAppBar +import com.tunjid.demo.common.ui.PaneNavigationRail +import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs +import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier import com.tunjid.treenav.compose.threepane.ThreePane -import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope import com.tunjid.treenav.compose.threepane.threePaneEntry fun chatPaneEntry() = threePaneEntry( @@ -45,18 +48,30 @@ fun chatPaneEntry() = threePaneEntry( chat = destination, ) } - ChatScreen( - movableSharedElementScope = requireThreePaneMovableSharedElementScope(), - state = viewModel.state.collectAsStateWithLifecycle().value, - onAction = viewModel.accept, - modifier = Modifier.fillMaxSize(), - ) - LaunchedEffect(paneState.pane) { - viewModel.accept( - Action.UpdateInPrimaryPane( - isInPrimaryPane = paneState.pane == ThreePane.Primary + PaneScaffold( + modifier = Modifier + .predictiveBackBackgroundModifier(this) + .fillMaxSize(), + content = { + ChatScreen( + movableSharedElementScope = this, + state = viewModel.state.collectAsStateWithLifecycle().value, + onAction = viewModel.accept, ) - ) - } + LaunchedEffect(paneState.pane) { + viewModel.accept( + Action.UpdateInPrimaryPane( + isInPrimaryPane = paneState.pane == ThreePane.Primary + ) + ) + } + }, + navigationBar = { + PaneBottomAppBar() + }, + navigationRail = { + PaneNavigationRail() + }, + ) }, ) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt index 7be32cd..e2f7e6f 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt @@ -22,9 +22,12 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope import androidx.lifecycle.viewmodel.compose.viewModel +import com.tunjid.demo.common.ui.PaneBottomAppBar +import com.tunjid.demo.common.ui.PaneNavigationRail +import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.data.ChatsRepository import com.tunjid.demo.common.ui.data.SampleDestination -import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope +import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier import com.tunjid.treenav.compose.threepane.threePaneEntry fun chatRoomPaneEntry( @@ -37,11 +40,23 @@ fun chatRoomPaneEntry( chatsRepository = ChatsRepository ) } - ChatRoomsScreen( - movableSharedElementScope = requireThreePaneMovableSharedElementScope(), - state = viewModel.state.collectAsStateWithLifecycle().value, - onAction = viewModel.accept, - modifier = Modifier.fillMaxSize(), + PaneScaffold( + modifier = Modifier + .predictiveBackBackgroundModifier(this) + .fillMaxSize(), + content = { + ChatRoomsScreen( + movableSharedElementScope = this, + state = viewModel.state.collectAsStateWithLifecycle().value, + onAction = viewModel.accept, + ) + }, + navigationBar = { + PaneBottomAppBar() + }, + navigationRail = { + PaneNavigationRail() + }, ) } ) \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/Strategy.kt index 1434f13..32638d2 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/Strategy.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/Strategy.kt @@ -16,18 +16,22 @@ package com.tunjid.demo.common.ui.me +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope import androidx.lifecycle.viewmodel.compose.viewModel -import com.tunjid.demo.common.ui.data.SampleDestination +import com.tunjid.demo.common.ui.PaneBottomAppBar +import com.tunjid.demo.common.ui.PaneNavigationRail +import com.tunjid.demo.common.ui.PaneScaffold +import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier import com.tunjid.demo.common.ui.profile.ProfileScreen import com.tunjid.demo.common.ui.profile.ProfileViewModel -import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope import com.tunjid.treenav.compose.threepane.threePaneEntry fun mePaneEntry( -) = threePaneEntry( +) = threePaneEntry( render = { val scope = LocalLifecycleOwner.current.lifecycle.coroutineScope val viewModel = viewModel { @@ -37,10 +41,23 @@ fun mePaneEntry( roomName = null, ) } - ProfileScreen( - movableSharedElementScope = requireThreePaneMovableSharedElementScope(), - state = viewModel.state.collectAsStateWithLifecycle().value, - onAction = viewModel.accept + PaneScaffold( + modifier = Modifier + .predictiveBackBackgroundModifier(this) + .fillMaxSize(), + content = { + ProfileScreen( + movableSharedElementScope = this, + state = viewModel.state.collectAsStateWithLifecycle().value, + onAction = viewModel.accept, + ) + }, + navigationBar = { + PaneBottomAppBar() + }, + navigationRail = { + PaneNavigationRail() + }, ) } ) \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt index e74b3f7..ec07cd4 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt @@ -22,10 +22,13 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope import androidx.lifecycle.viewmodel.compose.viewModel +import com.tunjid.demo.common.ui.PaneBottomAppBar +import com.tunjid.demo.common.ui.PaneNavigationRail +import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs +import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier import com.tunjid.treenav.compose.threepane.ThreePane -import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope import com.tunjid.treenav.compose.threepane.threePaneEntry fun profilePaneEntry() = threePaneEntry( @@ -47,11 +50,24 @@ fun profilePaneEntry() = threePaneEntry( roomName = destination.roomName, ) } - ProfileScreen( - movableSharedElementScope = requireThreePaneMovableSharedElementScope(), - state = viewModel.state.collectAsStateWithLifecycle().value, - onAction = viewModel.accept, - modifier = Modifier.fillMaxSize(), + PaneScaffold( + modifier = Modifier + .predictiveBackBackgroundModifier(this) + .fillMaxSize(), + content = { + ProfileScreen( + movableSharedElementScope = this, + state = viewModel.state.collectAsStateWithLifecycle().value, + onAction = viewModel.accept, + modifier = Modifier.fillMaxSize() + ) + }, + navigationBar = { + PaneBottomAppBar() + }, + navigationRail = { + PaneNavigationRail() + }, ) }, ) \ No newline at end of file From e307ab8c34883a9279f3363cf61c848f544b5404 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 4 May 2025 14:37:03 -0400 Subject: [PATCH 03/11] Update gradle convention plugins --- .../kotlin/android-app-library-convention.kt | 7 +-- .../kotlin-library-convention.gradle.kts | 48 ++++++++----------- .../publishing-library-convention.gradle.kts | 12 ++--- gradle/libs.versions.toml | 22 +-------- library/compose/build.gradle.kts | 25 +++------- .../compose/PaneSharedTransitionScope.kt | 2 +- .../ThreePaneSharedTransitionScope.kt | 2 +- .../src/main/res/values/AndroidManifest.xml | 18 +++++++ library/strings/build.gradle.kts | 24 +--------- library/treenav/build.gradle.kts | 24 +--------- .../com/tunjid/treenav/MultiStackNav.kt | 2 +- .../kotlin/com/tunjid/treenav/StackNav.kt | 2 +- libraryVersion.properties | 6 +-- sample/common/build.gradle.kts | 20 -------- 14 files changed, 63 insertions(+), 151 deletions(-) create mode 100644 library/compose/src/main/res/values/AndroidManifest.xml diff --git a/build-logic/convention/src/main/kotlin/android-app-library-convention.kt b/build-logic/convention/src/main/kotlin/android-app-library-convention.kt index a124a5a..072e710 100644 --- a/build-logic/convention/src/main/kotlin/android-app-library-convention.kt +++ b/build-logic/convention/src/main/kotlin/android-app-library-convention.kt @@ -41,16 +41,13 @@ import org.gradle.api.artifacts.VersionCatalogsExtension fun org.gradle.api.Project.androidConfiguration( extension: CommonExtension<*, *, *, *, *, *> ) = extension.apply { - namespace = "com.tunjid.composables.${project.name}" - compileSdk = 35 + namespace = "com.tunjid.treenav.${project.name}" + compileSdk = 36 defaultConfig { minSdk = 23 } - buildFeatures { - compose = true - } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/build-logic/convention/src/main/kotlin/kotlin-library-convention.gradle.kts b/build-logic/convention/src/main/kotlin/kotlin-library-convention.gradle.kts index 608cc29..d4f69b1 100644 --- a/build-logic/convention/src/main/kotlin/kotlin-library-convention.gradle.kts +++ b/build-logic/convention/src/main/kotlin/kotlin-library-convention.gradle.kts @@ -38,13 +38,25 @@ plugins { } kotlin { - androidTarget() - jvm("desktop") -// js(IR) { -// browser() -// nodejs() -// binaries.executable() -// } + applyDefaultHierarchyTemplate() + androidTarget { + publishLibraryVariants("release") + } + jvm { + testRuns["test"].executionTask.configure { + useJUnit() + } + } + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = project.name + isStatic = true + } + } sourceSets { all { languageSettings.apply { @@ -58,28 +70,6 @@ kotlin { } } } - - targets.withType(KotlinNativeTarget::class.java) { - binaries.all { - binaryOptions["memoryModel"] = "experimental" - } - } configureKotlinJvm() } -// a temporary workaround for a bug in jsRun invocation - see https://youtrack.jetbrains.com/issue/KT-48273 -afterEvaluate { - rootProject.extensions.configure { - versions.webpackDevServer.version = "4.0.0" - versions.webpackCli.version = "4.10.0" - nodeVersion = "16.0.0" - } -} - - -// TODO: remove when https://youtrack.jetbrains.com/issue/KT-50778 fixed -project.tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile::class.java).configureEach { - kotlinOptions.freeCompilerArgs += listOf( - "-Xir-dce-runtime-diagnostic=log" - ) -} diff --git a/build-logic/convention/src/main/kotlin/publishing-library-convention.gradle.kts b/build-logic/convention/src/main/kotlin/publishing-library-convention.gradle.kts index 60fc96c..0be34be 100644 --- a/build-logic/convention/src/main/kotlin/publishing-library-convention.gradle.kts +++ b/build-logic/convention/src/main/kotlin/publishing-library-convention.gradle.kts @@ -57,12 +57,12 @@ publishing { artifact(javadocJar) pom { name.set(project.name) - description.set("A collection of utility composable functions") - url.set("https://github.com/tunjid/composables") + description.set("A kotlin multiplatform experiment for representing app navigation with tree like data structures") + url.set("https://github.com/tunjid/treenav") licenses { license { name.set("Apache License 2.0") - url.set("https://github.com/tunjid/composables/blob/main/LICENSE") + url.set("https://github.com/tunjid/treenav/blob/main/LICENSE.txt") } } developers { @@ -73,9 +73,9 @@ publishing { } } scm { - connection.set("scm:git:github.com/tunjid/composables.git") - developerConnection.set("scm:git:ssh://github.com/tunjid/composables.git") - url.set("https://github.com/tunjid/composables/tree/main") + connection.set("scm:git:github.com/tunjid/treenav.git") + developerConnection.set("scm:git:ssh://github.com/tunjid/treenav.git") + url.set("https://github.com/tunjid/treenav/tree/main") } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5b7ca99..138c8e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] androidGradlePlugin = "8.9.2" androidxActivity = "1.9.2" -activity-compose = "1.10.1" +activity-compose = "1.11.0-rc01" androidxAppCompat = "1.7.0" androidxBenchmark = "1.3.4" androidxCore = "1.16.0" @@ -39,26 +39,6 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } androidx-collection = { group = "androidx.collection", name = "collection", version.ref = "androidxCollection" } -androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" } -androidx-compose-foundation-foundation = { group = "androidx.compose.foundation", name = "foundation" } -androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" } -androidx-compose-material-core = { group = "androidx.compose.material", name = "material" } -androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" } -androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } -androidx-compose-ui-ui = { group = "androidx.compose.ui", name = "ui" } -androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } -androidx-compose-ui-platform = { group = "androidx.compose.ui", name = "ui-platform" } -androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } -androidx-compose-material3-material3 = { group = "androidx.compose.material3", name = "material3" } -androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive" } -androidx-compose-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout" } -androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation" } -androidx-compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class" } -androidx-savedstate-savedstate = { group = "androidx.compose.savedstate", name = "savedstate", version.ref = "androidxSavedState" } -androidx-savedstate-compose = { group = "androidx.compose.savedstate", name = "savedstate-compose", version.ref = "androidxSavedState" } jetbrains-compose-animation = { group = "org.jetbrains.compose.animation", name = "animation", version.ref = "jetbrainsCompose" } jetbrains-compose-foundation = { group = "org.jetbrains.compose.foundation", name = "foundation", version.ref = "jetbrainsCompose" } diff --git a/library/compose/build.gradle.kts b/library/compose/build.gradle.kts index b42c0a9..c7c7f15 100644 --- a/library/compose/build.gradle.kts +++ b/library/compose/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("publishing-library-convention") id("android-library-convention") id("kotlin-jvm-convention") + id("kotlin-library-convention") id("maven-publish") signing id("org.jetbrains.dokka") @@ -10,25 +11,13 @@ plugins { alias(libs.plugins.compose.compiler) } -kotlin { - applyDefaultHierarchyTemplate() - androidTarget() - jvm { - testRuns["test"].executionTask.configure { - useJUnit() - } - } - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64(), - ).forEach { iosTarget -> - iosTarget.binaries.framework { - baseName = "treenav-compose" - isStatic = true - } +android { + buildFeatures { + compose = true } +} +kotlin { sourceSets { commonMain { dependencies { @@ -62,8 +51,6 @@ kotlin { implementation(kotlin("test")) } } - val jvmMain by getting - val jvmTest by getting all { languageSettings.apply { diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt index a42696c..d85256e 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneSharedTransitionScope.kt @@ -45,7 +45,7 @@ interface PaneSharedTransitionScope : * * @see [SharedTransitionScope.sharedElement]. */ - fun Modifier.panedSharedElement( + fun Modifier.paneSharedElement( key: Any, boundsTransform: BoundsTransform = Defaults.DefaultBoundsTransform, placeHolderSize: PlaceHolderSize = contentSize, diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt index 69eed6e..b90b307 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt @@ -64,7 +64,7 @@ private class ThreePaneSharedTransitionScope @OptIn( SharedTransitionScope by sharedTransitionScope { @OptIn(ExperimentalSharedTransitionApi::class) - override fun Modifier.panedSharedElement( + override fun Modifier.paneSharedElement( key: Any, boundsTransform: BoundsTransform, placeHolderSize: PlaceHolderSize, diff --git a/library/compose/src/main/res/values/AndroidManifest.xml b/library/compose/src/main/res/values/AndroidManifest.xml new file mode 100644 index 0000000..1ac8415 --- /dev/null +++ b/library/compose/src/main/res/values/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/library/strings/build.gradle.kts b/library/strings/build.gradle.kts index f51d4d1..1ffac0f 100644 --- a/library/strings/build.gradle.kts +++ b/library/strings/build.gradle.kts @@ -17,34 +17,19 @@ plugins { kotlin("multiplatform") id("publishing-library-convention") + id("android-library-convention") id("kotlin-jvm-convention") + id("kotlin-library-convention") id("maven-publish") signing id("org.jetbrains.dokka") } kotlin { - applyDefaultHierarchyTemplate() js(IR) { nodejs() browser() } - jvm { - withJava() - testRuns["test"].executionTask.configure { - useJUnit() - } - } - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64(), - ).forEach { iosTarget -> - iosTarget.binaries.framework { - baseName = "treenav-strings" - isStatic = true - } - } linuxX64() macosX64() macosArm64() @@ -63,10 +48,5 @@ kotlin { implementation(kotlin("test")) } } - val jvmMain by getting - val jvmTest by getting - - val jsMain by getting - val jsTest by getting } } diff --git a/library/treenav/build.gradle.kts b/library/treenav/build.gradle.kts index 9d9d148..9345d69 100644 --- a/library/treenav/build.gradle.kts +++ b/library/treenav/build.gradle.kts @@ -17,34 +17,19 @@ plugins { kotlin("multiplatform") id("publishing-library-convention") + id("android-library-convention") id("kotlin-jvm-convention") + id("kotlin-library-convention") id("maven-publish") signing id("org.jetbrains.dokka") } kotlin { - applyDefaultHierarchyTemplate() js(IR) { nodejs() browser() } - jvm { - withJava() - testRuns["test"].executionTask.configure { - useJUnit() - } - } - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64(), - ).forEach { iosTarget -> - iosTarget.binaries.framework { - baseName = "treenav" - isStatic = true - } - } linuxX64() macosX64() macosArm64() @@ -58,10 +43,5 @@ kotlin { implementation(kotlin("test")) } } - val jvmMain by getting - val jvmTest by getting - - val jsMain by getting - val jsTest by getting } } \ No newline at end of file diff --git a/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/MultiStackNav.kt b/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/MultiStackNav.kt index 76489ed..8eca493 100644 --- a/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/MultiStackNav.kt +++ b/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/MultiStackNav.kt @@ -147,7 +147,7 @@ val MultiStackNav.current: Node? get() = stacks.getOrNull(currentIndex)?.childre inline fun MultiStackNav.current(): T? { val node = current ?: return null check(node is T) { - "Expected the current node to be of type ${T::class.qualifiedName} but was ${node::class.qualifiedName}." + "Expected the current node to be of type ${T::class} but was ${node::class}." } return node } \ No newline at end of file diff --git a/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/StackNav.kt b/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/StackNav.kt index a69fc67..fd1299e 100644 --- a/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/StackNav.kt +++ b/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/StackNav.kt @@ -118,7 +118,7 @@ val StackNav.current: Node? get() = children.lastOrNull() inline fun StackNav.current(): T? { val node = current ?: return null check(node is T) { - "Expected the current node to be of type ${T::class.qualifiedName} but was ${node::class.qualifiedName}." + "Expected the current node to be of type ${T::class.simpleName} but was ${node::class.simpleName}." } return node } diff --git a/libraryVersion.properties b/libraryVersion.properties index 45b681f..88ab60c 100644 --- a/libraryVersion.properties +++ b/libraryVersion.properties @@ -14,6 +14,6 @@ # limitations under the License. # groupId=com.tunjid.treenav -treenav_version=0.0.21 -strings_version=0.0.21 -compose_version=0.0.21 \ No newline at end of file +treenav_version=0.0.22j +strings_version=0.0.22j +compose_version=0.0.22j \ No newline at end of file diff --git a/sample/common/build.gradle.kts b/sample/common/build.gradle.kts index 96d91db..db8527c 100755 --- a/sample/common/build.gradle.kts +++ b/sample/common/build.gradle.kts @@ -22,16 +22,6 @@ plugins { } kotlin { - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64(), - ).forEach { iosTarget -> - iosTarget.binaries.framework { - baseName = "common" - isStatic = true - } - } sourceSets { named("commonMain") { dependencies { @@ -69,17 +59,7 @@ kotlin { } named("androidMain") { dependencies { - implementation(libs.androidx.compose.foundation.layout) } } - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(named("commonMain").get()) - iosX64Main.dependsOn(this) - iosArm64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - } } } From fce7bbb1591023a410c17a725ea01d9e9908e058 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 4 May 2025 15:05:29 -0400 Subject: [PATCH 04/11] Move threepane to its own module --- .../kotlin/android-app-library-convention.kt | 2 +- library/compose-threepane/.gitignore | 1 + library/compose-threepane/build.gradle.kts | 34 +++++++++++++++++++ .../treenav/compose/threepane/ThreePane.kt | 0 .../ThreePaneSharedTransitionScope.kt | 20 +++++------ .../transforms/BackPreviewTransform.kt | 0 .../MovableSharedElementTransform.kt | 16 +++++++++ .../transforms/ThreePaneAdaptiveTransform.kt | 16 +++++++++ libraryVersion.properties | 3 +- sample/common/build.gradle.kts | 1 + settings.gradle.kts | 2 +- 11 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 library/compose-threepane/.gitignore create mode 100644 library/compose-threepane/build.gradle.kts rename library/{compose/src/commonMain => compose-threepane/src/main}/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt (100%) rename library/{compose/src/commonMain => compose-threepane/src/main}/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt (88%) rename library/{compose/src/commonMain => compose-threepane/src/main}/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt (100%) rename library/{compose/src/commonMain => compose-threepane/src/main}/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt (90%) rename library/{compose/src/commonMain => compose-threepane/src/main}/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt (78%) diff --git a/build-logic/convention/src/main/kotlin/android-app-library-convention.kt b/build-logic/convention/src/main/kotlin/android-app-library-convention.kt index 072e710..512f675 100644 --- a/build-logic/convention/src/main/kotlin/android-app-library-convention.kt +++ b/build-logic/convention/src/main/kotlin/android-app-library-convention.kt @@ -41,7 +41,7 @@ import org.gradle.api.artifacts.VersionCatalogsExtension fun org.gradle.api.Project.androidConfiguration( extension: CommonExtension<*, *, *, *, *, *> ) = extension.apply { - namespace = "com.tunjid.treenav.${project.name}" + namespace = "com.tunjid.treenav.${project.name.replace("-", ".")}" compileSdk = 36 defaultConfig { diff --git a/library/compose-threepane/.gitignore b/library/compose-threepane/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/library/compose-threepane/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/library/compose-threepane/build.gradle.kts b/library/compose-threepane/build.gradle.kts new file mode 100644 index 0000000..6645d65 --- /dev/null +++ b/library/compose-threepane/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + kotlin("multiplatform") + id("publishing-library-convention") + id("android-library-convention") + id("kotlin-jvm-convention") + id("kotlin-library-convention") + id("maven-publish") + signing + id("org.jetbrains.dokka") + id("org.jetbrains.compose") + alias(libs.plugins.compose.compiler) +} + +android { + buildFeatures { + compose = true + } +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":library:treenav")) + implementation(project(":library:compose")) + + implementation(libs.jetbrains.compose.runtime) + implementation(libs.jetbrains.compose.foundation) + implementation(libs.jetbrains.compose.foundation.layout) + } + } + } +} + diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt b/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt similarity index 100% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt rename to library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt b/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt similarity index 88% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt rename to library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt index b90b307..15f26d7 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt +++ b/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt @@ -1,17 +1,17 @@ /* - * Copyright 2024 Adetunji Dahunsi + * Copyright 2021 Google LLC * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.tunjid.treenav.compose.threepane diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt b/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt similarity index 100% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt rename to library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt b/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt similarity index 90% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt rename to library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt index 3a74e02..f87f451 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt +++ b/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.tunjid.treenav.compose.threepane.transforms import androidx.compose.animation.BoundsTransform diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt b/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt similarity index 78% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt rename to library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt index b9293ba..3656215 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt +++ b/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.tunjid.treenav.compose.threepane.transforms import androidx.compose.runtime.State diff --git a/libraryVersion.properties b/libraryVersion.properties index 88ab60c..851cd22 100644 --- a/libraryVersion.properties +++ b/libraryVersion.properties @@ -16,4 +16,5 @@ groupId=com.tunjid.treenav treenav_version=0.0.22j strings_version=0.0.22j -compose_version=0.0.22j \ No newline at end of file +compose_version=0.0.22j +compose-threepane_version=0.0.22j \ No newline at end of file diff --git a/sample/common/build.gradle.kts b/sample/common/build.gradle.kts index db8527c..3e450ed 100755 --- a/sample/common/build.gradle.kts +++ b/sample/common/build.gradle.kts @@ -28,6 +28,7 @@ kotlin { implementation(project(":library:treenav")) implementation(project(":library:strings")) implementation(project(":library:compose")) + implementation(project(":library:compose-threepane")) implementation(compose.components.resources) diff --git a/settings.gradle.kts b/settings.gradle.kts index 95e7e3f..80776a3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -46,8 +46,8 @@ include( ":library:treenav", ":library:strings", ":library:compose", + ":library:compose-threepane", ":sample:android", ":sample:common", ":sample:desktop", ) - From 3bf4dbe961ae4240079f83309c75790cf593fcae Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 4 May 2025 15:28:21 -0400 Subject: [PATCH 05/11] Fix source files directory for three pane module --- .../kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt | 0 .../compose/threepane/ThreePaneSharedTransitionScope.kt | 0 .../compose/threepane/transforms/BackPreviewTransform.kt | 0 .../threepane/transforms/MovableSharedElementTransform.kt | 0 .../compose/threepane/transforms/ThreePaneAdaptiveTransform.kt | 0 .../tunjid/demo/common/ui/chat/{Strategy.kt => PaneEntry.kt} | 0 .../demo/common/ui/chatrooms/{Strategy.kt => PaneEntry.kt} | 3 +-- .../com/tunjid/demo/common/ui/me/{Strategy.kt => PaneEntry.kt} | 0 .../demo/common/ui/profile/{Strategy.kt => PaneEntry.kt} | 0 9 files changed, 1 insertion(+), 2 deletions(-) rename library/compose-threepane/src/{main => commonMain}/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt (100%) rename library/compose-threepane/src/{main => commonMain}/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt (100%) rename library/compose-threepane/src/{main => commonMain}/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt (100%) rename library/compose-threepane/src/{main => commonMain}/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt (100%) rename library/compose-threepane/src/{main => commonMain}/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt (100%) rename sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/{Strategy.kt => PaneEntry.kt} (100%) rename sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/{Strategy.kt => PaneEntry.kt} (95%) rename sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/{Strategy.kt => PaneEntry.kt} (100%) rename sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/{Strategy.kt => PaneEntry.kt} (100%) diff --git a/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt similarity index 100% rename from library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt rename to library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt diff --git a/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt similarity index 100% rename from library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt rename to library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt diff --git a/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt similarity index 100% rename from library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt rename to library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt diff --git a/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt similarity index 100% rename from library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt rename to library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt diff --git a/library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt similarity index 100% rename from library/compose-threepane/src/main/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt rename to library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/PaneEntry.kt similarity index 100% rename from sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt rename to sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/PaneEntry.kt diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.kt similarity index 95% rename from sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt rename to sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.kt index e2f7e6f..987e5ad 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chatrooms/PaneEntry.kt @@ -26,12 +26,11 @@ import com.tunjid.demo.common.ui.PaneBottomAppBar import com.tunjid.demo.common.ui.PaneNavigationRail import com.tunjid.demo.common.ui.PaneScaffold import com.tunjid.demo.common.ui.data.ChatsRepository -import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.predictiveBackBackgroundModifier import com.tunjid.treenav.compose.threepane.threePaneEntry fun chatRoomPaneEntry( -) = threePaneEntry( +) = threePaneEntry( render = { val scope = LocalLifecycleOwner.current.lifecycle.coroutineScope val viewModel = viewModel { diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/PaneEntry.kt similarity index 100% rename from sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/Strategy.kt rename to sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/me/PaneEntry.kt diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/PaneEntry.kt similarity index 100% rename from sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt rename to sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/PaneEntry.kt From 68a906347cb6a63d2fa1539dc328f99f6509cf76 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 4 May 2025 16:17:32 -0400 Subject: [PATCH 06/11] Add PaneMovableElementSharedTransitionScope --- ...PaneMovableElementSharedTransitionScope.kt | 59 +++++++++++++++++++ .../ThreePaneSharedTransitionScope.kt | 2 +- .../MovableSharedElementTransform.kt | 2 +- .../com/tunjid/demo/common/ui/PaneScaffold.kt | 9 ++- 4 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt diff --git a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt new file mode 100644 index 0000000..391c716 --- /dev/null +++ b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tunjid.treenav.compose.threepane + +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.PaneScope +import com.tunjid.treenav.compose.PaneSharedTransitionScope +import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope +import com.tunjid.treenav.compose.threepane.transforms.requireMovableSharedElementScope + + +@Stable +interface PaneMovableElementSharedTransitionScope : + PaneSharedTransitionScope, MovableSharedElementScope + +@Stable +private class DelegatingPaneMovableElementSharedTransitionScope( + val paneSharedTransitionScope: PaneSharedTransitionScope, + val movableSharedElementScope: MovableSharedElementScope, +) : PaneMovableElementSharedTransitionScope, + PaneSharedTransitionScope by paneSharedTransitionScope, + MovableSharedElementScope by movableSharedElementScope + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun PaneScope< + ThreePane, + Destination + >.rememberPaneMovableElementSharedTransitionScope(): PaneMovableElementSharedTransitionScope { + val movableSharedElementScope = requireMovableSharedElementScope() + val paneSharedTransitionScope = rememberPaneSharedTransitionScope( + movableSharedElementScope.sharedTransitionScope + ) + return remember { + DelegatingPaneMovableElementSharedTransitionScope( + paneSharedTransitionScope = paneSharedTransitionScope, + movableSharedElementScope = movableSharedElementScope, + ) + } +} + diff --git a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt index 15f26d7..bf3bc52 100644 --- a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt +++ b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePaneSharedTransitionScope.kt @@ -42,7 +42,7 @@ import com.tunjid.treenav.compose.PaneSharedTransitionScope fun PaneScope< ThreePane, Destination - >.rememberPanedSharedTransitionScope( + >.rememberPaneSharedTransitionScope( sharedTransitionScope: SharedTransitionScope, ): PaneSharedTransitionScope = remember { diff --git a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt index f87f451..c831f70 100644 --- a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt +++ b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt @@ -69,7 +69,7 @@ fun fun PaneScope< ThreePane, Destination - >.requireThreePaneMovableSharedElementScope(): MovableSharedElementScope { + >.requireMovableSharedElementScope(): MovableSharedElementScope { check(this is ThreePaneMovableSharedElementScope) { """ The current PaneScope (${this::class.qualifiedName}) is not an instance of diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt index 018598d..e1112cc 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntSize @@ -61,8 +60,8 @@ import com.tunjid.treenav.compose.PaneScope import com.tunjid.treenav.compose.PaneSharedTransitionScope import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope import com.tunjid.treenav.compose.threepane.ThreePane -import com.tunjid.treenav.compose.threepane.rememberPanedSharedTransitionScope -import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope +import com.tunjid.treenav.compose.threepane.rememberPaneSharedTransitionScope +import com.tunjid.treenav.compose.threepane.transforms.requireMovableSharedElementScope import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNotNull import kotlin.math.abs @@ -136,8 +135,8 @@ fun PaneScope.PaneScaffold( ) { val appState = LocalAppState.current val snackbarHostState = remember { SnackbarHostState() } - val movableSharedElementScope = requireThreePaneMovableSharedElementScope() - val paneSharedTransitionScope = rememberPanedSharedTransitionScope( + val movableSharedElementScope = requireMovableSharedElementScope() + val paneSharedTransitionScope = rememberPaneSharedTransitionScope( sharedTransitionScope = movableSharedElementScope.sharedTransitionScope, ) val paneScaffoldState = From c028b0aacb40e46151fadb5538f64ef5f134915c Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 4 May 2025 16:38:07 -0400 Subject: [PATCH 07/11] Add kdocs for PaneMovableElementSharedTransitionScope --- ...PaneMovableElementSharedTransitionScope.kt | 35 +++++++++----- .../MovableSharedElementTransform.kt | 5 ++ .../com/tunjid/demo/common/ui/PaneScaffold.kt | 47 ++++--------------- 3 files changed, 37 insertions(+), 50 deletions(-) diff --git a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt index 391c716..88d899c 100644 --- a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt +++ b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/PaneMovableElementSharedTransitionScope.kt @@ -26,26 +26,31 @@ import com.tunjid.treenav.compose.PaneSharedTransitionScope import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope import com.tunjid.treenav.compose.threepane.transforms.requireMovableSharedElementScope - +/** + * An interface providing both [MovableSharedElementScope] and [PaneSharedTransitionScope] for + * a [ThreePane] layout. + */ @Stable interface PaneMovableElementSharedTransitionScope : PaneSharedTransitionScope, MovableSharedElementScope -@Stable -private class DelegatingPaneMovableElementSharedTransitionScope( - val paneSharedTransitionScope: PaneSharedTransitionScope, - val movableSharedElementScope: MovableSharedElementScope, -) : PaneMovableElementSharedTransitionScope, - PaneSharedTransitionScope by paneSharedTransitionScope, - MovableSharedElementScope by movableSharedElementScope - +/** + * Remembers a [PaneMovableElementSharedTransitionScope] in the composition. + * + * @param movableSharedElementScope The [MovableSharedElementScope] used create a + * [PaneSharedTransitionScope] for this [PaneScope]. + * + * If one is not provided, one is retrieved from this [PaneScope] using + * [requireMovableSharedElementScope]. + */ @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun PaneScope< ThreePane, Destination - >.rememberPaneMovableElementSharedTransitionScope(): PaneMovableElementSharedTransitionScope { - val movableSharedElementScope = requireMovableSharedElementScope() + >.rememberPaneMovableElementSharedTransitionScope( + movableSharedElementScope: MovableSharedElementScope = requireMovableSharedElementScope() +): PaneMovableElementSharedTransitionScope { val paneSharedTransitionScope = rememberPaneSharedTransitionScope( movableSharedElementScope.sharedTransitionScope ) @@ -57,3 +62,11 @@ fun PaneScope< } } +@Stable +private class DelegatingPaneMovableElementSharedTransitionScope( + val paneSharedTransitionScope: PaneSharedTransitionScope, + val movableSharedElementScope: MovableSharedElementScope, +) : PaneMovableElementSharedTransitionScope, + PaneSharedTransitionScope by paneSharedTransitionScope, + MovableSharedElementScope by movableSharedElementScope + diff --git a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt index c831f70..f0d2407 100644 --- a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt +++ b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt @@ -66,6 +66,11 @@ fun previousTransform(movableSharedElementScope, destination) } +/** + * Requires that this [PaneScope] is a [MovableSharedElementScope], and returns it. In the + * case this [PaneScope] is not a [MovableSharedElementScope], an exception will be thrown. + */ +@Stable fun PaneScope< ThreePane, Destination diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt index e1112cc..d6db74b 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/PaneScaffold.kt @@ -19,7 +19,6 @@ package com.tunjid.demo.common.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.BoundsTransform import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.animateBounds import androidx.compose.animation.core.snap import androidx.compose.animation.core.spring @@ -57,11 +56,9 @@ import androidx.compose.ui.zIndex import com.tunjid.composables.ui.skipIf import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.PaneSharedTransitionScope -import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope +import com.tunjid.treenav.compose.threepane.PaneMovableElementSharedTransitionScope import com.tunjid.treenav.compose.threepane.ThreePane -import com.tunjid.treenav.compose.threepane.rememberPaneSharedTransitionScope -import com.tunjid.treenav.compose.threepane.transforms.requireMovableSharedElementScope +import com.tunjid.treenav.compose.threepane.rememberPaneMovableElementSharedTransitionScope import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNotNull import kotlin.math.abs @@ -69,11 +66,8 @@ import kotlin.math.abs @Stable class PaneScaffoldState internal constructor( private val appState: AppState, - private val movableSharedElementScope: MovableSharedElementScope, - paneSharedTransitionScope: PaneSharedTransitionScope, -) : - MovableSharedElementScope by movableSharedElementScope, - PaneSharedTransitionScope by paneSharedTransitionScope { + paneMovableElementSharedTransitionScope: PaneMovableElementSharedTransitionScope, +) : PaneMovableElementSharedTransitionScope by paneMovableElementSharedTransitionScope { internal val canShowBottomNavigation get() = !appState.isMediumScreenWidthOrWider @@ -97,27 +91,6 @@ class PaneScaffoldState internal constructor( internal fun hasMatchedSize(): Boolean = abs(scaffoldCurrentSize.width - scaffoldTargetSize.width) <= 2 && abs(scaffoldCurrentSize.height - scaffoldTargetSize.height) <= 2 - - @OptIn(ExperimentalSharedTransitionApi::class) - override fun movableSharedElementOf( - key: Any, - boundsTransform: BoundsTransform, - placeHolderSize: SharedTransitionScope.PlaceHolderSize, - renderInOverlayDuringTransition: Boolean, - zIndexInOverlay: Float, - clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip, - alternateOutgoingSharedElement:@Composable ((T, Modifier) -> Unit)?, - sharedElement:@Composable (T, Modifier) -> Unit, - ):@Composable (T, Modifier) -> Unit = movableSharedElementScope.movableSharedElementOf( - key = key, - boundsTransform = boundsTransform, - placeHolderSize = placeHolderSize, - renderInOverlayDuringTransition = renderInOverlayDuringTransition, - zIndexInOverlay = zIndexInOverlay, - clipInOverlayDuringTransition = clipInOverlayDuringTransition, - alternateOutgoingSharedElement = alternateOutgoingSharedElement, - sharedElement = sharedElement, - ) } @OptIn(ExperimentalSharedTransitionApi::class) @@ -135,16 +108,12 @@ fun PaneScope.PaneScaffold( ) { val appState = LocalAppState.current val snackbarHostState = remember { SnackbarHostState() } - val movableSharedElementScope = requireMovableSharedElementScope() - val paneSharedTransitionScope = rememberPaneSharedTransitionScope( - sharedTransitionScope = movableSharedElementScope.sharedTransitionScope, - ) + val paneMovableElementSharedTransitionScope = rememberPaneMovableElementSharedTransitionScope() val paneScaffoldState = - remember(appState, movableSharedElementScope, paneSharedTransitionScope) { + remember(appState, paneMovableElementSharedTransitionScope) { PaneScaffoldState( appState = appState, - movableSharedElementScope = movableSharedElementScope, - paneSharedTransitionScope = paneSharedTransitionScope, + paneMovableElementSharedTransitionScope = paneMovableElementSharedTransitionScope, ) } @@ -166,7 +135,7 @@ fun PaneScope.PaneScaffold( Scaffold( modifier = Modifier .animateBounds( - lookaheadScope = movableSharedElementScope.sharedTransitionScope, + lookaheadScope = paneMovableElementSharedTransitionScope, boundsTransform = remember { scaffoldBoundsTransform( paneScaffoldState = paneScaffoldState, From 548677696d290ae3bd9e6a3e25623156cd7d7c05 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 4 May 2025 16:42:20 -0400 Subject: [PATCH 08/11] Version bump to 0.0.22 --- libraryVersion.properties | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraryVersion.properties b/libraryVersion.properties index 851cd22..a43a917 100644 --- a/libraryVersion.properties +++ b/libraryVersion.properties @@ -14,7 +14,7 @@ # limitations under the License. # groupId=com.tunjid.treenav -treenav_version=0.0.22j -strings_version=0.0.22j -compose_version=0.0.22j -compose-threepane_version=0.0.22j \ No newline at end of file +treenav_version=0.0.22 +strings_version=0.0.22 +compose_version=0.0.22 +compose-threepane_version=0.0.22 \ No newline at end of file From 7977e101c5be0cfe2164d5fa7c1cc37866b476c5 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 4 May 2025 16:50:03 -0400 Subject: [PATCH 09/11] Opt in to experimental APIs --- .../src/main/kotlin/kotlin-jvm-convention.kt | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/kotlin-jvm-convention.kt b/build-logic/convention/src/main/kotlin/kotlin-jvm-convention.kt index 8e6d183..cd710a8 100644 --- a/build-logic/convention/src/main/kotlin/kotlin-jvm-convention.kt +++ b/build-logic/convention/src/main/kotlin/kotlin-jvm-convention.kt @@ -62,26 +62,13 @@ private fun org.gradle.api.Project.configureKotlin() { jvmTarget.set(JvmTarget.JVM_11) freeCompilerArgs.set( freeCompilerArgs.get() + listOf( - "-Xuse-experimental=androidx.compose.animation.ExperimentalAnimationApi", - "-Xuse-experimental=androidx.compose.material.ExperimentalMaterialApi", - "-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi", - "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", - "-Xuse-experimental=kotlinx.coroutines.FlowPreview" + "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", + "-opt-in=androidx.compose.material.ExperimentalMaterialApi", + "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlinx.coroutines.FlowPreview" ) ) } } -// val kotlinOptions = "kotlinOptions" -// if (extensions.findByName(kotlinOptions) != null) { -// extensions.configure(kotlinOptions, Action { -// jvmTarget = JavaVersion.VERSION_11.toString() -// freeCompilerArgs = freeCompilerArgs + listOf( -// "-Xuse-experimental=androidx.compose.animation.ExperimentalAnimationApi", -// "-Xuse-experimental=androidx.compose.material.ExperimentalMaterialApi", -// "-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi", -// "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", -// "-Xuse-experimental=kotlinx.coroutines.FlowPreview" -// ) -// }) -// } } \ No newline at end of file From 9403716a79a26eae31d710ee34a1d51bdca73f1a Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 4 May 2025 17:07:49 -0400 Subject: [PATCH 10/11] Rename PanedMovableSharedElementScope to PaneMovableSharedElementScope --- .../threepane/transforms/MovableSharedElementTransform.kt | 6 +++--- .../compose/moveablesharedelement/MovableSharedElements.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt index f0d2407..906ab36 100644 --- a/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt +++ b/library/compose-threepane/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt @@ -33,7 +33,7 @@ import com.tunjid.treenav.compose.transforms.RenderTransform import com.tunjid.treenav.compose.transforms.Transform import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementHostState import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementScope -import com.tunjid.treenav.compose.moveablesharedelement.PanedMovableSharedElementScope +import com.tunjid.treenav.compose.moveablesharedelement.PaneMovableSharedElementScope import com.tunjid.treenav.compose.threepane.ThreePane /** @@ -49,7 +49,7 @@ fun ): Transform = RenderTransform { destination, previousTransform -> val delegate = remember { - PanedMovableSharedElementScope( + PaneMovableSharedElementScope( paneScope = this, movableSharedElementHostState = movableSharedElementHostState, ) @@ -89,7 +89,7 @@ fun PaneScope< @Stable private class ThreePaneMovableSharedElementScope( private val hostState: MovableSharedElementHostState, - private val delegate: PanedMovableSharedElementScope, + private val delegate: PaneMovableSharedElementScope, ) : MovableSharedElementScope, PaneScope by delegate.paneScope { diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt index 6b95ef2..2c3ce1b 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/moveablesharedelement/MovableSharedElements.kt @@ -188,7 +188,7 @@ class MovableSharedElementHostState( */ @OptIn(ExperimentalSharedTransitionApi::class) @Stable -class PanedMovableSharedElementScope( +class PaneMovableSharedElementScope( paneScope: PaneScope, private val movableSharedElementHostState: MovableSharedElementHostState, ) : MovableSharedElementScope { From 26c7632880350cf11fb484fccf66234d95209b7d Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 4 May 2025 17:16:32 -0400 Subject: [PATCH 11/11] Fix tests --- .../SlotBasedAdaptiveNavigationStateTest.kt | 247 +++++++++--------- 1 file changed, 126 insertions(+), 121 deletions(-) diff --git a/library/compose/src/commonTest/kotlin/com/tunjid/treenav/compose/SlotBasedAdaptiveNavigationStateTest.kt b/library/compose/src/commonTest/kotlin/com/tunjid/treenav/compose/SlotBasedAdaptiveNavigationStateTest.kt index f59d720..2589d39 100644 --- a/library/compose/src/commonTest/kotlin/com/tunjid/treenav/compose/SlotBasedAdaptiveNavigationStateTest.kt +++ b/library/compose/src/commonTest/kotlin/com/tunjid/treenav/compose/SlotBasedAdaptiveNavigationStateTest.kt @@ -19,7 +19,6 @@ package com.tunjid.treenav.compose import com.tunjid.treenav.Node import com.tunjid.treenav.StackNav import com.tunjid.treenav.backStack -import com.tunjid.treenav.compose.threepane.ThreePane import com.tunjid.treenav.current import com.tunjid.treenav.pop import com.tunjid.treenav.push @@ -52,16 +51,22 @@ data class TestNode( override val id: String get() = name } +enum class TestPane { + One, + Two, + Three; +} + class SlotBasedAdaptiveNavigationStateTest { - private lateinit var subject: SlotBasedPanedNavigationState - private lateinit var panes: List + private lateinit var subject: SlotBasedPanedNavigationState + private lateinit var panes: List private lateinit var slots: Set @BeforeTest fun setup() { - panes = ThreePane.entries.toList() + panes = TestPane.entries.toList() slots = List(size = panes.size, init = ::Slot).toSet() subject = SlotBasedPanedNavigationState.initial( slots = slots @@ -73,20 +78,20 @@ class SlotBasedAdaptiveNavigationStateTest { subject.testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), + TestPane.One to TestNode(name = "A"), ) ) .apply { assertEquals( - expected = destinationFor(ThreePane.Primary), + expected = destinationFor(TestPane.One), actual = TestNode(name = "A"), ) assertEquals( - expected = adaptationsIn(ThreePane.Primary), + expected = adaptationsIn(TestPane.One), actual = setOf(Adaptation.Change), ) assertEquals( - expected = slotFor(ThreePane.Primary), + expected = slotFor(TestPane.One), actual = Slot(0), ) } @@ -98,51 +103,51 @@ class SlotBasedAdaptiveNavigationStateTest { .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), - ThreePane.Secondary to TestNode(name = "B"), - ThreePane.Tertiary to TestNode(name = "C"), + TestPane.One to TestNode(name = "A"), + TestPane.Two to TestNode(name = "B"), + TestPane.Three to TestNode(name = "C"), ) ) .apply { // Primary assertEquals( - expected = destinationFor(ThreePane.Primary), + expected = destinationFor(TestPane.One), actual = TestNode(name = "A") ) assertEquals( - expected = adaptationsIn(ThreePane.Primary), + expected = adaptationsIn(TestPane.One), actual = setOf(Adaptation.Change) ) assertEquals( - expected = slotFor(ThreePane.Primary), + expected = slotFor(TestPane.One), actual = Slot(0) ) // Secondary assertEquals( - expected = destinationFor(ThreePane.Secondary), + expected = destinationFor(TestPane.Two), actual = TestNode(name = "B") ) assertEquals( - expected = adaptationsIn(ThreePane.Secondary), + expected = adaptationsIn(TestPane.Two), actual = setOf(Adaptation.Change) ) assertEquals( - expected = slotFor(ThreePane.Secondary), + expected = slotFor(TestPane.Two), actual = Slot(1) ) // Tertiary assertEquals( - expected = destinationFor(ThreePane.Tertiary), + expected = destinationFor(TestPane.Three), actual = TestNode(name = "C") ) assertEquals( - expected = adaptationsIn(ThreePane.Tertiary), + expected = adaptationsIn(TestPane.Three), actual = setOf(Adaptation.Change) ) assertEquals( - expected = slotFor(ThreePane.Tertiary), + expected = slotFor(TestPane.Three), actual = Slot(2) ) } @@ -154,164 +159,164 @@ class SlotBasedAdaptiveNavigationStateTest { .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), + TestPane.One to TestNode(name = "A"), ) ) .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), + TestPane.One to TestNode(name = "A"), ) ) .apply { assertEquals( expected = TestNode(name = "A"), - actual = destinationFor(ThreePane.Primary), + actual = destinationFor(TestPane.One), ) assertEquals( expected = setOf(Adaptation.Same), - actual = adaptationsIn(ThreePane.Primary), + actual = adaptationsIn(TestPane.One), ) assertEquals( expected = Slot(0), - actual = slotFor(ThreePane.Primary), + actual = slotFor(TestPane.One), ) } } @Test - fun testSameAdaptationInThreePanes() { + fun testSameAdaptationInTestPanes() { subject .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), - ThreePane.Secondary to TestNode(name = "B"), - ThreePane.Tertiary to TestNode(name = "C"), + TestPane.One to TestNode(name = "A"), + TestPane.Two to TestNode(name = "B"), + TestPane.Three to TestNode(name = "C"), ) ) .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), - ThreePane.Secondary to TestNode(name = "B"), - ThreePane.Tertiary to TestNode(name = "C"), + TestPane.One to TestNode(name = "A"), + TestPane.Two to TestNode(name = "B"), + TestPane.Three to TestNode(name = "C"), ) ) .apply { // Primary assertEquals( expected = TestNode(name = "A"), - actual = destinationFor(ThreePane.Primary), + actual = destinationFor(TestPane.One), ) assertEquals( expected = setOf(Adaptation.Same), - actual = adaptationsIn(ThreePane.Primary), + actual = adaptationsIn(TestPane.One), ) assertEquals( expected = Slot(0), - actual = slotFor(ThreePane.Primary), + actual = slotFor(TestPane.One), ) // Secondary assertEquals( expected = TestNode(name = "B"), - actual = destinationFor(ThreePane.Secondary), + actual = destinationFor(TestPane.Two), ) assertEquals( expected = setOf(Adaptation.Same), - actual = adaptationsIn(ThreePane.Secondary), + actual = adaptationsIn(TestPane.Two), ) assertEquals( expected = Slot(1), - actual = slotFor(ThreePane.Secondary), + actual = slotFor(TestPane.Two), ) // Tertiary assertEquals( expected = TestNode(name = "C"), - actual = destinationFor(ThreePane.Tertiary), + actual = destinationFor(TestPane.Three), ) assertEquals( expected = setOf(Adaptation.Same), - actual = adaptationsIn(ThreePane.Tertiary), + actual = adaptationsIn(TestPane.Three), ) assertEquals( expected = Slot(2), - actual = slotFor(ThreePane.Tertiary), + actual = slotFor(TestPane.Three), ) } } @Test - fun testChangeAdaptationInThreePanes() { + fun testChangeAdaptationInTestPanes() { subject .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), - ThreePane.Secondary to TestNode(name = "B"), - ThreePane.Tertiary to TestNode(name = "C"), + TestPane.One to TestNode(name = "A"), + TestPane.Two to TestNode(name = "B"), + TestPane.Three to TestNode(name = "C"), ) ) .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "B"), - ThreePane.Secondary to TestNode(name = "C"), - ThreePane.Tertiary to TestNode(name = "A"), + TestPane.One to TestNode(name = "B"), + TestPane.Two to TestNode(name = "C"), + TestPane.Three to TestNode(name = "A"), ) ) .apply { // Primary assertEquals( expected = TestNode(name = "B"), - actual = destinationFor(ThreePane.Primary), + actual = destinationFor(TestPane.One), ) assertEquals( expected = setOf( - Adaptation.Swap(from = ThreePane.Primary, to = ThreePane.Tertiary), - Adaptation.Swap(from = ThreePane.Secondary, to = ThreePane.Primary), + Adaptation.Swap(from = TestPane.One, to = TestPane.Three), + Adaptation.Swap(from = TestPane.Two, to = TestPane.One), ), - actual = adaptationsIn(ThreePane.Primary), + actual = adaptationsIn(TestPane.One), ) assertEquals( expected = Slot(1), - actual = slotFor(ThreePane.Primary), + actual = slotFor(TestPane.One), ) // Secondary assertEquals( expected = TestNode(name = "C"), - actual = destinationFor(ThreePane.Secondary), + actual = destinationFor(TestPane.Two), ) assertEquals( expected = setOf( - Adaptation.Swap(from = ThreePane.Tertiary, to = ThreePane.Secondary), - Adaptation.Swap(from = ThreePane.Secondary, to = ThreePane.Primary), + Adaptation.Swap(from = TestPane.Three, to = TestPane.Two), + Adaptation.Swap(from = TestPane.Two, to = TestPane.One), ), - actual = adaptationsIn(ThreePane.Secondary), + actual = adaptationsIn(TestPane.Two), ) assertEquals( expected = Slot(2), - actual = slotFor(ThreePane.Secondary), + actual = slotFor(TestPane.Two), ) // Tertiary assertEquals( expected = TestNode(name = "A"), - actual = destinationFor(ThreePane.Tertiary), + actual = destinationFor(TestPane.Three), ) assertEquals( expected = setOf( - Adaptation.Swap(from = ThreePane.Tertiary, to = ThreePane.Secondary), - Adaptation.Swap(from = ThreePane.Primary, to = ThreePane.Tertiary), + Adaptation.Swap(from = TestPane.Three, to = TestPane.Two), + Adaptation.Swap(from = TestPane.One, to = TestPane.Three), ), - actual = adaptationsIn(ThreePane.Tertiary), + actual = adaptationsIn(TestPane.Three), ) assertEquals( expected = Slot(0), - actual = slotFor(ThreePane.Tertiary), + actual = slotFor(TestPane.Three), ) } } @@ -322,48 +327,48 @@ class SlotBasedAdaptiveNavigationStateTest { .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), + TestPane.One to TestNode(name = "A"), ) ) .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "B"), - ThreePane.Secondary to TestNode(name = "A"), + TestPane.One to TestNode(name = "B"), + TestPane.Two to TestNode(name = "A"), ) ) .apply { // Destination assertions assertEquals( expected = TestNode(name = "B"), - actual = destinationFor(ThreePane.Primary), + actual = destinationFor(TestPane.One), ) assertEquals( expected = TestNode(name = "A"), - actual = destinationFor(ThreePane.Secondary), + actual = destinationFor(TestPane.Two), ) // Adaptation assertions assertEquals( expected = setOf( - Adaptation.Swap(from = ThreePane.Primary, to = ThreePane.Secondary), + Adaptation.Swap(from = TestPane.One, to = TestPane.Two), ), - actual = adaptationsIn(ThreePane.Primary), + actual = adaptationsIn(TestPane.One), ) assertEquals( - expected = setOf(ThreePane.PrimaryToSecondary), - actual = adaptationsIn(ThreePane.Secondary), + expected = setOf(Adaptation.Swap(TestPane.One, TestPane.Two)), + actual = adaptationsIn(TestPane.Two), ) // Slot assertions assertEquals( // Secondary should reuse slot 0 expected = Slot(0), - actual = slotFor(ThreePane.Secondary), + actual = slotFor(TestPane.Two), ) assertEquals( expected = Slot(1), - actual = slotFor(ThreePane.Primary), + actual = slotFor(TestPane.One), ) } } @@ -374,63 +379,63 @@ class SlotBasedAdaptiveNavigationStateTest { .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), + TestPane.One to TestNode(name = "A"), ) ) .apply { assertEquals( expected = Slot(0), - actual = slotFor(ThreePane.Primary), + actual = slotFor(TestPane.One), ) } .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "B"), + TestPane.One to TestNode(name = "B"), ) ) .apply { assertEquals( expected = Slot(0), - actual = slotFor(ThreePane.Primary), + actual = slotFor(TestPane.One), ) } .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), - ThreePane.TransientPrimary to TestNode(name = "B"), + TestPane.One to TestNode(name = "A"), + TestPane.Three to TestNode(name = "B"), ) ) .apply { // Destination assertions assertEquals( expected = TestNode(name = "A"), - actual = destinationFor(ThreePane.Primary), + actual = destinationFor(TestPane.One), ) assertEquals( expected = TestNode(name = "B"), - actual = destinationFor(ThreePane.TransientPrimary), + actual = destinationFor(TestPane.Three), ) // Adaptation assertions assertEquals( - expected = setOf(ThreePane.PrimaryToTransient), - actual = adaptationsIn(ThreePane.Primary), + expected = setOf(Adaptation.Swap(TestPane.One, TestPane.Three)), + actual = adaptationsIn(TestPane.One), ) assertEquals( - expected = setOf(ThreePane.PrimaryToTransient), - actual = adaptationsIn(ThreePane.TransientPrimary), + expected = setOf(Adaptation.Swap(TestPane.One, TestPane.Three)), + actual = adaptationsIn(TestPane.Three), ) // Slot assertions assertEquals( expected = Slot(1), - actual = slotFor(ThreePane.Primary), + actual = slotFor(TestPane.One), ) assertEquals( expected = Slot(0), - actual = slotFor(ThreePane.TransientPrimary), + actual = slotFor(TestPane.Three), ) } } @@ -441,52 +446,52 @@ class SlotBasedAdaptiveNavigationStateTest { .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), - ThreePane.Secondary to TestNode(name = "B"), + TestPane.One to TestNode(name = "A"), + TestPane.Two to TestNode(name = "B"), ) ) .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "C"), - ThreePane.TransientPrimary to TestNode(name = "A"), + TestPane.One to TestNode(name = "C"), + TestPane.Three to TestNode(name = "A"), ) ) .apply { // Destination assertions assertEquals( expected = TestNode(name = "C"), - actual = destinationFor(ThreePane.Primary), + actual = destinationFor(TestPane.One), ) assertEquals( expected = TestNode(name = "A"), - actual = destinationFor(ThreePane.TransientPrimary), + actual = destinationFor(TestPane.Three), ) // Adaptation assertions assertEquals( expected = setOf( - Adaptation.Swap(from = ThreePane.Primary, to = ThreePane.TransientPrimary), + Adaptation.Swap(from = TestPane.One, to = TestPane.Three), ), - actual = adaptationsIn(ThreePane.Primary), + actual = adaptationsIn(TestPane.One), ) assertEquals( - expected = setOf(ThreePane.PrimaryToTransient), - actual = adaptationsIn(ThreePane.TransientPrimary), + expected = setOf(Adaptation.Swap(TestPane.One, TestPane.Three)), + actual = adaptationsIn(TestPane.Three), ) // Slot assertions assertEquals( expected = Slot(1), - actual = slotFor(ThreePane.Primary), + actual = slotFor(TestPane.One), ) assertEquals( expected = Slot(0), - actual = slotFor(ThreePane.TransientPrimary), + actual = slotFor(TestPane.Three), ) assertEquals( expected = null, - actual = slotFor(ThreePane.Secondary), + actual = slotFor(TestPane.Two), ) } } @@ -497,63 +502,63 @@ class SlotBasedAdaptiveNavigationStateTest { .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "A"), - ThreePane.Secondary to TestNode(name = "B"), + TestPane.One to TestNode(name = "A"), + TestPane.Two to TestNode(name = "B"), ) ) .testAdaptTo( navState = EmptyNavState, panesToDestinations = mapOf( - ThreePane.Primary to TestNode(name = "C"), - ThreePane.Secondary to TestNode(name = "D"), - ThreePane.TransientPrimary to TestNode(name = "A"), + TestPane.One to TestNode(name = "C"), + TestPane.Two to TestNode(name = "D"), + TestPane.Three to TestNode(name = "A"), ) ) .apply { // Destination assertions assertEquals( expected = TestNode(name = "C"), - actual = destinationFor(ThreePane.Primary), + actual = destinationFor(TestPane.One), ) assertEquals( expected = TestNode(name = "D"), - actual = destinationFor(ThreePane.Secondary), + actual = destinationFor(TestPane.Two), ) assertEquals( expected = TestNode(name = "A"), - actual = destinationFor(ThreePane.TransientPrimary), + actual = destinationFor(TestPane.Three), ) // Adaptation assertions assertEquals( expected = setOf( - Adaptation.Swap(from = ThreePane.Primary, to = ThreePane.TransientPrimary) + Adaptation.Swap(from = TestPane.One, to = TestPane.Three) ), - actual = adaptationsIn(ThreePane.Primary), + actual = adaptationsIn(TestPane.One), ) assertEquals( expected = setOf(Adaptation.Change), - actual = adaptationsIn(ThreePane.Secondary), + actual = adaptationsIn(TestPane.Two), ) assertEquals( expected = setOf( - Adaptation.Swap(from = ThreePane.Primary, to = ThreePane.TransientPrimary) + Adaptation.Swap(from = TestPane.One, to = TestPane.Three) ), - actual = adaptationsIn(ThreePane.TransientPrimary), + actual = adaptationsIn(TestPane.Three), ) // Slot assertions assertEquals( expected = Slot(1), - actual = slotFor(ThreePane.Primary), + actual = slotFor(TestPane.One), ) assertEquals( expected = Slot(2), - actual = slotFor(ThreePane.Secondary), + actual = slotFor(TestPane.Two), ) assertEquals( expected = Slot(0), - actual = slotFor(ThreePane.TransientPrimary), + actual = slotFor(TestPane.Three), ) } } @@ -570,7 +575,7 @@ class SlotBasedAdaptiveNavigationStateTest { .testAdaptTo( navState = navStates[index], panesToDestinations = mapOf( - ThreePane.Primary to navStates[index].current(), + TestPane.One to navStates[index].current(), ) ) .apply { @@ -587,7 +592,7 @@ class SlotBasedAdaptiveNavigationStateTest { .testAdaptTo( navState = poppedNavStates[index], panesToDestinations = mapOf( - ThreePane.Primary to navStates[index].current(), + TestPane.One to navStates[index].current(), ) ) .apply { @@ -596,9 +601,9 @@ class SlotBasedAdaptiveNavigationStateTest { } } - private fun SlotBasedPanedNavigationState.testAdaptTo( + private fun SlotBasedPanedNavigationState.testAdaptTo( navState: StackNav, - panesToDestinations: Map, + panesToDestinations: Map, ) = adaptTo( slots = slots, backStackIds = navState.backStack(