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.FiniteAnimationSpec
20 import androidx.compose.foundation.BorderStroke
21 import androidx.compose.foundation.interaction.Interaction
22 import androidx.compose.foundation.interaction.MutableInteractionSource
23 import androidx.compose.foundation.interaction.collectIsPressedAsState
24 import androidx.compose.foundation.layout.Arrangement
25 import androidx.compose.foundation.layout.Box
26 import androidx.compose.foundation.layout.PaddingValues
27 import androidx.compose.foundation.layout.Row
28 import androidx.compose.foundation.layout.RowScope
29 import androidx.compose.foundation.layout.calculateEndPadding
30 import androidx.compose.foundation.layout.calculateStartPadding
31 import androidx.compose.foundation.layout.defaultMinSize
32 import androidx.compose.foundation.layout.padding
33 import androidx.compose.foundation.shape.CircleShape
34 import androidx.compose.foundation.shape.CornerBasedShape
35 import androidx.compose.foundation.shape.CornerSize
36 import androidx.compose.foundation.shape.RoundedCornerShape
37 import androidx.compose.material3.internal.ProvideContentColorTextStyle
38 import androidx.compose.material3.internal.rememberAnimatedShape
39 import androidx.compose.material3.tokens.BaselineButtonTokens
40 import androidx.compose.material3.tokens.MotionSchemeKeyTokens
41 import androidx.compose.material3.tokens.SplitButtonSmallTokens
42 import androidx.compose.material3.tokens.StateTokens
43 import androidx.compose.runtime.Composable
44 import androidx.compose.runtime.CompositionLocalProvider
45 import androidx.compose.runtime.getValue
46 import androidx.compose.runtime.remember
47 import androidx.compose.ui.Alignment
48 import androidx.compose.ui.Modifier
49 import androidx.compose.ui.draw.drawWithContent
50 import androidx.compose.ui.graphics.Shape
51 import androidx.compose.ui.graphics.drawOutline
52 import androidx.compose.ui.layout.Layout
53 import androidx.compose.ui.layout.layoutId
54 import androidx.compose.ui.platform.LocalDensity
55 import androidx.compose.ui.platform.LocalLayoutDirection
56 import androidx.compose.ui.semantics.Role
57 import androidx.compose.ui.semantics.role
58 import androidx.compose.ui.semantics.semantics
59 import androidx.compose.ui.unit.Dp
60 import androidx.compose.ui.unit.constrainHeight
61 import androidx.compose.ui.unit.constrainWidth
62 import androidx.compose.ui.unit.dp
63 import androidx.compose.ui.unit.offset
64 import androidx.compose.ui.util.fastFirst
65 import androidx.compose.ui.util.fastMaxOfOrNull
66 import androidx.compose.ui.util.fastSumBy
67 
68 /**
69  * A [SplitButtonLayout] let user define a button group consisting of 2 buttons. The leading button
70  * performs a primary action, and the trailing button performs a secondary action that is
71  * contextually related to the primary action.
72  *
73  * @sample androidx.compose.material3.samples.FilledSplitButtonSample
74  * @sample androidx.compose.material3.samples.SplitButtonWithDropdownMenuSample
75  * @sample androidx.compose.material3.samples.TonalSplitButtonSample
76  * @sample androidx.compose.material3.samples.ElevatedSplitButtonSample
77  * @sample androidx.compose.material3.samples.OutlinedSplitButtonSample
78  * @sample androidx.compose.material3.samples.SplitButtonWithUnCheckableTrailingButtonSample
79  * @sample androidx.compose.material3.samples.SplitButtonWithTextSample
80  * @sample androidx.compose.material3.samples.SplitButtonWithIconSample
81  *
82  * Choose the best split button for an action based on the amount of emphasis it needs. The more
83  * important an action is, the higher emphasis its button should be.
84  *
85  * Use [SplitButtonDefaults.LeadingButton] and [SplitButtonDefaults.TrailingButton] to construct a
86  * `FilledSplitButton`. Filled split button is the high-emphasis version of split button. It should
87  * be used for emphasizing important or final actions.
88  *
89  * Use [SplitButtonDefaults.TonalLeadingButton] and [SplitButtonDefaults.TonalTrailingButton] to
90  * construct a `tonal SplitButton`. Tonal split button is the medium-emphasis version of split
91  * buttons. It's a middle ground between `filled SplitButton` and `outlined SplitButton`
92  *
93  * Use [SplitButtonDefaults.ElevatedLeadingButton] and [SplitButtonDefaults.ElevatedTrailingButton]
94  * to construct a `elevated SplitButton`. Elevated split buttons are essentially `tonal
95  * SplitButton`s with a shadow. To prevent shadow creep, only use them when absolutely necessary,
96  * such as when the button requires visual separation from patterned container.
97  *
98  * Use [SplitButtonDefaults.OutlinedLeadingButton] and [SplitButtonDefaults.OutlinedTrailingButton]
99  * to construct a `outlined SplitButton`. Outlined split buttons are medium-emphasis buttons. They
100  * contain actions that are important, but are not the primary action in an app. Outlined buttons
101  * pair well with `filled SplitButton`s to indicate an alternative, secondary action.
102  *
103  * @param leadingButton the leading button. You can specify your own composable or construct a
104  *   [SplitButtonDefaults.LeadingButton]
105  * @param trailingButton the trailing button.You can specify your own composable or construct a
106  *   [SplitButtonDefaults.TrailingButton]
107  * @param modifier the [Modifier] to be applied to this split button.
108  * @param spacing The spacing between the [leadingButton] and [trailingButton]
109  */
110 @ExperimentalMaterial3ExpressiveApi
111 @Composable
112 fun SplitButtonLayout(
113     leadingButton: @Composable () -> Unit,
114     trailingButton: @Composable () -> Unit,
115     modifier: Modifier = Modifier,
116     spacing: Dp = SplitButtonDefaults.Spacing,
117 ) {
118     Layout(
119         {
120             // Override min component size enforcement to avoid create extra padding internally
121             // Enforce it on the parent instead
122             CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
123                 Box(
124                     modifier = Modifier.layoutId(LeadingButtonLayoutId),
125                     contentAlignment = Alignment.Center,
126                     content = { leadingButton() }
127                 )
128                 Box(
129                     modifier = Modifier.layoutId(TrailingButtonLayoutId),
130                     contentAlignment = Alignment.Center,
131                     content = { trailingButton() }
132                 )
133             }
134         },
135         modifier.minimumInteractiveComponentSize(),
136         measurePolicy = { measurables, constraints ->
137             val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
138 
139             val leadingButtonPlaceable =
140                 measurables
141                     .fastFirst { it.layoutId == LeadingButtonLayoutId }
142                     .measure(looseConstraints)
143 
144             val trailingButtonPlaceable =
145                 measurables
146                     .fastFirst { it.layoutId == TrailingButtonLayoutId }
147                     .measure(
148                         looseConstraints
149                             .offset(
150                                 horizontal = -(leadingButtonPlaceable.width + spacing.roundToPx())
151                             )
152                             .copy(
153                                 minHeight = leadingButtonPlaceable.height,
154                                 maxHeight = leadingButtonPlaceable.height
155                             )
156                     )
157 
158             val placeables = listOf(leadingButtonPlaceable, trailingButtonPlaceable)
159 
160             val contentWidth = placeables.fastSumBy { it.width } + spacing.roundToPx()
161             val contentHeight = placeables.fastMaxOfOrNull { it.height } ?: 0
162 
163             val width = constraints.constrainWidth(contentWidth)
164             val height = constraints.constrainHeight(contentHeight)
165 
166             layout(width, height) {
167                 leadingButtonPlaceable.placeRelative(0, 0)
168                 trailingButtonPlaceable.placeRelative(
169                     x = leadingButtonPlaceable.width + spacing.roundToPx(),
170                     y = 0
171                 )
172             }
173         }
174     )
175 }
176 
177 /** Contains default values used by [SplitButtonLayout] and its style variants. */
178 @ExperimentalMaterial3ExpressiveApi
179 object SplitButtonDefaults {
180     /** Default icon size for the leading button */
181     val LeadingIconSize = BaselineButtonTokens.IconSize
182 
183     /** Default icon size for the trailing button */
184     val TrailingIconSize = SplitButtonSmallTokens.TrailingIconSize
185 
186     /** Default spacing between the `leading` and `trailing` button */
187     val Spacing = SplitButtonSmallTokens.BetweenSpace
188 
189     /** Default size for the leading button end corners and trailing button start corners */
190     // TODO update token to dp size and use it here
191     val InnerCornerSize = SplitButtonSmallTokens.InnerCornerSize
192     private val InnerCornerSizePressed = SplitButtonSmallTokens.InnerCornerPressedSize
193 
194     /**
195      * Default percentage size for the leading button start corners and trailing button end corners
196      */
197     val OuterCornerSize = ShapeDefaults.CornerFull
198 
199     /** Default content padding of the leading button */
200     val LeadingButtonContentPadding =
201         PaddingValues(
202             start = SplitButtonSmallTokens.LeadingButtonLeadingSpace,
203             end = SplitButtonSmallTokens.LeadingButtonTrailingSpace
204         )
205 
206     /** Default content padding of the trailing button */
207     val TrailingButtonContentPadding =
208         PaddingValues(
209             start = SplitButtonSmallTokens.TrailingButtonLeadingSpace,
210             end = SplitButtonSmallTokens.TrailingButtonTrailingSpace
211         )
212 
213     /**
214      * Default minimum width of the [LeadingButton], applies to all 4 variants of the split button
215      */
216     private val LeadingButtonMinWidth = 48.dp
217 
218     /**
219      * Default minimum height of the split button. This applies to both [LeadingButton] and
220      * [TrailingButton]. Applies to all 4 variants of the split button
221      */
222     private val MinHeight = SplitButtonSmallTokens.ContainerHeight
223 
224     /** Default minimum width of the [TrailingButton]. */
225     private val TrailingButtonMinWidth = LeadingButtonMinWidth
226 
227     /** Trailing button state layer alpha when in checked state */
228     private const val TrailingButtonStateLayerAlpha = StateTokens.PressedStateLayerOpacity
229 
230     /** Default shape of the leading button. */
leadingButtonShapenull231     private fun leadingButtonShape(endCornerSize: CornerSize = InnerCornerSize) =
232         RoundedCornerShape(OuterCornerSize, endCornerSize, endCornerSize, OuterCornerSize)
233 
234     private val LeadingPressedShape =
235         RoundedCornerShape(
236             topStart = OuterCornerSize,
237             bottomStart = OuterCornerSize,
238             topEnd = InnerCornerSizePressed,
239             bottomEnd = InnerCornerSizePressed
240         )
241     private val TrailingPressedShape =
242         RoundedCornerShape(
243             topStart = InnerCornerSizePressed,
244             bottomStart = InnerCornerSizePressed,
245             topEnd = OuterCornerSize,
246             bottomEnd = OuterCornerSize
247         )
248     private val TrailingCheckedShape = CircleShape
249 
250     /**
251      * Default shapes for the leading button. This defines the shapes the leading button should
252      * morph to when enabled, pressed etc.
253      *
254      * @param endCornerSize the size for top end corner and bottom end corner
255      */
256     fun leadingButtonShapes(endCornerSize: CornerSize = InnerCornerSize) =
257         SplitButtonShapes(
258             shape = leadingButtonShape(endCornerSize),
259             pressedShape = LeadingPressedShape,
260             checkedShape = null,
261         )
262 
263     /** Default shape of the trailing button */
264     private fun trailingButtonShape(startCornerSize: CornerSize = InnerCornerSize) =
265         RoundedCornerShape(startCornerSize, OuterCornerSize, OuterCornerSize, startCornerSize)
266 
267     /**
268      * Default shapes for the trailing button
269      *
270      * @param startCornerSize the size for top start corner and bottom start corner
271      */
272     fun trailingButtonShapes(startCornerSize: CornerSize = InnerCornerSize) =
273         SplitButtonShapes(
274             shape = trailingButtonShape(startCornerSize),
275             pressedShape = TrailingPressedShape,
276             checkedShape = TrailingCheckedShape
277         )
278 
279     /**
280      * Create a default `leading` button that has the same visual as a Filled[Button]. To create a
281      * `tonal`, `outlined`, or `elevated` version, the default value of [Button] params can be
282      * passed in. For example, [ElevatedButton].
283      *
284      * The default text style for internal [Text] components will be set to [Typography.labelLarge].
285      *
286      * @param onClick called when the button is clicked
287      * @param modifier the [Modifier] to be applied to this button.
288      * @param enabled controls the enabled state of the split button. When `false`, this component
289      *   will not respond to user input, and it will appear visually disabled and disabled to
290      *   accessibility services.
291      * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
292      *   on the user's interaction with the button.
293      * @param colors [ButtonColors] that will be used to resolve the colors for this button in
294      *   different states. See [ButtonDefaults.buttonColors].
295      * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
296      *   states. This controls the size of the shadow below the button. See
297      *   [ButtonElevation.shadowElevation].
298      * @param border the border to draw around the container of this button contentPadding the
299      *   spacing values to apply internally between the container and the content
300      * @param contentPadding the spacing values to apply internally between the container and the
301      *   content
302      * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
303      *   emitting [Interaction]s for this button. You can use this to change the button's appearance
304      *   or preview the button in different states. Note that if `null` is provided, interactions
305      *   will still happen internally.
306      * @param content the content for the button.
307      */
308     @ExperimentalMaterial3ExpressiveApi
309     @Composable
310     fun LeadingButton(
311         onClick: () -> Unit,
312         modifier: Modifier = Modifier,
313         enabled: Boolean = true,
314         shapes: SplitButtonShapes = leadingButtonShapes(),
315         colors: ButtonColors = ButtonDefaults.buttonColors(),
316         elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
317         border: BorderStroke? = null,
318         contentPadding: PaddingValues = LeadingButtonContentPadding,
319         interactionSource: MutableInteractionSource? = null,
320         content: @Composable RowScope.() -> Unit
321     ) {
322         @Suppress("NAME_SHADOWING")
323         val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
324 
325         // TODO Load the motionScheme tokens from the component tokens file
326         val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value<Float>()
327         val pressed by interactionSource.collectIsPressedAsState()
328         val contentColor = colors.contentColor(enabled)
329         val containerColor = colors.containerColor(enabled)
330 
331         Surface(
332             onClick = onClick,
333             modifier = modifier.semantics { role = Role.Button },
334             enabled = enabled,
335             shape = shapeByInteraction(shapes, pressed, checked = false, defaultAnimationSpec),
336             color = containerColor,
337             contentColor = contentColor,
338             shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
339             border = border,
340             interactionSource = interactionSource
341         ) {
342             ProvideContentColorTextStyle(
343                 contentColor = contentColor,
344                 textStyle = MaterialTheme.typography.labelLarge
345             ) {
346                 Row(
347                     Modifier.defaultMinSize(minWidth = LeadingButtonMinWidth, minHeight = MinHeight)
348                         .padding(contentPadding),
349                     horizontalArrangement = Arrangement.Center,
350                     verticalAlignment = Alignment.CenterVertically,
351                     content = content
352                 )
353             }
354         }
355     }
356 
357     /**
358      * Creates a `trailing` button that has the same visual as a [Button].
359      *
360      * To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
361      * can be passed in. For example, [ElevatedButton].
362      *
363      * The default text style for internal [Text] components will be set to [Typography.labelLarge].
364      *
365      * @param onClick called when the button is clicked
366      * @param modifier the [Modifier] to be applied to this button.
367      * @param enabled controls the enabled state of the split button. When `false`, this component
368      *   will not respond to user input, and it will appear visually disabled and disabled to
369      *   accessibility services.
370      * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
371      *   on the user's interaction with the button.
372      * @param colors [ButtonColors] that will be used to resolve the colors for this button in
373      *   different states. See [ButtonDefaults.buttonColors].
374      * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
375      *   states. This controls the size of the shadow below the button. See
376      *   [ButtonElevation.shadowElevation].
377      * @param border the border to draw around the container of this button contentPadding the
378      *   spacing values to apply internally between the container and the content
379      * @param contentPadding the spacing values to apply internally between the container and the
380      *   content
381      * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
382      *   emitting [Interaction]s for this button. You can use this to change the button's appearance
383      *   or preview the button in different states. Note that if `null` is provided, interactions
384      *   will still happen internally.
385      * @param content the content to be placed in the button
386      */
387     @Composable
388     @ExperimentalMaterial3ExpressiveApi
TrailingButtonnull389     fun TrailingButton(
390         onClick: () -> Unit,
391         modifier: Modifier = Modifier,
392         enabled: Boolean = true,
393         shapes: SplitButtonShapes = trailingButtonShapes(),
394         colors: ButtonColors = ButtonDefaults.buttonColors(),
395         elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
396         border: BorderStroke? = null,
397         contentPadding: PaddingValues = TrailingButtonContentPadding,
398         interactionSource: MutableInteractionSource? = null,
399         content: @Composable RowScope.() -> Unit
400     ) {
401         @Suppress("NAME_SHADOWING")
402         val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
403 
404         // TODO Load the motionScheme tokens from the component tokens file
405         val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value<Float>()
406         val pressed by interactionSource.collectIsPressedAsState()
407         val layoutDirection = LocalLayoutDirection.current
408         val shape = shapeByInteraction(shapes, pressed, false, defaultAnimationSpec)
409         val contentColor = colors.contentColor(enabled)
410         val containerColor = colors.containerColor(enabled)
411 
412         Surface(
413             onClick = onClick,
414             modifier = modifier.semantics { role = Role.Button },
415             enabled = enabled,
416             shape = shape,
417             color = containerColor,
418             contentColor = contentColor,
419             shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
420             border = border,
421             interactionSource = interactionSource
422         ) {
423             ProvideContentColorTextStyle(
424                 contentColor = contentColor,
425                 textStyle = MaterialTheme.typography.labelLarge
426             ) {
427                 Row(
428                     Modifier.defaultMinSize(
429                             minWidth = TrailingButtonMinWidth,
430                             minHeight = MinHeight
431                         )
432                         .then(
433                             when (shape) {
434                                 is ShapeWithHorizontalCenterOptically -> {
435                                     Modifier.horizontalCenterOptically(
436                                         shape = shape,
437                                         maxStartOffset =
438                                             contentPadding.calculateStartPadding(layoutDirection),
439                                         maxEndOffset =
440                                             contentPadding.calculateEndPadding(layoutDirection)
441                                     )
442                                 }
443                                 is CornerBasedShape -> {
444                                     Modifier.horizontalCenterOptically(
445                                         shape = shape,
446                                         maxStartOffset =
447                                             contentPadding.calculateStartPadding(layoutDirection),
448                                         maxEndOffset =
449                                             contentPadding.calculateEndPadding(layoutDirection)
450                                     )
451                                 }
452                                 else -> {
453                                     Modifier
454                                 }
455                             }
456                         )
457                         .padding(contentPadding),
458                     horizontalArrangement = Arrangement.Center,
459                     verticalAlignment = Alignment.CenterVertically,
460                     content = content
461                 )
462             }
463         }
464     }
465 
466     /**
467      * Creates a `trailing` button that has the same visual as a [Button]. When [checked] is updated
468      * from `false` to `true`, the buttons corners will morph to `full` by default. Pressed shape
469      * and checked shape can be customized via [shapes] param.
470      *
471      * To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
472      * can be passed in. For example, [ElevatedButton].
473      *
474      * The default text style for internal [Text] components will be set to [Typography.labelLarge].
475      *
476      * @param checked indicates whether the button is checked. This will trigger the corner morphing
477      *   animation to reflect the updated state.
478      * @param onCheckedChange called when the button is clicked
479      * @param modifier the [Modifier] to be applied to this button.
480      * @param enabled controls the enabled state of the split button. When `false`, this component
481      *   will not respond to user input, and it will appear visually disabled and disabled to
482      *   accessibility services.
483      * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
484      *   on the user's interaction with the button.
485      * @param colors [ButtonColors] that will be used to resolve the colors for this button in
486      *   different states. See [ButtonDefaults.buttonColors].
487      * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
488      *   states. This controls the size of the shadow below the button. See
489      *   [ButtonElevation.shadowElevation].
490      * @param border the border to draw around the container of this button contentPadding the
491      *   spacing values to apply internally between the container and the content
492      * @param contentPadding the spacing values to apply internally between the container and the
493      *   content
494      * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
495      *   emitting [Interaction]s for this button. You can use this to change the button's appearance
496      *   or preview the button in different states. Note that if `null` is provided, interactions
497      *   will still happen internally.
498      * @param content the content to be placed in the button
499      */
500     @Composable
501     @ExperimentalMaterial3ExpressiveApi
TrailingButtonnull502     fun TrailingButton(
503         checked: Boolean,
504         onCheckedChange: (Boolean) -> Unit,
505         modifier: Modifier = Modifier,
506         enabled: Boolean = true,
507         shapes: SplitButtonShapes = trailingButtonShapes(),
508         colors: ButtonColors = ButtonDefaults.buttonColors(),
509         elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
510         border: BorderStroke? = null,
511         contentPadding: PaddingValues = TrailingButtonContentPadding,
512         interactionSource: MutableInteractionSource? = null,
513         content: @Composable RowScope.() -> Unit
514     ) {
515         @Suppress("NAME_SHADOWING")
516         val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
517 
518         // TODO Load the motionScheme tokens from the component tokens file
519         val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value<Float>()
520         val pressed by interactionSource.collectIsPressedAsState()
521         val layoutDirection = LocalLayoutDirection.current
522         val density = LocalDensity.current
523         val shape = shapeByInteraction(shapes, pressed, checked, defaultAnimationSpec)
524         val contentColor = colors.contentColor(enabled)
525         val containerColor = colors.containerColor(enabled)
526 
527         Surface(
528             checked = checked,
529             onCheckedChange = onCheckedChange,
530             modifier =
531                 modifier
532                     .drawWithContent {
533                         drawContent()
534                         if (checked) {
535                             drawOutline(
536                                 outline = shape.createOutline(size, layoutDirection, density),
537                                 color = contentColor,
538                                 alpha = TrailingButtonStateLayerAlpha
539                             )
540                         }
541                     }
542                     .semantics { role = Role.Button },
543             enabled = enabled,
544             shape = shape,
545             color = containerColor,
546             contentColor = contentColor,
547             shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
548             border = border,
549             interactionSource = interactionSource
550         ) {
551             ProvideContentColorTextStyle(
552                 contentColor = contentColor,
553                 textStyle = MaterialTheme.typography.labelLarge
554             ) {
555                 Row(
556                     Modifier.defaultMinSize(
557                             minWidth = TrailingButtonMinWidth,
558                             minHeight = MinHeight
559                         )
560                         .then(
561                             when (shape) {
562                                 is ShapeWithHorizontalCenterOptically -> {
563                                     Modifier.horizontalCenterOptically(
564                                         shape = shape,
565                                         maxStartOffset =
566                                             contentPadding.calculateStartPadding(layoutDirection),
567                                         maxEndOffset =
568                                             contentPadding.calculateEndPadding(layoutDirection)
569                                     )
570                                 }
571                                 is CornerBasedShape -> {
572                                     Modifier.horizontalCenterOptically(
573                                         shape = shape,
574                                         maxStartOffset =
575                                             contentPadding.calculateStartPadding(layoutDirection),
576                                         maxEndOffset =
577                                             contentPadding.calculateEndPadding(layoutDirection)
578                                     )
579                                 }
580                                 else -> {
581                                     Modifier
582                                 }
583                             }
584                         )
585                         .padding(contentPadding),
586                     horizontalArrangement = Arrangement.Center,
587                     verticalAlignment = Alignment.CenterVertically,
588                     content = content
589                 )
590             }
591         }
592     }
593 
594     /**
595      * Create a tonal `leading` button that has the same visual as a Tonal[Button]. To create a
596      * `filled`, `outlined`, or `elevated` version, the default value of [Button] params can be
597      * passed in. For example, [ElevatedButton].
598      *
599      * The default text style for internal [Text] components will be set to [Typography.labelLarge].
600      *
601      * @param onClick called when the button is clicked
602      * @param modifier the [Modifier] to be applied to this button.
603      * @param enabled controls the enabled state of the split button. When `false`, this component
604      *   will not respond to user input, and it will appear visually disabled and disabled to
605      *   accessibility services.
606      * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
607      *   on the user's interaction with the button.
608      * @param colors [ButtonColors] that will be used to resolve the colors for this button in
609      *   different states. See [ButtonDefaults.buttonColors].
610      * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
611      *   states. This controls the size of the shadow below the button. See
612      *   [ButtonElevation.shadowElevation].
613      * @param border the border to draw around the container of this button contentPadding the
614      *   spacing values to apply internally between the container and the content
615      * @param contentPadding the spacing values to apply internally between the container and the
616      *   content
617      * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
618      *   emitting [Interaction]s for this button. You can use this to change the button's appearance
619      *   or preview the button in different states. Note that if `null` is provided, interactions
620      *   will still happen internally.
621      * @param content the content for the button.
622      */
623     @ExperimentalMaterial3ExpressiveApi
624     @Composable
TonalLeadingButtonnull625     fun TonalLeadingButton(
626         onClick: () -> Unit,
627         modifier: Modifier = Modifier,
628         enabled: Boolean = true,
629         shapes: SplitButtonShapes = leadingButtonShapes(),
630         colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(),
631         elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(),
632         border: BorderStroke? = null,
633         contentPadding: PaddingValues = LeadingButtonContentPadding,
634         interactionSource: MutableInteractionSource? = null,
635         content: @Composable RowScope.() -> Unit
636     ) {
637         LeadingButton(
638             modifier = modifier,
639             onClick = onClick,
640             enabled = enabled,
641             shapes = shapes,
642             colors = colors,
643             elevation = elevation,
644             border = border,
645             contentPadding = contentPadding,
646             interactionSource = interactionSource,
647             content = content,
648         )
649     }
650 
651     /**
652      * Creates a tonal `trailing` button that has the same visual as a [FilledTonalButton]. When
653      * [checked] is updated from `false` to `true`, the buttons corners will morph to `full` by
654      * default. Pressed shape and checked shape can be customized via [shapes] param.
655      *
656      * To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
657      * can be passed in. For example, [ElevatedButton].
658      *
659      * The default text style for internal [Text] components will be set to [Typography.labelLarge].
660      *
661      * @param checked indicates whether the button is checked. This will trigger the corner morphing
662      *   animation to reflect the updated state.
663      * @param onCheckedChange called when the button is clicked
664      * @param modifier the [Modifier] to be applied to this button.
665      * @param enabled controls the enabled state of the split button. When `false`, this component
666      *   will not respond to user input, and it will appear visually disabled and disabled to
667      *   accessibility services.
668      * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
669      *   on the user's interaction with the button.
670      * @param colors [ButtonColors] that will be used to resolve the colors for this button in
671      *   different states. See [ButtonDefaults.buttonColors].
672      * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
673      *   states. This controls the size of the shadow below the button. See
674      *   [ButtonElevation.shadowElevation].
675      * @param border the border to draw around the container of this button contentPadding the
676      *   spacing values to apply internally between the container and the content
677      * @param contentPadding the spacing values to apply internally between the container and the
678      *   content
679      * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
680      *   emitting [Interaction]s for this button. You can use this to change the button's appearance
681      *   or preview the button in different states. Note that if `null` is provided, interactions
682      *   will still happen internally.
683      * @param content the content to be placed in the button
684      */
685     @ExperimentalMaterial3ExpressiveApi
686     @Composable
TonalTrailingButtonnull687     fun TonalTrailingButton(
688         checked: Boolean,
689         onCheckedChange: (Boolean) -> Unit,
690         modifier: Modifier = Modifier,
691         enabled: Boolean = true,
692         shapes: SplitButtonShapes = trailingButtonShapes(),
693         colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(),
694         elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(),
695         border: BorderStroke? = null,
696         contentPadding: PaddingValues = TrailingButtonContentPadding,
697         interactionSource: MutableInteractionSource? = null,
698         content: @Composable RowScope.() -> Unit
699     ) {
700         TrailingButton(
701             checked = checked,
702             onCheckedChange = onCheckedChange,
703             modifier = modifier,
704             enabled = enabled,
705             shapes = shapes,
706             colors = colors,
707             elevation = elevation,
708             border = border,
709             contentPadding = contentPadding,
710             interactionSource = interactionSource,
711             content = content
712         )
713     }
714 
715     /**
716      * Create a elevated `leading` button that has the same visual as a [ElevatedButton]. To create
717      * a `filled`, `outlined`, or `elevated` version, the default value of [Button] params can be
718      * passed in. For example, [ElevatedButton].
719      *
720      * The default text style for internal [Text] components will be set to [Typography.labelLarge].
721      *
722      * @param onClick called when the button is clicked
723      * @param modifier the [Modifier] to be applied to this button.
724      * @param enabled controls the enabled state of the split button. When `false`, this component
725      *   will not respond to user input, and it will appear visually disabled and disabled to
726      *   accessibility services.
727      * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
728      *   on the user's interaction with the button.
729      * @param colors [ButtonColors] that will be used to resolve the colors for this button in
730      *   different states. See [ButtonDefaults.buttonColors].
731      * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
732      *   states. This controls the size of the shadow below the button. See
733      *   [ButtonElevation.shadowElevation].
734      * @param border the border to draw around the container of this button contentPadding the
735      *   spacing values to apply internally between the container and the content
736      * @param contentPadding the spacing values to apply internally between the container and the
737      *   content
738      * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
739      *   emitting [Interaction]s for this button. You can use this to change the button's appearance
740      *   or preview the button in different states. Note that if `null` is provided, interactions
741      *   will still happen internally.
742      * @param content the content for the button.
743      */
744     @ExperimentalMaterial3ExpressiveApi
745     @Composable
OutlinedLeadingButtonnull746     fun OutlinedLeadingButton(
747         onClick: () -> Unit,
748         modifier: Modifier = Modifier,
749         enabled: Boolean = true,
750         shapes: SplitButtonShapes = leadingButtonShapes(),
751         colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
752         elevation: ButtonElevation? = null,
753         border: BorderStroke? = ButtonDefaults.outlinedButtonBorder(enabled),
754         contentPadding: PaddingValues = LeadingButtonContentPadding,
755         interactionSource: MutableInteractionSource? = null,
756         content: @Composable RowScope.() -> Unit
757     ) {
758         LeadingButton(
759             modifier = modifier,
760             onClick = onClick,
761             enabled = enabled,
762             shapes = shapes,
763             colors = colors,
764             elevation = elevation,
765             border = border,
766             contentPadding = contentPadding,
767             interactionSource = interactionSource,
768             content = content,
769         )
770     }
771 
772     /**
773      * Creates a outlined `trailing` button that has the same visual as a [OutlinedButton]. When
774      * [checked] is updated from `false` to `true`, the buttons corners will morph to `full` by
775      * default. Pressed shape and checked shape can be customized via [shapes] param.
776      *
777      * To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
778      * can be passed in. For example, [ElevatedButton].
779      *
780      * The default text style for internal [Text] components will be set to [Typography.labelLarge].
781      *
782      * @param checked indicates whether the button is checked. This will trigger the corner morphing
783      *   animation to reflect the updated state.
784      * @param onCheckedChange called when the button is clicked
785      * @param modifier the [Modifier] to be applied to this button.
786      * @param enabled controls the enabled state of the split button. When `false`, this component
787      *   will not respond to user input, and it will appear visually disabled and disabled to
788      *   accessibility services.
789      * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
790      *   on the user's interaction with the button.
791      * @param colors [ButtonColors] that will be used to resolve the colors for this button in
792      *   different states. See [ButtonDefaults.buttonColors].
793      * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
794      *   states. This controls the size of the shadow below the button. See
795      *   [ButtonElevation.shadowElevation].
796      * @param border the border to draw around the container of this button contentPadding the
797      *   spacing values to apply internally between the container and the content
798      * @param contentPadding the spacing values to apply internally between the container and the
799      *   content
800      * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
801      *   emitting [Interaction]s for this button. You can use this to change the button's appearance
802      *   or preview the button in different states. Note that if `null` is provided, interactions
803      *   will still happen internally.
804      * @param content the content to be placed in the button
805      */
806     @ExperimentalMaterial3ExpressiveApi
807     @Composable
OutlinedTrailingButtonnull808     fun OutlinedTrailingButton(
809         checked: Boolean,
810         onCheckedChange: (Boolean) -> Unit,
811         modifier: Modifier = Modifier,
812         enabled: Boolean = true,
813         shapes: SplitButtonShapes = trailingButtonShapes(),
814         colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
815         elevation: ButtonElevation? = null,
816         border: BorderStroke? = ButtonDefaults.outlinedButtonBorder(enabled),
817         contentPadding: PaddingValues = TrailingButtonContentPadding,
818         interactionSource: MutableInteractionSource? = null,
819         content: @Composable RowScope.() -> Unit
820     ) {
821         TrailingButton(
822             checked = checked,
823             onCheckedChange = onCheckedChange,
824             modifier = modifier,
825             enabled = enabled,
826             shapes = shapes,
827             colors = colors,
828             elevation = elevation,
829             border = border,
830             contentPadding = contentPadding,
831             interactionSource = interactionSource,
832             content = content
833         )
834     }
835 
836     /**
837      * Create a elevated `leading` button that has the same visual as a [ElevatedButton]. To create
838      * a `filled`, `outlined`, or `elevated` version, the default value of [Button] params can be
839      * passed in. For example, [ElevatedButton].
840      *
841      * The default text style for internal [Text] components will be set to [Typography.labelLarge].
842      *
843      * @param onClick called when the button is clicked
844      * @param modifier the [Modifier] to be applied to this button.
845      * @param enabled controls the enabled state of the split button. When `false`, this component
846      *   will not respond to user input, and it will appear visually disabled and disabled to
847      *   accessibility services.
848      * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
849      *   on the user's interaction with the button.
850      * @param colors [ButtonColors] that will be used to resolve the colors for this button in
851      *   different states. See [ButtonDefaults.buttonColors].
852      * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
853      *   states. This controls the size of the shadow below the button. See
854      *   [ButtonElevation.shadowElevation].
855      * @param border the border to draw around the container of this button contentPadding the
856      *   spacing values to apply internally between the container and the content
857      * @param contentPadding the spacing values to apply internally between the container and the
858      *   content
859      * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
860      *   emitting [Interaction]s for this button. You can use this to change the button's appearance
861      *   or preview the button in different states. Note that if `null` is provided, interactions
862      *   will still happen internally.
863      * @param content the content for the button.
864      */
865     @ExperimentalMaterial3ExpressiveApi
866     @Composable
ElevatedLeadingButtonnull867     fun ElevatedLeadingButton(
868         onClick: () -> Unit,
869         modifier: Modifier = Modifier,
870         enabled: Boolean = true,
871         shapes: SplitButtonShapes = leadingButtonShapes(),
872         colors: ButtonColors = ButtonDefaults.elevatedButtonColors(),
873         elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(),
874         border: BorderStroke? = null,
875         contentPadding: PaddingValues = LeadingButtonContentPadding,
876         interactionSource: MutableInteractionSource? = null,
877         content: @Composable RowScope.() -> Unit
878     ) {
879         LeadingButton(
880             modifier = modifier,
881             onClick = onClick,
882             enabled = enabled,
883             shapes = shapes,
884             colors = colors,
885             elevation = elevation,
886             border = border,
887             contentPadding = contentPadding,
888             interactionSource = interactionSource,
889             content = content,
890         )
891     }
892 
893     /**
894      * Creates a elevated `trailing` button that has the same visual as a [ElevatedButton]. When
895      * [checked] is updated from `false` to `true`, the buttons corners will morph to `full` by
896      * default. Pressed shape and checked shape can be customized via [shapes] param.
897      *
898      * To create a `tonal`, `outlined`, or `elevated` version, the default value of [Button] params
899      * can be passed in. For example, [ElevatedButton].
900      *
901      * The default text style for internal [Text] components will be set to [Typography.labelLarge].
902      *
903      * @param checked indicates whether the button is checked. This will trigger the corner morphing
904      *   animation to reflect the updated state.
905      * @param onCheckedChange called when the button is clicked
906      * @param modifier the [Modifier] to be applied to this button.
907      * @param enabled controls the enabled state of the split button. When `false`, this component
908      *   will not respond to user input, and it will appear visually disabled and disabled to
909      *   accessibility services.
910      * @param shapes the [SplitButtonShapes] that the trailing button will morph between depending
911      *   on the user's interaction with the button.
912      * @param colors [ButtonColors] that will be used to resolve the colors for this button in
913      *   different states. See [ButtonDefaults.buttonColors].
914      * @param elevation [ButtonElevation] used to resolve the elevation for this button in different
915      *   states. This controls the size of the shadow below the button. See
916      *   [ButtonElevation.shadowElevation].
917      * @param border the border to draw around the container of this button contentPadding the
918      *   spacing values to apply internally between the container and the content
919      * @param contentPadding the spacing values to apply internally between the container and the
920      *   content
921      * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
922      *   emitting [Interaction]s for this button. You can use this to change the button's appearance
923      *   or preview the button in different states. Note that if `null` is provided, interactions
924      *   will still happen internally.
925      * @param content the content to be placed in the button
926      */
927     @ExperimentalMaterial3ExpressiveApi
928     @Composable
ElevatedTrailingButtonnull929     fun ElevatedTrailingButton(
930         checked: Boolean,
931         onCheckedChange: (Boolean) -> Unit,
932         modifier: Modifier = Modifier,
933         enabled: Boolean = true,
934         shapes: SplitButtonShapes = trailingButtonShapes(),
935         colors: ButtonColors = ButtonDefaults.elevatedButtonColors(),
936         elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(),
937         border: BorderStroke? = null,
938         contentPadding: PaddingValues = TrailingButtonContentPadding,
939         interactionSource: MutableInteractionSource? = null,
940         content: @Composable RowScope.() -> Unit
941     ) {
942         TrailingButton(
943             checked = checked,
944             onCheckedChange = onCheckedChange,
945             modifier = modifier,
946             enabled = enabled,
947             shapes = shapes,
948             colors = colors,
949             elevation = elevation,
950             border = border,
951             contentPadding = contentPadding,
952             interactionSource = interactionSource,
953             content = content
954         )
955     }
956 }
957 
958 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
959 @Composable
shapeByInteractionnull960 private fun shapeByInteraction(
961     shapes: SplitButtonShapes,
962     pressed: Boolean,
963     checked: Boolean,
964     animationSpec: FiniteAnimationSpec<Float>
965 ): Shape {
966     val shape =
967         if (pressed) {
968             shapes.pressedShape ?: shapes.shape
969         } else if (checked) {
970             shapes.checkedShape ?: shapes.shape
971         } else shapes.shape
972 
973     if (shapes.hasRoundedCornerShapes) {
974         return rememberAnimatedShape(shape as RoundedCornerShape, animationSpec)
975     }
976     return shape
977 }
978 
979 /**
980  * The shapes that will be used in [SplitButtonLayout]. Split button will morph between these shapes
981  * depending on the interaction of the buttons, assuming all of the shapes are [CornerBasedShape]s.
982  *
983  * @property shape is the default shape.
984  * @property pressedShape is the pressed shape.
985  * @property checkedShape is the checked shape.
986  */
987 @ExperimentalMaterial3ExpressiveApi
988 class SplitButtonShapes(val shape: Shape, val pressedShape: Shape?, val checkedShape: Shape?) {
equalsnull989     override fun equals(other: Any?): Boolean {
990         if (this === other) return true
991         if (other == null || other !is SplitButtonShapes) return false
992 
993         if (shape != other.shape) return false
994         if (pressedShape != other.pressedShape) return false
995         if (checkedShape != other.checkedShape) return false
996 
997         return true
998     }
999 
hashCodenull1000     override fun hashCode(): Int {
1001         var result = shape.hashCode()
1002         if (pressedShape != null) {
1003             result = 31 * result + pressedShape.hashCode()
1004         }
1005         if (checkedShape != null) {
1006             result = 31 * result + checkedShape.hashCode()
1007         }
1008 
1009         return result
1010     }
1011 }
1012 
1013 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
1014 private val SplitButtonShapes.hasRoundedCornerShapes: Boolean
1015     get() {
1016         // Ignore null shapes and only check default shape for RoundedCorner
1017         if (pressedShape != null && pressedShape !is RoundedCornerShape) return false
1018         if (checkedShape != null && checkedShape !is RoundedCornerShape) return false
1019         return shape is RoundedCornerShape
1020     }
1021 
1022 private const val LeadingButtonLayoutId = "LeadingButton"
1023 private const val TrailingButtonLayoutId = "TrailingButton"
1024