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 * 
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 * 
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 * 
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