From 9c2a4b1c3c72cc880e24c94fa5106e38094338b7 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 9 Feb 2025 10:01:27 -0500 Subject: [PATCH 01/11] Refactor to use Display terminology and Transform type --- .../treenav/compose/MultiPaneDisplay.kt | 108 +++++ .../treenav/compose/MultiPaneDisplayState.kt | 170 +++++++ .../com/tunjid/treenav/compose/PaneEntry.kt | 19 + .../com/tunjid/treenav/compose/PaneScope.kt | 2 +- .../tunjid/treenav/compose/PaneStrategy.kt | 66 --- .../tunjid/treenav/compose/PanedNavHost.kt | 46 -- .../compose/PanedNavHostConfiguration.kt | 136 ------ .../treenav/compose/PanedNavigationState.kt | 24 - .../compose/SavedStatePanedNavHostState.kt | 431 +++++++----------- .../compose/SlotBasedPanedNavigationState.kt | 20 +- ...ation.kt => AnimatePaneBoundsTransform.kt} | 42 +- ...figuration.kt => PaneModifierTransform.kt} | 30 +- .../compose/configurations/Transforms.kt | 35 ++ .../MovableSharedElements.kt | 4 +- .../treenav/compose/threepane/ThreePane.kt | 57 ++- .../PredictiveBackConfiguration.kt | 73 --- .../ThreePaneAdaptiveConfiguration.kt | 51 --- .../MovableSharedElementTransform.kt} | 63 ++- .../transforms/PredictiveBackTransform.kt | 79 ++++ .../transforms/ThreePaneAdaptiveTransform.kt | 44 ++ .../com/tunjid/demo/common/ui/DemoApp.kt | 137 +++--- .../tunjid/demo/common/ui/chat/Strategy.kt | 2 +- .../demo/common/ui/chatrooms/Strategy.kt | 2 +- .../com/tunjid/demo/common/ui/me/Strategy.kt | 2 +- .../tunjid/demo/common/ui/profile/Strategy.kt | 2 +- 25 files changed, 798 insertions(+), 847 deletions(-) create mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt create mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt create mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneEntry.kt delete mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneStrategy.kt delete mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavHost.kt delete mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavHostConfiguration.kt rename library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/{AnimatePaneBoundsConfiguration.kt => AnimatePaneBoundsTransform.kt} (62%) rename library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/{PaneModifierConfiguration.kt => PaneModifierTransform.kt} (55%) create mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/Transforms.kt delete mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/PredictiveBackConfiguration.kt delete mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/ThreePaneAdaptiveConfiguration.kt rename library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/{configurations/MovableSharedElementConfiguration.kt => transforms/MovableSharedElementTransform.kt} (74%) create mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt create mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt new file mode 100644 index 0000000..966366d --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt @@ -0,0 +1,108 @@ +/* + * 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.AnimatedContent +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.SaveableStateHolder +import androidx.compose.ui.Modifier +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import com.tunjid.treenav.Node + +/** + * Scope that provides context about individual panes [Pane] in an [MultiPaneDisplay]. + */ +@Stable +interface MultiPaneDisplayScope { + + @Composable + fun Destination( + pane: Pane, + ) + + fun adaptationsIn( + pane: Pane, + ): Set + + fun nodeFor( + pane: Pane, + ): Destination? +} + +/** + * A Display that provides the following for each + * navigation [Destination] that shows up in its panes: + * + * - A single [SaveableStateHolder] for each navigation [Destination] that shows up in its panes. + * [SaveableStateHolder.SaveableStateProvider] is keyed on the [Destination]s [Node.id]. + * + * - A [ViewModelStoreOwner] for each [Destination] via [LocalViewModelStoreOwner]. + * Once present in the navigation tree, a [Destination] will always use the same + * [ViewModelStoreOwner], regardless of where in the tree it is, until its is removed from the tree. + * [Destination]s are unique based on their [Node.id]. + * + * - A [LifecycleOwner] for each [Destination] via [LocalLifecycleOwner]. This [LifecycleOwner] + * follows the [Lifecycle] of its immediate parent, unless it is animating out or placed in the + * backstack. This is defined by [PaneScope.isActive], which is a function of the backing + * [AnimatedContent] for each [Pane] displayed and if the current [Destination] + * matches [MultiPaneDisplayScope.nodeFor] in the visible [Pane]. + * + * @param state the driving [MultiPaneDisplayState] that applies adaptive semantics and + * strategies for each navigation destination shown in the [MultiPaneDisplay]. + */ +@Composable +fun MultiPaneDisplay( + state: MultiPaneDisplayState, + modifier: Modifier = Modifier, + content: @Composable MultiPaneDisplayScope.() -> Unit, +) { + + val navigationState by state.navigationState + val panesToNodes = state.panesToDestinations() + val saveableStateHolder = rememberPanedSaveableStateHolder() + + val panedContentScope = remember { + SlottedMultiPaneDisplayScope( + panes = state.panes, + initialPanesToNodes = panesToNodes, + saveableStateHolder = saveableStateHolder, + displayState = state, + ) + } + + LaunchedEffect(navigationState, panesToNodes) { + panedContentScope.onNewNavigationState( + navigationState = navigationState, + panesToNodes = panesToNodes + ) + } + + Box( + modifier = modifier + ) { + panedContentScope.content() + } +} \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt new file mode 100644 index 0000000..a441367 --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt @@ -0,0 +1,170 @@ +/* + * 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.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.configurations.DestinationTransform +import com.tunjid.treenav.compose.configurations.PaneTransform +import com.tunjid.treenav.compose.configurations.RenderTransform +import com.tunjid.treenav.compose.configurations.Transform + +/** + * Class for configuring a [MultiPaneDisplay] for selecting, adapting and placing navigation + * destinations into different panes from an arbitrary [navigationState]. + * + * @param panes a list of panes that is possible to show in the [MultiPaneDisplay] in all + * possible configurations. The panes should consist of enum class instances, or a sealed class + * hierarchy of kotlin objects. + * @param navigationState the navigation state to be adapted into various panes. + * @param backStackTransform a transform to read the back stack of the navigation state. + * @param destinationTransform a transform of the [navigationState] to its current destination. + * @param panesToDestinationsTransform provides the strategy used to adapt the current + * [Destination] to the panes available. + * @param renderTransform the transform used to render a [Destination] in its pane. + */ +class MultiPaneDisplayState internal constructor( + val panes: List, + val navigationState: State, + val backStackTransform: (NavigationState) -> List, + val destinationTransform: (NavigationState) -> Destination, + val panesToDestinationsTransform: @Composable (Destination) -> Map, + val renderTransform: @Composable PaneScope.(Destination) -> Unit, +) { + internal val currentDestination: State = derivedStateOf { + destinationTransform(navigationState.value) + } +} + +/** + * Provides an [MultiPaneDisplayState] for configuring a [MultiPaneDisplay] for + * showing different navigation destinations into different panes from an arbitrary + * [navigationState]. + * + * @param panes a list of panes that is possible to show in the [MultiPaneDisplay] in all + * possible configurations. The panes should consist of enum class instances, or a sealed class + * hierarchy of kotlin objects. + * @param navigationState the navigation state to be adapted into various panes. + * @param backStackTransform a transform to read the back stack of the navigation state. + * @param destinationTransform a transform of the [navigationState] to its current destination. + * @param paneEntry provides the [Transform]s and content needed to render + * a [Destination] in its pane. + * @param transforms a list of transforms applied to every [Destination] before it is + * rendered in its pane. Order matters; they are applied from last to first. + */ +fun MultiPaneDisplayState( + panes: List, + navigationState: State, + backStackTransform: (NavigationState) -> List, + destinationTransform: (NavigationState) -> Destination, + paneEntry: (Destination) -> PaneEntry, + transforms: List>, +) = transforms.fold( + initial = MultiPaneDisplayState( + panes = panes, + navigationState = navigationState, + backStackTransform = backStackTransform, + destinationTransform = destinationTransform, + panesToDestinationsTransform = { destination -> + paneEntry(destination).paneTransform(destination) + }, + renderTransform = { destination -> + val nav = paneEntry(destination) + with(nav.renderTransform) { + Render( + destination = destination, + original = nav.content, + ) + } + } + ), + operation = MultiPaneDisplayState::plus +) + +/** + * The current destination in a given [paneScope]. + */ +@Composable +internal fun MultiPaneDisplayState< + Pane, + *, + Destination + >.Destination( + paneScope: PaneScope, +) { + val current = remember(paneScope.paneState.currentDestination) { + paneScope.paneState.currentDestination + } ?: return + + paneScope.renderTransform(current) +} + +/** + * THe current pane mapping to use in the [MultiPaneDisplay]. + */ +@Composable +internal fun + MultiPaneDisplayState.panesToDestinations(): Map { + val current by currentDestination + return panesToDestinationsTransform(current) +} + +private operator fun + MultiPaneDisplayState.plus( + transform: Transform, +): MultiPaneDisplayState = + MultiPaneDisplayState( + panes = panes, + navigationState = navigationState, + backStackTransform = backStackTransform, + destinationTransform = when (transform) { + is DestinationTransform -> { destination -> + transform.toDestination( + navigationState = destination, + original = destinationTransform + ) + } + + else -> destinationTransform + }, + panesToDestinationsTransform = when (transform) { + is PaneTransform -> { destination -> + transform.toPanesAndDestinations( + destination = destination, + original = panesToDestinationsTransform, + ) + } + + else -> panesToDestinationsTransform + }, + renderTransform = when (transform) { + is RenderTransform -> { destination -> + with(transform) { + Render( + destination = destination, + original = renderTransform, + ) + } + } + + else -> renderTransform + }, + ) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneEntry.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneEntry.kt new file mode 100644 index 0000000..39c352c --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneEntry.kt @@ -0,0 +1,19 @@ +package com.tunjid.treenav.compose + +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.configurations.RenderTransform + +/** + * Provides the logic used to select, configure and place a navigation [Destination] for each + * pane [Pane] for the current active navigation [Destination]. + */ +@Stable +class PaneEntry( + internal val renderTransform: RenderTransform, + internal val paneTransform: @Composable (Destination) -> Map, + internal val content: @Composable PaneScope.(Destination) -> Unit, +) \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneScope.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneScope.kt index d245ff0..acaf5ef 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneScope.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneScope.kt @@ -91,7 +91,7 @@ internal data class SlotPaneState( ) : PaneState /** - * A spot taken by an [PaneStrategy] that may be moved in from pane to pane. + * A spot taken by an [PaneEntry] that may be moved in from pane to pane. */ @JvmInline internal value class Slot internal constructor(val index: Int) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneStrategy.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneStrategy.kt deleted file mode 100644 index ce23829..0000000 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneStrategy.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tunjid.treenav.compose - -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import com.tunjid.treenav.Node - -/** - * Provides the logic used to select, configure and place a navigation [Destination] for each - * pane [Pane] for the current active navigation [Destination]. - */ -@Stable -class PaneStrategy internal constructor( - val transitions: PaneScope.() -> PaneScope.Transitions, - /** - * Defines what route to show in the secondary panel alongside this route - */ - val paneMapper: @Composable (Destination) -> Map, - val render: @Composable PaneScope.(Destination) -> Unit -) - -/** - * Allows for defining the logic used to select, configure and place a navigation - * [Destination] for each pane [Pane] for the current active navigation [Destination]. - * - * @param transitions the transitions to run within each [PaneScope]. - * @param paneMapping provides the mapping of panes to destinations for a given destination [Destination]. - * @param render defines the Composable rendered for each destination - * in a given [PaneScope]. - */ -fun paneStrategy( - transitions: PaneScope.() -> PaneScope.Transitions = { NoTransition }, - paneMapping: @Composable (Destination) -> Map = { emptyMap() }, - render: @Composable PaneScope.(Destination) -> Unit -) = PaneStrategy( - paneMapper = paneMapping, - transitions = transitions, - render = render -) - -private val NoTransition = PaneScope.Transitions( - enter = EnterTransition.None, - exit = ExitTransition.None, -) - -/** - * Creates a new [PaneStrategy] from this by conditionally overriding parts of it. - * - * @see paneStrategy - * - * @param transitions the transitions to run within each [PaneScope]. - * @param paneMapping provides the mapping of panes to destinations for a given - * navigation [Destination]. - * @param render defines the Composable rendered for each destination - * in a given [PaneScope]. - */ -fun PaneStrategy.delegated( - transitions: PaneScope.() -> PaneScope.Transitions = this.transitions, - paneMapping: @Composable (Destination) -> Map = this.paneMapper, - render: @Composable PaneScope.(Destination) -> Unit = this.render -) = paneStrategy( - transitions = transitions, - paneMapping = paneMapping, - render = render -) \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavHost.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavHost.kt deleted file mode 100644 index bb89cd8..0000000 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavHost.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tunjid.treenav.Node - -/** - * Creates a navigation host for destinations [Destination] that can show up - * in arbitrary panes [Pane]. - * - * @param state the [PanedNavHostState] producing the [PanedNavHostScope] that provides - * context about the panes in [PanedNavHost]. - * @param modifier The modifier to be applied to the layout. - * @param content [PanedNavHostScope] receiving lambda allowing for placing each pane in its - * appropriate slot. - * - */ -@Composable -fun PanedNavHost( - state: PanedNavHostState, - modifier: Modifier = Modifier, - content: @Composable PanedNavHostScope.() -> Unit -) { - Box( - modifier = modifier - ) { - state.scope().content() - } -} \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavHostConfiguration.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavHostConfiguration.kt deleted file mode 100644 index 03b13c5..0000000 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavHostConfiguration.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import com.tunjid.treenav.Node - -/** - * Class for configuring a [PanedNavHost] for selecting, adapting and placing navigation - * destinations into different panes from an arbitrary [navigationState]. - * - * @param navigationState the navigation state to be adapted into various panes. - * @param destinationTransform a transform of the [navigationState] to its current destination. - * @param strategyTransform provides the strategy used to adapt the current destination to the - * panes available. - */ -@Stable -class PanedNavHostConfiguration internal constructor( - val navigationState: State, - val destinationTransform: (NavigationState) -> Destination, - val strategyTransform: (destination: Destination) -> PaneStrategy -) { - internal val currentDestination: State = derivedStateOf { - destinationTransform(navigationState.value) - } -} - -/** - * Provides an [PanedNavHostConfiguration] for configuring an [PanedNavHost] for - * showing different navigation destinations into different panes from an arbitrary - * [navigationState]. - * - * @param navigationState the navigation state to with destinations [Destination] various - * panes [Pane]. - * @param destinationTransform a transform of the [navigationState] to its current destination. - * It is read inside a [derivedStateOf] block, so reads of snapshot - * state objects will be observed. - * @param strategyTransform provides the strategy used to adapt the current destination to the - * panes available. - */ -fun panedNavHostConfiguration( - navigationState: State, - destinationTransform: (NavigationState) -> Destination, - strategyTransform: (destination: Destination) -> PaneStrategy -) = PanedNavHostConfiguration( - navigationState = navigationState, - destinationTransform = destinationTransform, - strategyTransform = strategyTransform, -) - -/** - * Creates a new [PanedNavHost] by delegating to [this] and rendering destinations into different panes. - * - * @param destinationTransform a transform of [PanedNavHostConfiguration.navigationState] - * to its current destination. It is read inside a [derivedStateOf] block, so reads of snapshot - * state objects will be observed. - * @param strategyTransform provides the strategy used to adapt the current destination to the - * panes available. - */ -fun PanedNavHostConfiguration< - Pane, - NavigationState, - Destination - >.delegated( - destinationTransform: (NavigationState) -> Destination = this@delegated.destinationTransform, - strategyTransform: (destination: Destination) -> PaneStrategy -) = panedNavHostConfiguration( - navigationState = this@delegated.navigationState, - destinationTransform = destinationTransform, - strategyTransform = strategyTransform, -) - -/** - * The current destination in a given [paneScope]. - */ -@Composable -internal fun PanedNavHostConfiguration< - Pane, - *, - Destination - >.Destination( - paneScope: PaneScope -) { - val current = remember(paneScope.paneState.currentDestination) { - paneScope.paneState.currentDestination - } ?: return - with(strategyTransform(current)) { - val enterAndExit = transitions(paneScope) - with(paneScope) { - Box( - modifier = Modifier.animateEnterExit( - enter = enterAndExit.enter, - exit = enterAndExit.exit - ) - ) { - render(current) - } - } - } -} - -/** - * THe current pane mapping to use in the [PanedNavHost]. - */ -@Composable -internal fun PanedNavHostConfiguration< - Pane, - *, - Destination - >.paneMapping(): Map { - val current by currentDestination - return current.let { - strategyTransform(it).paneMapper(it) - } -} \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavigationState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavigationState.kt index c8bd66d..a41bef7 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavigationState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PanedNavigationState.kt @@ -1,29 +1,5 @@ package com.tunjid.treenav.compose -import com.tunjid.treenav.Node - -/** - * State providing details about data in each pane [Pane] it hosts. - */ -interface PanedNavigationState { - - /** - * The current [Destination] in this [pane]. - * @param pane the [Pane] to query. - */ - fun destinationFor( - pane: Pane, - ): Destination? - - /** - * Adaptations involving this [pane] after the last navigation state change. - * @param pane the affected [Pane]. - */ - fun adaptationsIn( - pane: Pane, - ): Set -} - /** * A description of the process that the layout undertook to adapt to the present * pane in its new configuration. diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt index 3b00f8a..1c039d6 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt @@ -18,10 +18,7 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.SaveableStateHolder -import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.setValue -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.currentStateAsState @@ -32,299 +29,207 @@ import com.tunjid.treenav.compose.lifecycle.DestinationViewModelStoreCreator import com.tunjid.treenav.compose.lifecycle.rememberDestinationLifecycleOwner import com.tunjid.treenav.traverse - -/** - * A host for adaptive navigation for panes [Pane] and destinations [Destination]. - */ @Stable -interface PanedNavHostState { +internal class SlottedMultiPaneDisplayScope( + panes: List, + initialPanesToNodes: Map, + saveableStateHolder: SaveableStateHolder, + val displayState: MultiPaneDisplayState, +) : MultiPaneDisplayScope, SaveableStateHolder by saveableStateHolder { + + private val slots = List( + size = panes.size, + init = ::Slot + ).toSet() + + private var panedNavigationState by mutableStateOf( + value = SlotBasedPanedNavigationState.initial(slots = slots) + .adaptTo( + slots = slots, + panesToNodes = initialPanesToNodes, + backStackIds = displayState.navigationState.value.backStackIds(), + ) + ) + + private val destinationViewModelStoreCreator = DestinationViewModelStoreCreator( + validNodeIdsReader = { panedNavigationState.backStackIds + panedNavigationState.destinationIdsAnimatingOut } + ) + + private val slotsToRoutes = + mutableStateMapOf Unit>().also { map -> + map[null] = {} + slots.forEach { slot -> + map[slot] = movableContentOf { Render(slot) } + } + } /** - * Creates the scope that provides context about individual panes [Pane] in a [PanedNavHost]. + * Retrieves the a [ViewModelStoreOwner] for a given [destination]. All destinations + * with the same [Node.id] share the same [ViewModelStoreOwner]. + * + * The [destination] must be present in the navigation tree, otherwise an + * [IllegalStateException] will be thrown. + * + * @param destination The destination for which the [ViewModelStoreOwner] should + * be retrieved. */ - @Composable - fun scope(): PanedNavHostScope -} - -/** - * Scope that provides context about individual panes [Pane] in an [PanedNavHost]. - */ -@Stable -interface PanedNavHostScope { + fun viewModelStoreOwnerFor(destination: Destination): ViewModelStoreOwner = + destinationViewModelStoreCreator.viewModelStoreOwnerFor(destination) @Composable - fun Destination( - pane: Pane - ) + override fun Destination(pane: Pane) { + val slot = panedNavigationState.slotFor(pane) + slotsToRoutes[slot]?.invoke() + } - fun adaptationsIn( + override fun adaptationsIn( pane: Pane, - ): Set + ): Set = panedNavigationState.adaptationsIn(pane) - fun nodeFor( + override fun nodeFor( pane: Pane, - ): Destination? -} - -/** - * An implementation of an [PanedNavHostState] that provides the following for each - * navigation [Destination] that shows up in its panes: - * - * - A single [SaveableStateHolder] for each navigation [Destination] that shows up in its panes. - * [SaveableStateHolder.SaveableStateProvider] is keyed on the [Destination]s [Node.id]. - * - * - A [ViewModelStoreOwner] for each [Destination] via [LocalViewModelStoreOwner]. - * Once present in the navigation tree, a [Destination] will always use the same - * [ViewModelStoreOwner], regardless of where in the tree it is, until its is removed from the tree. - * [Destination]s are unique based on their [Node.id]. - * - * - A [LifecycleOwner] for each [Destination] via [LocalLifecycleOwner]. This [LifecycleOwner] - * follows the [Lifecycle] of its immediate parent, unless it is animating out or placed in the - * backstack. This is defined by [PaneScope.isActive], which is a function of the backing - * [AnimatedContent] for each [Pane] displayed and if the current [Destination] - * matches [PanedNavHostScope.nodeFor] in the visible [Pane]. - * - * @param panes a list of panes that is possible to show in the [PanedNavHost] in all - * possible configurations. The panes should consist of enum class instances, or a sealed class - * hierarchy of kotlin objects. - * @param configuration the [PanedNavHostConfiguration] that applies adaptive semantics and - * strategies for each navigation destination shown in the [PanedNavHost]. - */ -@Stable -class SavedStatePanedNavHostState( - private val panes: List, - private val configuration: PanedNavHostConfiguration, -) : PanedNavHostState { - - @Composable - override fun scope(): PanedNavHostScope { - val navigationState by configuration.navigationState - val panesToNodes = configuration.paneMapping() - val saveableStateHolder = rememberPanedSaveableStateHolder() - - val panedContentScope = remember { - NavHostScope( - panes = panes, - navHostConfiguration = configuration, - initialPanesToNodes = panesToNodes, - saveableStateHolder = saveableStateHolder, - ) - } - - LaunchedEffect(navigationState, panesToNodes) { - panedContentScope.onNewNavigationState( - navigationState = navigationState, - panesToNodes = panesToNodes + ): Destination? = panedNavigationState.destinationFor(pane) + + internal fun onNewNavigationState( + navigationState: Node, + panesToNodes: Map, + ) { + updateAdaptiveNavigationState { + adaptTo( + slots = slots.toSet(), + panesToNodes = panesToNodes, + backStackIds = navigationState.backStackIds() ) } - - return panedContentScope } - companion object { - @Stable - class NavHostScope internal constructor( - panes: List, - initialPanesToNodes: Map, - saveableStateHolder: SaveableStateHolder, - val navHostConfiguration: PanedNavHostConfiguration, - ) : PanedNavHostScope, SaveableStateHolder by saveableStateHolder { - - private val slots = List( - size = panes.size, - init = ::Slot - ).toSet() - - private var panedNavigationState by mutableStateOf( - value = SlotBasedPanedNavigationState.initial(slots = slots) - .adaptTo( - slots = slots, - panesToNodes = initialPanesToNodes, - backStackIds = navHostConfiguration.navigationState.value.backStackIds(), - ) - ) - - private val destinationViewModelStoreCreator = DestinationViewModelStoreCreator( - validNodeIdsReader = { panedNavigationState.backStackIds + panedNavigationState.destinationIdsAnimatingOut } - ) - - private val slotsToRoutes = - mutableStateMapOf Unit>().also { map -> - map[null] = {} - slots.forEach { slot -> - map[slot] = movableContentOf { Render(slot) } - } - } - - /** - * Retrieves the a [ViewModelStoreOwner] for a given [destination]. All destinations - * with the same [Node.id] share the same [ViewModelStoreOwner]. - * - * The [destination] must be present in the navigation tree, otherwise an - * [IllegalStateException] will be thrown. - * - * @param destination The destination for which the [ViewModelStoreOwner] should - * be retrieved. - */ - fun viewModelStoreOwnerFor(destination: Destination): ViewModelStoreOwner = - destinationViewModelStoreCreator.viewModelStoreOwnerFor(destination) - - @Composable - override fun Destination(pane: Pane) { - val slot = panedNavigationState.slotFor(pane) - slotsToRoutes[slot]?.invoke() + /** + * Renders [slot] into its pane with scopes that allow for animations + * and shared elements. + */ + @Composable + private fun Render( + slot: Slot, + ) { + val paneTransition = updateTransition( + targetState = panedNavigationState.paneStateFor(slot), + label = "$slot-PaneTransition", + ) + paneTransition.AnimatedContent( + contentKey = { it.currentDestination?.id }, + transitionSpec = { + ContentTransform( + targetContentEnter = EnterTransition.None, + initialContentExit = ExitTransition.None, + sizeTransform = null, + ) } - - override fun adaptationsIn( - pane: Pane - ): Set = panedNavigationState.adaptationsIn(pane) - - override fun nodeFor( - pane: Pane - ): Destination? = panedNavigationState.destinationFor(pane) - - internal fun onNewNavigationState( - navigationState: Node, - panesToNodes: Map, - ) { - updateAdaptiveNavigationState { - adaptTo( - slots = slots.toSet(), - panesToNodes = panesToNodes, - backStackIds = navigationState.backStackIds() - ) - } + ) { targetPaneState -> + val scope = remember { + AnimatedPaneScope( + paneState = targetPaneState, + activeState = derivedStateOf { + val activePaneState = panedNavigationState.paneStateFor(slot) + activePaneState.currentDestination?.id == targetPaneState.currentDestination?.id + }, + animatedContentScope = this@AnimatedContent, + ) } - /** - * Renders [slot] into its pane with scopes that allow for animations - * and shared elements. - */ - @Composable - private fun Render( - slot: Slot, - ) { - val paneTransition = updateTransition( - targetState = panedNavigationState.paneStateFor(slot), - label = "$slot-PaneTransition", - ) - paneTransition.AnimatedContent( - contentKey = { it.currentDestination?.id }, - transitionSpec = { - ContentTransform( - targetContentEnter = EnterTransition.None, - initialContentExit = ExitTransition.None, - sizeTransform = null, - ) - } - ) { targetPaneState -> - val scope = remember { - AnimatedPaneScope( - paneState = targetPaneState, - activeState = derivedStateOf { - val activePaneState = panedNavigationState.paneStateFor(slot) - activePaneState.currentDestination?.id == targetPaneState.currentDestination?.id - }, - animatedContentScope = this@AnimatedContent, - ) - } + // While technically a backwards write, it stabilizes and ensures the values are + // correct at first composition + scope.paneState = targetPaneState - // While technically a backwards write, it stabilizes and ensures the values are - // correct at first composition - scope.paneState = targetPaneState + val destination = targetPaneState.currentDestination + if (destination != null) { + val destinationLifecycleOwner = rememberDestinationLifecycleOwner( + destination + ) + val destinationViewModelOwner = remember(destination.id) { + destinationViewModelStoreCreator + .viewModelStoreOwnerFor(destination) + } - val destination = targetPaneState.currentDestination - if (destination != null) { - val destinationLifecycleOwner = rememberDestinationLifecycleOwner( - destination - ) - val destinationViewModelOwner = remember(destination.id) { - destinationViewModelStoreCreator - .viewModelStoreOwnerFor(destination) + CompositionLocalProvider( + LocalLifecycleOwner provides destinationLifecycleOwner, + LocalViewModelStoreOwner provides destinationViewModelOwner, + ) { + SaveableStateProvider(destination.id) { + displayState.Destination(paneScope = scope) + + DisposableEffect(Unit) { + onDispose { + val backstackIds = panedNavigationState.backStackIds + if (!backstackIds.contains(destination.id)) removeState( + destination.id + ) + } } - CompositionLocalProvider( - LocalLifecycleOwner provides destinationLifecycleOwner, - LocalViewModelStoreOwner provides destinationViewModelOwner, + val hostLifecycleState by destinationLifecycleOwner.hostLifecycleState.currentStateAsState() + DisposableEffect( + hostLifecycleState, + scope.isActive, + panedNavigationState, ) { - SaveableStateProvider(destination.id) { - navHostConfiguration.Destination(paneScope = scope) - - DisposableEffect(Unit) { - onDispose { - val backstackIds = panedNavigationState.backStackIds - if (!backstackIds.contains(destination.id)) removeState( - destination.id - ) - } - } - - val hostLifecycleState by destinationLifecycleOwner.hostLifecycleState.currentStateAsState() - DisposableEffect( - hostLifecycleState, - scope.isActive, - panedNavigationState, - ) { - destinationLifecycleOwner.update( - hostLifecycleState = hostLifecycleState, - paneScope = scope, - panedNavigationState = panedNavigationState - ) - onDispose { - destinationLifecycleOwner.update( - hostLifecycleState = hostLifecycleState, - paneScope = scope, - panedNavigationState = panedNavigationState - ) - } - } + destinationLifecycleOwner.update( + hostLifecycleState = hostLifecycleState, + paneScope = scope, + panedNavigationState = panedNavigationState + ) + onDispose { + destinationLifecycleOwner.update( + hostLifecycleState = hostLifecycleState, + paneScope = scope, + panedNavigationState = panedNavigationState + ) } } } + } + } - // Add destination ids that are animating out - LaunchedEffect(transition.isRunning) { - if (transition.targetState == EnterExitState.PostExit) { - val destinationId = targetPaneState.currentDestination?.id - ?: return@LaunchedEffect - updateAdaptiveNavigationState { - copy(destinationIdsAnimatingOut = destinationIdsAnimatingOut + destinationId) - } - } - } - // Remove route ids that have animated out - DisposableEffect(Unit) { - onDispose { - val routeId = targetPaneState.currentDestination?.id ?: return@onDispose - updateAdaptiveNavigationState { - copy(destinationIdsAnimatingOut = destinationIdsAnimatingOut - routeId).prune() - } - targetPaneState.currentDestination?.let(destinationViewModelStoreCreator::clearStoreFor) - } + // Add destination ids that are animating out + LaunchedEffect(transition.isRunning) { + if (transition.targetState == EnterExitState.PostExit) { + val destinationId = targetPaneState.currentDestination?.id + ?: return@LaunchedEffect + updateAdaptiveNavigationState { + copy(destinationIdsAnimatingOut = destinationIdsAnimatingOut + destinationId) } } } - - private inline fun updateAdaptiveNavigationState( - block: SlotBasedPanedNavigationState.() -> SlotBasedPanedNavigationState - ) { - panedNavigationState = panedNavigationState.block() + // Remove route ids that have animated out + DisposableEffect(Unit) { + onDispose { + val routeId = targetPaneState.currentDestination?.id ?: return@onDispose + updateAdaptiveNavigationState { + copy(destinationIdsAnimatingOut = destinationIdsAnimatingOut - routeId).prune() + } + targetPaneState.currentDestination?.let(destinationViewModelStoreCreator::clearStoreFor) + } } } + } - private fun Node.backStackIds() = - mutableSetOf().apply { - traverse(Order.DepthFirst) { add(it.id) } - } + private inline fun updateAdaptiveNavigationState( + block: SlotBasedPanedNavigationState.() -> SlotBasedPanedNavigationState, + ) { + panedNavigationState = panedNavigationState.block() } } -fun PanedNavHostScope< - Pane, - Destination - >.requireSavedStatePanedNavHostScope(): SavedStatePanedNavHostState.Companion.NavHostScope { - check(this is SavedStatePanedNavHostState.Companion.NavHostScope) { - "This PanedNavHostScope instance is not a SavedStatePanedNavHostScope" +private fun Node.backStackIds() = + mutableSetOf().apply { + traverse(Order.DepthFirst) { add(it.id) } } - return this -} + +//fun PanedNavHostScope< +// Pane, +// Destination +// >.requireSavedStatePanedNavHostScope(): SavedStatePanedNavHostState.Companion.NavHostScope { +// check(this is SavedStatePanedNavHostState.Companion.NavHostScope) { +// "This PanedNavHostScope instance is not a SavedStatePanedNavHostScope" +// } +// return this +//} diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlotBasedPanedNavigationState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlotBasedPanedNavigationState.kt index b2a548a..471ed00 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlotBasedPanedNavigationState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlotBasedPanedNavigationState.kt @@ -49,7 +49,7 @@ internal data class SlotBasedPanedNavigationState( * A set of node ids that are animating out. */ val destinationIdsAnimatingOut: Set, -) : PanedNavigationState { +) { companion object { internal fun initial( slots: Collection, @@ -66,7 +66,7 @@ internal data class SlotBasedPanedNavigationState( } internal fun paneStateFor( - slot: Slot + slot: Slot, ): PaneState { val node = destinationFor(slot) val pane = node?.let(::paneFor) @@ -80,19 +80,19 @@ internal data class SlotBasedPanedNavigationState( } internal fun slotFor( - pane: Pane + pane: Pane, ): Slot? = destinationIdsToAdaptiveSlots[ panesToDestinations[pane]?.id ] private fun paneFor( - node: Node + node: Node, ): Pane? = panesToDestinations.firstNotNullOfOrNull { (pane, paneRoute) -> if (paneRoute?.id == node.id) pane else null } private fun destinationFor( - slot: Slot + slot: Slot, ): Destination? = destinationIdsToAdaptiveSlots.firstNotNullOfOrNull { (nodeId, nodeSlot) -> if (nodeSlot == slot) panesToDestinations.firstNotNullOfOrNull { (_, node) -> if (node?.id == nodeId) node @@ -101,12 +101,12 @@ internal data class SlotBasedPanedNavigationState( else null } - override fun destinationFor( - pane: Pane + fun destinationFor( + pane: Pane, ): Destination? = panesToDestinations[pane] - override fun adaptationsIn( - pane: Pane + fun adaptationsIn( + pane: Pane, ): Set { val swaps = swapAdaptations.filter { pane in it } return if (swaps.isEmpty()) when (panesToDestinations[pane]?.id) { @@ -206,7 +206,7 @@ internal fun SlotBasedPanedNavigationState SlotBasedPanedNavigationState.prune(): SlotBasedPanedNavigationState = copy( diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/AnimatePaneBoundsConfiguration.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/AnimatePaneBoundsTransform.kt similarity index 62% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/AnimatePaneBoundsConfiguration.kt rename to library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/AnimatePaneBoundsTransform.kt index 29fdc28..e761ff5 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/AnimatePaneBoundsConfiguration.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/AnimatePaneBoundsTransform.kt @@ -23,15 +23,14 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.LookaheadScope import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.MultiPaneDisplayState import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.PanedNavHostConfiguration -import com.tunjid.treenav.compose.delegated import com.tunjid.treenav.compose.utilities.AnimatedBoundsState import com.tunjid.treenav.compose.utilities.AnimatedBoundsState.Companion.animateBounds import com.tunjid.treenav.compose.utilities.DefaultBoundsTransform /** - * A [PanedNavHostConfiguration] that animates the bounds of each [Pane] displayed within it. + * A [MultiPaneDisplayState] that animates the bounds of each [Pane] displayed within it. * This is useful for scenarios where the panes move within a layout hierarchy to accommodate * other panes. * @@ -41,32 +40,23 @@ import com.tunjid.treenav.compose.utilities.DefaultBoundsTransform * skipping an animation in progress. */ @OptIn(ExperimentalSharedTransitionApi::class) -fun PanedNavHostConfiguration< - Pane, - NavigationState, - Destination - >.animatePaneBoundsConfiguration( +fun animatePaneBoundsTransform( lookaheadScope: LookaheadScope, paneBoundsTransform: PaneScope.() -> BoundsTransform = { DefaultBoundsTransform }, shouldAnimatePane: PaneScope.() -> Boolean = { true }, -): PanedNavHostConfiguration = - delegated { navigationDestination -> - val originalStrategy = strategyTransform(navigationDestination) - originalStrategy.delegated( - render = render@{ paneDestination -> - Box( - modifier = Modifier.animateBounds( - state = remember { - AnimatedBoundsState( - lookaheadScope = lookaheadScope, - boundsTransform = paneBoundsTransform(), - inProgress = { shouldAnimatePane() } - ) - } +): Transform = + RenderTransform { destination, original -> + Box( + modifier = Modifier.animateBounds( + state = remember { + AnimatedBoundsState( + lookaheadScope = lookaheadScope, + boundsTransform = paneBoundsTransform(), + inProgress = { shouldAnimatePane() } ) - ) { - originalStrategy.render(this@render, paneDestination) } - } - ) + ) + ) { + original(destination) + } } \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/PaneModifierConfiguration.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/PaneModifierTransform.kt similarity index 55% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/PaneModifierConfiguration.kt rename to library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/PaneModifierTransform.kt index 31d328d..3e27305 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/PaneModifierConfiguration.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/PaneModifierTransform.kt @@ -19,32 +19,22 @@ package com.tunjid.treenav.compose.configurations import androidx.compose.foundation.layout.Box import androidx.compose.ui.Modifier import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.MultiPaneDisplayState import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.PanedNavHostConfiguration -import com.tunjid.treenav.compose.delegated /** - * A [PanedNavHostConfiguration] that allows for centrally defining the [Modifier] for + * A [MultiPaneDisplayState] that allows for centrally defining the [Modifier] for * each [Pane] displayed within it. * * @param paneModifier a lambda for specifying the [Modifier] for each [Pane] in a [PaneScope]. */ -fun PanedNavHostConfiguration< - Pane, - NavigationState, - Destination - >.paneModifierConfiguration( +fun paneModifierTransform( paneModifier: PaneScope.() -> Modifier = { Modifier }, -): PanedNavHostConfiguration = - delegated { navigationDestination -> - val originalStrategy = strategyTransform(navigationDestination) - originalStrategy.delegated( - render = render@{ paneDestination -> - Box( - modifier = paneModifier() - ) { - originalStrategy.render(this@render, paneDestination) - } - } - ) +): Transform = + RenderTransform { destination, original -> + Box( + modifier = paneModifier() + ) { + original(destination) + } } \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/Transforms.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/Transforms.kt new file mode 100644 index 0000000..055d17b --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/Transforms.kt @@ -0,0 +1,35 @@ +package com.tunjid.treenav.compose.configurations + +import androidx.compose.runtime.Composable +import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.PaneScope + + +interface Transform + +fun interface DestinationTransform + : Transform { + fun toDestination( + navigationState: NavigationState, + original: (NavigationState) -> Destination, + ): Destination +} + +fun interface PaneTransform + : Transform { + @Composable + fun toPanesAndDestinations( + destination: Destination, + original: @Composable (Destination) -> Map, + ): Map +} + +fun interface RenderTransform + : Transform { + @Composable + fun PaneScope.Render( + destination: Destination, + original: @Composable PaneScope.(Destination) -> Unit, + ) +} + 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 cae07b2..9ce21a6 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 @@ -21,7 +21,7 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import com.tunjid.treenav.Node import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.PanedNavHost +import com.tunjid.treenav.compose.MultiPaneDisplay import com.tunjid.treenav.compose.utilities.DefaultBoundsTransform /** @@ -129,7 +129,7 @@ fun MovableSharedElementScope.updatedMovableSharedElementOf( ) /** - * State for managing movable shared elements within a single [PanedNavHost]. + * State for managing movable shared elements within a single [MultiPaneDisplay]. */ @OptIn(ExperimentalSharedTransitionApi::class) @Stable diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt index 8b28abb..e8894a2 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt @@ -22,15 +22,16 @@ import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.tunjid.treenav.Node import com.tunjid.treenav.compose.Adaptation.Swap +import com.tunjid.treenav.compose.PaneEntry import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.PaneStrategy -import com.tunjid.treenav.compose.paneStrategy /** - * A [PaneStrategy] for apps that display up to 3 major panes as once. + * A [PaneEntry] for apps that display up to 3 major panes as once. * It also provides extra panes for transient content. */ enum class ThreePane { @@ -101,40 +102,50 @@ fun threePaneListDetailStrategy( paneMapping: @Composable (R) -> Map = { mapOf(ThreePane.Primary to it) }, - render: @Composable PaneScope.(R) -> Unit -): PaneStrategy = paneStrategy( - paneMapping = paneMapping, - transitions = { + render: @Composable PaneScope.(R) -> Unit, +) = PaneEntry( + paneTransform = paneMapping, + renderTransform = { destination, original -> val state = paneState - when (state.pane) { + val modifier = when (state.pane) { ThreePane.Primary, - ThreePane.Secondary -> when { + ThreePane.Secondary, + -> when { ThreePane.PrimaryToSecondary in state.adaptations || ThreePane.SecondaryToPrimary in state.adaptations - -> NoTransition + -> Modifier - else -> PaneScope.Transitions( - enter = enterTransition(), - exit = exitTransition() - ) + else -> Modifier + .animateEnterExit( + enter = enterTransition(), + exit = exitTransition() + ) } ThreePane.TransientPrimary -> when { - ThreePane.PrimaryToTransient in state.adaptations -> NoTransition + ThreePane.PrimaryToTransient in state.adaptations -> Modifier + + else -> Modifier + .animateEnterExit( + enter = enterTransition(), + exit = exitTransition() + ) + } - else -> PaneScope.Transitions( + else -> Modifier + .animateEnterExit( enter = enterTransition(), exit = exitTransition() ) - } - - else -> PaneScope.Transitions( - enter = enterTransition(), - exit = exitTransition() - ) } + Box( + modifier = modifier + ) { + original(destination) + } + }, - render = render + content = render ) private val RouteTransitionAnimationSpec: FiniteAnimationSpec = tween( diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/PredictiveBackConfiguration.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/PredictiveBackConfiguration.kt deleted file mode 100644 index 6ae19ed..0000000 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/PredictiveBackConfiguration.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.configurations - -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import com.tunjid.treenav.Node -import com.tunjid.treenav.compose.PanedNavHostConfiguration -import com.tunjid.treenav.compose.delegated -import com.tunjid.treenav.compose.threepane.ThreePane - -/** - * An [PanedNavHostConfiguration] that moves the destination in a [ThreePane.Primary] pane, to - * to the [ThreePane.TransientPrimary] pane when a predictive back gesture is in progress. - * - * @param isPreviewingBack provides the state of the predictive back gesture. - * True if the gesture is ongoing. - * @param backPreviewTransform provides the [NavigationState] if the predictive back gesture - * were to be completed. - */ -inline fun PanedNavHostConfiguration< - ThreePane, - NavigationState, - Destination - >.predictiveBackConfiguration( - isPreviewingBack: State, - crossinline backPreviewTransform: NavigationState.() -> NavigationState, -): PanedNavHostConfiguration { - var lastPrimaryDestination by mutableStateOf(null) - return delegated( - destinationTransform = { navigationState -> - val current = destinationTransform(navigationState) - lastPrimaryDestination = current - if (isPreviewingBack.value) destinationTransform(navigationState.backPreviewTransform()) - else current - }, - strategyTransform = { navigationDestination -> - val originalStrategy = strategyTransform(navigationDestination) - originalStrategy.delegated( - paneMapping = paneMapper@{ navigationDestinationToMap -> - val originalMapping = originalStrategy.paneMapper(navigationDestinationToMap) - val isPreviewing by isPreviewingBack - if (!isPreviewing) return@paneMapper originalMapping - // Back is being previewed, therefore the original mapping is already for back. - // Pass the previous primary value into transient. - val transientDestination = checkNotNull(lastPrimaryDestination) { - "Attempted to show last destination without calling destination transform" - } - val paneMapping = strategyTransform(transientDestination) - .paneMapper(transientDestination) - val transient = paneMapping[ThreePane.Primary] - originalMapping + (ThreePane.TransientPrimary to transient) - } - ) - } - ) -} diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/ThreePaneAdaptiveConfiguration.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/ThreePaneAdaptiveConfiguration.kt deleted file mode 100644 index b6087cd..0000000 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/ThreePaneAdaptiveConfiguration.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tunjid.treenav.compose.threepane.configurations - -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tunjid.treenav.Node -import com.tunjid.treenav.compose.PanedNavHostConfiguration -import com.tunjid.treenav.compose.delegated -import com.tunjid.treenav.compose.threepane.ThreePane - -/** - * An [PanedNavHostConfiguration] that selectively displays panes for a [ThreePane] layout - * based on the space available determined by the [windowWidthState]. - * - * @param windowWidthState provides the current width of the display in Dp. - */ -fun PanedNavHostConfiguration< - ThreePane, - NavigationState, - Destination - >.threePanedNavHostConfiguration( - windowWidthState: State, - secondaryPaneBreakPoint: State = mutableStateOf(SECONDARY_PANE_MIN_WIDTH_BREAKPOINT_DP), - tertiaryPaneBreakPoint: State = mutableStateOf(TERTIARY_PANE_MIN_WIDTH_BREAKPOINT_DP), -): PanedNavHostConfiguration = delegated { destination -> - val originalStrategy = strategyTransform(destination) - originalStrategy.delegated( - paneMapping = { navigationDestinationToMap -> - // Consider navigation state different if window size class changes - val windowWidthDp by windowWidthState - val originalMapping = originalStrategy.paneMapper(navigationDestinationToMap) - val primaryNode = originalMapping[ThreePane.Primary] - mapOf( - ThreePane.Primary to primaryNode, - ThreePane.Secondary to originalMapping[ThreePane.Secondary].takeIf { secondaryDestination -> - secondaryDestination?.id != primaryNode?.id - && windowWidthDp >= secondaryPaneBreakPoint.value - }, - ThreePane.Tertiary to originalMapping[ThreePane.Tertiary].takeIf { tertiaryDestination -> - tertiaryDestination?.id != primaryNode?.id - && windowWidthDp >= tertiaryPaneBreakPoint.value - }, - ) - } - ) -} - -private val SECONDARY_PANE_MIN_WIDTH_BREAKPOINT_DP = 600.dp -private val TERTIARY_PANE_MIN_WIDTH_BREAKPOINT_DP = 1200.dp \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt similarity index 74% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt rename to library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt index 9ba39cf..86013d8 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/MovableSharedElementConfiguration.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/MovableSharedElementTransform.kt @@ -1,4 +1,4 @@ -package com.tunjid.treenav.compose.threepane.configurations +package com.tunjid.treenav.compose.threepane.transforms import androidx.compose.animation.BoundsTransform import androidx.compose.animation.ExperimentalSharedTransitionApi @@ -11,51 +11,43 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.MultiPaneDisplay import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.PanedNavHost -import com.tunjid.treenav.compose.PanedNavHostConfiguration -import com.tunjid.treenav.compose.delegated -import com.tunjid.treenav.compose.moveablesharedelement.PanedMovableSharedElementScope +import com.tunjid.treenav.compose.configurations.RenderTransform +import com.tunjid.treenav.compose.configurations.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.threepane.ThreePane /** - * An [PanedNavHostConfiguration] that applies semantics of movable shared elements to + * A [Transform] that applies semantics of movable shared elements to * [ThreePane] layouts. * * @param movableSharedElementHostState the host state for coordinating movable shared elements. - * There should be one instance of this per [PanedNavHost]. + * There should be one instance of this per [MultiPaneDisplay]. */ -fun PanedNavHostConfiguration< - ThreePane, - NavigationState, - Destination - >.threePanedMovableSharedElementConfiguration( +fun + threePanedMovableSharedElementTransform( movableSharedElementHostState: MovableSharedElementHostState, -): PanedNavHostConfiguration = - delegated { navigationDestination -> - val originalStrategy = strategyTransform(navigationDestination) - originalStrategy.delegated( - render = { paneDestination -> - val delegate = remember { - PanedMovableSharedElementScope( - paneScope = this, - movableSharedElementHostState = movableSharedElementHostState, - ) - } - delegate.paneScope = this +): Transform = + RenderTransform { navigationDestination, original -> + val delegate = remember { + PanedMovableSharedElementScope( + paneScope = this, + movableSharedElementHostState = movableSharedElementHostState, + ) + } + delegate.paneScope = this - val movableSharedElementScope = remember { - ThreePaneMovableSharedElementScope( - hostState = movableSharedElementHostState, - delegate = delegate, - ) - } + val movableSharedElementScope = remember { + ThreePaneMovableSharedElementScope( + hostState = movableSharedElementHostState, + delegate = delegate, + ) + } - originalStrategy.render(movableSharedElementScope, paneDestination) - }, - ) + original(movableSharedElementScope, navigationDestination) } fun PaneScope< @@ -89,7 +81,7 @@ private class ThreePaneMovableSharedElementScope( zIndexInOverlay: Float, clipInOverlayDuringTransition: OverlayClip, alternateOutgoingSharedElement: (@Composable (T, Modifier) -> Unit)?, - sharedElement: @Composable (T, Modifier) -> Unit + sharedElement: @Composable (T, Modifier) -> Unit, ): @Composable (T, Modifier) -> Unit = when (paneState.pane) { null -> throw IllegalArgumentException( "Shared elements may only be used in non null panes" @@ -127,7 +119,8 @@ private class ThreePaneMovableSharedElementScope( // In the other panes use the element as is ThreePane.Secondary, ThreePane.Tertiary, - ThreePane.Overlay -> alternateOutgoingSharedElement ?: sharedElement + ThreePane.Overlay, + -> alternateOutgoingSharedElement ?: sharedElement } } diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt new file mode 100644 index 0000000..6c52a26 --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt @@ -0,0 +1,79 @@ +/* + * 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.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.configurations.DestinationTransform +import com.tunjid.treenav.compose.configurations.PaneTransform +import com.tunjid.treenav.compose.configurations.Transform +import com.tunjid.treenav.compose.threepane.ThreePane + +/** + * An [Transform] that moves the destination in a [ThreePane.Primary] pane, to + * to the [ThreePane.TransientPrimary] pane when a predictive back gesture is in progress. + * + * @param isPreviewingBack provides the state of the predictive back gesture. + * True if the gesture is ongoing. + * @param backPreviewTransform provides the [NavigationState] if the predictive back gesture + * were to be completed. + */ +inline fun + predictiveBackTransform( + isPreviewingBack: State, + crossinline backPreviewTransform: NavigationState.() -> NavigationState, +): Transform { + return object : + DestinationTransform, + PaneTransform { + + var lastPrimaryDestination by mutableStateOf(null) + + override fun toDestination( + navigationState: NavigationState, + original: (NavigationState) -> Destination, + ): Destination { + val current = original(navigationState) + lastPrimaryDestination = current + return if (isPreviewingBack.value) original(navigationState.backPreviewTransform()) + else current + } + + @Composable + override fun toPanesAndDestinations( + destination: Destination, + original: @Composable (Destination) -> Map, + ): Map { + val originalMapping = original(destination) + val isPreviewing by isPreviewingBack + if (!isPreviewing) return originalMapping + // Back is being previewed, therefore the original mapping is already for back. + // Pass the previous primary value into transient. + val transientDestination = checkNotNull(lastPrimaryDestination) { + "Attempted to show last destination without calling destination transform" + } + val paneMapping = original(transientDestination) + val transient = paneMapping[ThreePane.Primary] + return originalMapping + (ThreePane.TransientPrimary to transient) + } + } +} + diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt new file mode 100644 index 0000000..6a7c39e --- /dev/null +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt @@ -0,0 +1,44 @@ +package com.tunjid.treenav.compose.threepane.transforms + +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.configurations.PaneTransform +import com.tunjid.treenav.compose.configurations.Transform +import com.tunjid.treenav.compose.threepane.ThreePane + +/** + * An [Transform] that selectively displays panes for a [ThreePane] layout + * based on the space available determined by the [windowWidthState]. + * + * @param windowWidthState provides the current width of the display in Dp. + */ +fun + threePanedAdaptiveTransform( + windowWidthState: State, + secondaryPaneBreakPoint: State = mutableStateOf(SECONDARY_PANE_MIN_WIDTH_BREAKPOINT_DP), + tertiaryPaneBreakPoint: State = mutableStateOf(TERTIARY_PANE_MIN_WIDTH_BREAKPOINT_DP), +): Transform = + PaneTransform { destination, original -> + // Consider navigation state different if window size class changes + val windowWidthDp by windowWidthState + val originalMapping = original(destination) + val primaryNode = originalMapping[ThreePane.Primary] + mapOf( + ThreePane.Primary to primaryNode, + ThreePane.Secondary to originalMapping[ThreePane.Secondary].takeIf { secondaryDestination -> + secondaryDestination?.id != primaryNode?.id + && windowWidthDp >= secondaryPaneBreakPoint.value + }, + ThreePane.Tertiary to originalMapping[ThreePane.Tertiary].takeIf { tertiaryDestination -> + tertiaryDestination?.id != primaryNode?.id + && windowWidthDp >= tertiaryPaneBreakPoint.value + }, + ) + } + +private val SECONDARY_PANE_MIN_WIDTH_BREAKPOINT_DP = 600.dp +private val TERTIARY_PANE_MIN_WIDTH_BREAKPOINT_DP = 1200.dp \ No newline at end of file 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 a0659aa..a9ad413 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 @@ -48,7 +48,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -75,18 +74,17 @@ import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.me.mePaneStrategy import com.tunjid.demo.common.ui.profile.profilePaneStrategy import com.tunjid.treenav.MultiStackNav -import com.tunjid.treenav.compose.PanedNavHost -import com.tunjid.treenav.compose.PanedNavHostConfiguration -import com.tunjid.treenav.compose.PanedNavHostScope -import com.tunjid.treenav.compose.SavedStatePanedNavHostState -import com.tunjid.treenav.compose.configurations.animatePaneBoundsConfiguration -import com.tunjid.treenav.compose.configurations.paneModifierConfiguration +import com.tunjid.treenav.compose.MultiPaneDisplay +import com.tunjid.treenav.compose.MultiPaneDisplayState +import com.tunjid.treenav.compose.MultiPaneDisplayScope +import com.tunjid.treenav.compose.configurations.Transform +import com.tunjid.treenav.compose.configurations.animatePaneBoundsTransform +import com.tunjid.treenav.compose.configurations.paneModifierTransform import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementHostState -import com.tunjid.treenav.compose.panedNavHostConfiguration import com.tunjid.treenav.compose.threepane.ThreePane -import com.tunjid.treenav.compose.threepane.configurations.predictiveBackConfiguration -import com.tunjid.treenav.compose.threepane.configurations.threePanedMovableSharedElementConfiguration -import com.tunjid.treenav.compose.threepane.configurations.threePanedNavHostConfiguration +import com.tunjid.treenav.compose.threepane.transforms.predictiveBackTransform +import com.tunjid.treenav.compose.threepane.transforms.threePanedAdaptiveTransform +import com.tunjid.treenav.compose.threepane.transforms.threePanedMovableSharedElementTransform import com.tunjid.treenav.current import com.tunjid.treenav.pop import com.tunjid.treenav.popToRoot @@ -128,40 +126,46 @@ fun App( canAnimatePanes = !interactingWithPanes } - PanedNavHost( + MultiPaneDisplay( modifier = Modifier .fillMaxSize(), - state = appState.rememberPanedNavHostState { - this - .threePanedNavHostConfiguration( - windowWidthState = derivedStateOf { - appState.splitLayoutState.size + state = appState.rememberPanedNavHostState( + listOf( + threePanedAdaptiveTransform( + windowWidthState = remember { + derivedStateOf { + appState.splitLayoutState.size + } } - ) - .predictiveBackConfiguration( - isPreviewingBack = derivedStateOf { - appState.isPreviewingBack + ), + predictiveBackTransform( + isPreviewingBack = remember { + derivedStateOf { + appState.isPreviewingBack + } }, backPreviewTransform = MultiStackNav::pop, - ) - .threePanedMovableSharedElementConfiguration( + ), + threePanedMovableSharedElementTransform( movableSharedElementHostState = movableSharedElementHostState - ) - .animatePaneBoundsConfiguration( + ), + animatePaneBoundsTransform( lookaheadScope = this@SharedTransitionScope, shouldAnimatePane = { when (paneState.pane) { ThreePane.Primary, ThreePane.TransientPrimary, ThreePane.Secondary, - ThreePane.Tertiary -> canAnimatePanes + ThreePane.Tertiary, + -> canAnimatePanes null, - ThreePane.Overlay -> false + ThreePane.Overlay, + -> false } } - ) - .paneModifierConfiguration { + ), + paneModifierTransform { if (paneState.pane == ThreePane.TransientPrimary) Modifier .fillMaxSize() .backPreview(appState.backPreviewState) @@ -169,7 +173,8 @@ fun App( else Modifier .fillMaxSize() } - }, + ) + ), ) { appState.panedNavHostScope = this appState.splitLayoutState.visibleCount = appState.filteredPaneOrder.size @@ -262,15 +267,13 @@ fun InteractionSource.isActive(): Boolean { @Stable class AppState( - private val navigationRepository: NavigationRepository = NavigationRepository + private val navigationRepository: NavigationRepository = NavigationRepository, ) { private val navigationState = mutableStateOf( navigationRepository.navigationStateFlow.value ) - private val panedNavHostConfiguration = sampleAppNavHostConfiguration( - navigationState - ) + private val paneInteractionSourceList = mutableStateListOf() private val paneRenderOrder = listOf( ThreePane.Tertiary, @@ -293,7 +296,7 @@ class AppState( internal val isPreviewingBack get() = !backPreviewState.progress.isNaN() - internal var panedNavHostScope by mutableStateOf?>( + internal var panedNavHostScope by mutableStateOf?>( null ) @@ -326,16 +329,38 @@ class AppState( companion object { @Composable fun AppState.rememberPanedNavHostState( - configurationBlock: PanedNavHostConfiguration< - ThreePane, - MultiStackNav, - SampleDestination - >.() -> PanedNavHostConfiguration - ): SavedStatePanedNavHostState { - val panedNavHostState = remember { - SavedStatePanedNavHostState( + transforms: List>, + ): MultiPaneDisplayState { + val displayState = remember { + MultiPaneDisplayState( panes = ThreePane.entries.toList(), - configuration = panedNavHostConfiguration.configurationBlock(), + navigationState = navigationState, + backStackTransform = { multiStackNav -> + generateSequence(multiStackNav) { current -> + current.pop().takeUnless(current::equals) + } + .flatMap { listOf(it.current) + (it.current?.children ?: emptyList()) } + .filterIsInstance() + .toList() + .asReversed() + }, + destinationTransform = { + it.current as? SampleDestination ?: throw IllegalArgumentException( + "MultiStackNav leaf node ${it.current} must be an AppDestination" + ) + }, + paneEntry = { destination -> + when (destination) { + SampleDestination.NavTabs.ChatRooms -> chatRoomPaneStrategy() + + SampleDestination.NavTabs.Me -> mePaneStrategy() + + is SampleDestination.Chat -> chatPaneStrategy() + + is SampleDestination.Profile -> profilePaneStrategy() + } + }, + transforms = transforms, ) } DisposableEffect(Unit) { @@ -346,32 +371,10 @@ class AppState( } onDispose { job.cancel() } } - return panedNavHostState + return displayState } } } -private fun sampleAppNavHostConfiguration( - multiStackNavState: State -) = panedNavHostConfiguration( - navigationState = multiStackNavState, - destinationTransform = { multiStackNav -> - multiStackNav.current as? SampleDestination ?: throw IllegalArgumentException( - "MultiStackNav leaf node ${multiStackNav.current} must be an AppDestination" - ) - }, - strategyTransform = { destination -> - when (destination) { - SampleDestination.NavTabs.ChatRooms -> chatRoomPaneStrategy() - - SampleDestination.NavTabs.Me -> mePaneStrategy() - - is SampleDestination.Chat -> chatPaneStrategy() - - is SampleDestination.Profile -> profilePaneStrategy() - } - } -) - private val PaneSeparatorActiveWidthDp = 56.dp private val PaneSeparatorTouchTargetWidthDp = 16.dp \ No newline at end of file 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 452a8ad..d1eca83 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 @@ -26,7 +26,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs import com.tunjid.treenav.compose.threepane.ThreePane -import com.tunjid.treenav.compose.threepane.configurations.requireThreePaneMovableSharedElementScope +import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope import com.tunjid.treenav.compose.threepane.threePaneListDetailStrategy fun chatPaneStrategy() = threePaneListDetailStrategy( 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 273b8ba..af82ad7 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 @@ -24,7 +24,7 @@ import androidx.lifecycle.coroutineScope import androidx.lifecycle.viewmodel.compose.viewModel import com.tunjid.demo.common.ui.data.ChatsRepository import com.tunjid.demo.common.ui.data.SampleDestination -import com.tunjid.treenav.compose.threepane.configurations.requireThreePaneMovableSharedElementScope +import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope import com.tunjid.treenav.compose.threepane.threePaneListDetailStrategy fun chatRoomPaneStrategy( 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 a16fccb..82595f8 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 @@ -23,7 +23,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.profile.ProfileScreen import com.tunjid.demo.common.ui.profile.ProfileViewModel -import com.tunjid.treenav.compose.threepane.configurations.requireThreePaneMovableSharedElementScope +import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope import com.tunjid.treenav.compose.threepane.threePaneListDetailStrategy fun mePaneStrategy( 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 de8a569..9291ea8 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 @@ -25,7 +25,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs import com.tunjid.treenav.compose.threepane.ThreePane -import com.tunjid.treenav.compose.threepane.configurations.requireThreePaneMovableSharedElementScope +import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope import com.tunjid.treenav.compose.threepane.threePaneListDetailStrategy fun profilePaneStrategy() = threePaneListDetailStrategy( From 16a816020cb7eeea4f1d97c6a87ea5806893663e Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 9 Feb 2025 18:24:06 -0500 Subject: [PATCH 02/11] Add backstack methods for MultiStackNav and StackNav and tests --- .../com/tunjid/treenav/MultiStackNav.kt | 32 ++++++++- .../kotlin/com/tunjid/treenav/StackNav.kt | 32 ++++++++- .../commonTest/kotlin/MultiStackNavTest.kt | 68 ++++++++++++++++++ .../src/commonTest/kotlin/StackNavTest.kt | 71 ++++++++++++++++++- .../com/tunjid/demo/common/ui/DemoApp.kt | 10 +-- 5 files changed, 205 insertions(+), 8 deletions(-) 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 8bdc6ec..2998830 100644 --- a/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/MultiStackNav.kt +++ b/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/MultiStackNav.kt @@ -24,7 +24,7 @@ data class MultiStackNav( val name: String, val indexHistory: List = listOf(0), val currentIndex: Int = 0, - val stacks: List = listOf() + val stacks: List = listOf(), ) : Node { override val id: String get() = name override val children: List get() = stacks @@ -79,6 +79,36 @@ fun MultiStackNav.popToRoot(indexToPop: Int = currentIndex) = copy( } ) +/** + * Returns a sequence of each destination on the back stack for this [StackNav] as defined by + * [MultiStackNav.pop]. + * + * @param includeCurrentDestinationChildren when true, the result of [Node.children] for each + * [Node] is included in the back stack. + * @param placeChildrenBeforeParent when true, the result of [Node.children] are paced before + * the parent [Node] in the back stack. + */ +fun MultiStackNav.backStack( + includeCurrentDestinationChildren: Boolean, + placeChildrenBeforeParent: Boolean = false, +): Sequence = + if (!includeCurrentDestinationChildren && placeChildrenBeforeParent) throw IllegalArgumentException( + "Cannot place children nodes before the parent if children are not included" + ) + else generateSequence(this) { current -> + current.pop().takeUnless(current::equals) + } + .flatMap { nav -> + val parent = listOfNotNull(nav.current) + val children = nav.current + ?.children + ?.takeIf { includeCurrentDestinationChildren } + ?: emptyList() + + if (placeChildrenBeforeParent) children + parent + else parent + children + } + /** * Performs the given [operation] with the [StackNav] at [MultiStackNav.currentIndex] */ 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 6efef3d..19a4a20 100644 --- a/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/StackNav.kt +++ b/library/treenav/src/commonMain/kotlin/com/tunjid/treenav/StackNav.kt @@ -21,7 +21,7 @@ package com.tunjid.treenav */ data class StackNav( val name: String, - override val children: List = listOf() + override val children: List = listOf(), ) : Node { override val id: String get() = name } @@ -55,6 +55,36 @@ fun StackNav.popToRoot() = copy( children = children.take(1) ) +/** + * Returns a sequence of each destination on the back stack for this [StackNav] as defined by + * [StackNav.pop]. + * + * @param includeCurrentDestinationChildren when true, the result of [Node.children] for each + * [Node] is included in the back stack. + * @param placeChildrenBeforeParent when true, the result of [Node.children] are paced before + * the parent [Node] in the back stack. + */ +fun StackNav.backStack( + includeCurrentDestinationChildren: Boolean, + placeChildrenBeforeParent: Boolean = false, +): Sequence = + if (!includeCurrentDestinationChildren && placeChildrenBeforeParent) throw IllegalArgumentException( + "Cannot place children nodes before the parent if children are not included" + ) + else generateSequence(this) { current -> + current.pop().takeUnless(current::equals) + } + .flatMap { nav -> + val parent = listOfNotNull(nav.current) + val children = nav.current + ?.children + ?.takeIf { includeCurrentDestinationChildren } + ?: emptyList() + + if (placeChildrenBeforeParent) children + parent + else parent + children + } + /** * Indicates if there's a [Node] available to pop up to */ diff --git a/library/treenav/src/commonTest/kotlin/MultiStackNavTest.kt b/library/treenav/src/commonTest/kotlin/MultiStackNavTest.kt index a3a24d8..c279453 100644 --- a/library/treenav/src/commonTest/kotlin/MultiStackNavTest.kt +++ b/library/treenav/src/commonTest/kotlin/MultiStackNavTest.kt @@ -17,6 +17,7 @@ import com.tunjid.treenav.MultiStackNav import com.tunjid.treenav.Order import com.tunjid.treenav.StackNav +import com.tunjid.treenav.backStack import com.tunjid.treenav.flatten import com.tunjid.treenav.minus import com.tunjid.treenav.pop @@ -317,4 +318,71 @@ class MultiStackNavTest { .toSet() ) } + + @Test + fun testBackStack() { + val pushed = subject + .push(TestNode("A", children = listOf(TestNode("1")))) + .push(TestNode("B")) + .push(TestNode("C")) + .switch(toIndex = 2) + .push(TestNode("D")) + .push(TestNode("E", children = listOf(TestNode("1"), TestNode("2")))) + .switch(toIndex = 1) + .push(TestNode("F")) + + assertEquals( + listOf( + TestNode("F"), + TestNode("E", children = listOf(TestNode("1"), TestNode("2"))), + TestNode("D"), + TestNode("C"), + TestNode("B"), + TestNode("A", children = listOf(TestNode("1"))), + ), + pushed.backStack( + includeCurrentDestinationChildren = false, + placeChildrenBeforeParent = false, + ) + .toList() + ) + + assertEquals( + expected = listOf( + TestNode("F"), + TestNode("E", children = listOf(TestNode("1"), TestNode("2"))), + TestNode("1"), + TestNode("2"), + TestNode("D"), + TestNode("C"), + TestNode("B"), + TestNode("A", children = listOf(TestNode("1"))), + TestNode("1"), + ), + actual = pushed.backStack( + includeCurrentDestinationChildren = true, + placeChildrenBeforeParent = false, + ) + .toList() + ) + + assertEquals( + expected = listOf( + TestNode("F"), + TestNode("1"), + TestNode("2"), + TestNode("E", children = listOf(TestNode("1"), TestNode("2"))), + TestNode("D"), + TestNode("C"), + TestNode("B"), + TestNode("1"), + TestNode("A", children = listOf(TestNode("1"))), + ), + actual = pushed.backStack( + includeCurrentDestinationChildren = true, + placeChildrenBeforeParent = true, + ) + .toList() + ) + } } diff --git a/library/treenav/src/commonTest/kotlin/StackNavTest.kt b/library/treenav/src/commonTest/kotlin/StackNavTest.kt index a3113ff..09043e0 100644 --- a/library/treenav/src/commonTest/kotlin/StackNavTest.kt +++ b/library/treenav/src/commonTest/kotlin/StackNavTest.kt @@ -17,6 +17,7 @@ import com.tunjid.treenav.Node import com.tunjid.treenav.Order import com.tunjid.treenav.StackNav +import com.tunjid.treenav.backStack import com.tunjid.treenav.current import com.tunjid.treenav.flatten import com.tunjid.treenav.minus @@ -44,7 +45,10 @@ import kotlin.test.assertTrue * limitations under the License. */ -data class TestNode(val name: String) : Node { +data class TestNode( + val name: String, + override val children: List = emptyList(), +) : Node { override val id: String get() = name } @@ -152,4 +156,69 @@ class StackNavTest { .children ) } + + @Test + fun testBackStack() { + val pushed = subject + .push(TestNode("A", children = listOf(TestNode("1")))) + .push(TestNode("B")) + .push(TestNode("C")) + .push(TestNode("D")) + .push(TestNode("E", children = listOf(TestNode("1"), TestNode("2")))) + .push(TestNode("F")) + + assertEquals( + listOf( + TestNode("F"), + TestNode("E", children = listOf(TestNode("1"), TestNode("2"))), + TestNode("D"), + TestNode("C"), + TestNode("B"), + TestNode("A", children = listOf(TestNode("1"))), + ), + pushed.backStack( + includeCurrentDestinationChildren = false, + placeChildrenBeforeParent = false, + ) + .toList() + ) + + assertEquals( + expected = listOf( + TestNode("F"), + TestNode("E", children = listOf(TestNode("1"), TestNode("2"))), + TestNode("1"), + TestNode("2"), + TestNode("D"), + TestNode("C"), + TestNode("B"), + TestNode("A", children = listOf(TestNode("1"))), + TestNode("1"), + ), + actual = pushed.backStack( + includeCurrentDestinationChildren = true, + placeChildrenBeforeParent = false, + ) + .toList() + ) + + assertEquals( + expected = listOf( + TestNode("F"), + TestNode("1"), + TestNode("2"), + TestNode("E", children = listOf(TestNode("1"), TestNode("2"))), + TestNode("D"), + TestNode("C"), + TestNode("B"), + TestNode("1"), + TestNode("A", children = listOf(TestNode("1"))), + ), + actual = pushed.backStack( + includeCurrentDestinationChildren = true, + placeChildrenBeforeParent = true, + ) + .toList() + ) + } } 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 a9ad413..214fd34 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 @@ -74,6 +74,7 @@ import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.me.mePaneStrategy import com.tunjid.demo.common.ui.profile.profilePaneStrategy import com.tunjid.treenav.MultiStackNav +import com.tunjid.treenav.backStack import com.tunjid.treenav.compose.MultiPaneDisplay import com.tunjid.treenav.compose.MultiPaneDisplayState import com.tunjid.treenav.compose.MultiPaneDisplayScope @@ -336,13 +337,12 @@ class AppState( panes = ThreePane.entries.toList(), navigationState = navigationState, backStackTransform = { multiStackNav -> - generateSequence(multiStackNav) { current -> - current.pop().takeUnless(current::equals) - } - .flatMap { listOf(it.current) + (it.current?.children ?: emptyList()) } + multiStackNav.backStack( + includeCurrentDestinationChildren = true, + placeChildrenBeforeParent = true, + ) .filterIsInstance() .toList() - .asReversed() }, destinationTransform = { it.current as? SampleDestination ?: throw IllegalArgumentException( From 901b1f281baa2e926e9301df42977400595404cb Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 9 Feb 2025 19:12:05 -0500 Subject: [PATCH 03/11] Use back stack transform --- .../treenav/compose/MultiPaneDisplayState.kt | 8 +++---- .../compose/SavedStatePanedNavHostState.kt | 23 +++++++++++-------- .../com/tunjid/demo/common/ui/DemoApp.kt | 2 +- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt index a441367..4cf71f3 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt @@ -65,7 +65,7 @@ class MultiPaneDisplayState in * @param navigationState the navigation state to be adapted into various panes. * @param backStackTransform a transform to read the back stack of the navigation state. * @param destinationTransform a transform of the [navigationState] to its current destination. - * @param paneEntry provides the [Transform]s and content needed to render + * @param entryProvider provides the [Transform]s and content needed to render * a [Destination] in its pane. * @param transforms a list of transforms applied to every [Destination] before it is * rendered in its pane. Order matters; they are applied from last to first. @@ -75,7 +75,7 @@ fun MultiPaneDisplayState( navigationState: State, backStackTransform: (NavigationState) -> List, destinationTransform: (NavigationState) -> Destination, - paneEntry: (Destination) -> PaneEntry, + entryProvider: (Destination) -> PaneEntry, transforms: List>, ) = transforms.fold( initial = MultiPaneDisplayState( @@ -84,10 +84,10 @@ fun MultiPaneDisplayState( backStackTransform = backStackTransform, destinationTransform = destinationTransform, panesToDestinationsTransform = { destination -> - paneEntry(destination).paneTransform(destination) + entryProvider(destination).paneTransform(destination) }, renderTransform = { destination -> - val nav = paneEntry(destination) + val nav = entryProvider(destination) with(nav.renderTransform) { Render( destination = destination, diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt index 1c039d6..08406ef 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt @@ -24,17 +24,15 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.currentStateAsState import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import com.tunjid.treenav.Node -import com.tunjid.treenav.Order import com.tunjid.treenav.compose.lifecycle.DestinationViewModelStoreCreator import com.tunjid.treenav.compose.lifecycle.rememberDestinationLifecycleOwner -import com.tunjid.treenav.traverse @Stable -internal class SlottedMultiPaneDisplayScope( +internal class SlottedMultiPaneDisplayScope( panes: List, initialPanesToNodes: Map, saveableStateHolder: SaveableStateHolder, - val displayState: MultiPaneDisplayState, + val displayState: MultiPaneDisplayState, ) : MultiPaneDisplayScope, SaveableStateHolder by saveableStateHolder { private val slots = List( @@ -91,7 +89,7 @@ internal class SlottedMultiPaneDisplayScope( ): Destination? = panedNavigationState.destinationFor(pane) internal fun onNewNavigationState( - navigationState: Node, + navigationState: NavigationState, panesToNodes: Map, ) { updateAdaptiveNavigationState { @@ -217,12 +215,17 @@ internal class SlottedMultiPaneDisplayScope( ) { panedNavigationState = panedNavigationState.block() } -} -private fun Node.backStackIds() = - mutableSetOf().apply { - traverse(Order.DepthFirst) { add(it.id) } - } + + private fun NavigationState.backStackIds(): MutableSet = + displayState.backStackTransform( + this + ) + .fold(mutableSetOf()) { set, destination -> + set.add(destination.id) + set + } +} //fun PanedNavHostScope< // Pane, 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 214fd34..77ac9fe 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 @@ -349,7 +349,7 @@ class AppState( "MultiStackNav leaf node ${it.current} must be an AppDestination" ) }, - paneEntry = { destination -> + entryProvider = { destination -> when (destination) { SampleDestination.NavTabs.ChatRooms -> chatRoomPaneStrategy() From 2b738c44dbeb056267c7b0852e7fb3576323bcd2 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 9 Feb 2025 19:51:53 -0500 Subject: [PATCH 04/11] Rename threePaneStrategy to threePaneEntry --- .../kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt | 5 +++-- .../kotlin/com/tunjid/demo/common/ui/chat/Strategy.kt | 4 ++-- .../kotlin/com/tunjid/demo/common/ui/chatrooms/Strategy.kt | 4 ++-- .../kotlin/com/tunjid/demo/common/ui/me/Strategy.kt | 4 ++-- .../kotlin/com/tunjid/demo/common/ui/profile/Strategy.kt | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt index e8894a2..f7d5911 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/ThreePane.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tunjid.treenav.Node import com.tunjid.treenav.compose.Adaptation.Swap +import com.tunjid.treenav.compose.MultiPaneDisplay import com.tunjid.treenav.compose.PaneEntry import com.tunjid.treenav.compose.PaneScope @@ -85,7 +86,7 @@ enum class ThreePane { } /** - * A strategy for selectively running animations in list detail flows. When: + * A [PaneEntry] for selectively running animations in [ThreePane] [MultiPaneDisplay]. When: * - A navigation destination moves between the [ThreePane.Primary] and [ThreePane.Secondary] * panes, the pane animations are not run to provide a seamless movement experience. * - A navigation destination moves between the [ThreePane.Primary] and @@ -96,7 +97,7 @@ enum class ThreePane { * @param paneMapping the mapping of panes to navigation destinations. * @param render the Composable for rendering the current destination. */ -fun threePaneListDetailStrategy( +fun threePaneEntry( enterTransition: PaneScope.() -> EnterTransition = { DefaultFadeIn }, exitTransition: PaneScope.() -> ExitTransition = { DefaultFadeOut }, paneMapping: @Composable (R) -> Map = { 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 d1eca83..a175546 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 @@ -27,9 +27,9 @@ import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs import com.tunjid.treenav.compose.threepane.ThreePane import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope -import com.tunjid.treenav.compose.threepane.threePaneListDetailStrategy +import com.tunjid.treenav.compose.threepane.threePaneEntry -fun chatPaneStrategy() = threePaneListDetailStrategy( +fun chatPaneStrategy() = threePaneEntry( paneMapping = { destination -> mapOf( ThreePane.Primary to destination, 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 af82ad7..d5541c3 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 @@ -25,10 +25,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel 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.treenav.compose.threepane.threePaneListDetailStrategy +import com.tunjid.treenav.compose.threepane.threePaneEntry fun chatRoomPaneStrategy( -) = threePaneListDetailStrategy( +) = 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/Strategy.kt index 82595f8..594fa6f 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 @@ -24,10 +24,10 @@ import com.tunjid.demo.common.ui.data.SampleDestination 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.threePaneListDetailStrategy +import com.tunjid.treenav.compose.threepane.threePaneEntry fun mePaneStrategy( -) = threePaneListDetailStrategy( +) = threePaneEntry( render = { val scope = LocalLifecycleOwner.current.lifecycle.coroutineScope val viewModel = viewModel { 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 9291ea8..c229e19 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 @@ -26,9 +26,9 @@ import com.tunjid.demo.common.ui.data.SampleDestination import com.tunjid.demo.common.ui.data.SampleDestination.NavTabs import com.tunjid.treenav.compose.threepane.ThreePane import com.tunjid.treenav.compose.threepane.transforms.requireThreePaneMovableSharedElementScope -import com.tunjid.treenav.compose.threepane.threePaneListDetailStrategy +import com.tunjid.treenav.compose.threepane.threePaneEntry -fun profilePaneStrategy() = threePaneListDetailStrategy( +fun profilePaneStrategy() = threePaneEntry( paneMapping = { destination -> check(destination is SampleDestination.Profile) mapOf( From 9540279367d9a6c0578346688026b15697b7e2b9 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Sun, 9 Feb 2025 20:20:32 -0500 Subject: [PATCH 05/11] Rename original to previousTransform in Transform --- .../treenav/compose/MultiPaneDisplayState.kt | 16 ++--- .../com/tunjid/treenav/compose/PaneEntry.kt | 4 +- .../AnimatePaneBoundsTransform.kt | 62 ------------------- .../MovableSharedElementTransform.kt | 8 +-- .../transforms/PredictiveBackTransform.kt | 26 ++++---- .../transforms/ThreePaneAdaptiveTransform.kt | 8 +-- .../PaneModifierTransform.kt | 6 +- .../Transforms.kt | 8 +-- .../com/tunjid/demo/common/ui/DemoApp.kt | 44 ++++++------- 9 files changed, 60 insertions(+), 122 deletions(-) delete mode 100644 library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/AnimatePaneBoundsTransform.kt rename library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/{configurations => transforms}/PaneModifierTransform.kt (90%) rename library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/{configurations => transforms}/Transforms.kt (75%) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt index 4cf71f3..13eaf00 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt @@ -22,10 +22,10 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.tunjid.treenav.Node -import com.tunjid.treenav.compose.configurations.DestinationTransform -import com.tunjid.treenav.compose.configurations.PaneTransform -import com.tunjid.treenav.compose.configurations.RenderTransform -import com.tunjid.treenav.compose.configurations.Transform +import com.tunjid.treenav.compose.transforms.DestinationTransform +import com.tunjid.treenav.compose.transforms.PaneTransform +import com.tunjid.treenav.compose.transforms.RenderTransform +import com.tunjid.treenav.compose.transforms.Transform /** * Class for configuring a [MultiPaneDisplay] for selecting, adapting and placing navigation @@ -91,7 +91,7 @@ fun MultiPaneDisplayState( with(nav.renderTransform) { Render( destination = destination, - original = nav.content, + previousTransform = nav.content, ) } } @@ -139,7 +139,7 @@ private operator fun is DestinationTransform -> { destination -> transform.toDestination( navigationState = destination, - original = destinationTransform + previousTransform = destinationTransform ) } @@ -149,7 +149,7 @@ private operator fun is PaneTransform -> { destination -> transform.toPanesAndDestinations( destination = destination, - original = panesToDestinationsTransform, + previousTransform = panesToDestinationsTransform, ) } @@ -160,7 +160,7 @@ private operator fun with(transform) { Render( destination = destination, - original = renderTransform, + previousTransform = renderTransform, ) } } diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneEntry.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneEntry.kt index 39c352c..7caeea5 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneEntry.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/PaneEntry.kt @@ -1,11 +1,9 @@ package com.tunjid.treenav.compose -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import com.tunjid.treenav.Node -import com.tunjid.treenav.compose.configurations.RenderTransform +import com.tunjid.treenav.compose.transforms.RenderTransform /** * Provides the logic used to select, configure and place a navigation [Destination] for each diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/AnimatePaneBoundsTransform.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/AnimatePaneBoundsTransform.kt deleted file mode 100644 index e761ff5..0000000 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/AnimatePaneBoundsTransform.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.configurations - -import androidx.compose.animation.BoundsTransform -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.LookaheadScope -import com.tunjid.treenav.Node -import com.tunjid.treenav.compose.MultiPaneDisplayState -import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.utilities.AnimatedBoundsState -import com.tunjid.treenav.compose.utilities.AnimatedBoundsState.Companion.animateBounds -import com.tunjid.treenav.compose.utilities.DefaultBoundsTransform - -/** - * A [MultiPaneDisplayState] that animates the bounds of each [Pane] displayed within it. - * This is useful for scenarios where the panes move within a layout hierarchy to accommodate - * other panes. - * - * @param lookaheadScope the root [LookaheadScope] where the panes are rendered in. - * @param paneBoundsTransform a lambda providing the [BoundsTransform] for each [Pane]. - * @param shouldAnimatePane a lambda for toggling when the pane can be animated. It allows for - * skipping an animation in progress. - */ -@OptIn(ExperimentalSharedTransitionApi::class) -fun animatePaneBoundsTransform( - lookaheadScope: LookaheadScope, - paneBoundsTransform: PaneScope.() -> BoundsTransform = { DefaultBoundsTransform }, - shouldAnimatePane: PaneScope.() -> Boolean = { true }, -): Transform = - RenderTransform { destination, original -> - Box( - modifier = Modifier.animateBounds( - state = remember { - AnimatedBoundsState( - lookaheadScope = lookaheadScope, - boundsTransform = paneBoundsTransform(), - inProgress = { shouldAnimatePane() } - ) - } - ) - ) { - original(destination) - } - } \ 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 86013d8..6345087 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 @@ -13,8 +13,8 @@ import androidx.compose.ui.Modifier import com.tunjid.treenav.Node import com.tunjid.treenav.compose.MultiPaneDisplay import com.tunjid.treenav.compose.PaneScope -import com.tunjid.treenav.compose.configurations.RenderTransform -import com.tunjid.treenav.compose.configurations.Transform +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 @@ -31,7 +31,7 @@ fun threePanedMovableSharedElementTransform( movableSharedElementHostState: MovableSharedElementHostState, ): Transform = - RenderTransform { navigationDestination, original -> + RenderTransform { destination, previousTransform -> val delegate = remember { PanedMovableSharedElementScope( paneScope = this, @@ -47,7 +47,7 @@ fun ) } - original(movableSharedElementScope, navigationDestination) + previousTransform(movableSharedElementScope, destination) } fun PaneScope< diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt index 6c52a26..fb06efa 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt @@ -22,9 +22,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.tunjid.treenav.Node -import com.tunjid.treenav.compose.configurations.DestinationTransform -import com.tunjid.treenav.compose.configurations.PaneTransform -import com.tunjid.treenav.compose.configurations.Transform +import com.tunjid.treenav.compose.transforms.DestinationTransform +import com.tunjid.treenav.compose.transforms.PaneTransform +import com.tunjid.treenav.compose.transforms.Transform import com.tunjid.treenav.compose.threepane.ThreePane /** @@ -49,30 +49,30 @@ inline fun override fun toDestination( navigationState: NavigationState, - original: (NavigationState) -> Destination, + previousTransform: (NavigationState) -> Destination, ): Destination { - val current = original(navigationState) - lastPrimaryDestination = current - return if (isPreviewingBack.value) original(navigationState.backPreviewTransform()) - else current + val previousDestination = previousTransform(navigationState) + lastPrimaryDestination = previousDestination + return if (isPreviewingBack.value) previousTransform(navigationState.backPreviewTransform()) + else previousDestination } @Composable override fun toPanesAndDestinations( destination: Destination, - original: @Composable (Destination) -> Map, + previousTransform: @Composable (Destination) -> Map, ): Map { - val originalMapping = original(destination) + val previousMapping = previousTransform(destination) val isPreviewing by isPreviewingBack - if (!isPreviewing) return originalMapping + if (!isPreviewing) return previousMapping // Back is being previewed, therefore the original mapping is already for back. // Pass the previous primary value into transient. val transientDestination = checkNotNull(lastPrimaryDestination) { "Attempted to show last destination without calling destination transform" } - val paneMapping = original(transientDestination) + val paneMapping = previousTransform(transientDestination) val transient = paneMapping[ThreePane.Primary] - return originalMapping + (ThreePane.TransientPrimary to transient) + return previousMapping + (ThreePane.TransientPrimary to transient) } } } diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt index 6a7c39e..4694971 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/ThreePaneAdaptiveTransform.kt @@ -6,8 +6,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tunjid.treenav.Node -import com.tunjid.treenav.compose.configurations.PaneTransform -import com.tunjid.treenav.compose.configurations.Transform +import com.tunjid.treenav.compose.transforms.PaneTransform +import com.tunjid.treenav.compose.transforms.Transform import com.tunjid.treenav.compose.threepane.ThreePane /** @@ -22,10 +22,10 @@ fun secondaryPaneBreakPoint: State = mutableStateOf(SECONDARY_PANE_MIN_WIDTH_BREAKPOINT_DP), tertiaryPaneBreakPoint: State = mutableStateOf(TERTIARY_PANE_MIN_WIDTH_BREAKPOINT_DP), ): Transform = - PaneTransform { destination, original -> + PaneTransform { destination, previousTransform -> // Consider navigation state different if window size class changes val windowWidthDp by windowWidthState - val originalMapping = original(destination) + val originalMapping = previousTransform(destination) val primaryNode = originalMapping[ThreePane.Primary] mapOf( ThreePane.Primary to primaryNode, diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/PaneModifierTransform.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/PaneModifierTransform.kt similarity index 90% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/PaneModifierTransform.kt rename to library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/PaneModifierTransform.kt index 3e27305..0f5f970 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/PaneModifierTransform.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/PaneModifierTransform.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.tunjid.treenav.compose.configurations +package com.tunjid.treenav.compose.transforms import androidx.compose.foundation.layout.Box import androidx.compose.ui.Modifier @@ -31,10 +31,10 @@ import com.tunjid.treenav.compose.PaneScope fun paneModifierTransform( paneModifier: PaneScope.() -> Modifier = { Modifier }, ): Transform = - RenderTransform { destination, original -> + RenderTransform { destination, previousTransform -> Box( modifier = paneModifier() ) { - original(destination) + previousTransform(destination) } } \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/Transforms.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/Transforms.kt similarity index 75% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/Transforms.kt rename to library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/Transforms.kt index 055d17b..50ad4f8 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/configurations/Transforms.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/Transforms.kt @@ -1,4 +1,4 @@ -package com.tunjid.treenav.compose.configurations +package com.tunjid.treenav.compose.transforms import androidx.compose.runtime.Composable import com.tunjid.treenav.Node @@ -11,7 +11,7 @@ fun interface DestinationTransform { fun toDestination( navigationState: NavigationState, - original: (NavigationState) -> Destination, + previousTransform: (NavigationState) -> Destination, ): Destination } @@ -20,7 +20,7 @@ fun interface PaneTransform @Composable fun toPanesAndDestinations( destination: Destination, - original: @Composable (Destination) -> Map, + previousTransform: @Composable (Destination) -> Map, ): Map } @@ -29,7 +29,7 @@ fun interface RenderTransform @Composable fun PaneScope.Render( destination: Destination, - original: @Composable PaneScope.(Destination) -> Unit, + previousTransform: @Composable PaneScope.(Destination) -> Unit, ) } 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 77ac9fe..5d65bd6 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 @@ -18,9 +18,12 @@ 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.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 @@ -76,16 +79,15 @@ import com.tunjid.demo.common.ui.profile.profilePaneStrategy import com.tunjid.treenav.MultiStackNav import com.tunjid.treenav.backStack import com.tunjid.treenav.compose.MultiPaneDisplay -import com.tunjid.treenav.compose.MultiPaneDisplayState import com.tunjid.treenav.compose.MultiPaneDisplayScope -import com.tunjid.treenav.compose.configurations.Transform -import com.tunjid.treenav.compose.configurations.animatePaneBoundsTransform -import com.tunjid.treenav.compose.configurations.paneModifierTransform +import com.tunjid.treenav.compose.MultiPaneDisplayState import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementHostState import com.tunjid.treenav.compose.threepane.ThreePane import com.tunjid.treenav.compose.threepane.transforms.predictiveBackTransform import com.tunjid.treenav.compose.threepane.transforms.threePanedAdaptiveTransform import com.tunjid.treenav.compose.threepane.transforms.threePanedMovableSharedElementTransform +import com.tunjid.treenav.compose.transforms.Transform +import com.tunjid.treenav.compose.transforms.paneModifierTransform import com.tunjid.treenav.current import com.tunjid.treenav.pop import com.tunjid.treenav.popToRoot @@ -150,28 +152,28 @@ fun App( threePanedMovableSharedElementTransform( movableSharedElementHostState = movableSharedElementHostState ), - animatePaneBoundsTransform( - lookaheadScope = this@SharedTransitionScope, - shouldAnimatePane = { - when (paneState.pane) { - ThreePane.Primary, - ThreePane.TransientPrimary, - ThreePane.Secondary, - ThreePane.Tertiary, - -> canAnimatePanes + 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, - -> false + null, + ThreePane.Overlay, + -> snap() + } } - } - ), - paneModifierTransform { - 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() } ) From 6712e4c4b9bcf51495fe1db1045a19e8c1350a6f Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Mon, 10 Feb 2025 10:03:21 -0500 Subject: [PATCH 06/11] Pass backstack directly to SlottedMultiPaneDisplayScope --- .../treenav/compose/MultiPaneDisplay.kt | 15 +++++++---- ...ate.kt => SlottedMultiPaneDisplayScope.kt} | 26 +++++++++---------- 2 files changed, 22 insertions(+), 19 deletions(-) rename library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/{SavedStatePanedNavHostState.kt => SlottedMultiPaneDisplayScope.kt} (93%) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt index 966366d..1523048 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt @@ -19,8 +19,9 @@ package com.tunjid.treenav.compose import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.SaveableStateHolder @@ -80,24 +81,28 @@ fun MultiPaneDisplay( content: @Composable MultiPaneDisplayScope.() -> Unit, ) { - val navigationState by state.navigationState + val backStack by derivedStateOf { + state.backStackTransform(state.navigationState.value) + } val panesToNodes = state.panesToDestinations() val saveableStateHolder = rememberPanedSaveableStateHolder() val panedContentScope = remember { SlottedMultiPaneDisplayScope( panes = state.panes, + initialBackStack = backStack, initialPanesToNodes = panesToNodes, saveableStateHolder = saveableStateHolder, displayState = state, ) } - LaunchedEffect(navigationState, panesToNodes) { - panedContentScope.onNewNavigationState( - navigationState = navigationState, + DisposableEffect(backStack, panesToNodes) { + panedContentScope.onBackStackChanged( + backStack = backStack, panesToNodes = panesToNodes ) + onDispose { } } Box( diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlottedMultiPaneDisplayScope.kt similarity index 93% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt rename to library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlottedMultiPaneDisplayScope.kt index 08406ef..6369058 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlottedMultiPaneDisplayScope.kt @@ -28,11 +28,12 @@ import com.tunjid.treenav.compose.lifecycle.DestinationViewModelStoreCreator import com.tunjid.treenav.compose.lifecycle.rememberDestinationLifecycleOwner @Stable -internal class SlottedMultiPaneDisplayScope( +internal class SlottedMultiPaneDisplayScope( panes: List, + initialBackStack: List, initialPanesToNodes: Map, saveableStateHolder: SaveableStateHolder, - val displayState: MultiPaneDisplayState, + val displayState: MultiPaneDisplayState, ) : MultiPaneDisplayScope, SaveableStateHolder by saveableStateHolder { private val slots = List( @@ -45,7 +46,7 @@ internal class SlottedMultiPaneDisplayScope, panesToNodes: Map, ) { updateAdaptiveNavigationState { adaptTo( slots = slots.toSet(), panesToNodes = panesToNodes, - backStackIds = navigationState.backStackIds() + backStackIds = backStack.ids() ) } } @@ -217,14 +218,11 @@ internal class SlottedMultiPaneDisplayScope = - displayState.backStackTransform( - this - ) - .fold(mutableSetOf()) { set, destination -> - set.add(destination.id) - set - } + private fun List.ids(): MutableSet = + fold(mutableSetOf()) { set, destination -> + set.add(destination.id) + set + } } //fun PanedNavHostScope< From d3d3a0df38947fc193590d35e19d563efe7aa3b7 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Mon, 10 Feb 2025 17:35:03 -0500 Subject: [PATCH 07/11] Remember backstack --- .../kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt index 1523048..c86c77d 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt @@ -81,8 +81,10 @@ fun MultiPaneDisplay( content: @Composable MultiPaneDisplayScope.() -> Unit, ) { - val backStack by derivedStateOf { - state.backStackTransform(state.navigationState.value) + val backStack by remember { + derivedStateOf { + state.backStackTransform(state.navigationState.value) + } } val panesToNodes = state.panesToDestinations() val saveableStateHolder = rememberPanedSaveableStateHolder() From 5e90bad7cd61b0394c4313d7f0eb4ab0ffd38ca1 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Mon, 10 Feb 2025 22:21:25 -0500 Subject: [PATCH 08/11] Pass rende method to DisplayScope --- .../treenav/compose/MultiPaneDisplay.kt | 60 ++++++++++++------- .../treenav/compose/MultiPaneDisplayState.kt | 30 ---------- .../compose/SlottedMultiPaneDisplayScope.kt | 4 +- 3 files changed, 40 insertions(+), 54 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt index c86c77d..8ddcff9 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt @@ -80,36 +80,52 @@ fun MultiPaneDisplay( modifier: Modifier = Modifier, content: @Composable MultiPaneDisplayScope.() -> Unit, ) { - val backStack by remember { derivedStateOf { state.backStackTransform(state.navigationState.value) } } - val panesToNodes = state.panesToDestinations() - val saveableStateHolder = rememberPanedSaveableStateHolder() - - val panedContentScope = remember { - SlottedMultiPaneDisplayScope( - panes = state.panes, - initialBackStack = backStack, - initialPanesToNodes = panesToNodes, - saveableStateHolder = saveableStateHolder, - displayState = state, - ) - } - - DisposableEffect(backStack, panesToNodes) { - panedContentScope.onBackStackChanged( - backStack = backStack, - panesToNodes = panesToNodes - ) - onDispose { } - } Box( modifier = modifier ) { - panedContentScope.content() + val panesToNodes = state.panesToDestinations() + val saveableStateHolder = rememberPanedSaveableStateHolder() + val displayScope = remember { + SlottedMultiPaneDisplayScope( + panes = state.panes, + initialBackStack = backStack, + initialPanesToNodes = panesToNodes, + saveableStateHolder = saveableStateHolder, + paneRenderer = { + val currentDestination = remember(paneState.currentDestination) { + paneState.currentDestination + } + currentDestination?.let { destination -> + state.renderTransform(this, destination) + } + }, + ) + } + + DisposableEffect(backStack, panesToNodes) { + displayScope.onBackStackChanged( + backStack = backStack, + panesToNodes = panesToNodes + ) + onDispose { } + } + + displayScope.content() } +} + +/** + * The current pane mapping to use in the [MultiPaneDisplay]. + */ +@Composable +private fun + MultiPaneDisplayState.panesToDestinations(): Map { + val current by currentDestination + return panesToDestinationsTransform(current) } \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt index 13eaf00..ff2f589 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt @@ -19,8 +19,6 @@ package com.tunjid.treenav.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import com.tunjid.treenav.Node import com.tunjid.treenav.compose.transforms.DestinationTransform import com.tunjid.treenav.compose.transforms.PaneTransform @@ -99,34 +97,6 @@ fun MultiPaneDisplayState( operation = MultiPaneDisplayState::plus ) -/** - * The current destination in a given [paneScope]. - */ -@Composable -internal fun MultiPaneDisplayState< - Pane, - *, - Destination - >.Destination( - paneScope: PaneScope, -) { - val current = remember(paneScope.paneState.currentDestination) { - paneScope.paneState.currentDestination - } ?: return - - paneScope.renderTransform(current) -} - -/** - * THe current pane mapping to use in the [MultiPaneDisplay]. - */ -@Composable -internal fun - MultiPaneDisplayState.panesToDestinations(): Map { - val current by currentDestination - return panesToDestinationsTransform(current) -} - private operator fun MultiPaneDisplayState.plus( transform: Transform, diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlottedMultiPaneDisplayScope.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlottedMultiPaneDisplayScope.kt index 6369058..85327f8 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlottedMultiPaneDisplayScope.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlottedMultiPaneDisplayScope.kt @@ -33,7 +33,7 @@ internal class SlottedMultiPaneDisplayScope( initialBackStack: List, initialPanesToNodes: Map, saveableStateHolder: SaveableStateHolder, - val displayState: MultiPaneDisplayState, + private val paneRenderer: @Composable (PaneScope.() -> Unit), ) : MultiPaneDisplayScope, SaveableStateHolder by saveableStateHolder { private val slots = List( @@ -154,7 +154,7 @@ internal class SlottedMultiPaneDisplayScope( LocalViewModelStoreOwner provides destinationViewModelOwner, ) { SaveableStateProvider(destination.id) { - displayState.Destination(paneScope = scope) + scope.paneRenderer() DisposableEffect(Unit) { onDispose { From a535f4640e0530d68d908691579e73eafb257c91 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Tue, 11 Feb 2025 18:39:04 -0500 Subject: [PATCH 09/11] Renamed nodeFor to destinationIn in MultiPaneDisplayScope --- .../treenav/compose/MultiPaneDisplay.kt | 58 +++---------------- .../compose/SlottedMultiPaneDisplayScope.kt | 47 +++++++++++++-- .../com/tunjid/demo/common/ui/DemoApp.kt | 12 ++-- 3 files changed, 56 insertions(+), 61 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt index 8ddcff9..8135b2e 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplay.kt @@ -19,11 +19,7 @@ package com.tunjid.treenav.compose import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.SaveableStateHolder import androidx.compose.ui.Modifier import androidx.lifecycle.Lifecycle @@ -48,7 +44,7 @@ interface MultiPaneDisplayScope { pane: Pane, ): Set - fun nodeFor( + fun destinationIn( pane: Pane, ): Destination? } @@ -69,10 +65,10 @@ interface MultiPaneDisplayScope { * follows the [Lifecycle] of its immediate parent, unless it is animating out or placed in the * backstack. This is defined by [PaneScope.isActive], which is a function of the backing * [AnimatedContent] for each [Pane] displayed and if the current [Destination] - * matches [MultiPaneDisplayScope.nodeFor] in the visible [Pane]. + * matches [MultiPaneDisplayScope.destinationIn] in the visible [Pane]. * * @param state the driving [MultiPaneDisplayState] that applies adaptive semantics and - * strategies for each navigation destination shown in the [MultiPaneDisplay]. + * transforms for each navigation destination shown in the [MultiPaneDisplay]. */ @Composable fun MultiPaneDisplay( @@ -80,52 +76,12 @@ fun MultiPaneDisplay( modifier: Modifier = Modifier, content: @Composable MultiPaneDisplayScope.() -> Unit, ) { - val backStack by remember { - derivedStateOf { - state.backStackTransform(state.navigationState.value) - } - } - Box( modifier = modifier ) { - val panesToNodes = state.panesToDestinations() - val saveableStateHolder = rememberPanedSaveableStateHolder() - val displayScope = remember { - SlottedMultiPaneDisplayScope( - panes = state.panes, - initialBackStack = backStack, - initialPanesToNodes = panesToNodes, - saveableStateHolder = saveableStateHolder, - paneRenderer = { - val currentDestination = remember(paneState.currentDestination) { - paneState.currentDestination - } - currentDestination?.let { destination -> - state.renderTransform(this, destination) - } - }, - ) - } - - DisposableEffect(backStack, panesToNodes) { - displayScope.onBackStackChanged( - backStack = backStack, - panesToNodes = panesToNodes - ) - onDispose { } - } - - displayScope.content() + SlottedMultiPaneDisplayScope( + state = state, + content = content + ) } } - -/** - * The current pane mapping to use in the [MultiPaneDisplay]. - */ -@Composable -private fun - MultiPaneDisplayState.panesToDestinations(): Map { - val current by currentDestination - return panesToDestinationsTransform(current) -} \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlottedMultiPaneDisplayScope.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlottedMultiPaneDisplayScope.kt index 85327f8..2cf1464 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlottedMultiPaneDisplayScope.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SlottedMultiPaneDisplayScope.kt @@ -27,8 +27,48 @@ import com.tunjid.treenav.Node import com.tunjid.treenav.compose.lifecycle.DestinationViewModelStoreCreator import com.tunjid.treenav.compose.lifecycle.rememberDestinationLifecycleOwner +@Composable +internal fun SlottedMultiPaneDisplayScope( + state: MultiPaneDisplayState, + content: @Composable (MultiPaneDisplayScope.() -> Unit), +) { + val backStack by remember { + derivedStateOf { + state.backStackTransform(state.navigationState.value) + } + } + val panesToNodes = state.panesToDestinationsTransform(state.currentDestination.value) + val saveableStateHolder = rememberPanedSaveableStateHolder() + val displayScope = remember { + SlottedMultiPaneDisplayScope( + panes = state.panes, + initialBackStack = backStack, + initialPanesToNodes = panesToNodes, + saveableStateHolder = saveableStateHolder, + paneRenderer = { + val currentDestination = remember(paneState.currentDestination) { + paneState.currentDestination + } + currentDestination?.let { destination -> + state.renderTransform(this, destination) + } + }, + ) + } + + DisposableEffect(backStack, panesToNodes) { + displayScope.onBackStackChanged( + backStack = backStack, + panesToNodes = panesToNodes + ) + onDispose { } + } + + displayScope.content() +} + @Stable -internal class SlottedMultiPaneDisplayScope( +private class SlottedMultiPaneDisplayScope( panes: List, initialBackStack: List, initialPanesToNodes: Map, @@ -85,11 +125,11 @@ internal class SlottedMultiPaneDisplayScope( pane: Pane, ): Set = panedNavigationState.adaptationsIn(pane) - override fun nodeFor( + override fun destinationIn( pane: Pane, ): Destination? = panedNavigationState.destinationFor(pane) - internal fun onBackStackChanged( + fun onBackStackChanged( backStack: List, panesToNodes: Map, ) { @@ -217,7 +257,6 @@ internal class SlottedMultiPaneDisplayScope( panedNavigationState = panedNavigationState.block() } - private fun List.ids(): MutableSet = fold(mutableSetOf()) { set, destination -> set.add(destination.id) 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 5d65bd6..9aff10a 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 @@ -69,7 +69,7 @@ import com.tunjid.composables.backpreview.BackPreviewState import com.tunjid.composables.backpreview.backPreview import com.tunjid.composables.splitlayout.SplitLayout import com.tunjid.composables.splitlayout.SplitLayoutState -import com.tunjid.demo.common.ui.AppState.Companion.rememberPanedNavHostState +import com.tunjid.demo.common.ui.AppState.Companion.rememberMultiPaneDisplayState import com.tunjid.demo.common.ui.chat.chatPaneStrategy import com.tunjid.demo.common.ui.chatrooms.chatRoomPaneStrategy import com.tunjid.demo.common.ui.data.NavigationRepository @@ -132,7 +132,7 @@ fun App( MultiPaneDisplay( modifier = Modifier .fillMaxSize(), - state = appState.rememberPanedNavHostState( + state = appState.rememberMultiPaneDisplayState( listOf( threePanedAdaptiveTransform( windowWidthState = remember { @@ -179,7 +179,7 @@ fun App( ) ), ) { - appState.panedNavHostScope = this + appState.displayScope = this appState.splitLayoutState.visibleCount = appState.filteredPaneOrder.size SplitLayout( state = appState.splitLayoutState, @@ -299,12 +299,12 @@ class AppState( internal val isPreviewingBack get() = !backPreviewState.progress.isNaN() - internal var panedNavHostScope by mutableStateOf?>( + internal var displayScope by mutableStateOf?>( null ) val filteredPaneOrder: List by derivedStateOf { - paneRenderOrder.filter { panedNavHostScope?.nodeFor(it) != null } + paneRenderOrder.filter { displayScope?.destinationIn(it) != null } } fun setTab(destination: SampleDestination.NavTabs) { @@ -331,7 +331,7 @@ class AppState( companion object { @Composable - fun AppState.rememberPanedNavHostState( + fun AppState.rememberMultiPaneDisplayState( transforms: List>, ): MultiPaneDisplayState { val displayState = remember { From 30d98cfb8c0ebec55c260840402532f2446de8c5 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Wed, 12 Feb 2025 14:39:17 -0500 Subject: [PATCH 10/11] Add CompoundTransform and update docs --- .../treenav/compose/MultiPaneDisplayState.kt | 7 +- .../transforms/PredictiveBackTransform.kt | 35 +++------ .../treenav/compose/transforms/Transforms.kt | 75 ++++++++++++++++++- 3 files changed, 90 insertions(+), 27 deletions(-) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt index ff2f589..072c299 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/MultiPaneDisplayState.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import com.tunjid.treenav.Node +import com.tunjid.treenav.compose.transforms.CompoundTransform import com.tunjid.treenav.compose.transforms.DestinationTransform import com.tunjid.treenav.compose.transforms.PaneTransform import com.tunjid.treenav.compose.transforms.RenderTransform @@ -101,7 +102,11 @@ private operator fun MultiPaneDisplayState.plus( transform: Transform, ): MultiPaneDisplayState = - MultiPaneDisplayState( + if (transform is CompoundTransform) transform.transforms.fold( + initial = this, + operation = MultiPaneDisplayState::plus, + ) + else MultiPaneDisplayState( panes = panes, navigationState = navigationState, backStackTransform = backStackTransform, diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt index fb06efa..4005512 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt @@ -16,16 +16,14 @@ package com.tunjid.treenav.compose.threepane.transforms -import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.tunjid.treenav.Node -import com.tunjid.treenav.compose.transforms.DestinationTransform -import com.tunjid.treenav.compose.transforms.PaneTransform -import com.tunjid.treenav.compose.transforms.Transform import com.tunjid.treenav.compose.threepane.ThreePane +import com.tunjid.treenav.compose.transforms.Transform +import com.tunjid.treenav.compose.transforms.compoundTransform /** * An [Transform] that moves the destination in a [ThreePane.Primary] pane, to @@ -41,30 +39,19 @@ inline fun isPreviewingBack: State, crossinline backPreviewTransform: NavigationState.() -> NavigationState, ): Transform { - return object : - DestinationTransform, - PaneTransform { - - var lastPrimaryDestination by mutableStateOf(null) + var lastPrimaryDestination by mutableStateOf(null) - override fun toDestination( - navigationState: NavigationState, - previousTransform: (NavigationState) -> Destination, - ): Destination { + return compoundTransform( + destinationTransform = { navigationState, previousTransform -> val previousDestination = previousTransform(navigationState) lastPrimaryDestination = previousDestination - return if (isPreviewingBack.value) previousTransform(navigationState.backPreviewTransform()) + if (isPreviewingBack.value) previousTransform(navigationState.backPreviewTransform()) else previousDestination - } - - @Composable - override fun toPanesAndDestinations( - destination: Destination, - previousTransform: @Composable (Destination) -> Map, - ): Map { + }, + paneTransform = paneTransform@{ destination, previousTransform -> val previousMapping = previousTransform(destination) val isPreviewing by isPreviewingBack - if (!isPreviewing) return previousMapping + if (!isPreviewing) return@paneTransform previousMapping // Back is being previewed, therefore the original mapping is already for back. // Pass the previous primary value into transient. val transientDestination = checkNotNull(lastPrimaryDestination) { @@ -72,8 +59,8 @@ inline fun } val paneMapping = previousTransform(transientDestination) val transient = paneMapping[ThreePane.Primary] - return previousMapping + (ThreePane.TransientPrimary to transient) + previousMapping + (ThreePane.TransientPrimary to transient) } - } + ) } diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/Transforms.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/Transforms.kt index 50ad4f8..a7078be 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/Transforms.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/transforms/Transforms.kt @@ -3,20 +3,50 @@ package com.tunjid.treenav.compose.transforms import androidx.compose.runtime.Composable import com.tunjid.treenav.Node import com.tunjid.treenav.compose.PaneScope +import com.tunjid.treenav.compose.MultiPaneDisplay +import com.tunjid.treenav.compose.MultiPaneDisplayState +/** + * Provides APIs for adjusting what is presented in a [MultiPaneDisplay]. + */ +sealed interface Transform -interface Transform - +/** + * A [Transform] that allows for changing the current [Destination] in the [MultiPaneDisplay] + * sees without actually modifying the backing [NavigationState]. + */ fun interface DestinationTransform : Transform { + + /** + * Given a [NavigationState], provide the current [Destination] to show. The [Destination] + * returned must already exist in the back stack of the [MultiPaneDisplayState.navigationState]. + * + * @param navigationState the current navigation state. + * @param previousTransform a [Transform] that when invoked, returns the [Destination] that + * would have been shown pre-transform that can then be composed with new logic. + */ fun toDestination( navigationState: NavigationState, previousTransform: (NavigationState) -> Destination, ): Destination } +/** + * A [Transform] that allows for changing which [Destination] shows in which [Pane]. + */ fun interface PaneTransform : Transform { + + /** + * Given the current [Destination], provide what [Destination] to show in a [Pane]. + * Each [Destination] in the returned mapping must already exist in the + * back stack of the [MultiPaneDisplayState.navigationState]. + * + * @param destination the current [Destination] to display. + * @param previousTransform a [Transform] that when invoked, returns the pane to destination + * mapping for the current [Destination] pre-transform that can then be composed with new logic. + */ @Composable fun toPanesAndDestinations( destination: Destination, @@ -24,8 +54,21 @@ fun interface PaneTransform ): Map } +/** + * A [Transform] that allows for the rendering semantics of a [Destination] in a given + * [PaneScope]. + */ fun interface RenderTransform : Transform { + + /** + * Given the current [Destination], and its [PaneScope], compose additional presentation + * logic. + * + * @param destination the current [Destination] to display in the provided [PaneScope]. + * @param previousTransform a [Transform] that when invoked, renders the [Destination] + * for the [PaneScope ]pre-transform that can then be composed with new logic. + */ @Composable fun PaneScope.Render( destination: Destination, @@ -33,3 +76,31 @@ fun interface RenderTransform ) } +internal class CompoundTransform( + destinationTransform: DestinationTransform?, + paneTransform: PaneTransform?, + renderTransform: RenderTransform?, +) : Transform { + val transforms = listOfNotNull( + destinationTransform, + paneTransform, + renderTransform, + ) +} + +/** + * Creates a transform that an aggregation of the transforms provided to it. + * + * @see DestinationTransform + * @see PaneTransform + * @see RenderTransform + */ +fun compoundTransform( + destinationTransform: DestinationTransform? = null, + paneTransform: PaneTransform? = null, + renderTransform: RenderTransform? = null, +): Transform = CompoundTransform( + destinationTransform = destinationTransform, + paneTransform = paneTransform, + renderTransform = renderTransform, +) \ No newline at end of file From cb7538acf5ff1b50b833bd4d05f005a5aa2806a0 Mon Sep 17 00:00:00 2001 From: Adetunji Dahunsi Date: Wed, 12 Feb 2025 14:47:41 -0500 Subject: [PATCH 11/11] Update more docs --- ...iveBackTransform.kt => BackPreviewTransform.kt} | 14 +++++++------- .../kotlin/com/tunjid/demo/common/ui/DemoApp.kt | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) rename library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/{PredictiveBackTransform.kt => BackPreviewTransform.kt} (85%) diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt similarity index 85% rename from library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt rename to library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt index 4005512..3651a30 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/PredictiveBackTransform.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/transforms/BackPreviewTransform.kt @@ -26,18 +26,18 @@ import com.tunjid.treenav.compose.transforms.Transform import com.tunjid.treenav.compose.transforms.compoundTransform /** - * An [Transform] that moves the destination in a [ThreePane.Primary] pane, to - * to the [ThreePane.TransientPrimary] pane when a predictive back gesture is in progress. + * An [Transform] that moves the current [Destination] in a [ThreePane.Primary] pane, to + * to the [ThreePane.TransientPrimary] pane when "back" is being previewed. * * @param isPreviewingBack provides the state of the predictive back gesture. * True if the gesture is ongoing. - * @param backPreviewTransform provides the [NavigationState] if the predictive back gesture - * were to be completed. + * @param navigationStateBackTransform provides the [NavigationState] if the app were to + * go "back". */ inline fun - predictiveBackTransform( + backPreviewTransform( isPreviewingBack: State, - crossinline backPreviewTransform: NavigationState.() -> NavigationState, + crossinline navigationStateBackTransform: NavigationState.() -> NavigationState, ): Transform { var lastPrimaryDestination by mutableStateOf(null) @@ -45,7 +45,7 @@ inline fun destinationTransform = { navigationState, previousTransform -> val previousDestination = previousTransform(navigationState) lastPrimaryDestination = previousDestination - if (isPreviewingBack.value) previousTransform(navigationState.backPreviewTransform()) + if (isPreviewingBack.value) previousTransform(navigationState.navigationStateBackTransform()) else previousDestination }, paneTransform = paneTransform@{ destination, previousTransform -> 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 9aff10a..c536f14 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 @@ -83,7 +83,7 @@ import com.tunjid.treenav.compose.MultiPaneDisplayScope import com.tunjid.treenav.compose.MultiPaneDisplayState import com.tunjid.treenav.compose.moveablesharedelement.MovableSharedElementHostState import com.tunjid.treenav.compose.threepane.ThreePane -import com.tunjid.treenav.compose.threepane.transforms.predictiveBackTransform +import com.tunjid.treenav.compose.threepane.transforms.backPreviewTransform import com.tunjid.treenav.compose.threepane.transforms.threePanedAdaptiveTransform import com.tunjid.treenav.compose.threepane.transforms.threePanedMovableSharedElementTransform import com.tunjid.treenav.compose.transforms.Transform @@ -141,13 +141,13 @@ fun App( } } ), - predictiveBackTransform( + backPreviewTransform( isPreviewingBack = remember { derivedStateOf { appState.isPreviewingBack } }, - backPreviewTransform = MultiStackNav::pop, + navigationStateBackTransform = MultiStackNav::pop, ), threePanedMovableSharedElementTransform( movableSharedElementHostState = movableSharedElementHostState