1 /*
<lambda>null2  * Copyright (C) 2023 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 com.android.systemui.dialog.ui.composable
18 
19 import androidx.compose.foundation.layout.Box
20 import androidx.compose.foundation.layout.Column
21 import androidx.compose.foundation.layout.PaddingValues
22 import androidx.compose.foundation.layout.Spacer
23 import androidx.compose.foundation.layout.defaultMinSize
24 import androidx.compose.foundation.layout.fillMaxWidth
25 import androidx.compose.foundation.layout.height
26 import androidx.compose.foundation.layout.padding
27 import androidx.compose.foundation.rememberScrollState
28 import androidx.compose.foundation.verticalScroll
29 import androidx.compose.material3.LocalContentColor
30 import androidx.compose.material3.MaterialTheme
31 import androidx.compose.material3.ProvideTextStyle
32 import androidx.compose.runtime.Composable
33 import androidx.compose.runtime.CompositionLocalProvider
34 import androidx.compose.ui.Alignment
35 import androidx.compose.ui.Modifier
36 import androidx.compose.ui.layout.Layout
37 import androidx.compose.ui.layout.Placeable
38 import androidx.compose.ui.layout.layoutId
39 import androidx.compose.ui.text.style.TextAlign
40 import androidx.compose.ui.unit.dp
41 import kotlin.math.roundToInt
42 
43 /**
44  * The content of an AlertDialog which can be used together with
45  * [SystemUIDialogFactory.create][com.android.systemui.statusbar.phone.create] to create an alert
46  * dialog in Compose.
47  *
48  * @see com.android.systemui.statusbar.phone.create
49  */
50 @Composable
51 fun AlertDialogContent(
52     title: @Composable () -> Unit,
53     content: @Composable () -> Unit,
54     modifier: Modifier = Modifier,
55     icon: (@Composable () -> Unit)? = null,
56     positiveButton: (@Composable () -> Unit)? = null,
57     negativeButton: (@Composable () -> Unit)? = null,
58     neutralButton: (@Composable () -> Unit)? = null,
59 ) {
60     Column(
61         modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(DialogPaddings),
62         horizontalAlignment = Alignment.CenterHorizontally,
63     ) {
64         // Icon.
65         if (icon != null) {
66             val defaultSize = 32.dp
67             Box(
68                 Modifier.defaultMinSize(minWidth = defaultSize, minHeight = defaultSize),
69                 propagateMinConstraints = true,
70             ) {
71                 val iconColor = MaterialTheme.colorScheme.primary
72                 CompositionLocalProvider(LocalContentColor provides iconColor) { icon() }
73             }
74 
75             Spacer(Modifier.height(16.dp))
76         }
77 
78         // Title.
79         val titleColor = MaterialTheme.colorScheme.onSurface
80         CompositionLocalProvider(LocalContentColor provides titleColor) {
81             ProvideTextStyle(
82                 MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
83             ) {
84                 title()
85             }
86         }
87         Spacer(Modifier.height(16.dp))
88 
89         // Content.
90         val contentColor = MaterialTheme.colorScheme.onSurfaceVariant
91         Box {
92             CompositionLocalProvider(LocalContentColor provides contentColor) {
93                 ProvideTextStyle(
94                     MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center)
95                 ) {
96                     content()
97                 }
98             }
99         }
100         Spacer(Modifier.height(32.dp))
101 
102         // Buttons.
103         if (positiveButton != null || negativeButton != null || neutralButton != null) {
104             AlertDialogButtons(
105                 positiveButton = positiveButton,
106                 negativeButton = negativeButton,
107                 neutralButton = neutralButton,
108             )
109         }
110     }
111 }
112 
113 @Composable
AlertDialogButtonsnull114 private fun AlertDialogButtons(
115     positiveButton: (@Composable () -> Unit)?,
116     negativeButton: (@Composable () -> Unit)?,
117     neutralButton: (@Composable () -> Unit)?,
118     modifier: Modifier = Modifier,
119 ) {
120     Layout(
121         content = {
122             positiveButton?.let { Box(Modifier.layoutId("positive")) { it() } }
123             negativeButton?.let { Box(Modifier.layoutId("negative")) { it() } }
124             neutralButton?.let { Box(Modifier.layoutId("neutral")) { it() } }
125         },
126         modifier,
127     ) { measurables, constraints ->
128         check(constraints.hasBoundedWidth) {
129             "AlertDialogButtons should not be composed in an horizontally scrollable layout"
130         }
131         val maxWidth = constraints.maxWidth
132 
133         // Measure the buttons.
134         var positive: Placeable? = null
135         var negative: Placeable? = null
136         var neutral: Placeable? = null
137         for (i in measurables.indices) {
138             val measurable = measurables[i]
139             when (val layoutId = measurable.layoutId) {
140                 "positive" -> positive = measurable.measure(constraints)
141                 "negative" -> negative = measurable.measure(constraints)
142                 "neutral" -> neutral = measurable.measure(constraints)
143                 else -> error("Unexpected layoutId=$layoutId")
144             }
145         }
146 
147         fun Placeable?.width() = this?.width ?: 0
148         fun Placeable?.height() = this?.height ?: 0
149 
150         // The min horizontal spacing between buttons.
151         val horizontalSpacing = 8.dp.toPx()
152         val totalHorizontalSpacing = (measurables.size - 1) * horizontalSpacing
153         val requiredWidth =
154             positive.width() + negative.width() + neutral.width() + totalHorizontalSpacing
155 
156         if (requiredWidth <= maxWidth) {
157             // Stack horizontally: [neutral][flexSpace][negative][positive].
158             val height = maxOf(positive.height(), negative.height(), neutral.height())
159             layout(maxWidth, height) {
160                 positive?.let { it.placeRelative(maxWidth - it.width, 0) }
161 
162                 negative?.let { negative ->
163                     if (positive == null) {
164                         negative.placeRelative(maxWidth - negative.width, 0)
165                     } else {
166                         negative.placeRelative(
167                             maxWidth -
168                                 negative.width -
169                                 positive.width -
170                                 horizontalSpacing.roundToInt(),
171                             0,
172                         )
173                     }
174                 }
175 
176                 neutral?.placeRelative(0, 0)
177             }
178         } else {
179             // Stack vertically, aligned on the right (in LTR layouts):
180             //   [positive]
181             //   [negative]
182             //    [neutral]
183             //
184             // TODO(b/283817398): Introduce a ResponsiveDialogButtons composable to create buttons
185             // that have different styles when stacked horizontally, as shown in
186             // go/sysui-dialog-styling.
187             val height = positive.height() + negative.height() + neutral.height()
188             layout(maxWidth, height) {
189                 var y = 0
190                 fun Placeable.place() {
191                     placeRelative(maxWidth - width, y)
192                     y += this.height
193                 }
194 
195                 positive?.place()
196                 negative?.place()
197                 neutral?.place()
198             }
199         }
200     }
201 }
202 
203 private val DialogPaddings = PaddingValues(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 18.dp)
204