From a2c24ff0429ad2502b612c9ab1f2b09966629f73 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:22:45 +0600 Subject: [PATCH] Use upstream scaffold (#3834) --- .../eu/kanade/tachiyomi/ui/home/HomeScreen.kt | 245 ++++++------ gradle/libs.versions.toml | 1 + presentation-core/build.gradle.kts | 1 + .../core/components/material/NavigationBar.kt | 48 --- .../components/material/NavigationRail.kt | 63 ---- .../core/components/material/Scaffold.kt | 354 ++---------------- 6 files changed, 146 insertions(+), 566 deletions(-) delete mode 100644 presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationBar.kt delete mode 100644 presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt index a915d2d03..a2b5add8a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt @@ -2,22 +2,18 @@ package eu.kanade.tachiyomi.ui.home import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.shrinkVertically import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Arrangement import androidx.compose.material3.Badge -import androidx.compose.material3.BadgedBox import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItem +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType +import androidx.compose.material3.adaptive.navigationsuite.rememberNavigationSuiteScaffoldState +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -29,6 +25,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -52,9 +49,6 @@ import mihon.app.di.appGraph import soup.compose.material.motion.animation.materialFadeThroughIn import soup.compose.material.motion.animation.materialFadeThroughOut import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.material.NavigationBar -import tachiyomi.presentation.core.components.material.NavigationRail -import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.pluralStringResource object HomeScreen : Screen() { @@ -86,52 +80,48 @@ object HomeScreen : Screen() { ) { tabNavigator -> // Provide usable navigator to content screen CompositionLocalProvider(LocalNavigator provides navigator) { - Scaffold( - startBar = { - if (isTabletUi()) { - NavigationRail { - TABS.fastForEach { - NavigationRailItem(it) - } - } + val tabletUi = isTabletUi() + val navigationSuiteType = if (tabletUi) { + NavigationSuiteType.NavigationRail + } else { + NavigationSuiteType.NavigationBar + } + val navigationSuiteState = rememberNavigationSuiteScaffoldState() + LaunchedEffect(navigationSuiteState, tabletUi) { + if (tabletUi) navigationSuiteState.show() + showBottomNavEvent.receiveAsFlow().collectLatest { show -> + if (tabletUi || show) { + navigationSuiteState.show() + } else { + navigationSuiteState.hide() } + } + } + + NavigationSuiteScaffold( + navigationSuiteType = navigationSuiteType, + state = navigationSuiteState, + navigationSuiteColors = NavigationSuiteDefaults.colors( + navigationRailContainerColor = MaterialTheme.colorScheme + .surfaceColorAtElevation(3.dp), + ), + navigationItemVerticalArrangement = Arrangement.Center, + navigationItems = { + TABS.fastForEach { NavigationSuiteItem(it, navigationSuiteType) } }, - bottomBar = { - if (!isTabletUi()) { - val bottomNavVisible by produceState(initialValue = true) { - showBottomNavEvent.receiveAsFlow().collectLatest { value = it } - } - AnimatedVisibility( - visible = bottomNavVisible, - enter = expandVertically(), - exit = shrinkVertically(), - ) { - NavigationBar { - TABS.fastForEach { - NavigationBarItem(it) - } - } - } - } - }, - contentWindowInsets = WindowInsets(0), - ) { contentPadding -> - Box( - modifier = Modifier - .padding(contentPadding) - .consumeWindowInsets(contentPadding), + ) { + AnimatedContent( + targetState = tabNavigator.current, + transitionSpec = { + materialFadeThroughIn( + initialScale = 1f, + durationMillis = TabFadeDuration, + ) togetherWith materialFadeThroughOut(durationMillis = TabFadeDuration) + }, + label = "tabContent", ) { - AnimatedContent( - targetState = tabNavigator.current, - transitionSpec = { - materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) togetherWith - materialFadeThroughOut(durationMillis = TabFadeDuration) - }, - label = "tabContent", - ) { - tabNavigator.saveableState(key = "currentTab", it) { - it.Content() - } + tabNavigator.saveableState(key = "currentTab", it) { + it.Content() } } } @@ -176,12 +166,16 @@ object HomeScreen : Screen() { } @Composable - private fun RowScope.NavigationBarItem(tab: eu.kanade.presentation.util.Tab) { + private fun NavigationSuiteItem( + tab: eu.kanade.presentation.util.Tab, + navigationSuiteType: NavigationSuiteType, + ) { val tabNavigator = LocalTabNavigator.current val navigator = LocalNavigator.currentOrThrow val scope = rememberCoroutineScope() val selected = tabNavigator.current::class == tab::class - NavigationBarItem( + NavigationSuiteItem( + navigationSuiteType = navigationSuiteType, selected = selected, onClick = { if (!selected) { @@ -190,7 +184,12 @@ object HomeScreen : Screen() { scope.launch { tab.onReselect(navigator) } } }, - icon = { NavigationIconItem(tab) }, + icon = { + Icon( + painter = tab.options.icon!!, + contentDescription = tab.options.title, + ) + }, label = { Text( text = tab.options.title, @@ -199,93 +198,59 @@ object HomeScreen : Screen() { overflow = TextOverflow.Ellipsis, ) }, - alwaysShowLabel = true, + badge = tabBadge(tab), ) } @Composable - fun NavigationRailItem(tab: eu.kanade.presentation.util.Tab) { - val tabNavigator = LocalTabNavigator.current - val navigator = LocalNavigator.currentOrThrow - val scope = rememberCoroutineScope() - val selected = tabNavigator.current::class == tab::class - NavigationRailItem( - selected = selected, - onClick = { - if (!selected) { - tabNavigator.current = tab - } else { - scope.launch { tab.onReselect(navigator) } - } - }, - icon = { NavigationIconItem(tab) }, - label = { - Text( - text = tab.options.title, - style = MaterialTheme.typography.labelLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - alwaysShowLabel = true, - ) - } - - @Composable - private fun NavigationIconItem(tab: eu.kanade.presentation.util.Tab) { + private fun tabBadge(tab: eu.kanade.presentation.util.Tab): (@Composable () -> Unit)? { val context = LocalContext.current - BadgedBox( - badge = { - when { - tab is UpdatesTab -> { - val count by produceState(initialValue = 0) { - val pref = context.appGraph.libraryPreferences - combine( - pref.newShowUpdatesCount.changes(), - pref.newUpdatesCount.changes(), - ) { show, count -> if (show) count else 0 } - .collectLatest { value = it } - } - if (count > 0) { - Badge { - val desc = pluralStringResource( - MR.plurals.notification_chapters_generic, - count = count, - count, - ) - Text( - text = count.toString(), - modifier = Modifier.semantics { contentDescription = desc }, - ) - } - } - } - BrowseTab::class.isInstance(tab) -> { - val count by produceState(initialValue = 0) { - context.appGraph.sourcePreferences.extensionUpdatesCount.changes() - .collectLatest { value = it } - } - if (count > 0) { - Badge { - val desc = pluralStringResource( - MR.plurals.update_check_notification_ext_updates, - count = count, - count, - ) - Text( - text = count.toString(), - modifier = Modifier.semantics { contentDescription = desc }, - ) - } - } + val count by produceState(initialValue = 0, tab) { + val graph = context.appGraph + when (tab) { + is UpdatesTab -> { + combine( + graph.libraryPreferences.newShowUpdatesCount.changes(), + graph.libraryPreferences.newUpdatesCount.changes(), + ) { show, count -> + if (show) count else 0 } + .collectLatest { value = it } } - }, - ) { - Icon( - painter = tab.options.icon!!, - contentDescription = tab.options.title, - ) + + is BrowseTab -> { + graph.sourcePreferences.extensionUpdatesCount.changes() + .collectLatest { value = it } + } + + else -> value = 0 + } + } + if (count <= 0) return null + return { + Badge { + val desc = when (tab) { + is UpdatesTab -> pluralStringResource( + MR.plurals.notification_chapters_generic, + count = count, + count, + ) + + is BrowseTab -> pluralStringResource( + MR.plurals.update_check_notification_ext_updates, + count = count, + count, + ) + + else -> null + } + Text( + text = count.toString(), + modifier = Modifier.semantics { + if (desc != null) contentDescription = desc + }, + ) + } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8fcc8e05f..838bbd34d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -93,6 +93,7 @@ androidx-compose-animationGraphics = { module = "androidx.compose.animation:anim androidx-compose-bom = { module = "androidx.compose:compose-bom-alpha", version.ref = "androidx-compose-bom" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } +androidx-compose-material3NavSuite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" } androidx-compose-materialIcons = { module = "androidx.compose.material:material-icons-extended" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } androidx-compose-runtimeAnnotation = { module = "androidx.compose.runtime:runtime-annotation" } diff --git a/presentation-core/build.gradle.kts b/presentation-core/build.gradle.kts index e622d95c4..a9dc143b1 100644 --- a/presentation-core/build.gradle.kts +++ b/presentation-core/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material3) + api(libs.androidx.compose.material3NavSuite) implementation(libs.androidx.compose.materialIcons) implementation(libs.androidx.compose.animation) implementation(libs.androidx.compose.animationGraphics) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationBar.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationBar.kt deleted file mode 100644 index 2f7f431e7..000000000 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationBar.kt +++ /dev/null @@ -1,48 +0,0 @@ -package tachiyomi.presentation.core.components.material - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBarDefaults -import androidx.compose.material3.contentColorFor -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -/** - * M3 Navbar with no horizontal spacer - * - * @see [androidx.compose.material3.NavigationBar] - */ -@Composable -fun NavigationBar( - modifier: Modifier = Modifier, - containerColor: Color = NavigationBarDefaults.containerColor, - contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), - tonalElevation: Dp = NavigationBarDefaults.Elevation, - windowInsets: WindowInsets = NavigationBarDefaults.windowInsets, - content: @Composable RowScope.() -> Unit, -) { - androidx.compose.material3.Surface( - color = containerColor, - contentColor = contentColor, - tonalElevation = tonalElevation, - modifier = modifier, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(windowInsets) - .height(80.dp) - .selectableGroup(), - content = content, - ) - } -} diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt deleted file mode 100644 index e33f6e4fe..000000000 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt +++ /dev/null @@ -1,63 +0,0 @@ -package tachiyomi.presentation.core.components.material - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationRailDefaults -import androidx.compose.material3.contentColorFor -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp - -/** - * Center-aligned M3 Navigation rail - * - * @see [androidx.compose.material3.NavigationRail] - */ -@Composable -fun NavigationRail( - modifier: Modifier = Modifier, - containerColor: Color = NavigationRailDefaults.ContainerColor, - contentColor: Color = contentColorFor(containerColor), - header: @Composable (ColumnScope.() -> Unit)? = null, - windowInsets: WindowInsets = NavigationRailDefaults.windowInsets, - content: @Composable ColumnScope.() -> Unit, -) { - androidx.compose.material3.Surface( - color = containerColor, - contentColor = contentColor, - modifier = modifier, - tonalElevation = 3.dp, - ) { - Column( - Modifier - .fillMaxHeight() - .windowInsetsPadding(windowInsets) - .widthIn(min = 80.dp) - .padding(vertical = MaterialTheme.padding.extraSmall) - .selectableGroup(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy( - MaterialTheme.padding.extraSmall, - alignment = Alignment.CenterVertically, - ), - ) { - if (header != null) { - header() - Spacer(Modifier.height(MaterialTheme.padding.small)) - } - content() - } - } -} diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt index a2c158931..736cd64ff 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt @@ -1,32 +1,9 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:Suppress("KDocUnresolvedReference") - package tachiyomi.presentation.core.components.material -import androidx.compose.foundation.layout.MutableWindowInsets +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.exclude -import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged -import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.TopAppBarDefaults @@ -34,68 +11,19 @@ import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.contentColorFor import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.SubcomposeLayout -import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.max -import androidx.compose.ui.util.fastForEach -import androidx.compose.ui.util.fastMap -import androidx.compose.ui.util.fastMaxBy -import kotlin.math.max -/** - * Material Design layout. - * - * Scaffold implements the basic material design visual layout structure. - * - * This component provides API to put together several material components to construct your - * screen, by ensuring proper layout strategy for them and collecting necessary data so these - * components will work together correctly. - * - * Simple example of a Scaffold with [SmallTopAppBar], [FloatingActionButton]: - * - * @sample androidx.compose.material3.samples.SimpleScaffoldWithTopBar - * - * To show a [Snackbar], use [SnackbarHostState.showSnackbar]. - * - * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar - * - * Tachiyomi changes: - * * Pass scroll behavior to top bar by default - * * Remove height constraint for expanded app bar - * * Also take account of fab height when providing inner padding - * * Fixes for fab and snackbar horizontal placements when [contentWindowInsets] is used - * * Handle consumed window insets - * * Add startBar slot for Navigation Rail - * - * @param modifier the [Modifier] to be applied to this scaffold - * @param topBar top app bar of the screen, typically a [SmallTopAppBar] - * @param startBar side bar on the start of the screen, typically a [NavigationRail] - * @param bottomBar bottom bar of the screen, typically a [NavigationBar] - * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via - * [SnackbarHostState.showSnackbar], typically a [SnackbarHost] - * @param floatingActionButton Main action button of the screen, typically a [FloatingActionButton] - * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition]. - * @param containerColor the color used for the background of this scaffold. Use [Color.Transparent] - * to have no color. - * @param contentColor the preferred color for content inside this scaffold. Defaults to either the - * matching content color for [containerColor], or to the current [LocalContentColor] if - * [containerColor] is not a color from the theme. - * @param contentWindowInsets window insets to be passed to content slot via PaddingValues params. - * Scaffold will take the insets into account from the top/bottom only if the topBar/ bottomBar - * are not present, as the scaffold expect topBar/bottomBar to handle insets instead - * @param content content of the screen. The lambda receives a [PaddingValues] that should be - * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to - * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to - * the child of the scroll, and not on the scroll itself. - */ -@ExperimentalMaterial3Api @Composable fun Scaffold( modifier: Modifier = Modifier, @@ -104,7 +32,6 @@ fun Scaffold( ), topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {}, bottomBar: @Composable () -> Unit = {}, - startBar: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, @@ -113,248 +40,45 @@ fun Scaffold( contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, content: @Composable (PaddingValues) -> Unit, ) { - // Tachiyomi: Handle consumed window insets - val remainingWindowInsets = remember { MutableWindowInsets() } - androidx.compose.material3.Surface( - modifier = Modifier - .nestedScroll(topBarScrollBehavior.nestedScrollConnection) - .onConsumedWindowInsetsChanged { - remainingWindowInsets.insets = contentWindowInsets.exclude( - it, - ) + var fabHeight by remember { mutableIntStateOf(0) } + val density = LocalDensity.current + + androidx.compose.material3.Scaffold( + modifier = modifier.nestedScroll(topBarScrollBehavior.nestedScrollConnection), + topBar = { topBar(topBarScrollBehavior) }, + bottomBar = bottomBar, + snackbarHost = snackbarHost, + floatingActionButton = { + Box(Modifier.onSizeChanged { fabHeight = it.height }) { + floatingActionButton() } - .then(modifier), - color = containerColor, + }, + floatingActionButtonPosition = floatingActionButtonPosition, + containerColor = containerColor, contentColor = contentColor, - ) { - ScaffoldLayout( - fabPosition = floatingActionButtonPosition, - topBar = { topBar(topBarScrollBehavior) }, - startBar = startBar, - bottomBar = bottomBar, - content = content, - snackbar = snackbarHost, - contentWindowInsets = remainingWindowInsets, - fab = floatingActionButton, - ) - } -} - -/** - * Layout for a [Scaffold]'s content. - * - * @param fabPosition [FabPosition] for the FAB (if present) - * @param topBar the content to place at the top of the [Scaffold], typically a [SmallTopAppBar] - * @param content the main 'body' of the [Scaffold] - * @param snackbar the [Snackbar] displayed on top of the [content] - * @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar] - * and above the [bottomBar] - * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the - * [content], typically a [NavigationBar]. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ScaffoldLayout( - fabPosition: FabPosition, - topBar: @Composable () -> Unit, - startBar: @Composable () -> Unit, - content: @Composable (PaddingValues) -> Unit, - snackbar: @Composable () -> Unit, - fab: @Composable () -> Unit, - contentWindowInsets: WindowInsets, - bottomBar: @Composable () -> Unit, -) { - SubcomposeLayout { constraints -> - val layoutWidth = constraints.maxWidth - val layoutHeight = constraints.maxHeight - - val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) - - /** - * Tachiyomi: Remove height constraint for expanded app bar - */ - val topBarConstraints = looseConstraints.copy(maxHeight = Constraints.Infinity) - - layout(layoutWidth, layoutHeight) { - val leftInset = contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection) - val rightInset = contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection) - val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout) - - // Tachiyomi: Add startBar slot for Navigation Rail - val startBarPlaceables = subcompose(ScaffoldLayoutContent.StartBar, startBar).fastMap { - it.measure(looseConstraints) - } - val startBarWidth = startBarPlaceables.fastMaxBy { it.width }?.width ?: 0 - - // Tachiyomi: layoutWidth after horizontal insets - val insetLayoutWidth = layoutWidth - leftInset - rightInset - startBarWidth - - val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap { - it.measure(topBarConstraints) - } - - val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0 - - val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap { - it.measure(looseConstraints) - } - - val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0 - val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0 - - // Tachiyomi: Calculate insets for snackbar placement offset - val snackbarLeft = if (snackbarPlaceables.isNotEmpty()) { - (insetLayoutWidth - snackbarWidth) / 2 + leftInset - } else { - 0 - } - - val fabPlaceables = - subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable -> - measurable.measure(looseConstraints) - } - - val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0 - val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0 - - val fabPlacement = if (fabPlaceables.isNotEmpty() && fabWidth != 0 && fabHeight != 0) { - // FAB distance from the left of the layout, taking into account LTR / RTL - // Tachiyomi: Calculate insets for fab placement offset - val fabLeftOffset = if (fabPosition == FabPosition.End) { - if (layoutDirection == LayoutDirection.Ltr) { - layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset - } else { - FabSpacing.roundToPx() + leftInset - } - } else { - leftInset + ((insetLayoutWidth - fabWidth) / 2) - } - - FabPlacement( - left = fabLeftOffset, - width = fabWidth, - height = fabHeight, - ) - } else { - null - } - - val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) { - bottomBar() - }.fastMap { it.measure(looseConstraints) } - - val bottomBarHeight = bottomBarPlaceables - .fastMaxBy { it.height } - ?.height - ?.takeIf { it != 0 } - val fabOffsetFromBottom = fabPlacement?.let { - max(bottomBarHeight ?: 0, bottomInset) + it.height + FabSpacing.roundToPx() - } - - val snackbarOffsetFromBottom = if (snackbarHeight != 0) { - snackbarHeight + (fabOffsetFromBottom ?: max(bottomBarHeight ?: 0, bottomInset)) - } else { - 0 - } - - val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) { - val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout) - val fabOffsetDp = fabOffsetFromBottom?.toDp() ?: 0.dp - val bottomBarHeightPx = bottomBarHeight ?: 0 - val innerPadding = PaddingValues( - top = if (topBarPlaceables.isEmpty()) { - insets.calculateTopPadding() - } else { - topBarHeight.toDp() - }, - // Tachiyomi: Also take account of fab height when providing inner padding - bottom = if (bottomBarPlaceables.isEmpty() || bottomBarHeightPx == 0) { - max(insets.calculateBottomPadding(), fabOffsetDp) - } else { - max(bottomBarHeightPx.toDp(), fabOffsetDp) - }, - start = max( - insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection), - startBarWidth.toDp(), - ), - end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection), - ) - content(innerPadding) - }.fastMap { it.measure(looseConstraints) } - - // Placing to control drawing order to match default elevation of each placeable - - bodyContentPlaceables.fastForEach { - it.place(0, 0) - } - startBarPlaceables.fastForEach { - it.placeRelative(0, 0) - } - topBarPlaceables.fastForEach { - it.place(0, 0) - } - snackbarPlaceables.fastForEach { - it.place( - snackbarLeft, - layoutHeight - snackbarOffsetFromBottom, - ) - } - // The bottom bar is always at the bottom of the layout - bottomBarPlaceables.fastForEach { - it.place(0, layoutHeight - (bottomBarHeight ?: 0)) - } - // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL - fabPlaceables.fastForEach { - it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0)) + contentWindowInsets = contentWindowInsets, + ) { contentPadding -> + val paddingValues = remember(contentPadding) { + FabAwarePaddingValues(contentPadding) { + if (fabHeight == 0) 0.dp else with(density) { fabHeight.toDp() } + FabSpacing } } + content(paddingValues) } } -/** - * The possible positions for a [FloatingActionButton] attached to a [Scaffold]. - */ -@ExperimentalMaterial3Api -@JvmInline -value class FabPosition internal constructor(private val value: Int) { - companion object { - /** - * Position FAB at the bottom of the screen in the center, above the [NavigationBar] (if it - * exists) - */ - val Center = FabPosition(0) +private class FabAwarePaddingValues( + private val base: PaddingValues, + private val extraBottom: () -> Dp, +) : PaddingValues { + override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = + base.calculateLeftPadding(layoutDirection) - /** - * Position FAB at the bottom of the screen at the end, above the [NavigationBar] (if it - * exists) - */ - val End = FabPosition(1) - } + override fun calculateTopPadding(): Dp = base.calculateTopPadding() - override fun toString(): String { - return when (this) { - Center -> "FabPosition.Center" - else -> "FabPosition.End" - } - } + override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = + base.calculateRightPadding(layoutDirection) + + override fun calculateBottomPadding(): Dp = base.calculateBottomPadding() + extraBottom() } - -/** - * Placement information for a [FloatingActionButton] inside a [Scaffold]. - * - * @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL - * support - * @property width the width of the FAB - * @property height the height of the FAB - */ -@Immutable -internal class FabPlacement( - val left: Int, - val width: Int, - val height: Int, -) - -// FAB spacing above the bottom bar / bottom of the Scaffold private val FabSpacing = 16.dp - -private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar, StartBar }