/* * 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. */ package eu.kanade.presentation.components import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.safeDrawing import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.contentColorFor import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.runtime.staticCompositionLocalOf 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.unit.LayoutDirection import androidx.compose.ui.unit.dp /** * 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: * * Remove height constraint for expanded app bar * * Also take account of fab height when providing inner padding * * @param modifier the [Modifier] to be applied to this scaffold * @param topBar top app bar of the screen, typically a [SmallTopAppBar] * @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 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, topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {}, bottomBar: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, containerColor: Color = MaterialTheme.colorScheme.background, contentColor: Color = contentColorFor(containerColor), content: @Composable (PaddingValues) -> Unit, ) { /** * Tachiyomi: Pass scroll behavior to topBar */ val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) androidx.compose.material3.Surface( modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection) .then(modifier), color = containerColor, contentColor = contentColor, ) { ScaffoldLayout( fabPosition = floatingActionButtonPosition, topBar = { topBar(scrollBehavior) }, bottomBar = bottomBar, content = content, snackbar = snackbarHost, 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, content: @Composable (PaddingValues) -> Unit, snackbar: @Composable () -> Unit, fab: @Composable () -> Unit, 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 topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).map { it.measure(topBarConstraints) } val topBarHeight = topBarPlaceables.maxByOrNull { it.height }?.height ?: 0 val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).map { it.measure(looseConstraints) } val snackbarHeight = snackbarPlaceables.maxByOrNull { it.height }?.height ?: 0 val snackbarWidth = snackbarPlaceables.maxByOrNull { it.width }?.width ?: 0 val fabPlaceables = subcompose(ScaffoldLayoutContent.Fab, fab).mapNotNull { measurable -> measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 } } val fabHeight = fabPlaceables.maxByOrNull { it.height }?.height ?: 0 val fabPlacement = if (fabPlaceables.isNotEmpty()) { val fabWidth = fabPlaceables.maxByOrNull { it.width }!!.width // FAB distance from the left of the layout, taking into account LTR / RTL val fabLeftOffset = if (fabPosition == FabPosition.End) { if (layoutDirection == LayoutDirection.Ltr) { layoutWidth - FabSpacing.roundToPx() - fabWidth } else { FabSpacing.roundToPx() } } else { (layoutWidth - fabWidth) / 2 } FabPlacement( left = fabLeftOffset, width = fabWidth, height = fabHeight, ) } else { null } val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) { CompositionLocalProvider( LocalFabPlacement provides fabPlacement, content = bottomBar, ) }.map { it.measure(looseConstraints) } val bottomBarHeight = bottomBarPlaceables.maxByOrNull { it.height }?.height ?: 0 val fabOffsetFromBottom = fabPlacement?.let { if (bottomBarHeight == 0) { it.height + FabSpacing.roundToPx() } else { // Total height is the bottom bar height + the FAB height + the padding // between the FAB and bottom bar bottomBarHeight + it.height + FabSpacing.roundToPx() } } val snackbarOffsetFromBottom = if (snackbarHeight != 0) { snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight) } else { 0 } /** * Tachiyomi: Also take account of fab height when providing inner padding */ val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) { val insets = WindowInsets.Companion.safeDrawing .asPaddingValues(this@SubcomposeLayout) val innerPadding = PaddingValues( top = if (topBarHeight == 0) { insets.calculateTopPadding() } else { topBarHeight.toDp() }, bottom = ( if (bottomBarHeight == 0) { insets.calculateBottomPadding() } else { bottomBarHeight.toDp() } ) + fabHeight.toDp(), start = insets.calculateLeftPadding((this@SubcomposeLayout).layoutDirection), end = insets.calculateRightPadding((this@SubcomposeLayout).layoutDirection), ) content(innerPadding) }.map { it.measure(looseConstraints) } // Placing to control drawing order to match default elevation of each placeable bodyContentPlaceables.forEach { it.place(0, 0) } topBarPlaceables.forEach { it.place(0, 0) } snackbarPlaceables.forEach { it.place( (layoutWidth - snackbarWidth) / 2, layoutHeight - snackbarOffsetFromBottom, ) } // The bottom bar is always at the bottom of the layout bottomBarPlaceables.forEach { it.place(0, layoutHeight - bottomBarHeight) } // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL fabPlacement?.let { placement -> fabPlaceables.forEach { it.place(placement.left, layoutHeight - fabOffsetFromBottom!!) } } } } } /** * The possible positions for a [FloatingActionButton] attached to a [Scaffold]. */ @ExperimentalMaterial3Api @JvmInline value class FabPosition internal constructor(@Suppress("unused") 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) /** * Position FAB at the bottom of the screen at the end, above the [NavigationBar] (if it * exists) */ val End = FabPosition(1) } override fun toString(): String { return when (this) { Center -> "FabPosition.Center" else -> "FabPosition.End" } } } /** * 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, ) /** * CompositionLocal containing a [FabPlacement] that is used to calculate the FAB bottom offset. */ internal val LocalFabPlacement = staticCompositionLocalOf { null } // FAB spacing above the bottom bar / bottom of the Scaffold private val FabSpacing = 16.dp private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar }