1 /*
<lambda>null2  * Copyright 2021 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.foundation.layout.Arrangement
20 import androidx.compose.foundation.layout.Box
21 import androidx.compose.foundation.layout.Column
22 import androidx.compose.foundation.layout.PaddingValues
23 import androidx.compose.foundation.layout.padding
24 import androidx.compose.foundation.layout.sizeIn
25 import androidx.compose.material3.internal.ProvideContentColorTextStyle
26 import androidx.compose.material3.internal.Strings
27 import androidx.compose.material3.internal.getString
28 import androidx.compose.material3.tokens.DialogTokens
29 import androidx.compose.runtime.Composable
30 import androidx.compose.runtime.CompositionLocalProvider
31 import androidx.compose.runtime.ProvidableCompositionLocal
32 import androidx.compose.runtime.compositionLocalOf
33 import androidx.compose.ui.Alignment
34 import androidx.compose.ui.Modifier
35 import androidx.compose.ui.graphics.Color
36 import androidx.compose.ui.graphics.Shape
37 import androidx.compose.ui.layout.Layout
38 import androidx.compose.ui.layout.Placeable
39 import androidx.compose.ui.semantics.paneTitle
40 import androidx.compose.ui.semantics.semantics
41 import androidx.compose.ui.unit.Dp
42 import androidx.compose.ui.unit.dp
43 import androidx.compose.ui.util.fastForEach
44 import androidx.compose.ui.util.fastForEachIndexed
45 import androidx.compose.ui.window.Dialog
46 import androidx.compose.ui.window.DialogProperties
47 import kotlin.math.max
48 
49 /**
50  * [Material Design basic dialog](https://m3.material.io/components/dialogs/overview)
51  *
52  * Dialogs provide important prompts in a user flow. They can require an action, communicate
53  * information, or help users accomplish a task.
54  *
55  * ![Basic dialog
56  * image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png)
57  *
58  * The dialog will position its buttons, typically [TextButton]s, based on the available space. By
59  * default it will try to place them horizontally next to each other and fallback to horizontal
60  * placement if not enough space is available.
61  *
62  * Simple usage:
63  *
64  * @sample androidx.compose.material3.samples.AlertDialogSample
65  *
66  * Usage with a "Hero" icon:
67  *
68  * @sample androidx.compose.material3.samples.AlertDialogWithIconSample
69  * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside or
70  *   pressing the back button. This is not called when the dismiss button is clicked.
71  * @param confirmButton button which is meant to confirm a proposed action, thus resolving what
72  *   triggered the dialog. The dialog does not set up any events for this button so they need to be
73  *   set up by the caller.
74  * @param modifier the [Modifier] to be applied to this dialog
75  * @param dismissButton button which is meant to dismiss the dialog. The dialog does not set up any
76  *   events for this button so they need to be set up by the caller.
77  * @param icon optional icon that will appear above the [title] or above the [text], in case a title
78  *   was not provided.
79  * @param title title which should specify the purpose of the dialog. The title is not mandatory,
80  *   because there may be sufficient information inside the [text].
81  * @param text text which presents the details regarding the dialog's purpose.
82  * @param shape defines the shape of this dialog's container
83  * @param containerColor the color used for the background of this dialog. Use [Color.Transparent]
84  *   to have no color.
85  * @param iconContentColor the content color used for the icon.
86  * @param titleContentColor the content color used for the title.
87  * @param textContentColor the content color used for the text.
88  * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
89  *   overlay is applied on top of the container. A higher tonal elevation value will result in a
90  *   darker color in light theme and lighter color in dark theme. See also: [Surface].
91  * @param properties typically platform specific properties to further configure the dialog.
92  * @see BasicAlertDialog
93  */
94 @Composable
95 expect fun AlertDialog(
96     onDismissRequest: () -> Unit,
97     confirmButton: @Composable () -> Unit,
98     modifier: Modifier = Modifier,
99     dismissButton: @Composable (() -> Unit)? = null,
100     icon: @Composable (() -> Unit)? = null,
101     title: @Composable (() -> Unit)? = null,
102     text: @Composable (() -> Unit)? = null,
103     shape: Shape = AlertDialogDefaults.shape,
104     containerColor: Color = AlertDialogDefaults.containerColor,
105     iconContentColor: Color = AlertDialogDefaults.iconContentColor,
106     titleContentColor: Color = AlertDialogDefaults.titleContentColor,
107     textContentColor: Color = AlertDialogDefaults.textContentColor,
108     tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
109     properties: DialogProperties = DialogProperties()
110 )
111 
112 /**
113  * [Basic alert dialog dialog](https://m3.material.io/components/dialogs/overview)
114  *
115  * Dialogs provide important prompts in a user flow. They can require an action, communicate
116  * information, or help users accomplish a task.
117  *
118  * ![Basic dialog
119  * image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png)
120  *
121  * This basic alert dialog expects an arbitrary content that is defined by the caller. Note that
122  * your content will need to define its own styling.
123  *
124  * By default, the displayed dialog has the minimum height and width that the Material Design spec
125  * defines. If required, these constraints can be overwritten by providing a `width` or `height`
126  * [Modifier]s.
127  *
128  * Basic alert dialog usage with custom content:
129  *
130  * @sample androidx.compose.material3.samples.BasicAlertDialogSample
131  * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside or
132  *   pressing the back button. This is not called when the dismiss button is clicked.
133  * @param modifier the [Modifier] to be applied to this dialog's content.
134  * @param properties typically platform specific properties to further configure the dialog.
135  * @param content the content of the dialog
136  */
137 @OptIn(ExperimentalMaterial3ComponentOverrideApi::class)
138 @ExperimentalMaterial3Api
139 @Composable
140 fun BasicAlertDialog(
141     onDismissRequest: () -> Unit,
142     modifier: Modifier = Modifier,
143     properties: DialogProperties = DialogProperties(),
144     content: @Composable () -> Unit
145 ) {
146     with(LocalBasicAlertDialogOverride.current) {
147         BasicAlertDialogOverrideScope(
148                 onDismissRequest = onDismissRequest,
149                 modifier = modifier,
150                 properties = properties,
151                 content = content
152             )
153             .BasicAlertDialog()
154     }
155 }
156 
157 /**
158  * This override provides the default behavior of the [BasicAlertDialog] component.
159  *
160  * [BasicAlertDialogOverride] used when no override is specified.
161  */
162 @OptIn(ExperimentalMaterial3Api::class)
163 @ExperimentalMaterial3ComponentOverrideApi
164 object DefaultBasicAlertDialogOverride : BasicAlertDialogOverride {
165     @Composable
BasicAlertDialognull166     override fun BasicAlertDialogOverrideScope.BasicAlertDialog() {
167         Dialog(
168             onDismissRequest = onDismissRequest,
169             properties = properties,
170         ) {
171             val dialogPaneDescription = getString(Strings.Dialog)
172             Box(
173                 modifier =
174                     modifier
175                         .sizeIn(minWidth = DialogMinWidth, maxWidth = DialogMaxWidth)
176                         .then(Modifier.semantics { paneTitle = dialogPaneDescription }),
177                 propagateMinConstraints = true
178             ) {
179                 content()
180             }
181         }
182     }
183 }
184 
185 /**
186  * [Basic alert dialog dialog](https://m3.material.io/components/dialogs/overview)
187  *
188  * Dialogs provide important prompts in a user flow. They can require an action, communicate
189  * information, or help users accomplish a task.
190  *
191  * ![Basic dialog
192  * image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png)
193  *
194  * This basic alert dialog expects an arbitrary content that is defined by the caller. Note that
195  * your content will need to define its own styling.
196  *
197  * By default, the displayed dialog has the minimum height and width that the Material Design spec
198  * defines. If required, these constraints can be overwritten by providing a `width` or `height`
199  * [Modifier]s.
200  *
201  * Basic alert dialog usage with custom content:
202  *
203  * @sample androidx.compose.material3.samples.BasicAlertDialogSample
204  * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside or
205  *   pressing the back button. This is not called when the dismiss button is clicked.
206  * @param modifier the [Modifier] to be applied to this dialog's content.
207  * @param properties typically platform specific properties to further configure the dialog.
208  * @param content the content of the dialog
209  */
210 @Deprecated(
211     "Use BasicAlertDialog instead",
212     replaceWith = ReplaceWith("BasicAlertDialog(onDismissRequest, modifier, properties, content)")
213 )
214 @ExperimentalMaterial3Api
215 @Composable
AlertDialognull216 fun AlertDialog(
217     onDismissRequest: () -> Unit,
218     modifier: Modifier = Modifier,
219     properties: DialogProperties = DialogProperties(),
220     content: @Composable () -> Unit
221 ) = BasicAlertDialog(onDismissRequest, modifier, properties, content)
222 
223 /** Contains default values used for [AlertDialog] and [BasicAlertDialog]. */
224 object AlertDialogDefaults {
225     /** The default shape for alert dialogs */
226     val shape: Shape
227         @Composable get() = DialogTokens.ContainerShape.value
228 
229     /** The default container color for alert dialogs */
230     val containerColor: Color
231         @Composable get() = DialogTokens.ContainerColor.value
232 
233     /** The default icon color for alert dialogs */
234     val iconContentColor: Color
235         @Composable get() = DialogTokens.IconColor.value
236 
237     /** The default title color for alert dialogs */
238     val titleContentColor: Color
239         @Composable get() = DialogTokens.HeadlineColor.value
240 
241     /** The default text color for alert dialogs */
242     val textContentColor: Color
243         @Composable get() = DialogTokens.SupportingTextColor.value
244 
245     /** The default tonal elevation for alert dialogs */
246     val TonalElevation: Dp = 0.dp
247 }
248 
249 @OptIn(ExperimentalMaterial3Api::class)
250 @Composable
251 internal fun AlertDialogImpl(
252     onDismissRequest: () -> Unit,
253     confirmButton: @Composable () -> Unit,
254     modifier: Modifier,
255     dismissButton: @Composable (() -> Unit)?,
256     icon: @Composable (() -> Unit)?,
257     title: @Composable (() -> Unit)?,
258     text: @Composable (() -> Unit)?,
259     shape: Shape,
260     containerColor: Color,
261     iconContentColor: Color,
262     titleContentColor: Color,
263     textContentColor: Color,
264     tonalElevation: Dp,
265     properties: DialogProperties
266 ) {
267     BasicAlertDialog(
268         onDismissRequest = onDismissRequest,
269         modifier = modifier,
270         properties = properties
<lambda>null271     ) {
272         AlertDialogContent(
273             buttons = {
274                 AlertDialogFlowRow(
275                     mainAxisSpacing = ButtonsMainAxisSpacing,
276                     crossAxisSpacing = ButtonsCrossAxisSpacing
277                 ) {
278                     dismissButton?.invoke()
279                     confirmButton()
280                 }
281             },
282             icon = icon,
283             title = title,
284             text = text,
285             shape = shape,
286             containerColor = containerColor,
287             tonalElevation = tonalElevation,
288             // Note that a button content color is provided here from the dialog's token, but in
289             // most cases, TextButtons should be used for dismiss and confirm buttons. TextButtons
290             // will not consume this provided content color value, and will used their own defined
291             // or default colors.
292             buttonContentColor = DialogTokens.ActionLabelTextColor.value,
293             iconContentColor = iconContentColor,
294             titleContentColor = titleContentColor,
295             textContentColor = textContentColor,
296         )
297     }
298 }
299 
300 @Composable
301 internal fun AlertDialogContent(
302     buttons: @Composable () -> Unit,
303     modifier: Modifier = Modifier,
304     icon: (@Composable () -> Unit)?,
305     title: (@Composable () -> Unit)?,
306     text: @Composable (() -> Unit)?,
307     shape: Shape,
308     containerColor: Color,
309     tonalElevation: Dp,
310     buttonContentColor: Color,
311     iconContentColor: Color,
312     titleContentColor: Color,
313     textContentColor: Color,
314 ) {
315     Surface(
316         modifier = modifier,
317         shape = shape,
318         color = containerColor,
319         tonalElevation = tonalElevation,
<lambda>null320     ) {
321         Column(modifier = Modifier.padding(DialogPadding)) {
322             icon?.let {
323                 CompositionLocalProvider(LocalContentColor provides iconContentColor) {
324                     Box(Modifier.padding(IconPadding).align(Alignment.CenterHorizontally)) {
325                         icon()
326                     }
327                 }
328             }
329             title?.let {
330                 ProvideContentColorTextStyle(
331                     contentColor = titleContentColor,
332                     textStyle = DialogTokens.HeadlineFont.value
333                 ) {
334                     Box(
335                         // Align the title to the center when an icon is present.
336                         Modifier.padding(TitlePadding)
337                             .align(
338                                 if (icon == null) {
339                                     Alignment.Start
340                                 } else {
341                                     Alignment.CenterHorizontally
342                                 }
343                             )
344                     ) {
345                         title()
346                     }
347                 }
348             }
349             text?.let {
350                 val textStyle = DialogTokens.SupportingTextFont.value
351                 ProvideContentColorTextStyle(
352                     contentColor = textContentColor,
353                     textStyle = textStyle
354                 ) {
355                     Box(
356                         Modifier.weight(weight = 1f, fill = false)
357                             .padding(TextPadding)
358                             .align(Alignment.Start)
359                     ) {
360                         text()
361                     }
362                 }
363             }
364             Box(modifier = Modifier.align(Alignment.End)) {
365                 val textStyle = DialogTokens.ActionLabelTextFont.value
366                 ProvideContentColorTextStyle(
367                     contentColor = buttonContentColor,
368                     textStyle = textStyle,
369                     content = buttons
370                 )
371             }
372         }
373     }
374 }
375 
376 /**
377  * Simple clone of FlowRow that arranges its children in a horizontal flow with limited
378  * customization.
379  */
380 @Composable
381 internal fun AlertDialogFlowRow(
382     mainAxisSpacing: Dp,
383     crossAxisSpacing: Dp,
384     content: @Composable () -> Unit
385 ) {
measurablesnull386     Layout(content) { measurables, constraints ->
387         val sequences = mutableListOf<List<Placeable>>()
388         val crossAxisSizes = mutableListOf<Int>()
389         val crossAxisPositions = mutableListOf<Int>()
390 
391         var mainAxisSpace = 0
392         var crossAxisSpace = 0
393 
394         val currentSequence = mutableListOf<Placeable>()
395         var currentMainAxisSize = 0
396         var currentCrossAxisSize = 0
397 
398         // Return whether the placeable can be added to the current sequence.
399         fun canAddToCurrentSequence(placeable: Placeable) =
400             currentSequence.isEmpty() ||
401                 currentMainAxisSize + mainAxisSpacing.roundToPx() + placeable.width <=
402                     constraints.maxWidth
403 
404         // Store current sequence information and start a new sequence.
405         fun startNewSequence() {
406             if (sequences.isNotEmpty()) {
407                 crossAxisSpace += crossAxisSpacing.roundToPx()
408             }
409             // Ensures that confirming actions appear above dismissive actions.
410             @Suppress("ListIterator") sequences.add(0, currentSequence.toList())
411             crossAxisSizes += currentCrossAxisSize
412             crossAxisPositions += crossAxisSpace
413 
414             crossAxisSpace += currentCrossAxisSize
415             mainAxisSpace = max(mainAxisSpace, currentMainAxisSize)
416 
417             currentSequence.clear()
418             currentMainAxisSize = 0
419             currentCrossAxisSize = 0
420         }
421 
422         measurables.fastForEach { measurable ->
423             // Ask the child for its preferred size.
424             val placeable = measurable.measure(constraints)
425 
426             // Start a new sequence if there is not enough space.
427             if (!canAddToCurrentSequence(placeable)) startNewSequence()
428 
429             // Add the child to the current sequence.
430             if (currentSequence.isNotEmpty()) {
431                 currentMainAxisSize += mainAxisSpacing.roundToPx()
432             }
433             currentSequence.add(placeable)
434             currentMainAxisSize += placeable.width
435             currentCrossAxisSize = max(currentCrossAxisSize, placeable.height)
436         }
437 
438         if (currentSequence.isNotEmpty()) startNewSequence()
439 
440         val mainAxisLayoutSize = max(mainAxisSpace, constraints.minWidth)
441 
442         val crossAxisLayoutSize = max(crossAxisSpace, constraints.minHeight)
443 
444         val layoutWidth = mainAxisLayoutSize
445 
446         val layoutHeight = crossAxisLayoutSize
447 
448         layout(layoutWidth, layoutHeight) {
449             sequences.fastForEachIndexed { i, placeables ->
450                 val childrenMainAxisSizes =
451                     IntArray(placeables.size) { j ->
452                         placeables[j].width +
453                             if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0
454                     }
455                 val arrangement = Arrangement.End
456                 val mainAxisPositions = IntArray(childrenMainAxisSizes.size)
457                 with(arrangement) {
458                     arrange(
459                         mainAxisLayoutSize,
460                         childrenMainAxisSizes,
461                         layoutDirection,
462                         mainAxisPositions
463                     )
464                 }
465                 placeables.fastForEachIndexed { j, placeable ->
466                     placeable.place(x = mainAxisPositions[j], y = crossAxisPositions[i])
467                 }
468             }
469         }
470     }
471 }
472 
473 internal val DialogMinWidth = 280.dp
474 internal val DialogMaxWidth = 560.dp
475 
476 private val ButtonsMainAxisSpacing = 8.dp
477 private val ButtonsCrossAxisSpacing = 12.dp
478 
479 // Paddings for each of the dialog's parts.
480 private val DialogPadding = PaddingValues(all = 24.dp)
481 private val IconPadding = PaddingValues(bottom = 16.dp)
482 private val TitlePadding = PaddingValues(bottom = 16.dp)
483 private val TextPadding = PaddingValues(bottom = 24.dp)
484 
485 /**
486  * Interface that allows libraries to override the behavior of the [BasicAlertDialog] component.
487  *
488  * To override this component, implement the member function of this interface, then provide the
489  * implementation to [LocalBasicAlertDialogOverride] in the Compose hierarchy.
490  */
491 @ExperimentalMaterial3ComponentOverrideApi
492 interface BasicAlertDialogOverride {
493     /** Behavior function that is called by the [BasicAlertDialog] component. */
BasicAlertDialognull494     @Composable fun BasicAlertDialogOverrideScope.BasicAlertDialog()
495 }
496 
497 /**
498  * Parameters available to [BasicAlertDialog].
499  *
500  * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside or
501  *   pressing the back button. This is not called when the dismiss button is clicked.
502  * @param modifier the [Modifier] to be applied to this dialog's content.
503  * @param properties typically platform specific properties to further configure the dialog.
504  * @param content the content of the dialog
505  */
506 @ExperimentalMaterial3ComponentOverrideApi
507 class BasicAlertDialogOverrideScope
508 internal constructor(
509     val onDismissRequest: () -> Unit,
510     val modifier: Modifier = Modifier,
511     val properties: DialogProperties = DialogProperties(),
512     val content: @Composable () -> Unit
513 )
514 
515 /** CompositionLocal containing the currently-selected [BasicAlertDialogOverride]. */
516 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
517 @get:ExperimentalMaterial3ComponentOverrideApi
518 @ExperimentalMaterial3ComponentOverrideApi
519 val LocalBasicAlertDialogOverride: ProvidableCompositionLocal<BasicAlertDialogOverride> =
520     compositionLocalOf {
521         DefaultBasicAlertDialogOverride
522     }
523