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 eeba64a..ce1c4ea 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 @@ -16,7 +16,6 @@ package com.tunjid.treenav.compose -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform import androidx.compose.animation.EnterTransition @@ -32,17 +31,12 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.saveable.SaveableStateHolder import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.staticCompositionLocalOf 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 import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.children +import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.destination import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.id import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.paneEnterTransition import com.tunjid.treenav.compose.MultiPaneDisplayState.Companion.paneExitTransition @@ -55,6 +49,7 @@ import com.tunjid.treenav.compose.navigation3.ui.NavigationEventHandler import com.tunjid.treenav.compose.navigation3.ui.Scene import com.tunjid.treenav.compose.navigation3.ui.SceneStrategy import com.tunjid.treenav.compose.navigation3.ui.rememberSceneSetupNavEntryDecorator +import com.tunjid.treenav.compose.panedecorators.PaneDecorator import kotlinx.coroutines.CancellationException /** @@ -63,42 +58,44 @@ import kotlinx.coroutines.CancellationException @Stable interface MultiPaneDisplayScope { + /** + * All possible panes in the [MultiPaneDisplayScope]. + */ val panes: Collection + /** + * Renders the given [Destination] in the provided [Pane]. + */ @Composable fun Destination( pane: Pane, ) + /** + * Provides the set of adaptations in the provided [Pane]. + */ fun adaptationsIn( pane: Pane, ): Set + /** + * Returns the [Destination] in the provided [Pane]. + */ fun destinationIn( pane: Pane, ): Destination? } /** - * A Display that provides the following for each - * navigation [Destination] that shows up in its panes: + * A Display that adapts the [MultiPaneDisplayState.navigationState] to + * the [MultiPaneDisplayState.panes] available depending on the [PaneDecorator]s the + * [MultiPaneDisplayState] has been configured with. * - * - 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.destinationIn] in the visible [Pane]. * * @param state the driving [MultiPaneDisplayState] that applies adaptive semantics and - * transforms for each navigation destination shown in the [MultiPaneDisplay]. + * decorators for each navigation destination shown in the [MultiPaneDisplay]. + * @param modifier optional [Modifier] for the display. + * @param content the content that should be displayed the receiving [MultiPaneDisplayScope]. */ @Composable fun MultiPaneDisplay( @@ -268,7 +265,7 @@ private class MultiPanePaneSceneStrategy @@ -287,8 +284,8 @@ private class MultiPanePaneSceneStrategy( - override val entries: List>, override val previousEntries: List>, + private val eligibleSceneEntries: List>, private val sceneKey: MultiPaneSceneKey, private val destination: Destination, private val slots: Set, @@ -304,14 +301,28 @@ private class MultiPaneDisplayScene( @Stable val multiPaneDisplayScope = PaneDestinationMultiPaneDisplayScope( panedNavigationState = panedNavigationState, - entries = entries, + currentEntries = ::entries, backStatus = backStatus, ) override val key: Any = sceneKey - override val content: @Composable () -> Unit = { + override val entries: List> + get() = when { + // Filtering of duplicates is already handled in NavDisplay + sceneKey.isPreviewingBack -> eligibleSceneEntries + // Since the display may adapt, the actual entries to show are a subset of all eligible + // entries that can show. + // This is so destinations animating out are shown by the SceneSetupNavEntryDecorator. + // Otherwise, they will be removed immediately and not animate. + else -> panedNavigationState.value.let { state -> + eligibleSceneEntries.filter { navEntry -> + state.paneFor(navEntry.destination()) != null + } + } + } + override val content: @Composable () -> Unit = { currentPanedNavigationState.rememberUpdatedPanedNavigationState( backStackIds = sceneKey.ids, panesToDestinations = panesToDestinations(destination), @@ -328,7 +339,7 @@ private class MultiPaneDisplayScene( @Stable class PaneDestinationMultiPaneDisplayScope( panedNavigationState: State>, - private val entries: List>, + private val currentEntries: () -> List>, private val backStatus: () -> BackStatus, ) : MultiPaneDisplayScope { @@ -340,7 +351,7 @@ private class MultiPaneDisplayScene( @Composable override fun Destination(pane: Pane) { val id = panedNavigationState.destinationFor(pane)?.id - val entry = entries.firstOrNull { it.id == id } ?: return + val entry = currentEntries().firstOrNull { it.id == id } ?: return val paneState = remember(panedNavigationState.identityHash()) { panedNavigationState.slotFor(pane)?.let(panedNavigationState::paneStateFor) @@ -408,6 +419,9 @@ private fun MultiPaneDisplayState SlotBasedPanedNavigationState.rememberUpdatedPanedNavigationState( backStackIds: List, @@ -429,6 +443,12 @@ private fun SlotBasedPanedNavigationState, val isPreviewingBack: Boolean, @@ -448,6 +468,10 @@ internal class MultiPaneSceneKey( override fun hashCode(): Int { return idsHash } + + override fun toString(): String { + return "MultiPaneSceneKey(ids = $ids, isPreviewingBack = $isPreviewingBack)" + } } internal sealed class BackStatus { 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 eec6320..7503e44 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,10 +49,6 @@ internal data class SlotBasedPanedNavigationState( * A set of node ids that may be returned to. */ val backStackIds: List, - /** - * A set of node ids that are animating out. - */ - val destinationIdsAnimatingOut: Set, ) { companion object { internal fun initial( @@ -65,7 +61,6 @@ internal data class SlotBasedPanedNavigationState( keySelector = Slot::toString ), backStackIds = emptyList(), - destinationIdsAnimatingOut = emptySet(), previousPanesToDestinations = emptyMap(), ) } @@ -90,10 +85,10 @@ internal data class SlotBasedPanedNavigationState( panesToDestinations[pane]?.id ] - private fun paneFor( - node: Node, - ): Pane? = panesToDestinations.firstNotNullOfOrNull { (pane, paneRoute) -> - if (paneRoute?.id == node.id) pane else null + internal fun paneFor( + destination: Node, + ): Pane? = panesToDestinations.firstNotNullOfOrNull { (pane, paneDestination) -> + if (paneDestination?.id == destination.id) pane else null } private fun destinationFor( @@ -211,7 +206,6 @@ internal fun SlotBasedPanedNavigationState Unit, modifier: Modifier = Modifier, ) { @@ -46,7 +47,7 @@ fun AvatarScreen( .fillMaxSize() ) { val profileName = state.profileName ?: state.profile?.name ?: "" - paneScaffoldState.updatedMovableSharedElementOf( + paneScaffoldState.updatedMovableStickySharedElementOf( sharedContentState = paneScaffoldState.rememberSharedContentState( key = "${state.roomName}-$profileName-profile" ), diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt index baa107f..96f98e9 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/chat/ChatScreen.kt @@ -56,6 +56,7 @@ import com.tunjid.demo.common.ui.SampleTopAppBar import com.tunjid.demo.common.ui.data.Message import com.tunjid.demo.common.ui.data.Profile import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf +import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableStickySharedElementOf import com.tunjid.treenav.compose.threepane.ThreePane import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone @@ -225,7 +226,7 @@ fun Message( } }, ) { - paneScaffoldState.updatedMovableSharedElementOf( + paneScaffoldState.updatedMovableStickySharedElementOf( sharedContentState = paneScaffoldState.rememberSharedContentState( key = "$roomName-${item.sender.name}-profile" ), diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt index 0feca02..773fd39 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/profile/ProfileScreen.kt @@ -43,7 +43,7 @@ import com.tunjid.demo.common.ui.ProfilePhoto import com.tunjid.demo.common.ui.ProfilePhotoArgs import com.tunjid.demo.common.ui.SampleTopAppBar import com.tunjid.demo.common.ui.rememberAppBarCollapsingHeaderState -import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf +import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableStickySharedElementOf import kotlin.math.roundToInt @Composable @@ -127,7 +127,7 @@ private fun ProfilePhoto( ) { val profileName = state.profileName ?: state.profile?.name if (profileName != null) { - paneScaffoldState.updatedMovableSharedElementOf( + paneScaffoldState.updatedMovableStickySharedElementOf( sharedContentState = paneScaffoldState.rememberSharedContentState( key = "${state.roomName}-$profileName-profile" ),