1 /*
<lambda>null2  * Copyright 2024 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.material3
18 
19 import androidx.compose.animation.core.Animatable
20 import androidx.compose.animation.core.AnimationVector1D
21 import androidx.compose.animation.core.animateDpAsState
22 import androidx.compose.animation.core.animateFloatAsState
23 import androidx.compose.foundation.Canvas
24 import androidx.compose.foundation.background
25 import androidx.compose.foundation.gestures.Orientation
26 import androidx.compose.foundation.gestures.detectTapGestures
27 import androidx.compose.foundation.gestures.draggable
28 import androidx.compose.foundation.interaction.Interaction
29 import androidx.compose.foundation.interaction.MutableInteractionSource
30 import androidx.compose.foundation.layout.Arrangement
31 import androidx.compose.foundation.layout.Box
32 import androidx.compose.foundation.layout.Spacer
33 import androidx.compose.foundation.layout.WindowInsets
34 import androidx.compose.foundation.layout.WindowInsetsSides
35 import androidx.compose.foundation.layout.fillMaxHeight
36 import androidx.compose.foundation.layout.fillMaxSize
37 import androidx.compose.foundation.layout.imePadding
38 import androidx.compose.foundation.layout.only
39 import androidx.compose.foundation.layout.padding
40 import androidx.compose.foundation.layout.widthIn
41 import androidx.compose.foundation.layout.windowInsetsPadding
42 import androidx.compose.foundation.selection.selectableGroup
43 import androidx.compose.material3.internal.DraggableAnchors
44 import androidx.compose.material3.internal.Strings
45 import androidx.compose.material3.internal.draggableAnchors
46 import androidx.compose.material3.internal.getString
47 import androidx.compose.material3.internal.systemBarsForVisualComponents
48 import androidx.compose.material3.tokens.MotionSchemeKeyTokens
49 import androidx.compose.material3.tokens.NavigationRailBaselineItemTokens
50 import androidx.compose.material3.tokens.NavigationRailCollapsedTokens
51 import androidx.compose.material3.tokens.NavigationRailColorTokens
52 import androidx.compose.material3.tokens.NavigationRailExpandedTokens
53 import androidx.compose.material3.tokens.NavigationRailHorizontalItemTokens
54 import androidx.compose.material3.tokens.NavigationRailVerticalItemTokens
55 import androidx.compose.material3.tokens.ScrimTokens
56 import androidx.compose.runtime.Composable
57 import androidx.compose.runtime.Immutable
58 import androidx.compose.runtime.LaunchedEffect
59 import androidx.compose.runtime.SideEffect
60 import androidx.compose.runtime.derivedStateOf
61 import androidx.compose.runtime.getValue
62 import androidx.compose.runtime.movableContentOf
63 import androidx.compose.runtime.mutableIntStateOf
64 import androidx.compose.runtime.mutableStateOf
65 import androidx.compose.runtime.remember
66 import androidx.compose.runtime.rememberCoroutineScope
67 import androidx.compose.runtime.setValue
68 import androidx.compose.ui.Modifier
69 import androidx.compose.ui.graphics.Color
70 import androidx.compose.ui.graphics.GraphicsLayerScope
71 import androidx.compose.ui.graphics.Shape
72 import androidx.compose.ui.graphics.TransformOrigin
73 import androidx.compose.ui.graphics.graphicsLayer
74 import androidx.compose.ui.graphics.isSpecified
75 import androidx.compose.ui.graphics.takeOrElse
76 import androidx.compose.ui.input.pointer.pointerInput
77 import androidx.compose.ui.layout.Layout
78 import androidx.compose.ui.layout.Measurable
79 import androidx.compose.ui.layout.MeasurePolicy
80 import androidx.compose.ui.layout.MeasureResult
81 import androidx.compose.ui.layout.MeasureScope
82 import androidx.compose.ui.layout.Placeable
83 import androidx.compose.ui.layout.layoutId
84 import androidx.compose.ui.platform.LocalDensity
85 import androidx.compose.ui.platform.LocalLayoutDirection
86 import androidx.compose.ui.semantics.contentDescription
87 import androidx.compose.ui.semantics.isTraversalGroup
88 import androidx.compose.ui.semantics.onClick
89 import androidx.compose.ui.semantics.paneTitle
90 import androidx.compose.ui.semantics.semantics
91 import androidx.compose.ui.unit.Constraints
92 import androidx.compose.ui.unit.Dp
93 import androidx.compose.ui.unit.LayoutDirection
94 import androidx.compose.ui.unit.constrain
95 import androidx.compose.ui.unit.dp
96 import androidx.compose.ui.unit.offset
97 import androidx.compose.ui.util.fastFirst
98 import androidx.compose.ui.util.fastForEachIndexed
99 import androidx.compose.ui.util.fastMap
100 import androidx.compose.ui.util.lerp
101 import kotlin.math.min
102 import kotlinx.coroutines.channels.Channel
103 import kotlinx.coroutines.launch
104 
105 /**
106  * Material design wide navigation rail.
107  *
108  * Wide navigation rails provide access to primary destinations in apps when using tablet and
109  * desktop screens.
110  *
111  * ![Wide navigation rail collapsed
112  * image](https://developer.android.com/images/reference/androidx/compose/material3/wide-navigation-rail-collapsed.png)
113  *
114  * ![Wide navigation rail expanded
115  * image](https://developer.android.com/images/reference/androidx/compose/material3/wide-navigation-rail-expanded.png)
116  *
117  * The wide navigation rail should be used to display multiple [WideNavigationRailItem]s, each
118  * representing a singular app destination, and, optionally, a header containing a menu button, a
119  * [FloatingActionButton], and/or a logo. Each destination is typically represented by an icon and a
120  * text label.
121  *
122  * The [WideNavigationRail] is collapsed by default, but it also supports being expanded via a
123  * [WideNavigationRailState]. When collapsed, the rail should display three to seven navigation
124  * items. A simple example looks like:
125  *
126  * @sample androidx.compose.material3.samples.WideNavigationRailCollapsedSample
127  *
128  * When expanded, the rail should display at least three navigation items. A simple example looks
129  * like:
130  *
131  * @sample androidx.compose.material3.samples.WideNavigationRailExpandedSample
132  *
133  * The [WideNavigationRail] also supports automatically animating between the collapsed and expanded
134  * values. That can be done like so:
135  *
136  * @sample androidx.compose.material3.samples.WideNavigationRailResponsiveSample
137  *
138  * For a modal variation of the wide navigation rail, see [ModalWideNavigationRail].
139  *
140  * Finally, the [WideNavigationRail] supports setting an [Arrangement.Vertical] for the items, with
141  * [Arrangement.Top] being the default. The header will always be at the top.
142  *
143  * See [WideNavigationRailItem] for configuration specific to each item, and not the overall
144  * [WideNavigationRail] component.
145  *
146  * @param modifier the [Modifier] to be applied to this wide navigation rail
147  * @param state the [WideNavigationRailState] of this wide navigation rail
148  * @param shape defines the shape of this wide navigation rail's container.
149  * @param colors [WideNavigationRailColors] that will be used to resolve the colors used for this
150  *   wide navigation rail. See [WideNavigationRailDefaults.colors]
151  * @param header optional header that may hold a [FloatingActionButton] or a logo
152  * @param windowInsets a window insets of the wide navigation rail
153  * @param arrangement the [Arrangement.Vertical] of this wide navigation rail for its content. Note
154  *   that if there's a header present, the items will be arranged on the remaining space below it,
155  *   except for the center arrangement which considers the entire height of the container
156  * @param content the content of this wide navigation rail, typically [WideNavigationRailItem]s
157  */
158 @ExperimentalMaterial3ExpressiveApi
159 @Composable
160 fun WideNavigationRail(
161     modifier: Modifier = Modifier,
162     state: WideNavigationRailState = rememberWideNavigationRailState(),
163     shape: Shape = WideNavigationRailDefaults.containerShape,
164     colors: WideNavigationRailColors = WideNavigationRailDefaults.colors(),
165     header: @Composable (() -> Unit)? = null,
166     windowInsets: WindowInsets = WideNavigationRailDefaults.windowInsets,
167     arrangement: Arrangement.Vertical = WideNavigationRailDefaults.arrangement,
168     content: @Composable () -> Unit
169 ) {
170     WideNavigationRailLayout(
171         modifier = modifier,
172         isModal = false,
173         expanded = state.targetValue.isExpanded,
174         colors = colors,
175         shape = shape,
176         header = header,
177         windowInsets = windowInsets,
178         arrangement = arrangement,
179         content = content
180     )
181 }
182 
183 @Composable
WideNavigationRailLayoutnull184 private fun WideNavigationRailLayout(
185     modifier: Modifier,
186     isModal: Boolean,
187     expanded: Boolean,
188     colors: WideNavigationRailColors,
189     shape: Shape,
190     header: @Composable (() -> Unit)?,
191     windowInsets: WindowInsets,
192     arrangement: Arrangement.Vertical,
193     content: @Composable () -> Unit
194 ) {
195     var currentWidth by remember { mutableIntStateOf(0) }
196     var actualMaxExpandedWidth by remember { mutableIntStateOf(0) }
197     val minimumA11ySize =
198         if (LocalMinimumInteractiveComponentSize.current == Dp.Unspecified) {
199             0.dp
200         } else {
201             LocalMinimumInteractiveComponentSize.current
202         }
203 
204     // TODO: Load the motionScheme tokens from the component tokens file.
205     val animationSpec = MotionSchemeKeyTokens.DefaultSpatial.value<Dp>()
206     val modalAnimationSpec = MotionSchemeKeyTokens.FastSpatial.value<Dp>()
207     val minWidth by
208         animateDpAsState(
209             targetValue = if (!expanded) CollapsedRailWidth else ExpandedRailMinWidth,
210             animationSpec = if (!isModal) animationSpec else modalAnimationSpec
211         )
212     val widthFullRange by
213         animateDpAsState(
214             targetValue = if (!expanded) CollapsedRailWidth else ExpandedRailMaxWidth,
215             animationSpec = if (!isModal) animationSpec else modalAnimationSpec
216         )
217     val itemVerticalSpacedBy by
218         animateDpAsState(
219             targetValue = if (!expanded) NavigationRailCollapsedTokens.ItemVerticalSpace else 0.dp,
220             animationSpec = animationSpec
221         )
222     val itemMinHeight by
223         animateDpAsState(
224             targetValue = if (!expanded) TopIconItemMinHeight else minimumA11ySize,
225             animationSpec = animationSpec
226         )
227 
228     Surface(
229         color = if (!isModal) colors.containerColor else colors.modalContainerColor,
230         contentColor = colors.contentColor,
231         shape = shape,
232         modifier = modifier,
233     ) {
234         Layout(
235             modifier =
236                 Modifier.fillMaxHeight()
237                     .windowInsetsPadding(windowInsets)
238                     .widthIn(max = ExpandedRailMaxWidth)
239                     .padding(top = WNRVerticalPadding)
240                     .selectableGroup()
241                     .semantics { isTraversalGroup = true },
242             content = {
243                 if (header != null) {
244                     Box(Modifier.layoutId(HeaderLayoutIdTag)) { header() }
245                 }
246                 content()
247             },
248             measurePolicy =
249                 object : MeasurePolicy {
250                     override fun MeasureScope.measure(
251                         measurables: List<Measurable>,
252                         constraints: Constraints
253                     ): MeasureResult {
254                         val height = constraints.maxHeight
255                         var itemsCount = measurables.size
256                         var actualExpandedMinWidth = constraints.minWidth
257                         val actualMinWidth =
258                             if (constraints.minWidth == 0) {
259                                 actualExpandedMinWidth =
260                                     ExpandedRailMinWidth.roundToPx()
261                                         .coerceAtMost(constraints.maxWidth)
262                                 minWidth.roundToPx().coerceAtMost(constraints.maxWidth)
263                             } else {
264                                 constraints.minWidth
265                             }
266                         // If there are no items, rail will be empty.
267                         if (itemsCount < 1) {
268                             return layout(actualMinWidth, height) {}
269                         }
270                         val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
271                         var itemsMeasurables = measurables
272 
273                         var constraintsOffset = 0
274                         var headerPlaceable: Placeable? = null
275                         if (header != null) {
276                             headerPlaceable =
277                                 measurables
278                                     .fastFirst { it.layoutId == HeaderLayoutIdTag }
279                                     .measure(looseConstraints)
280                             // Header is always first element in measurables list.
281                             if (itemsCount > 1)
282                                 itemsMeasurables = measurables.subList(1, itemsCount)
283                             // Real item count doesn't include the header.
284                             itemsCount--
285                             constraintsOffset = headerPlaceable.height
286                         }
287 
288                         val itemsPlaceables =
289                             if (itemsCount > 0) mutableListOf<Placeable>() else null
290                         val itemMaxWidthConstraint =
291                             if (expanded) looseConstraints.maxWidth else actualMinWidth
292                         var expandedItemMaxWidth = 0
293                         if (itemsPlaceables != null) {
294                             itemsMeasurables.fastMap {
295                                 val measuredItem =
296                                     it.measure(
297                                         looseConstraints
298                                             .offset(vertical = -constraintsOffset)
299                                             .constrain(
300                                                 Constraints.fitPrioritizingWidth(
301                                                     minWidth = minimumA11ySize.roundToPx(),
302                                                     minHeight = itemMinHeight.roundToPx(),
303                                                     maxWidth = itemMaxWidthConstraint,
304                                                     maxHeight = looseConstraints.maxHeight,
305                                                 )
306                                             )
307                                     )
308                                 val maxItemWidth = measuredItem.measuredWidth
309                                 if (expanded && expandedItemMaxWidth < maxItemWidth) {
310                                     expandedItemMaxWidth =
311                                         maxItemWidth + ItemHorizontalPadding.roundToPx()
312                                 }
313                                 constraintsOffset = measuredItem.height
314                                 itemsPlaceables.add(measuredItem)
315                             }
316                         }
317 
318                         var width = actualMinWidth
319                         // Limit collapsed rail to fixed width, but expanded rail can be as wide as
320                         // constraints.maxWidth
321                         if (expanded) {
322                             val widestElementWidth =
323                                 maxOf(expandedItemMaxWidth, headerPlaceable?.width ?: 0)
324 
325                             if (
326                                 widestElementWidth > actualMinWidth &&
327                                     widestElementWidth > actualExpandedMinWidth
328                             ) {
329                                 val widthConstrain =
330                                     maxOf(widestElementWidth, actualExpandedMinWidth)
331                                         .coerceAtMost(constraints.maxWidth)
332                                 // Use widthFullRange so there's no jump in animation for when the
333                                 // expanded width has to be wider than actualExpandedMinWidth.
334                                 width = widthFullRange.roundToPx().coerceAtMost(widthConstrain)
335                                 actualMaxExpandedWidth = width
336                             }
337                         } else {
338                             if (actualMaxExpandedWidth > 0) {
339                                 // Use widthFullRange so there's no jump in animation for the case
340                                 // when the expanded width was wider than actualExpandedMinWidth.
341                                 width =
342                                     widthFullRange
343                                         .roundToPx()
344                                         .coerceIn(
345                                             minimumValue = actualMinWidth,
346                                             maximumValue =
347                                                 currentWidth.coerceAtLeast(actualMinWidth)
348                                         )
349                             }
350                         }
351                         currentWidth = width
352 
353                         return layout(width, height) {
354                             val railHeight = height - WNRVerticalPadding.roundToPx()
355                             var headerOffset = 0
356                             if (headerPlaceable != null && headerPlaceable.height > 0) {
357                                 headerPlaceable.placeRelative(0, 0)
358                                 headerOffset +=
359                                     headerPlaceable.height + WNRHeaderPadding.roundToPx()
360                             }
361 
362                             if (itemsPlaceables != null) {
363                                 val layoutSize =
364                                     if (arrangement == Arrangement.Center) {
365                                         // For centered arrangement the items will be centered in
366                                         // the container, not in the remaining space below the
367                                         // header.
368                                         railHeight
369                                     } else {
370                                         railHeight - headerOffset
371                                     }
372                                 val sizes = IntArray(itemsPlaceables.size)
373                                 itemsPlaceables.fastForEachIndexed { index, item ->
374                                     sizes[index] = item.height
375                                     if (index < itemsPlaceables.size - 1) {
376                                         sizes[index] += itemVerticalSpacedBy.roundToPx()
377                                     }
378                                 }
379                                 val y = IntArray(itemsPlaceables.size)
380                                 with(arrangement) { arrange(layoutSize, sizes, y) }
381 
382                                 val offset =
383                                     if (arrangement == Arrangement.Center) 0 else headerOffset
384                                 itemsPlaceables.fastForEachIndexed { index, item ->
385                                     item.placeRelative(0, y[index] + offset)
386                                 }
387                             }
388                         }
389                     }
390                 }
391         )
392     }
393 }
394 
395 /**
396  * Material design modal wide navigation rail.
397  *
398  * Wide navigation rails provide access to primary destinations in apps when using tablet and
399  * desktop screens.
400  *
401  * The modal wide navigation rail should be used to display multiple [WideNavigationRailItem]s, each
402  * representing a singular app destination, and, optionally, a header containing a menu button, a
403  * [FloatingActionButton], and/or a logo. Each destination is typically represented by an icon and a
404  * text label.
405  *
406  * The [ModalWideNavigationRail] when collapsed behaves like a collapsed [WideNavigationRail]. When
407  * expanded, the modal wide navigation rail blocks interaction with the rest of an app’s content
408  * with a scrim. It is elevated above the app’s UI and doesn't affect the screen’s layout grid. That
409  * can be achieved like so:
410  *
411  * @sample androidx.compose.material3.samples.ModalWideNavigationRailSample
412  *
413  * For a dismissible [ModalWideNavigationRail], that enters from offscreen instead of expanding from
414  * the collapsed rail, set [hideOnCollapse] to true. That can be achieved like so:
415  *
416  * @sample androidx.compose.material3.samples.DismissibleModalWideNavigationRailSample
417  *
418  * See [WideNavigationRailItem] for configuration specific to each item, and not the overall
419  * [ModalWideNavigationRail] component.
420  *
421  * @param modifier the [Modifier] to be applied to this wide navigation rail
422  * @param state the [WideNavigationRailState] of this wide navigation rail
423  * @param hideOnCollapse whether this wide navigation rail should slide offscreen when it collapses
424  *   and be hidden, or stay on screen as a collapsed wide navigation rail (default)
425  * @param collapsedShape the shape of this wide navigation rail's container when it's collapsed
426  * @param expandedShape the shape of this wide navigation rail's container when it's expanded
427  * @param colors [WideNavigationRailColors] that will be used to resolve the colors used for this
428  *   wide navigation rail. See [WideNavigationRailDefaults.colors]
429  * @param header optional header that may hold a [FloatingActionButton] or a logo
430  * @param expandedHeaderTopPadding the padding to be applied to the top of the rail. It's usually
431  *   needed in order to align the content of the rail between the collapsed and expanded animation
432  * @param windowInsets a window insets of the wide navigation rail
433  * @param arrangement the [Arrangement.Vertical] of this wide navigation rail
434  * @param expandedProperties [ModalWideNavigationRailProperties] for further customization of the
435  *   expanded modal wide navigation rail's window behavior
436  * @param content the content of this modal wide navigation rail, usually [WideNavigationRailItem]s
437  */
438 @ExperimentalMaterial3ExpressiveApi
439 @Composable
ModalWideNavigationRailnull440 fun ModalWideNavigationRail(
441     modifier: Modifier = Modifier,
442     state: WideNavigationRailState = rememberWideNavigationRailState(),
443     hideOnCollapse: Boolean = false,
444     collapsedShape: Shape = WideNavigationRailDefaults.containerShape,
445     expandedShape: Shape = WideNavigationRailDefaults.modalContainerShape,
446     colors: WideNavigationRailColors = WideNavigationRailDefaults.colors(),
447     header: @Composable (() -> Unit)? = null,
448     expandedHeaderTopPadding: Dp = 0.dp,
449     windowInsets: WindowInsets = WideNavigationRailDefaults.windowInsets,
450     arrangement: Arrangement.Vertical = WideNavigationRailDefaults.arrangement,
451     expandedProperties: ModalWideNavigationRailProperties =
452         ModalWideNavigationRailDefaults.Properties,
453     content: @Composable () -> Unit
454 ) {
455     val rememberContent =
456         if (hideOnCollapse) {
457             content
458         } else remember(content) { movableContentOf(content) }
459 
460     val density = LocalDensity.current
461     // TODO: Load the motionScheme tokens from the component tokens file.
462     val modalStateAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value<Float>()
463     val modalState =
464         remember(state) {
465             ModalWideNavigationRailState(
466                 state = state,
467                 density = density,
468                 animationSpec = modalStateAnimationSpec,
469             )
470         }
471     val positionProgress =
472         animateFloatAsState(
473             targetValue = if (!state.targetValue.isExpanded) 0f else 1f,
474             // TODO: Load the motionScheme tokens from the component tokens file.
475             animationSpec = MotionSchemeKeyTokens.DefaultEffects.value()
476         )
477     val isCollapsed: Boolean by remember { derivedStateOf { positionProgress.value == 0f } }
478     val modalExpanded: Boolean by remember { derivedStateOf { positionProgress.value >= 0.3f } }
479     val animateToDismiss: suspend () -> Unit = {
480         if (hideOnCollapse) {
481             modalState.collapse()
482         }
483         state.collapse()
484     }
485 
486     val settleToDismiss: suspend (velocity: Float) -> Unit = {
487         if (hideOnCollapse) {
488             modalState.settle(it)
489             if (!modalState.targetValue.isExpanded) state.collapse()
490         }
491     }
492 
493     // Display a non modal rail when collapsed.
494     if (!hideOnCollapse && isCollapsed) {
495         WideNavigationRailLayout(
496             modifier = modifier,
497             isModal = false,
498             expanded = false,
499             colors = colors,
500             shape = collapsedShape,
501             header = header,
502             windowInsets = windowInsets,
503             arrangement = arrangement,
504             content = rememberContent
505         )
506     }
507 
508     val channel = remember { Channel<Boolean>(Channel.CONFLATED) }
509     if (hideOnCollapse) {
510         LaunchedEffect(channel) {
511             for (target in channel) {
512                 val newTarget = channel.tryReceive().getOrNull() ?: target
513                 launch {
514                     if (newTarget) {
515                         modalState.expand()
516                     } else {
517                         modalState.collapse()
518                     }
519                 }
520             }
521         }
522     }
523 
524     // Display a modal container when expanded.
525     if (!isCollapsed) {
526         if (!hideOnCollapse) {
527             // Have a spacer the size of the collapsed rail so that screen content doesn't shift.
528             Box(Modifier.background(color = colors.containerColor, shape = collapsedShape)) {
529                 Spacer(modifier = modifier.widthIn(min = CollapsedRailWidth).fillMaxHeight())
530             }
531         }
532 
533         val scope = rememberCoroutineScope()
534         val predictiveBackProgress = remember { Animatable(initialValue = 0f) }
535         val predictiveBackState = remember { RailPredictiveBackState() }
536 
537         SideEffect { channel.trySend(state.targetValue.isExpanded) }
538 
539         ModalWideNavigationRailDialog(
540             properties = expandedProperties,
541             onDismissRequest = { scope.launch { state.collapse() } },
542             onPredictiveBack = { backEvent ->
543                 scope.launch { predictiveBackProgress.snapTo(backEvent) }
544             },
545             onPredictiveBackCancelled = { scope.launch { predictiveBackProgress.animateTo(0f) } },
546             predictiveBackState = predictiveBackState
547         ) {
548             Box(modifier = Modifier.fillMaxSize().imePadding()) {
549                 val isScrimVisible =
550                     if (hideOnCollapse) {
551                         (modalState.targetValue != WideNavigationRailValue.Collapsed)
552                     } else {
553                         modalExpanded
554                     }
555 
556                 Scrim(
557                     color = colors.modalScrimColor,
558                     onDismissRequest = animateToDismiss,
559                     visible = isScrimVisible
560                 )
561 
562                 ModalWideNavigationRailContent(
563                     expanded = hideOnCollapse || modalExpanded,
564                     isStandaloneModal = hideOnCollapse,
565                     predictiveBackProgress = predictiveBackProgress,
566                     predictiveBackState = predictiveBackState,
567                     settleToDismiss = settleToDismiss,
568                     modifier = modifier,
569                     railState = modalState,
570                     colors = colors,
571                     shape = expandedShape,
572                     openModalRailMaxWidth = ExpandedRailMaxWidth,
573                     header = {
574                         Box(
575                             modifier = Modifier.padding(top = expandedHeaderTopPadding),
576                         ) {
577                             header?.invoke()
578                         }
579                     },
580                     windowInsets = windowInsets,
581                     gesturesEnabled = hideOnCollapse,
582                     arrangement = arrangement,
583                     content = rememberContent
584                 )
585             }
586         }
587     }
588 }
589 
590 /**
591  * Material Design wide navigation rail item.
592  *
593  * It's recommend for navigation items to always have a text label. A [WideNavigationRailItem]
594  * always displays labels (if they exist) when selected and unselected.
595  *
596  * The [WideNavigationRailItem] supports two different icon positions, top and start, which is
597  * controlled by the [iconPosition] param:
598  * - If the icon position is [NavigationItemIconPosition.Top] the icon will be displayed above the
599  *   label. This configuration should be used with collapsed wide navigation rails.
600  * - If the icon position is [NavigationItemIconPosition.Start] the icon will be displayed to the
601  *   start of the label. This configuration should be used with expanded wide navigation rails.
602  *
603  * However, if an animated item is desired, the [iconPosition] can be controlled via the expanded
604  * value of the associated [WideNavigationRail] or [ModalWideNavigationRail]. By default, it'll use
605  * the [railExpanded] to follow the configuration described above.
606  *
607  * @param selected whether this item is selected
608  * @param onClick called when this item is clicked
609  * @param icon icon for this item, typically an [Icon]
610  * @param label text label for this item
611  * @param modifier the [Modifier] to be applied to this item
612  * @param enabled controls the enabled state of this item. When `false`, this component will not
613  *   respond to user input, and it will appear visually disabled and disabled to accessibility
614  *   services.
615  * @param railExpanded whether the associated [WideNavigationRail] is expanded or collapsed
616  * @param iconPosition the [NavigationItemIconPosition] for the icon
617  * @param colors [NavigationItemColors] that will be used to resolve the colors used for this item
618  *   in different states. See [WideNavigationRailItemDefaults.colors]
619  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
620  *   emitting [Interaction]s for this item. You can use this to change the item's appearance or
621  *   preview the item in different states. Note that if `null` is provided, interactions will still
622  *   happen internally.
623  */
624 @ExperimentalMaterial3ExpressiveApi
625 @Composable
WideNavigationRailItemnull626 fun WideNavigationRailItem(
627     selected: Boolean,
628     onClick: () -> Unit,
629     icon: @Composable () -> Unit,
630     label: @Composable (() -> Unit)?,
631     modifier: Modifier = Modifier,
632     enabled: Boolean = true,
633     railExpanded: Boolean = false,
634     iconPosition: NavigationItemIconPosition =
635         WideNavigationRailItemDefaults.iconPositionFor(railExpanded),
636     colors: NavigationItemColors = WideNavigationRailItemDefaults.colors(),
637     interactionSource: MutableInteractionSource? = null,
638 ) {
639     @Suppress("NAME_SHADOWING")
640     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
641 
642     AnimatedNavigationItem(
643         selected = selected,
644         onClick = onClick,
645         icon = icon,
646         indicatorShape = NavigationRailBaselineItemTokens.ActiveIndicatorShape.value,
647         topIconIndicatorWidth = NavigationRailVerticalItemTokens.ActiveIndicatorWidth,
648         topIconLabelTextStyle = NavigationRailVerticalItemTokens.LabelTextFont.value,
649         startIconLabelTextStyle = NavigationRailHorizontalItemTokens.LabelTextFont.value,
650         topIconIndicatorHorizontalPadding = ItemTopIconIndicatorHorizontalPadding,
651         topIconIndicatorVerticalPadding = ItemTopIconIndicatorVerticalPadding,
652         topIconIndicatorToLabelVerticalPadding = NavigationRailVerticalItemTokens.IconLabelSpace,
653         startIconIndicatorHorizontalPadding =
654             NavigationRailHorizontalItemTokens.FullWidthLeadingSpace,
655         startIconIndicatorVerticalPadding = ItemStartIconIndicatorVerticalPadding,
656         noLabelIndicatorPadding = WNRItemNoLabelIndicatorPadding,
657         startIconToLabelHorizontalPadding = NavigationRailHorizontalItemTokens.IconLabelSpace,
658         itemHorizontalPadding = ItemHorizontalPadding,
659         colors = colors,
660         modifier = modifier,
661         enabled = enabled,
662         label = label,
663         iconPosition = iconPosition,
664         interactionSource = interactionSource,
665     )
666 }
667 
668 /**
669  * Represents the colors of the various elements of a wide navigation rail.
670  *
671  * @param containerColor the color used for the background of a non-modal wide navigation rail. Use
672  *   [Color.Transparent] to have no color
673  * @param contentColor the preferred color for content inside a wide navigation rail. Defaults to
674  *   either the matching content color for [containerColor], or to the current [LocalContentColor]
675  *   if [containerColor] is not a color from the theme
676  * @param modalContainerColor the color used for the background of a modal wide navigation rail. Use
677  *   [Color.Transparent] to have no color
678  * @param modalScrimColor the color used for the scrim overlay for background content of a modal
679  *   wide navigation rail
680  */
681 @Immutable
682 class WideNavigationRailColors(
683     val containerColor: Color,
684     val contentColor: Color,
685     val modalContainerColor: Color,
686     val modalScrimColor: Color,
687 ) {
688     /**
689      * Returns a copy of this NavigationRailColors, optionally overriding some of the values. This
690      * uses the Color.Unspecified to mean “use the value from the source”.
691      */
copynull692     fun copy(
693         containerColor: Color = this.containerColor,
694         contentColor: Color = this.contentColor,
695         modalContainerColor: Color = this.modalContainerColor,
696         modalScrimColor: Color = this.modalScrimColor,
697     ) =
698         WideNavigationRailColors(
699             containerColor = containerColor.takeOrElse { this.containerColor },
<lambda>null700             contentColor = contentColor.takeOrElse { this.contentColor },
<lambda>null701             modalContainerColor = modalContainerColor.takeOrElse { this.modalContainerColor },
<lambda>null702             modalScrimColor = modalScrimColor.takeOrElse { this.modalScrimColor },
703         )
704 
equalsnull705     override fun equals(other: Any?): Boolean {
706         if (this === other) return true
707         if (other == null || other !is WideNavigationRailColors) return false
708 
709         if (containerColor != other.containerColor) return false
710         if (contentColor != other.contentColor) return false
711         if (modalContainerColor != other.modalContainerColor) return false
712         if (modalScrimColor != other.modalScrimColor) return false
713 
714         return true
715     }
716 
hashCodenull717     override fun hashCode(): Int {
718         var result = containerColor.hashCode()
719         result = 31 * result + contentColor.hashCode()
720         result = 31 * result + modalContainerColor.hashCode()
721         result = 31 * result + modalScrimColor.hashCode()
722 
723         return result
724     }
725 }
726 
727 /** Defaults used in [WideNavigationRail]. */
728 @ExperimentalMaterial3ExpressiveApi
729 object WideNavigationRailDefaults {
730     /** Default container shape of a wide navigation rail. */
731     val containerShape: Shape
732         @Composable get() = NavigationRailCollapsedTokens.ContainerShape.value
733 
734     /** Default container shape of a modal wide navigation rail. */
735     val modalContainerShape: Shape
736         @Composable get() = NavigationRailExpandedTokens.ModalContainerShape.value
737 
738     /** Default arrangement for a wide navigation rail. */
739     val arrangement: Arrangement.Vertical
740         get() = Arrangement.Top
741 
742     /** Default window insets for a wide navigation rail. */
743     val windowInsets: WindowInsets
744         @Composable
745         get() =
746             WindowInsets.systemBarsForVisualComponents.only(
747                 WindowInsetsSides.Vertical + WindowInsetsSides.Start
748             )
749 
750     /**
751      * Creates a [WideNavigationRailColors] with the provided colors according to the Material
752      * specification.
753      */
colorsnull754     @Composable fun colors() = MaterialTheme.colorScheme.defaultWideWideNavigationRailColors
755 
756     /**
757      * Creates a [WideNavigationRailColors] with the provided colors according to the Material
758      * specification.
759      *
760      * @param containerColor the color used for the background of a non-modal wide navigation rail.
761      * @param contentColor the preferred color for content inside a wide navigation rail. Defaults
762      *   to either the matching content color for [containerColor], or to the current
763      *   [LocalContentColor] if [containerColor] is not a color from the theme
764      * @param modalContainerColor the color used for the background of a modal wide navigation rail.
765      * @param modalScrimColor the color used for the scrim overlay for background content of a modal
766      *   wide navigation rail
767      */
768     @Composable
769     fun colors(
770         containerColor: Color = WideNavigationRailDefaults.containerColor,
771         contentColor: Color = contentColorFor(containerColor),
772         modalContainerColor: Color = NavigationRailExpandedTokens.ModalContainerColor.value,
773         modalScrimColor: Color = ScrimTokens.ContainerColor.value.copy(ScrimTokens.ContainerOpacity)
774     ): WideNavigationRailColors =
775         MaterialTheme.colorScheme.defaultWideWideNavigationRailColors.copy(
776             containerColor = containerColor,
777             contentColor = contentColor,
778             modalContainerColor = modalContainerColor,
779             modalScrimColor = modalScrimColor
780         )
781 
782     private val containerColor: Color
783         @Composable get() = NavigationRailCollapsedTokens.ContainerColor.value
784 
785     private val ColorScheme.defaultWideWideNavigationRailColors: WideNavigationRailColors
786         @Composable
787         get() {
788             return defaultWideWideNavigationRailColorsCached
789                 ?: WideNavigationRailColors(
790                         containerColor = containerColor,
791                         contentColor = contentColorFor(containerColor),
792                         modalContainerColor =
793                             fromToken(NavigationRailExpandedTokens.ModalContainerColor),
794                         modalScrimColor =
795                             ScrimTokens.ContainerColor.value.copy(ScrimTokens.ContainerOpacity)
796                     )
797                     .also { defaultWideWideNavigationRailColorsCached = it }
798         }
799 }
800 
801 /** Defaults used in [WideNavigationRailItem]. */
802 @ExperimentalMaterial3ExpressiveApi
803 object WideNavigationRailItemDefaults {
804     /**
805      * The default icon position of a [WideNavigationRailItem] given whether the associated
806      * [WideNavigationRail] is collapsed or expanded.
807      */
iconPositionFornull808     fun iconPositionFor(railExpanded: Boolean) =
809         if (railExpanded) NavigationItemIconPosition.Start else NavigationItemIconPosition.Top
810 
811     /**
812      * Creates a [NavigationItemColors] with the provided colors according to the Material
813      * specification.
814      */
815     @Composable fun colors() = MaterialTheme.colorScheme.defaultWideNavigationRailItemColors
816 
817     /**
818      * Creates a [NavigationItemColors] with the provided colors according to the Material
819      * specification.
820      *
821      * @param selectedIconColor the color to use for the icon when the item is selected.
822      * @param selectedTextColor the color to use for the text label when the item is selected.
823      * @param selectedIndicatorColor the color to use for the indicator when the item is selected.
824      * @param unselectedIconColor the color to use for the icon when the item is unselected.
825      * @param unselectedTextColor the color to use for the text label when the item is unselected.
826      * @param disabledIconColor the color to use for the icon when the item is disabled.
827      * @param disabledTextColor the color to use for the text label when the item is disabled.
828      * @return the resulting [NavigationItemColors] used for [WideNavigationRailItem]
829      */
830     @Composable
831     fun colors(
832         selectedIconColor: Color = NavigationRailColorTokens.ItemActiveIcon.value,
833         selectedTextColor: Color = NavigationRailColorTokens.ItemActiveLabelText.value,
834         selectedIndicatorColor: Color = NavigationRailColorTokens.ItemActiveIndicator.value,
835         unselectedIconColor: Color = NavigationRailColorTokens.ItemInactiveIcon.value,
836         unselectedTextColor: Color = NavigationRailColorTokens.ItemInactiveLabelText.value,
837         disabledIconColor: Color = unselectedIconColor.copy(alpha = DisabledAlpha),
838         disabledTextColor: Color = unselectedTextColor.copy(alpha = DisabledAlpha),
839     ): NavigationItemColors =
840         MaterialTheme.colorScheme.defaultWideNavigationRailItemColors.copy(
841             selectedIconColor = selectedIconColor,
842             selectedTextColor = selectedTextColor,
843             selectedIndicatorColor = selectedIndicatorColor,
844             unselectedIconColor = unselectedIconColor,
845             unselectedTextColor = unselectedTextColor,
846             disabledIconColor = disabledIconColor,
847             disabledTextColor = disabledTextColor,
848         )
849 
850     private val ColorScheme.defaultWideNavigationRailItemColors: NavigationItemColors
851         get() {
852             return defaultWideNavigationRailItemColorsCached
853                 ?: NavigationItemColors(
854                         selectedIconColor = fromToken(NavigationRailColorTokens.ItemActiveIcon),
855                         selectedTextColor =
856                             fromToken(NavigationRailColorTokens.ItemActiveLabelText),
857                         selectedIndicatorColor =
858                             fromToken(NavigationRailColorTokens.ItemActiveIndicator),
859                         unselectedIconColor = fromToken(NavigationRailColorTokens.ItemInactiveIcon),
860                         unselectedTextColor =
861                             fromToken(NavigationRailColorTokens.ItemInactiveLabelText),
862                         disabledIconColor =
863                             fromToken(NavigationRailColorTokens.ItemInactiveIcon)
864                                 .copy(alpha = DisabledAlpha),
865                         disabledTextColor =
866                             fromToken(NavigationRailColorTokens.ItemInactiveLabelText)
867                                 .copy(alpha = DisabledAlpha),
868                     )
869                     .also { defaultWideNavigationRailItemColorsCached = it }
870         }
871 }
872 
873 /** Default values for [ModalWideNavigationRail]. */
874 @Immutable
875 @ExperimentalMaterial3ExpressiveApi
876 object ModalWideNavigationRailDefaults {
877 
878     /** Properties used to customize the window behavior of a [ModalWideNavigationRail]. */
879     val Properties: ModalWideNavigationRailProperties =
880         createDefaultModalWideNavigationRailProperties()
881 }
882 
883 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
createDefaultModalWideNavigationRailPropertiesnull884 internal expect fun createDefaultModalWideNavigationRailProperties():
885     ModalWideNavigationRailProperties
886 
887 @Immutable
888 @ExperimentalMaterial3ExpressiveApi
889 expect class ModalWideNavigationRailProperties(
890     shouldDismissOnBackPress: Boolean = true,
891 ) {
892     val shouldDismissOnBackPress: Boolean
893 }
894 
895 @ExperimentalMaterial3ExpressiveApi
896 @Composable
897 internal expect fun ModalWideNavigationRailDialog(
898     onDismissRequest: () -> Unit,
899     properties: ModalWideNavigationRailProperties,
900     onPredictiveBack: (Float) -> Unit,
901     onPredictiveBackCancelled: () -> Unit,
902     predictiveBackState: RailPredictiveBackState,
903     content: @Composable () -> Unit
904 )
905 
906 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
907 @Composable
ModalWideNavigationRailContentnull908 private fun ModalWideNavigationRailContent(
909     expanded: Boolean,
910     isStandaloneModal: Boolean,
911     predictiveBackProgress: Animatable<Float, AnimationVector1D>,
912     predictiveBackState: RailPredictiveBackState,
913     settleToDismiss: suspend (velocity: Float) -> Unit,
914     modifier: Modifier,
915     railState: ModalWideNavigationRailState,
916     colors: WideNavigationRailColors,
917     shape: Shape,
918     openModalRailMaxWidth: Dp,
919     header: @Composable (() -> Unit)?,
920     windowInsets: WindowInsets,
921     gesturesEnabled: Boolean,
922     arrangement: Arrangement.Vertical,
923     content: @Composable () -> Unit
924 ) {
925     val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
926     val railPaneTitle = getString(string = Strings.WideNavigationRailPaneTitle)
927 
928     Surface(
929         shape = shape,
930         color = colors.modalContainerColor,
931         modifier =
932             modifier
933                 .widthIn(max = openModalRailMaxWidth)
934                 .fillMaxHeight()
935                 .semantics { paneTitle = railPaneTitle }
936                 .graphicsLayer {
937                     val progress = predictiveBackProgress.value
938                     if (progress <= 0f) {
939                         return@graphicsLayer
940                     }
941                     val offset = railState.currentOffset
942                     val width = size.width
943                     if (!offset.isNaN() && !width.isNaN() && width != 0f) {
944                         // Apply the predictive back animation.
945                         scaleX =
946                             calculatePredictiveBackScaleX(
947                                 progress,
948                                 predictiveBackState.swipeEdgeMatchesRail
949                             )
950                         scaleY = calculatePredictiveBackScaleY(progress)
951                         transformOrigin =
952                             TransformOrigin(if (isRtl) 1f else 0f, PredictiveBackPivotFractionY)
953                     }
954                 }
955                 .draggableAnchors(railState.anchoredDraggableState, Orientation.Horizontal) {
956                     railSize,
957                     _ ->
958                     val width = railSize.width.toFloat()
959                     val minValue =
960                         if (isStandaloneModal) {
961                             if (isRtl) width else -width
962                         } else {
963                             0f
964                         }
965                     val maxValue = 0f
966                     return@draggableAnchors DraggableAnchors {
967                         WideNavigationRailValue.Collapsed at minValue
968                         WideNavigationRailValue.Expanded at maxValue
969                     } to railState.targetValue
970                 }
971                 .draggable(
972                     state = railState.anchoredDraggableState.draggableState,
973                     orientation = Orientation.Horizontal,
974                     enabled = gesturesEnabled,
975                     startDragImmediately = railState.anchoredDraggableState.isAnimationRunning,
976                     onDragStopped = { settleToDismiss(it) },
977                 )
978     ) {
979         WideNavigationRailLayout(
980             modifier =
981                 Modifier.graphicsLayer {
982                     val progress = predictiveBackProgress.value
983                     if (progress <= 0) {
984                         return@graphicsLayer
985                     }
986                     // Preserve the original aspect ratio and alignment due to the predictive back
987                     // animation.
988                     val predictiveBackScaleX =
989                         calculatePredictiveBackScaleX(
990                             progress,
991                             predictiveBackState.swipeEdgeMatchesRail
992                         )
993                     val predictiveBackScaleY = calculatePredictiveBackScaleY(progress)
994                     scaleX =
995                         if (predictiveBackScaleX != 0f) predictiveBackScaleY / predictiveBackScaleX
996                         else 1f
997                     transformOrigin =
998                         TransformOrigin(if (isRtl) 0f else 1f, PredictiveBackPivotFractionY)
999                 },
1000             expanded = expanded,
1001             shape = shape,
1002             colors = colors,
1003             header = header,
1004             windowInsets = windowInsets,
1005             arrangement = arrangement,
1006             isModal = true,
1007             content = content
1008         )
1009     }
1010 }
1011 
GraphicsLayerScopenull1012 private fun GraphicsLayerScope.calculatePredictiveBackScaleX(
1013     progress: Float,
1014     swipeEdgeMatchesRail: Boolean,
1015 ): Float {
1016     val width = size.width
1017     return if (width.isNaN() || width == 0f) {
1018         1f
1019     } else {
1020         val scaleXDirection = if (swipeEdgeMatchesRail) 1f else -1f
1021         1f +
1022             (scaleXDirection *
1023                 lerp(0f, min(PredictiveBackMaxScaleXDistance.toPx(), width), progress)) / width
1024     }
1025 }
1026 
GraphicsLayerScopenull1027 private fun GraphicsLayerScope.calculatePredictiveBackScaleY(
1028     progress: Float,
1029 ): Float {
1030     val height = size.height
1031     return if (height.isNaN() || height == 0f) {
1032         1f
1033     } else {
1034         1f - lerp(0f, min(PredictiveBackMaxScaleYDistance.toPx(), height), progress) / height
1035     }
1036 }
1037 
1038 @Composable
Scrimnull1039 private fun Scrim(color: Color, onDismissRequest: suspend () -> Unit, visible: Boolean) {
1040     if (color.isSpecified) {
1041         val alpha by
1042             animateFloatAsState(
1043                 targetValue = if (visible) 1f else 0f,
1044                 // TODO: Load the motionScheme tokens from the component tokens file.
1045                 animationSpec = MotionSchemeKeyTokens.DefaultEffects.value()
1046             )
1047         var dismiss by remember { mutableStateOf(false) }
1048         val closeModalRail = getString(Strings.CloseRail)
1049         val dismissModalRail =
1050             if (visible) {
1051                 Modifier.pointerInput(onDismissRequest) { detectTapGestures { dismiss = true } }
1052                     .semantics(mergeDescendants = true) {
1053                         contentDescription = closeModalRail
1054                         onClick {
1055                             dismiss = true
1056                             true
1057                         }
1058                     }
1059             } else {
1060                 Modifier
1061             }
1062         Canvas(Modifier.fillMaxSize().then(dismissModalRail)) {
1063             drawRect(color = color, alpha = alpha.coerceIn(0f, 1f))
1064         }
1065 
1066         LaunchedEffect(dismiss) { if (dismiss) onDismissRequest() }
1067     }
1068 }
1069 
1070 /*@VisibleForTesting*/
1071 internal val WNRItemNoLabelIndicatorPadding =
1072     (NavigationRailVerticalItemTokens.ActiveIndicatorWidth -
1073         NavigationRailBaselineItemTokens.IconSize) / 2
1074 
1075 private val ItemHorizontalPadding = 20.dp
1076 // Vertical padding between the contents of the wide navigation rail and its top/bottom.
1077 private val WNRVerticalPadding = NavigationRailCollapsedTokens.TopSpace
1078 // Padding at the bottom of the rail's header. This padding will only be added when the header is
1079 // not null and the rail arrangement is Top.
1080 private val WNRHeaderPadding: Dp = NavigationRailBaselineItemTokens.HeaderSpaceMinimum
1081 private val CollapsedRailWidth = NavigationRailCollapsedTokens.ContainerWidth
1082 private val ExpandedRailMinWidth = NavigationRailExpandedTokens.ContainerWidthMinimum
1083 private val ExpandedRailMaxWidth = NavigationRailExpandedTokens.ContainerWidthMaximum
1084 private val TopIconItemMinHeight = NavigationRailBaselineItemTokens.ContainerHeight
1085 private val ItemTopIconIndicatorVerticalPadding =
1086     (NavigationRailVerticalItemTokens.ActiveIndicatorHeight -
1087         NavigationRailBaselineItemTokens.IconSize) / 2
1088 private val ItemTopIconIndicatorHorizontalPadding =
1089     (NavigationRailVerticalItemTokens.ActiveIndicatorWidth -
1090         NavigationRailBaselineItemTokens.IconSize) / 2
1091 private val ItemStartIconIndicatorVerticalPadding =
1092     (NavigationRailHorizontalItemTokens.ActiveIndicatorHeight -
1093         NavigationRailBaselineItemTokens.IconSize) / 2
1094 private val PredictiveBackMaxScaleXDistance = 24.dp
1095 private val PredictiveBackMaxScaleYDistance = 48.dp
1096 
1097 private const val PredictiveBackPivotFractionY = 0.5f
1098 private const val HeaderLayoutIdTag: String = "header"
1099