Implement AppScaffold back-gesture.

This commit is contained in:
Alex Hart
2025-10-03 15:43:08 -03:00
committed by Michelle Tang
parent d36a4232be
commit 1d403d3dee
6 changed files with 404 additions and 65 deletions

View File

@@ -40,7 +40,6 @@ import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.PaneExpansionAnchor
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -159,8 +158,9 @@ import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.viewModel
import org.thoughtcrime.securesms.window.AppPaneDragHandle
import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.AppScaffoldNavigator
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
import org.thoughtcrime.securesms.window.rememberThreePaneScaffoldNavigatorDelegate
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider, Material3OnScrollHelperBinder, ConversationListFragment.Callback, CallLogFragment.Callback {
@@ -585,8 +585,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
windowSizeClass: WindowSizeClass,
contentLayoutData: MainContentLayoutData,
maxWidth: Dp
): ThreePaneScaffoldNavigator<Any> {
val scaffoldNavigator = rememberAppScaffoldNavigator(
): AppScaffoldNavigator<Any> {
val scaffoldNavigator = rememberThreePaneScaffoldNavigatorDelegate(
isSplitPane = windowSizeClass.isSplitPane(),
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)

View File

@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.megaphone.Megaphone
import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.window.AppScaffoldNavigator
import org.thoughtcrime.securesms.window.WindowSizeClass
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@@ -38,7 +39,7 @@ class MainNavigationViewModel(
) : ViewModel(), MainNavigationRouter {
private val megaphoneRepository = AppDependencies.megaphoneRepository
private var navigator: ThreePaneScaffoldNavigator<Any>? = null
private var navigator: AppScaffoldNavigator<Any>? = null
private var navigatorScope: CoroutineScope? = null
private var goToLegacyDetailLocation: ((MainNavigationDetailLocation) -> Unit)? = null
@@ -104,7 +105,7 @@ class MainNavigationViewModel(
* Sets the navigator on the view-model. This wraps the given navigator in our own delegating implementation
* such that we can react to navigateTo/Back signals and maintain proper state for internalDetailLocation.
*/
fun wrapNavigator(composeScope: CoroutineScope, threePaneScaffoldNavigator: ThreePaneScaffoldNavigator<Any>, goToLegacyDetailLocation: (MainNavigationDetailLocation) -> Unit): ThreePaneScaffoldNavigator<Any> {
fun wrapNavigator(composeScope: CoroutineScope, threePaneScaffoldNavigator: ThreePaneScaffoldNavigator<Any>, goToLegacyDetailLocation: (MainNavigationDetailLocation) -> Unit): AppScaffoldNavigator<Any> {
this.goToLegacyDetailLocation = goToLegacyDetailLocation
this.navigatorScope = composeScope
this.navigator = Nav(threePaneScaffoldNavigator)
@@ -269,9 +270,9 @@ class MainNavigationViewModel(
* Ensures that when the user navigates back from the PRIMARY to SECONDARY pane, we lock our pane until they choose another primary
* piece of content via [goTo].
*/
private inner class Nav<T>(private val delegate: ThreePaneScaffoldNavigator<T>) : ThreePaneScaffoldNavigator<T> by delegate {
private inner class Nav<T>(delegate: ThreePaneScaffoldNavigator<T>) : AppScaffoldNavigator<T>(delegate) {
override suspend fun seekBack(backNavigationBehavior: BackNavigationBehavior, fraction: Float) {
delegate.seekBack(backNavigationBehavior, fraction)
super.seekBack(backNavigationBehavior, fraction)
if (fraction == 0f) {
lockPaneToSecondary = true

View File

@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
@@ -26,26 +25,19 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.PaneExpansionState
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.defaultDragHandleSemantics
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.window.core.ExperimentalWindowCoreApi
@@ -208,12 +200,13 @@ enum class WindowSizeClass(
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun AppScaffold(
navigator: ThreePaneScaffoldNavigator<Any> = rememberListDetailPaneScaffoldNavigator<Any>(),
navigator: AppScaffoldNavigator<Any>,
detailContent: @Composable () -> Unit = {},
navRailContent: @Composable () -> Unit = {},
bottomNavContent: @Composable () -> Unit = {},
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(),
paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null,
animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default,
listContent: @Composable () -> Unit
) {
val isForcedCompact = WindowSizeClass.checkForcedCompact()
@@ -231,32 +224,25 @@ fun AppScaffold(
}
val minPaneWidth = navigator.scaffoldDirective.defaultPanePreferredWidth
val navigationState = navigator.state
NavigableListDetailPaneScaffold(
navigator = navigator,
listPane = {
val offset by animateDp(
targetWhenHiding = {
(-48).dp
},
targetWhenShowing = {
0.dp
}
)
val alpha by animateFloat {
1f
val animationState = with(animatorFactory) {
this@NavigableListDetailPaneScaffold.getListAnimationState(navigationState)
}
AnimatedPane(
enterTransition = EnterTransition.None,
exitTransition = ExitTransition.None,
modifier = Modifier.zIndex(0f)
modifier = Modifier
.zIndex(0f)
.then(animationState.parentModifier)
) {
Box(
modifier = Modifier
.alpha(alpha)
.offset(x = offset)
.then(animationState.toModifier())
.clipToBounds()
.layout { measurable, constraints ->
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
@@ -284,28 +270,20 @@ fun AppScaffold(
}
},
detailPane = {
val offset by animateDp(
targetWhenHiding = {
48.dp
},
targetWhenShowing = {
0.dp
}
)
val alpha by animateFloat {
1f
val animationState = with(animatorFactory) {
this@NavigableListDetailPaneScaffold.getDetailAnimationState(navigationState)
}
AnimatedPane(
enterTransition = EnterTransition.None,
exitTransition = ExitTransition.None,
modifier = Modifier.zIndex(1f)
modifier = Modifier
.zIndex(1f)
.then(animationState.parentModifier)
) {
Box(
modifier = Modifier
.alpha(alpha)
.offset(x = offset)
.then(animationState.toModifier())
.clipToBounds()
.layout { measurable, constraints ->
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
@@ -447,21 +425,3 @@ fun ThreePaneScaffoldScope.AppPaneDragHandle(
)
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun rememberAppScaffoldNavigator(
isSplitPane: Boolean,
horizontalPartitionSpacerSize: Dp,
defaultPanePreferredWidth: Dp
): ThreePaneScaffoldNavigator<Any> {
return rememberListDetailPaneScaffoldNavigator<Any>(
scaffoldDirective = calculatePaneScaffoldDirective(
currentWindowAdaptiveInfo()
).copy(
maxHorizontalPartitions = if (isSplitPane) 2 else 1,
horizontalPartitionSpacerSize = horizontalPartitionSpacerSize,
defaultPanePreferredWidth = defaultPanePreferredWidth
)
)
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.window
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Produces modifier that can be composed into another modifier chain.
* This object allows us to store "latest state" as we transition.
*
* @param parentModifier This modifier is applied to the [androidx.compose.material3.adaptive.layout.AnimatedPane] itself,
* allowing additional customization like overlays without
* the need for additional composables.
*/
data class AppScaffoldAnimationState(
val navigationState: AppScaffoldNavigator.NavigationState,
val alpha: Float = 1f,
val scale: Float = 1f,
val offset: Dp = 0.dp,
val clipShape: Shape = RoundedCornerShape(0.dp),
val parentModifier: Modifier = Modifier.Companion
) {
fun toModifier(): Modifier {
return Modifier
.alpha(alpha)
.scale(scale)
.offset(offset)
.clip(clipShape)
}
}
/**
* Allows for the customization of the AppScaffold Animators.
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
abstract class AppScaffoldAnimationStateFactory {
object Default : AppScaffoldAnimationStateFactory()
protected var latestListSeekState: AppScaffoldAnimationState = AppScaffoldAnimationState(AppScaffoldNavigator.NavigationState.SEEK)
protected var latestDetailSeekState: AppScaffoldAnimationState = AppScaffoldAnimationState(AppScaffoldNavigator.NavigationState.SEEK)
@Composable
fun ThreePaneScaffoldPaneScope.getListAnimationState(navigationState: AppScaffoldNavigator.NavigationState): AppScaffoldAnimationState {
return when (navigationState) {
AppScaffoldNavigator.NavigationState.INIT -> defaultListInitAnimationState()
AppScaffoldNavigator.NavigationState.SEEK -> defaultListSeekAnimationState().also {
latestListSeekState = it
}
AppScaffoldNavigator.NavigationState.RELEASE -> defaultListReleaseAnimationState(latestListSeekState)
}
}
@Composable
fun ThreePaneScaffoldPaneScope.getDetailAnimationState(navigationState: AppScaffoldNavigator.NavigationState): AppScaffoldAnimationState {
return when (navigationState) {
AppScaffoldNavigator.NavigationState.INIT -> defaultDetailInitAnimationState()
AppScaffoldNavigator.NavigationState.SEEK -> defaultDetailSeekAnimationState().also {
latestDetailSeekState = it
}
AppScaffoldNavigator.NavigationState.RELEASE -> defaultDetailReleaseAnimationState(latestDetailSeekState)
}
}
}

View File

@@ -6,27 +6,49 @@
package org.thoughtcrime.securesms.window
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.coerceAtMost
import androidx.compose.ui.unit.dp
private const val SEEK_DAMPING_RATIO = Spring.DampingRatioNoBouncy
private const val SEEK_STIFFNESS = Spring.StiffnessMedium
/**
* Default animation spec for back gesture seeking.
*/
fun <T> appScaffoldSeekSpring(): FiniteAnimationSpec<T> = spring(
dampingRatio = SEEK_DAMPING_RATIO,
stiffness = SEEK_STIFFNESS
)
private val easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1f)
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.animateDp(
transitionSpec: @Composable Transition.Segment<*>.() -> FiniteAnimationSpec<Dp> = { tween(durationMillis = 200, easing = easing) },
targetWhenHiding: () -> Dp = { 0.dp },
targetWhenShowing: () -> Dp
): State<Dp> {
return scaffoldStateTransition.animateDp(
transitionSpec = { tween(durationMillis = 200, easing = easing) }
transitionSpec = transitionSpec
) {
val isHiding = it[paneRole] == PaneAdaptedValue.Hidden
@@ -41,11 +63,12 @@ fun ThreePaneScaffoldPaneScope.animateDp(
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.animateFloat(
transitionSpec: @Composable Transition.Segment<*>.() -> FiniteAnimationSpec<Float> = { tween(durationMillis = 200, easing = easing) },
targetWhenHiding: () -> Float = { 0f },
targetWhenShowing: () -> Float
): State<Float> {
return scaffoldStateTransition.animateFloat(
transitionSpec = { tween(durationMillis = 200, easing = easing) }
transitionSpec = transitionSpec
) {
val isHiding = it[paneRole] == PaneAdaptedValue.Hidden
@@ -56,3 +79,157 @@ fun ThreePaneScaffoldPaneScope.animateFloat(
}
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.defaultListInitAnimationState(): AppScaffoldAnimationState {
val offset by animateDp(
targetWhenHiding = {
(-48).dp
},
targetWhenShowing = {
0.dp
}
)
val alpha by animateFloat {
1f
}
return AppScaffoldAnimationState(
AppScaffoldNavigator.NavigationState.INIT,
alpha = alpha,
offset = offset
)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.defaultListSeekAnimationState(): AppScaffoldAnimationState {
val scale by animateFloat(
transitionSpec = {
appScaffoldSeekSpring()
},
targetWhenShowing = { 0.5f },
targetWhenHiding = { 1f }
)
val offset by animateDp(
transitionSpec = {
appScaffoldSeekSpring()
},
targetWhenHiding = { -(88.dp) },
targetWhenShowing = { 0.dp }
)
return AppScaffoldAnimationState(
navigationState = AppScaffoldNavigator.NavigationState.SEEK,
offset = offset,
scale = scale.coerceAtLeast(0.9f),
parentModifier = Modifier.drawWithContent {
drawContent()
drawRect(Color(0f, 0f, 0f, 0.2f))
}
)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.defaultListReleaseAnimationState(from: AppScaffoldAnimationState): AppScaffoldAnimationState {
val scale by animateFloat(
targetWhenHiding = { from.scale },
targetWhenShowing = { 1f }
)
val offset by animateDp(
targetWhenHiding = { from.offset },
targetWhenShowing = { 0.dp }
)
val alpha by animateFloat(
targetWhenHiding = { 0.2f },
targetWhenShowing = { 0f }
)
return AppScaffoldAnimationState(
navigationState = AppScaffoldNavigator.NavigationState.RELEASE,
scale = scale,
offset = offset,
parentModifier = Modifier.drawWithContent {
drawContent()
drawRect(Color(0f, 0f, 0f, alpha))
}
)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.defaultDetailInitAnimationState(): AppScaffoldAnimationState {
val offset by animateDp(
targetWhenHiding = {
48.dp
},
targetWhenShowing = {
0.dp
}
)
val alpha by animateFloat {
1f
}
return AppScaffoldAnimationState(
navigationState = AppScaffoldNavigator.NavigationState.INIT,
alpha = alpha,
offset = offset
)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.defaultDetailSeekAnimationState(): AppScaffoldAnimationState {
val scale by animateFloat(
transitionSpec = {
appScaffoldSeekSpring()
},
targetWhenShowing = { 1f },
targetWhenHiding = { 0.5f }
)
val offset by animateDp(
transitionSpec = {
appScaffoldSeekSpring()
},
targetWhenShowing = { 0.dp },
targetWhenHiding = { 88.dp }
)
val roundedCorners by animateDp(
transitionSpec = {
appScaffoldSeekSpring()
}
) { 1000.dp }
return AppScaffoldAnimationState(
navigationState = AppScaffoldNavigator.NavigationState.SEEK,
scale = scale.coerceAtLeast(0.9f),
offset = offset,
clipShape = RoundedCornerShape(roundedCorners.coerceAtMost(42.dp))
)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.defaultDetailReleaseAnimationState(from: AppScaffoldAnimationState): AppScaffoldAnimationState {
val alpha by animateFloat { 1f }
return AppScaffoldAnimationState(
navigationState = AppScaffoldNavigator.NavigationState.RELEASE,
scale = from.scale,
offset = from.offset,
clipShape = from.clipShape,
alpha = alpha
)
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.window
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.annotation.RememberInComposition
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.Dp
/**
* AppScaffoldNavigator wraps a delegate navigator (such as the value returned by [rememberThreePaneScaffoldNavigatorDelegate]
* and implements a state machine that will produce [NavigationState] to allow proper animation coordination.
*/
@Stable
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
open class AppScaffoldNavigator<T> @RememberInComposition constructor(private val delegate: ThreePaneScaffoldNavigator<T>) : ThreePaneScaffoldNavigator<T> by delegate {
var state: NavigationState by mutableStateOf(NavigationState.INIT)
private set
override suspend fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: T?) {
state = NavigationState.INIT
return delegate.navigateTo(pane, contentKey)
}
override suspend fun navigateBack(backNavigationBehavior: BackNavigationBehavior): Boolean {
if (state == NavigationState.SEEK) {
state = NavigationState.RELEASE
}
return delegate.navigateBack(backNavigationBehavior)
}
override suspend fun seekBack(backNavigationBehavior: BackNavigationBehavior, fraction: Float) {
if (fraction > 0f && state != NavigationState.SEEK) {
state = NavigationState.SEEK
}
return delegate.seekBack(backNavigationBehavior, fraction)
}
/**
* State machine which describes the current navigation state to help with animation coordination.
*/
enum class NavigationState {
/**
* We've navigated to a new pane. This animation is used for both immediate
* pane entry and exit (such as tapping a back button instead of using a
* gesture)
*/
INIT,
/**
* The user is performing a back gesture seek action.
*/
SEEK,
/**
* The user has let go of a seek and will go back.
*/
RELEASE
}
}
/**
* Sane default navigator. If you want to provide your own implementation
* of AppScaffoldNavigator, utilize the remember pattern here but use
* [rememberThreePaneScaffoldNavigatorDelegate] to get a delegate and hand off
* to your own subclass of [AppScaffoldNavigator]
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun rememberAppScaffoldNavigator(
isSplitPane: Boolean,
horizontalPartitionSpacerSize: Dp,
defaultPanePreferredWidth: Dp
): AppScaffoldNavigator<Any> {
val delegate = rememberThreePaneScaffoldNavigatorDelegate(
isSplitPane,
horizontalPartitionSpacerSize,
defaultPanePreferredWidth
)
return remember(delegate) { AppScaffoldNavigator(delegate) }
}
/**
* Produces a ThreePaneScaffoldNavigatorDelegate. Since the developer can
* further modify navigator behavior, this is best done using the delegate pattern.
* Use this to grab the initial delegate, and then either subclass or create an
* instance of [AppScaffoldNavigator] keyed to this delegate.
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun rememberThreePaneScaffoldNavigatorDelegate(
isSplitPane: Boolean,
horizontalPartitionSpacerSize: Dp,
defaultPanePreferredWidth: Dp
): ThreePaneScaffoldNavigator<Any> {
return rememberListDetailPaneScaffoldNavigator<Any>(
scaffoldDirective = calculatePaneScaffoldDirective(
currentWindowAdaptiveInfo()
).copy(
maxHorizontalPartitions = if (isSplitPane) 2 else 1,
horizontalPartitionSpacerSize = horizontalPartitionSpacerSize,
defaultPanePreferredWidth = defaultPanePreferredWidth
)
)
}