Skip to content
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 @@ -9,14 +9,10 @@ import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
Expand All @@ -29,18 +25,17 @@ import androidx.navigation3.scene.SinglePaneSceneStrategy
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.navigation.DeeplinkAction
import com.flipcash.app.core.navigation.asNavBarTab
import com.flipcash.app.core.navigation.LocalTabBarPadding
import com.flipcash.app.internal.ui.AppNavigationBar
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavBillOverlayEntryDecorator
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavBlockingOverlayEntryDecorator
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavMessagingEntryDecorator
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavTabBarInsetEntryDecorator
import com.getcode.navigation.AppNavHost
import com.getcode.navigation.core.CodeNavigator
import com.getcode.navigation.results.NavResultStateRegistry
import com.getcode.navigation.scenes.ModalBottomSheetSceneStrategy
import com.getcode.ui.components.bars.BarManager
import com.getcode.ui.core.measured
import com.getcode.ui.theme.CodeScaffold
import dev.theolm.rinku.DeepLink

@Composable
Expand Down Expand Up @@ -148,23 +143,19 @@ internal fun NewAppContent(
deepLink: () -> DeepLink?,
onPendingAction: (DeeplinkAction) -> Unit = {},
) {
// The v2 nav bar is a single persistent overlay at the app root (below), so tab switches stay
// seamless — one instance, sliding selection pill, one haze source. It's a bottom OVERLAY over the
// full-height nav content, so hiding it for a modal/bill never resizes the content. Space for it is
// reserved PER ENTRY by NavTabBarInsetEntryDecorator (tab homes only), which keeps the inset stable
// through a push instead of collapsing a global value mid-transition (which jumped the outgoing
// screen).
val hazeState = rememberHazeState()
// The tab bar is a bottom OVERLAY, not a scaffold bottomBar: the nav content stays full-height, so
// hiding the bar for a modal (or bill) never resizes it — which otherwise re-fanned the wallet's
// collapsing card stack — and content scrolls edge-to-edge under the frosted bar. Reserve space for
// it via LocalTabBarPadding, latched to the tallest measured height and only on tab homes, so a
// modal-driven hide doesn't shrink the inset.
var tabBarHeight by remember { mutableStateOf(0.dp) }
val onTabHome = (codeNavigator.currentRouteKey as? AppRoute)?.asNavBarTab() != null
val tabBarPadding = if (onTabHome) PaddingValues(bottom = tabBarHeight) else PaddingValues()
val tabBarHeight = remember { mutableStateOf(0.dp) }

CodeScaffold { _ ->
Box(modifier = Modifier.fillMaxSize()) {
CompositionLocalProvider(LocalTabBarPadding provides tabBarPadding) {
// Mark the nav content as the haze source so the frosted tab bar blurs what scrolls
// beneath it.
Box(modifier = Modifier.hazeSource(hazeState)) {
AppNavHost(
Box(modifier = Modifier.fillMaxSize()) {
// Mark the nav content as the haze source so the frosted bar blurs whatever scrolls beneath it.
Box(modifier = Modifier.hazeSource(hazeState)) {
AppNavHost(
navigator = codeNavigator,
resultStateRegistry = resultStateRegistry,
decorators = listOf(
Expand All @@ -177,6 +168,9 @@ internal fun NewAppContent(
barManager
),
rememberNavBlockingOverlayEntryDecorator(),
// Inset-only: reserves LocalTabBarPadding per tab-home entry for the hoisted bar,
// so pushed/detail screens are not inset and the inset never collapses mid-push.
rememberNavTabBarInsetEntryDecorator(codeNavigator, tabBarHeight),
),
sceneStrategies = listOf(
ModalBottomSheetSceneStrategy(
Expand Down Expand Up @@ -231,18 +225,16 @@ internal fun NewAppContent(
onPendingAction = onPendingAction,
),
)
}
}

// Bottom overlay over the full-height nav content. Latch the tallest height so the
// reserved inset above stays stable when the bar hides for a modal/bill.
AppNavigationBar(
navigator = codeNavigator,
hazeState = hazeState,
modifier = Modifier
.align(Alignment.BottomCenter)
.measured { if (it.height > tabBarHeight) tabBarHeight = it.height },
)
}

// Single persistent bottom overlay. Latch the tallest measured height so the reserved inset
// stays stable when the bar hides for a modal/bill.
AppNavigationBar(
navigator = codeNavigator,
hazeState = hazeState,
modifier = Modifier
.align(Alignment.BottomCenter)
.measured { if (it.height > tabBarHeight.value) tabBarHeight.value = it.height },
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.flipcash.app.internal.ui.navigation.decorators

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.Dp
import androidx.navigation3.runtime.NavEntryDecorator
import androidx.navigation3.runtime.NavKey
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.navigation.LocalTabBarPadding
import com.flipcash.app.core.navigation.asNavBarTab
import com.getcode.navigation.core.CodeNavigator

/**
* Provides [LocalTabBarPadding] **per entry** so tab-home screens reserve space for the (separately
* hoisted) v2 navigation bar while pushed/detail screens do not.
*
* Crucially it renders *nothing* — the bar itself is a single persistent overlay at the app root (see
* AppNavigationBar in NewAppContent) so tab switches stay seamless (the selection pill slides, one haze
* source, no crossfade). This decorator only supplies the inset, and does so per entry:
*
* - The inset is a single global before this — toggled by the top route — collapsed mid-transition and
* jumped the still-visible outgoing tab screen. Here the value is fixed for the entry's lifetime, so
* the outgoing tab keeps its inset through a push while the incoming detail simply never had one.
* - The entry's route is resolved once (keyed by its stable contentKey), not re-read from the live
* backstack, so a tab switch (replaceAll) removing the outgoing entry can't flip it mid-animation.
*
* [tabBarHeight] is the height measured by the hoisted bar; reading `.value` here keeps the inset in
* sync once the bar is laid out.
*/
@Suppress("FunctionName")
fun NavTabBarInsetEntryDecorator(
navigator: CodeNavigator,
tabBarHeight: State<Dp>,
): NavEntryDecorator<NavKey> {
return NavEntryDecorator { entry ->
// entry.key is private; recover THIS entry's route by matching its contentKey (= key.toString())
// against the backstack, once per entry.
val route = remember(entry.contentKey) {
navigator.backStack.firstOrNull { it.toString() == entry.contentKey } as? AppRoute
}
val isTabHome = route?.asNavBarTab() != null
val padding = if (isTabHome) PaddingValues(bottom = tabBarHeight.value) else PaddingValues()

CompositionLocalProvider(LocalTabBarPadding provides padding) {
entry.Content()
}
}
}

@Composable
fun rememberNavTabBarInsetEntryDecorator(
navigator: CodeNavigator,
tabBarHeight: State<Dp>,
): NavEntryDecorator<NavKey> =
remember(navigator, tabBarHeight) { NavTabBarInsetEntryDecorator(navigator, tabBarHeight) }
Loading