1 /*
<lambda>null2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.material
18 
19 import androidx.compose.foundation.layout.ColumnScope
20 import androidx.compose.foundation.layout.PaddingValues
21 import androidx.compose.foundation.layout.WindowInsets
22 import androidx.compose.foundation.layout.asPaddingValues
23 import androidx.compose.foundation.layout.calculateEndPadding
24 import androidx.compose.foundation.layout.calculateStartPadding
25 import androidx.compose.foundation.layout.consumeWindowInsets
26 import androidx.compose.foundation.layout.exclude
27 import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
28 import androidx.compose.runtime.Composable
29 import androidx.compose.runtime.CompositionLocalProvider
30 import androidx.compose.runtime.Immutable
31 import androidx.compose.runtime.Stable
32 import androidx.compose.runtime.getValue
33 import androidx.compose.runtime.mutableStateOf
34 import androidx.compose.runtime.remember
35 import androidx.compose.runtime.setValue
36 import androidx.compose.runtime.staticCompositionLocalOf
37 import androidx.compose.ui.Modifier
38 import androidx.compose.ui.UiComposable
39 import androidx.compose.ui.graphics.Color
40 import androidx.compose.ui.graphics.Shape
41 import androidx.compose.ui.layout.SubcomposeLayout
42 import androidx.compose.ui.unit.Dp
43 import androidx.compose.ui.unit.LayoutDirection
44 import androidx.compose.ui.unit.dp
45 import androidx.compose.ui.unit.offset
46 import androidx.compose.ui.util.fastForEach
47 import androidx.compose.ui.util.fastMap
48 import androidx.compose.ui.util.fastMaxBy
49 import kotlin.jvm.JvmInline
50 
51 /**
52  * State for [Scaffold] composable component.
53  *
54  * Contains basic screen state, e.g. Drawer configuration, as well as sizes of components after
55  * layout has happened
56  *
57  * @param drawerState the drawer state
58  * @param snackbarHostState instance of [SnackbarHostState] to be used to show [Snackbar]s inside of
59  *   the [Scaffold]
60  */
61 @Stable class ScaffoldState(val drawerState: DrawerState, val snackbarHostState: SnackbarHostState)
62 
63 /**
64  * Creates a [ScaffoldState] with the default animation clock and memoizes it.
65  *
66  * @param drawerState the drawer state
67  * @param snackbarHostState instance of [SnackbarHostState] to be used to show [Snackbar]s inside of
68  *   the [Scaffold]
69  */
70 @Composable
71 fun rememberScaffoldState(
72     drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
73     snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
<lambda>null74 ): ScaffoldState = remember { ScaffoldState(drawerState, snackbarHostState) }
75 
76 /** The possible positions for a [FloatingActionButton] attached to a [Scaffold]. */
77 @JvmInline
78 value class FabPosition internal constructor(@Suppress("unused") private val value: Int) {
79     companion object {
80         /**
81          * Position FAB at the bottom of the screen at the start, above the [BottomAppBar] (if it
82          * exists)
83          */
84         val Start = FabPosition(0)
85 
86         /**
87          * Position FAB at the bottom of the screen in the center, above the [BottomAppBar] (if it
88          * exists)
89          */
90         val Center = FabPosition(1)
91 
92         /**
93          * Position FAB at the bottom of the screen at the end, above the [BottomAppBar] (if it
94          * exists)
95          */
96         val End = FabPosition(2)
97     }
98 
toStringnull99     override fun toString(): String {
100         return when (this) {
101             Start -> "FabPosition.Start"
102             Center -> "FabPosition.Center"
103             else -> "FabPosition.End"
104         }
105     }
106 }
107 
108 /**
109  * [Material Design layout](https://material.io/design/layout/understanding-layout.html)
110  *
111  * Scaffold implements the basic Material Design visual layout structure.
112  *
113  * This component provides API to put together several Material components to construct your screen,
114  * by ensuring proper layout strategy for them and collecting necessary data so these components
115  * will work together correctly.
116  *
117  * For similar components that implement different layout structures, see [BackdropScaffold], which
118  * uses a backdrop as the centerpiece of the screen, and [BottomSheetScaffold], which uses a
119  * persistent bottom sheet as the centerpiece of the screen.
120  *
121  * This particular overload provides ability to specify [WindowInsets]. Recommended value can be
122  * found in [ScaffoldDefaults.contentWindowInsets].
123  *
124  * Simple example of a Scaffold with [TopAppBar], [FloatingActionButton] and drawer:
125  *
126  * @sample androidx.compose.material.samples.SimpleScaffoldWithTopBar
127  *
128  * More fancy usage with [BottomAppBar] with cutout and docked [FloatingActionButton], which
129  * animates its shape when clicked:
130  *
131  * @sample androidx.compose.material.samples.ScaffoldWithBottomBarAndCutout
132  *
133  * To show a [Snackbar], use [SnackbarHostState.showSnackbar]. Scaffold state already have
134  * [ScaffoldState.snackbarHostState] when created.
135  *
136  * @sample androidx.compose.material.samples.ScaffoldWithSimpleSnackbar
137  * @param contentWindowInsets window insets to be passed to [content] slot via [PaddingValues]
138  *   params. Scaffold will take the insets into account from the top/bottom only if the [topBar]/
139  *   [bottomBar] are not present, as the scaffold expect [topBar]/[bottomBar] to handle insets
140  *   instead. Any insets consumed by other insets padding modifiers or [consumeWindowInsets] on a
141  *   parent layout will be excluded from [contentWindowInsets].
142  * @param modifier optional Modifier for the root of the [Scaffold]
143  * @param scaffoldState state of this scaffold widget. It contains the state of the screen, e.g.
144  *   variables to provide manual control over the drawer behavior, sizes of components, etc
145  * @param topBar top app bar of the screen. Consider using [TopAppBar].
146  * @param bottomBar bottom bar of the screen. Consider using [BottomAppBar].
147  * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
148  *   [SnackbarHostState.showSnackbar]. Usually it's a [SnackbarHost]
149  * @param floatingActionButton Main action button of your screen. Consider using
150  *   [FloatingActionButton] for this slot.
151  * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition] for
152  *   possible options available.
153  * @param isFloatingActionButtonDocked whether [floatingActionButton] should overlap with
154  *   [bottomBar] for half a height, if [bottomBar] exists. Ignored if there's no [bottomBar] or no
155  *   [floatingActionButton].
156  * @param drawerContent content of the Drawer sheet that can be pulled from the left side (right for
157  *   RTL).
158  * @param drawerGesturesEnabled whether or not drawer (if set) can be interacted with via gestures
159  * @param drawerShape shape of the drawer sheet (if set)
160  * @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the
161  *   drawer sheet (if set)
162  * @param drawerBackgroundColor background color to be used for the drawer sheet
163  * @param drawerContentColor color of the content to use inside the drawer sheet. Defaults to either
164  *   the matching content color for [drawerBackgroundColor], or, if it is not a color from the
165  *   theme, this will keep the same value set above this Surface.
166  * @param drawerScrimColor color of the scrim that obscures content when the drawer is open
167  * @param backgroundColor background of the scaffold body
168  * @param contentColor color of the content in scaffold body. Defaults to either the matching
169  *   content color for [backgroundColor], or, if it is not a color from the theme, this will keep
170  *   the same value set above this Surface.
171  * @param content content of your screen. The lambda receives an [PaddingValues] that should be
172  *   applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
173  *   properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
174  *   the child of the scroll, and not on the scroll itself.
175  */
176 @Composable
Scaffoldnull177 fun Scaffold(
178     contentWindowInsets: WindowInsets,
179     modifier: Modifier = Modifier,
180     scaffoldState: ScaffoldState = rememberScaffoldState(),
181     topBar: @Composable () -> Unit = {},
182     bottomBar: @Composable () -> Unit = {},
183     snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
184     floatingActionButton: @Composable () -> Unit = {},
185     floatingActionButtonPosition: FabPosition = FabPosition.End,
186     isFloatingActionButtonDocked: Boolean = false,
187     drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
188     drawerGesturesEnabled: Boolean = true,
189     drawerShape: Shape = MaterialTheme.shapes.large,
190     drawerElevation: Dp = DrawerDefaults.Elevation,
191     drawerBackgroundColor: Color = MaterialTheme.colors.surface,
192     drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
193     drawerScrimColor: Color = DrawerDefaults.scrimColor,
194     backgroundColor: Color = MaterialTheme.colors.background,
195     contentColor: Color = contentColorFor(backgroundColor),
196     content: @Composable (PaddingValues) -> Unit
197 ) {
<lambda>null198     val safeInsets = remember(contentWindowInsets) { MutableWindowInsets(contentWindowInsets) }
199     val child =
childModifiernull200         @Composable { childModifier: Modifier ->
201             Surface(
202                 modifier =
203                     childModifier.onConsumedWindowInsetsChanged { consumedWindowInsets ->
204                         // Exclude currently consumed window insets from user provided
205                         // contentWindowInsets
206                         safeInsets.insets = contentWindowInsets.exclude(consumedWindowInsets)
207                     },
208                 color = backgroundColor,
209                 contentColor = contentColor
210             ) {
211                 ScaffoldLayout(
212                     isFabDocked = isFloatingActionButtonDocked,
213                     fabPosition = floatingActionButtonPosition,
214                     topBar = topBar,
215                     content = content,
216                     contentWindowInsets = safeInsets,
217                     snackbar = { snackbarHost(scaffoldState.snackbarHostState) },
218                     fab = floatingActionButton,
219                     bottomBar = bottomBar
220                 )
221             }
222         }
223 
224     if (drawerContent != null) {
225         ModalDrawer(
226             modifier = modifier,
227             drawerState = scaffoldState.drawerState,
228             gesturesEnabled = drawerGesturesEnabled,
229             drawerContent = drawerContent,
230             drawerShape = drawerShape,
231             drawerElevation = drawerElevation,
232             drawerBackgroundColor = drawerBackgroundColor,
233             drawerContentColor = drawerContentColor,
234             scrimColor = drawerScrimColor,
<lambda>null235             content = { child(Modifier) }
236         )
237     } else {
238         child(modifier)
239     }
240 }
241 
242 /**
243  * [Material Design layout](https://material.io/design/layout/understanding-layout.html)
244  *
245  * Scaffold implements the basic Material Design visual layout structure.
246  *
247  * This component provides API to put together several Material components to construct your screen,
248  * by ensuring proper layout strategy for them and collecting necessary data so these components
249  * will work together correctly.
250  *
251  * For similar components that implement different layout structures, see [BackdropScaffold], which
252  * uses a backdrop as the centerpiece of the screen, and [BottomSheetScaffold], which uses a
253  * persistent bottom sheet as the centerpiece of the screen.
254  *
255  * Simple example of a Scaffold with [TopAppBar], [FloatingActionButton] and drawer:
256  *
257  * @sample androidx.compose.material.samples.SimpleScaffoldWithTopBar
258  *
259  * More fancy usage with [BottomAppBar] with cutout and docked [FloatingActionButton], which
260  * animates its shape when clicked:
261  *
262  * @sample androidx.compose.material.samples.ScaffoldWithBottomBarAndCutout
263  *
264  * To show a [Snackbar], use [SnackbarHostState.showSnackbar]. Scaffold state already have
265  * [ScaffoldState.snackbarHostState] when created
266  *
267  * @sample androidx.compose.material.samples.ScaffoldWithSimpleSnackbar
268  * @param modifier optional Modifier for the root of the [Scaffold]
269  * @param scaffoldState state of this scaffold widget. It contains the state of the screen, e.g.
270  *   variables to provide manual control over the drawer behavior, sizes of components, etc
271  * @param topBar top app bar of the screen. Consider using [TopAppBar].
272  * @param bottomBar bottom bar of the screen. Consider using [BottomAppBar].
273  * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
274  *   [SnackbarHostState.showSnackbar]. Usually it's a [SnackbarHost]
275  * @param floatingActionButton Main action button of your screen. Consider using
276  *   [FloatingActionButton] for this slot.
277  * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition] for
278  *   possible options available.
279  * @param isFloatingActionButtonDocked whether [floatingActionButton] should overlap with
280  *   [bottomBar] for half a height, if [bottomBar] exists. Ignored if there's no [bottomBar] or no
281  *   [floatingActionButton].
282  * @param drawerContent content of the Drawer sheet that can be pulled from the left side (right for
283  *   RTL).
284  * @param drawerGesturesEnabled whether or not drawer (if set) can be interacted with via gestures
285  * @param drawerShape shape of the drawer sheet (if set)
286  * @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the
287  *   drawer sheet (if set)
288  * @param drawerBackgroundColor background color to be used for the drawer sheet
289  * @param drawerContentColor color of the content to use inside the drawer sheet. Defaults to either
290  *   the matching content color for [drawerBackgroundColor], or, if it is not a color from the
291  *   theme, this will keep the same value set above this Surface.
292  * @param drawerScrimColor color of the scrim that obscures content when the drawer is open
293  * @param backgroundColor background of the scaffold body
294  * @param contentColor color of the content in scaffold body. Defaults to either the matching
295  *   content color for [backgroundColor], or, if it is not a color from the theme, this will keep
296  *   the same value set above this Surface.
297  * @param content content of your screen. The lambda receives an [PaddingValues] that should be
298  *   applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
299  *   properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
300  *   the child of the scroll, and not on the scroll itself.
301  */
302 @Composable
Scaffoldnull303 fun Scaffold(
304     modifier: Modifier = Modifier,
305     scaffoldState: ScaffoldState = rememberScaffoldState(),
306     topBar: @Composable () -> Unit = {},
307     bottomBar: @Composable () -> Unit = {},
308     snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
309     floatingActionButton: @Composable () -> Unit = {},
310     floatingActionButtonPosition: FabPosition = FabPosition.End,
311     isFloatingActionButtonDocked: Boolean = false,
312     drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
313     drawerGesturesEnabled: Boolean = true,
314     drawerShape: Shape = MaterialTheme.shapes.large,
315     drawerElevation: Dp = DrawerDefaults.Elevation,
316     drawerBackgroundColor: Color = MaterialTheme.colors.surface,
317     drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
318     drawerScrimColor: Color = DrawerDefaults.scrimColor,
319     backgroundColor: Color = MaterialTheme.colors.background,
320     contentColor: Color = contentColorFor(backgroundColor),
321     content: @Composable (PaddingValues) -> Unit
322 ) {
323     Scaffold(
324         WindowInsets(0.dp),
325         modifier,
326         scaffoldState,
327         topBar,
328         bottomBar,
329         snackbarHost,
330         floatingActionButton,
331         floatingActionButtonPosition,
332         isFloatingActionButtonDocked,
333         drawerContent,
334         drawerGesturesEnabled,
335         drawerShape,
336         drawerElevation,
337         drawerBackgroundColor,
338         drawerContentColor,
339         drawerScrimColor,
340         backgroundColor,
341         contentColor,
342         content
343     )
344 }
345 
346 /** Object containing various default values for [Scaffold] component. */
347 object ScaffoldDefaults {
348     /** Recommended insets to be used and consumed by the scaffold content slot */
349     val contentWindowInsets: WindowInsets
350         @Composable get() = WindowInsets.systemBarsForVisualComponents
351 }
352 
353 /**
354  * Layout for a [Scaffold]'s content.
355  *
356  * @param isFabDocked whether the FAB (if present) is docked to the bottom bar or not
357  * @param fabPosition [FabPosition] for the FAB (if present)
358  * @param topBar the content to place at the top of the [Scaffold], typically a [TopAppBar]
359  * @param content the main 'body' of the [Scaffold]
360  * @param snackbar the [Snackbar] displayed on top of the [content]
361  * @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar] and
362  *   above the [bottomBar]
363  * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the [content],
364  *   typically a [BottomAppBar].
365  */
366 @Composable
367 @UiComposable
ScaffoldLayoutnull368 private fun ScaffoldLayout(
369     isFabDocked: Boolean,
370     fabPosition: FabPosition,
371     topBar: @Composable @UiComposable () -> Unit,
372     content: @Composable @UiComposable (PaddingValues) -> Unit,
373     snackbar: @Composable @UiComposable () -> Unit,
374     fab: @Composable @UiComposable () -> Unit,
375     contentWindowInsets: WindowInsets,
376     bottomBar: @Composable @UiComposable () -> Unit
377 ) {
378     // Create the backing value for the content padding
379     // These values will be updated during measurement, but before subcomposing the body content
380     // Remembering and updating a single PaddingValues avoids needing to recompose when the values
381     // change
382     val contentPadding = remember {
383         object : PaddingValues {
384             var paddingHolder by mutableStateOf(PaddingValues(0.dp))
385 
386             override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp =
387                 paddingHolder.calculateLeftPadding(layoutDirection)
388 
389             override fun calculateTopPadding(): Dp = paddingHolder.calculateTopPadding()
390 
391             override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp =
392                 paddingHolder.calculateRightPadding(layoutDirection)
393 
394             override fun calculateBottomPadding(): Dp = paddingHolder.calculateBottomPadding()
395         }
396     }
397 
398     SubcomposeLayout { constraints ->
399         val layoutWidth = constraints.maxWidth
400         val layoutHeight = constraints.maxHeight
401 
402         val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
403 
404         val topBarPlaceables =
405             subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
406                 it.measure(looseConstraints)
407             }
408 
409         val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
410 
411         val snackbarPlaceables =
412             subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
413                 // respect only bottom and horizontal for snackbar and fab
414                 val leftInset = contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
415                 val rightInset =
416                     contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
417                 val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
418                 // offset the snackbar constraints by the insets values
419                 it.measure(looseConstraints.offset(-leftInset - rightInset, -bottomInset))
420             }
421 
422         val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
423 
424         val fabPlaceables =
425             subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable ->
426                 // respect only bottom and horizontal for snackbar and fab
427                 val leftInset = contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
428                 val rightInset =
429                     contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
430                 val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
431                 measurable.measure(looseConstraints.offset(-leftInset - rightInset, -bottomInset))
432             }
433 
434         val fabPlacement =
435             if (fabPlaceables.isNotEmpty()) {
436                 val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
437                 val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
438                 // FAB distance from the left of the layout, taking into account LTR / RTL
439                 if (fabWidth != 0 && fabHeight != 0) {
440                     val fabLeftOffset =
441                         when (fabPosition) {
442                             FabPosition.Start -> {
443                                 if (layoutDirection == LayoutDirection.Ltr) {
444                                     FabSpacing.roundToPx()
445                                 } else {
446                                     layoutWidth - FabSpacing.roundToPx() - fabWidth
447                                 }
448                             }
449                             FabPosition.End -> {
450                                 if (layoutDirection == LayoutDirection.Ltr) {
451                                     layoutWidth - FabSpacing.roundToPx() - fabWidth
452                                 } else {
453                                     FabSpacing.roundToPx()
454                                 }
455                             }
456                             else -> (layoutWidth - fabWidth) / 2
457                         }
458 
459                     FabPlacement(
460                         isDocked = isFabDocked,
461                         left = fabLeftOffset,
462                         width = fabWidth,
463                         height = fabHeight
464                     )
465                 } else {
466                     null
467                 }
468             } else {
469                 null
470             }
471 
472         val bottomBarPlaceables =
473             subcompose(ScaffoldLayoutContent.BottomBar) {
474                     CompositionLocalProvider(
475                         LocalFabPlacement provides fabPlacement,
476                         content = bottomBar
477                     )
478                 }
479                 .fastMap { it.measure(looseConstraints) }
480 
481         val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
482         val fabOffsetFromBottom =
483             fabPlacement?.let {
484                 if (bottomBarHeight == null) {
485                     it.height +
486                         FabSpacing.roundToPx() +
487                         contentWindowInsets.getBottom(this@SubcomposeLayout)
488                 } else {
489                     if (isFabDocked) {
490                         // Total height is the bottom bar height + half the FAB height
491                         bottomBarHeight + (it.height / 2)
492                     } else {
493                         // Total height is the bottom bar height + the FAB height + the padding
494                         // between the FAB and bottom bar
495                         bottomBarHeight + it.height + FabSpacing.roundToPx()
496                     }
497                 }
498             }
499 
500         val snackbarOffsetFromBottom =
501             if (snackbarHeight != 0) {
502                 snackbarHeight +
503                     (fabOffsetFromBottom
504                         ?: bottomBarHeight
505                         ?: contentWindowInsets.getBottom(this@SubcomposeLayout))
506             } else {
507                 0
508             }
509 
510         // Update the backing state for the content padding before subcomposing the body
511         val insets = contentWindowInsets.asPaddingValues(this)
512         contentPadding.paddingHolder =
513             PaddingValues(
514                 top =
515                     if (topBarPlaceables.isEmpty()) {
516                         insets.calculateTopPadding()
517                     } else {
518                         0.dp
519                     },
520                 bottom =
521                     if (bottomBarPlaceables.isEmpty() || bottomBarHeight == null) {
522                         insets.calculateBottomPadding()
523                     } else {
524                         bottomBarHeight.toDp()
525                     },
526                 start = insets.calculateStartPadding(layoutDirection),
527                 end = insets.calculateEndPadding(layoutDirection)
528             )
529 
530         val bodyContentHeight = layoutHeight - topBarHeight
531 
532         val bodyContentPlaceables =
533             subcompose(ScaffoldLayoutContent.MainContent) { content(contentPadding) }
534                 .fastMap { it.measure(looseConstraints.copy(maxHeight = bodyContentHeight)) }
535 
536         layout(layoutWidth, layoutHeight) {
537             // Placing to control drawing order to match default elevation of each placeable
538 
539             bodyContentPlaceables.fastForEach { it.place(0, topBarHeight) }
540             topBarPlaceables.fastForEach { it.place(0, 0) }
541             snackbarPlaceables.fastForEach { it.place(0, layoutHeight - snackbarOffsetFromBottom) }
542             // The bottom bar is always at the bottom of the layout
543             bottomBarPlaceables.fastForEach { it.place(0, layoutHeight - (bottomBarHeight ?: 0)) }
544             // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
545             fabPlaceables.fastForEach {
546                 it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0))
547             }
548         }
549     }
550 }
551 
552 /**
553  * Placement information for a [FloatingActionButton] inside a [Scaffold].
554  *
555  * @property isDocked whether the FAB should be docked with the bottom bar
556  * @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL
557  *   support
558  * @property width the width of the FAB
559  * @property height the height of the FAB
560  */
561 @Immutable
562 internal class FabPlacement(val isDocked: Boolean, val left: Int, val width: Int, val height: Int)
563 
564 /**
565  * CompositionLocal containing a [FabPlacement] that is read by [BottomAppBar] to calculate notch
566  * location.
567  */
<lambda>null568 internal val LocalFabPlacement = staticCompositionLocalOf<FabPlacement?> { null }
569 
570 // FAB spacing above the bottom bar / bottom of the Scaffold
571 private val FabSpacing = 16.dp
572 
573 private enum class ScaffoldLayoutContent {
574     TopBar,
575     MainContent,
576     Snackbar,
577     Fab,
578     BottomBar
579 }
580