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

Add SplitPaneState #45

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 4 commits into from
Jul 10, 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 @@ -125,9 +125,6 @@ private class ThreePaneMovableSharedElementScope<Destination : Node>(
override val inPredictiveBack: Boolean
get() = delegate.paneScope.inPredictiveBack

override val metadata: Map<String, Any>
get() = delegate.paneScope.metadata

@OptIn(ExperimentalSharedTransitionApi::class)
override fun <T> movableSharedElementOf(
sharedContentState: SharedTransitionScope.SharedContentState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,6 @@ private class MultiPaneDisplayScene<Pane, Destination : Node>(
val scope = remember {
AnimatedPaneScope(
backStatus = backStatus,
metadata = entry.metadata,
paneState = paneState,
animatedContentScope = animatedContentScope,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,6 @@ interface PaneScope<Pane, Destination : Node> : AnimatedVisibilityScope {
* Whether or not a predictive back gesture is in progress
*/
val inPredictiveBack: Boolean

/**
* Metadata associated with this [PaneScope] at the creation of its [PaneEntry].
*/
val metadata: Map<String, Any>
}

/**
Expand All @@ -65,7 +60,6 @@ interface PaneScope<Pane, Destination : Node> : AnimatedVisibilityScope {
@Stable
internal class AnimatedPaneScope<Pane, Destination : Node>(
val backStatus: () -> BackStatus,
override val metadata: Map<String, Any>,
paneState: PaneState<Pane, Destination>,
animatedContentScope: AnimatedContentScope,
) : PaneScope<Pane, Destination>, AnimatedVisibilityScope by animatedContentScope {
Expand Down
163 changes: 101 additions & 62 deletions sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,23 @@ import androidx.compose.runtime.CompositionLocalProvider
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.movableContentOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -110,7 +113,11 @@ fun App(
sharedTransitionScope = this
)
}

val windowWidth = rememberUpdatedState(
with(density) {
LocalWindowInfo.current.containerSize.width.toDp()
}
)
val displayState = appState.rememberMultiPaneDisplayState(
remember {
listOf(
Expand All @@ -121,9 +128,7 @@ fun App(
tertiaryPaneBreakPoint = mutableStateOf(
TertiaryPaneMinWidthBreakpointDp
),
windowWidthState = derivedStateOf {
appState.splitLayoutState.size
}
windowWidthState = windowWidth
),
threePaneMovableSharedElementDecorator(
movableSharedElementHostState = movableSharedElementHostState
Expand All @@ -137,42 +142,55 @@ fun App(
modifier = Modifier
.fillMaxSize(),
) {
appState.displayScope = this
appState.splitLayoutState.visibleCount = appState.filteredPaneOrder.size
SplitLayout(
state = appState.splitLayoutState,
modifier = Modifier
.fillMaxSize(),
itemSeparators = { paneIndex, offset ->
PaneSeparator(
splitLayoutState = appState.splitLayoutState,
interactionSource = appState.paneInteractionSourceAt(paneIndex),
index = paneIndex,
density = density,
xOffset = offset,
)
},
itemContent = { index ->
Destination(appState.filteredPaneOrder[index])
}
)
}
val splitPaneDisplayScope = remember {
SplitPaneDisplayScope(
displayScope = this,
windowWidth = windowWidth,
)
}.also {
it.update(
displayScope = this,
)
}

NavigationEventHandler(
enabled = displayState::canPop,
passThrough = true,
) { progress ->
try {
progress.collect { event ->
appState.backPreviewState.progress = event.progress
appState.backPreviewState.atStart =
event.swipeEdge == NavigationEvent.EDGE_LEFT
appState.backPreviewState.pointerOffset =
Offset(event.touchX, event.touchY).round()
CompositionLocalProvider(
LocalSplitPaneDisplayScope provides splitPaneDisplayScope
) {
SplitLayout(
state = splitPaneDisplayScope.splitLayoutState,
modifier = Modifier
.fillMaxSize(),
itemSeparators = { paneIndex, offset ->
PaneSeparator(
splitLayoutState = splitPaneDisplayScope.splitLayoutState,
interactionSource = appState.paneInteractionSourceAt(paneIndex),
index = paneIndex,
density = density,
xOffset = offset,
)
},
itemContent = { index ->
Destination(splitPaneDisplayScope.filteredPaneOrder[index])
}
)
}

NavigationEventHandler(
enabled = displayState::canPop,
passThrough = true,
) { progress ->
try {
progress.collect { event ->
appState.backPreviewState.progress = event.progress
appState.backPreviewState.atStart =
event.swipeEdge == NavigationEvent.EDGE_LEFT
appState.backPreviewState.pointerOffset =
Offset(event.touchX, event.touchY).round()
}
appState.backPreviewState.progress = 0f
} finally {
appState.backPreviewState.progress = 0f
}
appState.backPreviewState.progress = 0f
} finally {
appState.backPreviewState.progress = 0f
}
}
}
Expand Down Expand Up @@ -251,30 +269,12 @@ class AppState(
)

private val paneInteractionSourceList = mutableStateListOf<MutableInteractionSource>()
private val paneRenderOrder = listOf(
ThreePane.Tertiary,
ThreePane.Secondary,
ThreePane.Primary,
)


val currentNavigation by navigationState
val backPreviewState = BackPreviewState()
val splitLayoutState = SplitLayoutState(
orientation = Orientation.Horizontal,
maxCount = paneRenderOrder.size,
minSize = 10.dp,
keyAtIndex = { index ->
val indexDiff = paneRenderOrder.size - visibleCount
paneRenderOrder[index + indexDiff]
}
)
internal val dragToPopState = DragToPopState()

internal val isMediumScreenWidthOrWider get() = splitLayoutState.size >= SecondaryPaneMinWidthBreakpointDp

internal var displayScope by mutableStateOf<MultiPaneDisplayScope<ThreePane, SampleDestination>?>(
null
)
internal val dragToPopState = DragToPopState()

internal val movableNavigationBar =
movableContentOf<Modifier> { modifier ->
Expand All @@ -286,10 +286,6 @@ class AppState(
PaneNavigationRail(modifier)
}

val filteredPaneOrder: List<ThreePane> by derivedStateOf {
paneRenderOrder.filter { displayScope?.destinationIn(it) != null }
}

fun setTab(destination: SampleDestination.NavTabs) {
navigationRepository.navigate {
if (it.currentIndex == destination.ordinal) it.popToRoot()
Expand Down Expand Up @@ -350,10 +346,53 @@ class AppState(
}
}

@Stable
internal class SplitPaneDisplayScope(
displayScope: MultiPaneDisplayScope<ThreePane, SampleDestination>,
private val windowWidth: State<Dp>,
) {

private var displayScope by mutableStateOf(displayScope)

internal val filteredPaneOrder by derivedStateOf {
PaneRenderOrder.filter { displayScope.destinationIn(it) != null }
}

internal val splitLayoutState = SplitLayoutState(
orientation = Orientation.Horizontal,
maxCount = filteredPaneOrder.size,
initialVisibleCount = filteredPaneOrder.size,
minSize = 10.dp,
keyAtIndex = { index ->
filteredPaneOrder[index]
}
)

internal val isMediumScreenWidthOrWider
get() = windowWidth.value >= SecondaryPaneMinWidthBreakpointDp

fun update(
displayScope: MultiPaneDisplayScope<ThreePane, SampleDestination>
) {
this.displayScope = displayScope
splitLayoutState.visibleCount = filteredPaneOrder.size
}
}

internal val LocalSplitPaneDisplayScope = staticCompositionLocalOf<SplitPaneDisplayScope> {
TODO()
}

internal val LocalAppState = staticCompositionLocalOf<AppState> {
TODO()
}

private val PaneRenderOrder = listOf(
ThreePane.Tertiary,
ThreePane.Secondary,
ThreePane.Primary,
)

private val PaneSeparatorActiveWidthDp = 56.dp
private val PaneSeparatorTouchTargetWidthDp = 16.dp
internal val SecondaryPaneMinWidthBreakpointDp = 600.dp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntSize
import androidx.compose.ui.zIndex
import com.tunjid.demo.common.ui.data.SampleDestination
import com.tunjid.treenav.compose.PaneScope
Expand All @@ -59,24 +55,23 @@ import kotlinx.coroutines.flow.filterNotNull

@Stable
class PaneScaffoldState internal constructor(
private val appState: AppState,
private val splitPaneDisplayScope: SplitPaneDisplayScope,
threePaneMovableElementSharedTransitionScope: ThreePaneMovableElementSharedTransitionScope<SampleDestination>,
) : ThreePaneMovableElementSharedTransitionScope<SampleDestination> by threePaneMovableElementSharedTransitionScope {

internal val canShowNavigationBar get() = !appState.isMediumScreenWidthOrWider
internal val canShowNavigationBar get() = !splitPaneDisplayScope.isMediumScreenWidthOrWider

internal val canShowNavigationRail
get() = appState.filteredPaneOrder.firstOrNull() == paneState.pane
&& appState.isMediumScreenWidthOrWider
get() = splitPaneDisplayScope.filteredPaneOrder.firstOrNull() == paneState.pane
&& splitPaneDisplayScope.isMediumScreenWidthOrWider

val canUseMovableNavigationBar
internal val canUseMovableNavigationBar
get() = canShowNavigationBar && isActive && paneState.pane == ThreePane.Primary

val canUseMovableNavigationRail
internal val canUseMovableNavigationRail
get() = canShowNavigationRail && isActive

internal var scaffoldTargetSize by mutableStateOf(IntSize.Zero)
internal var scaffoldCurrentSize by mutableStateOf(IntSize.Zero)
internal val hasSiblings get() = splitPaneDisplayScope.filteredPaneOrder.size > 1

internal val defaultContainerColor: Color
@Composable get() {
Expand All @@ -94,12 +89,12 @@ class PaneScaffoldState internal constructor(

@Composable
fun PaneScope<ThreePane, SampleDestination>.rememberPaneScaffoldState(): PaneScaffoldState {
val appState = LocalAppState.current
val splitPaneDisplayScope = LocalSplitPaneDisplayScope.current
val paneMovableElementSharedTransitionScope =
rememberThreePaneMovableElementSharedTransitionScope()
return remember(appState) {
return remember(splitPaneDisplayScope, paneMovableElementSharedTransitionScope) {
PaneScaffoldState(
appState = appState,
splitPaneDisplayScope = splitPaneDisplayScope,
threePaneMovableElementSharedTransitionScope = paneMovableElementSharedTransitionScope,
)
}
Expand Down Expand Up @@ -144,11 +139,8 @@ fun PaneScaffoldState.PaneScaffold(
}
)
.padding(
horizontal = if (appState.filteredPaneOrder.size > 1) 8.dp else 0.dp
)
.onSizeChanged {
scaffoldCurrentSize = it
},
horizontal = if (hasSiblings) 8.dp else 0.dp
),
containerColor = containerColor,
topBar = {
topBar()
Expand Down Expand Up @@ -216,10 +208,7 @@ private inline fun PaneNavigationRailScaffold(
private fun scaffoldBoundsTransform(
paneScaffoldState: PaneScaffoldState,
canAnimatePane: () -> Boolean,
): BoundsTransform = BoundsTransform { _, targetBounds ->
paneScaffoldState.scaffoldTargetSize =
targetBounds.size.roundToIntSize()

): BoundsTransform = BoundsTransform { _, _ ->
when (paneScaffoldState.paneState.pane) {
ThreePane.Primary,
ThreePane.Secondary,
Expand Down