这是indexloc提供的服务,不要输入任何密码
Skip to content

Fix entries for MultiPaneDisplayScene #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

/**
Expand All @@ -63,42 +58,44 @@ import kotlinx.coroutines.CancellationException
@Stable
interface MultiPaneDisplayScope<Pane, Destination : Node> {

/**
* All possible panes in the [MultiPaneDisplayScope].
*/
val panes: Collection<Pane>

/**
* 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<Adaptation>

/**
* 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 <NavigationState : Node, Destination : Node, Pane> MultiPaneDisplay(
Expand Down Expand Up @@ -268,7 +265,7 @@ private class MultiPanePaneSceneStrategy<NavigationState : Node, Destination : N
panesToDestinations = state.destinationPanes,
onSceneDisposed = { scenes.remove(sceneKey) },
currentPanedNavigationState = panedNavigationState,
entries = entries.filter { it.id in activeIds },
eligibleSceneEntries = entries.filter { it.id in activeIds },
// Try to match up NavEntries to state using their id and children.
// Best case is O(n) where the backstack isn't shuffled.
previousEntries = poppedBackstack.map { poppedDestination ->
Expand All @@ -287,8 +284,8 @@ private class MultiPanePaneSceneStrategy<NavigationState : Node, Destination : N

@Stable
private class MultiPaneDisplayScene<Pane, Destination : Node>(
override val entries: List<NavEntry<Destination>>,
override val previousEntries: List<NavEntry<Destination>>,
private val eligibleSceneEntries: List<NavEntry<Destination>>,
private val sceneKey: MultiPaneSceneKey,
private val destination: Destination,
private val slots: Set<Slot>,
Expand All @@ -304,14 +301,28 @@ private class MultiPaneDisplayScene<Pane, Destination : Node>(
@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<NavEntry<Destination>>
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),
Expand All @@ -328,7 +339,7 @@ private class MultiPaneDisplayScene<Pane, Destination : Node>(
@Stable
class PaneDestinationMultiPaneDisplayScope<Pane, Destination : Node>(
panedNavigationState: State<SlotBasedPanedNavigationState<Pane, Destination>>,
private val entries: List<NavEntry<Destination>>,
private val currentEntries: () -> List<NavEntry<Destination>>,
private val backStatus: () -> BackStatus,
) : MultiPaneDisplayScope<Pane, Destination> {

Expand All @@ -340,7 +351,7 @@ private class MultiPaneDisplayScene<Pane, Destination : Node>(
@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)
Expand Down Expand Up @@ -408,6 +419,9 @@ private fun <NavigationState : Node> MultiPaneDisplayState<NavigationState, *, *
return state
}

/**
* Keep track of changes to the paned navigation state.
*/
@Composable
private fun <Destination : Node, Pane> SlotBasedPanedNavigationState<Pane, Destination>.rememberUpdatedPanedNavigationState(
backStackIds: List<String>,
Expand All @@ -429,6 +443,12 @@ private fun <Destination : Node, Pane> SlotBasedPanedNavigationState<Pane, Desti
}
}

/**
* Scene key with a flag if it was created for a predictive back gesture.
* NOTE: its equals and hashcode are completely independent of this predictive back flag.
* This is to let [NavDisplay] find the appropriate scene to go back to with this key. The
* flag is only used internally.
*/
internal class MultiPaneSceneKey(
val ids: List<String>,
val isPreviewingBack: Boolean,
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ internal data class SlotBasedPanedNavigationState<Pane, Destination : Node>(
* A set of node ids that may be returned to.
*/
val backStackIds: List<String>,
/**
* A set of node ids that are animating out.
*/
val destinationIdsAnimatingOut: Set<String>,
) {
companion object {
internal fun <Pane, Destination : Node> initial(
Expand All @@ -65,7 +61,6 @@ internal data class SlotBasedPanedNavigationState<Pane, Destination : Node>(
keySelector = Slot::toString
),
backStackIds = emptyList(),
destinationIdsAnimatingOut = emptySet(),
previousPanesToDestinations = emptyMap(),
)
}
Expand All @@ -90,10 +85,10 @@ internal data class SlotBasedPanedNavigationState<Pane, Destination : Node>(
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(
Expand Down Expand Up @@ -211,7 +206,6 @@ internal fun <Pane, Destination : Node> SlotBasedPanedNavigationState<Pane, Dest
destinationIdsToAdaptiveSlots = nodeIdsToAdaptiveSlots,
backStackIds = backStackIds,
panesToDestinations = panesToDestinations,
destinationIdsAnimatingOut = previous.destinationIdsAnimatingOut,
)

}
8 changes: 4 additions & 4 deletions libraryVersion.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# limitations under the License.
#
groupId=com.tunjid.treenav
treenav_version=0.0.40
strings_version=0.0.40
compose_version=0.0.40
compose-threepane_version=0.0.40
treenav_version=0.0.41
strings_version=0.0.41
compose_version=0.0.41
compose-threepane_version=0.0.41
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ import com.tunjid.demo.common.ui.PaneScaffoldState
import com.tunjid.demo.common.ui.ProfilePhoto
import com.tunjid.demo.common.ui.ProfilePhotoArgs
import com.tunjid.demo.common.ui.dragToPop
import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableSharedElementOf
import com.tunjid.treenav.compose.moveablesharedelement.updatedMovableStickySharedElementOf

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun AvatarScreen(
paneScaffoldState: PaneScaffoldState,
state: State,
@Suppress("UNUSED_PARAMETER")
onAction: (Action) -> Unit,
modifier: Modifier = Modifier,
) {
Expand All @@ -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"
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -225,7 +226,7 @@ fun Message(
}
},
) {
paneScaffoldState.updatedMovableSharedElementOf(
paneScaffoldState.updatedMovableStickySharedElementOf(
sharedContentState = paneScaffoldState.rememberSharedContentState(
key = "$roomName-${item.sender.name}-profile"
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
),
Expand Down